Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix-front into 6739-invoiceOut

This commit is contained in:
Jon Elias 2024-06-25 14:37:50 +02:00
commit 975c844d65
169 changed files with 8836 additions and 2497 deletions

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.
@ -11,6 +108,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- (Item) => Se añade la opción de añadir un comentario del motivo de hacer una foto - (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 - (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]

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.26.2", "version": "24.28.1",
"description": "Salix frontend", "description": "Salix frontend",
"productName": "Salix", "productName": "Salix",
"author": "Verdnatura", "author": "Verdnatura",

View File

@ -1,11 +1,10 @@
import axios from 'axios'; import axios from 'axios';
import { Notify } from 'quasar';
import { useSession } from 'src/composables/useSession'; import { useSession } from 'src/composables/useSession';
import { Router } from 'src/router'; import { Router } from 'src/router';
import { i18n } from './i18n'; import useNotify from 'src/composables/useNotify.js';
const session = useSession(); const session = useSession();
const { t } = i18n.global; const { notify } = useNotify();
axios.defaults.baseURL = '/api/'; axios.defaults.baseURL = '/api/';
@ -27,10 +26,7 @@ const onResponse = (response) => {
const isSaveRequest = method === 'patch'; const isSaveRequest = method === 'patch';
if (isSaveRequest) { if (isSaveRequest) {
Notify.create({ notify('globals.dataSaved', 'positive');
message: t('globals.dataSaved'),
type: 'positive',
});
} }
return response; return response;
@ -67,10 +63,7 @@ const onResponseError = (error) => {
return Promise.reject(error); return Promise.reject(error);
} }
Notify.create({ notify(message, 'negative');
message: t(message),
type: 'negative',
});
return Promise.reject(error); return Promise.reject(error);
}; };

View File

@ -27,7 +27,6 @@ export default {
this.$el.addEventListener('keyup', function (evt) { this.$el.addEventListener('keyup', function (evt) {
if (evt.key === 'Enter') { if (evt.key === 'Enter') {
const input = evt.target; const input = evt.target;
console.log('input', input);
if (input.type == 'textarea' && evt.shiftKey) { if (input.type == 'textarea' && evt.shiftKey) {
evt.preventDefault(); evt.preventDefault();
let { selectionStart, selectionEnd } = input; let { selectionStart, selectionEnd } = input;

View File

@ -67,6 +67,10 @@ const $props = defineProps({
default: '', default: '',
description: 'It is used for redirect on click "save and continue"', description: 'It is used for redirect on click "save and continue"',
}, },
hasSubtoolbar: {
type: Boolean,
default: true,
},
}); });
const isLoading = ref(false); const isLoading = ref(false);
@ -75,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']);
@ -89,6 +94,7 @@ defineExpose({
saveChanges, saveChanges,
getChanges, getChanges,
formData, formData,
vnPaginateRef,
}); });
async function fetch(data) { async function fetch(data) {
@ -97,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(
() => { () => {
@ -135,7 +148,7 @@ async function onSubmit() {
await saveChanges($props.saveFn ? formData.value : null); await saveChanges($props.saveFn ? formData.value : null);
} }
async function onSumbitAndGo() { async function onSubmitAndGo() {
await onSubmit(); await onSubmit();
push({ path: $props.goTo }); push({ path: $props.goTo });
} }
@ -271,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 () => {
@ -284,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
@ -298,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
@ -324,7 +339,7 @@ watch(formUrl, async () => {
/> />
<QBtnDropdown <QBtnDropdown
v-if="$props.goTo && $props.defaultSave" v-if="$props.goTo && $props.defaultSave"
@click="onSumbitAndGo" @click="onSubmitAndGo"
:label="tMobile('globals.saveAndContinue')" :label="tMobile('globals.saveAndContinue')"
:title="t('globals.saveAndContinue')" :title="t('globals.saveAndContinue')"
:disable="!hasChanges" :disable="!hasChanges"

View File

@ -12,6 +12,7 @@ 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 { useArrayData } from 'src/composables/useArrayData';
import { useRoute } from 'vue-router';
const { push } = useRouter(); const { push } = useRouter();
const quasar = useQuasar(); const quasar = useQuasar();
@ -20,6 +21,7 @@ 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: {
@ -28,7 +30,7 @@ const $props = defineProps({
}, },
model: { model: {
type: String, type: String,
default: '', default: null,
}, },
filter: { filter: {
type: Object, type: Object,
@ -81,18 +83,23 @@ const $props = defineProps({
default: '', default: '',
description: 'It is used for redirect on click "save and continue"', description: 'It is used for redirect on click "save and continue"',
}, },
reload: {
type: Boolean,
default: false,
},
}); });
const emit = defineEmits(['onFetch', 'onDataSaved']); const emit = defineEmits(['onFetch', 'onDataSaved']);
const modelValue = computed(
() => $props.model ?? `formModel_${route?.meta?.title ?? route.name}`
).value;
const componentIsRendered = ref(false); const componentIsRendered = ref(false);
const arrayData = useArrayData($props.model); const arrayData = useArrayData(modelValue);
const isLoading = ref(false); const isLoading = ref(false);
// Si elegimos observar los cambios del form significa que inicialmente las actions estaran deshabilitadas // Si elegimos observar los cambios del form significa que inicialmente las actions estaran deshabilitadas
const isResetting = ref(false); const isResetting = ref(false);
const hasChanges = ref(!$props.observeFormChanges); const hasChanges = ref(!$props.observeFormChanges);
const originalData = ref({}); const originalData = ref({});
const formData = computed(() => state.get($props.model)); const formData = computed(() => state.get(modelValue));
const formUrl = computed(() => $props.url); const formUrl = computed(() => $props.url);
const defaultButtons = computed(() => ({ const defaultButtons = computed(() => ({
save: { save: {
@ -114,11 +121,12 @@ onMounted(async () => {
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 && $props.url) await fetch(); if (!$props.formInitialData) {
if ($props.autoLoad && $props.url) await fetch();
else if (arrayData.store.data) updateAndEmit('onFetch', arrayData.store.data); else if (arrayData.store.data) updateAndEmit('onFetch', arrayData.store.data);
}
if ($props.observeFormChanges) { if ($props.observeFormChanges) {
watch( watch(
() => formData.value, () => formData.value,
@ -161,8 +169,8 @@ 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) return state.set($props.model, originalData.value); if (hasChanges.value) return state.set(modelValue, originalData.value);
if ($props.clearStoreOnUnmount) state.unset($props.model); if ($props.clearStoreOnUnmount) state.unset(modelValue);
}); });
async function fetch() { async function fetch() {
@ -174,7 +182,7 @@ async function fetch() {
updateAndEmit('onFetch', data); updateAndEmit('onFetch', data);
} catch (e) { } catch (e) {
state.set($props.model, {}); state.set(modelValue, {});
originalData.value = {}; originalData.value = {};
} }
} }
@ -197,6 +205,7 @@ async function save() {
if ($props.urlCreate) notify('globals.dataCreated', 'positive'); if ($props.urlCreate) notify('globals.dataCreated', 'positive');
updateAndEmit('onDataSaved', formData.value, response?.data); updateAndEmit('onDataSaved', formData.value, response?.data);
if ($props.reload) await arrayData.fetch({});
} catch (err) { } catch (err) {
console.error(err); console.error(err);
notify('errors.writeRequest', 'negative'); notify('errors.writeRequest', 'negative');
@ -235,14 +244,20 @@ function filter(value, update, filterOptions) {
} }
function updateAndEmit(evt, val, res) { function updateAndEmit(evt, val, res) {
state.set($props.model, val); state.set(modelValue, val);
originalData.value = val && JSON.parse(JSON.stringify(val)); originalData.value = val && JSON.parse(JSON.stringify(val));
if (!$props.url) arrayData.store.data = val; if (!$props.url) arrayData.store.data = val;
emit(evt, state.get($props.model), res); emit(evt, state.get(modelValue), res);
} }
defineExpose({ save, isLoading, hasChanges }); defineExpose({
save,
isLoading,
hasChanges,
reset,
fetch,
});
</script> </script>
<template> <template>
<div class="column items-center full-width"> <div class="column items-center full-width">

View File

@ -11,6 +11,7 @@ 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'; import { useClipboard } from 'src/composables/useClipboard';
import VnImg from 'src/components/ui/VnImg.vue';
const state = useState(); const state = useState();
const session = useSession(); const session = useSession();
@ -47,7 +48,6 @@ const darkMode = computed({
}); });
const user = state.getUser(); const user = state.getUser();
const token = session.getTokenMultimedia();
const warehousesData = ref(); const warehousesData = ref();
const companiesData = ref(); const companiesData = ref();
const accountBankData = ref(); const accountBankData = ref();
@ -149,10 +149,7 @@ function saveUserData(param, value) {
<div class="col column items-center q-mb-sm"> <div class="col column items-center q-mb-sm">
<QAvatar size="80px"> <QAvatar size="80px">
<QImg <VnImg :id="user.id" collection="user" size="160x160" />
:src="`/api/Images/user/160x160/${user.id}/download?access_token=${token}`"
spinner-color="white"
/>
</QAvatar> </QAvatar>
<div class="text-subtitle1 q-mt-md"> <div class="text-subtitle1 q-mt-md">

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import { onBeforeMount, computed, watchEffect } from 'vue'; import { onBeforeMount, 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';
@ -41,20 +41,6 @@ onBeforeMount(async () => {
if (!props.baseUrl) arrayData.store.filter.where = { id: route.params.id }; if (!props.baseUrl) arrayData.store.filter.where = { id: route.params.id };
await arrayData.fetch({ append: false }); await arrayData.fetch({ append: false });
}); });
if (props.baseUrl) {
onBeforeRouteUpdate(async (to, from) => {
if (to.params.id !== from.params.id) {
arrayData.store.url = `${props.baseUrl}/${to.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>
<QDrawer <QDrawer

View File

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

View File

@ -2,7 +2,12 @@
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
const emit = defineEmits(['update:modelValue', 'update:options', 'keyup.enter']); const emit = defineEmits([
'update:modelValue',
'update:options',
'keyup.enter',
'remove',
]);
const $props = defineProps({ const $props = defineProps({
modelValue: { modelValue: {
@ -13,6 +18,14 @@ const $props = defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
info: {
type: String,
default: '',
},
clearable: {
type: Boolean,
default: true,
},
}); });
const { t } = useI18n(); const { t } = useI18n();
@ -74,15 +87,19 @@ const inputRules = [
<template v-if="$slots.prepend" #prepend> <template v-if="$slots.prepend" #prepend>
<slot name="prepend" /> <slot name="prepend" />
</template> </template>
<template #append> <template #append>
<slot name="append" v-if="$slots.append && !$attrs.disabled" /> <slot name="append" v-if="$slots.append && !$attrs.disabled" />
<QIcon <QIcon
name="close" name="close"
size="xs" size="xs"
v-if="hover && value && !$attrs.disabled" v-if="hover && value && !$attrs.disabled && $props.clearable"
@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

@ -97,7 +97,7 @@ const styleAttrs = computed(() => {
<QIcon <QIcon
name="close" name="close"
size="xs" size="xs"
v-if="hover && value" v-if="hover && value && !readonly"
@click="onDateUpdate(null)" @click="onDateUpdate(null)"
></QIcon> ></QIcon>
<QIcon name="event" class="cursor-pointer"> <QIcon name="event" class="cursor-pointer">

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref } from 'vue'; import { ref, onUnmounted } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import axios from 'axios'; import axios from 'axios';
@ -376,6 +376,10 @@ async function clearFilter() {
} }
setLogTree(); setLogTree();
onUnmounted(() => {
stateStore.rightDrawer = false;
});
</script> </script>
<template> <template>
<FetchData <FetchData

View File

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

View File

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

View File

@ -1,6 +1,5 @@
<script setup> <script setup>
import { ref, toRefs, computed, watch } from 'vue'; import { ref, toRefs, computed, watch, onMounted } from 'vue';
import { onMounted } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import FetchData from 'src/components/FetchData.vue'; import FetchData from 'src/components/FetchData.vue';
const emit = defineEmits(['update:modelValue', 'update:options']); const emit = defineEmits(['update:modelValue', 'update:options']);
@ -16,11 +15,11 @@ const $props = defineProps({
}, },
optionLabel: { optionLabel: {
type: [String], type: [String],
default: '', default: 'name',
}, },
optionValue: { optionValue: {
type: String, type: String,
default: '', default: 'id',
}, },
optionFilter: { optionFilter: {
type: String, type: String,
@ -58,6 +57,10 @@ const $props = defineProps({
type: [Number, String], type: [Number, String],
default: '30', default: '30',
}, },
focusOnMount: {
type: Boolean,
default: false,
},
}); });
const { t } = useI18n(); const { t } = useI18n();
@ -116,7 +119,9 @@ async function fetchFilter(val) {
if (new RegExp(/\d/g).test(val)) key = optionFilter.value ?? optionValue.value; if (new RegExp(/\d/g).test(val)) key = optionFilter.value ?? optionValue.value;
const where = { ...{ [key]: { like: `%${val}%` } }, ...$props.where }; const where = { ...{ [key]: { like: `%${val}%` } }, ...$props.where };
return dataRef.value.fetch({ fields, where, order: sortBy, limit }); const fetchOptions = { where, order: sortBy, limit };
if (fields) fetchOptions.fields = fields;
return dataRef.value.fetch(fetchOptions);
} }
async function filterHandler(val, update) { async function filterHandler(val, update) {
@ -146,6 +151,10 @@ watch(modelValue, (newValue) => {
if (!myOptions.value.some((option) => option[optionValue.value] == newValue)) if (!myOptions.value.some((option) => option[optionValue.value] == newValue))
fetchFilter(newValue); fetchFilter(newValue);
}); });
onMounted(async () => {
if ($props.focusOnMount) setTimeout(() => vnSelectRef.value.showPopup(), 300);
});
</script> </script>
<template> <template>
@ -178,6 +187,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

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

View File

@ -39,6 +39,7 @@ const $props = defineProps({
}); });
const state = useState(); const state = useState();
const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const { viewSummary } = useSummaryDialog(); const { viewSummary } = useSummaryDialog();
let arrayData; let arrayData;
@ -57,7 +58,7 @@ onBeforeMount(async () => {
store = arrayData.store; store = arrayData.store;
entity = computed(() => (Array.isArray(store.data) ? store.data[0] : store.data)); entity = computed(() => (Array.isArray(store.data) ? store.data[0] : store.data));
// It enables to load data only once if the module is the same as the dataKey // It enables to load data only once if the module is the same as the dataKey
if ($props.dataKey !== useRoute().meta.moduleName) await getData(); if ($props.dataKey !== route.meta.moduleName || !route.params.id) await getData();
watch( watch(
() => [$props.url, $props.filter], () => [$props.url, $props.filter],
async () => await getData() async () => await getData()
@ -115,13 +116,13 @@ const emit = defineEmits(['onFetch']);
</QBtn> </QBtn>
</RouterLink> </RouterLink>
<QBtn <QBtn
v-if="$slots.menu"
color="white" color="white"
dense dense
flat flat
icon="more_vert" icon="more_vert"
round round
size="md" size="md"
:class="{ invisible: !$slots.menu }"
> >
<QTooltip> <QTooltip>
{{ t('components.cardDescriptor.moreOptions') }} {{ t('components.cardDescriptor.moreOptions') }}

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

@ -22,11 +22,15 @@ const props = defineProps({
type: String, type: String,
default: '', default: '',
}, },
moduleName: {
type: String,
default: null,
},
}); });
const emit = defineEmits(['onFetch']); const emit = defineEmits(['onFetch']);
const route = useRoute(); const route = useRoute();
const isSummary = ref(); const isSummary = ref();
const arrayData = useArrayData(props.dataKey || route.meta.moduleName, { const arrayData = useArrayData(props.dataKey, {
url: props.url, url: props.url,
filter: props.filter, filter: props.filter,
skip: 0, skip: 0,
@ -83,7 +87,7 @@ function existSummary(routes) {
v-if="showRedirectToSummaryIcon" v-if="showRedirectToSummaryIcon"
class="header link" class="header link"
:to="{ :to="{
name: `${route.meta.moduleName}Summary`, name: `${moduleName ?? route.meta.moduleName}Summary`,
params: { id: entityId || entity.id }, params: { id: entityId || entity.id },
}" }"
> >
@ -159,9 +163,9 @@ function existSummary(routes) {
margin-top: 2px; margin-top: 2px;
.label { .label {
color: var(--vn-label-color); color: var(--vn-label-color);
width: 8em; width: 9em;
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: wrap;
text-overflow: ellipsis; text-overflow: ellipsis;
margin-right: 10px; margin-right: 10px;
flex-grow: 0; flex-grow: 0;

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

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

View File

@ -4,11 +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 props = defineProps({ const params = defineModel({ default: {}, required: true, type: Object });
const $props = defineProps({
dataKey: { dataKey: {
type: String, type: String,
required: true, required: true,
@ -18,11 +18,6 @@ const props = defineProps({
required: false, required: false,
default: false, default: false,
}, },
params: {
type: Object,
required: false,
default: null,
},
showAll: { showAll: {
type: Boolean, type: Boolean,
default: true, default: true,
@ -40,12 +35,20 @@ const props = defineProps({
}, },
hiddenTags: { hiddenTags: {
type: Array, type: Array,
default: () => [], default: () => ['filter'],
}, },
customTags: { customTags: {
type: Array, type: Array,
default: () => [], default: () => [],
}, },
disableSubmitEvent: {
type: Boolean,
default: false,
},
searchUrl: {
type: String,
default: 'params',
},
redirect: { redirect: {
type: Boolean, type: Boolean,
default: true, default: true,
@ -54,61 +57,64 @@ const props = defineProps({
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 (props.params) userParams.value = JSON.parse(JSON.stringify(props.params)); emit('init', { params: params.value });
if (Object.keys(store.userParams).length > 0) {
userParams.value = JSON.parse(JSON.stringify(store.userParams));
}
emit('init', { params: userParams.value });
}); });
function setUserParams(watchedParams) {
if (!watchedParams) return;
if (typeof watchedParams == 'string') watchedParams = JSON.parse(watchedParams);
watchedParams = { ...watchedParams, ...watchedParams.filter?.where };
delete watchedParams.filter;
params.value = { ...params.value, ...watchedParams };
}
watch( watch(
() => route.query.params, () => route.query[$props.searchUrl],
(val) => { (val) => setUserParams(val)
if (!val) { );
userParams.value = {};
} else { watch(
const parsedParams = JSON.parse(val); () => arrayData.store.userParams,
userParams.value = { ...parsedParams }; (val) => setUserParams(val)
}
}
); );
const isLoading = ref(false); const isLoading = ref(false);
async function search() { async function search(evt) {
if (evt && $props.disableSubmitEvent) return;
store.filter.where = {}; store.filter.where = {};
isLoading.value = true; isLoading.value = true;
const params = { ...userParams.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 }); const { params: newParams } = await arrayData.addFilter({ params: params.value });
userParams.value = newParams; params.value = newParams;
if (!props.showAll && !Object.values(params).length) store.data = []; if (!$props.showAll && !Object.values(filter).length) store.data = [];
isLoading.value = false; isLoading.value = false;
emit('search'); emit('search');
if (props.redirect) 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');
if (props.redirect) navigate(store.data, {});
} }
async function clearFilters() { async function clearFilters() {
@ -117,18 +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];
} }
userParams.value = { ...newParams }; // Actualizar los params con los removibles params.value = {};
await arrayData.applyFilter({ params: userParams.value }); params.value = { ...newParams }; // Actualizar los params con los removibles
await arrayData.applyFilter({ params: params.value });
if (!props.showAll) { if (!$props.showAll) {
store.data = []; store.data = [];
} }
@ -136,36 +143,32 @@ async function clearFilters() {
emit('clear'); emit('clear');
} }
const tagsList = computed(() => const tagsList = computed(() => {
Object.entries(userParams.value) const tagList = [];
.filter(([key, value]) => value && !(props.hiddenTags || []).includes(key)) for (const key of Object.keys(params.value)) {
.map(([key, value]) => ({ const value = params.value[key];
label: key, if (value == null || ($props.hiddenTags || []).includes(key)) continue;
value: value, tagList.push({ label: key, value });
})) }
); return tagList;
});
const tags = computed(() => const tags = computed(() => {
tagsList.value.filter((tag) => !(props.customTags || []).includes(tag.label)) return tagsList.value.filter((tag) => !($props.customTags || []).includes(tag.key));
); });
const customTags = computed(() => const customTags = computed(() =>
tagsList.value.filter((tag) => (props.customTags || []).includes(tag.label)) tagsList.value.filter((tag) => ($props.customTags || []).includes(tag.key))
); );
async function remove(key) { async function remove(key) {
userParams.value[key] = null; 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}"`;
} }
@ -226,14 +229,14 @@ function formatValue(value) {
<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.label }}:</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"
@ -243,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
@ -255,7 +258,7 @@ function formatValue(value) {
dense dense
icon="search" icon="search"
rounded rounded
type="submit" :type="disableSubmitEvent ? 'button' : 'submit'"
unelevated unelevated
/> />
</QItemSection> </QItemSection>
@ -269,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

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

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);
@ -150,7 +169,7 @@ async function onLoad(index, done) {
done(isDone); done(isDone);
} }
defineExpose({ fetch, addFilter }); defineExpose({ fetch, addFilter, paginate });
</script> </script>
<template> <template>
@ -199,12 +218,6 @@ defineExpose({ fetch, addFilter });
<QSpinner color="orange" size="md" /> <QSpinner color="orange" size="md" />
</div> </div>
</QInfiniteScroll> </QInfiniteScroll>
<div
v-if="!isLoading && store.hasMoreData"
class="w-full flex justify-center q-mt-md"
>
<QBtn color="primary" :label="t('Load more data')" @click="paginate()" />
</div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -3,7 +3,6 @@ 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'; import { useStateStore } from 'src/stores/useStateStore';
@ -18,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: {
@ -73,10 +69,20 @@ const props = defineProps({
}, },
}); });
let arrayData = useArrayData(props.dataKey, { ...props });
let store = arrayData.store;
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( watch(
() => props.dataKey, () => props.dataKey,
@ -106,13 +112,6 @@ async function search() {
search: searchText.value, search: searchText.value,
}, },
}); });
if (!props.redirect) return;
navigate(store.data, {
customRouteRedirectName: props.customRouteRedirectName,
searchText: searchText.value,
});
} }
</script> </script>
<template> <template>

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

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

View File

@ -1,5 +1,5 @@
import { onMounted, ref, computed } from 'vue'; import { onMounted, ref, computed } from 'vue';
import { useRoute } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import axios from 'axios'; import axios from 'axios';
import { useArrayDataStore } from 'stores/useArrayDataStore'; import { useArrayDataStore } from 'stores/useArrayDataStore';
import { buildFilter } from 'filters/filterPanel'; import { buildFilter } from 'filters/filterPanel';
@ -13,6 +13,7 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
const store = arrayDataStore.get(key); const store = arrayDataStore.get(key);
const route = useRoute(); const route = useRoute();
const router = useRouter();
let canceller = null; let canceller = null;
const page = ref(1); const page = ref(1);
@ -22,8 +23,13 @@ export function useArrayData(key = useRoute().meta.moduleName, 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 };
} }
}); });
@ -40,13 +46,15 @@ export function useArrayData(key = useRoute().meta.moduleName, 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])
@ -87,8 +95,8 @@ export function useArrayData(key = useRoute().meta.moduleName, 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,
@ -118,6 +126,10 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
} }
} }
function deleteOption(option) {
delete store[option];
}
function cancelRequest() { function cancelRequest() {
if (canceller) { if (canceller) {
canceller.abort(); canceller.abort();
@ -128,7 +140,7 @@ export function useArrayData(key = useRoute().meta.moduleName, 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;
@ -137,7 +149,7 @@ export function useArrayData(key = useRoute().meta.moduleName, 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;
@ -149,15 +161,20 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
return { filter, params }; return { filter, params };
} }
async function addFilterWhere(where) {
const storedFilter = { ...store.userFilter };
if (!storedFilter?.where) storedFilter.where = {};
where = { ...storedFilter.where, ...where };
await addFilter({ filter: { where } });
}
function sanitizerParams(params, exprBuilder) { function sanitizerParams(params, exprBuilder) {
for (const param in params) { for (const param in params) {
if (params[param] === '' || params[param] === null) { if (params[param] === '' || params[param] === null) {
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;
@ -182,22 +199,34 @@ export function useArrayData(key = useRoute().meta.moduleName, 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 = JSON.stringify(store.userParams);
const url = new URL(window.location.href); if (store.navigate) {
const { hash: currentHash } = url; const { customRouteRedirectName, searchText } = store.navigate;
const [currentRoute] = currentHash.split('?'); if (customRouteRedirectName)
return router.push({
name: customRouteRedirectName,
params: { id: searchText },
});
const { matched: matches } = router.currentRoute.value;
const { path } = matches.at(-1);
const params = new URLSearchParams(); const to =
for (const param in query) params.append(param, query[param]); store?.data?.length === 1
? path.replace(/\/(list|:id)|-list/, `/${store.data[0].id}`)
: path.replace(/:id.*/, '');
url.hash = currentRoute + '?' + params.toString(); if (route.path != to) {
window.history.pushState({}, '', url.hash); 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);
@ -207,6 +236,7 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
fetch, fetch,
applyFilter, applyFilter,
addFilter, addFilter,
addFilterWhere,
refresh, refresh,
destroy, destroy,
loadMore, loadMore,
@ -214,5 +244,6 @@ export function useArrayData(key = useRoute().meta.moduleName, 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

@ -15,6 +15,7 @@ if (sessionStorage.getItem('user'))
user.value = JSON.parse(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);
@ -42,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;
@ -69,6 +78,8 @@ export function useState() {
setUser, setUser,
getRoles, getRoles,
setRoles, setRoles,
getAcls,
setAcls,
getTokenConfig, getTokenConfig,
setTokenConfig, setTokenConfig,
set, set,

View File

@ -115,6 +115,13 @@ select:-webkit-autofill {
background-color: var(--vn-accent-color); background-color: var(--vn-accent-color);
} }
.text-primary-light {
color: $primary-light !important;
}
.bg-primary-light {
background: $primary-light !important;
}
.fill-icon { .fill-icon {
font-variation-settings: 'FILL' 1; font-variation-settings: 'FILL' 1;
} }
@ -189,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;
}

View File

@ -28,7 +28,7 @@ $color-link: #66bfff;
$color-spacer-light: #a3a3a31f; $color-spacer-light: #a3a3a31f;
$color-spacer: #7979794d; $color-spacer: #7979794d;
$border-thin-light: 1px solid $color-spacer-light; $border-thin-light: 1px solid $color-spacer-light;
$primary-light: lighten($primary, 35%); $primary-light: #f5b351;
$dark-shadow-color: black; $dark-shadow-color: black;
$layout-shadow-dark: 0 0 10px 2px #00000033, 0 0px 10px #0000003d; $layout-shadow-dark: 0 0 10px 2px #00000033, 0 0px 10px #0000003d;
$spacing-md: 16px; $spacing-md: 16px;

View File

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

View File

@ -113,6 +113,7 @@ globals:
name: Name name: Name
new: New new: New
comment: Comment comment: Comment
observations: Observations
errors: errors:
statusUnauthorized: Access denied statusUnauthorized: Access denied
statusInternalServerError: An internal server error has ocurred statusInternalServerError: An internal server error has ocurred
@ -279,8 +280,8 @@ customer:
extendedList: extendedList:
tableVisibleColumns: tableVisibleColumns:
id: Identifier id: Identifier
name: Name name: Comercial name
socialName: Social name socialName: Business name
fi: Tax number fi: Tax number
salesPersonFk: Salesperson salesPersonFk: Salesperson
credit: Credit credit: Credit
@ -396,6 +397,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:
@ -420,6 +422,7 @@ entry:
buyingValue: Buying value buyingValue: Buying value
freightValue: Freight value freightValue: Freight value
comissionValue: Commission value comissionValue: Commission value
description: Description
packageValue: Package value packageValue: Package value
isIgnored: Is ignored isIgnored: Is ignored
price2: Grouping price2: Grouping
@ -441,6 +444,8 @@ ticket:
sms: Sms sms: Sms
notes: Notes notes: Notes
sale: Sale sale: Sale
ticketAdvance: Advance tickets
futureTickets: Future tickets
list: list:
nickname: Nickname nickname: Nickname
state: State state: State
@ -472,6 +477,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
@ -581,6 +587,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'
@ -838,7 +847,7 @@ worker:
calendar: Calendar calendar: Calendar
timeControl: Time control timeControl: Time control
locker: Locker locker: Locker
formation: Formation
list: list:
name: Name name: Name
email: Email email: Email
@ -908,6 +917,16 @@ worker:
payMethods: Pay method payMethods: Pay method
iban: IBAN iban: IBAN
bankEntity: Swift / BIC bankEntity: Swift / BIC
formation:
tableVisibleColumns:
course: Curso
startDate: Fecha Inicio
endDate: Fecha Fin
center: Centro Formación
invoice: Factura
amount: Importe
remark: Bonficado
hasDiploma: Diploma
imageNotFound: Image not found imageNotFound: Image not found
wagon: wagon:
pageTitles: pageTitles:
@ -962,7 +981,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
@ -986,6 +1005,18 @@ route:
shipped: Preparation date shipped: Preparation date
viewCmr: View CMR viewCmr: View CMR
downloadCmrs: Download CMRs downloadCmrs: Download CMRs
columnLabels:
Id: Id
vehicle: Vehicle
description: Description
isServed: Served
worker: Worker
date: Date
started: Started
actions: Actions
agency: Agency
volume: Volume
finished: Finished
supplier: supplier:
pageTitles: pageTitles:
suppliers: Suppliers suppliers: Suppliers
@ -1177,6 +1208,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

View File

@ -107,12 +107,14 @@ globals:
aliasUsers: Usuarios aliasUsers: Usuarios
subRoles: Subroles subRoles: Subroles
inheritedRoles: Roles heredados inheritedRoles: Roles heredados
workers: Trabajadores
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 comment: Comentario
observations: Observaciones
errors: errors:
statusUnauthorized: Acceso denegado statusUnauthorized: Acceso denegado
statusInternalServerError: Ha ocurrido un error interno del servidor statusInternalServerError: Ha ocurrido un error interno del servidor
@ -277,7 +279,7 @@ customer:
extendedList: extendedList:
tableVisibleColumns: tableVisibleColumns:
id: Identificador id: Identificador
name: Nombre name: Nombre Comercial
socialName: Razón social socialName: Razón social
fi: NIF / CIF fi: NIF / CIF
salesPersonFk: Comercial salesPersonFk: Comercial
@ -394,6 +396,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:
@ -418,6 +421,7 @@ entry:
buyingValue: Coste buyingValue: Coste
freightValue: Porte freightValue: Porte
comissionValue: Comisión comissionValue: Comisión
description: Descripción
packageValue: Embalaje packageValue: Embalaje
isIgnored: Ignorado isIgnored: Ignorado
price2: Grouping price2: Grouping
@ -439,6 +443,8 @@ ticket:
sms: Sms sms: Sms
notes: Notas notes: Notas
sale: Lineas del pedido sale: Lineas del pedido
ticketAdvance: Adelantar tickets
futureTickets: Tickets a futuro
list: list:
nickname: Alias nickname: Alias
state: Estado state: Estado
@ -470,6 +476,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
@ -836,6 +843,7 @@ worker:
calendar: Calendario calendar: Calendario
timeControl: Control de horario timeControl: Control de horario
locker: Taquilla locker: Taquilla
formation: Formación
list: list:
name: Nombre name: Nombre
email: Email email: Email
@ -896,6 +904,16 @@ worker:
payMethods: Método de pago payMethods: Método de pago
iban: IBAN iban: IBAN
bankEntity: Swift / BIC bankEntity: Swift / BIC
formation:
tableVisibleColumns:
course: Curso
startDate: Fecha Inicio
endDate: Fecha Fin
center: Centro Formación
invoice: Factura
amount: Importe
remark: Bonficado
hasDiploma: Diploma
imageNotFound: No se ha encontrado la imagen imageNotFound: No se ha encontrado la imagen
wagon: wagon:
pageTitles: pageTitles:
@ -950,7 +968,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
@ -974,6 +992,18 @@ route:
shipped: Fecha preparación shipped: Fecha preparación
viewCmr: Ver CMR viewCmr: Ver CMR
downloadCmrs: Descargar CMRs downloadCmrs: Descargar CMRs
columnLabels:
Id: Id
vehicle: Vehículo
description: Descripción
isServed: Servida
worker: Trabajador
date: Fecha
started: Iniciada
actions: Acciones
agency: Agencia
volume: Volumen
finished: Finalizada
supplier: supplier:
pageTitles: pageTitles:
suppliers: Proveedores suppliers: Proveedores
@ -1166,6 +1196,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

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

@ -8,12 +8,12 @@ import VnSearchbar from 'components/ui/VnSearchbar.vue';
import CardList from 'src/components/ui/CardList.vue'; import CardList from 'src/components/ui/CardList.vue';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import AclFilter from './Acls/AclFilter.vue'; import AclFilter from './Acls/AclFilter.vue';
import AclFormView from './Acls/AclFormView.vue';
import { useVnConfirm } from 'composables/useVnConfirm'; import { useVnConfirm } from 'composables/useVnConfirm';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import axios from 'axios'; import axios from 'axios';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
import AclFormView from './Acls/AclFormView.vue';
defineProps({ defineProps({
id: { id: {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,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

@ -1,11 +1,9 @@
<script setup> <script setup>
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import FormModel from 'components/FormModel.vue'; import FormModel from 'components/FormModel.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
</script> </script>

View File

@ -0,0 +1,48 @@
<script setup>
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import VnSelect from 'src/components/common/VnSelect.vue';
import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue';
import { ref, watch } from 'vue';
const route = useRoute();
const { t } = useI18n();
const formModelRef = ref(null);
const accountFilter = {
where: { id: route.params.id },
fields: ['id', 'email', 'nickname', 'name', 'accountStateFk', 'packages', 'pickup'],
include: [],
};
watch(
() => route.params.id,
() => formModelRef.value.reset()
);
</script>
<template>
<FormModel
ref="formModelRef"
:url="`VnUsers/preview`"
:url-update="`VnUsers/${route.params.id}/update-user`"
:filter="accountFilter"
model="Accounts"
auto-load
@on-data-saved="formModelRef.fetch()"
>
<template #form="{ data }">
<div class="q-gutter-y-sm">
<VnInput v-model="data.name" :label="t('account.card.nickname')" />
<VnInput v-model="data.nickname" :label="t('account.card.alias')" />
<VnInput v-model="data.email" :label="t('account.card.email')" />
<VnSelect
v-model="data.lang"
:options="['es', 'en']"
:label="t('account.card.lang')"
/>
</div>
</template>
</FormModel>
</template>

View File

@ -0,0 +1,34 @@
<script setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import VnCard from 'components/common/VnCard.vue';
import AccountDescriptor from './AccountDescriptor.vue';
const { t } = useI18n();
const route = useRoute();
const routeName = computed(() => route.name);
const customRouteRedirectName = computed(() => routeName.value);
const searchBarDataKeys = {
AccountSummary: 'AccountSummary',
AccountInheritedRoles: 'AccountInheritedRoles',
AccountMailForwarding: 'AccountMailForwarding',
AccountMailAlias: 'AccountMailAlias',
AccountPrivileges: 'AccountPrivileges',
AccountLog: 'AccountLog',
};
</script>
<template>
<VnCard
data-key="Account"
:descriptor="AccountDescriptor"
:search-data-key="searchBarDataKeys[routeName]"
:search-custom-route-redirect="customRouteRedirectName"
:search-redirect="!!customRouteRedirectName"
:searchbar-label="t('account.search')"
:searchbar-info="t('account.searchInfo')"
/>
</template>

View File

@ -0,0 +1,130 @@
<script setup>
import { ref, computed } from 'vue';
import { useRoute } 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 AccountDescriptorMenu from './AccountDescriptorMenu.vue';
import FetchData from 'src/components/FetchData.vue';
import VnImg from 'src/components/ui/VnImg.vue';
const $props = defineProps({
id: {
type: Number,
required: false,
default: null,
},
});
const route = useRoute();
const { t } = useI18n();
const entityId = computed(() => {
return $props.id || route.params.id;
});
const data = ref(useCardDescription());
const setData = (entity) => (data.value = useCardDescription(entity.nickname, entity.id));
const filter = {
where: { id: entityId },
fields: ['id', 'nickname', 'name', 'role'],
include: { relation: 'role', scope: { fields: ['id', 'name'] } },
};
const hasAccount = ref(false);
</script>
<template>
<FetchData
:url="`Accounts/${entityId}/exists`"
auto-load
@on-fetch="(data) => (hasAccount = data.exists)"
/>
<CardDescriptor
ref="descriptor"
:url="`VnUsers/preview`"
:filter="filter"
module="Account"
@on-fetch="setData"
data-key="AccountId"
:title="data.title"
:subtitle="data.subtitle"
>
<template #header-extra-action>
<QBtn
round
flat
size="md"
color="white"
icon="face"
:to="{ name: 'AccountList' }"
>
<QTooltip>
{{ t('Go to module index') }}
</QTooltip>
</QBtn>
</template>
<template #menu>
<AccountDescriptorMenu :has-account="hasAccount" />
</template>
<template #before>
<!-- falla id :id="entityId.value" collection="user" size="160x160" -->
<VnImg :id="entityId" collection="user" size="160x160" class="photo">
<template #error>
<div
class="absolute-full picture text-center q-pa-md flex flex-center"
>
<div>
<div class="text-grey-5" style="opacity: 0.4; font-size: 5vh">
<QIcon name="vn:claims" />
</div>
<div class="text-grey-5" style="opacity: 0.4">
{{ t('account.imageNotFound') }}
</div>
</div>
</div>
</template>
</VnImg>
</template>
<template #body="{ entity }">
<VnLv :label="t('account.card.nickname')" :value="entity.nickname" />
<VnLv :label="t('account.card.role')" :value="entity.role.name" />
</template>
<template #actions="{ entity }">
<QCardActions class="q-gutter-x-md">
<QIcon
v-if="!entity.active"
color="primary"
name="vn:disabled"
flat
round
size="sm"
class="fill-icon"
>
<QTooltip>{{ t('account.card.deactivated') }}</QTooltip>
</QIcon>
<QIcon
color="primary"
name="contact_mail"
v-if="entity.hasAccount"
flat
round
size="sm"
class="fill-icon"
>
<QTooltip>{{ t('account.card.enabled') }}</QTooltip>
</QIcon>
</QCardActions>
</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,187 @@
<script setup>
import axios from 'axios';
import { computed, ref, toRefs } from 'vue';
import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n';
import { useVnConfirm } from 'composables/useVnConfirm';
import { useRoute } from 'vue-router';
import { useArrayData } from 'src/composables/useArrayData';
import CustomerChangePassword from 'src/pages/Customer/components/CustomerChangePassword.vue';
import VnConfirm from 'src/components/ui/VnConfirm.vue';
const quasar = useQuasar();
const $props = defineProps({
hasAccount: {
type: Boolean,
default: false,
required: true,
},
});
const { t } = useI18n();
const { hasAccount } = toRefs($props);
const { openConfirmationModal } = useVnConfirm();
const route = useRoute();
const account = computed(() => useArrayData('AccountId').store.data[0]);
account.value.hasAccount = hasAccount.value;
const entityId = computed(() => +route.params.id);
async function updateStatusAccount(active) {
if (active) {
await axios.post(`Accounts`, { id: entityId.value });
} else {
await axios.delete(`Accounts/${entityId.value}`);
}
account.value.hasAccount = active;
const status = active ? 'enable' : 'disable';
quasar.notify({
message: t(`account.card.${status}Account.success`),
type: 'positive',
});
}
async function updateStatusUser(active) {
await axios.patch(`VnUsers/${entityId.value}`, { active });
account.value.active = active;
const status = active ? 'activate' : 'deactivate';
quasar.notify({
message: t(`account.card.actions.${status}User.success`),
type: 'positive',
});
}
function setPassword() {
quasar.dialog({
component: CustomerChangePassword,
componentProps: {
id: entityId.value,
},
});
}
const showSyncDialog = ref(false);
const syncPassword = ref(null);
const shouldSyncPassword = ref(false);
async function sync() {
const params = { force: true };
if (shouldSyncPassword.value) params.password = syncPassword.value;
await axios.patch(`Accounts/${account.value.name}/sync`, {
params,
});
quasar.notify({
message: t('account.card.actions.sync.success'),
type: 'positive',
});
}
</script>
<template>
<VnConfirm
v-model="showSyncDialog"
:message="t('account.card.actions.sync.message')"
:title="t('account.card.actions.sync.title')"
:promise="sync"
>
<template #customHTML>
{{ shouldSyncPassword }}
<QCheckbox
:label="t('account.card.actions.sync.checkbox')"
v-model="shouldSyncPassword"
class="full-width"
clearable
clear-icon="close"
>
<QIcon style="padding-left: 10px" color="primary" name="info" size="sm">
<QTooltip>{{ t('account.card.actions.sync.tooltip') }}</QTooltip>
</QIcon></QCheckbox
>
<QInput
v-if="shouldSyncPassword"
:label="t('login.password')"
v-model="syncPassword"
class="full-width"
clearable
clear-icon="close"
type="password"
/>
</template>
</VnConfirm>
<QItem v-ripple clickable @click="setPassword">
<QItemSection>{{ t('account.card.actions.setPassword') }}</QItemSection>
</QItem>
<QItem
v-if="!account.hasAccount"
v-ripple
clickable
@click="
openConfirmationModal(
t('account.card.actions.enableAccount.title'),
t('account.card.actions.enableAccount.subtitle'),
() => updateStatusAccount(true)
)
"
>
<QItemSection>{{ t('account.card.actions.enableAccount.name') }}</QItemSection>
</QItem>
<QItem
v-if="account.hasAccount"
v-ripple
clickable
@click="
openConfirmationModal(
t('account.card.actions.disableAccount.title'),
t('account.card.actions.disableAccount.subtitle'),
() => updateStatusAccount(false)
)
"
>
<QItemSection>{{ t('account.card.actions.disableAccount.name') }}</QItemSection>
</QItem>
<QItem
v-if="!account.active"
v-ripple
clickable
@click="
openConfirmationModal(
t('account.card.actions.activateUser.title'),
t('account.card.actions.activateUser.title'),
() => updateStatusUser(true)
)
"
>
<QItemSection>{{ t('account.card.actions.activateUser.name') }}</QItemSection>
</QItem>
<QItem
v-if="account.active"
v-ripple
clickable
@click="
openConfirmationModal(
t('account.card.actions.deactivateUser.title'),
t('account.card.actions.deactivateUser.title'),
() => updateStatusUser(false)
)
"
>
<QItemSection>{{ t('account.card.actions.deactivateUser.name') }}</QItemSection>
</QItem>
<QItem v-ripple clickable @click="showSyncDialog = true">
<QItemSection>{{ t('account.card.actions.sync.name') }}</QItemSection>
</QItem>
<QSeparator />
<QItem
@click="
openConfirmationModal(
t('account.card.actions.delete.title'),
t('account.card.actions.delete.subTitle'),
removeAccount
)
"
v-ripple
clickable
>
<QItemSection avatar>
<QIcon name="delete" />
</QItemSection>
<QItemSection>{{ t('account.card.actions.delete.name') }}</QItemSection>
</QItem>
</template>

View File

@ -0,0 +1,7 @@
<script setup>
import InheritedRoles from '../InheritedRoles.vue';
</script>
<template>
<InheritedRoles data-key="AccountInheritedRoles" />
</template>

View File

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

View File

@ -0,0 +1,187 @@
<script setup>
import { computed, ref, watch, onMounted, nextTick } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import VnPaginate from 'components/ui/VnPaginate.vue';
import AccountMailAliasCreateForm from './AccountMailAliasCreateForm.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 { openConfirmationModal } = useVnConfirm();
const { notify } = useNotify();
const paginateRef = ref(null);
const createMailAliasDialogRef = ref(null);
const arrayData = useArrayData('AccountMailAliases');
const store = arrayData.store;
const loading = ref(false);
const hasAccount = ref(false);
const data = computed(() => {
const dataCopy = store.data;
return dataCopy.sort((a, b) => a.alias?.alias.localeCompare(b.alias?.alias));
});
const filter = computed(() => ({
where: { account: route.params.id },
include: {
relation: 'alias',
scope: {
fields: ['id', 'alias', 'description'],
},
},
}));
const urlPath = 'MailAliasAccounts';
const columns = computed(() => [
{
name: 'name',
},
{
name: 'action',
},
]);
const fetchAccountExistence = async () => {
try {
const { data } = await axios.get(`Accounts/${route.params.id}/exists`);
return data.exists;
} catch (error) {
console.error('Error fetching account existence', error);
return false;
}
};
const deleteMailAlias = async (row) => {
try {
await axios.delete(`${urlPath}/${row.id}`);
fetchMailAliases();
notify(t('Unsubscribed from alias!'), 'positive');
} catch (error) {
console.error(error);
}
};
const createMailAlias = async (mailAliasFormData) => {
try {
await axios.post(urlPath, mailAliasFormData);
notify(t('Subscribed to alias!'), 'positive');
fetchMailAliases();
} catch (error) {
console.error(error);
}
};
const fetchMailAliases = async () => {
await nextTick();
paginateRef.value.fetch();
};
const getAccountData = async () => {
loading.value = true;
hasAccount.value = await fetchAccountExistence();
if (!hasAccount.value) {
loading.value = false;
store.data = [];
return;
}
await fetchMailAliases();
loading.value = false;
};
const openCreateMailAliasForm = () => createMailAliasDialogRef.value.show();
watch(
() => route.params.id,
() => {
store.url = urlPath;
store.filter = filter.value;
getAccountData();
}
);
onMounted(async () => await getAccountData());
</script>
<template>
<QPage class="column items-center q-pa-md">
<div class="full-width" style="max-width: 400px">
<QSpinner v-if="loading" color="primary" size="md" />
<VnPaginate
ref="paginateRef"
data-key="AccountMailAliases"
:filter="filter"
:url="urlPath"
auto-load
>
<template #body="{ rows }">
<QTable
v-if="hasAccount && !loading"
:rows="data"
:columns="columns"
hide-header
>
<template #body="{ row, rowIndex }">
<QTr>
<QTd>
<div class="column">
<span>{{ row.alias?.alias }}</span>
<span class="color-vn-label">{{
row.alias?.description
}}</span>
</div>
</QTd>
<QTd style="width: 50px !important">
<QIcon
name="delete"
size="sm"
class="cursor-pointer"
color="primary"
@click.stop.prevent="
openConfirmationModal(
t('User will be removed from alias'),
t('¿Seguro que quieres continuar?'),
() => deleteMailAlias(row, rows, rowIndex)
)
"
>
<QTooltip>
{{ t('globals.delete') }}
</QTooltip>
</QIcon>
</QTd>
</QTr>
</template>
</QTable>
</template>
</VnPaginate>
<h5 v-if="!hasAccount" class="text-center">
{{ t('account.mailForwarding.accountNotEnabled') }}
</h5>
</div>
<QDialog ref="createMailAliasDialogRef">
<AccountMailAliasCreateForm @on-submit-create-alias="createMailAlias" />
</QDialog>
<QPageSticky position="bottom-right" :offset="[18, 18]">
<QBtn fab icon="add" color="primary" @click="openCreateMailAliasForm()">
<QTooltip>{{ t('warehouses.add') }}</QTooltip>
</QBtn>
</QPageSticky>
</QPage>
</template>
<i18n>
es:
Unsubscribed from alias!: ¡Desuscrito del alias!
Subscribed to alias!: ¡Suscrito al alias!
User will be removed from alias: El usuario será borrado del alias
Are you sure you want to continue?: ¿Seguro que quieres continuar?
</i18n>

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(['onSubmitCreateAlias']);
const { t } = useI18n();
const route = useRoute();
const aliasFormData = reactive({
mailAlias: null,
account: route.params.id,
});
const aliasOptions = ref([]);
</script>
<template>
<FetchData
url="MailAliases"
:filter="{ fields: ['id', 'alias'], order: 'alias ASC' }"
auto-load
@on-fetch="(data) => (aliasOptions = data)"
/>
<FormPopup
model="ZoneWarehouse"
@on-submit="emit('onSubmitCreateAlias', aliasFormData)"
>
<template #form-inputs>
<VnRow class="row q-gutter-md q-mb-md">
<div class="col">
<VnSelect
:label="t('account.card.alias')"
v-model="aliasFormData.mailAlias"
:options="aliasOptions"
option-value="id"
option-label="alias"
hide-selected
:required="true"
/>
</div>
</VnRow>
</template>
</FormPopup>
</template>

View File

@ -0,0 +1,159 @@
<script setup>
import { ref, onMounted, watch, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import VnInput from 'src/components/common/VnInput.vue';
import VnRow from 'components/ui/VnRow.vue';
import axios from 'axios';
import { useStateStore } from 'stores/useStateStore';
import useNotify from 'src/composables/useNotify.js';
const { t } = useI18n();
const route = useRoute();
const stateStore = useStateStore();
const { notify } = useNotify();
const initialData = ref({});
const formData = ref({
forwardTo: null,
account: null,
});
const hasAccount = ref(false);
const hasData = ref(false);
const loading = ref(false);
const hasDataChanged = computed(
() =>
formData.value.forwardTo !== initialData.value.forwardTo ||
initialData.value.hasData !== hasData.value
);
const fetchAccountExistence = async () => {
try {
const { data } = await axios.get(`Accounts/${route.params.id}/exists`);
return data.exists;
} catch (error) {
console.error('Error fetching account existence', error);
return false;
}
};
const fetchMailForwards = async () => {
try {
const response = await axios.get(`MailForwards/${route.params.id}`);
return response.data;
} catch (err) {
console.error('Error fetching mail forwards', err);
return null;
}
};
const deleteMailForward = async () => {
try {
await axios.delete(`MailForwards/${route.params.id}`);
formData.value.forwardTo = null;
initialData.value.forwardTo = null;
initialData.value.hasData = hasData.value;
notify(t('globals.dataSaved'), 'positive');
} catch (err) {
console.error('Error deleting mail forward', err);
}
};
const updateMailForward = async () => {
try {
await axios.patch('MailForwards', formData.value);
initialData.value = { ...formData.value };
initialData.value.hasData = hasData.value;
} catch (err) {
console.error('Error creating mail forward', err);
}
};
const onSubmit = async () => {
if (hasData.value) await updateMailForward();
else await deleteMailForward();
};
const setInitialData = async () => {
loading.value = true;
initialData.value.account = route.params.id;
formData.value.account = route.params.id;
hasAccount.value = await fetchAccountExistence(route.params.id);
if (!hasAccount.value) {
loading.value = false;
return;
}
const result = await fetchMailForwards(route.params.id);
const forwardTo = result ? result.forwardTo : null;
formData.value.forwardTo = forwardTo;
initialData.value.forwardTo = forwardTo;
initialData.value.hasData = hasAccount.value && !!forwardTo;
hasData.value = hasAccount.value && !!forwardTo;
loading.value = false;
};
watch(
() => route.params.id,
() => setInitialData()
);
onMounted(async () => await setInitialData());
</script>
<template>
<div class="flex justify-center">
<QSpinner v-if="loading" color="primary" size="md" />
<QForm
v-else-if="hasAccount"
@submit="onSubmit()"
class="full-width"
style="max-width: 800px"
>
<Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown()">
<div>
<QBtnGroup push class="q-gutter-x-sm">
<slot name="moreActions" />
<QBtn
color="primary"
icon="restart_alt"
flat
@click="reset()"
:label="t('globals.reset')"
/>
<QBtn
color="primary"
icon="save"
@click="onSubmit()"
:disable="!hasDataChanged"
:label="t('globals.save')"
/>
</QBtnGroup>
</div>
</Teleport>
<QCard class="q-pa-lg">
<VnRow class="row q-mb-md">
<QCheckbox
v-model="hasData"
:label="t('account.mailForwarding.enableMailForwarding')"
:toggle-indeterminate="false"
/>
</VnRow>
<VnRow v-if="hasData" class="row q-gutter-md q-mb-md">
<VnInput
v-model="formData.forwardTo"
:label="t('account.mailForwarding.forwardingMail')"
:info="t('account.mailForwarding.mailInputInfo')"
>
</VnInput>
</VnRow>
</QCard>
</QForm>
<h5 v-else class="text-center">
{{ t('account.mailForwarding.accountNotEnabled') }}
</h5>
</div>
</template>

View File

@ -0,0 +1,49 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import FetchData from 'components/FetchData.vue';
import FormModel from 'components/FormModel.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
const { t } = useI18n();
const route = useRoute();
const rolesOptions = ref([]);
const formModelRef = ref();
</script>
<template>
<FetchData
url="VnRoles"
:filter="{ fields: ['id', 'name'], order: 'name ASC' }"
auto-load
@on-fetch="(data) => (rolesOptions = data)"
/>
<FormModel
ref="formModelRef"
model="AccountPrivileges"
:url="`VnUsers/${route.params.id}`"
:url-create="`VnUsers/${route.params.id}/privileges`"
auto-load
@on-data-saved="formModelRef.fetch()"
>
<template #form="{ data }">
<div class="q-gutter-y-sm">
<QCheckbox
v-model="data.hasGrant"
:label="t('account.card.privileges.delegate')"
/>
<VnSelect
:label="t('account.card.role')"
v-model="data.roleFk"
:options="rolesOptions"
option-value="id"
option-label="name"
hide-selected
:required="true"
/>
</div>
</template>
</FormModel>
</template>

View File

@ -0,0 +1,102 @@
<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('Account');
const account = ref(store.data);
const entityId = computed(() => $props.id || route.params.id);
const filter = {
where: { id: entityId },
fields: ['id', 'nickname', 'name', 'role'],
include: { relation: 'role', scope: { fields: ['id', 'name'] } },
};
</script>
<template>
<CardSummary
data-key="AccountSummary"
ref="AccountSummary"
url="VnUsers/preview"
:filter="filter"
@on-fetch="(data) => (account = data)"
>
<template #header>{{ account.id }} - {{ account.nickname }}</template>
<template #body>
<QCard class="vn-one">
<QCardSection class="q-pa-none">
<router-link
:to="{ name: 'AccountBasicData', params: { id: entityId } }"
class="header header-link"
>
{{ t('globals.pageTitles.basicData') }}
<QIcon name="open_in_new" />
</router-link>
</QCardSection>
<VnLv :label="t('account.card.nickname')" :value="account.nickname" />
<VnLv :label="t('account.card.role')" :value="account.role.name" />
</QCard>
</template>
</CardSummary>
</template>
<style lang="scss" scoped>
.q-dialog__inner--minimized > div {
max-width: 80%;
}
.container {
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 15px;
}
.multimedia-container {
flex: 1 0 21%;
}
.multimedia {
transition: all 0.5s;
opacity: 1;
height: 250px;
.q-img {
object-fit: cover;
background-color: black;
}
video {
object-fit: cover;
background-color: black;
}
}
.multimedia:hover {
opacity: 0.5;
}
.close-button {
top: 1%;
right: 10%;
}
.zindex {
z-index: 1;
}
.change-state {
width: 10%;
}
</style>

View File

@ -0,0 +1,104 @@
<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 props = defineProps({
dataKey: { type: String, required: true },
});
const route = useRoute();
const router = useRouter();
const paginateRef = ref(null);
const arrayData = useArrayData(props.dataKey);
const store = arrayData.store;
const data = computed(() => {
const dataCopy = store.data;
return dataCopy.sort((a, b) => a.role?.name.localeCompare(b.role?.name));
});
const filter = computed(() => ({
where: {
prindicpalType: 'USER',
principalId: route.params.id,
},
include: {
relation: 'role',
scope: {
fields: ['id', 'name', 'description'],
},
},
}));
const urlPath = 'RoleMappings';
const columns = computed(() => [
{
name: 'name',
},
]);
watch(
() => route.params.id,
() => {
store.url = urlPath;
store.filter = filter.value;
store.limit = 0;
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="dataKey"
:filter="filter"
:url="urlPath"
:limit="0"
auto-load
>
<template #body>
<QTable :rows="data" :columns="columns" hide-header>
<template #body="{ row }">
<QTr
@click="redirectToRoleSummary(row.role?.id)"
class="cursor-pointer"
>
<QTd>
<div class="column">
<span>{{ row.role?.name }}</span>
<span class="color-vn-label">{{
row.role?.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

@ -15,24 +15,75 @@ account:
privileges: Privileges privileges: Privileges
mailAlias: Mail Alias mailAlias: Mail Alias
mailForwarding: Mail Forwarding mailForwarding: Mail Forwarding
accountCreate: New user
aliasUsers: Users aliasUsers: Users
card: card:
name: Name name: Name
nickname: User nickname: User
role: Rol role: Role
email: Email email: Email
alias: Alias alias: Alias
lang: Language lang: Language
roleFk: Role
newUser: New user
ticketTracking: Ticket tracking
privileges:
delegate: Can delegate privileges
enabled: Account enabled!
disabled: Account disabled!
willActivated: User will activated
willDeactivated: User will be deactivated
activated: User activated!
deactivated: User deactivated!
actions: actions:
setPassword: Set password setPassword: Set password
disableAccount: disableAccount:
name: Disable account name: Disable account
title: La cuenta será deshabilitada title: The account will be disabled
subtitle: ¿Seguro que quieres continuar? subtitle: Are you sure you want to continue?
disableUser: Disable user success: 'Account disabled!'
sync: Sync enableAccount:
delete: Delete name: Enable account
title: The account will be enabled
subtitle: Are you sure you want to continue?
success: 'Account enabled!'
deactivateUser:
name: Deactivate user
title: The user will be deactivated
subtitle: Are you sure you want to continue?
success: 'User deactivated!'
activateUser:
name: Activate user
title: The user will be disabled
subtitle: Are you sure you want to continue?
success: 'User activated!'
sync:
name: Sync
title: The account will be sync
subtitle: Are you sure you want to continue?
success: 'User synchronized!'
checkbox: Synchronize password
message: Do you want to synchronize user?
tooltip: If password is not specified, just user attributes are synchronized
delete:
name: Delete
title: The account will be deleted
subtitle: Are you sure you want to continue?
success: ''
search: Search user search: Search user
searchInfo: You can search by id, name or nickname
create:
name: Name
nickname: Nickname
email: Email
role: Role
password: Password
active: Active
mailForwarding:
forwardingMail: Forward email
accountNotEnabled: Account not enabled
enableMailForwarding: Enable mail forwarding
mailInputInfo: All emails will be forwarded to the specified address.
role: role:
pageTitles: pageTitles:
inheritedRoles: Inherited Roles inheritedRoles: Inherited Roles
@ -67,6 +118,7 @@ ldap:
groupDN: Group DN groupDN: Group DN
testConnection: Test connection testConnection: Test connection
success: LDAP connection established! success: LDAP connection established!
password: Password
samba: samba:
enableSync: Enable synchronization enableSync: Enable synchronization
domainController: Domain controller domainController: Domain controller
@ -78,6 +130,21 @@ samba:
verifyCertificate: Verify certificate verifyCertificate: Verify certificate
testConnection: Test connection testConnection: Test connection
success: Samba connection established! 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: acls:
role: Role role: Role
accessType: Access type accessType: Access type

View File

@ -15,6 +15,7 @@ account:
privileges: Privilegios privileges: Privilegios
mailAlias: Alias de correo mailAlias: Alias de correo
mailForwarding: Reenvío de correo mailForwarding: Reenvío de correo
accountCreate: Nuevo usuario
aliasUsers: Usuarios aliasUsers: Usuarios
card: card:
nickname: Usuario nickname: Usuario
@ -22,27 +23,66 @@ account:
role: Rol role: Rol
email: Mail email: Mail
alias: Alias alias: Alias
lang: dioma lang: Idioma
roleFk: Rol
enabled: ¡Cuenta habilitada!
disabled: ¡Cuenta deshabilitada!
willActivated: El usuario será activado
willDeactivated: El usuario será desactivado
activated: ¡Usuario activado!
deactivated: ¡Usuario desactivado!
newUser: Nuevo usuario
privileges:
delegate: Puede delegar privilegios
actions: actions:
setPassword: Establecer contraseña setPassword: Establecer contraseña
disableAccount: disableAccount:
name: Deshabilitar cuenta name: Deshabilitar cuenta
title: La cuenta será deshabilitada title: La cuenta será deshabilitada
subtitle: ¿Seguro que quieres continuar? subtitle: ¿Seguro que quieres continuar?
disableUser: success: '¡Cuenta deshabilitada!'
enableAccount:
name: Habilitar cuenta
title: La cuenta será habilitada
subtitle: ¿Seguro que quieres continuar?
success: '¡Cuenta habilitada!'
deactivateUser:
name: Desactivar usuario name: Desactivar usuario
title: El usuario será deshabilitado title: El usuario será deshabilitado
subtitle: ¿Seguro que quieres continuar? subtitle: ¿Seguro que quieres continuar?
success: '¡Usuario desactivado!'
activateUser:
name: Activar usuario
title: El usuario será activado
subtitle: ¿Seguro que quieres continuar?
success: '¡Usuario activado!'
sync: sync:
name: Sincronizar name: Sincronizar
title: El usuario será sincronizado title: El usuario será sincronizado
subtitle: ¿Seguro que quieres continuar? subtitle: ¿Seguro que quieres continuar?
success: '¡Usuario sincronizado!'
checkbox: Sincronizar contraseña
message: ¿Quieres sincronizar el usuario?
tooltip: Si la contraseña no se especifica solo se sincronizarán lo atributos del usuario
delete: delete:
name: Eliminar name: Eliminar
title: El usuario será eliminado title: El usuario será eliminado
subtitle: ¿Seguro que quieres continuar? subtitle: ¿Seguro que quieres continuar?
success: ''
search: Buscar usuario search: Buscar usuario
searchInfo: Puedes buscar por id, nombre o usuario
create:
name: Nombre
nickname: Nombre mostrado
email: Email
role: Rol
password: Contraseña
active: Activo
mailForwarding:
forwardingMail: Dirección de reenvío
accountNotEnabled: Cuenta no habilitada
enableMailForwarding: Habilitar redirección de correo
mailInputInfo: Todos los correos serán reenviados a la dirección especificada, no se mantendrá copia de los mismos en el buzón del usuario.
role: role:
pageTitles: pageTitles:
inheritedRoles: Roles heredados inheritedRoles: Roles heredados
@ -70,6 +110,7 @@ mailAlias:
name: Nombre name: Nombre
isPublic: Público isPublic: Público
ldap: ldap:
password: Contraseña
enableSync: Habilitar sincronización enableSync: Habilitar sincronización
server: Servidor server: Servidor
rdn: RDN rdn: RDN
@ -86,9 +127,24 @@ samba:
userAD: Usuario AD userAD: Usuario AD
passwordAD: Contraseña AD passwordAD: Contraseña AD
domainPart: DN usuarios (sin la parte del dominio) domainPart: DN usuarios (sin la parte del dominio)
Verify certificate: Verificar certificado verifyCertificate: Verificar certificado
testConnection: Probar conexión testConnection: Probar conexión
success: ¡Conexión con Samba establecida! 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: acls:
role: Rol role: Rol
accessType: Tipo de acceso accessType: Tipo de acceso

View File

@ -33,7 +33,6 @@ function exprBuilder(param, value) {
url="Agencies" url="Agencies"
order="name" order="name"
:expr-builder="exprBuilder" :expr-builder="exprBuilder"
auto-load
> >
<template #body="{ rows }"> <template #body="{ rows }">
<CardList <CardList

View File

@ -14,7 +14,7 @@ const entityId = computed(() => $props.id || useRoute().params.id);
<template> <template>
<div class="q-pa-md"> <div class="q-pa-md">
<CardSummary :url="`Agencies/${entityId}`"> <CardSummary :url="`Agencies/${entityId}`" data-key="Agency">
<template #header="{ entity: agency }">{{ agency.name }}</template> <template #header="{ entity: agency }">{{ agency.name }}</template>
<template #body="{ entity: agency }"> <template #body="{ entity: agency }">
<QCard class="vn-one"> <QCard class="vn-one">

View File

@ -10,32 +10,13 @@ import VnInput from 'src/components/common/VnInput.vue';
import VnInputDate from 'components/common/VnInputDate.vue'; import VnInputDate from 'components/common/VnInputDate.vue';
import axios from 'axios'; import axios from 'axios';
import { useSession } from 'src/composables/useSession'; // import { useSession } from 'src/composables/useSession';
import VnImg from 'src/components/ui/VnImg.vue';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const { getTokenMultimedia } = useSession(); // const { getTokenMultimedia } = useSession();
const token = getTokenMultimedia(); // const token = getTokenMultimedia();
const claimFilter = {
fields: [
'id',
'clientFk',
'created',
'workerFk',
'claimStateFk',
'packages',
'pickup',
],
include: [
{
relation: 'client',
scope: {
fields: ['name'],
},
},
],
};
const claimStates = ref([]); const claimStates = ref([]);
const claimStatesCopy = ref([]); const claimStatesCopy = ref([]);
@ -87,11 +68,10 @@ const statesFilter = {
/> />
<FetchData url="ClaimStates" @on-fetch="setClaimStates" auto-load /> <FetchData url="ClaimStates" @on-fetch="setClaimStates" auto-load />
<FormModel <FormModel
:url="`Claims/${route.params.id}`" model="Claim"
:url-update="`Claims/updateClaim/${route.params.id}`" :url-update="`Claims/updateClaim/${route.params.id}`"
:filter="claimFilter"
model="claim"
auto-load auto-load
:reload="true"
> >
<template #form="{ data, validate, filter }"> <template #form="{ data, validate, filter }">
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">
@ -118,9 +98,11 @@ const statesFilter = {
> >
<template #before> <template #before>
<QAvatar color="orange"> <QAvatar color="orange">
<QImg <VnImg
v-if="data.workerFk" v-if="data.workerFk"
:src="`/api/Images/user/160x160/${data.workerFk}/download?access_token=${token}`" :size="'160x160'"
:id="data.workerFk"
collection="user"
spinner-color="white" spinner-color="white"
/> />
</QAvatar> </QAvatar>

View File

@ -2,6 +2,7 @@
import VnCard from 'components/common/VnCard.vue'; import VnCard from 'components/common/VnCard.vue';
import ClaimDescriptor from './ClaimDescriptor.vue'; import ClaimDescriptor from './ClaimDescriptor.vue';
import ClaimFilter from '../ClaimFilter.vue'; import ClaimFilter from '../ClaimFilter.vue';
import filter from './ClaimFilter.js';
</script> </script>
<template> <template>
<VnCard <VnCard
@ -13,5 +14,6 @@ import ClaimFilter from '../ClaimFilter.vue';
search-url="Claims/filter" search-url="Claims/filter"
searchbar-label="Search claim" searchbar-label="Search claim"
searchbar-info="You can search by claim id or customer name" searchbar-info="You can search by claim id or customer name"
:filter="filter"
/> />
</template> </template>

View File

@ -12,6 +12,7 @@ import useCardDescription from 'src/composables/useCardDescription';
import VnUserLink from 'src/components/ui/VnUserLink.vue'; import VnUserLink from 'src/components/ui/VnUserLink.vue';
import { getUrl } from 'src/composables/getUrl'; import { getUrl } from 'src/composables/getUrl';
import ZoneDescriptorProxy from 'src/pages/Zone/Card/ZoneDescriptorProxy.vue'; import ZoneDescriptorProxy from 'src/pages/Zone/Card/ZoneDescriptorProxy.vue';
import filter from './ClaimFilter.js';
const $props = defineProps({ const $props = defineProps({
id: { id: {
@ -29,49 +30,6 @@ const entityId = computed(() => {
return $props.id || route.params.id; return $props.id || route.params.id;
}); });
const filter = {
include: [
{
relation: 'client',
scope: {
include: [
{ relation: 'salesPersonUser' },
{
relation: 'claimsRatio',
scope: {
fields: ['claimingRate'],
limit: 1,
},
},
],
},
},
{
relation: 'claimState',
},
{
relation: 'ticket',
scope: {
include: [
{ relation: 'zone' },
{
relation: 'address',
scope: {
include: { relation: 'province' },
},
},
],
},
},
{
relation: 'worker',
scope: {
include: { relation: 'user' },
},
},
],
};
const STATE_COLOR = { const STATE_COLOR = {
pending: 'warning', pending: 'warning',
incomplete: 'info', incomplete: 'info',
@ -101,7 +59,7 @@ onMounted(async () => {
:title="data.title" :title="data.title"
:subtitle="data.subtitle" :subtitle="data.subtitle"
@on-fetch="setData" @on-fetch="setData"
data-key="claimData" data-key="Claim"
> >
<template #menu="{ entity }"> <template #menu="{ entity }">
<ClaimDescriptorMenu :claim="entity" /> <ClaimDescriptorMenu :claim="entity" />

View File

@ -0,0 +1,52 @@
export default {
fields: [
'id',
'clientFk',
'created',
'workerFk',
'claimStateFk',
'packages',
'pickup',
'ticketFk',
],
include: [
{
relation: 'client',
scope: {
include: [
{ relation: 'salesPersonUser' },
{
relation: 'claimsRatio',
scope: {
fields: ['claimingRate'],
limit: 1,
},
},
],
},
},
{
relation: 'claimState',
},
{
relation: 'ticket',
scope: {
include: [
{ relation: 'zone' },
{
relation: 'address',
scope: {
include: { relation: 'province' },
},
},
],
},
},
{
relation: 'worker',
scope: {
include: { relation: 'user' },
},
},
],
};

View File

@ -185,6 +185,7 @@ async function changeState(value) {
:url="`Claims/${entityId}/getSummary`" :url="`Claims/${entityId}/getSummary`"
:entity-id="entityId" :entity-id="entityId"
@on-fetch="getClaimDms" @on-fetch="getClaimDms"
data-key="claimSummary"
> >
<template #header="{ entity: { claim } }"> <template #header="{ entity: { claim } }">
{{ claim.id }} - {{ claim.client.name }} ({{ claim.client.id }}) {{ claim.id }} - {{ claim.client.name }} ({{ claim.client.id }})

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

@ -3,16 +3,14 @@ import { ref } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useSession } from 'src/composables/useSession';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import FormModel from 'components/FormModel.vue'; import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import VnImg from 'src/components/ui/VnImg.vue';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const { getTokenMultimedia } = useSession();
const token = getTokenMultimedia();
const workers = ref([]); const workers = ref([]);
const workersCopy = ref([]); const workersCopy = ref([]);
@ -143,10 +141,11 @@ const filterOptions = {
> >
<template #prepend> <template #prepend>
<QAvatar color="orange"> <QAvatar color="orange">
<QImg <VnImg
:src="`/api/Images/user/160x160/${data.salesPersonFk}/download?access_token=${token}`"
spinner-color="white"
v-if="data.salesPersonFk" v-if="data.salesPersonFk"
:id="user.id"
collection="user"
spinner-color="white"
/> />
</QAvatar> </QAvatar>
</template> </template>

View File

@ -10,7 +10,7 @@ import CustomerFilter from '../CustomerFilter.vue';
:descriptor="CustomerDescriptor" :descriptor="CustomerDescriptor"
:filter-panel="CustomerFilter" :filter-panel="CustomerFilter"
search-data-key="CustomerList" search-data-key="CustomerList"
search-url="Clients/filter" search-url="Clients/extendedListFilter"
searchbar-label="Search customer" searchbar-label="Search customer"
searchbar-info="You can search by customer id or name" searchbar-info="You can search by customer id or name"
/> />

View File

@ -143,10 +143,6 @@ function handleLocation(data, location) {
</VnRow> </VnRow>
<VnRow> <VnRow>
<QCheckbox
:label="t('Incoterms authorization')"
v-model="data.hasIncoterms"
/>
<QCheckbox <QCheckbox
:label="t('Electronic invoice')" :label="t('Electronic invoice')"
v-model="data.hasElectronicInvoice" v-model="data.hasElectronicInvoice"

View File

@ -61,7 +61,11 @@ const creditWarning = computed(() => {
</script> </script>
<template> <template>
<CardSummary ref="summary" :url="`Clients/${entityId}/summary`"> <CardSummary
ref="summary"
:url="`Clients/${entityId}/summary`"
data-key="CustomerSummary"
>
<template #body="{ entity }"> <template #body="{ entity }">
<QCard class="vn-one"> <QCard class="vn-one">
<VnTitle <VnTitle
@ -306,10 +310,8 @@ const creditWarning = computed(() => {
:value="entity.recommendedCredit" :value="entity.recommendedCredit"
/> />
</QCard> </QCard>
<QCard> <QCard class="vn-one">
<div class="header"> <VnTitle :text="t('Latest tickets')" />
{{ t('Latest tickets') }}
</div>
<CustomerSummaryTable /> <CustomerSummaryTable />
</QCard> </QCard>
</template> </template>

View File

@ -28,7 +28,6 @@ const isLoading = ref(false);
const name = ref(null); const name = ref(null);
const usersPreviewRef = ref(null); const usersPreviewRef = ref(null);
const user = ref([]); const user = ref([]);
const userPasswords = ref(0);
const dataChanges = computed(() => { const dataChanges = computed(() => {
return ( return (
@ -45,7 +44,6 @@ const showChangePasswordDialog = () => {
component: CustomerChangePassword, component: CustomerChangePassword,
componentProps: { componentProps: {
id: route.params.id, id: route.params.id,
userPasswords: userPasswords.value,
promise: usersPreviewRef.value.fetch(), promise: usersPreviewRef.value.fetch(),
}, },
}); });
@ -97,11 +95,6 @@ const onSubmit = async () => {
@on-fetch="(data) => (canChangePassword = data)" @on-fetch="(data) => (canChangePassword = data)"
auto-load auto-load
/> />
<FetchData
@on-fetch="(data) => (userPasswords = data[0])"
auto-load
url="UserPasswords"
/>
<Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown()"> <Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown()">
<QBtnGroup push class="q-gutter-x-sm"> <QBtnGroup push class="q-gutter-x-sm">

View File

@ -29,7 +29,7 @@ const zones = ref();
@on-fetch="(data) => (workers = data)" @on-fetch="(data) => (workers = data)"
auto-load auto-load
/> />
<VnFilterPanel :data-key="props.dataKey" :search-button="true"> <VnFilterPanel :data-key="props.dataKey" :search-button="true" search-url="table">
<template #tags="{ tag, formatFn }"> <template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs"> <div class="q-gutter-x-xs">
<strong>{{ t(`params.${tag.label}`) }}: </strong> <strong>{{ t(`params.${tag.label}`) }}: </strong>

View File

@ -1,94 +1,439 @@
<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 VnPaginate from 'src/components/ui/VnPaginate.vue'; import VnTable from 'components/VnTable/VnTable.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue'; import VnLocation from 'src/components/common/VnLocation.vue';
import CustomerFilter from './CustomerFilter.vue'; import VnSearchbar from 'components/ui/VnSearchbar.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import CardList from 'src/components/ui/CardList.vue';
import 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 RightMenu from 'src/components/common/RightMenu.vue'; import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue';
import { toDate } from 'src/filters';
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>
<VnSearchbar <VnSearchbar
:info="t('You can search by customer id or name')" :info="t('You can search by customer id or name')"
:label="t('Search customer')" :label="t('Search customer')"
data-key="CustomerList" data-key="Customer"
/> />
<RightMenu> <VnTable
<template #right-panel> ref="tableRef"
<CustomerFilter data-key="CustomerList" /> data-key="Customer"
</template> url="Clients/extendedListFilter"
</RightMenu> :create="{
<QPage class="column items-center q-pa-md"> urlCreate: 'Clients/createWithUser',
<div class="vn-card-list"> title: 'Create client',
<VnPaginate onDataSaved: ({ id }) => tableRef.redirect(id),
auto-load formInitialData: {
data-key="CustomerList" active: true,
isEqualizated: false,
},
}"
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

@ -22,7 +22,7 @@ const balanceDueTotal = ref(0);
const selected = ref([]); const selected = ref([]);
const tableColumnComponents = { const tableColumnComponents = {
client: { clientFk: {
component: QBtn, component: QBtn,
props: () => ({ flat: true, class: 'link', noCaps: true }), props: () => ({ flat: true, class: 'link', noCaps: true }),
event: () => {}, event: () => {},
@ -40,7 +40,7 @@ const tableColumnComponents = {
props: () => ({ flat: true, class: 'link', noCaps: true }), props: () => ({ flat: true, class: 'link', noCaps: true }),
event: () => {}, event: () => {},
}, },
department: { departmentName: {
component: 'span', component: 'span',
props: () => {}, props: () => {},
event: () => {}, event: () => {},
@ -102,12 +102,12 @@ const columns = computed(() => [
align: 'left', align: 'left',
field: 'clientName', field: 'clientName',
label: t('Client'), label: t('Client'),
name: 'client', name: 'clientFk',
sortable: true, sortable: true,
}, },
{ {
align: 'left', align: 'left',
field: 'isWorker', field: ({ isWorker }) => Boolean(isWorker),
label: t('Is worker'), label: t('Is worker'),
name: 'isWorker', name: 'isWorker',
}, },
@ -122,7 +122,7 @@ const columns = computed(() => [
align: 'left', align: 'left',
field: 'departmentName', field: 'departmentName',
label: t('Department'), label: t('Department'),
name: 'department', name: 'departmentName',
sortable: true, sortable: true,
}, },
{ {
@ -204,48 +204,24 @@ const viewAddObservation = (rowsSelected) => {
}); });
}; };
const departments = ref(new Map());
const onFetch = async (data) => { const onFetch = async (data) => {
const salesPersonFks = data.map((item) => item.salesPersonFk);
const departmentNames = salesPersonFks.map(async (salesPersonFk) => {
try {
const { data: workerDepartment } = await axios.get(
`WorkerDepartments/${salesPersonFk}`
);
const { data: department } = await axios.get(
`Departments/${workerDepartment.departmentFk}`
);
departments.value.set(salesPersonFk, department.name);
} catch (error) {
console.error('Err: ', error);
}
});
const recoveryData = await axios.get('Recoveries'); const recoveryData = await axios.get('Recoveries');
const recoveries = recoveryData.data.map(({ clientFk, finished }) => ({ const recoveries = recoveryData.data.map(({ clientFk, finished }) => ({
clientFk, clientFk,
finished, finished,
})); }));
await Promise.all(departmentNames);
data.forEach((item) => { data.forEach((item) => {
item.departmentName = departments.value.get(item.salesPersonFk);
item.isWorker = item.businessTypeFk === 'worker';
const recovery = recoveries.find(({ clientFk }) => clientFk === item.clientFk); const recovery = recoveries.find(({ clientFk }) => clientFk === item.clientFk);
item.finished = recovery?.finished === null; item.finished = recovery?.finished === null;
}); });
for (const element of data) element.isWorker = element.businessTypeFk === 'worker';
balanceDueTotal.value = data.reduce((acc, { amount = 0 }) => acc + amount, 0); balanceDueTotal.value = data.reduce((acc, { amount = 0 }) => acc + amount, 0);
}; };
function exprBuilder(param, value) { function exprBuilder(param, value) {
switch (param) { switch (param) {
case 'clientFk': case 'clientFk':
return { [`d.${param}`]: value?.id }; return { [`d.${param}`]: value };
case 'creditInsurance': case 'creditInsurance':
case 'amount': case 'amount':
case 'workerFk': case 'workerFk':

View File

@ -1,7 +1,6 @@
<script setup> <script setup>
import { ref } from 'vue'; import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
@ -16,14 +15,13 @@ const props = defineProps({
}, },
}); });
const clients = ref();
const salespersons = ref(); const salespersons = ref();
const countries = ref(); const countries = ref();
const authors = ref(); const authors = ref();
const departments = ref();
</script> </script>
<template> <template>
<FetchData @on-fetch="(data) => (clients = data)" auto-load url="Clients" />
<FetchData <FetchData
:filter="{ where: { role: 'salesPerson' } }" :filter="{ where: { role: 'salesPerson' } }"
@on-fetch="(data) => (salespersons = data)" @on-fetch="(data) => (salespersons = data)"
@ -36,6 +34,7 @@ const authors = ref();
auto-load auto-load
url="Workers/activeWithInheritedRole" url="Workers/activeWithInheritedRole"
/> />
<FetchData @on-fetch="(data) => (departments = data)" auto-load url="Departments" />
<VnFilterPanel :data-key="props.dataKey" :search-button="true"> <VnFilterPanel :data-key="props.dataKey" :search-button="true">
<template #tags="{ tag, formatFn }"> <template #tags="{ tag, formatFn }">
@ -47,29 +46,22 @@ const authors = ref();
<template #body="{ params, searchFn }"> <template #body="{ params, searchFn }">
<QItem class="q-mb-sm"> <QItem class="q-mb-sm">
<QItemSection v-if="clients">
<VnSelect <VnSelect
:label="t('Client')" :label="t('Client')"
:options="clients" url="Clients"
dense dense
emit-value
hide-selected
map-options
option-label="name" option-label="name"
option-value="id" option-value="id"
outlined outlined
rounded rounded
use-input emit-value
hide-selected
map-options
v-model="params.clientFk" v-model="params.clientFk"
use-input
@update:model-value="searchFn()" @update:model-value="searchFn()"
auto-load
/> />
</QItemSection>
<QItemSection v-else>
<QSkeleton class="full-width" type="QInput" />
</QItemSection>
</QItem> </QItem>
<QItem class="q-mb-sm"> <QItem class="q-mb-sm">
<QItemSection v-if="salespersons"> <QItemSection v-if="salespersons">
<VnSelect <VnSelect
@ -93,6 +85,29 @@ const authors = ref();
<QSkeleton class="full-width" type="QInput" /> <QSkeleton class="full-width" type="QInput" />
</QItemSection> </QItemSection>
</QItem> </QItem>
<QItem class="q-mb-sm">
<QItemSection v-if="departments">
<VnSelect
:input-debounce="0"
:label="t('Departments')"
:options="departments"
dense
emit-value
hide-selected
map-options
option-label="name"
option-value="id"
outlined
rounded
use-input
v-model="params.departmentFk"
@update:model-value="searchFn()"
/>
</QItemSection>
<QItemSection v-else>
<QSkeleton class="full-width" type="QInput" />
</QItemSection>
</QItem>
<QItem class="q-mb-sm"> <QItem class="q-mb-sm">
<QItemSection v-if="countries"> <QItemSection v-if="countries">

View File

@ -1,619 +0,0 @@
<script setup>
import { ref, computed, onBeforeMount, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { QBtn, QIcon } from 'quasar';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue';
import CustomerExtendedListActions from './CustomerExtendedListActions.vue';
import CustomerExtendedListFilter from './CustomerExtendedListFilter.vue';
import TableVisibleColumns from 'src/components/common/TableVisibleColumns.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
import { useArrayData } from 'composables/useArrayData';
import { useStateStore } from 'stores/useStateStore';
import { toDate } from 'src/filters';
import RightMenu from 'src/components/common/RightMenu.vue';
const { t } = useI18n();
const router = useRouter();
const stateStore = useStateStore();
const arrayData = ref(null);
onBeforeMount(async () => {
arrayData.value = useArrayData('CustomerExtendedList', {
url: 'Clients/extendedListFilter',
limit: 0,
});
await arrayData.value.fetch({ append: false });
stateStore.rightDrawer = true;
});
onMounted(() => {
const filteredColumns = columns.value.filter(
(col) => col.name !== 'actions' && col.name !== 'customerStatus'
);
allColumnNames.value = filteredColumns.map((col) => col.name);
});
const selectedCustomerId = ref(0);
const selectedSalesPersonId = ref(0);
const allColumnNames = ref([]);
const visibleColumns = ref([]);
const tableColumnComponents = {
customerStatus: {
component: QIcon,
props: (prop) => ({
name: !prop.row.isActive
? 'vn:disabled'
: prop.row.isActive && prop.row.isFreezed
? 'vn:frozen'
: '',
color: 'primary',
size: 'sm',
}),
event: () => {},
},
id: {
component: QBtn,
props: () => ({ flat: true, color: 'blue' }),
event: (prop) => {
selectCustomerId(prop.row.id);
},
},
name: {
component: 'span',
props: () => {},
event: () => {},
},
socialName: {
component: 'span',
props: () => {},
event: () => {},
},
fi: {
component: 'span',
props: () => {},
event: () => {},
},
salesPersonFk: {
component: QBtn,
props: () => ({ flat: true, color: 'blue' }),
event: (prop) => selectSalesPersonId(prop.row.salesPersonFk),
},
credit: {
component: 'span',
props: () => {},
event: () => {},
},
creditInsurance: {
component: 'span',
props: () => {},
event: () => {},
},
phone: {
component: 'span',
props: () => {},
event: () => {},
},
mobile: {
component: 'span',
props: () => {},
event: () => {},
},
street: {
component: 'span',
props: () => {},
event: () => {},
},
countryFk: {
component: 'span',
props: () => {},
event: () => {},
},
provinceFk: {
component: 'span',
props: () => {},
event: () => {},
},
city: {
component: 'span',
props: () => {},
event: () => {},
},
postcode: {
component: 'span',
props: () => {},
event: () => {},
},
email: {
component: 'span',
props: () => {},
event: () => {},
},
created: {
component: 'span',
props: () => {},
event: () => {},
},
businessTypeFk: {
component: 'span',
props: () => {},
event: () => {},
},
payMethodFk: {
component: 'span',
props: () => {},
event: () => {},
},
sageTaxTypeFk: {
component: 'span',
props: () => {},
event: () => {},
},
sageTransactionTypeFk: {
component: 'span',
props: () => {},
event: () => {},
},
isActive: {
component: QIcon,
props: (prop) => ({
name: prop.row.isActive ? 'check' : 'close',
color: prop.row.isActive ? 'positive' : 'negative',
size: 'sm',
}),
event: () => {},
},
isVies: {
component: QIcon,
props: (prop) => ({
name: prop.row.isVies ? 'check' : 'close',
color: prop.row.isVies ? 'positive' : 'negative',
size: 'sm',
}),
event: () => {},
},
isTaxDataChecked: {
component: QIcon,
props: (prop) => ({
name: prop.row.isTaxDataChecked ? 'check' : 'close',
color: prop.row.isTaxDataChecked ? 'positive' : 'negative',
size: 'sm',
}),
event: () => {},
},
isEqualizated: {
component: QIcon,
props: (prop) => ({
name: prop.row.isEqualizated ? 'check' : 'close',
color: prop.row.isEqualizated ? 'positive' : 'negative',
size: 'sm',
}),
event: () => {},
},
isFreezed: {
component: QIcon,
props: (prop) => ({
name: prop.row.isFreezed ? 'check' : 'close',
color: prop.row.isFreezed ? 'positive' : 'negative',
size: 'sm',
}),
event: () => {},
},
hasToInvoice: {
component: QIcon,
props: (prop) => ({
name: prop.row.hasToInvoice ? 'check' : 'close',
color: prop.row.hasToInvoice ? 'positive' : 'negative',
size: 'sm',
}),
event: () => {},
},
hasToInvoiceByAddress: {
component: QIcon,
props: (prop) => ({
name: prop.row.hasToInvoiceByAddress ? 'check' : 'close',
color: prop.row.hasToInvoiceByAddress ? 'positive' : 'negative',
size: 'sm',
}),
event: () => {},
},
isToBeMailed: {
component: QIcon,
props: (prop) => ({
name: prop.row.isToBeMailed ? 'check' : 'close',
color: prop.row.isToBeMailed ? 'positive' : 'negative',
size: 'sm',
}),
event: () => {},
},
hasLcr: {
component: QIcon,
props: (prop) => ({
name: prop.row.hasLcr ? 'check' : 'close',
color: prop.row.hasLcr ? 'positive' : 'negative',
size: 'sm',
}),
event: () => {},
},
hasCoreVnl: {
component: QIcon,
props: (prop) => ({
name: prop.row.hasCoreVnl ? 'check' : 'close',
color: prop.row.hasCoreVnl ? 'positive' : 'negative',
size: 'sm',
}),
event: () => {},
},
hasSepaVnl: {
component: QIcon,
props: (prop) => ({
name: prop.row.hasSepaVnl ? 'check' : 'close',
color: prop.row.hasSepaVnl ? 'positive' : 'negative',
size: 'sm',
}),
event: () => {},
},
actions: {
component: CustomerExtendedListActions,
props: (prop) => ({
id: prop.row.id,
}),
event: () => {},
},
};
const columns = computed(() => [
{
align: 'left',
field: '',
label: '',
name: 'customerStatus',
format: () => ' ',
},
{
align: 'left',
field: 'id',
label: t('customer.extendedList.tableVisibleColumns.id'),
name: 'id',
},
{
align: 'left',
field: 'name',
label: t('customer.extendedList.tableVisibleColumns.name'),
name: 'name',
},
{
align: 'left',
field: 'socialName',
label: t('customer.extendedList.tableVisibleColumns.socialName'),
name: 'socialName',
},
{
align: 'left',
field: 'fi',
label: t('customer.extendedList.tableVisibleColumns.fi'),
name: 'fi',
},
{
align: 'left',
field: 'salesPerson',
label: t('customer.extendedList.tableVisibleColumns.salesPersonFk'),
name: 'salesPersonFk',
},
{
align: 'left',
field: 'credit',
label: t('customer.extendedList.tableVisibleColumns.credit'),
name: 'credit',
},
{
align: 'left',
field: 'creditInsurance',
label: t('customer.extendedList.tableVisibleColumns.creditInsurance'),
name: 'creditInsurance',
},
{
align: 'left',
field: 'phone',
label: t('customer.extendedList.tableVisibleColumns.phone'),
name: 'phone',
},
{
align: 'left',
field: 'mobile',
label: t('customer.extendedList.tableVisibleColumns.mobile'),
name: 'mobile',
},
{
align: 'left',
field: 'street',
label: t('customer.extendedList.tableVisibleColumns.street'),
name: 'street',
},
{
align: 'left',
field: 'country',
label: t('customer.extendedList.tableVisibleColumns.countryFk'),
name: 'countryFk',
},
{
align: 'left',
field: 'province',
label: t('customer.extendedList.tableVisibleColumns.provinceFk'),
name: 'provinceFk',
},
{
align: 'left',
field: 'city',
label: t('customer.extendedList.tableVisibleColumns.city'),
name: 'city',
},
{
align: 'left',
field: 'postcode',
label: t('customer.extendedList.tableVisibleColumns.postcode'),
name: 'postcode',
},
{
align: 'left',
field: 'email',
label: t('customer.extendedList.tableVisibleColumns.email'),
name: 'email',
},
{
align: 'left',
field: 'created',
label: t('customer.extendedList.tableVisibleColumns.created'),
name: 'created',
format: (value) => toDate(value),
},
{
align: 'left',
field: 'businessType',
label: t('customer.extendedList.tableVisibleColumns.businessTypeFk'),
name: 'businessTypeFk',
},
{
align: 'left',
field: 'payMethod',
label: t('customer.extendedList.tableVisibleColumns.payMethodFk'),
name: 'payMethodFk',
},
{
align: 'left',
field: 'sageTaxType',
label: t('customer.extendedList.tableVisibleColumns.sageTaxTypeFk'),
name: 'sageTaxTypeFk',
},
{
align: 'left',
field: 'sageTransactionType',
label: t('customer.extendedList.tableVisibleColumns.sageTransactionTypeFk'),
name: 'sageTransactionTypeFk',
},
{
align: 'left',
field: 'isActive',
label: t('customer.extendedList.tableVisibleColumns.isActive'),
name: 'isActive',
format: () => ' ',
},
{
align: 'left',
field: 'isVies',
label: t('customer.extendedList.tableVisibleColumns.isVies'),
name: 'isVies',
format: () => ' ',
},
{
align: 'left',
field: 'isTaxDataChecked',
label: t('customer.extendedList.tableVisibleColumns.isTaxDataChecked'),
name: 'isTaxDataChecked',
format: () => ' ',
},
{
align: 'left',
field: 'isEqualizated',
label: t('customer.extendedList.tableVisibleColumns.isEqualizated'),
name: 'isEqualizated',
format: () => ' ',
},
{
align: 'left',
field: 'isFreezed',
label: t('customer.extendedList.tableVisibleColumns.isFreezed'),
name: 'isFreezed',
format: () => ' ',
},
{
align: 'left',
field: 'hasToInvoice',
label: t('customer.extendedList.tableVisibleColumns.hasToInvoice'),
name: 'hasToInvoice',
format: () => ' ',
},
{
align: 'left',
field: 'hasToInvoiceByAddress',
label: t('customer.extendedList.tableVisibleColumns.hasToInvoiceByAddress'),
name: 'hasToInvoiceByAddress',
format: () => ' ',
},
{
align: 'left',
field: 'isToBeMailed',
label: t('customer.extendedList.tableVisibleColumns.isToBeMailed'),
name: 'isToBeMailed',
format: () => ' ',
},
{
align: 'left',
field: 'hasLcr',
label: t('customer.extendedList.tableVisibleColumns.hasLcr'),
name: 'hasLcr',
format: () => ' ',
},
{
align: 'left',
field: 'hasCoreVnl',
label: t('customer.extendedList.tableVisibleColumns.hasCoreVnl'),
name: 'hasCoreVnl',
format: () => ' ',
},
{
align: 'left',
field: 'hasSepaVnl',
label: t('customer.extendedList.tableVisibleColumns.hasSepaVnl'),
name: 'hasSepaVnl',
format: () => ' ',
},
{
align: 'right',
field: 'actions',
label: '',
name: 'actions',
},
]);
const stopEventPropagation = (event, col) => {
if (!['id', 'salesPersonFk'].includes(col.name)) return;
event.preventDefault();
event.stopPropagation();
};
const navigateToTravelId = (id) => router.push({ path: `/customer/${id}` });
const selectCustomerId = (id) => (selectedCustomerId.value = id);
const selectSalesPersonId = (id) => (selectedSalesPersonId.value = id);
</script>
<template>
<RightMenu>
<template #right-panel>
<CustomerExtendedListFilter
v-if="visibleColumns.length !== 0"
data-key="CustomerExtendedList"
:visible-columns="visibleColumns"
/>
</template>
</RightMenu>
<VnSubToolbar>
<template #st-data>
<TableVisibleColumns
:all-columns="allColumnNames"
table-code="clientsDetail"
labels-traductions-path="customer.extendedList.tableVisibleColumns"
@on-config-saved="
visibleColumns = ['customerStatus', ...$event, 'actions']
"
/>
</template>
</VnSubToolbar>
<QPage class="column items-center q-pa-md">
<VnPaginate
data-key="CustomerExtendedList"
url="Clients/extendedListFilter"
auto-load
>
<template #body="{ rows }">
<div class="q-pa-md">
<QTable
:columns="columns"
:rows="rows"
class="full-width q-mt-md"
row-key="id"
:visible-columns="visibleColumns"
@row-click="(evt, row, id) => navigateToTravelId(row.id)"
>
<template #body-cell="{ col, value }">
<QTd @click="stopEventPropagation($event, col)">
{{ value }}
</QTd>
</template>
<template #body-cell-customerStatus="props">
<QTd @click="stopEventPropagation($event, props.col)">
<component
:is="tableColumnComponents[props.col.name].component"
class="col-content"
v-bind="
tableColumnComponents[props.col.name].props(props)
"
@click="
tableColumnComponents[props.col.name].event(props)
"
>
</component>
</QTd>
</template>
<template #body-cell-id="props">
<QTd @click="stopEventPropagation($event, props.col)">
<component
:is="tableColumnComponents[props.col.name].component"
class="col-content"
v-bind="
tableColumnComponents[props.col.name].props(props)
"
@click="
tableColumnComponents[props.col.name].event(props)
"
>
<CustomerDescriptorProxy :id="props.row.id" />
{{ props.row.id }}
</component>
</QTd>
</template>
<template #body-cell-salesPersonFk="props">
<QTd @click="stopEventPropagation($event, props.col)">
<component
v-if="props.row.salesPerson"
class="col-content"
:is="tableColumnComponents[props.col.name].component"
v-bind="
tableColumnComponents[props.col.name].props(props)
"
@click="
tableColumnComponents[props.col.name].event(props)
"
>
<WorkerDescriptorProxy
:id="props.row.salesPersonFk"
/>
{{ props.row.salesPerson }}
</component>
<span class="col-content" v-else>-</span>
</QTd>
</template>
<template #body-cell-actions="props">
<QTd @click="stopEventPropagation($event, props.col)">
<component
:is="tableColumnComponents[props.col.name].component"
class="col-content"
v-bind="
tableColumnComponents[props.col.name].props(props)
"
@click="
tableColumnComponents[props.col.name].event(props)
"
/>
</QTd>
</template>
</QTable>
</div>
</template>
</VnPaginate>
</QPage>
</template>
<style lang="scss" scoped>
.col-content {
border-radius: 4px;
padding: 6px;
}
</style>

View File

@ -1,60 +0,0 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import CustomerSummary from '../Card/CustomerSummary.vue';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
const { t } = useI18n();
const router = useRouter();
const { viewSummary } = useSummaryDialog();
const $props = defineProps({
id: {
type: Number,
required: true,
},
});
const redirectToCreateView = () => {
router.push({
name: 'TicketList',
query: {
params: JSON.stringify({
clientFk: $props.id,
}),
},
});
};
</script>
<template>
<div>
<QIcon
@click.stop="redirectToCreateView"
color="primary"
name="vn:ticket"
size="sm"
>
<QTooltip>
{{ t('Client ticket list') }}
</QTooltip>
</QIcon>
<QIcon
@click.stop="viewSummary($props.id, CustomerSummary)"
class="q-ml-md"
color="primary"
name="preview"
size="sm"
>
<QTooltip>
{{ t('Preview') }}
</QTooltip>
</QIcon>
</div>
</template>
<i18n>
es:
Client ticket list: Listado de tickets del cliente
Preview: Vista previa
</i18n>

View File

@ -1,571 +0,0 @@
<script setup>
import { ref, computed } 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 VnInputDate from 'components/common/VnInputDate.vue';
import { dateRange } from 'src/filters';
const props = defineProps({
dataKey: {
type: String,
required: true,
},
visibleColumns: {
type: Array,
required: true,
},
});
const { t } = useI18n();
const clients = ref();
const workers = ref();
const countriesOptions = ref([]);
const provincesOptions = ref([]);
const paymethodsOptions = ref([]);
const businessTypesOptions = ref([]);
const sageTaxTypesOptions = ref([]);
const sageTransactionTypesOptions = ref([]);
const visibleColumnsSet = computed(() => new Set(props.visibleColumns));
const exprBuilder = (param, value) => {
switch (param) {
case 'created':
return {
'c.created': {
between: dateRange(value),
},
};
case 'id':
case 'name':
case 'socialName':
case 'fi':
case 'credit':
case 'creditInsurance':
case 'phone':
case 'mobile':
case 'street':
case 'city':
case 'postcode':
case 'email':
case 'isActive':
case 'isVies':
case 'isTaxDataChecked':
case 'isEqualizated':
case 'isFreezed':
case 'hasToInvoice':
case 'hasToInvoiceByAddress':
case 'isToBeMailed':
case 'hasSepaVnl':
case 'hasLcr':
case 'hasCoreVnl':
case 'countryFk':
case 'provinceFk':
case 'salesPersonFk':
case 'businessTypeFk':
case 'payMethodFk':
case 'sageTaxTypeFk':
case 'sageTransactionTypeFk':
return { [`c.${param}`]: value };
}
};
const shouldRenderColumn = (colName) => {
return visibleColumnsSet.value.has(colName);
};
</script>
<template>
<FetchData
url="Clients"
:filter="{ where: { role: 'socialName' } }"
@on-fetch="(data) => (clients = data)"
auto-load
/>
<FetchData
url="Workers/activeWithInheritedRole"
:filter="{ where: { role: 'salesPerson' } }"
@on-fetch="(data) => (workers = data)"
auto-load
/>
<FetchData
url="Workers/activeWithInheritedRole"
:filter="{ where: { role: 'salesPerson' } }"
@on-fetch="(data) => (workers = data)"
auto-load
/>
<FetchData
url="Countries"
:filter="{ fields: ['id', 'country'], order: 'country ASC' }"
@on-fetch="(data) => (countriesOptions = data)"
auto-load
/>
<FetchData
ref="provincesFetchDataRef"
@on-fetch="(data) => (provincesOptions = data)"
auto-load
url="Provinces"
/>
<FetchData
url="Paymethods"
@on-fetch="(data) => (paymethodsOptions = data)"
auto-load
/>
<FetchData
url="BusinessTypes"
@on-fetch="(data) => (businessTypesOptions = data)"
auto-load
/>
<FetchData
url="SageTaxTypes"
auto-load
@on-fetch="(data) => (sageTaxTypesOptions = data)"
/>
<FetchData
url="sageTransactionTypes"
auto-load
@on-fetch="(data) => (sageTransactionTypesOptions = data)"
/>
<VnFilterPanel
:data-key="props.dataKey"
:search-button="true"
:expr-builder="exprBuilder"
>
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
<strong
>{{ t(`customer.extendedList.tableVisibleColumns.${tag.label}`) }}:
</strong>
<span>{{ formatFn(tag.value) }}</span>
</div>
</template>
<template #body="{ params, searchFn }">
<QItem v-if="shouldRenderColumn('id')">
<QItemSection>
<VnInput
:label="t('customer.extendedList.tableVisibleColumns.id')"
v-model="params.id"
is-outlined
clearable
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('name')">
<QItemSection>
<VnInput
:label="t('customer.extendedList.tableVisibleColumns.name')"
v-model="params.name"
is-outlined
/>
</QItemSection>
</QItem>
<!-- <QItem class="q-mb-sm">
<QItemSection v-if="!clients">
<QSkeleton type="QInput" class="full-width" />
</QItemSection>
<QItemSection v-if="clients">
<VnSelect
:label="t('Social name')"
v-model="params.socialName"
@update:model-value="searchFn()"
:options="clients"
option-value="socialName"
option-label="socialName"
emit-value
map-options
use-input
hide-selected
dense
outlined
rounded
:input-debounce="0"
/>
</QItemSection>
</QItem> -->
<QItem v-if="shouldRenderColumn('fi')">
<QItemSection>
<VnInput
:label="t('customer.extendedList.tableVisibleColumns.fi')"
v-model="params.fi"
is-outlined
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('salesPersonFk')">
<QItemSection v-if="!workers">
<QSkeleton type="QInput" class="full-width" />
</QItemSection>
<QItemSection v-if="workers">
<VnSelect
:label="
t('customer.extendedList.tableVisibleColumns.salesPersonFk')
"
v-model="params.salesPersonFk"
@update:model-value="searchFn()"
:options="workers"
option-value="id"
option-label="name"
emit-value
map-options
use-input
hide-selected
dense
outlined
rounded
:input-debounce="0"
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('credit')">
<QItemSection>
<VnInput
:label="t('customer.extendedList.tableVisibleColumns.credit')"
v-model="params.credit"
is-outlined
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('creditInsurance')">
<QItemSection>
<VnInput
:label="
t('customer.extendedList.tableVisibleColumns.creditInsurance')
"
v-model="params.creditInsurance"
is-outlined
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('phone')">
<QItemSection>
<VnInput
:label="t('customer.extendedList.tableVisibleColumns.phone')"
v-model="params.phone"
is-outlined
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('mobile')">
<QItemSection>
<VnInput
:label="t('customer.extendedList.tableVisibleColumns.mobile')"
v-model="params.mobile"
is-outlined
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('street')">
<QItemSection>
<VnInput
:label="t('customer.extendedList.tableVisibleColumns.street')"
v-model="params.street"
is-outlined
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('countryFk')">
<QItemSection>
<VnSelect
:label="t('customer.extendedList.tableVisibleColumns.countryFk')"
v-model="params.countryFk"
@update:model-value="searchFn()"
:options="countriesOptions"
option-value="id"
option-label="country"
map-options
hide-selected
dense
outlined
rounded
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('provinceFk')">
<QItemSection>
<VnSelect
:label="t('customer.extendedList.tableVisibleColumns.provinceFk')"
v-model="params.provinceFk"
@update:model-value="searchFn()"
:options="provincesOptions"
option-value="id"
option-label="name"
map-options
hide-selected
dense
outlined
rounded
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('city')">
<QItemSection>
<VnInput
:label="t('customer.extendedList.tableVisibleColumns.city')"
v-model="params.city"
is-outlined
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('postcode')">
<QItemSection>
<VnInput
:label="t('customer.extendedList.tableVisibleColumns.postcode')"
v-model="params.postcode"
is-outlined
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('email')">
<QItemSection>
<VnInput
:label="t('customer.extendedList.tableVisibleColumns.email')"
v-model="params.email"
is-outlined
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('created')">
<QItemSection>
<VnInputDate
v-model="params.created"
:label="t('customer.extendedList.tableVisibleColumns.created')"
@update:model-value="searchFn()"
is-outlined
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('businessTypeFk')">
<QItemSection>
<VnSelect
:label="
t('customer.extendedList.tableVisibleColumns.businessTypeFk')
"
v-model="params.businessTypeFk"
:options="businessTypesOptions"
@update:model-value="searchFn()"
option-value="code"
option-label="description"
map-options
hide-selected
dense
outlined
rounded
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('payMethodFk')">
<QItemSection>
<VnSelect
:label="
t('customer.extendedList.tableVisibleColumns.payMethodFk')
"
v-model="params.payMethodFk"
:options="paymethodsOptions"
@update:model-value="searchFn()"
option-value="id"
option-label="name"
map-options
hide-selected
dense
outlined
rounded
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('sageTaxTypeFk')">
<QItemSection>
<VnSelect
:label="
t('customer.extendedList.tableVisibleColumns.sageTaxTypeFk')
"
v-model="params.sageTaxTypeFk"
@update:model-value="searchFn()"
:options="sageTaxTypesOptions"
option-value="id"
option-label="vat"
map-options
hide-selected
dense
outlined
rounded
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('sageTransactionTypeFk')">
<QItemSection>
<VnSelect
:label="
t(
'customer.extendedList.tableVisibleColumns.sageTransactionTypeFk'
)
"
v-model="params.sageTransactionTypeFk"
@update:model-value="searchFn()"
:options="sageTransactionTypesOptions"
option-value="id"
option-label="transaction"
map-options
hide-selected
dense
outlined
rounded
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('isActive') || shouldRenderColumn('isVies')">
<QItemSection v-if="shouldRenderColumn('isActive')">
<QCheckbox
v-model="params.isActive"
@update:model-value="searchFn()"
:label="t('customer.extendedList.tableVisibleColumns.isActive')"
toggle-indeterminate
:false-value="undefined"
/>
</QItemSection>
<QItemSection v-if="shouldRenderColumn('isVies')">
<QCheckbox
v-model="params.isVies"
@update:model-value="searchFn()"
:label="t('customer.extendedList.tableVisibleColumns.isVies')"
toggle-indeterminate
:false-value="undefined"
/>
</QItemSection>
</QItem>
<QItem
v-if="
shouldRenderColumn('isEqualizated') ||
shouldRenderColumn('isTaxDataChecked')
"
>
<QItemSection v-if="shouldRenderColumn('isTaxDataChecked')">
<QCheckbox
v-model="params.isTaxDataChecked"
@update:model-value="searchFn()"
:label="
t(
'customer.extendedList.tableVisibleColumns.isTaxDataChecked'
)
"
toggle-indeterminate
:false-value="undefined"
/>
</QItemSection>
<QItemSection v-if="shouldRenderColumn('isEqualizated')">
<QCheckbox
v-model="params.isEqualizated"
@update:model-value="searchFn()"
:label="
t('customer.extendedList.tableVisibleColumns.isEqualizated')
"
toggle-indeterminate
:false-value="undefined"
/>
</QItemSection>
</QItem>
<QItem
v-if="
shouldRenderColumn('hasToInvoice') || shouldRenderColumn('isFreezed')
"
>
<QItemSection v-if="shouldRenderColumn('isFreezed')">
<QCheckbox
v-model="params.isFreezed"
@update:model-value="searchFn()"
:label="t('customer.extendedList.tableVisibleColumns.isFreezed')"
toggle-indeterminate
:false-value="undefined"
/>
</QItemSection>
<QItemSection v-if="shouldRenderColumn('hasToInvoice')">
<QCheckbox
v-model="params.hasToInvoice"
@update:model-value="searchFn()"
:label="
t('customer.extendedList.tableVisibleColumns.hasToInvoice')
"
toggle-indeterminate
:false-value="undefined"
/>
</QItemSection>
</QItem>
<QItem
v-if="
shouldRenderColumn('isToBeMailed') ||
shouldRenderColumn('hasToInvoiceByAddress')
"
>
<QItemSection v-if="shouldRenderColumn('hasToInvoiceByAddress')">
<QCheckbox
v-model="params.hasToInvoiceByAddress"
@update:model-value="searchFn()"
:label="
t(
'customer.extendedList.tableVisibleColumns.hasToInvoiceByAddress'
)
"
toggle-indeterminate
:false-value="undefined"
/>
</QItemSection>
<QItemSection v-if="shouldRenderColumn('isToBeMailed')">
<QCheckbox
v-model="params.isToBeMailed"
@update:model-value="searchFn()"
:label="
t('customer.extendedList.tableVisibleColumns.isToBeMailed')
"
toggle-indeterminate
:false-value="undefined"
/>
</QItemSection>
</QItem>
<QItem
v-if="shouldRenderColumn('hasLcr') || shouldRenderColumn('hasCoreVnl')"
>
<QItemSection v-if="shouldRenderColumn('hasLcr')">
<QCheckbox
v-model="params.hasLcr"
@update:model-value="searchFn()"
:label="t('customer.extendedList.tableVisibleColumns.hasLcr')"
toggle-indeterminate
:false-value="undefined"
/>
</QItemSection>
<QItemSection v-if="shouldRenderColumn('hasCoreVnl')">
<QCheckbox
v-model="params.hasCoreVnl"
@update:model-value="searchFn()"
:label="t('customer.extendedList.tableVisibleColumns.hasCoreVnl')"
toggle-indeterminate
:false-value="undefined"
/>
</QItemSection>
</QItem>
<QItem v-if="shouldRenderColumn('hasSepaVnl')">
<QItemSection>
<QCheckbox
v-model="params.hasSepaVnl"
@update:model-value="searchFn()"
:label="t('customer.extendedList.tableVisibleColumns.hasSepaVnl')"
toggle-indeterminate
:false-value="undefined"
/>
</QItemSection>
</QItem>
<QSeparator />
</template>
</VnFilterPanel>
</template>
<i18n>
es:
Social name: Razón social
</i18n>

View File

@ -9,6 +9,7 @@ import useNotify from 'src/composables/useNotify';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import FetchData from 'src/components/FetchData.vue';
const { dialogRef } = useDialogPluginComponent(); const { dialogRef } = useDialogPluginComponent();
const { notify } = useNotify(); const { notify } = useNotify();
@ -19,15 +20,12 @@ const $props = defineProps({
type: String, type: String,
required: true, required: true,
}, },
userPasswords: {
type: Object,
required: true,
},
promise: { promise: {
type: Function, type: Function,
required: true, required: true,
}, },
}); });
const userPasswords = ref({});
const closeButton = ref(null); const closeButton = ref(null);
const isLoading = ref(false); const isLoading = ref(false);
@ -60,6 +58,11 @@ const onSubmit = async () => {
<template> <template>
<QDialog ref="dialogRef"> <QDialog ref="dialogRef">
<FetchData
@on-fetch="(data) => (userPasswords = data[0])"
auto-load
url="UserPasswords"
/>
<QCard class="q-pa-lg"> <QCard class="q-pa-lg">
<QCardSection> <QCardSection>
<QForm @submit.prevent="onSubmit"> <QForm @submit.prevent="onSubmit">
@ -71,7 +74,7 @@ const onSubmit = async () => {
<QIcon name="close" size="sm" /> <QIcon name="close" size="sm" />
</span> </span>
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md" style="flex-direction: column">
<div class="col"> <div class="col">
<VnInput <VnInput
:label="t('New password')" :label="t('New password')"
@ -84,11 +87,7 @@ const onSubmit = async () => {
<QTooltip> <QTooltip>
{{ {{
t('customer.card.passwordRequirements', { t('customer.card.passwordRequirements', {
length: $props.userPasswords.length, ...userPasswords,
nAlpha: $props.userPasswords.nAlpha,
nDigits: $props.userPasswords.nDigits,
nPunct: $props.userPasswords.nPunct,
nUpper: $props.userPasswords.nUpper,
}) })
}} }}
</QTooltip> </QTooltip>

View File

@ -162,6 +162,7 @@ const navigateToticketSummary = (id) => {
params: { id }, params: { id },
}); });
}; };
const commonColumns = (col) => ['date', 'state', 'total'].includes(col);
</script> </script>
<template> <template>
@ -171,13 +172,13 @@ const navigateToticketSummary = (id) => {
auto-load auto-load
url="Tickets" url="Tickets"
/> />
<QCard class="vn-one q-py-sm flex justify-between">
<QTable <QTable
:columns="columns" :columns="columns"
:pagination="{ rowsPerPage: 12 }" :pagination="{ rowsPerPage: 12 }"
:rows="rows" :rows="rows"
class="full-width q-mt-md" class="full-width"
row-key="id" row-key="id"
v-if="rows?.length"
> >
<template #body-cell="props"> <template #body-cell="props">
<QTd :props="props" @click="navigateToticketSummary(props.row.id)"> <QTd :props="props" @click="navigateToticketSummary(props.row.id)">
@ -185,19 +186,19 @@ const navigateToticketSummary = (id) => {
<component <component
:is="tableColumnComponents[props.col.name].component" :is="tableColumnComponents[props.col.name].component"
@click="tableColumnComponents[props.col.name].event(props)" @click="tableColumnComponents[props.col.name].event(props)"
class="rounded-borders q-pa-sm" class="rounded-borders"
v-bind="tableColumnComponents[props.col.name].props(props)" v-bind="tableColumnComponents[props.col.name].props(props)"
> >
<template <template v-if="!commonColumns(props.col.name)">
v-if=" <span
props.col.name === 'id' || :class="{
props.col.name === 'nickname' || link:
props.col.name === 'agency' ||
props.col.name === 'route' || props.col.name === 'route' ||
props.col.name === 'packages' props.col.name === 'nickname',
" }"
> >
{{ props.value }} {{ props.value }}
</span>
</template> </template>
<template v-if="props.col.name === 'date'"> <template v-if="props.col.name === 'date'">
<QBadge class="q-pa-sm" color="warning"> <QBadge class="q-pa-sm" color="warning">
@ -232,6 +233,7 @@ const navigateToticketSummary = (id) => {
</QTd> </QTd>
</template> </template>
</QTable> </QTable>
</QCard>
</template> </template>
<i18n> <i18n>

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import DepartmentDescriptor from './DepartmentDescriptor.vue'; import DepartmentDescriptor from './DepartmentDescriptor.vue';
import DepartmentSummaryDialog from './DepartmentSummaryDialog.vue'; import DepartmentSummary from './DepartmentSummary.vue';
const $props = defineProps({ const $props = defineProps({
id: { id: {
@ -15,7 +15,7 @@ const $props = defineProps({
<DepartmentDescriptor <DepartmentDescriptor
v-if="$props.id" v-if="$props.id"
:id="$props.id" :id="$props.id"
:summary="DepartmentSummaryDialog" :summary="DepartmentSummary"
/> />
</QPopupProxy> </QPopupProxy>
</template> </template>

View File

@ -32,6 +32,7 @@ onMounted(async () => {
:url="`Departments/${entityId}`" :url="`Departments/${entityId}`"
class="full-width" class="full-width"
style="max-width: 900px" style="max-width: 900px"
module-name="Department"
> >
<template #header="{ entity }"> <template #header="{ entity }">
<div>{{ entity.name }}</div> <div>{{ entity.name }}</div>

View File

@ -11,9 +11,9 @@ import VnInput from 'src/components/common/VnInput.vue';
import FetchedTags from 'components/ui/FetchedTags.vue'; import FetchedTags from 'components/ui/FetchedTags.vue';
import VnConfirm from 'components/ui/VnConfirm.vue'; import VnConfirm from 'components/ui/VnConfirm.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useStateStore } from 'stores/useStateStore';
import { toCurrency } from 'src/filters'; import { toCurrency } from 'src/filters';
import axios from 'axios'; import axios from 'axios';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
@ -22,7 +22,6 @@ const quasar = useQuasar();
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const { t } = useI18n(); const { t } = useI18n();
const stateStore = useStateStore();
const { notify } = useNotify(); const { notify } = useNotify();
const rowsSelected = ref([]); const rowsSelected = ref([]);
@ -82,6 +81,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 +156,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 +178,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 +231,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));
}; };
@ -297,7 +311,8 @@ const lockIconType = (groupingMode, mode) => {
auto-load auto-load
@on-fetch="(data) => (packagingsOptions = data)" @on-fetch="(data) => (packagingsOptions = data)"
/> />
<Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown()"> <VnSubToolbar>
<template #st-actions>
<QBtnGroup push style="column-gap: 10px"> <QBtnGroup push style="column-gap: 10px">
<slot name="moreBeforeActions" /> <slot name="moreBeforeActions" />
<QBtn <QBtn
@ -310,7 +325,8 @@ const lockIconType = (groupingMode, mode) => {
:title="t('globals.remove')" :title="t('globals.remove')"
/> />
</QBtnGroup> </QBtnGroup>
</Teleport> </template>
</VnSubToolbar>
<VnPaginate <VnPaginate
ref="entryBuysPaginateRef" ref="entryBuysPaginateRef"
data-key="EntryBuys" data-key="EntryBuys"
@ -386,19 +402,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 +426,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 +475,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

@ -135,14 +135,19 @@ watch;
<template #icons> <template #icons>
<QCardActions class="q-gutter-x-md"> <QCardActions class="q-gutter-x-md">
<QIcon <QIcon
v-if="currentEntry.isExcludedFromAvailable" v-if="currentEntry?.isExcludedFromAvailable"
name="vn:inventory" name="vn:inventory"
color="primary" color="primary"
size="xs" size="xs"
> >
<QTooltip>{{ t('Inventory entry') }}</QTooltip> <QTooltip>{{ t('Inventory entry') }}</QTooltip>
</QIcon> </QIcon>
<QIcon v-if="currentEntry.isRaid" name="vn:net" color="primary" size="xs"> <QIcon
v-if="currentEntry?.isRaid"
name="vn:net"
color="primary"
size="xs"
>
<QTooltip>{{ t('Virtual entry') }}</QTooltip> <QTooltip>{{ t('Virtual entry') }}</QTooltip>
</QIcon> </QIcon>
</QCardActions> </QCardActions>

View File

@ -161,6 +161,7 @@ const fetchEntryBuys = async () => {
ref="summaryRef" ref="summaryRef"
:url="`Entries/${entityId}/getEntry`" :url="`Entries/${entityId}/getEntry`"
@on-fetch="(data) => setEntryData(data)" @on-fetch="(data) => setEntryData(data)"
data-key="EntrySummary"
> >
<template #header-left> <template #header-left>
<router-link <router-link

View File

@ -16,14 +16,15 @@ import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import { toDate, toCurrency } from 'src/filters'; 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'; import RightMenu from 'src/components/common/RightMenu.vue';
import VnImg from 'src/components/ui/VnImg.vue';
const router = useRouter(); const router = useRouter();
const { getTokenMultimedia } = useSession(); // const { getTokenMultimedia } = useSession();
const token = getTokenMultimedia(); // const token = getTokenMultimedia();
const stateStore = useStateStore(); const stateStore = useStateStore();
const { t } = useI18n(); const { t } = useI18n();
@ -167,7 +168,7 @@ const columns = computed(() => [
}, },
}, },
{ {
label: t('globals.description'), label: t('entry.latestBuys.description'),
field: 'description', field: 'description',
name: 'description', name: 'description',
align: 'left', align: 'left',
@ -653,6 +654,15 @@ onUnmounted(() => (stateStore.rightDrawer = false));
<EntryLatestBuysFilter data-key="EntryLatestBuys" /> <EntryLatestBuysFilter data-key="EntryLatestBuys" />
</template> </template>
</RightMenu> </RightMenu>
<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>
<QPage class="column items-center q-pa-md"> <QPage class="column items-center q-pa-md">
<QTable <QTable
:rows="rows" :rows="rows"
@ -686,14 +696,7 @@ onUnmounted(() => (stateStore.rightDrawer = false));
</template> </template>
<template #body-cell-picture="{ row }"> <template #body-cell-picture="{ row }">
<QTd> <QTd>
<QImg <VnImg :id="row.itemFk" size="50x50" class="image" />
:src="`/api/Images/catalog/50x50/${row.itemFk}/download?access_token=${token}`"
spinner-color="primary"
:ratio="1"
height="50px"
width="50px"
class="image"
/>
</QTd> </QTd>
</template> </template>
<template #body-cell-itemFk="{ row }"> <template #body-cell-itemFk="{ row }">

View File

@ -184,13 +184,6 @@ const suppliersOptions = ref([]);
@click="removeTag(index, params, searchFn)" @click="removeTag(index, params, searchFn)"
/> />
</QItem> </QItem>
<QItem class="q-mt-lg">
<QIcon
name="add_circle"
class="filter-icon"
@click="tagValues.push({})"
/>
</QItem>
</template> </template>
</ItemsFilterPanel> </ItemsFilterPanel>
</template> </template>

View File

@ -10,8 +10,10 @@ import FetchData from 'src/components/FetchData.vue';
import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelect from 'src/components/common/VnSelect.vue';
import VnCurrency from 'src/components/common/VnCurrency.vue'; import VnCurrency from 'src/components/common/VnCurrency.vue';
import { toCurrency } from 'src/filters'; import { toCurrency } from 'src/filters';
import useNotify from 'src/composables/useNotify.js';
const route = useRoute(); const route = useRoute();
const { notify } = useNotify();
const { t } = useI18n(); const { t } = useI18n();
const arrayData = useArrayData(); const arrayData = useArrayData();
const invoiceIn = computed(() => arrayData.store.data); const invoiceIn = computed(() => arrayData.store.data);
@ -69,6 +71,7 @@ const isNotEuro = (code) => code != 'EUR';
async function insert() { async function insert() {
await axios.post('/InvoiceInDueDays/new', { id: +invoiceId }); await axios.post('/InvoiceInDueDays/new', { id: +invoiceId });
await invoiceInFormRef.value.reload(); await invoiceInFormRef.value.reload();
notify(t('globals.dataSaved'), 'positive');
} }
const getTotalAmount = (rows) => rows.reduce((acc, { amount }) => acc + +amount, 0); const getTotalAmount = (rows) => rows.reduce((acc, { amount }) => acc + +amount, 0);
</script> </script>

View File

@ -106,6 +106,7 @@ const ticketsColumns = ref([
ref="summary" ref="summary"
:url="`InvoiceOuts/${entityId}/summary`" :url="`InvoiceOuts/${entityId}/summary`"
:entity-id="entityId" :entity-id="entityId"
data-key="InvoiceOutSummary"
> >
<template #header="{ entity: { invoiceOut } }"> <template #header="{ entity: { invoiceOut } }">
<div>{{ invoiceOut.ref }} - {{ invoiceOut.client?.socialName }}</div> <div>{{ invoiceOut.ref }} - {{ invoiceOut.client?.socialName }}</div>

View File

@ -13,9 +13,9 @@ import ItemDescriptorImage from 'src/pages/Item/Card/ItemDescriptorImage.vue';
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
import useCardDescription from 'src/composables/useCardDescription'; import useCardDescription from 'src/composables/useCardDescription';
import { useSession } from 'src/composables/useSession';
import { getUrl } from 'src/composables/getUrl'; import { getUrl } from 'src/composables/getUrl';
import axios from 'axios'; import axios from 'axios';
import { dashIfEmpty } from 'src/filters';
const $props = defineProps({ const $props = defineProps({
id: { id: {
@ -41,14 +41,12 @@ const quasar = useQuasar();
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const { t } = useI18n(); const { t } = useI18n();
const { getTokenMultimedia } = useSession();
const state = useState(); const state = useState();
const user = state.getUser(); const user = state.getUser();
const entityId = computed(() => { const entityId = computed(() => {
return $props.id || route.params.id; return $props.id || route.params.id;
}); });
const image = ref(null);
const regularizeStockFormDialog = ref(null); const regularizeStockFormDialog = ref(null);
const item = ref(null); const item = ref(null);
const available = ref(null); const available = ref(null);
@ -66,17 +64,10 @@ const warehouseFk = computed({
}); });
onMounted(async () => { onMounted(async () => {
await getItemAvatar();
warehouseFk.value = user.value.warehouseFk; warehouseFk.value = user.value.warehouseFk;
salixUrl.value = await getUrl(''); salixUrl.value = await getUrl('');
}); });
const getItemAvatar = async () => {
const token = getTokenMultimedia();
const timeStamp = `timestamp=${Date.now()}`;
image.value = `/api/Images/catalog/200x200/${entityId.value}/download?access_token=${token}&${timeStamp}`;
};
const data = ref(useCardDescription()); const data = ref(useCardDescription());
const setData = (entity) => { const setData = (entity) => {
if (!entity) return; if (!entity) return;
@ -182,6 +173,10 @@ const openCloneDialog = async () => {
</span> </span>
</template> </template>
</VnLv> </VnLv>
<VnLv
:label="t('item.descriptor.producer')"
:value="dashIfEmpty(entity.subName)"
/>
<VnLv <VnLv
v-if="entity.value5" v-if="entity.value5"
:label="t('item.descriptor.color')" :label="t('item.descriptor.color')"

View File

@ -3,8 +3,7 @@ import { ref, onMounted } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import EditPictureForm from 'components/EditPictureForm.vue'; import EditPictureForm from 'components/EditPictureForm.vue';
import VnImg from 'src/components/ui/VnImg.vue';
import { useSession } from 'src/composables/useSession';
import axios from 'axios'; import axios from 'axios';
const $props = defineProps({ const $props = defineProps({
@ -27,19 +26,12 @@ const $props = defineProps({
}); });
const { t } = useI18n(); const { t } = useI18n();
const { getTokenMultimedia } = useSession();
const image = ref(null); const image = ref(null);
const editPhotoFormDialog = ref(null); const editPhotoFormDialog = ref(null);
const showEditPhotoForm = ref(false); const showEditPhotoForm = ref(false);
const warehouseName = ref(null); const warehouseName = ref(null);
const getItemAvatar = async () => {
const token = getTokenMultimedia();
const timeStamp = `timestamp=${Date.now()}`;
image.value = `/api/Images/catalog/200x200/${$props.entityId}/download?access_token=${token}&${timeStamp}`;
};
const toggleEditPictureForm = () => { const toggleEditPictureForm = () => {
showEditPhotoForm.value = !showEditPhotoForm.value; showEditPhotoForm.value = !showEditPhotoForm.value;
}; };
@ -62,14 +54,17 @@ const getWarehouseName = async (warehouseFk) => {
}; };
onMounted(async () => { onMounted(async () => {
getItemAvatar();
getItemConfigs(); getItemConfigs();
}); });
const handlePhotoUpdated = (evt = false) => {
image.value.reload(evt);
};
</script> </script>
<template> <template>
<div class="relative-position"> <div class="relative-position">
<QImg :src="image" spinner-color="primary" style="min-height: 256px"> <VnImg ref="image" :id="$props.entityId" @refresh="handlePhotoUpdated(true)">
<template #error> <template #error>
<div class="absolute-full picture text-center q-pa-md flex flex-center"> <div class="absolute-full picture text-center q-pa-md flex flex-center">
<div> <div>
@ -82,7 +77,7 @@ onMounted(async () => {
</div> </div>
</div> </div>
</template> </template>
</QImg> </VnImg>
<QBtn <QBtn
v-if="showEditButton" v-if="showEditButton"
color="primary" color="primary"
@ -97,7 +92,7 @@ onMounted(async () => {
collection="catalog" collection="catalog"
:id="entityId" :id="entityId"
@close-form="toggleEditPictureForm()" @close-form="toggleEditPictureForm()"
@on-photo-uploaded="getItemAvatar()" @on-photo-uploaded="handlePhotoUpdated"
/> />
</QDialog> </QDialog>
</QBtn> </QBtn>

View File

@ -33,7 +33,6 @@ const user = state.getUser();
const fixedPrices = ref([]); const fixedPrices = ref([]);
const fixedPricesOriginalData = ref([]); const fixedPricesOriginalData = ref([]);
const warehousesOptions = ref([]); const warehousesOptions = ref([]);
const itemsWithNameOptions = ref([]);
const rowsSelected = ref([]); const rowsSelected = ref([]);
const exprBuilder = (param, value) => { const exprBuilder = (param, value) => {
@ -371,12 +370,6 @@ onUnmounted(() => (stateStore.rightDrawer = false));
auto-load auto-load
@on-fetch="(data) => onWarehousesFetched(data)" @on-fetch="(data) => onWarehousesFetched(data)"
/> />
<FetchData
url="Items/withName"
:filter="{ fields: ['id', 'name'], order: 'id DESC' }"
auto-load
@on-fetch="(data) => (itemsWithNameOptions = data)"
/>
<RightMenu> <RightMenu>
<template #right-panel> <template #right-panel>
<ItemFixedPriceFilter <ItemFixedPriceFilter
@ -419,7 +412,7 @@ onUnmounted(() => (stateStore.rightDrawer = false));
<template #body-cell-itemId="props"> <template #body-cell-itemId="props">
<QTd> <QTd>
<VnSelect <VnSelect
:options="itemsWithNameOptions" url="Items/withName"
hide-selected hide-selected
option-label="id" option-label="id"
option-value="id" option-value="id"
@ -562,7 +555,7 @@ onUnmounted(() => (stateStore.rightDrawer = false));
</QTd> </QTd>
</template> </template>
</QTable> </QTable>
<QPageSticky v-if="rowsSelected.length > 0" :offset="[20, 20]"> <QPageSticky v-if="rowsSelected.length" :offset="[20, 20]">
<QBtn @click="openEditTableCellDialog()" color="primary" fab icon="edit" /> <QBtn @click="openEditTableCellDialog()" color="primary" fab icon="edit" />
<QTooltip> <QTooltip>
{{ t('Edit fixed price(s)') }} {{ t('Edit fixed price(s)') }}

View File

@ -16,17 +16,15 @@ import ItemListFilter from './ItemListFilter.vue';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import { toDateFormat } from 'src/filters/date.js'; import { toDateFormat } from 'src/filters/date.js';
import { useSession } from 'composables/useSession';
import { dashIfEmpty } from 'src/filters'; import { dashIfEmpty } from 'src/filters';
import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import { useVnConfirm } from 'composables/useVnConfirm'; import { useVnConfirm } from 'composables/useVnConfirm';
import axios from 'axios'; import axios from 'axios';
import RightMenu from 'src/components/common/RightMenu.vue'; import RightMenu from 'src/components/common/RightMenu.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import VnImg from 'src/components/ui/VnImg.vue';
const router = useRouter(); const router = useRouter();
const { getTokenMultimedia } = useSession();
const token = getTokenMultimedia();
const stateStore = useStateStore(); const stateStore = useStateStore();
const { t } = useI18n(); const { t } = useI18n();
const { viewSummary } = useSummaryDialog(); const { viewSummary } = useSummaryDialog();
@ -491,10 +489,9 @@ onUnmounted(() => (stateStore.rightDrawer = false));
</template> </template>
<template #body-cell-picture="{ row }"> <template #body-cell-picture="{ row }">
<QTd> <QTd>
<QImg <VnImg
:src="`/api/Images/catalog/50x50/${row.id}/download?access_token=${token}`" size="50x50"
spinner-color="primary" :id="row.id"
:ratio="1"
height="50px" height="50px"
width="50px" width="50px"
class="image" class="image"

View File

@ -3,14 +3,15 @@ import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import axios from 'axios'; import axios from 'axios';
import VnInput from 'components/common/VnInput.vue';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnSelect from 'components/common/VnSelect.vue'; import VnSelect from 'components/common/VnSelect.vue';
import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue'; import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue';
import { useValidator } from 'src/composables/useValidator';
import VnInput from 'src/components/common/VnInput.vue';
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
const props = defineProps({ const props = defineProps({
dataKey: { dataKey: {
@ -21,32 +22,34 @@ const props = defineProps({
type: Array, type: Array,
required: true, required: true,
}, },
tagValue: {
type: Array,
required: true,
},
}); });
const categoryList = ref(null); const categoryList = ref(null);
const selectedCategoryFk = ref(null); const selectedCategoryFk = ref(null);
const typeList = ref(null); const typeList = ref(null);
const selectedTypeFk = ref(null); const selectedTypeFk = ref(null);
const validationsStore = useValidator();
const selectedOrder = ref(null);
const selectedOrderField = ref(null);
const moreFields = ref([]);
const moreFieldsOrder = ref([]);
const createValue = (val, done) => {
if (val.length > 2) {
if (!tagOptions.value.includes(val)) {
done(tagOptions.value, 'add-unique');
}
tagValues.value.push({ value: val });
}
};
const resetCategory = () => { const resetCategory = () => {
selectedCategoryFk.value = null; selectedCategoryFk.value = null;
typeList.value = null; typeList.value = null;
}; };
const selectedOrder = ref(null);
const orderList = [
{ way: 'ASC', name: 'Ascendant' },
{ way: 'DESC', name: 'Descendant' },
];
const selectedOrderField = ref(null);
const OrderFields = [
{ field: 'relevancy DESC, name', name: 'Relevancy', priority: 999 },
{ field: 'showOrder, price', name: 'Color and price', priority: 999 },
{ field: 'name', name: 'Name', priority: 999 },
{ field: 'price', name: 'Price', priority: 999 },
];
const clearFilter = (key) => { const clearFilter = (key) => {
if (key === 'categoryFk') { if (key === 'categoryFk') {
resetCategory(); resetCategory();
@ -72,21 +75,6 @@ const loadTypes = async (categoryFk) => {
typeList.value = data; typeList.value = data;
}; };
const onFilterInit = async ({ params }) => {
if (params.typeFk) {
selectedTypeFk.value = params.typeFk;
}
if (params.categoryFk) {
await loadTypes(params.categoryFk);
selectedCategoryFk.value = params.categoryFk;
}
if (params.orderBy) {
orderByParam.value = JSON.parse(params.orderBy);
selectedOrder.value = orderByParam.value?.way;
selectedOrderField.value = orderByParam.value?.field;
}
};
const selectedCategory = computed(() => const selectedCategory = computed(() =>
(categoryList.value || []).find( (categoryList.value || []).find(
(category) => category?.id === selectedCategoryFk.value (category) => category?.id === selectedCategoryFk.value
@ -109,10 +97,7 @@ function exprBuilder(param, value) {
const selectedTag = ref(null); const selectedTag = ref(null);
const tagValues = ref([{}]); const tagValues = ref([{}]);
const tagOptions = ref(null); const tagOptions = ref([]);
const isButtonDisabled = computed(
() => !selectedTag.value || tagValues.value.some((item) => !item.value)
);
const applyTagFilter = (params, search) => { const applyTagFilter = (params, search) => {
if (!tagValues.value?.length) { if (!tagValues.value?.length) {
@ -125,12 +110,12 @@ const applyTagFilter = (params, search) => {
} }
params.tagGroups.push( params.tagGroups.push(
JSON.stringify({ JSON.stringify({
values: tagValues.value, values: tagValues.value.filter((obj) => Object.keys(obj).length > 0),
tagSelection: { tagSelection: {
...selectedTag.value, ...selectedTag.value,
orgShowField: selectedTag.value.name, orgShowField: selectedTag?.value?.name,
}, },
tagFk: selectedTag.value.tagFk, tagFk: selectedTag?.value?.tagFk,
}) })
); );
search(); search();
@ -147,20 +132,52 @@ const removeTagChip = (selection, params, search) => {
search(); search();
}; };
const orderByParam = ref(null); const onOrderChange = (value, params) => {
const tagObj = JSON.parse(params.orderBy);
const onOrderFieldChange = (value, params, search) => { tagObj.way = value.name;
const orderBy = Object.assign({}, orderByParam.value, { field: value.field }); params.orderBy = JSON.stringify(tagObj);
params.orderBy = JSON.stringify(orderBy);
search();
}; };
const onOrderChange = (value, params, search) => { const onOrderFieldChange = (value, params) => {
const orderBy = Object.assign({}, orderByParam.value, { way: value.way }); const tagObj = JSON.parse(params.orderBy); // esto donde va
params.orderBy = JSON.stringify(orderBy); const fields = {
search(); Relevancy: (value) => value + ' DESC, name',
ColorAndPrice: 'showOrder, price',
Name: 'name',
Price: 'price',
};
let tagField = fields[value];
if (!tagField) return;
if (typeof tagField === 'function') tagField = tagField(value);
tagObj.field = tagField;
params.orderBy = JSON.stringify(tagObj);
switch (value) {
case 'Relevancy':
tagObj.field = value + ' DESC, name';
params.orderBy = JSON.stringify(tagObj);
console.log('params: ', params);
break;
case 'ColorAndPrice':
tagObj.field = 'showOrder, price';
params.orderBy = JSON.stringify(tagObj);
console.log('params: ', params);
break;
case 'Name':
tagObj.field = 'name';
params.orderBy = JSON.stringify(tagObj);
console.log('params: ', params);
break;
case 'Price':
tagObj.field = 'price';
params.orderBy = JSON.stringify(tagObj);
console.log('params: ', params);
break;
}
}; };
const _moreFields = ['ASC', 'DESC'];
const _moreFieldsTypes = ['Relevancy', 'ColorAndPrice', 'Name', 'Price'];
const setCategoryList = (data) => { const setCategoryList = (data) => {
categoryList.value = (data || []) categoryList.value = (data || [])
.filter((category) => category.display) .filter((category) => category.display)
@ -168,6 +185,8 @@ const setCategoryList = (data) => {
...category, ...category,
icon: `vn:${(category.icon || '').split('-')[1]}`, icon: `vn:${(category.icon || '').split('-')[1]}`,
})); }));
moreFields.value = useLang(_moreFields);
moreFieldsOrder.value = useLang(_moreFieldsTypes);
}; };
const getCategoryClass = (category, params) => { const getCategoryClass = (category, params) => {
@ -175,6 +194,20 @@ const getCategoryClass = (category, params) => {
return 'active'; return 'active';
} }
}; };
const useLang = (values) => {
const { models } = validationsStore;
const properties = models.Item?.properties || {};
return values.map((name) => {
let prop = properties[name];
const label = t(`params.${name}`);
return {
name,
label,
type: prop ? prop.type : null,
};
});
};
</script> </script>
<template> <template>
@ -182,9 +215,9 @@ const getCategoryClass = (category, params) => {
<VnFilterPanel <VnFilterPanel
:data-key="props.dataKey" :data-key="props.dataKey"
:hidden-tags="['orderFk', 'orderBy']" :hidden-tags="['orderFk', 'orderBy']"
:unremovable-params="['orderFk', 'orderBy']"
:expr-builder="exprBuilder" :expr-builder="exprBuilder"
:custom-tags="['tagGroups']" :custom-tags="['tagGroups']"
@init="onFilterInit"
@remove="clearFilter" @remove="clearFilter"
> >
<template #tags="{ tag, formatFn }"> <template #tags="{ tag, formatFn }">
@ -274,40 +307,29 @@ const getCategoryClass = (category, params) => {
<QItem class="q-my-md"> <QItem class="q-my-md">
<QItemSection> <QItemSection>
<VnSelect <VnSelect
:label="t('params.order')" :label="t('Order')"
v-model="selectedOrder" v-model="selectedOrder"
:options="orderList || []" :options="moreFields"
option-value="way" option-label="label"
option-label="name"
dense dense
outlined outlined
rounded rounded
:emit-value="false" @update:model-value="(value) => onOrderChange(value, params)"
use-input
:is-clearable="false"
@update:model-value="
(value) => onOrderChange(value, params, searchFn)
"
/> />
</QItemSection> </QItemSection>
</QItem> </QItem>
<QItem class="q-mb-md"> <QItem class="q-mb-md">
<QItemSection> <QItemSection>
<VnSelect <VnSelect
:label="t('params.order')" :label="t('Order by')"
v-model="selectedOrderField" v-model="selectedOrderField"
:options="OrderFields || []" :options="moreFieldsOrder"
option-value="field" option-label="label"
option-label="name" option-value="name"
dense dense
outlined outlined
rounded rounded
:emit-value="false" @update:model-value="(value) => onOrderFieldChange(value, params)"
use-input
:is-clearable="false"
@update:model-value="
(value) => onOrderFieldChange(value, params, searchFn)
"
/> />
</QItemSection> </QItemSection>
</QItem> </QItem>
@ -333,15 +355,30 @@ const getCategoryClass = (category, params) => {
:key="value" :key="value"
class="q-mt-md filter-value" class="q-mt-md filter-value"
> >
<VnInput <FetchData
v-if="selectedTag?.isFree" v-if="selectedTag"
v-model="value.value" :url="`Tags/${selectedTag}/filterValue`"
:label="t('params.value')" limit="30"
is-outlined auto-load
class="filter-input" @on-fetch="(data) => (tagOptions = data)"
/> />
<VnSelect <VnSelect
v-else v-if="!selectedTag"
:label="t('params.value')"
v-model="value.value"
:options="tagValue || []"
option-value="value"
option-label="value"
dense
outlined
rounded
emit-value
use-input
class="filter-input"
@new-value="createValue"
/>
<VnSelect
v-else-if="selectedTag === 1"
:label="t('params.value')" :label="t('params.value')"
v-model="value.value" v-model="value.value"
:options="tagOptions || []" :options="tagOptions || []"
@ -352,18 +389,18 @@ const getCategoryClass = (category, params) => {
rounded rounded
emit-value emit-value
use-input use-input
:disable="!selectedTag" class="filter-input"
@new-value="createValue"
/>
<VnInput
v-else
:label="t('params.value')"
v-model="value.value"
dense
outlined
rounded
class="filter-input" class="filter-input"
/> />
<FetchData
v-if="selectedTag && !selectedTag.isFree"
:url="`Tags/${selectedTag?.id}/filterValue`"
limit="30"
auto-load
@on-fetch="(data) => (tagOptions = data)"
/>
<QIcon <QIcon
name="delete" name="delete"
class="filter-icon" class="filter-icon"
@ -388,7 +425,6 @@ const getCategoryClass = (category, params) => {
rounded rounded
type="button" type="button"
unelevated unelevated
:disable="isButtonDisabled"
@click.stop="applyTagFilter(params, searchFn)" @click.stop="applyTagFilter(params, searchFn)"
/> />
</QItemSection> </QItemSection>
@ -453,6 +489,12 @@ en:
tag: Tag tag: Tag
value: Value value: Value
order: Order order: Order
ASC: Ascendant
DESC: Descendant
Relevancy: Relevancy
ColorAndPrice: Color and price
Name: Name
Price: Price
es: es:
params: params:
type: Tipo type: Tipo
@ -460,6 +502,14 @@ es:
tag: Etiqueta tag: Etiqueta
value: Valor value: Valor
order: Orden order: Orden
ASC: Ascendiente
DESC: Descendiente
Relevancy: Relevancia
ColorAndPrice: Color y precio
Name: Nombre
Price: Precio
Order: Orden
Order by: Ordenar por
Plant: Planta Plant: Planta
Flower: Flor Flower: Flor
Handmade: Confección Handmade: Confección

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