0
0
Fork 0

Merge pull request '7863-devToTest_2434' (!613) from 7863-devToTest_2434 into test

Reviewed-on: verdnatura/salix-front#613
Reviewed-by: Guillermo Bonet <guillermo@verdnatura.es>
This commit is contained in:
Alex Moreno 2024-08-13 06:58:12 +00:00
commit ecfe933039
127 changed files with 8344 additions and 11278 deletions

33
.husky/addReferenceTag.js Normal file
View File

@ -0,0 +1,33 @@
const fs = require('fs');
const path = require('path');
function getCurrentBranchName(p = process.cwd()) {
if (!fs.existsSync(p)) return false;
const gitHeadPath = path.join(p, '.git', 'HEAD');
if (!fs.existsSync(gitHeadPath))
return getCurrentBranchName(path.resolve(p, '..'));
const headContent = fs.readFileSync(gitHeadPath, 'utf-8');
return headContent.trim().split('/')[2];
}
const branchName = getCurrentBranchName();
if (branchName) {
const msgPath = `.git/COMMIT_EDITMSG`;
const msg = fs.readFileSync(msgPath, 'utf-8');
const reference = branchName.match(/^\d+/);
const referenceTag = `refs #${reference}`;
if (!msg.includes(referenceTag) && reference) {
const splitedMsg = msg.split(':');
if (splitedMsg.length > 1) {
const finalMsg = splitedMsg[0] + ': ' + referenceTag + splitedMsg.slice(1).join(':');
fs.writeFileSync(msgPath, finalMsg);
}
}
}

8
.husky/commit-msg Executable file
View File

@ -0,0 +1,8 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
echo "Running husky commit-msg hook"
npx --no-install commitlint --edit
echo "Adding reference tag to commit message"
node .husky/addReferenceTag.js

1
commitlint.config.js Normal file
View File

@ -0,0 +1 @@
module.exports = { extends: ['@commitlint/config-conventional'] };

View File

@ -1,6 +1,6 @@
{ {
"name": "salix-front", "name": "salix-front",
"version": "24.32.0", "version": "24.34.0",
"description": "Salix frontend", "description": "Salix frontend",
"productName": "Salix", "productName": "Salix",
"author": "Verdnatura", "author": "Verdnatura",
@ -13,7 +13,10 @@
"test:e2e:ci": "cd ../salix && gulp docker && cd ../salix-front && cypress run", "test:e2e:ci": "cd ../salix && gulp docker && cd ../salix-front && cypress run",
"test": "echo \"See package.json => scripts for available tests.\" && exit 0", "test": "echo \"See package.json => scripts for available tests.\" && exit 0",
"test:unit": "vitest", "test:unit": "vitest",
"test:unit:ci": "vitest run" "test:unit:ci": "vitest run",
"commitlint": "commitlint --edit",
"prepare": "npx husky install",
"addReferenceTag": "node .husky/addReferenceTag.js"
}, },
"dependencies": { "dependencies": {
"@quasar/cli": "^2.3.0", "@quasar/cli": "^2.3.0",
@ -29,6 +32,8 @@
"vue-router": "^4.2.1" "vue-router": "^4.2.1"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "^19.2.1",
"@commitlint/config-conventional": "^19.1.0",
"@intlify/unplugin-vue-i18n": "^0.8.1", "@intlify/unplugin-vue-i18n": "^0.8.1",
"@pinia/testing": "^0.1.2", "@pinia/testing": "^0.1.2",
"@quasar/app-vite": "^1.7.3", "@quasar/app-vite": "^1.7.3",
@ -41,6 +46,7 @@
"eslint-config-prettier": "^8.8.0", "eslint-config-prettier": "^8.8.0",
"eslint-plugin-cypress": "^2.13.3", "eslint-plugin-cypress": "^2.13.3",
"eslint-plugin-vue": "^9.14.1", "eslint-plugin-vue": "^9.14.1",
"husky": "^8.0.0",
"postcss": "^8.4.23", "postcss": "^8.4.23",
"prettier": "^2.8.8", "prettier": "^2.8.8",
"vitest": "^0.31.1" "vitest": "^0.31.1"

File diff suppressed because it is too large Load Diff

View File

@ -100,7 +100,6 @@ 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(modelValue)); const formData = computed(() => state.get(modelValue));
const formUrl = computed(() => $props.url);
const defaultButtons = computed(() => ({ const defaultButtons = computed(() => ({
save: { save: {
color: 'primary', color: 'primary',
@ -148,11 +147,14 @@ if (!$props.url)
(val) => updateAndEmit('onFetch', val) (val) => updateAndEmit('onFetch', val)
); );
watch(formUrl, async () => { watch(
() => [$props.url, $props.filter],
async () => {
originalData.value = null; originalData.value = null;
reset(); reset();
await fetch(); await fetch();
}); }
);
onBeforeRouteLeave((to, from, next) => { onBeforeRouteLeave((to, from, next) => {
if (hasChanges.value && $props.observeFormChanges) if (hasChanges.value && $props.observeFormChanges)

View File

@ -7,7 +7,7 @@ import { useQuasar } from 'quasar';
import PinnedModules from './PinnedModules.vue'; import PinnedModules from './PinnedModules.vue';
import UserPanel from 'components/UserPanel.vue'; import UserPanel from 'components/UserPanel.vue';
import VnBreadcrumbs from './common/VnBreadcrumbs.vue'; import VnBreadcrumbs from './common/VnBreadcrumbs.vue';
import VnImg from 'src/components/ui/VnImg.vue'; import VnAvatar from './ui/VnAvatar.vue';
const { t } = useI18n(); const { t } = useI18n();
const stateStore = useStateStore(); const stateStore = useStateStore();
@ -72,22 +72,13 @@ const pinnedModulesRef = ref();
</QTooltip> </QTooltip>
<PinnedModules ref="pinnedModulesRef" /> <PinnedModules ref="pinnedModulesRef" />
</QBtn> </QBtn>
<QBtn <QBtn class="q-pa-none" rounded dense flat no-wrap id="user">
:class="{ 'q-pa-none': quasar.platform.is.mobile }" <VnAvatar
rounded :worker-id="user.id"
dense :title="user.name"
flat size="lg"
no-wrap color="transparent"
id="user"
>
<QAvatar size="lg">
<VnImg
:id="user.id"
collection="user"
size="160x160"
:zoom-size="null"
/> />
</QAvatar>
<QTooltip bottom> <QTooltip bottom>
{{ t('globals.userPanel') }} {{ t('globals.userPanel') }}
</QTooltip> </QTooltip>

View File

@ -11,8 +11,8 @@ 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';
import { useRole } from 'src/composables/useRole'; import { useRole } from 'src/composables/useRole';
import VnAvatar from './ui/VnAvatar.vue';
const state = useState(); const state = useState();
const session = useSession(); const session = useSession();
@ -136,7 +136,7 @@ const isEmployee = computed(() => useRole().isEmployee());
@update:model-value="saveLanguage" @update:model-value="saveLanguage"
:label="t(`globals.lang['${userLocale}']`)" :label="t(`globals.lang['${userLocale}']`)"
icon="public" icon="public"
color="orange" color="primary"
false-value="es" false-value="es"
true-value="en" true-value="en"
/> />
@ -145,7 +145,7 @@ const isEmployee = computed(() => useRole().isEmployee());
@update:model-value="saveDarkMode" @update:model-value="saveDarkMode"
:label="t(`globals.darkMode`)" :label="t(`globals.darkMode`)"
checked-icon="dark_mode" checked-icon="dark_mode"
color="orange" color="primary"
unchecked-icon="light_mode" unchecked-icon="light_mode"
/> />
</div> </div>
@ -153,10 +153,12 @@ const isEmployee = computed(() => useRole().isEmployee());
<QSeparator vertical inset class="q-mx-lg" /> <QSeparator vertical inset class="q-mx-lg" />
<div class="col column items-center q-mb-sm"> <div class="col column items-center q-mb-sm">
<QAvatar size="80px"> <VnAvatar
<VnImg :id="user.id" collection="user" size="160x160" /> :worker-id="user.id"
</QAvatar> :title="user.name"
size="xxl"
color="transparent"
/>
<div class="text-subtitle1 q-mt-md"> <div class="text-subtitle1 q-mt-md">
<strong>{{ user.nickname }}</strong> <strong>{{ user.nickname }}</strong>
</div> </div>
@ -168,7 +170,7 @@ const isEmployee = computed(() => useRole().isEmployee());
</div> </div>
<QBtn <QBtn
id="logout" id="logout"
color="orange" color="primary"
flat flat
:label="t('globals.logOut')" :label="t('globals.logOut')"
size="sm" size="sm"

View File

@ -7,9 +7,11 @@ import { dashIfEmpty } from 'src/filters';
import VnSelect from 'components/common/VnSelect.vue'; import VnSelect from 'components/common/VnSelect.vue';
import VnSelectCache from 'components/common/VnSelectCache.vue'; import VnSelectCache from 'components/common/VnSelectCache.vue';
import VnInput from 'components/common/VnInput.vue'; import VnInput from 'components/common/VnInput.vue';
import VnInputNumber from 'components/common/VnInputNumber.vue';
import VnInputDate from 'components/common/VnInputDate.vue'; import VnInputDate from 'components/common/VnInputDate.vue';
import VnInputTime from 'components/common/VnInputTime.vue'; import VnInputTime from 'components/common/VnInputTime.vue';
import VnComponent from 'components/common/VnComponent.vue'; import VnComponent from 'components/common/VnComponent.vue';
import VnUserLink from 'components/ui/VnUserLink.vue';
const model = defineModel(undefined, { required: true }); const model = defineModel(undefined, { required: true });
const $props = defineProps({ const $props = defineProps({
@ -66,7 +68,7 @@ const defaultComponents = {
}, },
}, },
number: { number: {
component: markRaw(VnInput), component: markRaw(VnInputNumber),
attrs: { attrs: {
disable: !$props.isEditable, disable: !$props.isEditable,
class: 'fit', class: 'fit',
@ -98,14 +100,14 @@ const defaultComponents = {
}, },
checkbox: { checkbox: {
component: markRaw(QCheckbox), component: markRaw(QCheckbox),
attrs: (prop) => { attrs: ({ model }) => {
const defaultAttrs = { const defaultAttrs = {
disable: !$props.isEditable, disable: !$props.isEditable,
'model-value': Boolean(prop), 'model-value': Boolean(model),
class: 'no-padding fit', class: 'no-padding fit',
}; };
if (typeof prop == 'number') { if (typeof model == 'number') {
defaultAttrs['true-value'] = 1; defaultAttrs['true-value'] = 1;
defaultAttrs['false-value'] = 0; defaultAttrs['false-value'] = 0;
} }
@ -126,6 +128,9 @@ const defaultComponents = {
icon: { icon: {
component: markRaw(QIcon), component: markRaw(QIcon),
}, },
userLink: {
component: markRaw(VnUserLink),
},
}; };
const value = computed(() => { const value = computed(() => {
@ -146,7 +151,7 @@ const col = computed(() => {
}; };
} }
if ( if (
(newColumn.name.startsWith('is') || newColumn.name.startsWith('has')) && (/^is[A-Z]/.test(newColumn.name) || /^has[A-Z]/.test(newColumn.name)) &&
newColumn.component == null newColumn.component == null
) )
newColumn.component = 'checkbox'; newColumn.component = 'checkbox';
@ -163,14 +168,14 @@ const components = computed(() => $props.components ?? defaultComponents);
v-if="col.before" v-if="col.before"
:prop="col.before" :prop="col.before"
:components="components" :components="components"
:value="model" :value="{ row, model }"
v-model="model" v-model="model"
/> />
<VnComponent <VnComponent
v-if="col.component" v-if="col.component"
:prop="col" :prop="col"
:components="components" :components="components"
:value="model" :value="{ row, model }"
v-model="model" v-model="model"
/> />
<span :title="value" v-else>{{ value }}</span> <span :title="value" v-else>{{ value }}</span>
@ -178,7 +183,7 @@ const components = computed(() => $props.components ?? defaultComponents);
v-if="col.after" v-if="col.after"
:prop="col.after" :prop="col.after"
:components="components" :components="components"
:value="model" :value="{ row, model }"
v-model="model" v-model="model"
/> />
</div> </div>

View File

@ -10,6 +10,8 @@ import VnInputDate from 'components/common/VnInputDate.vue';
import VnInputTime from 'components/common/VnInputTime.vue'; import VnInputTime from 'components/common/VnInputTime.vue';
import VnTableColumn from 'components/VnTable/VnColumn.vue'; import VnTableColumn from 'components/VnTable/VnColumn.vue';
defineExpose({ addFilter });
const $props = defineProps({ const $props = defineProps({
column: { column: {
type: Object, type: Object,
@ -32,7 +34,7 @@ const model = defineModel(undefined, { required: true });
const arrayData = useArrayData($props.dataKey, { searchUrl: $props.searchUrl }); const arrayData = useArrayData($props.dataKey, { searchUrl: $props.searchUrl });
const columnFilter = computed(() => $props.column?.columnFilter); const columnFilter = computed(() => $props.column?.columnFilter);
const updateEvent = { 'update:modelValue': addFilter }; const updateEvent = { 'update:modelValue': addFilter, remove: () => addFilter(null) };
const enterEvent = { const enterEvent = {
'keyup.enter': () => addFilter(model.value), 'keyup.enter': () => addFilter(model.value),
remove: () => addFilter(null), remove: () => addFilter(null),

View File

@ -126,7 +126,7 @@ const tableModes = [
]; ];
onBeforeMount(() => { onBeforeMount(() => {
setUserParams(route.query[$props.searchUrl]); setUserParams(route.query[$props.searchUrl]);
hasParams.value = Object.keys(params.value).length !== 0; hasParams.value = params.value && Object.keys(params.value).length !== 0;
}); });
onMounted(() => { onMounted(() => {
@ -164,13 +164,16 @@ watch(
const isTableMode = computed(() => mode.value == TABLE_MODE); const isTableMode = computed(() => mode.value == TABLE_MODE);
function setUserParams(watchedParams) { function setUserParams(watchedParams, watchedOrder) {
if (!watchedParams) return; if (!watchedParams) return;
if (typeof watchedParams == 'string') watchedParams = JSON.parse(watchedParams); if (typeof watchedParams == 'string') watchedParams = JSON.parse(watchedParams);
const filter = JSON.parse(watchedParams?.filter ?? '{}'); const filter =
typeof watchedParams?.filter == 'string'
? JSON.parse(watchedParams?.filter ?? '{}')
: watchedParams?.filter;
const where = filter?.where; const where = filter?.where;
const order = filter?.order; const order = watchedOrder ?? filter?.order;
watchedParams = { ...watchedParams, ...where }; watchedParams = { ...watchedParams, ...where };
delete watchedParams.filter; delete watchedParams.filter;
@ -291,6 +294,7 @@ defineExpose({
v-model="params" v-model="params"
:search-url="searchUrl" :search-url="searchUrl"
:redirect="!!redirect" :redirect="!!redirect"
@set-user-params="setUserParams"
> >
<template #body> <template #body>
<div <div
@ -333,6 +337,7 @@ defineExpose({
v-bind="$attrs" v-bind="$attrs"
:limit="20" :limit="20"
ref="CrudModelRef" ref="CrudModelRef"
@on-fetch="(...args) => emit('onFetch', ...args)"
:search-url="searchUrl" :search-url="searchUrl"
:disable-infinite-scroll="isTableMode" :disable-infinite-scroll="isTableMode"
@save-changes="reload" @save-changes="reload"
@ -370,7 +375,7 @@ defineExpose({
<template #top-left v-if="!$props.withoutHeader"> <template #top-left v-if="!$props.withoutHeader">
<slot name="top-left"></slot> <slot name="top-left"></slot>
</template> </template>
<template #top-right> <template #top-right v-if="!$props.withoutHeader">
<VnVisibleColumn <VnVisibleColumn
v-if="isTableMode" v-if="isTableMode"
v-model="splittedColumns.columns" v-model="splittedColumns.columns"
@ -397,7 +402,7 @@ defineExpose({
<div <div
class="column self-start q-ml-xs ellipsis" class="column self-start q-ml-xs ellipsis"
:class="`text-${col?.align ?? 'left'}`" :class="`text-${col?.align ?? 'left'}`"
style="height: 75px" :style="$props.columnSearch ? 'height: 75px' : ''"
> >
<div <div
class="row items-center no-wrap" class="row items-center no-wrap"
@ -438,7 +443,7 @@ defineExpose({
</VnTableChip> </VnTableChip>
</QTd> </QTd>
</template> </template>
<template #body-cell="{ col, row }"> <template #body-cell="{ col, row, rowIndex }">
<!-- Columns --> <!-- Columns -->
<QTd <QTd
auto-width auto-width
@ -451,7 +456,12 @@ defineExpose({
rowCtrlClickFunction($event, row) rowCtrlClickFunction($event, row)
" "
> >
<slot :name="`column-${col.name}`" :col="col" :row="row"> <slot
:name="`column-${col.name}`"
:col="col"
:row="row"
:row-index="rowIndex"
>
<VnTableColumn <VnTableColumn
:column="col" :column="col"
:row="row" :row="row"
@ -471,7 +481,6 @@ defineExpose({
> >
<QBtn <QBtn
v-for="(btn, index) of col.actions" v-for="(btn, index) of col.actions"
v-show="btn.show ? btn.show(row) : true"
:key="index" :key="index"
:title="btn.title" :title="btn.title"
:icon="btn.icon" :icon="btn.icon"
@ -482,6 +491,11 @@ defineExpose({
? 'text-primary-light' ? 'text-primary-light'
: 'color-vn-text ' : 'color-vn-text '
" "
:style="`visibility: ${
(btn.show && btn.show(row)) ?? true
? 'visible'
: 'hidden'
}`"
@click="btn.action(row)" @click="btn.action(row)"
/> />
</QTd> </QTd>
@ -539,7 +553,9 @@ defineExpose({
:class="$props.cardClass" :class="$props.cardClass"
> >
<div <div
v-for="col of splittedColumns.cardVisible" v-for="(
col, index
) of splittedColumns.cardVisible"
:key="col.name" :key="col.name"
class="fields" class="fields"
> >
@ -560,6 +576,7 @@ defineExpose({
:name="`column-${col.name}`" :name="`column-${col.name}`"
:col="col" :col="col"
:row="row" :row="row"
:row-index="index"
> >
<VnTableColumn <VnTableColumn
:column="col" :column="col"
@ -619,9 +636,15 @@ defineExpose({
> >
<template #form-inputs="{ data }"> <template #form-inputs="{ data }">
<div class="grid-create"> <div class="grid-create">
<VnTableColumn <slot
v-for="column of splittedColumns.create" v-for="column of splittedColumns.create"
:key="column.name" :key="column.name"
:name="`column-create-${column.name}`"
:data="data"
:column-name="column.name"
:label="column.label"
>
<VnTableColumn
:column="column" :column="column"
:row="{}" :row="{}"
default="input" default="input"
@ -629,6 +652,7 @@ defineExpose({
:show-label="true" :show-label="true"
component-prop="columnCreate" component-prop="columnCreate"
/> />
</slot>
<slot name="more-create-dialog" :data="data" /> <slot name="more-create-dialog" :data="data" />
</div> </div>
</template> </template>

View File

@ -46,7 +46,7 @@ const stateStore = useStateStore();
</div> </div>
</Teleport> </Teleport>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above> <QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<QScrollArea class="fit text-grey-8"> <QScrollArea class="fit">
<div id="right-panel"></div> <div id="right-panel"></div>
<slot v-if="!hasContent" name="right-panel" /> <slot v-if="!hasContent" name="right-panel" />
</QScrollArea> </QScrollArea>

View File

@ -17,13 +17,7 @@ const props = defineProps({
descriptor: { type: Object, required: true }, descriptor: { type: Object, required: true },
filterPanel: { type: Object, default: undefined }, filterPanel: { type: Object, default: undefined },
searchDataKey: { type: String, default: undefined }, searchDataKey: { type: String, default: undefined },
searchUrl: { type: String, default: undefined }, searchbarProps: { type: Object, default: undefined },
searchbarLabel: { type: String, default: '' },
searchbarInfo: { type: String, default: '' },
searchCustomRouteRedirect: { type: String, default: undefined },
searchRedirect: { type: Boolean, default: true },
searchMakeFetch: { type: Boolean, default: true },
searchUrlQuery: { type: String, default: undefined },
}); });
const stateStore = useStateStore(); const stateStore = useStateStore();
@ -66,15 +60,7 @@ if (props.baseUrl) {
</QScrollArea> </QScrollArea>
</QDrawer> </QDrawer>
<slot name="searchbar" v-if="props.searchDataKey"> <slot name="searchbar" v-if="props.searchDataKey">
<VnSearchbar <VnSearchbar :data-key="props.searchDataKey" v-bind="props.searchbarProps" />
:data-key="props.searchDataKey"
:url="props.searchUrl"
:label="props.searchbarLabel"
:info="props.searchbarInfo"
:search-url="props.searchUrlQuery"
:custom-route-redirect-name="searchCustomRouteRedirect"
:redirect="searchRedirect"
/>
</slot> </slot>
<slot v-else name="searchbar" /> <slot v-else name="searchbar" />
<RightMenu> <RightMenu>

View File

@ -35,7 +35,7 @@ function mix(toComponent) {
...toComponent, ...toComponent,
...toValueAttrs(customComponent?.forceAttrs), ...toValueAttrs(customComponent?.forceAttrs),
}, },
event: event ?? customComponent?.event, event: { ...customComponent?.event, ...event },
}; };
return mixed; return mixed;
} }

View File

@ -1,34 +0,0 @@
<script setup>
import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useCapitalize } from 'src/composables/useCapitalize';
import VnInput from 'src/components/common/VnInput.vue';
const props = defineProps({
modelValue: { type: [String, Number], default: '' },
});
const { t } = useI18n();
const emit = defineEmits(['update:modelValue']);
const amount = computed({
get() {
return props.modelValue;
},
set(val) {
emit('update:modelValue', val);
},
});
</script>
<template>
<VnInput
v-model="amount"
type="number"
step="any"
:label="useCapitalize(t('amount'))"
/>
</template>
<i18n>
es:
amount: importe
</i18n>

View File

@ -0,0 +1,8 @@
<script setup>
import VnInput from 'src/components/common/VnInput.vue';
const model = defineModel({ type: [Number, String] });
</script>
<template>
<VnInput v-bind="$attrs" v-model.number="model" type="number" />
</template>

View File

@ -2,7 +2,7 @@
import { ref, toRefs, computed, watch, onMounted } from 'vue'; import { ref, toRefs, computed, watch, 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', 'remove']);
const $props = defineProps({ const $props = defineProps({
modelValue: { modelValue: {
@ -49,6 +49,10 @@ const $props = defineProps({
type: Array, type: Array,
default: null, default: null,
}, },
include: {
type: [Object, Array],
default: null,
},
where: { where: {
type: Object, type: Object,
default: null, default: null,
@ -142,7 +146,7 @@ function filter(val, options) {
async function fetchFilter(val) { async function fetchFilter(val) {
if (!$props.url || !dataRef.value) return; if (!$props.url || !dataRef.value) return;
const { fields, sortBy, limit } = $props; const { fields, include, sortBy, limit } = $props;
const key = const key =
optionFilterValue.value ?? optionFilterValue.value ??
(new RegExp(/\d/g).test(val) (new RegExp(/\d/g).test(val)
@ -153,7 +157,7 @@ async function fetchFilter(val) {
? { [key]: { like: `%${val}%` } } ? { [key]: { like: `%${val}%` } }
: { [key]: val }; : { [key]: val };
const where = { ...(val ? defaultWhere : {}), ...$props.where }; const where = { ...(val ? defaultWhere : {}), ...$props.where };
const fetchOptions = { where, limit }; const fetchOptions = { where, include, limit };
if (fields) fetchOptions.fields = fields; if (fields) fetchOptions.fields = fields;
if (sortBy) fetchOptions.order = sortBy; if (sortBy) fetchOptions.order = sortBy;
return dataRef.value.fetch(fetchOptions); return dataRef.value.fetch(fetchOptions);
@ -168,7 +172,10 @@ async function filterHandler(val, update) {
let newOptions; let newOptions;
if (!$props.defaultFilter) return update(); if (!$props.defaultFilter) return update();
if ($props.url) { if (
$props.url &&
($props.limit || (!$props.limit && Object.keys(myOptions.value).length === 0))
) {
newOptions = await fetchFilter(val); newOptions = await fetchFilter(val);
} else newOptions = filter(val, myOptionsOriginal.value); } else newOptions = filter(val, myOptionsOriginal.value);
update( update(
@ -221,7 +228,12 @@ function nullishToTrue(value) {
<QIcon <QIcon
v-show="value" v-show="value"
name="close" name="close"
@click.stop="value = null" @click.stop="
() => {
value = null;
emit('remove');
}
"
class="cursor-pointer" class="cursor-pointer"
size="xs" size="xs"
/> />

View File

@ -31,7 +31,7 @@ const dialog = ref(null);
<div class="container order-catalog-item overflow-hidden"> <div class="container order-catalog-item overflow-hidden">
<QCard class="card shadow-6"> <QCard class="card shadow-6">
<div class="img-wrapper"> <div class="img-wrapper">
<VnImg :id="item.id" zoom-size="lg" class="image" /> <VnImg :id="item.id" class="image" />
<div v-if="item.hex && isCatalog" class="item-color-container"> <div v-if="item.hex && isCatalog" class="item-color-container">
<div <div
class="item-color" class="item-color"

View File

@ -73,8 +73,7 @@ const containerClasses = computed(() => {
.q-calendar-month__head--workweek, .q-calendar-month__head--workweek,
.q-calendar-month__head--weekday, .q-calendar-month__head--weekday,
// .q-calendar-month__workweek.q-past-day, // .q-calendar-month__workweek.q-past-day,
.q-calendar-month__week :nth-child(6), .q-calendar-month__week :nth-child(n+6):nth-child(-n+7) {
:nth-child(7) {
color: var(--vn-label-color); color: var(--vn-label-color);
} }

View File

@ -1,45 +1,62 @@
<script setup> <script setup>
import { computed, ref } from 'vue'; import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useSession } from 'src/composables/useSession'; import { useSession } from 'src/composables/useSession';
import { useColor } from 'src/composables/useColor'; import { useColor } from 'src/composables/useColor';
import { getCssVar } from 'quasar';
const $props = defineProps({ const $props = defineProps({
workerId: { type: Number, required: true }, workerId: { type: Number, required: true },
description: { type: String, default: null }, description: { type: String, default: null },
size: { type: String, default: null },
title: { type: String, default: null }, title: { type: String, default: null },
color: { type: String, default: null },
}); });
const { getTokenMultimedia } = useSession(); const { getTokenMultimedia } = useSession();
const token = getTokenMultimedia(); const token = getTokenMultimedia();
const { t } = useI18n(); const { t } = useI18n();
const title = computed(() => $props.title ?? t('globals.system')); const src = computed(
() => `/api/Images/user/160x160/${$props.workerId}/download?access_token=${token}`
);
const title = computed(() => $props.title?.toUpperCase() || t('globals.system'));
const showLetter = ref(false); const showLetter = ref(false);
const backgroundColor = computed(() => {
const color = $props.color || useColor(title.value);
return getCssVar(color) || color;
});
watch(src, () => (showLetter.value = false));
</script> </script>
<template> <template>
<div class="avatar-picture column items-center"> <div class="column items-center">
<QAvatar <QAvatar
:style="{ :style="{ backgroundColor }"
backgroundColor: useColor(title), v-bind="$attrs"
}" :title="title || t('globals.system')"
:size="$props.size"
:title="title"
> >
<template v-if="showLetter">{{ title.charAt(0) }}</template> <template v-if="showLetter">
<QImg {{ title.charAt(0) }}
v-else </template>
:src="`/api/Images/user/160x160/${$props.workerId}/download?access_token=${token}`" <QImg v-else :src="src" spinner-color="white" @error="showLetter = true" />
spinner-color="white"
@error="showLetter = true"
/>
</QAvatar> </QAvatar>
<div class="description"> <div class="description">
<slot name="description" v-if="$props.description"> <slot name="description" v-if="description">
<p> <p v-text="description" />
{{ $props.description }}
</p>
</slot> </slot>
</div> </div>
</div> </div>
</template> </template>
<style lang="scss" scoped>
[size='xxl'] {
.q-avatar,
.q-img {
width: 80px;
height: 80px;
}
.q-img {
object-fit: cover;
}
}
</style>

View File

@ -57,7 +57,7 @@ const $props = defineProps({
}, },
}); });
defineExpose({ search }); defineExpose({ search, sanitizer });
const emit = defineEmits([ const emit = defineEmits([
'update:modelValue', 'update:modelValue',
'refresh', 'refresh',
@ -65,6 +65,7 @@ const emit = defineEmits([
'search', 'search',
'init', 'init',
'remove', 'remove',
'setUserParams',
]); ]);
const arrayData = useArrayData($props.dataKey, { const arrayData = useArrayData($props.dataKey, {
@ -81,22 +82,26 @@ onMounted(() => {
}); });
function setUserParams(watchedParams) { function setUserParams(watchedParams) {
if (!watchedParams) return; if (!watchedParams || Object.keys(watchedParams).length == 0) return;
if (typeof watchedParams == 'string') watchedParams = JSON.parse(watchedParams); if (typeof watchedParams == 'string') watchedParams = JSON.parse(watchedParams);
if (typeof watchedParams?.filter == 'string')
watchedParams.filter = JSON.parse(watchedParams.filter);
watchedParams = { ...watchedParams, ...watchedParams.filter?.where }; watchedParams = { ...watchedParams, ...watchedParams.filter?.where };
const order = watchedParams.filter?.order;
delete watchedParams.filter; delete watchedParams.filter;
userParams.value = { ...userParams.value, ...watchedParams }; userParams.value = { ...userParams.value, ...sanitizer(watchedParams) };
emit('setUserParams', userParams.value, order);
} }
watch( watch(
() => route.query[$props.searchUrl], () => [route.query[$props.searchUrl], arrayData.store.userParams],
(val) => setUserParams(val) ([newSearchUrl, newUserParams], [oldSearchUrl, oldUserParams]) => {
); if (newSearchUrl || oldSearchUrl) setUserParams(newSearchUrl);
if (newUserParams || oldUserParams) setUserParams(newUserParams);
watch( }
() => arrayData.store.userParams,
(val) => setUserParams(val)
); );
watch( watch(
@ -190,6 +195,14 @@ function formatValue(value) {
return `"${value}"`; return `"${value}"`;
} }
function sanitizer(params) {
for (const [key, value] of Object.entries(params)) {
if (typeof value == 'object')
params[key] = Object.values(value)[0].replaceAll('%', '');
}
return params;
}
</script> </script>
<template> <template>
@ -272,7 +285,7 @@ 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="sanitizer(userParams)" :search-fn="search"></slot>
</QList> </QList>
</QForm> </QForm>
<QInnerLoading <QInnerLoading

View File

@ -1,6 +1,8 @@
<script setup> <script setup>
import { ref, computed } from 'vue'; import { ref } from 'vue';
import { useSession } from 'src/composables/useSession'; import { useSession } from 'src/composables/useSession';
import noImage from '/no-user.png';
import { useRole } from 'src/composables/useRole';
const $props = defineProps({ const $props = defineProps({
storage: { storage: {
@ -11,14 +13,17 @@ const $props = defineProps({
type: String, type: String,
default: 'catalog', default: 'catalog',
}, },
size: { resolution: {
type: String, type: String,
default: '200x200', default: '200x200',
}, },
zoomSize: { zoomResolution: {
type: String, type: String,
required: false, default: null,
default: 'lg', },
zoom: {
type: Boolean,
default: true,
}, },
id: { id: {
type: Number, type: Number,
@ -28,14 +33,16 @@ const $props = defineProps({
const show = ref(false); const show = ref(false);
const token = useSession().getTokenMultimedia(); const token = useSession().getTokenMultimedia();
const timeStamp = ref(`timestamp=${Date.now()}`); const timeStamp = ref(`timestamp=${Date.now()}`);
import noImage from '/no-user.png';
import { useRole } from 'src/composables/useRole';
const url = computed(() => {
const isEmployee = useRole().isEmployee(); const isEmployee = useRole().isEmployee();
const getUrl = (zoom = false) => {
const curResolution = zoom
? $props.zoomResolution || $props.resolution
: $props.resolution;
return isEmployee return isEmployee
? `/api/${$props.storage}/${$props.collection}/${$props.size}/${$props.id}/download?access_token=${token}&${timeStamp.value}` ? `/api/${$props.storage}/${$props.collection}/${curResolution}/${$props.id}/download?access_token=${token}&${timeStamp.value}`
: noImage; : noImage;
}); };
const reload = () => { const reload = () => {
timeStamp.value = `timestamp=${Date.now()}`; timeStamp.value = `timestamp=${Date.now()}`;
}; };
@ -45,23 +52,21 @@ defineExpose({
</script> </script>
<template> <template>
<QImg <QImg
:class="{ zoomIn: $props.zoomSize }" :class="{ zoomIn: zoom }"
:src="url" :src="getUrl()"
v-bind="$attrs" v-bind="$attrs"
@click="show = !show" @click.stop="show = $props.zoom ? true : false"
spinner-color="primary" spinner-color="primary"
/> />
<QDialog v-model="show" v-if="$props.zoomSize"> <QDialog v-if="$props.zoom" v-model="show">
<QImg <QImg
:src="url" :src="getUrl(true)"
size="full"
class="img_zoom"
v-bind="$attrs" v-bind="$attrs"
spinner-color="primary" spinner-color="primary"
class="img_zoom"
/> />
</QDialog> </QDialog>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.q-img { .q-img {
&.zoomIn { &.zoomIn {

View File

@ -34,8 +34,8 @@ function handleKeyUp(event) {
} }
async function insert() { async function insert() {
const body = $props.body; const body = $props.body;
Object.assign(body, { text: newNote.value }); const newBody = { ...body, ...{ text: newNote.value } };
await axios.post($props.url, body); await axios.post($props.url, newBody);
await vnPaginateRef.value.fetch(); await vnPaginateRef.value.fetch();
newNote.value = ''; newNote.value = '';
} }

View File

@ -221,7 +221,7 @@ defineExpose({ fetch, addFilter, paginate });
> >
<slot name="body" :rows="store.data"></slot> <slot name="body" :rows="store.data"></slot>
<div v-if="isLoading" class="info-row q-pa-md text-center"> <div v-if="isLoading" class="info-row q-pa-md text-center">
<QSpinner color="orange" size="md" /> <QSpinner color="primary" size="md" />
</div> </div>
</QInfiniteScroll> </QInfiniteScroll>
</template> </template>

View File

@ -1,15 +1,12 @@
<script setup>
defineProps({ wrap: { type: Boolean, default: false } });
</script>
<template> <template>
<div class="vn-row q-gutter-md q-mb-md" :class="{ wrap }"> <div class="vn-row q-gutter-md q-mb-md">
<slot></slot> <slot />
</div> </div>
</template> </template>
<style lang="scss" scopped> <style lang="scss" scoped>
.vn-row { .vn-row {
display: flex; display: flex;
> * { > :deep(*) {
flex: 1; flex: 1;
} }
} }

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { onBeforeMount } from 'vue'; import { watch, computed } from 'vue';
import { date } from 'quasar'; import { date } from 'quasar';
import VnPaginate from 'src/components/ui/VnPaginate.vue'; import VnPaginate from 'src/components/ui/VnPaginate.vue';
import VnAvatar from '../ui/VnAvatar.vue'; import VnAvatar from '../ui/VnAvatar.vue';
@ -10,7 +10,8 @@ const $props = defineProps({
where: { type: Object, default: () => {} }, where: { type: Object, default: () => {} },
}); });
const filter = { const filter = computed(() => {
return {
fields: ['smsFk'], fields: ['smsFk'],
include: { include: {
relation: 'sms', relation: 'sms',
@ -32,9 +33,9 @@ const filter = {
}, },
}, },
}, },
...{ where: $props.where },
}; };
});
onBeforeMount(() => (filter.where = $props.where));
function formatNumber(number) { function formatNumber(number) {
if (number.length <= 10) return number; if (number.length <= 10) return number;

View File

@ -18,4 +18,3 @@ const { t } = useI18n();
</slot> </slot>
<WorkerDescriptorProxy v-if="$props.workerId" :id="$props.workerId" /> <WorkerDescriptorProxy v-if="$props.workerId" :id="$props.workerId" />
</template> </template>
<style scoped></style>

View File

@ -3,12 +3,14 @@ import { useRole } from './useRole';
import { useAcl } from './useAcl'; import { useAcl } from './useAcl';
import { useUserConfig } from './useUserConfig'; import { useUserConfig } from './useUserConfig';
import axios from 'axios'; import axios from 'axios';
import { useRouter } from 'vue-router';
import useNotify from './useNotify'; import useNotify from './useNotify';
import { useTokenConfig } from './useTokenConfig'; import { useTokenConfig } from './useTokenConfig';
const TOKEN_MULTIMEDIA = 'tokenMultimedia'; const TOKEN_MULTIMEDIA = 'tokenMultimedia';
const TOKEN = 'token'; const TOKEN = 'token';
export function useSession() { export function useSession() {
const router = useRouter();
const { notify } = useNotify(); const { notify } = useNotify();
let isCheckingToken = false; let isCheckingToken = false;
let intervalId = null; let intervalId = null;
@ -102,6 +104,31 @@ export function useSession() {
startInterval(); startInterval();
} }
async function setLogin(data) {
const {
data: { multimediaToken },
} = await axios.get('VnUsers/ShareToken', {
headers: { Authorization: data.token },
});
if (!multimediaToken) return;
await login({
...data,
created: Date.now(),
tokenMultimedia: multimediaToken.id,
});
notify('login.loginSuccess', 'positive');
const currentRoute = router.currentRoute.value;
if (currentRoute.query?.redirect) {
router.push(currentRoute.query.redirect);
} else {
router.push({ name: 'Dashboard' });
}
}
function isLoggedIn() { function isLoggedIn() {
const localToken = localStorage.getItem(TOKEN); const localToken = localStorage.getItem(TOKEN);
const sessionToken = sessionStorage.getItem(TOKEN); const sessionToken = sessionStorage.getItem(TOKEN);
@ -163,6 +190,7 @@ export function useSession() {
setToken, setToken,
destroy, destroy,
login, login,
setLogin,
isLoggedIn, isLoggedIn,
checkValidity, checkValidity,
setSession, setSession,

View File

@ -90,6 +90,9 @@ globals:
salesPerson: SalesPerson salesPerson: SalesPerson
send: Send send: Send
code: Code code: Code
since: Since
from: From
to: To
pageTitles: pageTitles:
logIn: Login logIn: Login
summary: Summary summary: Summary
@ -246,6 +249,8 @@ globals:
mailForwarding: Mail forwarding mailForwarding: Mail forwarding
mailAlias: Mail alias mailAlias: Mail alias
privileges: Privileges privileges: Privileges
ldap: LDAP
samba: Samba
created: Created created: Created
worker: Worker worker: Worker
now: Now now: Now
@ -257,6 +262,7 @@ globals:
unsavedPopup: unsavedPopup:
title: Unsaved changes will be lost title: Unsaved changes will be lost
subtitle: Are you sure exit without saving? subtitle: Are you sure exit without saving?
createInvoiceIn: Create invoice in
errors: errors:
statusUnauthorized: Access denied statusUnauthorized: Access denied
statusInternalServerError: An internal server error has ocurred statusInternalServerError: An internal server error has ocurred

View File

@ -90,6 +90,9 @@ globals:
salesPerson: Comercial salesPerson: Comercial
send: Enviar send: Enviar
code: Código code: Código
since: Desde
from: Desde
to: Hasta
pageTitles: pageTitles:
logIn: Inicio de sesión logIn: Inicio de sesión
summary: Resumen summary: Resumen
@ -248,6 +251,8 @@ globals:
components: Componentes components: Componentes
pictures: Fotos pictures: Fotos
packages: Bultos packages: Bultos
ldap: LDAP
samba: Samba
created: Fecha creación created: Fecha creación
worker: Trabajador worker: Trabajador
now: Ahora now: Ahora
@ -259,6 +264,8 @@ globals:
unsavedPopup: unsavedPopup:
title: Los cambios que no haya guardado se perderán title: Los cambios que no haya guardado se perderán
subtitle: ¿Seguro que quiere salir sin guardar? subtitle: ¿Seguro que quiere salir sin guardar?
createInvoiceIn: Crear factura recibida
errors: errors:
statusUnauthorized: Acceso denegado statusUnauthorized: Acceso denegado
statusInternalServerError: Ha ocurrido un error interno del servidor statusInternalServerError: Ha ocurrido un error interno del servidor
@ -695,8 +702,6 @@ invoiceOut:
percentageText: '{getPercentage}% {getAddressNumber} de {getNAddresses}' percentageText: '{getPercentage}% {getAddressNumber} de {getNAddresses}'
pdfsNumberText: '{nPdfs} de {totalPdfs} PDFs' pdfsNumberText: '{nPdfs} de {totalPdfs} PDFs'
negativeBases: negativeBases:
from: Desde
to: Hasta
company: Empresa company: Empresa
country: País country: País
clientId: Id cliente clientId: Id cliente
@ -1238,8 +1243,6 @@ components:
# LatestBuysFilter # LatestBuysFilter
salesPersonFk: Comprador salesPersonFk: Comprador
supplierFk: Proveedor supplierFk: Proveedor
from: Desde
to: Hasta
active: Activo active: Activo
visible: Visible visible: Visible
floramondo: Floramondo floramondo: Floramondo

View File

@ -1,11 +1,9 @@
<script setup> <script setup>
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
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 VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import axios from 'axios'; import axios from 'axios';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';

View File

@ -1,19 +1,15 @@
<script setup> <script setup>
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { ref } from 'vue'; import { ref, computed } from 'vue';
import FetchData from 'components/FetchData.vue';
import VnPaginate from 'components/ui/VnPaginate.vue';
import VnSearchbar from 'components/ui/VnSearchbar.vue';
import CardList from 'src/components/ui/CardList.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import AclFilter from './Acls/AclFilter.vue';
import AclFormView from './Acls/AclFormView.vue';
import { useVnConfirm } from 'composables/useVnConfirm';
import { useStateStore } from 'stores/useStateStore'; import { 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 { useQuasar } from 'quasar';
import FetchData from 'components/FetchData.vue';
import VnTable from 'components/VnTable/VnTable.vue';
import VnSearchbar from 'components/ui/VnSearchbar.vue';
import VnConfirm from 'components/ui/VnConfirm.vue';
defineProps({ defineProps({
id: { id: {
@ -25,10 +21,9 @@ defineProps({
const { notify } = useNotify(); const { notify } = useNotify();
const { t } = useI18n(); const { t } = useI18n();
const stateStore = useStateStore(); const stateStore = useStateStore();
const { openConfirmationModal } = useVnConfirm(); const quasar = useQuasar();
const paginateRef = ref(); const tableRef = ref();
const formDialog = ref(false);
const rolesOptions = ref([]); const rolesOptions = ref([]);
const exprBuilder = (param, value) => { const exprBuilder = (param, value) => {
@ -40,21 +35,86 @@ const exprBuilder = (param, value) => {
} }
}; };
const deleteAcl = async (id) => { const columns = computed(() => [
{
align: 'left',
name: 'id',
label: t('id'),
isId: true,
field: 'id',
cardVisible: true,
},
{
align: 'left',
name: 'model',
label: t('model'),
field: 'model',
cardVisible: true,
create: true,
},
{
align: 'left',
name: 'principalId',
label: t('principalId'),
field: 'principalId',
cardVisible: true,
create: true,
},
{
align: 'left',
name: 'property',
label: t('property'),
field: 'property',
cardVisible: true,
create: true,
},
{
align: 'left',
name: 'accessType',
label: t('accessType'),
field: 'accessType',
cardVisible: true,
create: true,
},
{
align: 'right',
label: '',
name: 'tableActions',
actions: [
{
title: t('Delete'),
icon: 'delete',
action: deleteAcl,
isPrimary: true,
},
],
},
]);
const deleteAcl = async ({ id }) => {
try { try {
await new Promise((resolve) => {
quasar
.dialog({
component: VnConfirm,
componentProps: {
title: t('Remove ACL'),
message: t('Do you want to remove this ACL?'),
},
})
.onOk(() => {
resolve(true);
})
.onCancel(() => {
resolve(false);
});
});
await axios.delete(`ACLs/${id}`); await axios.delete(`ACLs/${id}`);
paginateRef.value.fetch(); tableRef.value.reload();
notify('ACL removed', 'positive'); notify('ACL removed', 'positive');
} catch (error) { } catch (error) {
console.error('Error deleting Acl: ', error); console.error('Error deleting Acl: ', error);
} }
}; };
function showFormDialog(data) {
formDialog.value = {
show: true,
formInitialData: { ...data },
};
}
</script> </script>
<template> <template>
@ -64,8 +124,7 @@ function showFormDialog(data) {
@on-fetch="(data) => (rolesOptions = data)" @on-fetch="(data) => (rolesOptions = data)"
auto-load auto-load
/> />
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#searchbar">
<VnSearchbar <VnSearchbar
data-key="AccountAcls" data-key="AccountAcls"
url="ACLs" url="ACLs"
@ -73,74 +132,27 @@ function showFormDialog(data) {
:label="t('acls.search')" :label="t('acls.search')"
:info="t('acls.searchInfo')" :info="t('acls.searchInfo')"
/> />
</Teleport>
</template>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above> <QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
<QScrollArea class="fit text-grey-8">
<AclFilter data-key="AccountAcls" />
</QScrollArea>
</QDrawer> </QDrawer>
<VnTable
<QPage class="flex justify-center q-pa-md"> ref="tableRef"
<div class="vn-card-list">
<VnPaginate
ref="paginateRef"
data-key="AccountAcls" data-key="AccountAcls"
url="ACLs" :url="`ACLs`"
:expr-builder="exprBuilder" :create="{
> urlCreate: 'ACLs',
<template #body="{ rows }"> title: 'Create ACL',
<CardList onDataSaved: () => tableRef.reload(),
v-for="row of rows" formInitialData: {},
:id="row.id" }"
:key="row.id" order="id DESC"
:title="`${row.model}.${row.property}`" :columns="columns"
@click="showFormDialog(row)" default-mode="table"
> auto-load
<template #list-items> :right-search="true"
<VnLv :label="t('acls.role')" :value="row.principalId" /> :is-editable="true"
<VnLv :label="t('acls.accessType')" :value="row.accessType" /> :use-model="true"
<VnLv
:label="t('acls.permissions')"
:value="row.permission"
/> />
</template> </template>
<template #actions>
<QBtn
:label="t('globals.delete')"
@click.stop="
openConfirmationModal(
t('ACL will be removed'),
t('Are you sure you want to continue?'),
() => deleteAcl(row.id)
)
"
color="primary"
style="margin-top: 15px"
/>
</template>
</CardList>
</template>
</VnPaginate>
</div>
<QDialog
v-model="formDialog.show"
transition-show="scale"
transition-hide="scale"
>
<AclFormView
:form-initial-data="formDialog.formInitialData"
@on-data-change="paginateRef.fetch()"
:roles-options="rolesOptions"
/>
</QDialog>
<QPageSticky position="bottom-right" :offset="[18, 18]">
<QBtn fab icon="add" color="primary" @click="showFormDialog()">
<QTooltip class="text-no-wrap">{{ t('New ACL') }}</QTooltip>
</QBtn>
</QPageSticky>
</QPage>
</template>
<i18n> <i18n>
es: es:
@ -148,4 +160,6 @@ es:
ACL removed: ACL eliminado ACL removed: ACL eliminado
ACL will be removed: El ACL será eliminado ACL will be removed: El ACL será eliminado
Are you sure you want to continue?: ¿Seguro que quieres continuar? Are you sure you want to continue?: ¿Seguro que quieres continuar?
Remove ACL: Eliminar Acl
Do you want to remove this ACL?: ¿Quieres eliminar este ACL?
</i18n> </i18n>

View File

@ -1,30 +1,13 @@
<script setup> <script setup>
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { ref } from 'vue'; import { ref, computed } from 'vue';
import VnTable from 'components/VnTable/VnTable.vue';
import VnPaginate from 'components/ui/VnPaginate.vue';
import VnSearchbar from 'components/ui/VnSearchbar.vue'; import VnSearchbar from 'components/ui/VnSearchbar.vue';
import CardList from 'src/components/ui/CardList.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import AliasSummary from './Alias/Card/AliasSummary.vue';
import AliasCreateForm from './Alias/AliasCreateForm.vue';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
defineProps({ const tableRef = ref();
id: {
type: Number,
default: 0,
},
});
const { t } = useI18n(); const { t } = useI18n();
const { viewSummary } = useSummaryDialog();
const router = useRouter();
const stateStore = useStateStore(); const stateStore = useStateStore();
const aliasCreateDialogRef = ref(null);
const exprBuilder = (param, value) => { const exprBuilder = (param, value) => {
switch (param) { switch (param) {
@ -34,10 +17,32 @@ const exprBuilder = (param, value) => {
: { alias: { like: `%${value}%` } }; : { alias: { like: `%${value}%` } };
} }
}; };
const columns = computed(() => [
const navigate = (id) => router.push({ name: 'AliasSummary', params: { id } }); {
align: 'left',
const openCreateModal = () => aliasCreateDialogRef.value.show(); name: 'id',
label: t('id'),
isId: true,
field: 'id',
cardVisible: true,
},
{
align: 'left',
name: 'alias',
label: t('alias'),
field: 'alias',
cardVisible: true,
create: true,
},
{
align: 'left',
name: 'description',
label: t('description'),
field: 'description',
cardVisible: true,
create: true,
},
]);
</script> </script>
<template> <template>
@ -52,54 +57,22 @@ const openCreateModal = () => aliasCreateDialogRef.value.show();
/> />
</Teleport> </Teleport>
</template> </template>
<QPage class="flex justify-center q-pa-md"> <VnTable
<div class="vn-card-list"> ref="tableRef"
<VnPaginate
ref="paginateRef"
data-key="AccountAliasList" data-key="AccountAliasList"
url="MailAliases" :url="`MailAliases`"
:expr-builder="exprBuilder" :create="{
> urlCreate: 'MailAliases',
<template #body="{ rows }"> title: 'Create MailAlias',
<CardList onDataSaved: ({ id }) => tableRef.redirect(id),
v-for="row of rows" formInitialData: {},
:id="row.id" }"
:key="row.id" order="id DESC"
:title="row.alias" :columns="columns"
@click="navigate(row.id)" default-mode="table"
> auto-load
<template #list-items> redirect="account/alias"
<VnLv :label="t('mailAlias.alias')" :value="row.alias"> :is-editable="true"
</VnLv> :use-model="true"
<VnLv
:label="t('mailAlias.description')"
:value="row.description"
>
</VnLv>
</template>
<template #actions>
<QBtn
:label="t('components.smartCard.openSummary')"
@click.stop="viewSummary(row.id, AliasSummary)"
color="primary"
style="margin-top: 15px"
/> />
</template> </template>
</CardList>
</template>
</VnPaginate>
</div>
<QDialog
ref="aliasCreateDialogRef"
transition-show="scale"
transition-hide="scale"
>
<AliasCreateForm />
</QDialog>
<QPageSticky position="bottom-right" :offset="[18, 18]">
<QBtn fab icon="add" color="primary" @click="openCreateModal()">
<QTooltip class="text-no-wrap">{{ t('mailAlias.newAlias') }}</QTooltip>
</QBtn>
</QPageSticky>
</QPage>
</template>

View File

@ -2,11 +2,9 @@
import { ref } from 'vue'; import { ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import VnPaginate from 'components/ui/VnPaginate.vue'; import VnPaginate from 'components/ui/VnPaginate.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 { toDateTimeFormat } from 'src/filters/date.js'; import { toDateTimeFormat } from 'src/filters/date.js';
import axios from 'axios'; import axios from 'axios';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';

View File

@ -2,7 +2,6 @@
import { reactive, ref } from 'vue'; import { reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import FormModelPopup from 'components/FormModelPopup.vue'; import FormModelPopup from 'components/FormModelPopup.vue';
import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelect from 'src/components/common/VnSelect.vue';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
@ -15,7 +14,6 @@ const newAccountForm = reactive({
active: true, active: true,
}); });
const rolesOptions = ref([]); const rolesOptions = ref([]);
const redirectToAccountBasicData = (_, { id }) => { const redirectToAccountBasicData = (_, { id }) => {
router.push({ name: 'AccountBasicData', params: { id } }); router.push({ name: 'AccountBasicData', params: { id } });
}; };

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 VnSelect from 'components/common/VnSelect.vue'; import VnSelect from 'components/common/VnSelect.vue';

View File

@ -1,12 +1,10 @@
<script setup> <script setup>
import { ref, onMounted, computed } from 'vue'; import { ref, onMounted, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
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 VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import { useArrayData } from 'src/composables/useArrayData'; import { useArrayData } from 'src/composables/useArrayData';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
import axios from 'axios'; import axios from 'axios';
@ -15,7 +13,6 @@ const { t } = useI18n();
const { notify } = useNotify(); const { notify } = useNotify();
const arrayData = useArrayData('AccountLdap'); const arrayData = useArrayData('AccountLdap');
const URL_UPDATE = `LdapConfigs/${1}`; const URL_UPDATE = `LdapConfigs/${1}`;
const URL_CREATE = `LdapConfigs`; const URL_CREATE = `LdapConfigs`;
const DEFAULT_DATA = { const DEFAULT_DATA = {
@ -27,11 +24,9 @@ const DEFAULT_DATA = {
server: null, server: null,
userDn: null, userDn: null,
}; };
const initialData = ref({ const initialData = ref({
...DEFAULT_DATA, ...DEFAULT_DATA,
}); });
const hasData = computed({ const hasData = computed({
get: () => initialData.value.hasData, get: () => initialData.value.hasData,
set: (val) => { set: (val) => {
@ -40,12 +35,10 @@ const hasData = computed({
else formCustomFn.value = null; else formCustomFn.value = null;
}, },
}); });
const initialDataLoaded = ref(false); const initialDataLoaded = ref(false);
const formUrlCreate = ref(null); const formUrlCreate = ref(null);
const formUrlUpdate = ref(null); const formUrlUpdate = ref(null);
const formCustomFn = ref(null); const formCustomFn = ref(null);
const onTestConection = async () => { const onTestConection = async () => {
try { try {
await axios.get(`LdapConfigs/test`); await axios.get(`LdapConfigs/test`);
@ -54,7 +47,6 @@ const onTestConection = async () => {
console.error('Error testing connection', error); console.error('Error testing connection', error);
} }
}; };
const getInitialLdapConfig = async () => { const getInitialLdapConfig = async () => {
try { try {
initialDataLoaded.value = false; initialDataLoaded.value = false;
@ -79,7 +71,6 @@ const getInitialLdapConfig = async () => {
initialDataLoaded.value = true; initialDataLoaded.value = true;
} }
}; };
const deleteMailForward = async () => { const deleteMailForward = async () => {
try { try {
await axios.delete(URL_UPDATE); await axios.delete(URL_UPDATE);

View File

@ -1,33 +1,66 @@
<script setup> <script setup>
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { ref, computed } from 'vue';
import { computed, ref } from 'vue'; import VnTable from 'components/VnTable/VnTable.vue';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
import VnSearchbar from 'components/ui/VnSearchbar.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 AccountSummary from './Card/AccountSummary.vue';
import AccountFilter from './AccountFilter.vue';
import AccountCreate from './AccountCreate.vue';
import { useSummaryDialog } from 'src/composables/useSummaryDialog'; 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 { t } = useI18n();
const { viewSummary } = useSummaryDialog(); const { viewSummary } = useSummaryDialog();
const accountCreateDialogRef = ref(null); const tableRef = ref();
const showNewUserBtn = computed(() => useRole().hasAny(['itManagement']));
const filter = {
fields: ['id', 'nickname', 'name', 'role'],
include: { relation: 'role', scope: { fields: ['id', 'name'] } },
};
const columns = computed(() => [
{
align: 'left',
name: 'id',
label: t('id'),
isId: true,
field: 'id',
cardVisible: true,
},
{
align: 'left',
name: 'username',
label: t('nickname'),
isTitle: true,
component: 'input',
columnField: {
component: null,
},
columnFilter: {
inWhere: true,
},
cardVisible: true,
create: true,
},
{
align: 'left',
name: 'name',
label: t('name'),
component: 'input',
columnField: {
component: null,
},
columnFilter: {
inWhere: true,
},
cardVisible: true,
create: true,
},
{
align: 'right',
label: '',
name: 'tableActions',
actions: [
{
title: t('View Summary'),
icon: 'preview',
action: (row) => viewSummary(row.id, AccountSummary),
},
],
},
]);
const exprBuilder = (param, value) => { const exprBuilder = (param, value) => {
switch (param) { switch (param) {
case 'search': case 'search':
@ -46,99 +79,25 @@ const exprBuilder = (param, value) => {
return { [param]: value }; 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> </script>
<template> <template>
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#searchbar">
<VnSearchbar <VnSearchbar
data-key="AccountList"
url="VnUsers/preview"
:expr-builder="exprBuilder"
:label="t('account.search')" :label="t('account.search')"
data-key="AccountUsers"
:expr-builder="exprBuilder"
:info="t('account.searchInfo')" :info="t('account.searchInfo')"
/> />
</Teleport>
<Teleport to="#actions-append"> <VnTable
<div class="row q-gutter-x-sm"> ref="tableRef"
<QBtn data-key="AccountUsers"
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" url="VnUsers/preview"
order="id DESC"
:columns="columns"
default-mode="table"
auto-load auto-load
> redirect="account"
<template #body="{ rows }"> :use-model="true"
<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> </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

@ -1,23 +1,18 @@
<script setup> <script setup>
import { ref, onMounted, computed } from 'vue'; import { ref, onMounted, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
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 VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import { useArrayData } from 'src/composables/useArrayData'; import { useArrayData } from 'src/composables/useArrayData';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
import axios from 'axios'; import axios from 'axios';
const { t } = useI18n(); const { t } = useI18n();
const { notify } = useNotify(); const { notify } = useNotify();
const arrayData = useArrayData('AccountSamba'); const arrayData = useArrayData('AccountSamba');
const formModel = ref(null); const formModel = ref(null);
const URL_UPDATE = `SambaConfigs/${1}`; const URL_UPDATE = `SambaConfigs/${1}`;
const URL_CREATE = `SambaConfigs`; const URL_CREATE = `SambaConfigs`;

View File

@ -25,9 +25,11 @@ const searchBarDataKeys = {
base-url="MailAliases" base-url="MailAliases"
:descriptor="AliasDescriptor" :descriptor="AliasDescriptor"
:search-data-key="searchBarDataKeys[routeName]" :search-data-key="searchBarDataKeys[routeName]"
:search-custom-route-redirect="customRouteRedirectName" :searchbar-props="{
:search-redirect="!!customRouteRedirectName" redirect: !!customRouteRedirectName,
:searchbar-label="t('mailAlias.search')" customRouteRedirectName,
:searchbar-info="t('mailAlias.searchInfo')" info: t('mailAlias.searchInfo'),
label: t('mailAlias.search'),
}"
/> />
</template> </template>

View File

@ -28,6 +28,7 @@ const entityId = computed(() => $props.id || route.params.id);
ref="summary" ref="summary"
:url="`MailAliases/${entityId}`" :url="`MailAliases/${entityId}`"
@on-fetch="(data) => (alias = data)" @on-fetch="(data) => (alias = data)"
data-key="MailAliasesSummary"
> >
<template #header> {{ alias.id }} - {{ alias.alias }} </template> <template #header> {{ alias.id }} - {{ alias.alias }} </template>
<template #body> <template #body>

View File

@ -26,9 +26,11 @@ const searchBarDataKeys = {
data-key="Account" data-key="Account"
:descriptor="AccountDescriptor" :descriptor="AccountDescriptor"
:search-data-key="searchBarDataKeys[routeName]" :search-data-key="searchBarDataKeys[routeName]"
:search-custom-route-redirect="customRouteRedirectName" :searchbar-props="{
:search-redirect="!!customRouteRedirectName" redirect: !!customRouteRedirectName,
:searchbar-label="t('account.search')" customRouteRedirectName,
:searchbar-info="t('account.searchInfo')" label: t('account.search'),
info: t('account.searchInfo'),
}"
/> />
</template> </template>

View File

@ -54,7 +54,7 @@ const hasAccount = ref(false);
</template> </template>
<template #before> <template #before>
<!-- falla id :id="entityId.value" collection="user" size="160x160" --> <!-- falla id :id="entityId.value" collection="user" size="160x160" -->
<VnImg :id="entityId" collection="user" size="160x160" class="photo"> <VnImg :id="entityId" collection="user" resolution="160x160" class="photo">
<template #error> <template #error>
<div <div
class="absolute-full picture text-center q-pa-md flex flex-center" class="absolute-full picture text-center q-pa-md flex flex-center"

View File

@ -8,7 +8,7 @@ import { useRoute } from 'vue-router';
import { useArrayData } from 'src/composables/useArrayData'; import { useArrayData } from 'src/composables/useArrayData';
import CustomerChangePassword from 'src/pages/Customer/components/CustomerChangePassword.vue'; import CustomerChangePassword from 'src/pages/Customer/components/CustomerChangePassword.vue';
import VnConfirm from 'src/components/ui/VnConfirm.vue'; import VnConfirm from 'src/components/ui/VnConfirm.vue';
import useNotify from 'src/composables/useNotify.js';
const quasar = useQuasar(); const quasar = useQuasar();
const $props = defineProps({ const $props = defineProps({
hasAccount: { hasAccount: {
@ -21,7 +21,7 @@ const { t } = useI18n();
const { hasAccount } = toRefs($props); const { hasAccount } = toRefs($props);
const { openConfirmationModal } = useVnConfirm(); const { openConfirmationModal } = useVnConfirm();
const route = useRoute(); const route = useRoute();
const { notify } = useNotify();
const account = computed(() => useArrayData('AccountId').store.data[0]); const account = computed(() => useArrayData('AccountId').store.data[0]);
account.value.hasAccount = hasAccount.value; account.value.hasAccount = hasAccount.value;
const entityId = computed(() => +route.params.id); const entityId = computed(() => +route.params.id);
@ -71,55 +71,17 @@ async function sync() {
type: 'positive', type: 'positive',
}); });
} }
const removeAccount = async () => {
try {
await axios.delete(`VnUsers/${account.value.id}`);
notify(t('Account removed'), 'positive');
} catch (error) {
console.error('Error deleting user', error);
}
};
</script> </script>
<template> <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 <QItem
v-if="account.hasAccount" v-if="account.hasAccount"
v-ripple v-ripple
@ -168,20 +130,10 @@ async function sync() {
</QItem> </QItem>
<QSeparator /> <QSeparator />
<QItem <!-- <QItem @click="removeAccount(id)" v-ripple clickable>
@click="
openConfirmationModal(
t('account.card.actions.delete.title'),
t('account.card.actions.delete.subTitle'),
removeAccount
)
"
v-ripple
clickable
>
<QItemSection avatar> <QItemSection avatar>
<QIcon name="delete" /> <QIcon name="delete" />
</QItemSection> </QItemSection>
<QItemSection>{{ t('account.card.actions.delete.name') }}</QItemSection> <QItemSection>{{ t('account.card.actions.delete.name') }}</QItemSection>
</QItem> </QItem> -->
</template> </template>

View File

@ -85,7 +85,7 @@ const fetchMailAliases = async () => {
paginateRef.value.fetch(); paginateRef.value.fetch();
}; };
const getAccountData = async () => { const getAccountData = async (reload = true) => {
loading.value = true; loading.value = true;
hasAccount.value = await fetchAccountExistence(); hasAccount.value = await fetchAccountExistence();
if (!hasAccount.value) { if (!hasAccount.value) {
@ -93,7 +93,7 @@ const getAccountData = async () => {
store.data = []; store.data = [];
return; return;
} }
await fetchMailAliases(); reload && (await fetchMailAliases());
loading.value = false; loading.value = false;
}; };
@ -102,13 +102,11 @@ const openCreateMailAliasForm = () => createMailAliasDialogRef.value.show();
watch( watch(
() => route.params.id, () => route.params.id,
() => { () => {
store.url = urlPath;
store.filter = filter.value;
getAccountData(); getAccountData();
} }
); );
onMounted(async () => await getAccountData()); onMounted(async () => await getAccountData(false));
</script> </script>
<template> <template>

View File

@ -1,23 +1,17 @@
<script setup> <script setup>
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import VnPaginate from 'components/ui/VnPaginate.vue'; import VnPaginate from 'components/ui/VnPaginate.vue';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
const props = defineProps({ const props = defineProps({
dataKey: { type: String, required: true }, dataKey: { type: String, required: true },
}); });
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const paginateRef = ref(null); const paginateRef = ref(null);
const arrayData = useArrayData(props.dataKey); const arrayData = useArrayData(props.dataKey);
const store = arrayData.store; const store = arrayData.store;
const data = computed(() => { const data = computed(() => {
const dataCopy = store.data; const dataCopy = store.data;
return dataCopy.sort((a, b) => a.role?.name.localeCompare(b.role?.name)); return dataCopy.sort((a, b) => a.role?.name.localeCompare(b.role?.name));
@ -37,7 +31,6 @@ const filter = computed(() => ({
})); }));
const urlPath = 'RoleMappings'; const urlPath = 'RoleMappings';
const columns = computed(() => [ const columns = computed(() => [
{ {
name: 'name', name: 'name',

View File

@ -1,26 +1,48 @@
<script setup> <script setup>
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { computed, ref } from 'vue';
import { ref } from 'vue'; import VnTable from 'components/VnTable/VnTable.vue';
import { useRoute } from 'vue-router';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import CardList from 'src/components/ui/CardList.vue';
import RoleSummary from './Card/RoleSummary.vue';
import RoleForm from './Card/RoleForm.vue';
import VnSearchbar from 'components/ui/VnSearchbar.vue'; import VnSearchbar from 'components/ui/VnSearchbar.vue';
import AccountRolesFilter from './AccountRolesFilter.vue';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import { useSummaryDialog } from 'src/composables/useSummaryDialog'; const route = useRoute();
const stateStore = useStateStore(); const stateStore = useStateStore();
const router = useRouter();
const { t } = useI18n(); const { t } = useI18n();
const { viewSummary } = useSummaryDialog(); const $props = defineProps({
id: {
const roleCreateDialogRef = ref(null); type: Number,
default: 0,
},
});
const tableRef = ref();
const entityId = computed(() => $props.id || route.params.id);
const columns = computed(() => [
{
align: 'left',
name: 'id',
label: t('id'),
isId: true,
columnFilter: {
inWhere: true,
},
cardVisible: true,
},
{
align: 'left',
name: 'name',
label: t('name'),
cardVisible: true,
create: true,
},
{
align: 'left',
name: 'description',
label: t('description'),
cardVisible: true,
create: true,
},
]);
const exprBuilder = (param, value) => { const exprBuilder = (param, value) => {
switch (param) { switch (param) {
case 'search': case 'search':
@ -37,95 +59,36 @@ const exprBuilder = (param, value) => {
return { [param]: { like: `%${value}%` } }; return { [param]: { like: `%${value}%` } };
} }
}; };
const openCreateModal = () => roleCreateDialogRef.value.show();
const getApiUrl = () => new URL(window.location).origin;
const navigate = (event, id) => {
if (event.ctrlKey || event.metaKey)
return window.open(`${getApiUrl()}/#/account/role/${id}/summary`);
router.push({ name: 'RoleSummary', params: { id } });
};
</script> </script>
<template> <template>
<template v-if="stateStore.isHeaderMounted()"> <template v-if="stateStore.isHeaderMounted()">
<Teleport to="#searchbar"> <Teleport to="#searchbar">
<VnSearchbar <VnSearchbar
data-key="RolesList" data-key="Roles"
url="VnRoles" :expr-builder="exprBuilder"
:label="t('role.searchRoles')" :label="t('role.searchRoles')"
:info="t('role.searchInfo')" :info="t('role.searchInfo')"
/> />
</Teleport> </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> </template>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above> <VnTable
<QScrollArea class="fit text-grey-8"> ref="tableRef"
<AccountRolesFilter data-key="RolesList" :expr-builder="exprBuilder" /> data-key="Roles"
</QScrollArea> :url="`VnRoles`"
</QDrawer> :create="{
<QPage class="column items-center q-pa-md"> urlCreate: 'VnRoles',
<div class="vn-card-list"> title: t('Create rol'),
<VnPaginate data-key="RolesList" url="VnRoles"> onDataSaved: ({ id }) => tableRef.redirect(id),
<template #body="{ rows }"> formInitialData: {
<CardList editorFk: entityId,
:id="row.id" },
:key="row.id" }"
:title="row.name" order="id ASC"
@click="navigate($event, row.id)" :columns="columns"
v-for="row of rows" default-mode="table"
> auto-load
<template #list-items> redirect="account/role"
<div style="flex-direction: column; width: 100%"> :is-editable="true"
<VnLv :label="t('role.card.name')" :value="row.name">
</VnLv>
<VnLv
:label="t('role.card.description')"
:value="row.description"
>
</VnLv>
</div>
</template>
<template #actions>
<QBtn
:label="t('components.smartCard.openSummary')"
@click.stop="viewSummary(row.id, RoleSummary)"
color="primary"
style="margin-top: 15px"
/> />
</template> </template>
</CardList>
</template>
</VnPaginate>
</div>
<QDialog
ref="roleCreateDialogRef"
transition-show="scale"
transition-hide="scale"
>
<RoleForm />
</QDialog>
<QPageSticky :offset="[20, 20]">
<QBtn fab icon="add" color="primary" @click="openCreateModal()" />
<QTooltip>
{{ t('role.newRole') }}
</QTooltip>
</QPageSticky>
</QPage>
</template>

View File

@ -1,6 +1,5 @@
<script setup> <script setup>
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
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';

View File

@ -2,7 +2,6 @@
import { computed } from 'vue'; import { computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import VnCard from 'components/common/VnCard.vue'; import VnCard from 'components/common/VnCard.vue';
import RoleDescriptor from './RoleDescriptor.vue'; import RoleDescriptor from './RoleDescriptor.vue';
@ -24,9 +23,11 @@ const searchBarDataKeys = {
data-key="Role" data-key="Role"
:descriptor="RoleDescriptor" :descriptor="RoleDescriptor"
:search-data-key="searchBarDataKeys[routeName]" :search-data-key="searchBarDataKeys[routeName]"
:search-custom-route-redirect="customRouteRedirectName" :searchbar-props="{
:search-redirect="!!customRouteRedirectName" redirect: !!customRouteRedirectName,
:searchbar-label="t('role.searchRoles')" customRouteRedirectName,
:searchbar-info="t('role.searchInfo')" label: t('role.searchRoles'),
info: t('role.searchInfo'),
}"
/> />
</template> </template>

View File

@ -1,12 +1,10 @@
<script setup> <script setup>
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import CardDescriptor from 'components/ui/CardDescriptor.vue'; import CardDescriptor from 'components/ui/CardDescriptor.vue';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import useCardDescription from 'src/composables/useCardDescription'; import useCardDescription from 'src/composables/useCardDescription';
import { useQuasar } from 'quasar';
import axios from 'axios'; import axios from 'axios';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
const $props = defineProps({ const $props = defineProps({
@ -23,9 +21,6 @@ const $props = defineProps({
const route = useRoute(); const route = useRoute();
const quasar = useQuasar();
const router = useRouter();
const { notify } = useNotify(); const { notify } = useNotify();
const { t } = useI18n(); const { t } = useI18n();
const entityId = computed(() => { const entityId = computed(() => {
@ -36,29 +31,13 @@ const setData = (entity) => (data.value = useCardDescription(entity.name, entity
const filter = { const filter = {
where: { id: entityId }, where: { id: entityId },
}; };
const removeRole = () => { const removeRole = async () => {
quasar
.dialog({
title: 'Are you sure you want to delete it?',
message: 'Delete department',
ok: {
push: true,
color: 'primary',
},
cancel: true,
})
.onOk(async () => {
try { try {
await axios.post( await axios.delete(`VnRoles/${entityId.value}`);
`/Departments/${entityId.value}/removeChild`, notify(t('Role removed'), 'positive');
entityId.value } catch (error) {
); console.error('Error deleting role', error);
router.push({ name: 'WorkerDepartment' });
notify('department.departmentRemoved', 'positive');
} catch (err) {
console.error('Error removing department');
} }
});
}; };
</script> </script>

View File

@ -1,7 +1,6 @@
<script setup> <script setup>
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import FormModelPopup from 'components/FormModelPopup.vue'; import FormModelPopup from 'components/FormModelPopup.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';

View File

@ -2,17 +2,14 @@
import { reactive, ref } from 'vue'; import { reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelect from 'src/components/common/VnSelect.vue';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
import FormPopup from 'components/FormPopup.vue'; import FormPopup from 'components/FormPopup.vue';
const emit = defineEmits(['onSubmitCreateSubrole']); const emit = defineEmits(['onSubmitCreateSubrole']);
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
const subRoleFormData = reactive({ const subRoleFormData = reactive({
inheritsFrom: null, inheritsFrom: null,
role: route.params.id, role: route.params.id,

View File

@ -2,10 +2,8 @@
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import VnPaginate from 'components/ui/VnPaginate.vue'; import VnPaginate from 'components/ui/VnPaginate.vue';
import SubRoleCreateForm from './SubRoleCreateForm.vue'; import SubRoleCreateForm from './SubRoleCreateForm.vue';
import { useVnConfirm } from 'composables/useVnConfirm'; import { useVnConfirm } from 'composables/useVnConfirm';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
@ -16,10 +14,8 @@ const route = useRoute();
const router = useRouter(); const router = useRouter();
const { openConfirmationModal } = useVnConfirm(); const { openConfirmationModal } = useVnConfirm();
const { notify } = useNotify(); const { notify } = useNotify();
const paginateRef = ref(null); const paginateRef = ref(null);
const createSubRoleDialogRef = ref(null); const createSubRoleDialogRef = ref(null);
const arrayData = useArrayData('SubRoles'); const arrayData = useArrayData('SubRoles');
const store = arrayData.store; const store = arrayData.store;

View File

@ -68,7 +68,7 @@ account:
delete: delete:
name: Delete name: Delete
title: The account will be deleted title: The account will be deleted
subtitle: Are you sure you want to continue? subTitle: Are you sure you want to continue?
success: '' success: ''
search: Search user search: Search user
searchInfo: You can search by id, name or nickname searchInfo: You can search by id, name or nickname

View File

@ -67,7 +67,7 @@ account:
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: '' success: ''
search: Buscar usuario search: Buscar usuario
searchInfo: Puedes buscar por id, nombre o usuario searchInfo: Puedes buscar por id, nombre o usuario

View File

@ -10,13 +10,10 @@ 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 VnAvatar from 'src/components/ui/VnAvatar.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 claimStates = ref([]); const claimStates = ref([]);
const claimStatesCopy = ref([]); const claimStatesCopy = ref([]);
@ -94,15 +91,14 @@ const statesFilter = {
:rules="validate('claim.claimStateFk')" :rules="validate('claim.claimStateFk')"
> >
<template #before> <template #before>
<QAvatar color="orange"> <VnAvatar
<VnImg :worker-id="data.workerFk"
v-if="data.workerFk" size="md"
:size="'160x160'" :title="
:id="data.workerFk" workersOptions.find(({ id }) => id == data.workerFk)?.name
collection="user" "
spinner-color="white" color="primary"
/> />
</QAvatar>
</template> </template>
</VnSelect> </VnSelect>
<QSelect <QSelect

View File

@ -11,9 +11,11 @@ import filter from './ClaimFilter.js';
:descriptor="ClaimDescriptor" :descriptor="ClaimDescriptor"
:filter-panel="ClaimFilter" :filter-panel="ClaimFilter"
search-data-key="ClaimList" search-data-key="ClaimList"
search-url="Claims/filter"
searchbar-label="Search claim"
searchbar-info="You can search by claim id or customer name"
:filter="filter" :filter="filter"
:searchbar-props="{
url: 'Claims/filter',
label: 'Search claim',
info: 'You can search by claim id or customer name',
}"
/> />
</template> </template>

View File

@ -1,120 +1,171 @@
<script setup> <script setup>
import { computed, onBeforeMount, ref, watch } from 'vue'; import { computed, onBeforeMount, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useRole } from 'src/composables/useRole';
import axios from 'axios'; import axios from 'axios';
import { QCheckbox, QBtn, useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { toCurrency, toDate, toDateHourMin } from 'src/filters'; import { toCurrency, toDate, toDateHourMin } from 'src/filters';
import { useState } from 'src/composables/useState'; import { useState } from 'composables/useState';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import { useValidator } from 'src/composables/useValidator'; import { usePrintService } from 'composables/usePrintService';
import { usePrintService } from 'src/composables/usePrintService'; import { useSession } from 'composables/useSession';
import { useSession } from 'src/composables/useSession'; import { useVnConfirm } from 'composables/useVnConfirm';
import VnTable from 'components/VnTable/VnTable.vue';
import VnInput from 'components/common/VnInput.vue';
import VnSubToolbar from 'components/ui/VnSubToolbar.vue';
import VnFilter from 'components/VnTable/VnFilter.vue';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
import FetchData from 'components/FetchData.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import CustomerNewPayment from 'src/pages/Customer/components/CustomerNewPayment.vue'; import CustomerNewPayment from 'src/pages/Customer/components/CustomerNewPayment.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import InvoiceOutDescriptorProxy from 'src/pages/InvoiceOut/Card/InvoiceOutDescriptorProxy.vue'; import InvoiceOutDescriptorProxy from 'src/pages/InvoiceOut/Card/InvoiceOutDescriptorProxy.vue';
const session = useSession();
const tokenMultimedia = session.getTokenMultimedia();
const { openConfirmationModal } = useVnConfirm();
const { sendEmail } = usePrintService(); const { sendEmail } = usePrintService();
const { t } = useI18n(); const { t } = useI18n();
const { validate } = useValidator(); const { hasAny } = useRole();
const session = useSession();
const tokenMultimedia = session.getTokenMultimedia();
const quasar = useQuasar(); const quasar = useQuasar();
const route = useRoute(); const route = useRoute();
const state = useState(); const state = useState();
const stateStore = useStateStore(); const stateStore = useStateStore();
const user = state.getUser(); const user = state.getUser();
const clientRisks = ref(null); const clientRisk = ref([]);
const clientRisksRef = ref(null); const tableRef = ref();
const companiesOptions = ref([]); const companyId = ref();
const companyId = ref(null); const companyLastId = ref(user.value.companyFk);
const receiptsRef = ref(null); const balances = ref([]);
const receiptsData = ref([]); const vnFilterRef = ref({});
const filter = computed(() => {
const filterCompanies = { order: ['code'] }; return {
const userParams = {
clientId: route.params.id, clientId: route.params.id,
companyId: user.value.companyFk, companyId: companyId.value ?? user.value.companyFk,
}; };
const filter = { });
include: { relation: 'company', scope: { fields: ['code'] } },
where: { clientFk: route.params.id, companyFk: user.value.companyFk }, const companyFilterColumn = {
align: 'left',
name: 'companyId',
label: t('Company'),
component: 'select',
attrs: {
url: 'Companies',
optionLabel: 'code',
sortBy: 'code',
limit: 0,
},
columnFilter: {
event: {
remove: () => (companyId.value = null),
'update:modelValue': (newCompanyFk) => {
if (!newCompanyFk) return;
vnFilterRef.value.addFilter(newCompanyFk);
companyLastId.value = newCompanyFk;
},
blur: () =>
!companyId.value &&
(companyId.value = companyLastId.value ?? user.value.companyFk),
},
},
visible: false,
}; };
const columns = computed(() => [ const columns = computed(() => [
{ {
align: 'left', align: 'left',
field: 'payed', name: 'payed',
format: (value) => toDate(value),
label: t('Date'), label: t('Date'),
name: 'date', format: ({ payed }) => toDate(payed),
cardVisible: true,
}, },
{ {
align: 'left', align: 'left',
field: 'created', name: 'created',
format: (value) => toDateHourMin(value),
label: t('Creation date'), label: t('Creation date'),
name: 'creationDate', format: ({ created }) => toDateHourMin(created),
cardVisible: true,
}, },
{ {
align: 'left', align: 'left',
field: 'userName', name: 'workerFk',
label: t('Employee'), label: t('Employee'),
name: 'employee', columnField: {
component: 'userLink',
attrs: ({ row }) => {
return {
workerId: row.workerFk,
name: row.userName,
};
},
},
cardVisible: true,
}, },
{ {
align: 'left', align: 'left',
field: 'description', name: 'description',
label: t('Reference'), label: t('Reference'),
name: 'reference', isTitle: true,
class: 'extend',
}, },
{ {
align: 'left', align: 'right',
field: 'bankFk', name: 'bankFk',
label: t('Bank'), label: t('Bank'),
name: 'bank', cardVisible: true,
}, },
{ {
align: 'right', align: 'right',
field: 'debit',
format: (value) => value && toCurrency(value),
label: t('Debit'),
name: 'debit', name: 'debit',
label: t('Debit'),
format: ({ debit }) => debit && toCurrency(debit),
isId: true,
}, },
{ {
align: 'right', align: 'right',
field: 'credit', name: 'credit',
format: (value) => value && toCurrency(value),
label: t('Havings'), label: t('Havings'),
name: 'havings', format: ({ credit }) => credit && toCurrency(credit),
cardVisible: true,
}, },
{ {
align: 'right', align: 'right',
field: 'balance',
format: (value) => value && toCurrency(value),
label: t('Balance'),
name: 'balance', name: 'balance',
label: t('Balance'),
format: ({ balance }) => toCurrency(balance),
cardVisible: true,
}, },
{ {
align: 'left', align: 'left',
field: 'isConciliate', name: 'isConciliate',
label: t('Conciliated'), label: t('Conciliated'),
name: 'conciliated', cardVisible: true,
}, },
{ {
align: 'left', align: 'left',
field: 'totalWithVat', name: 'tableActions',
label: '', actions: [
name: 'actions', {
title: t('globals.downloadPdf'),
icon: 'cloud_download',
show: (row) => row.isInvoice,
action: (row) => showBalancePdf(row),
},
{
title: t('Send compensation'),
icon: 'outgoing_mail',
show: (row) => !!row.isCompensation,
action: ({ id }) =>
openConfirmationModal(
t('Send compensation'),
t('Do you want to report compensation to the client by mail?'),
() => sendEmail(`Receipts/${id}/balance-compensation-email`)
),
},
],
}, },
]); ]);
@ -123,249 +174,124 @@ onBeforeMount(() => {
companyId.value = user.value.companyFk; companyId.value = user.value.companyFk;
}); });
watch( async function getClientRisk() {
() => route.params.id, const { data } = await axios.get(`clientRisks`, {
(newValue) => { params: {
if (!newValue) return; filter: JSON.stringify({
userParams.clientId = newValue; include: { relation: 'company', scope: { fields: ['code'] } },
filter.where.clientFk = newValue; where: { clientFk: route.params.id, companyFk: user.value.companyFk },
getData(); }),
},
});
clientRisk.value = data;
return clientRisk.value;
} }
);
const getData = () => { async function getCurrentBalance() {
receiptsRef.value?.fetch(); const currentBalance = (await getClientRisk()).find((balance) => {
clientRisksRef.value?.fetch();
};
const getCurrentBalance = () => {
const currentBalance = clientRisks.value.find((balance) => {
return balance.companyFk === companyId.value; return balance.companyFk === companyId.value;
}); });
return currentBalance && currentBalance.amount; return currentBalance && currentBalance.amount;
};
const onFetch = (balances) => {
balances.forEach((balance, index) => {
if (index === 0) {
balance.balance = getCurrentBalance();
} else {
let previousBalance = balances[index - 1];
balance.balance =
previousBalance.balance -
(previousBalance.debit - previousBalance.credit);
} }
});
receiptsData.value = balances; async function onFetch(data) {
}; balances.value = [];
for (const [index, balance] of data.entries()) {
if (index === 0) {
balance.balance = await getCurrentBalance();
continue;
}
const previousBalance = data[index - 1];
balance.balance =
previousBalance?.balance - (previousBalance?.debit - previousBalance?.credit);
}
balances.value = data;
}
const showNewPaymentDialog = () => { const showNewPaymentDialog = () => {
quasar.dialog({ quasar.dialog({
component: CustomerNewPayment, component: CustomerNewPayment,
componentProps: { componentProps: {
companyId: companyId.value, companyId: companyId.value,
totalCredit: clientRisks.value[0]?.amount, totalCredit: clientRisk.value[0]?.amount,
promise: getData, promise: () => tableRef.value.reload(),
}, },
}); });
}; };
const updateCompanyId = (id) => { const showBalancePdf = ({ id }) => {
if (id) { const url = `api/InvoiceOuts/${id}/download?access_token=${tokenMultimedia}`;
companyId.value = id;
userParams.companyId = id;
filter.where.companyFk = id;
}
getData();
};
const saveFieldValue = async (row) => {
try {
const payload = { description: row.description };
await axios.patch(`Receipts/${row.id}`, payload);
} catch (err) {
return err;
}
};
const sendEmailAction = () => {
sendEmail(`Suppliers/${route.params.id}/campaign-metrics-email`);
};
const showBalancePdf = (balance) => {
const url = `api/InvoiceOuts/${balance.id}/download?access_token=${tokenMultimedia}`;
window.open(url, '_blank'); window.open(url, '_blank');
}; };
</script> </script>
<template> <template>
<FetchData <VnSubToolbar class="q-mb-md">
:filter="filter" <template #st-data>
@on-fetch="(data) => (clientRisks = data)" <div class="column justify-center q-px-md q-py-sm">
auto-load <span class="text-bold">{{ t('Total by company') }}</span>
ref="clientRisksRef" <div class="row justify-center" v-if="clientRisk?.length">
url="ClientRisks" {{ clientRisk[0].company.code }}:
{{ toCurrency(clientRisk[0].amount) }}
</div>
</div>
</template>
<template #st-actions>
<div>
<VnFilter
ref="vnFilterRef"
v-model="companyId"
data-key="CustomerBalance"
:column="companyFilterColumn"
search-url="balance"
/> />
<FetchData </div>
:filter="filterCompanies" </template>
@on-fetch="(data) => (companiesOptions = data)" </VnSubToolbar>
auto-load <VnTable
url="Companies" ref="tableRef"
/>
<VnPaginate
auto-load
data-key="CustomerBalance" data-key="CustomerBalance"
url="Receipts/filter" url="Receipts/filter"
:user-params="userParams" search-url="balance"
ref="receiptsRef" :user-params="filter"
@on-fetch="onFetch"
>
<template #body="{ rows }">
<QTable
:columns="columns" :columns="columns"
:no-data-label="t('globals.noResults')" :right-search="false"
:rows-per-page-options="[0]" :is-editable="false"
:rows="rows" :column-search="false"
class="full-width q-mt-md" @on-fetch="onFetch"
row-key="id" auto-load
> >
<template #body-cell-employee="{ row }"> <template #column-balance="{ rowIndex }">
<QTd auto-width @click.stop> {{ toCurrency(balances[rowIndex]?.balance) }}
<QBtn color="blue" flat no-caps>{{ row.userName }}</QBtn>
<WorkerDescriptorProxy :id="row.workerFk" />
</QTd>
</template> </template>
<template #column-description="{ row }">
<template #body-cell-reference="{ row }"> <div class="link" v-if="row.isInvoice">
<QTd auto-width @click.stop v-if="row.isInvoice"> {{ row.description }}
<QBtn color="blue" dense flat> <InvoiceOutDescriptorProxy :id="row.description" />
{{ t('bill', { ref: row.description }) }} </div>
</QBtn> <span v-else class="q-pa-xs dotted rounded-borders" :title="row.description">
<InvoiceOutDescriptorProxy :id="row.id" /> {{ row.description }}
</QTd>
<QTd v-else>
<VnInput
@keyup.enter="saveFieldValue(row)"
autofocus
clearable
dense
v-model="row.description"
/>
</QTd>
</template>
<template #body-cell-conciliated="{ row }">
<QTd align="center">
<QCheckbox :model-value="row.isConciliate === 1" disable />
</QTd>
</template>
<template #body-cell-actions="{ row }">
<QTd align="center">
<QIcon
@click.stop="showDialog = true"
class="q-ml-md fill-icon"
color="primary"
name="outgoing_mail"
size="sm"
v-if="row.isCompensation"
>
<QTooltip>
{{ t('Send compensation') }}
</QTooltip>
</QIcon>
<QIcon
@click="showBalancePdf(row)"
class="q-ml-md fill-icon"
color="primary"
name="cloud_download"
size="sm"
v-if="row.hasPdf"
>
<QTooltip>
{{ t('globals.downloadPdf') }}
</QTooltip>
</QIcon>
<QDialog v-model="showDialog">
<QCard class="q-pa-sm">
<QCardSection>
<span
ref="closeButton"
class="flex justify-end color-vn-label"
v-close-popup
>
<QIcon name="close" size="sm" />
</span> </span>
<div class="text-h6"> <QPopupEdit
{{ t('Send compensation') }} v-model="row.description"
</div> v-slot="scope"
</QCardSection> @save="
<QCardSection> (value) =>
<div> value != row.description &&
{{ axios.patch(`Receipts/${row.id}`, { description: value })
t( "
'Do you want to report compensation to the client by mail?' auto-save
) >
}} <VnInput
</div> v-model="scope.value"
</QCardSection> :disable="!hasAny(['administrative'])"
<QCardActions class="flex justify-end q-mb-sm"> @keypress.enter="scope.set"
<QBtn autofocus
:label="t('globals.cancel')"
color="primary"
flat
v-close-popup
/> />
<QBtn </QPopupEdit>
:label="t('globals.save')"
@click="sendEmailAction"
class="q-ml-sm"
color="primary"
/>
</QCardActions>
</QCard>
</QDialog>
</QTd>
</template> </template>
</QTable> </VnTable>
</template> <QPageSticky :offset="[18, 18]" style="z-index: 2">
</VnPaginate>
<QDrawer :width="256" show-if-above side="right" v-model="stateStore.rightDrawer">
<div class="q-mt-xl q-px-md">
<VnSelect
:label="t('Company')"
:options="companiesOptions"
@update:model-value="updateCompanyId($event)"
hide-selected
option-label="code"
option-value="id"
v-model="companyId"
:rules="validate('entry.companyFk')"
/>
</div>
<QCard class="q-ma-md q-pa-md q-mt-lg" v-if="receiptsData?.length">
<QCardSection>
<div class="flex justify-center text-subtitle1 text-bold">
{{ t('Total by company') }}
</div>
<div class="flex justify-center">
<div class="q-mr-sm" v-if="clientRisks?.length">
{{ clientRisks[0].company.code }}:
</div>
<div v-if="clientRisks?.length">
{{ toCurrency(clientRisks[0].amount) }}
</div>
</div>
</QCardSection>
</QCard>
</QDrawer>
<QPageSticky :offset="[18, 18]">
<QBtn @click.stop="showNewPaymentDialog()" color="primary" fab icon="add" /> <QBtn @click.stop="showNewPaymentDialog()" color="primary" fab icon="add" />
<QTooltip> <QTooltip>
{{ t('New payment') }} {{ t('New payment') }}
@ -393,3 +319,12 @@ es:
Send compensation: Enviar compensación Send compensation: Enviar compensación
Do you want to report compensation to the client by mail?: ¿Desea informar de la compensación al cliente por correo? Do you want to report compensation to the client by mail?: ¿Desea informar de la compensación al cliente por correo?
</i18n> </i18n>
<style lang="scss" scoped>
.dotted {
border: 1px dotted var(--vn-header-color);
}
.dotted:hover {
border: 1px dotted $primary;
}
</style>

View File

@ -7,66 +7,29 @@ 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'; import VnSelect from 'src/components/common/VnSelect.vue';
import VnAvatar from 'src/components/ui/VnAvatar.vue';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const workers = ref([]);
const workersCopy = ref([]);
const businessTypes = ref([]); const businessTypes = ref([]);
const contactChannels = ref([]); const contactChannels = ref([]);
const title = ref();
function setWorkers(data) {
workers.value = data;
workersCopy.value = data;
}
const filterOptions = {
options: workers,
filterFn: (options, value) => {
const search = value.toLowerCase();
if (value === '') return workersCopy.value;
return options.value.filter((row) => {
const id = row.id;
const name = row.name.toLowerCase();
const idMatches = id === search;
const nameMatches = name.indexOf(search) > -1;
return idMatches || nameMatches;
});
},
};
</script> </script>
<template> <template>
<fetch-data <FetchData
url="Workers/activeWithInheritedRole"
:filter="{ where: { role: 'salesPerson' } }"
@on-fetch="setWorkers"
auto-load
/>
<fetch-data
url="ContactChannels" url="ContactChannels"
@on-fetch="(data) => (contactChannels = data)" @on-fetch="(data) => (contactChannels = data)"
auto-load auto-load
/> />
<fetch-data <FetchData
url="BusinessTypes" url="BusinessTypes"
@on-fetch="(data) => (businessTypes = data)" @on-fetch="(data) => (businessTypes = data)"
auto-load auto-load
/> />
<fetch-data
:filter="filter"
@on-fetch="(data) => (clients = data)"
auto-load
url="Clients"
/>
<FormModel :url="`Clients/${route.params.id}`" auto-load model="customer"> <FormModel :url="`Clients/${route.params.id}`" auto-load model="customer">
<template #form="{ data, validate, filter }"> <template #form="{ data, validate }">
<VnRow> <VnRow>
<VnInput <VnInput
:label="t('globals.name')" :label="t('globals.name')"
@ -75,7 +38,6 @@ const filterOptions = {
clearable clearable
v-model="data.name" v-model="data.name"
/> />
<QSelect <QSelect
:input-debounce="0" :input-debounce="0"
:label="t('customer.basicData.businessType')" :label="t('customer.basicData.businessType')"
@ -126,30 +88,25 @@ const filterOptions = {
/> />
</VnRow> </VnRow>
<VnRow> <VnRow>
<QSelect <VnSelect
:input-debounce="0" url="Workers/activeWithInheritedRole"
:label="t('customer.basicData.salesPerson')" :filter="{ where: { role: 'salesPerson' } }"
:options="workers" option-filter="firstName"
:rules="validate('client.salesPersonFk')"
@filter="(value, update) => filter(value, update, filterOptions)"
emit-value
map-options
option-label="name"
option-value="id"
use-input
v-model="data.salesPersonFk" v-model="data.salesPersonFk"
:label="t('customer.basicData.salesPerson')"
:rules="validate('client.salesPersonFk')"
:use-like="false"
:emit-value="false"
@update:model-value="(val) => (title = val?.nickname)"
> >
<template #prepend> <template #prepend>
<QAvatar color="orange"> <VnAvatar
<VnImg :worker-id="data.salesPersonFk"
v-if="data.salesPersonFk" color="primary"
:id="user.id" :title="title"
collection="user"
spinner-color="white"
/> />
</QAvatar>
</template> </template>
</QSelect> </VnSelect>
<QSelect <QSelect
v-model="data.contactChannelFk" v-model="data.contactChannelFk"
:options="contactChannels" :options="contactChannels"

View File

@ -10,8 +10,10 @@ 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/extendedListFilter" :searchbar-props="{
searchbar-label="Search customer" url: 'Clients/extendedListFilter',
searchbar-info="You can search by customer id or name" label: 'Search customer',
info: 'You can search by customer id or name',
}"
/> />
</template> </template>

View File

@ -1,8 +1,7 @@
<script setup> <script setup>
import { computed } from 'vue'; 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 { QBtn } from 'quasar';
import { toCurrency, toDateHourMin } from 'src/filters'; import { toCurrency, toDateHourMin } from 'src/filters';
import VnTable from 'components/VnTable/VnTable.vue'; import VnTable from 'components/VnTable/VnTable.vue';
import VnUserLink from 'src/components/ui/VnUserLink.vue'; import VnUserLink from 'src/components/ui/VnUserLink.vue';
@ -10,7 +9,8 @@ import VnUserLink from 'src/components/ui/VnUserLink.vue';
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
const filter = { const filter = computed(() => {
return {
include: [ include: [
{ {
relation: 'worker', relation: 'worker',
@ -21,10 +21,11 @@ const filter = {
}, },
], ],
where: { clientFk: +route.params.id }, where: { clientFk: +route.params.id },
order: ['created DESC'],
limit: 20,
}; };
});
const tableRef = ref();
const tableData = ref([]);
const columns = computed(() => [ const columns = computed(() => [
{ {
align: 'left', align: 'left',
@ -43,39 +44,44 @@ const columns = computed(() => [
name: 'amount', name: 'amount',
format: ({ amount }) => toCurrency(amount), format: ({ amount }) => toCurrency(amount),
}, },
{
label: t('Credit'),
name: 'credit',
create: true,
visible: false,
attrs: {
autofocus: true,
},
},
]); ]);
</script> </script>
<template> <template>
<!-- Column titles are missing -->
<VnTable <VnTable
ref="tableRef" ref="tableRef"
data-key="ClientCredit" data-key="ClientCredit"
url="ClientCredits" url="ClientCredits"
search-url="credits"
:filter="filter" :filter="filter"
:order="['created DESC']"
:columns="columns" :columns="columns"
default-mode="table"
auto-load auto-load
:right-search="false" :right-search="false"
:is-editable="false" :is-editable="false"
:use-model="true" :use-model="true"
:column-search="false" :column-search="false"
:disable-option="{ card: true }" :disable-option="{ card: true }"
@on-fetch="(data) => (tableData = data)"
:create="{
urlUpdate: `Clients/${route.params.id}`,
title: t('New credit'),
onDataSaved: () => tableRef.reload(),
formInitialData: { credit: tableData.at(0)?.amount },
}"
> >
<template #column-employee="{ row }"> <template #column-employee="{ row }">
<VnUserLink :name="row?.worker?.user?.name" :worker-id="row.worker?.id" /> <VnUserLink :name="row?.worker?.user?.name" :worker-id="row.worker?.id" />
</template> </template>
</VnTable> </VnTable>
<QPageSticky :offset="[18, 18]">
<QBtn
@click.stop="$router.push({ name: 'CustomerCreditCreate' })"
color="primary"
fab
icon="add"
/>
<QTooltip>
{{ t('New credit') }}
</QTooltip>
</QPageSticky>
</template> </template>
<i18n> <i18n>
es: es:

View File

@ -96,15 +96,12 @@ function handleLocation(data, location) {
:roles-allowed-to-create="['deliveryAssistant']" :roles-allowed-to-create="['deliveryAssistant']"
v-model="data.postcode" v-model="data.postcode"
@update:model-value="(location) => handleLocation(data, location)" @update:model-value="(location) => handleLocation(data, location)"
> />
</VnLocation>
</VnRow> </VnRow>
<VnRow> <VnRow>
<QCheckbox :label="t('Active')" v-model="data.isActive" /> <QCheckbox :label="t('Active')" v-model="data.isActive" />
<QCheckbox :label="t('Frozen')" v-model="data.isFreezed" /> <QCheckbox :label="t('Frozen')" v-model="data.isFreezed" />
</VnRow> </VnRow>
<VnRow> <VnRow>
<QCheckbox :label="t('Has to invoice')" v-model="data.hasToInvoice" /> <QCheckbox :label="t('Has to invoice')" v-model="data.hasToInvoice" />
<div> <div>

View File

@ -2,20 +2,17 @@
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { QBtn } from 'quasar';
import { useStateStore } from 'src/stores/useStateStore';
import { toCurrency } from 'src/filters'; import { toCurrency } from 'src/filters';
import { toDateTimeFormat } from 'src/filters/date'; import { toDateTimeFormat } from 'src/filters/date';
import FetchData from 'components/FetchData.vue'; import VnTable from 'components/VnTable/VnTable.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
const stateStore = computed(() => useStateStore());
const rows = ref([]); const rows = ref([]);
const totalAmount = ref(); const totalAmount = ref();
const tableRef = ref();
const filter = { const filter = computed(() => {
return {
include: [ include: [
{ {
relation: 'greugeType', relation: 'greugeType',
@ -30,73 +27,61 @@ const filter = {
}, },
}, },
], ],
order: 'shipped DESC, amount',
where: { where: {
clientFk: `${route.params.id}`, clientFk: route.params.id,
},
limit: 20,
};
const tableColumnComponents = {
date: {
component: 'span',
props: () => {},
event: () => {},
},
createdBy: {
component: QBtn,
props: () => ({ flat: true, color: 'blue', noCaps: true }),
event: () => {},
},
comment: {
component: 'span',
props: () => {},
event: () => {},
},
type: {
component: 'span',
props: () => {},
event: () => {},
},
amount: {
component: 'span',
props: () => {},
event: () => {},
}, },
}; };
});
const columns = computed(() => [ const columns = computed(() => [
{ {
align: 'left', align: 'left',
field: 'shipped',
label: t('Date'), label: t('Date'),
name: 'date', name: 'shipped',
format: (value) => toDateTimeFormat(value), format: ({ shipped }) => toDateTimeFormat(shipped),
create: true,
columnCreate: {
component: 'date',
autofocus: true,
},
}, },
{ {
align: 'left', align: 'left',
field: (value) => value?.user?.name, name: 'userFk',
label: t('Created by'), label: t('Created by'),
name: 'createdBy', component: 'userLink',
attrs: ({ row }) => {
return {
defaultName: true,
workerId: row.user?.id,
name: row.user?.name,
};
},
}, },
{ {
align: 'left', align: 'left',
field: 'description', name: 'description',
label: t('Comment'), label: t('Comment'),
name: 'comment', create: true,
}, },
{ {
align: 'left', align: 'left',
field: (value) => value?.greugeType?.name, name: 'greugeTypeFk',
format: ({ greugeType }) => greugeType?.name,
label: t('Type'), label: t('Type'),
name: 'type', create: true,
columnCreate: {
component: 'select',
url: 'greugeTypes',
limit: 0,
},
}, },
{ {
align: 'left', align: 'left',
field: 'amount',
label: t('Amount'),
name: 'amount', name: 'amount',
format: (value) => toCurrency(value), label: t('Amount'),
format: ({ amount }) => toCurrency(amount),
create: true,
}, },
]); ]);
@ -107,60 +92,33 @@ const setRows = (data) => {
</script> </script>
<template> <template>
<FetchData :filter="filter" @on-fetch="setRows" auto-load url="greuges" /> <VnTable
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="300" show-if-above> ref="tableRef"
<QCard class="full-width q-pa-sm"> data-key="Greuges"
<h6 class="flex justify-end q-my-lg q-pr-lg" v-if="totalAmount !== undefined"> url="Greuges"
<span class="color-vn-label q-mr-md">{{ t('Total') }}:</span> search-url="greuges"
{{ toCurrency(totalAmount) }} :filter="filter"
</h6> :order="['shipped DESC', 'amount']"
<QSkeleton v-else type="QInput" square />
</QCard>
</QDrawer>
<div class="full-width flex justify-center">
<QPage class="card-width q-pa-lg">
<QCard class="q-pa-sm q-mt-md">
<QTable
:columns="columns" :columns="columns"
:no-data-label="t('globals.noResults')" :right-search="false"
:pagination="{ rowsPerPage: 12 }" :is-editable="false"
:rows="rows" :use-model="true"
class="full-width q-mt-md" :column-search="false"
row-key="id" @on-fetch="(data) => setRows(data)"
:create="{
urlCreate: `Greuges`,
title: t('New credit'),
onDataSaved: () => tableRef.reload(),
formInitialData: { shipped: new Date(), clientFk: route.params.id },
}"
auto-load
> >
<template #body-cell="props"> <template #top-left>
<QTd :props="props"> <QCard class="q-px-md q-py-sm">
<QTr :props="props" class="cursor-pointer"> {{ t('Total') }}: {{ toCurrency(totalAmount) }}
<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)
"
>
{{ props.value }}
<WorkerDescriptorProxy
:id="props.row.userFk"
v-if="props.col.name === 'createdBy'"
/>
</component>
</QTr>
</QTd>
</template>
</QTable>
</QCard> </QCard>
</QPage> </template>
</div> </VnTable>
<QPageSticky :offset="[18, 18]">
<QBtn color="primary" fab icon="add" :to="{ name: 'CustomerGreugeCreate' }" />
<QTooltip>
{{ t('New greuge') }}
</QTooltip>
</QPageSticky>
</template> </template>
<style lang="scss"> <style lang="scss">

View File

@ -1,262 +1,6 @@
<script setup> <script setup>
import { onBeforeMount, ref } from 'vue'; import VnLog from 'src/components/common/VnLog.vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { useStateStore } from 'stores/useStateStore';
import FetchData from 'components/FetchData.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
const { t } = useI18n();
const route = useRoute();
const stateStore = useStateStore();
const clientLogs = ref(null);
const urlClientLogsEditors = ref(null);
const urlClientLogsModels = ref(null);
const clientLogsModelsOptions = ref([]);
const clientLogsOptions = ref([]);
const clientLogsEditorsOptions = ref([]);
const radioButtonValue = ref('all');
const insert = ref(false);
const update = ref(false);
const deletes = ref(false);
const select = ref(false);
const neq = ref(null);
const inq = ref([]);
const filterClientLogs = {
fields: [
'id',
'originFk',
'userFk',
'action',
'changedModel',
'oldInstance',
'newInstance',
'creationDate',
'changedModel',
'changedModelId',
'changedModelValue',
'description',
],
include: [
{
relation: 'user',
scope: {
fields: ['nickname', 'name', 'image'],
include: { relation: 'worker', scope: { fields: ['id'] } },
},
},
],
order: ['creationDate DESC', 'id DESC'],
limit: 20,
};
const filterClientLogsEditors = {
fields: ['id', 'nickname', 'name', 'image'],
order: 'nickname',
limit: 30,
};
const filterClientLogsModels = { order: ['changedModel'] };
const urlBase = `ClientLogs/${route.params.id}`;
onBeforeMount(() => {
stateStore.rightDrawer = true;
filterClientLogs.where = {
and: [
{ originFk: `${route.params.id}` },
{ userFk: { neq: radioButtonValue.value } },
{ action: { inq: inq.value } },
],
};
urlClientLogsEditors.value = `${urlBase}/editors`;
urlClientLogsModels.value = `${urlBase}/models`;
});
const getClientLogs = async (value, status) => {
if (status === 'neq') {
neq.value = value;
} else {
setInq(value, status);
}
filterClientLogs.where = {
and: [
{ originFk: `${route.params.id}` },
{ userFk: { neq: neq.value } },
{ action: { inq: inq.value } },
],
};
clientLogs.value?.fetch();
};
const setInq = (value, status) => {
if (status) {
if (!inq.value.includes(value)) {
inq.value.push(value);
}
} else {
inq.value = inq.value.filter((item) => item !== value);
}
};
</script> </script>
<template> <template>
<FetchData <VnLog model="Client" />
:filter="filterClientLogs"
@on-fetch="(data) => (clientLogsOptions = data)"
auto-load
url="ClientLogs"
ref="clientLogs"
/>
<FetchData
:filter="filterClientLogsEditors"
@on-fetch="(data) => (clientLogsEditorsOptions = data)"
auto-load
:url="urlClientLogsEditors"
/>
<FetchData
:filter="filterClientLogsModels"
@on-fetch="(data) => (clientLogsModelsOptions = data)"
auto-load
:url="urlClientLogsModels"
/>
<h5 class="flex justify-center color-vn-label">
{{ t('globals.noResults') }}
</h5>
<QDrawer :width="256" show-if-above side="right" v-model="stateStore.rightDrawer">
<div class="q-mt-sm q-px-md">
<VnInput :label="t('Search')" clearable>
<template #append>
<QIcon name="info" class="cursor-pointer">
<QTooltip>
{{ t('Search by id or concept') }}
</QTooltip>
</QIcon>
</template> </template>
</VnInput>
<VnSelect
:label="t('Entity')"
:options="[]"
class="q-mt-md"
hide-selected
option-label="name"
option-value="id"
/>
<div class="q-mt-lg">
<QRadio
:dark="true"
:label="t('All')"
@update:model-value="getClientLogs($event, 'neq')"
dense
v-model="radioButtonValue"
val="all"
/>
</div>
<div class="q-mt-md">
<QRadio
:dark="true"
:label="t('User')"
@update:model-value="getClientLogs($event, 'neq')"
dense
v-model="radioButtonValue"
val="user"
/>
</div>
<div class="q-mt-md">
<QRadio
:dark="true"
:label="t('System')"
@update:model-value="getClientLogs($event, 'neq')"
dense
v-model="radioButtonValue"
val="system"
/>
</div>
<VnSelect
:label="t('User')"
:options="[]"
class="q-mt-sm"
hide-selected
option-label="name"
option-value="id"
/>
<VnInput :label="t('Changes')" clearable class="q-mt-sm">
<template #append>
<QIcon name="info" class="cursor-pointer">
<QTooltip>
{{ t('Search by changes') }}
</QTooltip>
</QIcon>
</template>
</VnInput>
<div class="q-mt-md">
<QCheckbox
:label="t('Creates')"
@update:model-value="getClientLogs('insert', $event)"
v-model="insert"
/>
</div>
<div>
<QCheckbox
:label="t('Edits')"
@update:model-value="getClientLogs('update', $event)"
v-model="update"
/>
</div>
<div>
<QCheckbox
:label="t('Deletes')"
@update:model-value="getClientLogs('delete', $event)"
v-model="deletes"
/>
</div>
<div>
<QCheckbox
:label="t('Accesses')"
@update:model-value="getClientLogs('select', $event)"
v-model="select"
/>
</div>
<VnInputDate :label="t('Date')" class="q-mt-sm" />
<VnInput :label="t('To')" clearable class="q-mt-md" />
</div>
</QDrawer>
<QPageSticky
:offset="[18, 18]"
v-if="radioButtonValue !== 'all' || insert || update || deletes || select"
>
<QBtn color="primary" fab icon="filter_alt_off" />
<QTooltip>
{{ t('Quit filter') }}
</QTooltip>
</QPageSticky>
</template>
<i18n>
es:
Search: Buscar
Search by id or concept: xxx
Entity: Entidad
All: Todo
User: Usuario
System: Sistema
Changes: Cambios
Search by changes: xxx
Creates: Crea
Edits: Modifica
Deletes: Elimina
Accesses: Accede
Date: Fecha
To: Hasta
Quit filter: Quitar filtro
</i18n>

View File

@ -1,83 +1,26 @@
<script setup> <script setup>
import { useI18n } from 'vue-i18n'; import { computed } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute } from 'vue-router';
import VnNotes from 'src/components/ui/VnNotes.vue';
import { toDateTimeFormat } from 'src/filters/date';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
const { t } = useI18n();
const route = useRoute(); const route = useRoute();
const router = useRouter();
const noteFilter = { const noteFilter = computed(() => {
return {
order: 'created DESC', order: 'created DESC',
where: { where: {
clientFk: `${route.params.id}`, clientFk: `${route.params.id}`,
}, },
}; };
});
const toCustomerNoteCreate = () => {
router.push({ name: 'CustomerNoteCreate' });
};
</script> </script>
<template> <template>
<div class="full-width flex justify-center"> <VnNotes
<QCard class="card-width q-pa-lg">
<VnPaginate
data-key="CustomerNotes"
url="clientObservations" url="clientObservations"
auto-load :add-note="true"
:filter="noteFilter" :filter="noteFilter"
> :body="{ clientFk: route.params.id }"
<template #body="{ rows }"> style="overflow-y: auto"
<div v-if="rows.length"> />
<QCard
v-for="(item, index) in rows"
:key="index"
class="q-pa-md q-rounded custom-border"
:class="{ 'q-mb-md': index < rows.length - 1 }"
>
<div class="flex justify-between">
<p class="color-vn-label">
{{ item.worker.user.nickname }}
</p>
<p class="color-vn-label">
{{ toDateTimeFormat(item?.created) }}
</p>
</div>
<h6 class="q-mt-xs q-mb-none">{{ item.text }}</h6>
</QCard>
</div>
<div v-else>
<h5 class="flex justify-center color-vn-label">
{{ t('globals.noResults') }}
</h5>
</div>
</template> </template>
</VnPaginate>
</QCard>
</div>
<QPageSticky :offset="[18, 18]">
<QBtn @click.stop="toCustomerNoteCreate()" color="primary" fab icon="add" />
<QTooltip>
{{ t('New note') }}
</QTooltip>
</QPageSticky>
</template>
<style lang="scss">
.custom-border {
border: 2px solid var(--vn-accent-color);
border-radius: 10px;
padding: 10px;
}
</style>
<i18n>
es:
New note: Nueva nota
</i18n>

View File

@ -1,146 +1,107 @@
<script setup> <script setup>
import axios from 'axios';
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router'; import { useRoute } from 'vue-router';
import { toCurrency, toDate } from 'src/filters'; import { toCurrency, toDate } from 'src/filters';
import FetchData from 'components/FetchData.vue'; import VnTable from 'components/VnTable/VnTable.vue';
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
const router = useRouter(); const tableRef = ref();
const rows = ref([]); const filter = computed(() => {
return {
const filter = {
where: { clientFk: route.params.id }, where: { clientFk: route.params.id },
order: ['started DESC'],
limit: 20,
}; };
});
const tableColumnComponents = { const componentColumn = (type) => {
since: { return {
component: 'span', columnFilter: {
props: () => {}, component: type,
event: () => {},
}, },
to: { columnCreate: {
component: 'span', component: type,
props: () => {},
event: () => {},
},
amount: {
component: 'span',
props: () => {},
event: () => {},
},
period: {
component: 'span',
props: () => {},
event: () => {},
}, },
}; };
};
const columns = computed(() => [ const columns = computed(() => [
{ {
align: 'left', align: 'left',
field: 'started', label: t('globals.since'),
label: t('Since'), name: 'started',
name: 'since', format: ({ started }) => toDate(started),
format: (value) => toDate(value), create: true,
...componentColumn('date'),
}, },
{ {
align: 'left', align: 'left',
field: 'finished', name: 'finished',
label: t('To'), label: t('globals.to'),
name: 'to', format: ({ finished }) => toDate(finished),
format: (value) => toDate(value), create: true,
...componentColumn('date'),
}, },
{ {
align: 'left', align: 'left',
field: 'amount',
label: t('Amount'),
name: 'amount', name: 'amount',
format: (value) => toCurrency(value), label: t('globals.amount'),
format: ({ amount }) => toCurrency(amount),
create: true,
...componentColumn('number'),
}, },
{ {
align: 'left', align: 'left',
field: 'period',
label: t('Period'),
name: 'period', name: 'period',
label: t('Period'),
create: true,
...componentColumn('number'),
},
{
align: 'left',
name: 'tableActions',
actions: [
{
title: t('Finish that recovery period'),
icon: 'lock',
show: (row) => !row.finished,
action: ({ id }) => setFinished(id),
isPrimary: true,
},
],
}, },
]); ]);
const toCustomerRecoverieCreate = () => { function setFinished(id) {
router.push({ name: 'CustomerRecoverieCreate' }); axios.patch(`Recoveries/${id}`, { finished: Date.vnNow() });
}; tableRef.value.reload();
}
</script> </script>
<template> <template>
<FetchData <VnTable
:filter="filter" ref="tableRef"
@on-fetch="(data) => (rows = data)" data-key="Recoveries"
auto-load
url="Recoveries" url="Recoveries"
/> search-url="recoveries"
:filter="filter"
<div class="full-width flex justify-center"> order="started DESC"
<QPage class="card-width q-pa-lg">
<QTable
:columns="columns" :columns="columns"
:no-data-label="t('globals.noResults')" :use-model="true"
:pagination="{ rowsPerPage: 12 }" :right-search="false"
:rows="rows" :create="{
class="full-width q-mt-md" urlCreate: 'Recoveries',
row-key="id" title: 'New recovery',
> onDataSaved: () => tableRef.reload(),
<template #body-cell="props"> formInitialData: { clientFk: route.params.id, started: Date.vnNew() },
<QTd :props="props"> }"
<QTr :props="props" class="cursor-pointer"> auto-load
<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)
"
>
{{ props.value }}
</component>
</QTr>
</QTd>
</template> </template>
</QTable>
</QPage>
</div>
<QPageSticky :offset="[18, 18]">
<QBtn @click.stop="toCustomerRecoverieCreate()" color="primary" fab icon="add" />
<QTooltip>
{{ t('New recoverie') }}
</QTooltip>
</QPageSticky>
</template>
<style lang="scss">
.consignees-card {
border: 2px solid var(--vn-accent-color);
border-radius: 10px;
padding: 10px;
}
.label-color {
color: var(--vn-label-color);
}
</style>
<i18n> <i18n>
es: es:
Since: Desde
To: Hasta
Amount: Importe
Period: Periodo Period: Periodo
New recoverie: Nuevo recobro New recovery: Nuevo recobro
Finish that recovery period: Terminar recobro
</i18n> </i18n>

View File

@ -1,17 +1,16 @@
<script setup> <script setup>
import { computed } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import VnSms from 'src/components/ui/VnSms.vue'; import VnSms from 'src/components/ui/VnSms.vue';
const route = useRoute(); const route = useRoute();
const id = route.params.id; const where = computed(() => {
return {
const where = { clientFk: route.params.id,
clientFk: id,
ticketFk: null, ticketFk: null,
}; };
});
</script> </script>
<template> <template>
<div class="column items-center">
<VnSms url="clientSms" :where="where" /> <VnSms url="clientSms" :where="where" />
</div>
</template> </template>

View File

@ -2,164 +2,81 @@
import { computed, ref } from 'vue'; 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 { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useValidator } from 'src/composables/useValidator';
import useNotify from 'src/composables/useNotify';
import { useStateStore } from 'stores/useStateStore';
import FetchData from 'components/FetchData.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import CustomerChangePassword from 'src/pages/Customer/components/CustomerChangePassword.vue'; import CustomerChangePassword from 'src/pages/Customer/components/CustomerChangePassword.vue';
import FormModel from 'components/FormModel.vue';
const { notify } = useNotify();
const { t } = useI18n(); const { t } = useI18n();
const { validate } = useValidator();
const quasar = useQuasar(); const quasar = useQuasar();
const route = useRoute(); const route = useRoute();
const stateStore = useStateStore();
const active = ref(false);
const canChangePassword = ref(0); const canChangePassword = ref(0);
const email = ref(null);
const isLoading = ref(false);
const name = ref(null);
const usersPreviewRef = ref(null);
const user = ref([]);
const dataChanges = computed(() => { const filter = computed(() => {
return ( return {
user.value.active !== active.value || fields: ['active', 'email', 'name'],
user.value.email !== email.value || where: { id: route.params.id },
user.value.name !== name.value };
);
}); });
const filter = { where: { id: `${route.params.id}` } };
const showChangePasswordDialog = () => { const showChangePasswordDialog = () => {
quasar.dialog({ quasar.dialog({
component: CustomerChangePassword, component: CustomerChangePassword,
componentProps: { componentProps: {
id: route.params.id, id: route.params.id,
promise: usersPreviewRef.value.fetch(),
}, },
}); });
}; };
const setInitialData = () => { async function hasCustomerRole() {
if (user.value.length) { const { data } = await axios(`Clients/${route.params.id}/hasCustomerRole`);
active.value = user.value[0].active; canChangePassword.value = data;
email.value = user.value[0].email;
name.value = user.value[0].name;
} }
};
const onSubmit = async () => {
isLoading.value = true;
const payload = {
active: active.value,
email: email.value,
name: name.value,
};
try {
await axios.patch(`Clients/${route.params.id}/updateUser`, payload);
notify('globals.dataSaved', 'positive');
if (usersPreviewRef.value) usersPreviewRef.value.fetch();
} catch (error) {
notify('errors.create', 'negative');
} finally {
isLoading.value = false;
}
};
</script> </script>
<template> <template>
<FetchData <FormModel
url="VnUsers/preview"
:url-update="`Clients/${route.params.id}/updateUser`"
:filter="filter" :filter="filter"
@on-fetch=" model="webAccess"
(data) => { :mapper="
user = data; ({ active, name, email }) => {
setInitialData(); return {
active,
name,
email,
};
} }
" "
@on-fetch="hasCustomerRole()"
auto-load auto-load
ref="usersPreviewRef" >
url="VnUsers/preview" <template #form="{ data, validate }">
/> <QCheckbox :label="t('Enable web access')" v-model="data.active" />
<FetchData <VnInput :label="t('User')" clearable v-model="data.name" />
:url="`Clients/${route.params.id}/hasCustomerRole`"
@on-fetch="(data) => (canChangePassword = data)"
auto-load
/>
<Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown()">
<QBtnGroup push class="q-gutter-x-sm">
<QBtn
:disabled="isLoading"
:label="t('globals.reset')"
:loading="isLoading"
@click="setInitialData"
color="primary"
flat
icon="restart_alt"
type="reset"
/>
<QBtn
:disabled="isLoading"
:label="t('Change password')"
:loading="isLoading"
@click.stop="showChangePasswordDialog()"
color="primary"
flat
icon="edit"
v-if="canChangePassword"
/>
<QBtn
:disabled="isLoading || !dataChanges"
:label="t('globals.save')"
:loading="isLoading"
@click="onSubmit"
color="primary"
icon="save"
/>
</QBtnGroup>
</Teleport>
<div class="full-width flex justify-center">
<QCard class="card-width q-pa-lg">
<QCardSection>
<QForm>
<QCheckbox :label="t('Enable web access')" v-model="active" />
<div class="q-px-sm">
<VnInput :label="t('User')" clearable v-model="name" />
<VnInput <VnInput
:label="t('Recovery email')" :label="t('Recovery email')"
:rules="validate('client.email')" :rules="validate('client.email')"
clearable clearable
type="email" type="email"
v-model="email" v-model="data.email"
class="q-mt-sm" class="q-mt-sm"
> :info="t('This email is used for user to regain access their account')"
<template #append> />
<QIcon name="info" class="cursor-pointer">
<QTooltip>{{
t(
'This email is used for user to regain access their account'
)
}}</QTooltip>
</QIcon>
</template> </template>
</VnInput> <template #moreActions>
</div> <QBtn
</QForm> :label="t('Change password')"
</QCardSection> color="primary"
</QCard> icon="edit"
</div> :disable="!canChangePassword"
@click="showChangePasswordDialog()"
/>
</template>
</FormModel>
</template> </template>
<i18n> <i18n>

View File

@ -105,9 +105,9 @@ const columns = computed(() => [
component: null, component: null,
after: { after: {
component: markRaw(VnLinkPhone), component: markRaw(VnLinkPhone),
attrs: (prop) => { attrs: ({ model }) => {
return { return {
'phone-number': prop, 'phone-number': model,
}; };
}, },
}, },

View File

@ -3,7 +3,7 @@ import { useI18n } from 'vue-i18n';
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';
import VnInputDate from 'components/common/VnInputDate.vue'; import VnInputDate from 'components/common/VnInputDate.vue';
import VnCurrency from 'src/components/common/VnCurrency.vue'; import VnInputNumber from 'src/components/common/VnInputNumber.vue';
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps({ const props = defineProps({
@ -47,7 +47,11 @@ const props = defineProps({
</QItem> </QItem>
<QItem> <QItem>
<QItemSection> <QItemSection>
<VnCurrency v-model="params.amount" is-outlined /> <VnInputNumber
:label="t('Amount')"
v-model="params.amount"
is-outlined
/>
</QItemSection> </QItemSection>
</QItem> </QItem>
<QItem> <QItem>

View File

@ -1,67 +0,0 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import FetchData from 'components/FetchData.vue';
import FormModel from 'components/FormModel.vue';
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const initialData = ref({});
const setClient = (data) => {
initialData.value.credit = data.credit;
};
const toCustomerCredits = () => {
router.push({
name: 'CustomerCredits',
params: {
id: route.params.id,
},
});
};
</script>
<template>
<FetchData
:filter="filter"
@on-fetch="setClient"
auto-load
:url="`Clients/${route.params.id}/getCard`"
/>
<FormModel
:form-initial-data="initialData"
:observe-form-changes="false"
:url-update="`/Clients/${route.params.id}`"
@on-data-saved="toCustomerCredits()"
>
<template #moreActions>
<QBtn
:label="t('globals.cancel')"
@click="toCustomerCredits"
color="primary"
flat
icon="close"
/>
</template>
<template #form="{ data }">
<QInput
:label="t('Credit')"
clearable
type="number"
v-model.number="data.credit"
/>
</template>
</FormModel>
</template>
<i18n>
es:
Credit: Crédito
</i18n>

View File

@ -1,97 +0,0 @@
<script setup>
import { onMounted, reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import FetchData from 'components/FetchData.vue';
import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnInputDate from 'components/common/VnInputDate.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const initialData = reactive({
shipped: '2001-01-01T11:00:00.000Z',
});
const greugeTypes = ref([]);
onMounted(() => {
initialData.clientFk = `${route.params.id}`;
});
const toCustomerGreuges = () => {
router.push({
name: 'CustomerGreuges',
params: {
id: route.params.id,
},
});
};
</script>
<template>
<fetch-data @on-fetch="(data) => (greugeTypes = data)" auto-load url="greugeTypes" />
<FormModel
:form-initial-data="initialData"
:observe-form-changes="false"
@on-data-saved="toCustomerGreuges()"
model="client"
url-create="Greuges"
>
<template #moreActions>
<QBtn
:label="t('globals.cancel')"
@click="toCustomerGreuges"
color="primary"
flat
icon="close"
/>
</template>
<template #form="{ data }">
<VnRow>
<VnInput
:label="t('Amount')"
clearable
type="number"
v-model="data.amount"
/>
<VnInputDate :label="t('Date')" v-model="data.shipped" />
</VnRow>
<VnRow>
<VnInput :label="t('Comment')" clearable v-model="data.description" />
<VnSelect
:label="t('Type')"
:options="greugeTypes"
hide-selected
option-label="name"
option-value="id"
v-model="data.greugeTypeFk"
/>
</VnRow>
</template>
</FormModel>
</template>
<style lang="scss" scoped>
.add-icon {
cursor: pointer;
background-color: $primary;
border-radius: 50px;
}
</style>
<i18n>
es:
Amount: Importe
Date: Fecha
Comment: Comentario
Type: Tipo
</i18n>

View File

@ -2,21 +2,25 @@
import { onBeforeMount, reactive, ref } from 'vue'; import { onBeforeMount, reactive, 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 { useDialogPluginComponent } from 'quasar'; import { useDialogPluginComponent } from 'quasar';
import { usePrintService } from 'composables/usePrintService';
import useNotify from 'src/composables/useNotify.js';
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 VnInputDate from 'components/common/VnInputDate.vue'; import VnInputDate from 'components/common/VnInputDate.vue';
import VnInputNumber from 'components/common/VnInputNumber.vue';
import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelect from 'src/components/common/VnSelect.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import { useState } from 'src/composables/useState';
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
const { notify } = useNotify();
const { sendEmail, openReport } = usePrintService();
const { dialogRef } = useDialogPluginComponent(); const { dialogRef } = useDialogPluginComponent();
const state = useState();
const user = state.getUser();
const $props = defineProps({ const $props = defineProps({
companyId: { companyId: {
@ -29,7 +33,7 @@ const $props = defineProps({
}, },
promise: { promise: {
type: Function, type: Function,
required: true, default: null,
}, },
}); });
@ -38,11 +42,12 @@ const urlCreate = ref([]);
const companyOptions = ref([]); const companyOptions = ref([]);
const bankOptions = ref([]); const bankOptions = ref([]);
const clientFindOne = ref([]); const clientFindOne = ref([]);
const deliveredAmount = ref(null); const viewReceipt = ref();
const amountToReturn = ref(null); const shouldSendEmail = ref(false);
const viewRecipt = ref(true); const maxAmount = ref();
const sendEmail = ref(false); const accountingType = ref({});
const isLoading = ref(false); const isCash = ref(false);
const formModelRef = ref(false);
const filterBanks = { const filterBanks = {
fields: ['id', 'bank', 'accountingTypeFk'], fields: ['id', 'bank', 'accountingTypeFk'],
@ -57,77 +62,135 @@ const filterClientFindOne = {
id: route.params.id, id: route.params.id,
}, },
}; };
const initialData = reactive({ const initialData = reactive({
amountPaid: $props.totalCredit, amountPaid: $props.totalCredit,
clientFk: route.params.id, clientFk: route.params.id,
companyFk: $props.companyId, companyFk: $props.companyId,
email: clientFindOne.value.email, email: clientFindOne.value.email,
bankFk: user.value.localBankFk,
}); });
onBeforeMount(() => { onBeforeMount(() => {
urlCreate.value = `Clients/${route.params.id}/createReceipt`; urlCreate.value = `Clients/${route.params.id}/createReceipt`;
}); });
const setPaymentType = (id) => { function setPaymentType(accounting) {
initialData.payed = '2001-01-01T11:00:00.000Z'; if (!accounting) return;
if (id === 1) initialData.description = 'Credit card'; accountingType.value = accounting.accountingType;
if (id === 2) initialData.description = 'Cash';
if (id === 3 || id === 3117) initialData.description = ''; initialData.description = [];
if (id === 4) initialData.description = 'Transfer'; initialData.payed = Date.vnNew();
}; isCash.value = accountingType.value.code == 'cash';
viewReceipt.value = isCash.value;
if (accountingType.value.daysInFuture)
initialData.payed.setDate(
initialData.payed.getDate() + accountingType.value.daysInFuture
);
maxAmount.value = accountingType.value && accountingType.value.maxAmount;
if (accountingType.value.code == 'compensation')
return (initialData.description = '');
if (accountingType.value.receiptDescription)
initialData.description.push(accountingType.value.receiptDescription);
if (initialData.description) initialData.description.push(initialData.description);
initialData.description = initialData.description.join(', ');
}
const calculateFromAmount = (event) => { const calculateFromAmount = (event) => {
amountToReturn.value = parseFloat(event) * -1 + parseFloat(deliveredAmount.value); initialData.amountToReturn =
parseFloat(initialData.deliveredAmount) + parseFloat(event) * -1;
}; };
const calculateFromDeliveredAmount = (event) => { const calculateFromDeliveredAmount = (event) => {
amountToReturn.value = parseFloat($props.totalCredit) * -1 + parseFloat(event); initialData.amountToReturn = parseFloat(event) - initialData.amountPaid;
}; };
const setClientEmail = (data) => { function onBeforeSave(data) {
initialData.email = data.email; const exceededAmount = data.amountPaid > maxAmount.value;
}; if (isCash.value && exceededAmount)
return notify(t('Amount exceeded', { maxAmount: maxAmount.value }), 'negative');
const onDataSaved = async () => { if (isCash.value && shouldSendEmail.value && !data.email)
isLoading.value = true; return notify(t('There is no assigned email for this client'), 'negative');
if ($props.promise) {
data.bankFk = data.bankFk.id;
return data;
}
async function onDataSaved(formData, { id }) {
try { try {
await $props.promise(); if (shouldSendEmail.value && isCash.value)
await sendEmail(`Receipts/${id}/receipt-email`, {
recipient: formData.email,
});
if (viewReceipt.value) openReport(`Receipts/${id}/receipt-pdf`);
} finally { } finally {
isLoading.value = false; if ($props.promise) $props.promise();
if (closeButton.value) closeButton.value.click(); if (closeButton.value) closeButton.value.click();
} }
} }
async function accountShortToStandard({ target: { value } }) {
if (!value) return (initialData.description = '');
initialData.compensationAccount = value.replace('.', '0'.repeat(11 - value.length));
const params = { bankAccount: initialData.compensationAccount };
const { data } = await axios(`Clients/getClientOrSupplierReference`, { params });
if (!data.clientId) {
initialData.description = t('Supplier Compensation Reference', {
supplierId: data.supplierId,
supplierName: data.supplierName,
});
return;
}
initialData.description = t('Client Compensation Reference', {
clientId: data.clientId,
clientName: data.clientName,
});
}
async function getAmountPaid() {
const filter = {
where: {
clientFk: route.params.id,
companyFk: initialData.companyFk,
},
}; };
const { data } = await axios(`ClientRisks`, {
params: { filter: JSON.stringify(filter) },
});
initialData.amountPaid = (data?.length && data[0].amount) || undefined;
}
</script> </script>
<template> <template>
<QDialog ref="dialogRef"> <QDialog ref="dialogRef" persistent>
<fetch-data <FetchData
@on-fetch="(data) => (companyOptions = data)" @on-fetch="(data) => (companyOptions = data)"
auto-load auto-load
url="Companies" url="Companies"
/> />
<fetch-data <FetchData
:filter="filterBanks" :filter="filterBanks"
@on-fetch="(data) => (bankOptions = data)" @on-fetch="(data) => (bankOptions = data)"
auto-load auto-load
url="Accountings" url="Accountings"
/> />
<fetch-data <FetchData
:filter="filterClientFindOne" :filter="filterClientFindOne"
@on-fetch="setClientEmail" @on-fetch="({ email }) => (initialData.email = email)"
auto-load auto-load
url="Clients/findOne" url="Clients/findOne"
/> />
<FormModel <FormModel
:default-actions="false" ref="formModelRef"
:form-initial-data="initialData" :form-initial-data="initialData"
:observe-form-changes="false" :observe-form-changes="false"
:url-create="urlCreate" :url-create="urlCreate"
@on-data-saved="onDataSaved()" :mapper="onBeforeSave"
@on-data-saved="onDataSaved"
> >
<template #form="{ data, validate }"> <template #form="{ data, validate }">
<span ref="closeButton" class="row justify-end close-icon" v-close-popup> <span ref="closeButton" class="row justify-end close-icon" v-close-popup>
@ -151,19 +214,22 @@ const onDataSaved = async () => {
option-label="code" option-label="code"
option-value="id" option-value="id"
v-model="data.companyFk" v-model="data.companyFk"
@update:model-value="getAmountPaid()"
/> />
</VnRow> </VnRow>
<VnRow> <VnRow>
<VnSelect <VnSelect
:label="t('Bank')" :label="t('Bank')"
:options="bankOptions"
:required="true"
@update:model-value="setPaymentType($event)"
hide-selected
option-label="bank"
option-value="id"
v-model="data.bankFk" v-model="data.bankFk"
url="Accountings"
option-label="bank"
:include="{ relation: 'accountingType' }"
sort-by="id"
:limit="0"
@update:model-value="
(value, options) => setPaymentType(value, options)
"
:emit-value="false"
> >
<template #option="scope"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem v-bind="scope.itemProps">
@ -180,22 +246,21 @@ const onDataSaved = async () => {
:required="true" :required="true"
@update:model-value="calculateFromAmount($event)" @update:model-value="calculateFromAmount($event)"
clearable clearable
type="number"
v-model.number="data.amountPaid" v-model.number="data.amountPaid"
/> />
</VnRow> </VnRow>
<div v-if="data.bankFk?.accountingType?.code == 'compensation'">
<div class="text-h6" v-if="data.bankFk === 3 || data.bankFk === 3117"> <div class="text-h6">
{{ t('Compensation') }} {{ t('Compensation') }}
</div> </div>
<VnRow> <VnRow>
<div class="col" v-if="data.bankFk === 3 || data.bankFk === 3117">
<VnInput <VnInput
:label="t('Compensation account')" :label="t('Compensation account')"
clearable clearable
v-model="data.compensationAccount" v-model="data.compensationAccount"
@blur="accountShortToStandard"
/> />
</VnRow>
</div> </div>
<VnInput <VnInput
:label="t('Reference')" :label="t('Reference')"
@ -203,39 +268,32 @@ const onDataSaved = async () => {
clearable clearable
v-model="data.description" v-model="data.description"
/> />
</VnRow>
<div class="q-mt-lg" v-if="data.bankFk === 2"> <div v-if="data.bankFk?.accountingType?.code == 'cash'">
<div class="text-h6">{{ t('Cash') }}</div> <div class="text-h6">{{ t('Cash') }}</div>
<VnRow> <VnRow>
<VnInput <VnInputNumber
:label="t('Delivered amount')" :label="t('Delivered amount')"
@update:model-value="calculateFromDeliveredAmount($event)" @update:model-value="calculateFromDeliveredAmount($event)"
clearable clearable
type="number" v-model="data.deliveredAmount"
v-model="deliveredAmount"
/> />
<VnInput <VnInputNumber
:label="t('Amount to return')" :label="t('Amount to return')"
clearable
disable disable
type="number" v-model="data.amountToReturn"
v-model="amountToReturn"
/> />
</VnRow> </VnRow>
<VnRow> <VnRow>
<QCheckbox v-model="viewRecipt" /> <QCheckbox v-model="viewReceipt" :label="t('View recipt')" />
<QCheckbox v-model="sendEmail" /> <QCheckbox v-model="shouldSendEmail" :label="t('Send email')" />
</VnRow> </VnRow>
</div> </div>
<div class="q-mt-lg row justify-end"> <div class="q-mt-lg row justify-end">
<QBtn <QBtn
:disabled="isLoading" :disabled="formModelRef.isLoading"
:label="t('globals.cancel')" :label="t('globals.cancel')"
:loading="isLoading" :loading="formModelRef.isLoading"
class="q-ml-sm" class="q-ml-sm"
color="primary" color="primary"
flat flat
@ -243,9 +301,9 @@ const onDataSaved = async () => {
v-close-popup v-close-popup
/> />
<QBtn <QBtn
:disabled="isLoading" :disabled="formModelRef.isLoading"
:label="t('globals.save')" :label="t('globals.save')"
:loading="isLoading" :loading="formModelRef.isLoading"
color="primary" color="primary"
type="submit" type="submit"
/> />
@ -270,4 +328,8 @@ es:
Send email: Enviar correo Send email: Enviar correo
Compensation: Compensación Compensation: Compensación
Compensation account: Cuenta para compensar Compensation account: Cuenta para compensar
Supplier Compensation Reference: ({supplierId}) Ntro Proveedor {supplierName}
Client Compensation Reference: ({clientId}) Ntro Cliente {clientName}
There is no assigned email for this client: No hay correo asignado para este cliente
Amount exceeded: Según ley contra el fraude no se puede recibir cobros por importe igual o superior a {maxAmount}
</i18n> </i18n>

View File

@ -1,57 +0,0 @@
<script setup>
import { onMounted, reactive } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue';
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const initialData = reactive({});
onMounted(() => {
initialData.clientFk = `${route.params.id}`;
});
const toCustomerNotes = () => {
router.push({
name: 'CustomerNotes',
params: {
id: route.params.id,
},
});
};
</script>
<template>
<FormModel
:form-initial-data="initialData"
:observe-form-changes="false"
@on-data-saved="toCustomerNotes()"
url-create="ClientObservations"
>
<template #moreActions>
<QBtn
:label="t('globals.cancel')"
@click="toCustomerNotes"
color="primary"
flat
icon="close"
/>
</template>
<template #form="{ data }">
<VnRow>
<QInput :label="t('Note')" type="textarea" v-model="data.text" />
</VnRow>
</template>
</FormModel>
</template>
<i18n>
es:
Note: Nota
</i18n>

View File

@ -10,8 +10,10 @@ import EntryFilter from '../EntryFilter.vue';
:descriptor="EntryDescriptor" :descriptor="EntryDescriptor"
:filter-panel="EntryFilter" :filter-panel="EntryFilter"
search-data-key="EntryList" search-data-key="EntryList"
search-url="Entries/filter" :searchbar-props="{
searchbar-label="Search entries" url: 'Entries/filter',
searchbar-info="You can search by entry reference" label: 'Search entries',
info: 'You can search by entry reference',
}"
/> />
</template> </template>

View File

@ -18,9 +18,9 @@ const columns = [
name: 'itemFk', name: 'itemFk',
columnField: { columnField: {
component: VnImg, component: VnImg,
attrs: (id) => { attrs: ({ row }) => {
return { return {
id, id: row.id,
size: '50x50', size: '50x50',
width: '50px', width: '50px',
}; };

View File

@ -192,7 +192,7 @@ onMounted(async () => {
:filter="entryFilter" :filter="entryFilter"
:create="{ :create="{
urlCreate: 'Entries', urlCreate: 'Entries',
title: 'Create entry', title: t('Create entry'),
onDataSaved: ({ id }) => tableRef.redirect(id), onDataSaved: ({ id }) => tableRef.redirect(id),
formInitialData: {}, formInitialData: {},
}" }"
@ -210,4 +210,5 @@ es:
Virtual entry: Es una redada Virtual entry: Es una redada
Search entries: Buscar entradas Search entries: Buscar entradas
You can search by entry reference: Puedes buscar por referencia de la entrada You can search by entry reference: Puedes buscar por referencia de la entrada
Create entry: Crear entrada
</i18n> </i18n>

View File

@ -130,8 +130,6 @@ onBeforeMount(async () => {
}); });
onBeforeRouteUpdate(async (to, from) => { onBeforeRouteUpdate(async (to, from) => {
invoiceInCorrection.correcting.length = 0;
invoiceInCorrection.corrected = null;
if (to.params.id !== from.params.id) { if (to.params.id !== from.params.id) {
await setInvoiceCorrection(to.params.id); await setInvoiceCorrection(to.params.id);
const { data } = await axios.get(`InvoiceIns/${to.params.id}/getTotals`); const { data } = await axios.get(`InvoiceIns/${to.params.id}/getTotals`);
@ -140,6 +138,8 @@ onBeforeRouteUpdate(async (to, from) => {
}); });
async function setInvoiceCorrection(id) { async function setInvoiceCorrection(id) {
invoiceInCorrection.correcting.length = 0;
invoiceInCorrection.corrected = null;
const { data: correctingData } = await axios.get('InvoiceInCorrections', { const { data: correctingData } = await axios.get('InvoiceInCorrections', {
params: { filter: { where: { correctingFk: id } } }, params: { filter: { where: { correctingFk: id } } },
}); });
@ -198,7 +198,6 @@ async function cloneInvoice() {
const isAdministrative = () => hasAny(['administrative']); const isAdministrative = () => hasAny(['administrative']);
const isAgricultural = () => { const isAgricultural = () => {
console.error(config);
if (!config.value) return false; if (!config.value) return false;
return ( return (
invoiceIn.value?.supplier?.sageFarmerWithholdingFk === invoiceIn.value?.supplier?.sageFarmerWithholdingFk ===

View File

@ -8,9 +8,10 @@ import { useArrayData } from 'src/composables/useArrayData';
import CrudModel from 'src/components/CrudModel.vue'; import CrudModel from 'src/components/CrudModel.vue';
import FetchData from 'src/components/FetchData.vue'; 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 { toCurrency } from 'src/filters'; import { toCurrency } from 'src/filters';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import VnInputNumber from 'src/components/common/VnInputNumber.vue';
const route = useRoute(); const route = useRoute();
const { notify } = useNotify(); const { notify } = useNotify();
@ -22,9 +23,6 @@ const rowsSelected = ref([]);
const banks = ref([]); const banks = ref([]);
const invoiceInFormRef = ref(); const invoiceInFormRef = ref();
const invoiceId = +route.params.id; const invoiceId = +route.params.id;
const placeholder = 'yyyy/mm/dd';
const filter = { where: { invoiceInFk: invoiceId } }; const filter = { where: { invoiceInFk: invoiceId } };
const columns = computed(() => [ const columns = computed(() => [
@ -104,42 +102,7 @@ const getTotalAmount = (rows) => rows.reduce((acc, { amount }) => acc + +amount,
> >
<template #body-cell-duedate="{ row }"> <template #body-cell-duedate="{ row }">
<QTd> <QTd>
<QInput <VnInputDate v-model="row.dueDated" />
v-model="row.dueDated"
mask="date"
:placeholder="placeholder"
clearable
clear-icon="close"
>
<template #append>
<QIcon name="event" class="cursor-pointer">
<QPopupProxy
cover
transition-show="scale"
transition-hide="scale"
>
<QDate v-model="row.dueDated" landscape>
<div
class="row items-center justify-end q-gutter-sm"
>
<QBtn
:label="t('globals.cancel')"
color="primary"
flat
v-close-popup
/>
<QBtn
:label="t('globals.confirm')"
color="primary"
flat
v-close-popup
/>
</div>
</QDate>
</QPopupProxy>
</QIcon>
</template>
</QInput>
</QTd> </QTd>
</template> </template>
<template #body-cell-bank="{ row, col }"> <template #body-cell-bank="{ row, col }">
@ -164,7 +127,7 @@ const getTotalAmount = (rows) => rows.reduce((acc, { amount }) => acc + +amount,
</template> </template>
<template #body-cell-amount="{ row }"> <template #body-cell-amount="{ row }">
<QTd> <QTd>
<VnCurrency <VnInputNumber
v-model="row.amount" v-model="row.amount"
:is-outlined="false" :is-outlined="false"
clearable clearable
@ -174,7 +137,7 @@ const getTotalAmount = (rows) => rows.reduce((acc, { amount }) => acc + +amount,
</template> </template>
<template #body-cell-foreignvalue="{ row }"> <template #body-cell-foreignvalue="{ row }">
<QTd> <QTd>
<QInput <VnInputNumber
:class="{ :class="{
'no-pointer-events': !isNotEuro(invoiceIn.currency.code), 'no-pointer-events': !isNotEuro(invoiceIn.currency.code),
}" }"
@ -207,51 +170,11 @@ const getTotalAmount = (rows) => rows.reduce((acc, { amount }) => acc + +amount,
<QSeparator /> <QSeparator />
<QList> <QList>
<QItem> <QItem>
<QInput <VnInputDate
class="full-width" class="full-width"
:label="t('Date')" :label="t('Date')"
v-model="props.row.dueDated" v-model="props.row.dueDated"
mask="date"
:placeholder="placeholder"
clearable
clear-icon="close"
>
<template #append>
<QIcon name="event" class="cursor-pointer">
<QPopupProxy
cover
transition-show="scale"
transition-hide="scale"
>
<QDate
v-model="props.row.dueDated"
landscape
>
<div
class="row items-center justify-end q-gutter-sm"
>
<QBtn
:label="
t('globals.cancel')
"
color="primary"
flat
v-close-popup
/> />
<QBtn
:label="
t('globals.confirm')
"
color="primary"
flat
v-close-popup
/>
</div>
</QDate>
</QPopupProxy>
</QIcon>
</template>
</QInput>
</QItem> </QItem>
<QItem> <QItem>
<VnSelect <VnSelect
@ -274,16 +197,14 @@ const getTotalAmount = (rows) => rows.reduce((acc, { amount }) => acc + +amount,
</VnSelect> </VnSelect>
</QItem> </QItem>
<QItem> <QItem>
<QInput <VnInputNumber
:label="t('Amount')" :label="t('Amount')"
class="full-width" class="full-width"
v-model="props.row.amount" v-model="props.row.amount"
clearable
clear-icon="close"
/> />
</QItem> </QItem>
<QItem> <QItem>
<QInput <VnInputNumber
:label="t('Foreign value')" :label="t('Foreign value')"
class="full-width" class="full-width"
:class="{ :class="{

View File

@ -7,6 +7,7 @@ import CrudModel from 'src/components/CrudModel.vue';
import FetchData from 'src/components/FetchData.vue'; import FetchData from 'src/components/FetchData.vue';
import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelect from 'src/components/common/VnSelect.vue';
import { useArrayData } from 'src/composables/useArrayData'; import { useArrayData } from 'src/composables/useArrayData';
import VnInputNumber from 'src/components/common/VnInputNumber.vue';
const { t } = useI18n(); const { t } = useI18n();
@ -115,11 +116,7 @@ const formatOpt = (row, { model, options }, prop) => {
> >
<template #body-cell="{ row, col }"> <template #body-cell="{ row, col }">
<QTd> <QTd>
<QInput <VnInputNumber v-model="row[col.name]" />
v-model="row[col.name]"
clearable
clear-icon="close"
/>
</QTd> </QTd>
</template> </template>
<template #body-cell-code="{ row, col }"> <template #body-cell-code="{ row, col }">
@ -203,7 +200,7 @@ const formatOpt = (row, { model, options }, prop) => {
]" ]"
:key="index" :key="index"
> >
<QInput <VnInputNumber
:label="t(value)" :label="t(value)"
class="full-width" class="full-width"
v-model="props.row[value]" v-model="props.row[value]"

View File

@ -120,7 +120,6 @@ const intrastatColumns = ref([
}, },
sortable: true, sortable: true,
align: 'left', align: 'left',
style: 'width: 10px',
}, },
{ {
name: 'amount', name: 'amount',
@ -128,7 +127,6 @@ const intrastatColumns = ref([
field: (row) => toCurrency(row.amount, currency.value), field: (row) => toCurrency(row.amount, currency.value),
sortable: true, sortable: true,
align: 'left', align: 'left',
style: 'width: 10px',
}, },
{ {
name: 'net', name: 'net',
@ -415,6 +413,11 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
</QTh> </QTh>
</QTr> </QTr>
</template> </template>
<template #body-cell-code="{ value: codeCell }">
<QTd :title="codeCell" shrink>
{{ codeCell }}
</QTd>
</template>
<template #bottom-row> <template #bottom-row>
<QTr class="bg"> <QTr class="bg">
<QTd></QTd> <QTd></QTd>

View File

@ -9,7 +9,8 @@ import { toCurrency } from 'src/filters';
import FetchData from 'src/components/FetchData.vue'; import FetchData from 'src/components/FetchData.vue';
import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelect from 'src/components/common/VnSelect.vue';
import CrudModel from 'src/components/CrudModel.vue'; import CrudModel from 'src/components/CrudModel.vue';
import VnCurrency from 'src/components/common/VnCurrency.vue'; import VnInput from 'src/components/common/VnInput.vue';
import VnInputNumber from 'src/components/common/VnInputNumber.vue';
const { t } = useI18n(); const { t } = useI18n();
const quasar = useQuasar(); const quasar = useQuasar();
@ -205,7 +206,7 @@ const formatOpt = (row, { model, options }, prop) => {
:grid="$q.screen.lt.sm" :grid="$q.screen.lt.sm"
> >
<template #body-cell-expense="{ row, col }"> <template #body-cell-expense="{ row, col }">
<QTd auto-width> <QTd>
<VnSelect <VnSelect
v-model="row[col.model]" v-model="row[col.model]"
:options="col.options" :options="col.options"
@ -223,6 +224,7 @@ const formatOpt = (row, { model, options }, prop) => {
name="close" name="close"
@click.stop="value = null" @click.stop="value = null"
class="cursor-pointer" class="cursor-pointer"
size="xs"
/> />
<QIcon <QIcon
@click.stop.prevent="newExpenseRef.show()" @click.stop.prevent="newExpenseRef.show()"
@ -240,7 +242,7 @@ const formatOpt = (row, { model, options }, prop) => {
</template> </template>
<template #body-cell-taxablebase="{ row }"> <template #body-cell-taxablebase="{ row }">
<QTd> <QTd>
<VnCurrency <VnInputNumber
:class="{ :class="{
'no-pointer-events': isNotEuro(invoiceIn.currency.code), 'no-pointer-events': isNotEuro(invoiceIn.currency.code),
}" }"
@ -308,7 +310,7 @@ const formatOpt = (row, { model, options }, prop) => {
</template> </template>
<template #body-cell-foreignvalue="{ row }"> <template #body-cell-foreignvalue="{ row }">
<QTd> <QTd>
<QInput <VnInputNumber
:class="{ :class="{
'no-pointer-events': !isNotEuro(invoiceIn.currency.code), 'no-pointer-events': !isNotEuro(invoiceIn.currency.code),
}" }"
@ -356,7 +358,7 @@ const formatOpt = (row, { model, options }, prop) => {
</VnSelect> </VnSelect>
</QItem> </QItem>
<QItem> <QItem>
<VnCurrency <VnInputNumber
:label="t('Taxable base')" :label="t('Taxable base')"
:class="{ :class="{
'no-pointer-events': isNotEuro( 'no-pointer-events': isNotEuro(
@ -421,7 +423,7 @@ const formatOpt = (row, { model, options }, prop) => {
{{ toCurrency(taxRate(props.row), currency) }} {{ toCurrency(taxRate(props.row), currency) }}
</QItem> </QItem>
<QItem> <QItem>
<QInput <VnInputNumber
:label="t('Foreign value')" :label="t('Foreign value')"
class="full-width" class="full-width"
:class="{ :class="{
@ -453,7 +455,11 @@ const formatOpt = (row, { model, options }, prop) => {
</QCardSection> </QCardSection>
<QCardSection class="q-pt-none"> <QCardSection class="q-pt-none">
<QItem> <QItem>
<QInput :label="`${t('Code')}*`" v-model="newExpense.code" /> <VnInput
:label="`${t('Code')}*`"
v-model="newExpense.code"
:required="true"
/>
<QCheckbox <QCheckbox
dense dense
size="sm" size="sm"
@ -462,7 +468,7 @@ const formatOpt = (row, { model, options }, prop) => {
/> />
</QItem> </QItem>
<QItem> <QItem>
<QInput <VnInput
:label="`${t('Descripction')}*`" :label="`${t('Descripction')}*`"
v-model="newExpense.description" v-model="newExpense.description"
/> />

View File

@ -26,8 +26,7 @@ const newInvoiceIn = reactive({
companyFk: user.value.companyFk || null, companyFk: user.value.companyFk || null,
issued: Date.vnNew(), issued: Date.vnNew(),
}); });
const suppliersOptions = ref([]); const companies = ref([]);
const companiesOptions = ref([]);
const redirectToInvoiceInBasicData = (__, { id }) => { const redirectToInvoiceInBasicData = (__, { id }) => {
router.push({ name: 'InvoiceInBasicData', params: { id } }); router.push({ name: 'InvoiceInBasicData', params: { id } });
@ -35,19 +34,12 @@ const redirectToInvoiceInBasicData = (__, { id }) => {
</script> </script>
<template> <template>
<FetchData
url="Suppliers"
:filter="{ fields: ['id', 'nickname'] }"
order="nickname"
@on-fetch="(data) => (suppliersOptions = data)"
auto-load
/>
<FetchData <FetchData
ref="companiesRef" ref="companiesRef"
url="Companies" url="Companies"
:filter="{ fields: ['id', 'code'] }" :filter="{ fields: ['id', 'code'] }"
order="code" order="code"
@on-fetch="(data) => (companiesOptions = data)" @on-fetch="(data) => (companies = data)"
auto-load auto-load
/> />
<template v-if="stateStore.isHeaderMounted()"> <template v-if="stateStore.isHeaderMounted()">
@ -69,9 +61,10 @@ const redirectToInvoiceInBasicData = (__, { id }) => {
<template #form="{ data, validate }"> <template #form="{ data, validate }">
<VnRow> <VnRow>
<VnSelect <VnSelect
url="Suppliers"
:fields="['id', 'nickname']"
:label="t('Supplier')" :label="t('Supplier')"
v-model="data.supplierFk" v-model="data.supplierFk"
:options="suppliersOptions"
option-value="id" option-value="id"
option-label="nickname" option-label="nickname"
hide-selected hide-selected
@ -98,7 +91,7 @@ const redirectToInvoiceInBasicData = (__, { id }) => {
<VnSelect <VnSelect
:label="t('Company')" :label="t('Company')"
v-model="data.companyFk" v-model="data.companyFk"
:options="companiesOptions" :options="companies"
option-value="id" option-value="id"
option-label="code" option-label="code"
map-options map-options

View File

@ -6,8 +6,8 @@ import VnSelect from 'components/common/VnSelect.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';
import VnInputDate from 'components/common/VnInputDate.vue'; import VnInputDate from 'components/common/VnInputDate.vue';
import VnCurrency from 'src/components/common/VnCurrency.vue';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import VnInputNumber from 'src/components/common/VnInputNumber.vue';
const { t } = useI18n(); const { t } = useI18n();
defineProps({ dataKey: { type: String, required: true } }); defineProps({ dataKey: { type: String, required: true } });
@ -28,6 +28,22 @@ const activities = ref([]);
</div> </div>
</template> </template>
<template #body="{ params, searchFn }"> <template #body="{ params, searchFn }">
<QItem>
<QItemSection>
<VnSelect
v-model="params.supplierFk"
url="Suppliers"
:fields="['id', 'nickname']"
:label="t('params.supplierFk')"
option-value="id"
option-label="nickname"
dense
outlined
rounded
:filter-options="['id', 'name']"
/>
</QItemSection>
</QItem>
<QItem> <QItem>
<QItemSection> <QItemSection>
<VnInput <VnInput
@ -50,17 +66,30 @@ const activities = ref([]);
</QItem> </QItem>
<QItem> <QItem>
<QItemSection> <QItemSection>
<VnSelect <VnInput
v-model="params.supplierFk" :label="t('params.serialNumber')"
url="Suppliers" v-model="params.serialNumber"
:fields="['id', 'nickname']" is-outlined
:label="t('params.supplierFk')" lazy-rules
option-value="id" />
option-label="nickname" </QItemSection>
dense </QItem>
outlined <QItem>
rounded <QItemSection>
:filter-options="['id', 'name']" <VnInput
:label="t('params.serial')"
v-model="params.serial"
is-outlined
lazy-rules
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInputDate
:label="t('Issued')"
v-model="params.issued"
is-outlined
/> />
</QItemSection> </QItemSection>
</QItem> </QItem>
@ -76,39 +105,20 @@ const activities = ref([]);
</QItem> </QItem>
<QItem> <QItem>
<QItemSection> <QItemSection>
<VnCurrency v-model="params.amount" is-outlined /> <VnInput
</QItemSection> :label="t('params.awb')"
</QItem> v-model="params.awbCode"
<QItem>
<QItemSection>
<VnInputDate :label="t('From')" v-model="params.from" is-outlined />
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInputDate :label="t('To')" v-model="params.to" is-outlined />
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInputDate
:label="t('Issued')"
v-model="params.issued"
is-outlined is-outlined
lazy-rules
/> />
</QItemSection> </QItemSection>
</QItem> </QItem>
<QItem> <QItem>
<QItemSection> <QItemSection>
<VnSelect <VnInputNumber
:label="t('params.supplierActivityFk')" :label="t('Amount')"
v-model="params.supplierActivityFk" v-model="params.amount"
dense is-outlined
outlined
rounded
option-value="code"
option-label="name"
:options="activities"
/> />
</QItemSection> </QItemSection>
</QItem> </QItem>
@ -133,32 +143,16 @@ const activities = ref([]);
<QExpansionItem :label="t('More options')" expand-separator> <QExpansionItem :label="t('More options')" expand-separator>
<QItem> <QItem>
<QItemSection> <QItemSection>
<VnInput <VnInputDate
:label="t('params.serialNumber')" :label="t('From')"
v-model="params.serialNumber" v-model="params.from"
is-outlined is-outlined
lazy-rules
/> />
</QItemSection> </QItemSection>
</QItem> </QItem>
<QItem> <QItem>
<QItemSection> <QItemSection>
<VnInput <VnInputDate :label="t('To')" v-model="params.to" is-outlined />
:label="t('params.serial')"
v-model="params.serial"
is-outlined
lazy-rules
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInput
:label="t('params.awb')"
v-model="params.awbCode"
is-outlined
lazy-rules
/>
</QItemSection> </QItemSection>
</QItem> </QItem>
</QExpansionItem> </QExpansionItem>

View File

@ -1,18 +1,19 @@
<script setup> <script setup>
import { onMounted, onUnmounted } from 'vue'; import { ref, computed, onMounted, onUnmounted } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import { downloadFile } from 'src/composables/downloadFile'; import { downloadFile } from 'src/composables/downloadFile';
import { toDate, toCurrency } from 'src/filters/index'; import { toDate, toCurrency } from 'src/filters/index';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import CardList from 'src/components/ui/CardList.vue';
import InvoiceInFilter from './InvoiceInFilter.vue'; import InvoiceInFilter from './InvoiceInFilter.vue';
import InvoiceInSummary from './Card/InvoiceInSummary.vue'; import InvoiceInSummary from './Card/InvoiceInSummary.vue';
import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import SupplierDescriptorProxy from 'src/pages/Supplier/Card/SupplierDescriptorProxy.vue'; import SupplierDescriptorProxy from 'src/pages/Supplier/Card/SupplierDescriptorProxy.vue';
import RightMenu from 'src/components/common/RightMenu.vue'; import RightMenu from 'src/components/common/RightMenu.vue';
import InvoiceInSearchbar from 'src/pages/InvoiceIn/InvoiceInSearchbar.vue'; import InvoiceInSearchbar from 'src/pages/InvoiceIn/InvoiceInSearchbar.vue';
import VnTable from 'src/components/VnTable/VnTable.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
const stateStore = useStateStore(); const stateStore = useStateStore();
const { viewSummary } = useSummaryDialog(); const { viewSummary } = useSummaryDialog();
@ -20,8 +21,91 @@ const { t } = useI18n();
onMounted(async () => (stateStore.rightDrawer = true)); onMounted(async () => (stateStore.rightDrawer = true));
onUnmounted(() => (stateStore.rightDrawer = false)); onUnmounted(() => (stateStore.rightDrawer = false));
</script>
const tableRef = ref();
const cols = computed(() => [
{
align: 'left',
name: 'id',
label: 'Id',
},
{
align: 'left',
name: 'supplierFk',
label: t('invoiceIn.list.supplier'),
columnFilter: {
component: 'select',
attrs: {
url: 'Suppliers',
fields: ['id', 'name'],
},
},
columnClass: 'expand',
},
{
align: 'left',
name: 'supplierRef',
label: t('invoiceIn.list.supplierRef'),
},
{
align: 'left',
name: 'serialNumber',
label: t('invoiceIn.list.serialNumber'),
},
{
align: 'left',
name: 'serial',
label: t('invoiceIn.list.serial'),
},
{
align: 'left',
label: t('invoiceIn.list.issued'),
name: 'issued',
component: null,
columnFilter: {
component: 'date',
},
format: (row, dashIfEmpty) => dashIfEmpty(toDate(row.issued)),
},
{
align: 'left',
name: 'isBooked',
label: t('invoiceIn.list.isBooked'),
columnFilter: false,
},
{
align: 'left',
name: 'awbCode',
label: t('invoiceIn.list.awb'),
},
{
align: 'left',
name: 'amount',
label: t('invoiceIn.list.amount'),
format: ({ amount }) => toCurrency(amount),
},
{
align: 'right',
name: 'tableActions',
actions: [
{
title: t('components.smartCard.openSummary'),
icon: 'preview',
type: 'submit',
action: (row) => viewSummary(row.id, InvoiceInSummary),
},
{
title: t('globals.download'),
icon: 'download',
type: 'submit',
isPrimary: true,
action: (row) => downloadFile(row.dmsFk),
},
],
},
]);
</script>
<template> <template>
<InvoiceInSearchbar /> <InvoiceInSearchbar />
<RightMenu> <RightMenu>
@ -29,92 +113,63 @@ onUnmounted(() => (stateStore.rightDrawer = false));
<InvoiceInFilter data-key="InvoiceInList" /> <InvoiceInFilter data-key="InvoiceInList" />
</template> </template>
</RightMenu> </RightMenu>
<QPage class="column items-center q-pa-md"> <VnTable
<div class="vn-card-list"> ref="tableRef"
<VnPaginate
data-key="InvoiceInList" data-key="InvoiceInList"
url="InvoiceIns/filter" url="InvoiceIns/filter"
order="issued DESC, id DESC" :order="['issued DESC', 'id DESC']"
auto-load :create="{
urlCreate: 'InvoiceIns',
title: t('globals.createInvoiceIn'),
onDataSaved: ({ id }) => tableRef.redirect(id),
formInitialData: {},
}"
redirect="invoice-in"
:columns="cols"
:right-search="false"
:disable-option="{ card: true }"
:auto-load="!!$route.query.params"
> >
<template #body="{ rows }"> <template #column-supplierFk="{ row }">
<CardList <span class="link" @click.stop>
v-for="(row, index) of rows"
:key="index"
:title="row.supplierRef"
@click="$router.push({ path: `/invoice-in/${row.id}` })"
:id="row.id"
>
<template #list-items>
<VnLv
:label="t('invoiceIn.list.supplierRef')"
:value="row.supplierRef"
/>
<VnLv
:label="t('invoiceIn.list.supplier')"
:value="row.supplierName"
@click.stop
>
<template #value>
<span class="link">
{{ row.supplierName }} {{ row.supplierName }}
<SupplierDescriptorProxy :id="row.supplierFk" /> <SupplierDescriptorProxy :id="row.supplierFk" />
</span> </span>
</template> </template>
</VnLv> <template #more-create-dialog="{ data }">
<VnLv <VnSelect
:label="t('invoiceIn.list.serialNumber')" v-model="data.supplierFk"
:value="row.serialNumber" url="Suppliers"
/> :fields="['id', 'nickname']"
<VnLv :label="t('Supplier')"
:label="t('invoiceIn.list.serial')" option-value="id"
:value="row.serial" option-label="nickname"
/> :filter-options="['id', 'name']"
<VnLv :required="true"
:label="t('invoiceIn.list.issued')" >
:value="toDate(row.issued)" <template #option="scope">
/> <QItem v-bind="scope.itemProps">
<VnLv :label="t('invoiceIn.list.awb')" :value="row.awbCode" /> <QItemSection>
<VnLv <QItemLabel>{{ scope.opt?.nickname }}</QItemLabel>
:label="t('invoiceIn.list.amount')" <QItemLabel caption> #{{ scope.opt?.id }} </QItemLabel>
:value="toCurrency(row.amount)" </QItemSection>
/> </QItem>
<VnLv
:label="t('invoiceIn.list.isBooked')"
:value="!!row.isBooked"
/>
</template> </template>
<template #actions> </VnSelect>
<QBtn <VnInput
:label="t('components.smartCard.openCard')" :label="t('invoiceIn.summary.supplierRef')"
@click.stop=" v-model="data.supplierRef"
$router.push({ path: `/invoice-in/${row.id}` })
"
outline
type="reset"
/> />
<VnSelect
<QBtn url="Companies"
:label="t('components.smartCard.openSummary')" :label="t('Company')"
@click.stop="viewSummary(row.id, InvoiceInSummary)" :fields="['id', 'code']"
color="primary" v-model="data.companyFk"
type="submit" option-value="id"
class="q-mt-sm" option-label="code"
/> :required="true"
<QBtn
:label="t('globals.download')"
class="q-mt-sm"
@click.stop="downloadFile(row.dmsFk)"
type="submit"
color="primary"
/> />
<VnInputDate :label="t('invoiceIn.summary.issued')" v-model="data.issued" />
</template> </template>
</CardList> </VnTable>
</template>
</VnPaginate>
</div>
</QPage>
<QPageSticky position="bottom-right" :offset="[20, 20]">
<QBtn color="primary" icon="add" size="lg" round :href="`/#/invoice-in/create`" />
</QPageSticky>
</template> </template>

View File

@ -10,8 +10,10 @@ import InvoiceOutFilter from '../InvoiceOutFilter.vue';
:descriptor="InvoiceOutDescriptor" :descriptor="InvoiceOutDescriptor"
:filter-panel="InvoiceOutFilter" :filter-panel="InvoiceOutFilter"
search-data-key="InvoiceOutList" search-data-key="InvoiceOutList"
search-url="InvoiceOuts/filter" :searchbar-props="{
searchbar-label="Search invoice" url: 'InvoiceOuts/filter',
searchbar-info="You can search by invoice reference" label: 'Search invoice',
info: 'You can search by invoice reference',
}"
/> />
</template> </template>

View File

@ -6,7 +6,7 @@ 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';
import VnInputDate from 'components/common/VnInputDate.vue'; import VnInputDate from 'components/common/VnInputDate.vue';
import VnCurrency from 'src/components/common/VnCurrency.vue'; import VnInputNumber from 'src/components/common/VnInputNumber.vue';
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps({ const props = defineProps({
@ -58,7 +58,7 @@ function setWorkers(data) {
</QItem> </QItem>
<QItem> <QItem>
<QItemSection> <QItemSection>
<VnCurrency <VnInputNumber
:label="t('Amount')" :label="t('Amount')"
v-model="params.amount" v-model="params.amount"
is-outlined is-outlined

View File

@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n';
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';
import VnInputDate from 'components/common/VnInputDate.vue'; import VnInputDate from 'components/common/VnInputDate.vue';
import VnCurrency from 'src/components/common/VnCurrency.vue'; import VnInputNumber from 'src/components/common/VnInputNumber.vue';
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps({ const props = defineProps({
@ -86,7 +86,7 @@ const props = defineProps({
</QItem> </QItem>
<QItem> <QItem>
<QItemSection> <QItemSection>
<VnCurrency <VnInputNumber
v-model="params.amount" v-model="params.amount"
:label="t('invoiceOut.negativeBases.amount')" :label="t('invoiceOut.negativeBases.amount')"
is-outlined is-outlined

View File

@ -36,15 +36,6 @@ const onIntrastatCreated = (response, formData) => {
@on-fetch="(data) => (itemTypesOptions = data)" @on-fetch="(data) => (itemTypesOptions = data)"
auto-load auto-load
/> />
<FetchData
url="Items/withName"
:filter="{
fields: ['id', 'name'],
order: 'id DESC',
}"
@on-fetch="(data) => (itemsWithNameOptions = data)"
auto-load
/>
<FetchData <FetchData
url="Intrastats" url="Intrastats"
:filter="{ :filter="{
@ -73,7 +64,7 @@ const onIntrastatCreated = (response, formData) => {
<template #form="{ data }"> <template #form="{ data }">
<VnRow> <VnRow>
<VnSelect <VnSelect
:label="t('basicData.type')" :label="t('itemBasicData.type')"
v-model="data.typeFk" v-model="data.typeFk"
:options="itemTypesOptions" :options="itemTypesOptions"
option-value="id" option-value="id"
@ -92,19 +83,21 @@ const onIntrastatCreated = (response, formData) => {
</QItem> </QItem>
</template> </template>
</VnSelect> </VnSelect>
<VnInput :label="t('basicData.reference')" v-model="data.comment" /> <VnInput :label="t('itemBasicData.reference')" v-model="data.comment" />
<VnInput :label="t('basicData.relevancy')" v-model="data.relevancy" /> <VnInput :label="t('itemBasicData.relevancy')" v-model="data.relevancy" />
</VnRow> </VnRow>
<VnRow> <VnRow>
<VnInput :label="t('basicData.stems')" v-model="data.stems" /> <VnInput :label="t('itemBasicData.stems')" v-model="data.stems" />
<VnInput <VnInput
:label="t('basicData.multiplier')" :label="t('itemBasicData.multiplier')"
v-model="data.stemMultiplier" v-model="data.stemMultiplier"
/> />
<VnSelectDialog <VnSelectDialog
:label="t('basicData.generic')" :label="t('itemBasicData.generic')"
v-model="data.genericFk" v-model="data.genericFk"
:options="itemsWithNameOptions" url="Items/withName"
:fields="['id', 'name']"
sort-by="id DESC"
option-value="id" option-value="id"
option-label="name" option-label="name"
map-options map-options
@ -129,7 +122,7 @@ const onIntrastatCreated = (response, formData) => {
</VnRow> </VnRow>
<VnRow> <VnRow>
<VnSelectDialog <VnSelectDialog
:label="t('basicData.intrastat')" :label="t('itemBasicData.intrastat')"
v-model="data.intrastatFk" v-model="data.intrastatFk"
:options="intrastatsOptions" :options="intrastatsOptions"
option-value="id" option-value="id"
@ -156,7 +149,7 @@ const onIntrastatCreated = (response, formData) => {
</VnSelectDialog> </VnSelectDialog>
<div class="col"> <div class="col">
<VnSelect <VnSelect
:label="t('basicData.expense')" :label="t('itemBasicData.expense')"
v-model="data.expenseFk" v-model="data.expenseFk"
:options="expensesOptions" :options="expensesOptions"
option-value="id" option-value="id"
@ -168,61 +161,64 @@ const onIntrastatCreated = (response, formData) => {
</VnRow> </VnRow>
<VnRow> <VnRow>
<VnInput <VnInput
:label="t('basicData.weightByPiece')" :label="t('itemBasicData.weightByPiece')"
v-model.number="data.weightByPiece" v-model.number="data.weightByPiece"
:min="0" :min="0"
type="number" type="number"
/> />
<VnInput <VnInput
:label="t('basicData.boxUnits')" :label="t('itemBasicData.boxUnits')"
v-model.number="data.packingOut" v-model.number="data.packingOut"
:min="0" :min="0"
type="number" type="number"
/> />
<VnInput <VnInput
:label="t('basicData.recycledPlastic')" :label="t('itemBasicData.recycledPlastic')"
v-model.number="data.recycledPlastic" v-model.number="data.recycledPlastic"
:min="0" :min="0"
type="number" type="number"
/> />
<VnInput <VnInput
:label="t('basicData.nonRecycledPlastic')" :label="t('itemBasicData.nonRecycledPlastic')"
v-model.number="data.nonRecycledPlastic" v-model.number="data.nonRecycledPlastic"
:min="0" :min="0"
type="number" type="number"
/> />
</VnRow> </VnRow>
<VnRow> <VnRow>
<QCheckbox v-model="data.isActive" :label="t('basicData.isActive')" /> <QCheckbox v-model="data.isActive" :label="t('itemBasicData.isActive')" />
<QCheckbox v-model="data.hasKgPrice" :label="t('basicData.hasKgPrice')" /> <QCheckbox
v-model="data.hasKgPrice"
:label="t('itemBasicData.hasKgPrice')"
/>
<div> <div>
<QCheckbox <QCheckbox
v-model="data.isFragile" v-model="data.isFragile"
:label="t('basicData.isFragile')" :label="t('itemBasicData.isFragile')"
class="q-mr-sm" class="q-mr-sm"
/> />
<QIcon name="info" class="cursor-pointer" size="xs"> <QIcon name="info" class="cursor-pointer" size="xs">
<QTooltip max-width="300px"> <QTooltip max-width="300px">
{{ t('basicData.isFragileTooltip') }} {{ t('itemBasicData.isFragileTooltip') }}
</QTooltip> </QTooltip>
</QIcon> </QIcon>
</div> </div>
<div> <div>
<QCheckbox <QCheckbox
v-model="data.isPhotoRequested" v-model="data.isPhotoRequested"
:label="t('basicData.isPhotoRequested')" :label="t('itemBasicData.isPhotoRequested')"
class="q-mr-sm" class="q-mr-sm"
/> />
<QIcon name="info" class="cursor-pointer" size="xs"> <QIcon name="info" class="cursor-pointer" size="xs">
<QTooltip> <QTooltip>
{{ t('basicData.isPhotoRequestedTooltip') }} {{ t('itemBasicData.isPhotoRequestedTooltip') }}
</QTooltip> </QTooltip>
</QIcon> </QIcon>
</div> </div>
</VnRow> </VnRow>
<VnRow> <VnRow>
<VnInput <VnInput
:label="t('basicData.description')" :label="t('itemBasicData.description')"
type="textarea" type="textarea"
v-model="data.description" v-model="data.description"
fill-input fill-input

View File

@ -10,8 +10,10 @@ import ItemListFilter from '../ItemListFilter.vue';
:descriptor="ItemDescriptor" :descriptor="ItemDescriptor"
:filter-panel="ItemListFilter" :filter-panel="ItemListFilter"
search-data-key="ItemList" search-data-key="ItemList"
search-url="Items/filter" :searchbar-props="{
searchbar-label="searchbar.label" url: 'Items/filter',
searchbar-info="searchbar.info" label: 'searchbar.labelr',
info: 'searchbar.info',
}"
/> />
</template> </template>

View File

@ -112,7 +112,7 @@ const openCloneDialog = async () => {
.dialog({ .dialog({
component: VnConfirm, component: VnConfirm,
componentProps: { componentProps: {
title: t("All it's properties will be copied"), title: t('All its properties will be copied'),
message: t('Do you want to clone this item?'), message: t('Do you want to clone this item?'),
}, },
}) })
@ -215,7 +215,7 @@ const openCloneDialog = async () => {
<i18n> <i18n>
es: es:
Regularize stock: Regularizar stock Regularize stock: Regularizar stock
All it's properties will be copied: Todas sus propiedades serán copiadas All its properties will be copied: Todas sus propiedades serán copiadas
Do you want to clone this item?: ¿Desea clonar este artículo? Do you want to clone this item?: ¿Desea clonar este artículo?
</i18n> </i18n>

View File

@ -64,7 +64,7 @@ const handlePhotoUpdated = (evt = false) => {
<template> <template>
<div class="relative-position"> <div class="relative-position">
<VnImg ref="image" :id="$props.entityId" @refresh="handlePhotoUpdated(true)"> <VnImg ref="image" :id="$props.entityId" zoom-resolution="1600x900">
<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>

View File

@ -17,6 +17,7 @@ import { toDateFormat } from 'src/filters/date.js';
import { dashIfEmpty } from 'src/filters'; import { dashIfEmpty } from 'src/filters';
import { date } from 'quasar'; import { date } from 'quasar';
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
import axios from 'axios';
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
@ -34,6 +35,7 @@ const itemsBalanceFilter = reactive({
const itemBalances = ref([]); const itemBalances = ref([]);
const warehouseFk = ref(null); const warehouseFk = ref(null);
const _showWhatsBeforeInventory = ref(false); const _showWhatsBeforeInventory = ref(false);
const inventoriedDate = ref(null);
const columns = computed(() => [ const columns = computed(() => [
{ {
@ -99,7 +101,7 @@ const showWhatsBeforeInventory = computed({
set: (val) => { set: (val) => {
_showWhatsBeforeInventory.value = val; _showWhatsBeforeInventory.value = val;
if (!val) itemsBalanceFilter.where.date = null; if (!val) itemsBalanceFilter.where.date = null;
else itemsBalanceFilter.where.date = new Date(); else itemsBalanceFilter.where.date = inventoriedDate.value ?? new Date();
}, },
}); });
@ -161,6 +163,8 @@ onMounted(async () => {
if (route.query.warehouseFk) warehouseFk.value = route.query.warehouseFk; if (route.query.warehouseFk) warehouseFk.value = route.query.warehouseFk;
else if (user.value) warehouseFk.value = user.value.warehouseFk; else if (user.value) warehouseFk.value = user.value.warehouseFk;
itemsBalanceFilter.where.warehouseFk = warehouseFk.value; itemsBalanceFilter.where.warehouseFk = warehouseFk.value;
const { data } = await axios.get('Configs/findOne');
inventoriedDate.value = data.inventoried;
await fetchItemBalances(); await fetchItemBalances();
await scrollToToday(); await scrollToToday();
}); });
@ -293,7 +297,7 @@ watch(
> >
{{ row.entityId }} {{ row.entityId }}
</component> </component>
<span class="link"> <span :class="{ link: row.entityId }">
{{ dashIfEmpty(row.entityName) }} {{ dashIfEmpty(row.entityName) }}
</span> </span>
</QBadge> </QBadge>

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { onMounted, computed, onUnmounted, reactive, ref } from 'vue'; import { onMounted, computed, onUnmounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { dateRange } from 'src/filters'; import { dateRange } from 'src/filters';
@ -11,6 +11,7 @@ import { toDateTimeFormat } from 'src/filters/date.js';
import { dashIfEmpty } from 'src/filters'; import { dashIfEmpty } from 'src/filters';
import { toCurrency } from 'filters/index'; import { toCurrency } from 'filters/index';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
@ -35,26 +36,8 @@ const exprBuilder = (param, value) => {
} }
}; };
const datedRange = reactive({ const from = ref();
from: null, const to = ref();
to: null,
});
const from = computed({
get: () => datedRange.from,
set: (val) => {
updateFrom(val);
updateFilter();
},
});
const to = computed({
get: () => datedRange.to,
set: (val) => {
updateTo(val);
updateFilter();
},
});
const arrayData = useArrayData('ItemLastEntries', { const arrayData = useArrayData('ItemLastEntries', {
url: 'Items/lastEntriesFilter', url: 'Items/lastEntriesFilter',
@ -162,41 +145,48 @@ const fetchItemLastEntries = async () => {
itemLastEntries.value = data; itemLastEntries.value = data;
}; };
const updateFrom = async (date) => { const getDate = (date, type) => {
if (type == 'from') {
date.setHours(0, 0, 0, 0); date.setHours(0, 0, 0, 0);
datedRange.from = date.toISOString(); } else if (type == 'to') {
}; date.setHours(23, 59, 59, 999);
}
const updateTo = async (date) => { return date.toISOString();
date.setHours(23, 59, 59, 59);
datedRange.to = date.toISOString();
}; };
const updateFilter = async () => { const updateFilter = async () => {
arrayData.store.userFilter.where.landed = { let filter;
between: [datedRange.from, datedRange.to], if (!from.value && to.value) filter = { lte: to.value };
}; else if (from.value && !to.value) filter = { gte: from.value };
else if (from.value && to.value) filter = { between: [from.value, to.value] };
arrayData.store.userFilter.where.landed = filter;
await fetchItemLastEntries(); await fetchItemLastEntries();
}; };
onMounted(async () => { onMounted(async () => {
const _from = Date.vnNew(); const _from = Date.vnNew();
_from.setDate(_from.getDate() - 75); _from.setDate(_from.getDate() - 75);
updateFrom(_from); from.value = getDate(_from, 'from');
const _to = Date.vnNew(); const _to = Date.vnNew();
_to.setDate(_to.getDate() + 10); _to.setDate(_to.getDate() + 10);
updateTo(_to); to.value = getDate(Date.vnNew(), 'to');
updateFilter(); updateFilter();
watch([from, to], ([nFrom, nTo], [oFrom, oTo]) => {
if (nFrom && nFrom != oFrom) nFrom = getDate(new Date(nFrom), 'from');
if (nTo && nTo != oTo) nTo = getDate(new Date(nTo), 'to');
updateFilter();
});
}); });
onUnmounted(() => (stateStore.rightDrawer = false)); onUnmounted(() => (stateStore.rightDrawer = false));
</script> </script>
<template> <template>
<QToolbar class="justify-end"> <VnSubToolbar>
<div id="st-data" class="row"> <template #st-data>
<VnInputDate <VnInputDate
:label="t('lastEntries.since')" :label="t('lastEntries.since')"
dense dense
@ -204,11 +194,9 @@ onUnmounted(() => (stateStore.rightDrawer = false));
class="q-mr-lg" class="q-mr-lg"
/> />
<VnInputDate :label="t('lastEntries.to')" dense v-model="to" /> <VnInputDate :label="t('lastEntries.to')" dense v-model="to" />
</div> </template>
<QSpace /> </VnSubToolbar>
<div id="st-actions"></div> <QPage class="column items-center q-pa-xd">
</QToolbar>
<QPage class="column items-center q-pa-md">
<QTable <QTable
:rows="itemLastEntries" :rows="itemLastEntries"
:columns="columns" :columns="columns"

View File

@ -42,9 +42,10 @@ const onItemTagsFetched = async (itemTags) => {
}); });
}; };
const handleTagSelected = (rows, index, tag) => { const handleTagSelected = (rows, index, tagId) => {
const tag = tagOptions.value.find((t) => t.id === tagId);
rows[index].tag = tag; rows[index].tag = tag;
rows[index].tagFk = tag.id; rows[index].tagFk = tagId;
rows[index].value = null; rows[index].value = null;
getSelectedTagValues(rows[index]); getSelectedTagValues(rows[index]);
}; };
@ -94,7 +95,6 @@ const insertTag = (rows) => {
:filter="{ :filter="{
fields: ['id', 'itemFk', 'tagFk', 'value', 'priority'], fields: ['id', 'itemFk', 'tagFk', 'value', 'priority'],
where: { itemFk: route.params.id }, where: { itemFk: route.params.id },
order: 'priority ASC',
include: { include: {
relation: 'tag', relation: 'tag',
scope: { scope: {
@ -102,16 +102,13 @@ const insertTag = (rows) => {
}, },
}, },
}" }"
order="priority"
auto-load auto-load
@on-fetch="onItemTagsFetched" @on-fetch="onItemTagsFetched"
> >
<template #body="{ rows, validate }"> <template #body="{ rows, validate }">
<QCard class="q-pl-lg q-py-md"> <QCard class="q-px-lg q-pt-md q-pb-sm">
<VnRow <VnRow v-for="(row, index) in rows" :key="index">
v-for="(row, index) in rows"
:key="index"
class="row q-gutter-md q-mb-md"
>
<VnSelect <VnSelect
:label="t('itemTags.tag')" :label="t('itemTags.tag')"
:options="tagOptions" :options="tagOptions"
@ -119,7 +116,7 @@ const insertTag = (rows) => {
option-label="name" option-label="name"
hide-selected hide-selected
@update:model-value=" @update:model-value="
($event) => handleTagSelected(rows, index, $event) (val) => handleTagSelected(rows, index, val)
" "
:required="true" :required="true"
:rules="validate('itemTag.tagFk')" :rules="validate('itemTag.tagFk')"
@ -146,7 +143,6 @@ const insertTag = (rows) => {
v-model="row.value" v-model="row.value"
:label="t('itemTags.value')" :label="t('itemTags.value')"
:is-clearable="false" :is-clearable="false"
style="width: 100%"
/> />
<VnInput <VnInput
:label="t('itemTags.relevancy')" :label="t('itemTags.relevancy')"
@ -155,7 +151,7 @@ const insertTag = (rows) => {
:required="true" :required="true"
:rules="validate('itemTag.priority')" :rules="validate('itemTag.priority')"
/> />
<div class="col-1 row justify-center items-center"> <div class="row justify-center items-center" style="flex: 0">
<QIcon <QIcon
@click="itemTagsRef.remove([row])" @click="itemTagsRef.remove([row])"
class="fill-icon-on-hover" class="fill-icon-on-hover"
@ -169,7 +165,7 @@ const insertTag = (rows) => {
</QIcon> </QIcon>
</div> </div>
</VnRow> </VnRow>
<VnRow> <VnRow class="justify-center items-center">
<QIcon <QIcon
@click="insertTag(rows)" @click="insertTag(rows)"
class="cursor-pointer" class="cursor-pointer"
@ -177,6 +173,7 @@ const insertTag = (rows) => {
color="primary" color="primary"
name="add" name="add"
size="sm" size="sm"
style="flex: 0"
> >
<QTooltip> <QTooltip>
{{ t('itemTags.addTag') }} {{ t('itemTags.addTag') }}

View File

@ -25,7 +25,7 @@ itemDiary:
showBefore: Show what's before the inventory showBefore: Show what's before the inventory
since: Since since: Since
warehouse: Warehouse warehouse: Warehouse
basicData: itemBasicData:
type: Type type: Type
reference: Reference reference: Reference
relevancy: Relevancy relevancy: Relevancy

View File

@ -25,7 +25,7 @@ itemDiary:
showBefore: Mostrar lo anterior al inventario showBefore: Mostrar lo anterior al inventario
since: Desde since: Desde
warehouse: Almacén warehouse: Almacén
basicData: itemBasicData:
type: Tipo type: Tipo
reference: Referencia reference: Referencia
relevancy: Relevancia relevancy: Relevancia

View File

@ -28,35 +28,10 @@ async function onSubmit() {
}; };
try { try {
const { data } = await axios.post('Accounts/login', params); const { data } = await axios.post('Accounts/login', params);
if (!data) return; if (!data) return;
const {
data: { multimediaToken },
} = await axios.get('VnUsers/ShareToken', {
headers: { Authorization: data.token },
});
if (!multimediaToken) return; data.keepLogin = keepLogin.value;
await session.setLogin(data);
const login = {
...data,
created: Date.now(),
tokenMultimedia: multimediaToken.id,
keepLogin: keepLogin.value,
};
await session.login(login);
quasar.notify({
message: t('login.loginSuccess'),
type: 'positive',
});
const currentRoute = router.currentRoute.value;
if (currentRoute.query && currentRoute.query.redirect) {
router.push(currentRoute.query.redirect);
} else {
router.push({ name: 'Dashboard' });
}
} catch (res) { } catch (res) {
if (res.response?.data?.error?.code === 'REQUIRES_2FA') { if (res.response?.data?.error?.code === 'REQUIRES_2FA') {
Notify.create({ Notify.create({

View File

@ -25,21 +25,10 @@ async function onSubmit() {
try { try {
params.code = code.value; params.code = code.value;
const { data } = await axios.post('VnUsers/validate-auth', params); const { data } = await axios.post('VnUsers/validate-auth', params);
if (!data) return; if (!data) return;
await session.login(data.token, params.keepLogin); data.keepLogin = params.keepLogin;
quasar.notify({ await session.setLogin(data);
message: t('login.loginSuccess'),
type: 'positive',
});
const currentRoute = router.currentRoute.value;
if (currentRoute.query && currentRoute.query.redirect) {
router.push(currentRoute.query.redirect);
} else {
router.push({ name: 'Dashboard' });
}
} catch (e) { } catch (e) {
quasar.notify({ quasar.notify({
message: e.response?.data?.error.message, message: e.response?.data?.error.message,

View File

@ -70,9 +70,9 @@ const columns = computed(() => [
name: 'image', name: 'image',
columnField: { columnField: {
component: VnImg, component: VnImg,
attrs: (id) => { attrs: ({ row }) => {
return { return {
id, id: row.id,
width: '50px', width: '50px',
}; };
}, },

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