0
0
Fork 0

Merge branch 'dev' into 7017_formModel_validations

This commit is contained in:
Javier Segarra 2024-07-16 09:26:22 +02:00
commit 8798598f41
249 changed files with 13348 additions and 5279 deletions

View File

@ -14,5 +14,5 @@
"[vue]": { "[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "editor.defaultFormatter": "esbenp.prettier-vscode"
}, },
"cSpell.words": ["axios"] "cSpell.words": ["axios", "composables"]
} }

View File

@ -1,3 +1,169 @@
# Version 24.28 - 2024-07-09
### Added 🆕
- Change header titles style by:wbuezas
- chore: refs #7436 fix e2e (origin/7436-showQCheckbox) by:jorgep
- feat: #7196 eslint (origin/7196-cjsToEsm) by:jgallego
- feat: adapt tu VnTable → CrudModel by:alexm
- feat(CustomerFIlter): use correct table by:alexm
- feat(customerList): add searchbar by:alexm
- feat: customerList is customerExtendedList by:alexm
- feat: fixes #7196 by:jgallego
- feat: refs #6739 transferInvoice new checkbox and functionality by:Jon
- feat: refs #6825 create vnTable and add in CustomerExtendedList by:alexm
- feat: refs #6825 create vnTableColumn, cardActions by:alexm
- feat: refs #6825 fix modes by:alexm
- feat: refs #6825 qchip color by:alexm
- feat: refs #6825 right filter panel (6825-vnTable) by:alexm
- feat: refs #6825 scroll for table mode by:alexm
- feat: refs #6825 share filters, create popup by:alexm
- feat: refs #6825 VnComponent mix component and attrs Form to create new row by:alexm
- feat: refs #6825 VnTableFilter and VnPanelFilter init by:alexm
- feat: refs #6826 added rol summary link by:Jon
- feat: refs #6896 created VnImg and added to order module by:Jon
- feat: refs #6896 new filters by:Jon
- feat: refs #7129 fix some code and add order by:pablone
- feat: refs #7436 show checkbox by:jorgep
- feat: refs #7545 Deleted hasIncoterms client column (origin/7545-hasIncoterms) by:guillermo
- feat(TicketService): use correct format by:alexm
- feat(url): sepate filters by:alexm
- feat(VnFilter): merge objects by:alexm
- feat(VnTable): is-editable and use-model. fix: checkbox by:alexm
- feat(VnTable): refs #6825 actions sticky by:alexm
- feat(VnTable): refs #6825 addInWhere by:alexm
- feat(VnTable): refs #6825 dinamic columns by:alexm
- feat(VnTable): refs #6825 execute function when create by:alexm
- feat(VnTable): refs #6825 fix ellipsis and add titles by:alexm
- feat(VnTable): refs #6825 merge where's by:alexm
- feat(VnTable): refs #6825 move to folder. fix checkboxs by:alexm
- feat(VnTable): refs #6825 remove field prop. Add actions in table by:alexm
- feat(VnTable): refs #6825 use checkbox if startsWith 'is' or 'has' by:alexm
- feat(VnTable): refs #6825 VnTableChip component by:alexm
- feat(vnTable): reload data when change url by:alexm
- feat(WorkerFormation): add columnFilter by:alexm
- feat(WorkerFormation): is-editable and use-model by:alexm
- fix: notify icon style by:Javier Segarra
- refactor: refs #6896 fixed styles by:Jon
- Revert "feat: fixes #7196" by:alexm
- style: refs #6464 changed checkbox and qbtn styles by:Jon
### Changed 📦
- perf: Remove div.col by:Javier Segarra
- perf: remove ItemPicture by:Javier Segarra
- perf: replace ItemPicture in favour of VnImg by:Javier Segarra
- refactor by:wbuezas
- refactor: refs #5447 changed warehouse filter by:Jon
- refactor: refs #5447 changed warehouse out filter behavior by:Jon
- refactor: refs #5447 fixed filter if continent not selected by:Jon
- refactor: refs #5447 fix request by:Jon
- refactor: refs #5447 refactor filters by:Jon
- refactor: refs #6739 changed invoice functions' name by:Jon
- refactor: refs #6739 changed router.push by:Jon
- refactor: refs #6739 deleted useless const by:Jon
- refactor: refs #6739 fix redirect transferInvoice by:Jon
- refactor: refs #6739 new confirmation window by:Jon
- refactor: refs #6739 requested changes by:Jon
- refactor: refs #6739 updated transferInvoice function by:Jon
- refactor: refs #6896 changes requested in PR by:Jon
- refactor: refs #6896 end migration orders by:Jon
- refactor: refs #6896 fixed styles by:Jon
- refactor: refs #6896 fix qdrawer by:Jon
- refactor: refs #6896 refactor VnImg by:Jon
- refactor: refs #6896 requested changes by:Jon
- refactor: refs #6977 fix VnImg props (origin/6977-ClonedURL) by:Jon
- refactor: refs #6977 refactor VnImg by:Jon
- refactor: refs #6977 use VnImg by:Jon
- refactors by:alexm
### Fixed 🛠️
- chore: refs #7436 fix e2e (origin/7436-showQCheckbox) by:jorgep
- feat: fixes #7196 by:jgallego
- feat: refs #6825 fix modes by:alexm
- feat: refs #7129 fix some code and add order by:pablone
- feat(VnTable): is-editable and use-model. fix: checkbox by:alexm
- feat(VnTable): refs #6825 fix ellipsis and add titles by:alexm
- feat(VnTable): refs #6825 move to folder. fix checkboxs by:alexm
- fix(ArrayData): refs #6825 router.replace and use filter.where by:alexm
- fix: bug replace by:alexm
- fix: column hidden v-if by:Javier Segarra
- fix: comment 4 by:Javier Segarra
- fix: comments by:Javier Segarra
- fix: cypress.config to mjs by:alexm
- fix(EntryBuys): fix VnSubtoolbar by:alexm
- fixes: fix vnFilter params and redirect by:alexm
- fix: fix warnings by:alexm
- fix: invoiceDueDay test by:alexm
- fix log view not refreshing when changing id param by:wbuezas
- fix: map selected by:Javier Segarra
- fix: merge dev by:Javier Segarra
- fix: notify icon style by:Javier Segarra
- fix: point 1 by:Javier Segarra
- fix: point 3 by:Javier Segarra
- fix: refs #5447 deleted console.log by:Jon
- fix: refs 6464 deleted useless class in checkbox by:Jon
- fix: refs #6464 fix error isLoading by:Jon
- fix: refs #6739 changed checkbox field by:Jon
- fix: refs #6825 css by:carlossa
- fix: refs #6826 fix redirect by:Jon
- fix: refs #6826 fix roleDescriptor by:Jon
- fix: refs #7129 fix e2e by:pablone
- fix: refs #7129 fix module routes by:pablone
- fix: refs #7129 fix some issues on load and tools by:pablone
- fix: refs #7129 remove consoleLog by:pablone
- fix: refs #7129 remove fix from claim lines by:pablone
- fix: refs #7274 fix duplicate rows by:jorgep
- fix: refs #7433 skeleton by:jorgep
- fix: refs #7623 bugs & tests by:jorgep
- fix: refs #7623 disable router update by:jorgep
- fix: refs #7623 redirect by:jorgep
- fix: refs #7623 test by:jorgep
- fix: refs #7623 update add updateRoute prop in VnPaginate by:jorgep
- fix: refs #7623 updating skip param by:jorgep
- fix: revert cypress mjs by:alexm
- fix: SkeletonTable by:alexm
- fix: state translations by:Javier Segarra
- fix: ticket order by:Javier Segarra
- fix(ticket router): typo by:alexm
- fix(TicketService): pay use selected by:alexm
- fix: TravelLog by:Javier Segarra
- fix(url): filter by:alexm
- fix(url): redirect by:alexm
- fix(VnFilter): filter with params by:alexm
- fix(VnFilterPanel): remove key by:alexm
- fix(VnTable): create scss by:alexm
- fix(VnTable): duplicate fetch by:alexm
- fix(VnTable): Qtable v-bind by:alexm
- fix(VnTable): refs #6825 checkbox align and color by:alexm
- fix(VnTable): refs #6825 fix click sticky column by:alexm
- fix(VnTable): refs #6825 fix events and css by:alexm
- fix(VnTable): refs #6825 VnInputDate by:alexm
- fix(VnTable): showLabel by:alexm
- fix(VnTable): warns by:alexm
- fix: WorkerNotificationsManager test by:alexm
- fix: WorkerSelect option format by:Javier Segarra
- refactor: refs #5447 fixed filter if continent not selected by:Jon
- refactor: refs #5447 fix request by:Jon
- refactor: refs #6739 fix redirect transferInvoice by:Jon
- refactor: refs #6896 fixed styles by:Jon
- refactor: refs #6896 fix qdrawer by:Jon
- refactor: refs #6977 fix VnImg props (origin/6977-ClonedURL) by:Jon
- refs #6504 fix formModel claimFilter claimCard (origin/6504-fixCardClaim) by:carlossa
- refs #7406 fix components by:carlossa
- refs #7406 fix pr by:carlossa
- refs #7406 fix props by:carlossa
- refs #7406 fix Tb components create by:carlossa
- refs #7406 fix trad by:carlossa
- refs #7406 fix url by:carlossa
- refs #7406 fix VnTable columns by:carlossa
- refs #7409 fix balance and formation by:carlossa
- refs #7409 fix trad by:carlossa
- Revert "feat: fixes #7196" by:alexm
- test: fix intermitent e2e by:alexm
- test: fix vnSearchbar adapt to vnTable (origin/7648_dev_customerEntries) by:alexm
# Version 24.24 - 2024-06-11 # Version 24.24 - 2024-06-11
### Added 🆕 ### Added 🆕

View File

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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

BIN
public/no-image.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
public/no-user.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

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

View File

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

View File

@ -67,7 +67,7 @@ const $props = defineProps({
default: '', default: '',
description: 'It is used for redirect on click "save and continue"', description: 'It is used for redirect on click "save and continue"',
}, },
hasSubtoolbar: { hasSubToolbar: {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
@ -98,19 +98,17 @@ defineExpose({
}); });
async function fetch(data) { async function fetch(data) {
if (data && Array.isArray(data)) {
let $index = 0;
data.map((d) => (d.$index = $index++));
}
resetData(data); resetData(data);
emit('onFetch', data); emit('onFetch', data);
return data; return data;
} }
function resetData(data) { function resetData(data) {
if (!data) return; if (!data) return;
if (data && Array.isArray(data)) {
let $index = 0;
data.map((d) => (d.$index = $index++));
}
originalData.value = JSON.parse(JSON.stringify(data)); originalData.value = JSON.parse(JSON.stringify(data));
formData.value = JSON.parse(JSON.stringify(data)); formData.value = JSON.parse(JSON.stringify(data));
@ -148,7 +146,7 @@ async function onSubmit() {
await saveChanges($props.saveFn ? formData.value : null); await saveChanges($props.saveFn ? formData.value : null);
} }
async function onSumbitAndGo() { async function onSubmitAndGo() {
await onSubmit(); await onSubmit();
push({ path: $props.goTo }); push({ path: $props.goTo });
} }
@ -299,7 +297,7 @@ watch(formUrl, async () => {
:url="url" :url="url"
:limit="limit" :limit="limit"
@on-fetch="fetch" @on-fetch="fetch"
@on-change="resetData" @on-change="fetch"
:skeleton="false" :skeleton="false"
ref="vnPaginateRef" ref="vnPaginateRef"
v-bind="$attrs" v-bind="$attrs"
@ -313,8 +311,11 @@ watch(formUrl, async () => {
></slot> ></slot>
</template> </template>
</VnPaginate> </VnPaginate>
<SkeletonTable v-if="!formData" :columns="$attrs.columns?.length" /> <SkeletonTable
<Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown() && hasSubtoolbar"> v-if="!formData && $attrs.autoLoad"
:columns="$attrs.columns?.length"
/>
<Teleport to="#st-actions" v-if="stateStore?.isSubToolbarShown() && hasSubToolbar">
<QBtnGroup push style="column-gap: 10px"> <QBtnGroup push style="column-gap: 10px">
<slot name="moreBeforeActions" /> <slot name="moreBeforeActions" />
<QBtn <QBtn
@ -339,7 +340,7 @@ watch(formUrl, async () => {
/> />
<QBtnDropdown <QBtnDropdown
v-if="$props.goTo && $props.defaultSave" v-if="$props.goTo && $props.defaultSave"
@click="onSumbitAndGo" @click="onSubmitAndGo"
:label="tMobile('globals.saveAndContinue')" :label="tMobile('globals.saveAndContinue')"
:title="t('globals.saveAndContinue')" :title="t('globals.saveAndContinue')"
:disable="!hasChanges" :disable="!hasChanges"

View File

@ -209,11 +209,11 @@ async function save() {
updateAndEmit('onDataSaved', formData.value, response?.data); updateAndEmit('onDataSaved', formData.value, response?.data);
if ($props.reload) await arrayData.fetch({}); if ($props.reload) await arrayData.fetch({});
hasChanges.value = false;
} catch (err) { } catch (err) {
console.error(err); console.error(err);
notify('errors.writeRequest', 'negative'); notify('errors.writeRequest', 'negative');
} finally { } finally {
hasChanges.value = false;
isLoading.value = false; isLoading.value = false;
} }
} }
@ -274,74 +274,78 @@ defineExpose({
> >
<QCard> <QCard>
<slot <slot
v-if="formData"
name="form" name="form"
:data="formData" :data="formData"
:validate="validate" :validate="validate"
:filter="filter" :filter="filter"
/> />
<SkeletonForm v-else />
</QCard> </QCard>
</QForm> </QForm>
</div> </div>
<Teleport <Teleport
to="#st-actions" to="#st-actions"
v-if="stateStore?.isSubToolbarShown() && componentIsRendered" v-if="
$props.defaultActions &&
stateStore?.isSubToolbarShown() &&
componentIsRendered
"
> >
<div v-if="$props.defaultActions"> <QBtnGroup push class="q-gutter-x-sm">
<QBtnGroup push class="q-gutter-x-sm"> <slot name="moreActions" />
<slot name="moreActions" /> <QBtn
<QBtn :label="tMobile(defaultButtons.reset.label)"
:label="tMobile(defaultButtons.reset.label)" :color="defaultButtons.reset.color"
:color="defaultButtons.reset.color" :icon="defaultButtons.reset.icon"
:icon="defaultButtons.reset.icon" flat
flat @click="defaultButtons.reset.click"
@click="defaultButtons.reset.click" :disable="!hasChanges"
:disable="!hasChanges" :title="t(defaultButtons.reset.label)"
:title="t(defaultButtons.reset.label)" />
/> <QBtnDropdown
<QBtnDropdown v-if="$props.goTo"
v-if="$props.goTo" @click="saveAndGo"
@click="saveAndGo" :label="tMobile('globals.saveAndContinue')"
:label="tMobile('globals.saveAndContinue')" :title="t('globals.saveAndContinue')"
:title="t('globals.saveAndContinue')" :disable="!hasChanges"
:disable="!hasChanges" color="primary"
color="primary" icon="save"
icon="save" split
split >
> <QList>
<QList> <QItem
<QItem clickable
clickable v-close-popup
v-close-popup @click="save"
@click="save" :title="t('globals.save')"
:title="t('globals.save')" >
> <QItemSection>
<QItemSection> <QItemLabel>
<QItemLabel> <QIcon
<QIcon name="save"
name="save" color="white"
color="white" class="q-mr-sm"
class="q-mr-sm" size="sm"
size="sm" />
/> {{ t('globals.save').toUpperCase() }}
{{ t('globals.save').toUpperCase() }} </QItemLabel>
</QItemLabel> </QItemSection>
</QItemSection> </QItem>
</QItem> </QList>
</QList> </QBtnDropdown>
</QBtnDropdown> <QBtn
<QBtn v-else
v-else :label="tMobile('globals.save')"
:label="tMobile('globals.save')" color="primary"
color="primary" icon="save"
icon="save" @click="defaultButtons.save.click"
@click="defaultButtons.save.click" :disable="!hasChanges"
:disable="!hasChanges" :title="t(defaultButtons.save.label)"
:title="t(defaultButtons.save.label)" />
/> </QBtnGroup>
</QBtnGroup>
</div>
</Teleport> </Teleport>
<SkeletonForm v-if="!formData" />
<QInnerLoading <QInnerLoading
:showing="isLoading" :showing="isLoading"
:label="t('globals.pleaseWait')" :label="t('globals.pleaseWait')"

View File

@ -23,18 +23,15 @@ const formModelRef = ref(null);
const closeButton = ref(null); const closeButton = ref(null);
const onDataSaved = (formData, requestResponse) => { const onDataSaved = (formData, requestResponse) => {
closeForm(); if (closeButton.value) closeButton.value.click();
emit('onDataSaved', formData, requestResponse); emit('onDataSaved', formData, requestResponse);
}; };
const isLoading = computed(() => formModelRef.value?.isLoading); const isLoading = computed(() => formModelRef.value?.isLoading);
const closeForm = async () => {
if (closeButton.value) closeButton.value.click();
};
defineExpose({ defineExpose({
isLoading, isLoading,
onDataSaved,
}); });
</script> </script>

View File

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

View File

@ -21,7 +21,7 @@ const itemComputed = computed(() => {
</script> </script>
<template> <template>
<QItem <QItem
active-class="bg-hover" active-class="bg-vn-hover"
class="min-height" class="min-height"
:to="{ name: itemComputed.name }" :to="{ name: itemComputed.name }"
clickable clickable

View File

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

View File

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

View File

@ -11,12 +11,15 @@ 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';
const state = useState(); const state = useState();
const session = useSession(); const session = useSession();
const router = useRouter(); const router = useRouter();
const { t, locale } = useI18n(); const { t, locale } = useI18n();
const { copyText } = useClipboard(); const { copyText } = useClipboard();
const userLocale = computed({ const userLocale = computed({
get() { get() {
return locale.value; return locale.value;
@ -47,7 +50,6 @@ const darkMode = computed({
}); });
const user = state.getUser(); const user = state.getUser();
const token = session.getTokenMultimedia();
const warehousesData = ref(); const warehousesData = ref();
const companiesData = ref(); const companiesData = ref();
const accountBankData = ref(); const accountBankData = ref();
@ -99,6 +101,7 @@ function saveUserData(param, value) {
axios.post('UserConfigs/setUserConfig', { [param]: value }); axios.post('UserConfigs/setUserConfig', { [param]: value });
localUserData(); localUserData();
} }
const isEmployee = computed(() => useRole().isEmployee());
</script> </script>
<template> <template>
@ -109,12 +112,14 @@ function saveUserData(param, value) {
auto-load auto-load
/> />
<FetchData <FetchData
v-if="isEmployee"
url="Companies" url="Companies"
order="name" order="name"
@on-fetch="(data) => (companiesData = data)" @on-fetch="(data) => (companiesData = data)"
auto-load auto-load
/> />
<FetchData <FetchData
v-if="isEmployee"
url="Accountings" url="Accountings"
order="name" order="name"
@on-fetch="(data) => (accountBankData = data)" @on-fetch="(data) => (accountBankData = data)"
@ -149,10 +154,7 @@ function saveUserData(param, value) {
<div class="col column items-center q-mb-sm"> <div class="col column items-center q-mb-sm">
<QAvatar size="80px"> <QAvatar size="80px">
<QImg <VnImg :id="user.id" collection="user" size="160x160" />
:src="`/api/Images/user/160x160/${user.id}/download?access_token=${token}`"
spinner-color="white"
/>
</QAvatar> </QAvatar>
<div class="text-subtitle1 q-mt-md"> <div class="text-subtitle1 q-mt-md">

View File

@ -5,8 +5,10 @@ import { dashIfEmpty } from 'src/filters';
/* basic input */ /* basic input */
import VnSelect from 'components/common/VnSelect.vue'; import VnSelect from 'components/common/VnSelect.vue';
import VnSelectCache from 'components/common/VnSelectCache.vue';
import VnInput from 'components/common/VnInput.vue'; import VnInput from 'components/common/VnInput.vue';
import VnInputDate from 'components/common/VnInputDate.vue'; import VnInputDate from 'components/common/VnInputDate.vue';
import VnInputTime from 'components/common/VnInputTime.vue';
import VnComponent from 'components/common/VnComponent.vue'; import VnComponent from 'components/common/VnComponent.vue';
const model = defineModel(undefined, { required: true }); const model = defineModel(undefined, { required: true });
@ -41,11 +43,23 @@ const $props = defineProps({
}, },
}); });
const defaultSelect = {
attrs: {
row: $props.row,
disable: !$props.isEditable,
class: 'fit',
},
forceAttrs: {
label: $props.showLabel && $props.column.label,
},
};
const defaultComponents = { const defaultComponents = {
input: { input: {
component: markRaw(VnInput), component: markRaw(VnInput),
attrs: { attrs: {
disable: !$props.isEditable, disable: !$props.isEditable,
class: 'fit',
}, },
forceAttrs: { forceAttrs: {
label: $props.showLabel && $props.column.label, label: $props.showLabel && $props.column.label,
@ -55,6 +69,7 @@ const defaultComponents = {
component: markRaw(VnInput), component: markRaw(VnInput),
attrs: { attrs: {
disable: !$props.isEditable, disable: !$props.isEditable,
class: 'fit',
}, },
forceAttrs: { forceAttrs: {
label: $props.showLabel && $props.column.label, label: $props.showLabel && $props.column.label,
@ -66,6 +81,16 @@ const defaultComponents = {
readonly: true, readonly: true,
disable: !$props.isEditable, disable: !$props.isEditable,
style: 'min-width: 125px', style: 'min-width: 125px',
class: 'fit',
},
forceAttrs: {
label: $props.showLabel && $props.column.label,
},
},
time: {
component: markRaw(VnInputTime),
attrs: {
disable: !$props.isEditable,
}, },
forceAttrs: { forceAttrs: {
label: $props.showLabel && $props.column.label, label: $props.showLabel && $props.column.label,
@ -77,7 +102,7 @@ const defaultComponents = {
const defaultAttrs = { const defaultAttrs = {
disable: !$props.isEditable, disable: !$props.isEditable,
'model-value': Boolean(prop), 'model-value': Boolean(prop),
class: 'no-padding', class: 'no-padding fit',
}; };
if (typeof prop == 'number') { if (typeof prop == 'number') {
@ -91,13 +116,12 @@ const defaultComponents = {
}, },
}, },
select: { select: {
component: markRaw(VnSelectCache),
...defaultSelect,
},
rawSelect: {
component: markRaw(VnSelect), component: markRaw(VnSelect),
attrs: { ...defaultSelect,
disable: !$props.isEditable,
},
forceAttrs: {
label: $props.showLabel && $props.column.label,
},
}, },
icon: { icon: {
component: markRaw(QIcon), component: markRaw(QIcon),
@ -134,7 +158,7 @@ const col = computed(() => {
const components = computed(() => $props.components ?? defaultComponents); const components = computed(() => $props.components ?? defaultComponents);
</script> </script>
<template> <template>
<div class="row no-wrap fit"> <div class="row no-wrap">
<VnComponent <VnComponent
v-if="col.before" v-if="col.before"
:prop="col.before" :prop="col.before"

View File

@ -7,6 +7,7 @@ import { useArrayData } from 'composables/useArrayData';
import VnSelect from 'components/common/VnSelect.vue'; import VnSelect from 'components/common/VnSelect.vue';
import VnInput from 'components/common/VnInput.vue'; import VnInput from 'components/common/VnInput.vue';
import VnInputDate from 'components/common/VnInputDate.vue'; import VnInputDate from 'components/common/VnInputDate.vue';
import VnInputTime from 'components/common/VnInputTime.vue';
import VnTableColumn from 'components/VnTable/VnColumn.vue'; import VnTableColumn from 'components/VnTable/VnColumn.vue';
const $props = defineProps({ const $props = defineProps({
@ -39,7 +40,7 @@ const enterEvent = {
const defaultAttrs = { const defaultAttrs = {
filled: !$props.showTitle, filled: !$props.showTitle,
class: 'q-px-sm q-pb-xs q-pt-none', class: 'q-px-xs q-pb-xs q-pt-none fit',
dense: true, dense: true,
}; };
@ -75,12 +76,23 @@ const components = {
}, },
forceAttrs, forceAttrs,
}, },
time: {
component: markRaw(VnInputTime),
event: updateEvent,
attrs: {
...defaultAttrs,
disable: !$props.isEditable,
},
forceAttrs: {
label: $props.showLabel && $props.column.label,
},
},
checkbox: { checkbox: {
component: markRaw(QCheckbox), component: markRaw(QCheckbox),
event: updateEvent, event: updateEvent,
attrs: { attrs: {
dense: true, dense: true,
class: $props.showTitle ? 'q-py-sm q-mt-md' : 'q-px-md q-py-xs', class: $props.showTitle ? 'q-py-sm q-mt-md' : 'q-px-md q-py-xs fit',
'toggle-indeterminate': true, 'toggle-indeterminate': true,
}, },
forceAttrs, forceAttrs,
@ -89,7 +101,7 @@ const components = {
component: markRaw(VnSelect), component: markRaw(VnSelect),
event: updateEvent, event: updateEvent,
attrs: { attrs: {
class: 'q-px-md q-pb-xs q-pt-none', class: 'q-px-sm q-pb-xs q-pt-none fit',
dense: true, dense: true,
filled: !$props.showTitle, filled: !$props.showTitle,
}, },
@ -127,14 +139,11 @@ const showFilter = computed(
</script> </script>
<template> <template>
<div <div
v-if="showTitle" v-if="showFilter"
class="q-pt-sm q-px-sm ellipsis" class="full-width"
:class="`text-${column?.align ?? 'left'}`" :class="alignRow()"
:style="!showFilter ? { 'min-height': 72 + 'px' } : ''" style="max-height: 45px; overflow: hidden"
> >
{{ column?.label }}
</div>
<div v-if="showFilter" class="full-width" :class="alignRow()">
<VnTableColumn <VnTableColumn
:column="$props.column" :column="$props.column"
default="input" default="input"

View File

@ -0,0 +1,95 @@
<script setup>
import { ref } from 'vue';
import { useArrayData } from 'composables/useArrayData';
const model = defineModel({ type: Object, required: true });
const $props = defineProps({
name: {
type: String,
default: '',
},
label: {
type: String,
default: undefined,
},
dataKey: {
type: String,
required: true,
},
searchUrl: {
type: String,
default: 'params',
},
vertical: {
type: Boolean,
default: false,
},
});
const hover = ref();
const arrayData = useArrayData($props.dataKey, { searchUrl: $props.searchUrl });
async function orderBy(name, direction) {
if (!name) return;
switch (direction) {
case 'DESC':
direction = undefined;
break;
case undefined:
direction = 'ASC';
break;
case 'ASC':
direction = 'DESC';
break;
}
if (!direction) return await arrayData.deleteOrder(name);
await arrayData.addOrder(name, direction);
}
defineExpose({ orderBy });
</script>
<template>
<div
@mouseenter="hover = true"
@mouseleave="hover = false"
@click="orderBy(name, model?.direction)"
class="row items-center no-wrap cursor-pointer"
>
<span :title="label">{{ label }}</span>
<QChip
v-if="name"
:label="!vertical && model?.index"
:icon="
(model?.index || hover) && !vertical
? model?.direction == 'DESC'
? 'arrow_downward'
: 'arrow_upward'
: undefined
"
:size="vertical ? '' : 'sm'"
:class="[
model?.index ? 'color-vn-text' : 'bg-transparent',
vertical ? 'q-px-none' : '',
]"
class="no-box-shadow"
:clickable="true"
style="min-width: 40px"
>
<div
class="column flex-center"
v-if="vertical"
:style="!model?.index && 'color: #5d5d5d'"
>
{{ model?.index }}
<QIcon
:name="
model?.index
? model?.direction == 'DESC'
? 'arrow_downward'
: 'arrow_upward'
: 'swap_vert'
"
size="xs"
/>
</div>
</QChip>
</div>
</template>

View File

@ -3,16 +3,18 @@ import { ref, onMounted, computed, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import FormModelPopup from 'components/FormModelPopup.vue';
import CrudModel from 'src/components/CrudModel.vue';
import VnFilterPanel from 'components/ui/VnFilterPanel.vue';
import VnLv from 'components/ui/VnLv.vue';
import CrudModel from 'src/components/CrudModel.vue';
import FormModelPopup from 'components/FormModelPopup.vue';
import VnFilterPanel from 'components/ui/VnFilterPanel.vue';
import VnTableColumn from 'components/VnTable/VnColumn.vue'; import VnTableColumn from 'components/VnTable/VnColumn.vue';
import VnTableFilter from 'components/VnTable/VnFilter.vue'; import VnTableFilter from 'components/VnTable/VnFilter.vue';
import VnTableChip from 'components/VnTable/VnChip.vue'; import VnTableChip from 'components/VnTable/VnChip.vue';
import VnVisibleColumn from 'src/components/VnTable/VnVisibleColumn.vue';
import VnLv from 'components/ui/VnLv.vue';
import VnTableOrder from 'src/components/VnTable/VnOrder.vue';
const $props = defineProps({ const $props = defineProps({
columns: { columns: {
@ -21,7 +23,7 @@ const $props = defineProps({
}, },
defaultMode: { defaultMode: {
type: String, type: String,
default: 'card', // 'table', 'card' default: 'table', // 'table', 'card'
}, },
columnSearch: { columnSearch: {
type: Boolean, type: Boolean,
@ -32,7 +34,7 @@ const $props = defineProps({
default: true, default: true,
}, },
rowClick: { rowClick: {
type: Function, type: [Function, Boolean],
default: null, default: null,
}, },
redirect: { redirect: {
@ -59,6 +61,30 @@ const $props = defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
hasSubToolbar: {
type: Boolean,
default: true,
},
disableOption: {
type: Object,
default: () => ({ card: false, table: false }),
},
withoutHeader: {
type: Boolean,
default: false,
},
tableCode: {
type: String,
default: null,
},
table: {
type: Object,
default: () => ({}),
},
tableHeight: {
type: String,
default: '90vh',
},
}); });
const { t } = useI18n(); const { t } = useI18n();
const stateStore = useStateStore(); const stateStore = useStateStore();
@ -72,19 +98,23 @@ const mode = ref(DEFAULT_MODE);
const selected = ref([]); const selected = ref([]);
const routeQuery = JSON.parse(route?.query[$props.searchUrl] ?? '{}'); const routeQuery = JSON.parse(route?.query[$props.searchUrl] ?? '{}');
const params = ref({ ...routeQuery, ...routeQuery.filter?.where }); const params = ref({ ...routeQuery, ...routeQuery.filter?.where });
const orders = ref(parseOrder(routeQuery.filter?.order));
const CrudModelRef = ref({}); const CrudModelRef = ref({});
const showForm = ref(false); const showForm = ref(false);
const splittedColumns = ref({ columns: [] }); const splittedColumns = ref({ columns: [] });
const columnsVisibilitySkiped = ref();
const tableModes = [ const tableModes = [
{ {
icon: 'view_column', icon: 'view_column',
title: t('table view'), title: t('table view'),
value: TABLE_MODE, value: TABLE_MODE,
disable: $props.disableOption?.table,
}, },
{ {
icon: 'grid_view', icon: 'grid_view',
title: t('grid view'), title: t('grid view'),
value: DEFAULT_MODE, value: DEFAULT_MODE,
disable: $props.disableOption?.card,
}, },
]; ];
@ -92,6 +122,12 @@ onMounted(() => {
mode.value = quasar.platform.is.mobile ? DEFAULT_MODE : $props.defaultMode; mode.value = quasar.platform.is.mobile ? DEFAULT_MODE : $props.defaultMode;
stateStore.rightDrawer = true; stateStore.rightDrawer = true;
setUserParams(route.query[$props.searchUrl]); setUserParams(route.query[$props.searchUrl]);
columnsVisibilitySkiped.value = [
...splittedColumns.value.columns
.filter((c) => c.visible == false)
.map((c) => c.name),
...['tableActions'],
];
}); });
watch( watch(
@ -105,14 +141,21 @@ watch(
(val) => setUserParams(val) (val) => setUserParams(val)
); );
const isTableMode = computed(() => mode.value == TABLE_MODE);
function setUserParams(watchedParams) { function setUserParams(watchedParams) {
if (!watchedParams) return; if (!watchedParams) return;
if (typeof watchedParams == 'string') watchedParams = JSON.parse(watchedParams); if (typeof watchedParams == 'string') watchedParams = JSON.parse(watchedParams);
const where = JSON.parse(watchedParams?.filter)?.where; const filter = JSON.parse(watchedParams?.filter);
const where = filter?.where;
const order = filter?.order;
watchedParams = { ...watchedParams, ...where }; watchedParams = { ...watchedParams, ...where };
delete watchedParams.filter; delete watchedParams.filter;
delete params.value?.filter;
params.value = { ...params.value, ...watchedParams }; params.value = { ...params.value, ...watchedParams };
orders.value = parseOrder(order);
} }
function splitColumns(columns) { function splitColumns(columns) {
@ -120,7 +163,7 @@ function splitColumns(columns) {
columns: [], columns: [],
chips: [], chips: [],
create: [], create: [],
visible: [], cardVisible: [],
}; };
for (const col of columns) { for (const col of columns) {
@ -128,7 +171,7 @@ function splitColumns(columns) {
if (col.chip) splittedColumns.value.chips.push(col); if (col.chip) splittedColumns.value.chips.push(col);
if (col.isTitle) splittedColumns.value.title = col; if (col.isTitle) splittedColumns.value.title = col;
if (col.create) splittedColumns.value.create.push(col); if (col.create) splittedColumns.value.create.push(col);
if (col.cardVisible) splittedColumns.value.visible.push(col); if (col.cardVisible) splittedColumns.value.cardVisible.push(col);
if ($props.isEditable && col.disable == null) col.disable = false; if ($props.isEditable && col.disable == null) col.disable = false;
if ($props.useModel) col.columnFilter = { ...col.columnFilter, inWhere: true }; if ($props.useModel) col.columnFilter = { ...col.columnFilter, inWhere: true };
splittedColumns.value.columns.push(col); splittedColumns.value.columns.push(col);
@ -144,12 +187,13 @@ function splitColumns(columns) {
label: t('status'), label: t('status'),
name: 'tableStatus', name: 'tableStatus',
columnFilter: false, columnFilter: false,
orderBy: false,
}); });
} }
} }
const rowClickFunction = computed(() => { const rowClickFunction = computed(() => {
if ($props.rowClick) return $props.rowClick; if ($props.rowClick != undefined) return $props.rowClick;
if ($props.redirect) return ({ id }) => redirectFn(id); if ($props.redirect) return ({ id }) => redirectFn(id);
return () => {}; return () => {};
}); });
@ -175,11 +219,25 @@ function columnName(col) {
} }
function getColAlign(col) { function getColAlign(col) {
return 'text-' + (col.align ?? 'left') return 'text-' + (col.align ?? 'left');
} }
function parseOrder(urlOrders) {
const orderObject = {};
if (!urlOrders) return orderObject;
if (typeof urlOrders == 'string') urlOrders = [urlOrders];
for (const [index, orders] of urlOrders.entries()) {
const [name, direction] = orders.split(' ');
orderObject[name] = { direction, index: index + 1 };
}
return orderObject;
}
const emit = defineEmits(['onFetch', 'update:selected', 'saveChanges']);
defineExpose({ defineExpose({
reload, reload,
redirect: redirectFn, redirect: redirectFn,
selected,
}); });
</script> </script>
<template> <template>
@ -195,24 +253,35 @@ defineExpose({
:data-key="$attrs['data-key']" :data-key="$attrs['data-key']"
:search-button="true" :search-button="true"
v-model="params" v-model="params"
:disable-submit-event="true"
:search-url="searchUrl" :search-url="searchUrl"
:redirect="!!redirect"
> >
<template #body> <template #body>
<VnTableFilter <div
:column="col" class="row no-wrap flex-center"
:data-key="$attrs['data-key']"
v-for="col of splittedColumns.columns" v-for="col of splittedColumns.columns"
:key="col.id" :key="col.id"
v-model="params[columnName(col)]" >
:search-url="searchUrl" <VnTableFilter
:column="col"
:data-key="$attrs['data-key']"
v-model="params[columnName(col)]"
:search-url="searchUrl"
/>
<VnTableOrder
v-model="orders[col.name]"
:name="col.orderBy ?? col.name"
:data-key="$attrs['data-key']"
:search-url="searchUrl"
:vertical="true"
/>
</div>
<slot
name="moreFilterPanel"
:params="params"
:columns="splittedColumns.columns"
/> />
</template> </template>
<slot
name="moreFilterPanel"
:params="params"
:columns="splittedColumns.columns"
/>
</VnFilterPanel> </VnFilterPanel>
</QScrollArea> </QScrollArea>
</QDrawer> </QDrawer>
@ -223,41 +292,48 @@ defineExpose({
:limit="20" :limit="20"
ref="CrudModelRef" ref="CrudModelRef"
:search-url="searchUrl" :search-url="searchUrl"
:disable-infinite-scroll="mode == TABLE_MODE" :disable-infinite-scroll="isTableMode"
@save-changes="reload" @save-changes="reload"
:has-subtoolbar="isEditable" :has-sub-toolbar="$attrs['hasSubToolbar'] ?? isEditable"
> >
<template
v-for="(_, slotName) in $slots"
#[slotName]="slotData"
:key="slotName"
>
<slot :name="slotName" v-bind="slotData ?? {}" :key="slotName" />
</template>
<template #body="{ rows }"> <template #body="{ rows }">
<QTable <QTable
v-bind="$attrs['QTable']" v-bind="table"
class="vnTable" class="vnTable"
:columns="splittedColumns.columns" :columns="splittedColumns.columns"
:rows="rows" :rows="rows"
v-model:selected="selected" v-model:selected="selected"
:grid="mode != TABLE_MODE" :grid="!isTableMode"
table-header-class="bg-header" table-header-class="bg-header"
card-container-class="grid-three" card-container-class="grid-three"
flat flat
:style="mode == TABLE_MODE && 'max-height: 90vh'" :style="isTableMode && `max-height: ${tableHeight}`"
virtual-scroll virtual-scroll
@virtual-scroll=" @virtual-scroll="
(event) => (event) =>
event.index > rows.length - 2 && event.index > rows.length - 2 &&
CrudModelRef.vnPaginateRef.paginate() CrudModelRef.vnPaginateRef.paginate()
" "
@row-click="(_, row) => rowClickFunction(row)" @row-click="(_, row) => rowClickFunction && rowClickFunction(row)"
@update:selected="emit('update:selected', $event)"
> >
<template #top-left> <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>
<!-- <QBtn <VnVisibleColumn
icon="visibility" v-if="isTableMode"
title="asd" v-model="splittedColumns.columns"
class="bg-vn-section-color q-mr-md" :table-code="tableCode ?? route.name"
dense :skip="columnsVisibilitySkiped"
v-if="mode == 'table'" />
/> -->
<QBtnToggle <QBtnToggle
v-model="mode" v-model="mode"
toggle-color="primary" toggle-color="primary"
@ -266,6 +342,7 @@ defineExpose({
:options="tableModes" :options="tableModes"
/> />
<QBtn <QBtn
v-if="$props.rightSearch"
icon="filter_alt" icon="filter_alt"
title="asd" title="asd"
class="bg-vn-section-color q-ml-md" class="bg-vn-section-color q-ml-md"
@ -274,18 +351,33 @@ defineExpose({
/> />
</template> </template>
<template #header-cell="{ col }"> <template #header-cell="{ col }">
<QTh <QTh v-if="col.visible ?? true" auto-width>
auto-width <div
style="min-width: 100px" class="column self-start q-ml-xs ellipsis"
v-if="$props.columnSearch" :class="`text-${col?.align ?? 'left'}`"
> style="height: 75px"
<VnTableFilter >
:column="col" <div
:show-title="true" class="row items-center no-wrap"
:data-key="$attrs['data-key']" style="height: 30px"
v-model="params[columnName(col)]" >
:search-url="searchUrl" <VnTableOrder
/> v-model="orders[col.name]"
:name="col.orderBy ?? col.name"
:label="col?.label"
:data-key="$attrs['data-key']"
:search-url="searchUrl"
/>
</div>
<VnTableFilter
v-if="$props.columnSearch"
:column="col"
:show-title="true"
:data-key="$attrs['data-key']"
v-model="params[columnName(col)]"
:search-url="searchUrl"
/>
</div>
</QTh> </QTh>
</template> </template>
<template #header-cell-tableActions> <template #header-cell-tableActions>
@ -308,15 +400,18 @@ defineExpose({
<QTd <QTd
auto-width auto-width
class="no-margin q-px-xs" class="no-margin q-px-xs"
:class="getColAlign(col)" :class="[getColAlign(col), col.class, col.columnField?.class]"
v-if="col.visible ?? true"
> >
<VnTableColumn <slot :name="`column-${col.name}`" :col="col" :row="row">
:column="col" <VnTableColumn
:row="row" :column="col"
:is-editable="false" :row="row"
v-model="row[col.name]" :is-editable="false"
component-prop="columnField" v-model="row[col.name]"
/> component-prop="columnField"
/>
</slot>
</QTd> </QTd>
</template> </template>
<template #body-cell-tableActions="{ col, row }"> <template #body-cell-tableActions="{ col, row }">
@ -395,15 +490,15 @@ defineExpose({
:class="$props.cardClass" :class="$props.cardClass"
> >
<div <div
v-for="col of splittedColumns.visible" v-for="col of splittedColumns.cardVisible"
:key="col.name" :key="col.name"
class="fields" class="fields"
> >
<VnLv <VnLv
:label=" :label="
!col.component && !col.component && col.label
col.label && ? `${col.label}:`
`${col.label}:` : ''
" "
> >
<template #value> <template #value>
@ -412,14 +507,20 @@ defineExpose({
stopEventPropagation($event) stopEventPropagation($event)
" "
> >
<VnTableColumn <slot
:column="col" :name="`column-${col.name}`"
:col="col"
:row="row" :row="row"
:is-editable="false" >
v-model="row[col.name]" <VnTableColumn
component-prop="columnField" :column="col"
:show-label="true" :row="row"
/> :is-editable="false"
v-model="row[col.name]"
component-prop="columnField"
:show-label="true"
/>
</slot>
</span> </span>
</template> </template>
</VnLv> </VnLv>
@ -477,6 +578,7 @@ defineExpose({
default="input" default="input"
v-model="data[column.name]" v-model="data[column.name]"
:show-label="true" :show-label="true"
component-prop="columnCreate"
/> />
<slot name="more-create-dialog" :data="data" /> <slot name="more-create-dialog" :data="data" />
</div> </div>
@ -487,8 +589,12 @@ defineExpose({
<i18n> <i18n>
en: en:
status: Status status: Status
table view: Table view
grid view: Grid view
es: es:
status: Estados status: Estados
table view: Vista en tabla
grid view: Vista en cuadrícula
</i18n> </i18n>
<style lang="scss"> <style lang="scss">
@ -498,7 +604,11 @@ es:
} }
.bg-header { .bg-header {
background-color: #5d5d5d; background-color: var(--vn-header-color);
color: var(--vn-text-color);
}
.color-vn-text {
color: var(--vn-text-color); color: var(--vn-text-color);
} }
@ -507,7 +617,7 @@ es:
.q-table--dark tr, .q-table--dark tr,
.q-table--dark th, .q-table--dark th,
.q-table--dark td { .q-table--dark td {
border-color: #222222; border-color: var(--vn-section-color);
} }
.q-table__container > div:first-child { .q-table__container > div:first-child {
@ -577,7 +687,7 @@ es:
right: 0; right: 0;
} }
td.sticky { td.sticky {
background-color: var(--q-dark); background-color: var(--vn-section-color);
z-index: 1; z-index: 1;
} }
} }

View File

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

View File

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

View File

@ -18,7 +18,7 @@ watchEffect(() => {
(matched) => Object.keys(matched.meta).length (matched) => Object.keys(matched.meta).length
); );
breadcrumbs.value.length = 0; breadcrumbs.value.length = 0;
if (!matched.value[0]) return;
if (matched.value[0].name != 'Dashboard') { if (matched.value[0].name != 'Dashboard') {
root.value = useCamelCase(matched.value[0].path.substring(1).toLowerCase()); root.value = useCamelCase(matched.value[0].path.substring(1).toLowerCase());

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import { onBeforeMount, computed } from 'vue'; import { onBeforeMount, computed } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute, onBeforeRouteUpdate } from 'vue-router';
import { useArrayData } from 'src/composables/useArrayData'; import { useArrayData } from 'src/composables/useArrayData';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import useCardSize from 'src/composables/useCardSize'; import useCardSize from 'src/composables/useCardSize';
@ -39,8 +39,17 @@ const arrayData = useArrayData(props.dataKey, {
onBeforeMount(async () => { onBeforeMount(async () => {
if (!props.baseUrl) arrayData.store.filter.where = { id: route.params.id }; if (!props.baseUrl) arrayData.store.filter.where = { id: route.params.id };
await arrayData.fetch({ append: false }); await arrayData.fetch({ append: false, updateRouter: false });
}); });
if (props.baseUrl) {
onBeforeRouteUpdate(async (to, from) => {
if (to.params.id !== from.params.id) {
arrayData.store.url = `${props.baseUrl}/${to.params.id}`;
await arrayData.fetch({ append: false, updateRouter: false });
}
});
}
</script> </script>
<template> <template>
<QDrawer <QDrawer

View File

@ -12,7 +12,7 @@ const $props = defineProps({
default: () => {}, default: () => {},
}, },
value: { value: {
type: [Object, Number, String], type: [Object, Number, String, Boolean],
default: () => {}, default: () => {},
}, },
}); });
@ -54,7 +54,6 @@ function toValueAttrs(attrs) {
v-bind="mix(toComponent).attrs" v-bind="mix(toComponent).attrs"
v-on="mix(toComponent).event ?? {}" v-on="mix(toComponent).event ?? {}"
v-model="model" v-model="model"
class="fit"
/> />
</span> </span>
</template> </template>

View File

@ -273,6 +273,10 @@ function shouldRenderButton(button, isExternal = false) {
if (button.name == 'download') return true; if (button.name == 'download') return true;
return button.external === isExternal; return button.external === isExternal;
} }
defineExpose({
dmsRef,
});
</script> </script>
<template> <template>
<VnPaginate <VnPaginate
@ -374,7 +378,11 @@ function shouldRenderButton(button, isExternal = false) {
/> />
</QDialog> </QDialog>
<QPageSticky position="bottom-right" :offset="[25, 25]"> <QPageSticky position="bottom-right" :offset="[25, 25]">
<QBtn fab color="primary" icon="add" @click="showFormDialog()" /> <QBtn fab color="primary" icon="add" @click="showFormDialog()" class="fill-icon">
<QTooltip>
{{ t('Upload file') }}
</QTooltip>
</QBtn>
</QPageSticky> </QPageSticky>
</template> </template>
<style scoped> <style scoped>
@ -392,4 +400,5 @@ en:
es: es:
contentTypesInfo: Tipos de archivo permitidos {allowedContentTypes} contentTypesInfo: Tipos de archivo permitidos {allowedContentTypes}
Generate identifier for original file: Generar identificador para archivo original Generate identifier for original file: Generar identificador para archivo original
Upload file: Subir fichero
</i18n> </i18n>

View File

@ -22,6 +22,10 @@ const $props = defineProps({
type: String, type: String,
default: '', default: '',
}, },
clearable: {
type: Boolean,
default: true,
},
}); });
const { t } = useI18n(); const { t } = useI18n();
@ -86,8 +90,13 @@ const mixinRules = [
<QIcon <QIcon
name="close" name="close"
size="xs" size="xs"
v-if="hover && value && !$attrs.disabled" v-if="hover && value && !$attrs.disabled && $props.clearable"
@click="value = null" @click="
() => {
value = null;
emit('remove');
}
"
></QIcon> ></QIcon>
<QIcon v-if="info" name="info"> <QIcon v-if="info" name="info">
<QTooltip max-width="350px"> <QTooltip max-width="350px">

View File

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

View File

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

View File

@ -49,6 +49,7 @@ const filter = {
'changedModelId', 'changedModelId',
'changedModelValue', 'changedModelValue',
'description', 'description',
'summaryId',
], ],
include: [ include: [
{ {
@ -459,12 +460,12 @@ onUnmounted(() => {
:style="{ :style="{
backgroundColor: useColor(modelLog.model), backgroundColor: useColor(modelLog.model),
}" }"
:title="modelLog.model" :title="`${modelLog.model} #${modelLog.id}`"
> >
{{ t(modelLog.modelI18n) }} {{ t(modelLog.modelI18n) }}
</QChip> </QChip>
<span class="model-id" v-if="modelLog.id" <span class="model-id" v-if="modelLog.summaryId"
>#{{ modelLog.id }}</span >#{{ modelLog.summaryId }}</span
> >
<span class="model-value" :title="modelLog.showValue"> <span class="model-value" :title="modelLog.showValue">
{{ modelLog.showValue }} {{ modelLog.showValue }}

View File

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

View File

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

View File

@ -1,8 +1,9 @@
<script setup> <script setup>
import { ref, toRefs, computed, watch } from 'vue'; import { ref, toRefs, computed, watch, onMounted } from 'vue';
import { onMounted } from 'vue';
import FetchData from 'src/components/FetchData.vue'; import FetchData from 'src/components/FetchData.vue';
import { useValidator } from 'src/composables/useValidator'; import { useValidator } from 'src/composables/useValidator';
import { useAttrs } from 'vue';
const { validations } = useValidator(); const { validations } = useValidator();
const emit = defineEmits(['update:modelValue', 'update:options']); const emit = defineEmits(['update:modelValue', 'update:options']);
const $props = defineProps({ const $props = defineProps({
@ -58,15 +59,26 @@ const $props = defineProps({
type: [Number, String], type: [Number, String],
default: '30', default: '30',
}, },
focusOnMount: {
type: Boolean,
default: false,
},
useLike: {
type: Boolean,
default: true,
},
}); });
const requiredFieldRule = (val) => validations({}).presence(val); const requiredFieldRule = (val) => validations({}).presence(val);
const $attrs = useAttrs();
const mixinRules = () =>
$attrs.required ? [$attrs?.rules ?? [], requiredFieldRule] : null;
const { optionLabel, optionValue, optionFilter, options, modelValue } = toRefs($props); const { optionLabel, optionValue, optionFilter, options, modelValue } = toRefs($props);
const myOptions = ref([]); const myOptions = ref([]);
const myOptionsOriginal = ref([]); const myOptionsOriginal = ref([]);
const vnSelectRef = ref(); const vnSelectRef = ref();
const dataRef = ref(); const dataRef = ref();
const lastVal = ref();
const value = computed({ const value = computed({
get() { get() {
@ -77,6 +89,27 @@ const value = computed({
}, },
}); });
watch(options, (newValue) => {
setOptions(newValue);
});
watch(modelValue, (newValue) => {
if (!myOptions.value.some((option) => option[optionValue.value] == newValue))
fetchFilter(newValue);
});
onMounted(() => {
setOptions(options.value);
if ($props.url && $props.modelValue && !findKeyInOptions())
fetchFilter($props.modelValue);
if ($props.focusOnMount) setTimeout(() => vnSelectRef.value.showPopup(), 300);
});
function findKeyInOptions() {
if (!$props.options) return;
return filter($props.modelValue, $props.options)?.length;
}
function setOptions(data) { function setOptions(data) {
myOptions.value = JSON.parse(JSON.stringify(data)); myOptions.value = JSON.parse(JSON.stringify(data));
myOptionsOriginal.value = JSON.parse(JSON.stringify(data)); myOptionsOriginal.value = JSON.parse(JSON.stringify(data));
@ -112,19 +145,28 @@ async function fetchFilter(val) {
if (!$props.url || !dataRef.value) return; if (!$props.url || !dataRef.value) return;
const { fields, sortBy, limit } = $props; const { fields, sortBy, limit } = $props;
let key = optionLabel.value; let key = optionFilter.value ?? optionLabel.value;
if (new RegExp(/\d/g).test(val)) key = optionFilter.value ?? optionValue.value; if (new RegExp(/\d/g).test(val)) key = optionValue.value;
const where = { ...{ [key]: { like: `%${val}%` } }, ...$props.where }; const defaultWhere = $props.useLike
? { [key]: { like: `%${val}%` } }
: { [key]: val };
const where = { ...(val ? defaultWhere : {}), ...$props.where };
const fetchOptions = { where, order: sortBy, limit }; const fetchOptions = { where, order: sortBy, limit };
if (fields) fetchOptions.fields = fields; if (fields) fetchOptions.fields = fields;
return dataRef.value.fetch(fetchOptions); return dataRef.value.fetch(fetchOptions);
} }
async function filterHandler(val, update) { async function filterHandler(val, update) {
if (!$props.defaultFilter) return update(); if (!val && lastVal.value === val) {
lastVal.value = val;
return update();
}
lastVal.value = val;
let newOptions; let newOptions;
if (!$props.defaultFilter) return update();
if ($props.url) { if ($props.url) {
newOptions = await fetchFilter(val); newOptions = await fetchFilter(val);
} else newOptions = filter(val, myOptionsOriginal.value); } else newOptions = filter(val, myOptionsOriginal.value);
@ -148,11 +190,6 @@ watch(modelValue, (newValue) => {
if (!myOptions.value.some((option) => option[optionValue.value] == newValue)) if (!myOptions.value.some((option) => option[optionValue.value] == newValue))
fetchFilter(newValue); fetchFilter(newValue);
}); });
import { useAttrs } from 'vue';
const $attrs = useAttrs();
const mixinRules = () =>
$attrs.required ? [$attrs?.rules ?? [], requiredFieldRule] : null;
</script> </script>
<template> <template>

View File

@ -0,0 +1,25 @@
<script setup>
import { ref, onBeforeMount, useAttrs } from 'vue';
import VnSelect from 'src/components/common/VnSelect.vue';
const $props = defineProps({
row: {
type: [Object],
default: null,
},
find: {
type: String,
default: null,
},
});
const options = ref([]);
onBeforeMount(async () => {
const { url } = useAttrs();
const findBy = $props.find ?? url?.charAt(0)?.toLocaleLowerCase() + url?.slice(1, -1);
if (findBy) options.value = [$props.row[findBy]];
});
</script>
<template>
<VnSelect v-bind="$attrs" :options="$attrs.options ?? options" />
</template>

View File

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

View File

@ -39,13 +39,14 @@ const $props = defineProps({
}); });
const state = useState(); const state = useState();
const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const { viewSummary } = useSummaryDialog(); const { viewSummary } = useSummaryDialog();
let arrayData; let arrayData;
let store; let store;
let entity; let entity;
const isLoading = ref(false); const isLoading = ref(false);
const isSameDataKey = computed(() => $props.dataKey === route.meta.moduleName);
defineExpose({ getData }); defineExpose({ getData });
onBeforeMount(async () => { onBeforeMount(async () => {
@ -55,12 +56,19 @@ onBeforeMount(async () => {
skip: 0, skip: 0,
}); });
store = arrayData.store; store = arrayData.store;
entity = computed(() => (Array.isArray(store.data) ? store.data[0] : store.data)); entity = computed(() => {
const data = (Array.isArray(store.data) ? store.data[0] : store.data) ?? {};
if (data) emit('onFetch', data);
return data;
});
// It enables to load data only once if the module is the same as the dataKey // It enables to load data only once if the module is the same as the dataKey
if ($props.dataKey !== useRoute().meta.moduleName) await getData(); if (!isSameDataKey.value || !route.params.id) await getData();
watch( watch(
() => [$props.url, $props.filter], () => [$props.url, $props.filter],
async () => await getData() async () => {
if (!isSameDataKey.value) await getData();
}
); );
}); });
@ -76,14 +84,50 @@ async function getData() {
isLoading.value = false; isLoading.value = false;
} }
} }
function getValueFromPath(path) {
if (!path) return;
const keys = path.toString().split('.');
let current = entity.value;
for (const key of keys) {
if (current[key] === undefined) return undefined;
else current = current[key];
}
return current;
}
const emit = defineEmits(['onFetch']); const emit = defineEmits(['onFetch']);
const iconModule = computed(() => route.matched[1].meta.icon);
const toModule = computed(() =>
route.matched[1].path.split('/').length > 2
? route.matched[1].redirect
: route.matched[1].children[0].redirect
);
</script> </script>
<template> <template>
<div class="descriptor"> <div class="descriptor">
<template v-if="entity && !isLoading"> <template v-if="entity && !isLoading">
<div class="header bg-primary q-pa-sm justify-between"> <div class="header bg-primary q-pa-sm justify-between">
<slot name="header-extra-action" /> <slot name="header-extra-action"
><QBtn
round
flat
dense
size="md"
:icon="iconModule"
color="white"
class="link"
:to="toModule"
>
<QTooltip>
{{ t('globals.goToModuleIndex') }}
</QTooltip>
</QBtn></slot
>
<QBtn <QBtn
@click.stop="viewSummary(entity.id, $props.summary)" @click.stop="viewSummary(entity.id, $props.summary)"
round round
@ -126,9 +170,9 @@ const emit = defineEmits(['onFetch']);
<QTooltip> <QTooltip>
{{ t('components.cardDescriptor.moreOptions') }} {{ t('components.cardDescriptor.moreOptions') }}
</QTooltip> </QTooltip>
<QMenu> <QMenu ref="menuRef">
<QList> <QList>
<slot name="menu" :entity="entity" /> <slot name="menu" :entity="entity" :menu-ref="menuRef" />
</QList> </QList>
</QMenu> </QMenu>
</QBtn> </QBtn>
@ -138,8 +182,8 @@ const emit = defineEmits(['onFetch']);
<QList dense> <QList dense>
<QItemLabel header class="ellipsis text-h5" :lines="1"> <QItemLabel header class="ellipsis text-h5" :lines="1">
<div class="title"> <div class="title">
<span v-if="$props.title" :title="$props.title"> <span v-if="$props.title" :title="getValueFromPath(title)">
{{ entity[title] ?? $props.title }} {{ getValueFromPath(title) ?? $props.title }}
</span> </span>
<slot v-else name="description" :entity="entity"> <slot v-else name="description" :entity="entity">
<span :title="entity.name"> <span :title="entity.name">
@ -150,7 +194,7 @@ const emit = defineEmits(['onFetch']);
</QItemLabel> </QItemLabel>
<QItem dense> <QItem dense>
<QItemLabel class="subtitle" caption> <QItemLabel class="subtitle" caption>
#{{ $props.subtitle ?? entity.id }} #{{ getValueFromPath(subtitle) ?? entity.id }}
</QItemLabel> </QItemLabel>
</QItem> </QItem>
</QList> </QList>

View File

@ -22,11 +22,15 @@ const props = defineProps({
type: String, type: String,
default: '', default: '',
}, },
moduleName: {
type: String,
default: null,
},
}); });
const emit = defineEmits(['onFetch']); const emit = defineEmits(['onFetch']);
const route = useRoute(); const route = useRoute();
const isSummary = ref(); const isSummary = ref();
const arrayData = useArrayData(props.dataKey || route.meta.moduleName, { const arrayData = useArrayData(props.dataKey, {
url: props.url, url: props.url,
filter: props.filter, filter: props.filter,
skip: 0, skip: 0,
@ -83,7 +87,7 @@ function existSummary(routes) {
v-if="showRedirectToSummaryIcon" v-if="showRedirectToSummaryIcon"
class="header link" class="header link"
:to="{ :to="{
name: `${route.meta.moduleName}Summary`, name: `${moduleName ?? route.meta.moduleName}Summary`,
params: { id: entityId || entity.id }, params: { id: entityId || entity.id },
}" }"
> >
@ -183,15 +187,10 @@ function existSummary(routes) {
color: lighten($primary, 20%); color: lighten($primary, 20%);
} }
.q-checkbox { .q-checkbox {
display: flex;
margin-bottom: 9px;
& .q-checkbox__label { & .q-checkbox__label {
margin-left: 31px;
color: var(--vn-text-color); color: var(--vn-text-color);
} }
& .q-checkbox__inner { & .q-checkbox__inner {
position: absolute;
left: 0;
color: var(--vn-label-color); color: var(--vn-label-color);
} }
} }

View File

@ -7,7 +7,7 @@ import VnImg from 'src/components/ui/VnImg.vue';
import OrderCatalogItemDialog from 'pages/Order/Card/OrderCatalogItemDialog.vue'; import OrderCatalogItemDialog from 'pages/Order/Card/OrderCatalogItemDialog.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import toCurrency from '../../../filters/toCurrency'; import { toCurrency } from 'filters/index';
const DEFAULT_PRICE_KG = 0; const DEFAULT_PRICE_KG = 0;
@ -18,6 +18,10 @@ defineProps({
type: Object, type: Object,
required: true, required: true,
}, },
isCatalog: {
type: Boolean,
default: false,
},
}); });
const dialog = ref(null); const dialog = ref(null);
@ -27,8 +31,8 @@ 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" class="image" /> <VnImg :id="item.id" zoom-size="lg" class="image" />
<div v-if="item.hex" class="item-color-container"> <div v-if="item.hex && isCatalog" class="item-color-container">
<div <div
class="item-color" class="item-color"
:style="{ backgroundColor: `#${item.hex}` }" :style="{ backgroundColor: `#${item.hex}` }"
@ -48,13 +52,18 @@ const dialog = ref(null);
:value="item?.[`value${index + 4}`]" :value="item?.[`value${index + 4}`]"
/> />
</template> </template>
<div v-if="item.minQuantity" class="min-quantity">
<QIcon name="production_quantity_limits" size="xs" />
{{ item.minQuantity }}
</div>
<div class="footer"> <div class="footer">
<div class="price"> <div class="price">
<p> <p v-if="isCatalog">
{{ item.available }} {{ t('to') }} {{ item.available }} {{ t('to') }}
{{ toCurrency(item.price) }} {{ toCurrency(item.price) }}
</p> </p>
<QIcon name="add_circle" class="icon"> <slot name="price" />
<QIcon v-if="isCatalog" name="add_circle" class="icon">
<QTooltip>{{ t('globals.add') }}</QTooltip> <QTooltip>{{ t('globals.add') }}</QTooltip>
<QPopupProxy ref="dialog"> <QPopupProxy ref="dialog">
<OrderCatalogItemDialog <OrderCatalogItemDialog
@ -128,6 +137,11 @@ const dialog = ref(null);
} }
} }
.min-quantity {
text-align: right;
color: $negative !important;
}
.footer { .footer {
.price { .price {
overflow: hidden; overflow: hidden;

View File

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

View File

@ -7,27 +7,29 @@ import toDate from 'filters/toDate';
import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue'; import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue';
const { t } = useI18n(); const { t } = useI18n();
const params = defineModel({ default: {}, required: true, type: Object });
const $props = defineProps({ const $props = defineProps({
modelValue: {
type: Object,
default: () => {},
},
dataKey: { dataKey: {
type: String, type: String,
required: true, required: true,
}, },
searchButton: { searchButton: {
type: Boolean, type: Boolean,
required: false,
default: false, default: false,
}, },
showAll: { showAll: {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
unremovableParams: { unRemovableParams: {
type: Array, type: Array,
required: false, required: false,
default: () => [], default: () => [],
description: description: `Some filters come with default search parameters and require a value.
'Algunos filtros vienen con parametros de búsqueda por default y necesitan tener si o si un valor, por eso de ser necesario, esta prop nos sirve para saber que filtros podemos remover y cuales no', This prop helps us determine which filters can be removed and which cannot.`,
}, },
exprBuilder: { exprBuilder: {
type: Function, type: Function,
@ -55,18 +57,27 @@ const $props = defineProps({
}, },
}); });
const emit = defineEmits(['refresh', 'clear', 'search', 'init', 'remove']); defineExpose({ search });
const emit = defineEmits([
'update:modelValue',
'refresh',
'clear',
'search',
'init',
'remove',
]);
const arrayData = useArrayData($props.dataKey, { const arrayData = useArrayData($props.dataKey, {
exprBuilder: $props.exprBuilder, exprBuilder: $props.exprBuilder,
searchUrl: $props.searchUrl, searchUrl: $props.searchUrl,
navigate: {}, navigate: $props.redirect ? {} : null,
}); });
const route = useRoute(); const route = useRoute();
const store = arrayData.store; const store = arrayData.store;
const userParams = ref({});
onMounted(() => { onMounted(() => {
emit('init', { params: params.value }); userParams.value = $props.modelValue ?? {};
emit('init', { params: userParams.value });
}); });
function setUserParams(watchedParams) { function setUserParams(watchedParams) {
@ -75,7 +86,7 @@ function setUserParams(watchedParams) {
if (typeof watchedParams == 'string') watchedParams = JSON.parse(watchedParams); if (typeof watchedParams == 'string') watchedParams = JSON.parse(watchedParams);
watchedParams = { ...watchedParams, ...watchedParams.filter?.where }; watchedParams = { ...watchedParams, ...watchedParams.filter?.where };
delete watchedParams.filter; delete watchedParams.filter;
params.value = { ...params.value, ...watchedParams }; userParams.value = { ...userParams.value, ...watchedParams };
} }
watch( watch(
@ -88,18 +99,21 @@ watch(
(val) => setUserParams(val) (val) => setUserParams(val)
); );
watch(
() => $props.modelValue,
(val) => (userParams.value = val ?? {})
);
const isLoading = ref(false); const isLoading = ref(false);
async function search(evt) { async function search(evt) {
if (evt && $props.disableSubmitEvent) return; if (evt && $props.disableSubmitEvent) return;
store.filter.where = {}; store.filter.where = {};
isLoading.value = true; isLoading.value = true;
const filter = { ...params.value }; const filter = { ...userParams.value };
store.userParamsChanged = true; store.userParamsChanged = true;
store.filter.skip = 0; const { params: newParams } = await arrayData.addFilter({ params: userParams.value });
store.skip = 0; userParams.value = newParams;
const { params: newParams } = await arrayData.addFilter({ params: params.value });
params.value = newParams;
if (!$props.showAll && !Object.values(filter).length) store.data = []; if (!$props.showAll && !Object.values(filter).length) store.data = [];
@ -109,8 +123,9 @@ async function search(evt) {
async function reload() { async function reload() {
isLoading.value = true; isLoading.value = true;
const params = Object.values(params.value).filter((param) => param); const params = Object.values(userParams.value).filter((param) => param);
store.skip = 0;
store.page = 1;
await arrayData.fetch({ append: false }); await arrayData.fetch({ append: false });
if (!$props.showAll && !params.length) store.data = []; if (!$props.showAll && !params.length) store.data = [];
isLoading.value = false; isLoading.value = false;
@ -120,20 +135,19 @@ async function reload() {
async function clearFilters() { async function clearFilters() {
isLoading.value = true; isLoading.value = true;
store.userParamsChanged = true; store.userParamsChanged = true;
store.filter.skip = 0; arrayData.reset(['skip', 'filter.skip', 'page']);
store.skip = 0;
// Filtrar los params no removibles // Filtrar los params no removibles
const removableFilters = Object.keys(params.value).filter((param) => const removableFilters = Object.keys(userParams.value).filter((param) =>
$props.unremovableParams.includes(param) $props.unRemovableParams.includes(param)
); );
const newParams = {}; const newParams = {};
// Conservar solo los params que no son removibles // Conservar solo los params que no son removibles
for (const key of removableFilters) { for (const key of removableFilters) {
newParams[key] = params.value[key]; newParams[key] = userParams.value[key];
} }
params.value = {}; userParams.value = {};
params.value = { ...newParams }; // Actualizar los params con los removibles userParams.value = { ...newParams }; // Actualizar los params con los removibles
await arrayData.applyFilter({ params: params.value }); await arrayData.applyFilter({ params: userParams.value });
if (!$props.showAll) { if (!$props.showAll) {
store.data = []; store.data = [];
@ -141,12 +155,13 @@ async function clearFilters() {
isLoading.value = false; isLoading.value = false;
emit('clear'); emit('clear');
emit('update:modelValue', userParams.value);
} }
const tagsList = computed(() => { const tagsList = computed(() => {
const tagList = []; const tagList = [];
for (const key of Object.keys(params.value)) { for (const key of Object.keys(userParams.value)) {
const value = params.value[key]; const value = userParams.value[key];
if (value == null || ($props.hiddenTags || []).includes(key)) continue; if (value == null || ($props.hiddenTags || []).includes(key)) continue;
tagList.push({ label: key, value }); tagList.push({ label: key, value });
} }
@ -161,9 +176,10 @@ const customTags = computed(() =>
); );
async function remove(key) { async function remove(key) {
params.value[key] = undefined; userParams.value[key] = undefined;
search(); search();
emit('remove', key); emit('remove', key);
emit('update:modelValue', userParams.value);
} }
function formatValue(value) { function formatValue(value) {
@ -175,6 +191,14 @@ function formatValue(value) {
</script> </script>
<template> <template>
<QBtn
class="q-mt-lg q-mr-xs q-mb-lg"
round
color="primary"
style="position: fixed; z-index: 1; right: 0; bottom: 0"
icon="search"
@click="search()"
></QBtn>
<QForm @submit="search" id="filterPanelForm"> <QForm @submit="search" id="filterPanelForm">
<QList dense> <QList dense>
<QItem class="q-mt-xs"> <QItem class="q-mt-xs">
@ -223,7 +247,7 @@ function formatValue(value) {
<VnFilterPanelChip <VnFilterPanelChip
v-for="chip of tags" v-for="chip of tags"
:key="chip.label" :key="chip.label"
:removable="!unremovableParams.includes(chip.label)" :removable="!unRemovableParams.includes(chip.label)"
@remove="remove(chip.label)" @remove="remove(chip.label)"
> >
<slot name="tags" :tag="chip" :format-fn="formatValue"> <slot name="tags" :tag="chip" :format-fn="formatValue">
@ -236,7 +260,7 @@ function formatValue(value) {
<slot <slot
v-if="$slots.customTags" v-if="$slots.customTags"
name="customTags" name="customTags"
:params="params" :params="userParams"
:tags="customTags" :tags="customTags"
:format-fn="formatValue" :format-fn="formatValue"
:search-fn="search" :search-fn="search"
@ -246,25 +270,8 @@ 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="params" :search-fn="search"></slot> <slot name="body" :params="userParams" :search-fn="search"></slot>
</QList> </QList>
<template v-if="$props.searchButton">
<QItem>
<QItemSection class="q-py-sm">
<QBtn
:label="t('Search')"
class="full-width"
color="primary"
dense
icon="search"
rounded
:type="disableSubmitEvent ? 'button' : 'submit'"
unelevated
/>
</QItemSection>
</QItem>
<QSeparator />
</template>
</QForm> </QForm>
<QInnerLoading <QInnerLoading
:label="t('globals.pleaseWait')" :label="t('globals.pleaseWait')"

View File

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

View File

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

View File

@ -21,7 +21,7 @@ const currentUser = ref(state.getUser());
const newNote = ref(''); const newNote = ref('');
const vnPaginateRef = ref(); const vnPaginateRef = ref();
function handleKeyUp(event) { function handleKeyUp(event) {
if (event.key === 'Enter') { if (event.key === 'Enter') {
event.preventDefault(); event.preventDefault();
if (!event.shiftKey) insert(); if (!event.shiftKey) insert();
} }
@ -78,6 +78,7 @@ async function insert() {
ref="vnPaginateRef" ref="vnPaginateRef"
class="show" class="show"
v-bind="$attrs" v-bind="$attrs"
search-url="notes"
> >
<template #body="{ rows }"> <template #body="{ rows }">
<TransitionGroup name="list" tag="div" class="column items-center full-width"> <TransitionGroup name="list" tag="div" class="column items-center full-width">

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { onMounted, ref, watch } from 'vue'; import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
@ -26,6 +26,10 @@ const props = defineProps({
type: Object, type: Object,
default: null, default: null,
}, },
userFilter: {
type: Object,
default: null,
},
where: { where: {
type: Object, type: Object,
default: null, default: null,
@ -80,6 +84,7 @@ const pagination = ref({
const arrayData = useArrayData(props.dataKey, { const arrayData = useArrayData(props.dataKey, {
url: props.url, url: props.url,
filter: props.filter, filter: props.filter,
userFilter: props.userFilter,
where: props.where, where: props.where,
limit: props.limit, limit: props.limit,
order: props.order, order: props.order,
@ -95,6 +100,8 @@ onMounted(async () => {
mounted.value = true; mounted.value = true;
}); });
onBeforeUnmount(() => arrayData.reset());
watch( watch(
() => props.data, () => props.data,
() => { () => {
@ -118,8 +125,7 @@ const addFilter = async (filter, params) => {
async function fetch(params) { async function fetch(params) {
useArrayData(props.dataKey, params); useArrayData(props.dataKey, params);
store.filter.skip = 0; arrayData.reset(['filter.skip', 'skip']);
store.skip = 0;
await arrayData.fetch({ append: false }); await arrayData.fetch({ append: false });
if (!store.hasMoreData) { if (!store.hasMoreData) {
isLoading.value = false; isLoading.value = false;

View File

@ -103,7 +103,7 @@ async function search() {
const staticParams = Object.entries(store.userParams).filter( const staticParams = Object.entries(store.userParams).filter(
([key, value]) => value && (props.staticParams || []).includes(key) ([key, value]) => value && (props.staticParams || []).includes(key)
); );
store.skip = 0; arrayData.reset(['skip', 'page']);
if (props.makeFetch) if (props.makeFetch)
await arrayData.applyFilter({ await arrayData.applyFilter({

View File

@ -1,6 +1,7 @@
<script setup> <script setup>
import { onMounted, onUnmounted, ref } from 'vue'; import { onMounted, onBeforeUnmount, ref } from 'vue';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
const stateStore = useStateStore(); const stateStore = useStateStore();
const actions = ref(null); const actions = ref(null);
const data = ref(null); const data = ref(null);
@ -24,13 +25,12 @@ onMounted(() => {
if (data.value) observer.observe(data.value, opts); if (data.value) observer.observe(data.value, opts);
}); });
onUnmounted(() => { onBeforeUnmount(() => stateStore.toggleSubToolbar());
stateStore.toggleSubToolbar();
});
</script> </script>
<template> <template>
<QToolbar <QToolbar
id="subToolbar"
class="justify-end sticky" class="justify-end sticky"
v-show="hasContent || $slots['st-actions'] || $slots['st-data']" v-show="hasContent || $slots['st-actions'] || $slots['st-data']"
> >

View File

@ -1,4 +1,4 @@
import { onMounted, ref, computed } from 'vue'; import { onMounted, computed } from 'vue';
import { useRouter, useRoute } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import axios from 'axios'; import axios from 'axios';
import { useArrayDataStore } from 'stores/useArrayDataStore'; import { useArrayDataStore } from 'stores/useArrayDataStore';
@ -16,20 +16,19 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
const router = useRouter(); const router = useRouter();
let canceller = null; let canceller = null;
const page = ref(1);
onMounted(() => { onMounted(() => {
setOptions(); setOptions();
store.skip = 0; reset(['skip']);
const query = route.query; const query = route.query;
const searchUrl = store.searchUrl; const searchUrl = store.searchUrl;
if (query[searchUrl]) { if (query[searchUrl]) {
const params = JSON.parse(query[searchUrl]); const params = JSON.parse(query[searchUrl]);
const filter = params?.filter; const filter = params?.filter && JSON.parse(params?.filter ?? '{}');
delete params.filter; delete params.filter;
store.userParams = { ...params, ...store.userParams }; store.userParams = { ...params, ...store.userParams };
store.userFilter = { ...JSON.parse(filter), ...store.userFilter }; store.userFilter = { ...filter, ...store.userFilter };
if (filter.order) store.order = filter.order;
} }
}); });
@ -71,9 +70,7 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
canceller = new AbortController(); canceller = new AbortController();
const filter = { const filter = {
order: store.order,
limit: store.limit, limit: store.limit,
skip: store.skip,
}; };
let exprFilter; let exprFilter;
@ -88,13 +85,19 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
} }
Object.assign(filter, store.userFilter, exprFilter); Object.assign(filter, store.userFilter, exprFilter);
Object.assign(store.filter, filter); let where;
const params = { if (filter?.where || store.filter?.where)
filter: JSON.stringify(store.filter), where = Object.assign(filter?.where ?? {}, store.filter?.where ?? {});
}; Object.assign(filter, store.filter);
filter.where = where;
const params = { filter };
Object.assign(params, userParams); Object.assign(params, userParams);
params.filter.skip = store.skip;
if (store.order && store.order.length) params.filter.order = store.order;
else delete params.filter.order;
params.filter = JSON.stringify(params.filter);
store.currentFilter = params; store.currentFilter = params;
store.isLoading = true; store.isLoading = true;
const response = await axios.get(store.url, { const response = await axios.get(store.url, {
@ -130,6 +133,10 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
delete store[option]; delete store[option];
} }
function reset(opts = []) {
if (arrayDataStore.get(key)) arrayDataStore.reset(key, opts);
}
function cancelRequest() { function cancelRequest() {
if (canceller) { if (canceller) {
canceller.abort(); canceller.abort();
@ -142,42 +149,82 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
store.filter = {}; store.filter = {};
if (params) store.userParams = { ...params }; if (params) store.userParams = { ...params };
const response = await fetch({ append: false }); const response = await fetch({});
return response; return response;
} }
async function addFilter({ filter, params }) { async function addFilter({ filter, params }) {
if (filter) store.userFilter = Object.assign(store.userFilter, filter); if (filter) store.filter = filter;
let userParams = { ...store.userParams, ...params }; let userParams = { ...store.userParams, ...params };
userParams = sanitizerParams(userParams, store?.exprBuilder); userParams = sanitizerParams(userParams, store?.exprBuilder);
store.userParams = userParams; store.userParams = userParams;
store.skip = 0; reset(['skip', 'filter.skip', 'page']);
store.filter.skip = 0;
page.value = 1;
await fetch({ append: false }); await fetch({});
return { filter, params }; return { filter, params };
} }
async function addFilterWhere(where) { async function addFilterWhere(where) {
const storedFilter = { ...store.userFilter }; const storedFilter = { ...store.filter };
if (!storedFilter?.where) storedFilter.where = {}; if (!storedFilter?.where) storedFilter.where = {};
where = { ...storedFilter.where, ...where }; where = { ...storedFilter.where, ...where };
await addFilter({ filter: { where } }); await addFilter({ filter: { where } });
} }
async function addOrder(field, direction = 'ASC') {
const newOrder = field + ' ' + direction;
let order = store.order || [];
if (typeof order == 'string') order = [order];
let index = order.findIndex((o) => o.split(' ')[0] === field);
if (index > -1) {
order[index] = newOrder;
} else {
index = order.length;
order.push(newOrder);
}
store.order = order;
reset(['skip', 'filter.skip', 'page']);
fetch({});
index++;
return { index, order };
}
async function deleteOrder(field) {
let order = store.order ?? [];
if (typeof order == 'string') order = [order];
const index = order.findIndex((o) => o.split(' ')[0] === field);
if (index > -1) order.splice(index, 1);
store.order = order;
fetch({});
}
function sanitizerParams(params, exprBuilder) { function sanitizerParams(params, exprBuilder) {
for (const param in params) { for (const param in params) {
if (params[param] === '' || params[param] === null) { if (params[param] === '' || params[param] === null) {
delete store.userParams[param]; delete store.userParams[param];
delete params[param]; delete params[param];
if (store.filter?.where) { if (store.filter?.where) {
const key = Object.keys(exprBuilder ? exprBuilder(param) : param); let key;
if (key[0]) delete store.filter.where[key[0]]; if (exprBuilder) {
if (Object.keys(store.filter.where).length === 0) { const result = exprBuilder(param);
delete store.filter.where; if (result !== undefined && result !== null)
key = Object.keys(result);
} else {
if (typeof param === 'object' && param !== null)
key = Object.keys(param);
}
if (key && key[0]) {
delete store.filter.where[key[0]];
if (Object.keys(store.filter.where).length === 0) {
delete store.filter.where;
}
} }
} }
} }
@ -188,14 +235,14 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
async function loadMore() { async function loadMore() {
if (!store.hasMoreData) return; if (!store.hasMoreData) return;
store.skip = store.limit * page.value; store.skip = store.limit * store.page;
page.value += 1; store.page += 1;
await fetch({ append: true }); await fetch({ append: true });
} }
async function refresh() { async function refresh() {
if (Object.values(store.userParams).length) await fetch({ append: false }); if (Object.values(store.userParams).length) await fetch({});
} }
function updateStateParams() { function updateStateParams() {
@ -237,6 +284,8 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
applyFilter, applyFilter,
addFilter, addFilter,
addFilterWhere, addFilterWhere,
addOrder,
deleteOrder,
refresh, refresh,
destroy, destroy,
loadMore, loadMore,
@ -245,5 +294,6 @@ export function useArrayData(key = useRoute().meta.moduleName, userOptions) {
updateStateParams, updateStateParams,
isLoading, isLoading,
deleteOption, deleteOption,
reset,
}; };
} }

View File

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

View File

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

View File

@ -4,8 +4,10 @@
body.body--light { body.body--light {
--font-color: black; --font-color: black;
--vn-section-color: #e0e0e0; --vn-header-color: #cecece;
--vn-page-color: #ffffff; --vn-page-color: #ffffff;
--vn-section-color: #e0e0e0;
--vn-section-hover-color: #b9b9b9;
--vn-text-color: var(--font-color); --vn-text-color: var(--font-color);
--vn-label-color: #5f5f5f; --vn-label-color: #5f5f5f;
--vn-accent-color: #e7e3e3; --vn-accent-color: #e7e3e3;
@ -17,8 +19,10 @@ body.body--light {
} }
} }
body.body--dark { body.body--dark {
--vn-header-color: #5d5d5d;
--vn-page-color: #222; --vn-page-color: #222;
--vn-section-color: #3d3d3d; --vn-section-color: #3d3d3d;
--vn-section-hover-color: #747474;
--vn-text-color: white; --vn-text-color: white;
--vn-label-color: #a8a8a8; --vn-label-color: #a8a8a8;
--vn-accent-color: #424242; --vn-accent-color: #424242;
@ -71,8 +75,9 @@ select:-webkit-autofill {
.bg-vn-section-color { .bg-vn-section-color {
background-color: var(--vn-section-color); background-color: var(--vn-section-color);
} }
.bg-hover {
background-color: #666666; .bg-vn-hover {
background-color: var(--vn-section-hover-color);
} }
.color-vn-label { .color-vn-label {
@ -152,6 +157,15 @@ select:-webkit-autofill {
color: var(--vn-label-color); color: var(--vn-label-color);
} }
.disabled {
& .q-checkbox__label {
color: var(--vn-text-color);
}
& .q-checkbox__inner {
color: var(--vn-label-color);
}
}
.q-chip, .q-chip,
.q-notification__message, .q-notification__message,
.q-notification__icon { .q-notification__icon {
@ -174,8 +188,6 @@ select:-webkit-autofill {
justify-content: center; justify-content: center;
} }
/* q-notification row items-stretch q-notification--standard bg-negative text-white */
.q-card, .q-card,
.q-table, .q-table,
.q-table__bottom, .q-table__bottom,
@ -219,3 +231,29 @@ input::-webkit-inner-spin-button {
*::-webkit-scrollbar-track { *::-webkit-scrollbar-track {
background: transparent; background: transparent;
} }
.q-table {
thead,
tbody {
th {
.q-select {
max-width: 120px;
}
}
td {
padding: 1px 10px 1px 10px;
max-width: 100px;
div span {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.expand {
max-width: 400px;
}
}
}

View File

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

View File

@ -46,6 +46,7 @@ globals:
noPinnedModules: You don't have any pinned modules noPinnedModules: You don't have any pinned modules
summary: summary:
basicData: Basic data basicData: Basic data
daysOnward: Days onward
today: Today today: Today
yesterday: Yesterday yesterday: Yesterday
dateFormat: en-GB dateFormat: en-GB
@ -90,6 +91,7 @@ globals:
send: Send send: Send
code: Code code: Code
pageTitles: pageTitles:
logIn: Login
summary: Summary summary: Summary
basicData: Basic data basicData: Basic data
log: Logs log: Logs
@ -100,19 +102,158 @@ globals:
modes: Modes modes: Modes
zones: Zones zones: Zones
zonesList: Zones zonesList: Zones
deliveryList: Delivery days deliveryDays: Delivery days
upcomingList: Upcoming deliveries upcomingDeliveries: Upcoming deliveries
role: Role role: Role
alias: Alias alias: Alias
aliasUsers: Users aliasUsers: Users
subRoles: Subroles subRoles: Subroles
inheritedRoles: Inherited Roles inheritedRoles: Inherited Roles
customers: Customers
list: List
webPayments: Web Payments
extendedList: Extended list
notifications: Notifications
defaulter: Defaulter
customerCreate: New customer
fiscalData: Fiscal data
billingData: Billing data
consignees: Consignees
notes: Notes
credits: Credits
greuges: Greuges
balance: Balance
recoveries: Recoveries
webAccess: Web access
sms: Sms
creditManagement: Credit management
creditContracts: Credit contracts
creditOpinion: Credit opinion
others: Others
samples: Samples
consumption: Consumption
mandates: Mandates
contacts: Contacts
webPayment: Web payment
fileManagement: File management
unpaid: Unpaid
entries: Entries
buys: Buys
dms: File management
entryCreate: New entry
latestBuys: Latest buys
tickets: Tickets
ticketCreate: New Tickets
boxing: Boxing
sale: Sale
claims: Claims
claimCreate: New claim
lines: Lines
photos: Photos
development: Development
action: Action
invoiceOuts: Invoice out
negativeBases: Negative Bases
globalInvoicing: Global invoicing
invoiceOutCreate: Create invoice out
shelving: Shelving
shelvingList: Shelving List
shelvingCreate: New shelving
invoiceIns: Invoices In
invoiceInCreate: Create invoice in
vat: VAT
dueDay: Due day
intrastat: Intrastat
corrective: Corrective
order: Orders
orderList: List
orderCreate: New order
catalog: Catalog
volume: Volume
workers: Workers
workerCreate: New worker
department: Department
pda: PDA
pbx: Private Branch Exchange
calendar: Calendar
timeControl: Time control
locker: Locker
wagons: Wagons
wagonsList: Wagons List
wagonCreate: Create wagon
wagonEdit: Edit wagon
typesList: Types List
typeCreate: Create type
typeEdit: Edit type
wagonCounter: Trolley counter
roadmap: Roadmap
stops: Stops
routes: Routes
cmrsList: CMRs list
RouteList: List
routeCreate: New route
RouteRoadmap: Roadmaps
RouteRoadmapCreate: Create roadmap
autonomous: Autonomous
suppliers: Suppliers
supplier: Supplier
expedition: Expedition
services: Service
components: Components
pictures: Pictures
packages: Packages
tracking: Tracking
labeler: Labeler
supplierCreate: New supplier
accounts: Accounts
addresses: Addresses
agencyTerm: Agency agreement
travel: Travels
extraCommunity: Extra community
travelCreate: New travel
history: Log
thermographs: Thermograph
items: Items
diary: Diary
tags: Tags
create: Create
buyRequest: Buy requests
fixedPrice: Fixed prices
wasteBreakdown: Waste breakdown
itemCreate: New item
barcode: Barcodes
tax: Tax
botanical: Botanical
itemTypeCreate: New item type
family: Item Type
lastEntries: Last entries
itemType: Item type
monitors: Monitors
dashboard: Dashboard
users: Users
createTicket: Create ticket
ticketAdvance: Advance tickets
futureTickets: Future tickets
purchaseRequest: Purchase request
weeklyTickets: Weekly tickets
formation: Formation
locations: Locations
warehouses: Warehouses
saleTracking: Sale tracking
roles: Roles
connections: Connections
acls: ACLs
mailForwarding: Mail forwarding
mailAlias: Mail alias
privileges: Privileges
created: Created created: Created
worker: Worker worker: Worker
now: Now now: Now
name: Name name: Name
new: New new: New
comment: Comment comment: Comment
observations: Observations
goToModuleIndex: Go to module index
errors: errors:
statusUnauthorized: Access denied statusUnauthorized: Access denied
statusInternalServerError: An internal server error has ocurred statusInternalServerError: An internal server error has ocurred
@ -131,8 +272,6 @@ login:
loginError: Invalid username or password loginError: Invalid username or password
fieldRequired: This field is required fieldRequired: This field is required
twoFactorRequired: Two-factor verification required twoFactorRequired: Two-factor verification required
pageTitles:
logIn: Login
twoFactor: twoFactor:
code: Code code: Code
validate: Validate validate: Validate
@ -147,40 +286,8 @@ verifyEmail:
verifyEmail: Email verification verifyEmail: Email verification
dashboard: dashboard:
pageTitles: pageTitles:
dashboard: Dashboard
customer: customer:
pageTitles:
customers: Customers
list: List
webPayments: Web Payments
extendedList: Extended list
notifications: Notifications
defaulter: Defaulter
customerCreate: New customer
summary: Summary
basicData: Basic data
fiscalData: Fiscal data
billingData: Billing data
consignees: Consignees
notes: Notes
credits: Credits
greuges: Greuges
balance: Balance
recoveries: Recoveries
webAccess: Web access
log: Log
sms: Sms
creditManagement: Credit management
creditContracts: Credit contracts
creditOpinion: Credit opinion
others: Others
samples: Samples
consumption: Consumption
mandates: Mandates
contacts: Contacts
webPayment: Web payment
fileManagement: File management
unpaid: Unpaid
list: list:
phone: Phone phone: Phone
email: Email email: Email
@ -310,17 +417,6 @@ customer:
hasCoreVnl: VNL core received hasCoreVnl: VNL core received
hasSepaVnl: VNL B2B received hasSepaVnl: VNL B2B received
entry: entry:
pageTitles:
entries: Entries
list: List
summary: Summary
basicData: Basic data
buys: Buys
notes: Notes
dms: File management
log: Log
entryCreate: New entry
latestBuys: Latest buys
list: list:
newEntry: New entry newEntry: New entry
landed: Landed landed: Landed
@ -329,6 +425,18 @@ entry:
booked: Booked booked: Booked
confirmed: Confirmed confirmed: Confirmed
ordered: Ordered ordered: Ordered
tableVisibleColumns:
id: Id
reference: Reference
created: Creation
supplierFk: Supplier
isBooked: Booked
isConfirmed: Confirmed
isOrdered: Ordered
companyFk: Company
travelFk: Travel
isExcludedFromAvailable: Inventory
isRaid: Raid
summary: summary:
commission: Commission commission: Commission
currency: Currency currency: Currency
@ -404,34 +512,37 @@ entry:
landed: Landed landed: Landed
warehouseOut: Warehouse Out warehouseOut: Warehouse Out
latestBuys: latestBuys:
picture: Picture tableVisibleColumns:
itemFk: Item ID image: Picture
packing: Packing itemFk: Item ID
grouping: Grouping packing: Packing
quantity: Quantity grouping: Grouping
size: Size quantity: Quantity
tags: Tags size: Size
type: Type tags: Tags
intrastat: Intrastat type: Type
origin: Origin intrastat: Intrastat
weightByPiece: Weight/Piece origin: Origin
isActive: Active weightByPiece: Weight/Piece
family: Family isActive: Active
entryFk: Entry family: Family
buyingValue: Buying value entryFk: Entry
freightValue: Freight value buyingValue: Buying value
comissionValue: Commission value freightValue: Freight value
description: Description comissionValue: Commission value
packageValue: Package value description: Description
isIgnored: Is ignored packageValue: Package value
price2: Grouping isIgnored: Is ignored
price3: Packing price2: Grouping
minPrice: Min price3: Packing
ektFk: Ekt minPrice: Min
weight: Weight ektFk: Ekt
packagingFk: Package weight: Weight
packingOut: Package out packagingFk: Package
landing: Landing packingOut: Package out
landing: Landing
isExcludedFromAvailable: Es inventory
isRaid: Raid
ticket: ticket:
pageTitles: pageTitles:
tickets: Tickets tickets: Tickets
@ -443,6 +554,13 @@ ticket:
sms: Sms sms: Sms
notes: Notes notes: Notes
sale: Sale sale: Sale
dms: File management
volume: Volume
observation: Notes
ticketAdvance: Advance tickets
futureTickets: Future tickets
purchaseRequest: Purchase request
weeklyTickets: Weekly tickets
list: list:
nickname: Nickname nickname: Nickname
state: State state: State
@ -516,90 +634,7 @@ ticket:
landed: Landed landed: Landed
warehouse: Warehouse warehouse: Warehouse
agency: Agency agency: Agency
claim:
pageTitles:
claims: Claims
list: List
claimCreate: New claim
summary: Summary
basicData: Basic Data
lines: Lines
photos: Photos
development: Development
log: Audit logs
notes: Notes
action: Action
list:
customer: Customer
assignedTo: Assigned
created: Created
state: State
rmaList:
code: Code
records: records
card:
claimId: Claim ID
attendedBy: Attended by
created: Created
state: State
ticketId: Ticket ID
customerSummary: Customer summary
claimedTicket: Claimed ticket
saleTracking: Sale tracking
ticketTracking: Ticket tracking
commercial: Commercial
province: Province
zone: Zone
customerId: client ID
summary:
customer: Customer
assignedTo: Assigned
attendedBy: Attended by
created: Created
state: State
details: Details
item: Item
landed: Landed
quantity: Quantity
claimed: Claimed
price: Price
discount: Discount
total: Total
actions: Actions
responsibility: Responsibility
company: Company
person: Employee/Customer
notes: Notes
photos: Photos
development: Development
reason: Reason
result: Result
responsible: Responsible
worker: Worker
redelivery: Redelivery
changeState: Change state
basicData:
customer: Customer
assignedTo: Assigned
created: Created
state: State
pickup: Pick up
null: No
agency: Agency
delivery: Delivery
photo:
fileDescription: 'Claim id {claimId} from client {clientName} id {clientId}'
noData: 'There are no images/videos, click here or drag and drop the file'
dragDrop: Drag and drop it here
invoiceOut: invoiceOut:
pageTitles:
invoiceOuts: Invoice out
list: List
negativeBases: Negative Bases
globalInvoicing: Global invoicing
invoiceOutCreate: Create invoice out
summary: Summary
basicData: Basic Data
list: list:
ref: Reference ref: Reference
issued: Issued issued: Issued
@ -667,13 +702,6 @@ invoiceOut:
errors: errors:
downloadCsvFailed: CSV download failed downloadCsvFailed: CSV download failed
shelving: shelving:
pageTitles:
shelving: Shelving
shelvingList: Shelving List
shelvingCreate: New shelving
summary: Summary
basicData: Basic Data
log: Logs
list: list:
parking: Parking parking: Parking
priority: Priority priority: Priority
@ -700,17 +728,6 @@ parking:
info: You can search by parking code info: You can search by parking code
label: Search parking... label: Search parking...
invoiceIn: invoiceIn:
pageTitles:
invoiceIns: Invoices In
list: List
invoiceInCreate: Create invoice in
summary: Summary
basicData: Basic data
vat: VAT
dueDay: Due day
intrastat: Intrastat
corrective: Corrective
log: Logs
list: list:
ref: Reference ref: Reference
supplier: Supplier supplier: Supplier
@ -761,15 +778,6 @@ invoiceIn:
stems: Stems stems: Stems
country: Country country: Country
order: order:
pageTitles:
order: Orders
orderList: List
orderCreate: New order
summary: Summary
basicData: Basic Data
catalog: Catalog
volume: Volume
lines: Lines
field: field:
salesPersonFk: Sales Person salesPersonFk: Sales Person
clientFk: Client clientFk: Client
@ -844,7 +852,7 @@ worker:
calendar: Calendar calendar: Calendar
timeControl: Time control timeControl: Time control
locker: Locker locker: Locker
balance: Balance
list: list:
name: Name name: Name
email: Email email: Email
@ -914,17 +922,25 @@ worker:
payMethods: Pay method payMethods: Pay method
iban: IBAN iban: IBAN
bankEntity: Swift / BIC bankEntity: Swift / BIC
formation:
tableVisibleColumns:
course: Curso
startDate: Fecha Inicio
endDate: Fecha Fin
center: Centro Formación
invoice: Factura
amount: Importe
remark: Bonficado
hasDiploma: Diploma
imageNotFound: Image not found imageNotFound: Image not found
balance:
tableVisibleColumns:
paymentDate: Date
incomeType: Type
debit: Debt
credit: Have
concept: Concept
wagon: wagon:
pageTitles:
wagons: Wagons
wagonsList: Wagons List
wagonCreate: Create wagon
wagonEdit: Edit wagon
typesList: Types List
typeCreate: Create type
typeEdit: Edit type
wagonCounter: Trolley counter
type: type:
name: Name name: Name
submit: Submit submit: Submit
@ -953,20 +969,9 @@ wagon:
minHeightBetweenTrays: 'The minimum height between trays is ' minHeightBetweenTrays: 'The minimum height between trays is '
maxWagonHeight: 'The maximum height of the wagon is ' maxWagonHeight: 'The maximum height of the wagon is '
uncompleteTrays: There are incomplete trays uncompleteTrays: There are incomplete trays
route/roadmap:
pageTitles:
roadmap: Roadmap
summary: Summary
basicData: Basic Data
stops: Stops
roadmap:
pageTitles:
roadmap: Roadmap
summary: Summary
basicData: Basic Data
stops: Stops
route: route:
pageTitles: pageTitles:
agency: Agency List
routes: Routes routes: Routes
cmrsList: CMRs list cmrsList: CMRs list
RouteList: List RouteList: List
@ -1005,28 +1010,20 @@ route:
volume: Volume volume: Volume
finished: Finished finished: Finished
supplier: supplier:
pageTitles:
suppliers: Suppliers
supplier: Supplier
list: List
supplierCreate: New supplier
summary: Summary
basicData: Basic data
fiscalData: Fiscal data
billingData: Billing data
log: Log
accounts: Accounts
contacts: Contacts
addresses: Addresses
consumption: Consumption
agencyTerm: Agency agreement
dms: File management
list: list:
payMethod: Pay method payMethod: Pay method
payDeadline: Pay deadline payDeadline: Pay deadline
payDay: Pay day payDay: Pay day
account: Account account: Account
newSupplier: New supplier newSupplier: New supplier
tableVisibleColumns:
id: Id
name: Name
nif: NIF/CIF
nickname: Alias
account: Account
payMethod: Pay Method
payDay: Pay Day
summary: summary:
responsible: Responsible responsible: Responsible
notes: Notes notes: Notes
@ -1112,15 +1109,16 @@ supplier:
date: Date date: Date
reference: Reference reference: Reference
travel: travel:
pageTitles: travelList:
travel: Travels tableVisibleColumns:
list: List id: Id
summary: Summary ref: Reference
extraCommunity: Extra community agency: Agency
travelCreate: New travel shipped: Shipped
basicData: Basic data landed: Landed
history: Log warehouseIn: Warehouse in
thermographs: Thermograph warehouseOut: Warehouse out
totalEntries: Total entries
summary: summary:
confirmed: Confirmed confirmed: Confirmed
entryId: Entry Id entryId: Entry Id
@ -1167,24 +1165,6 @@ travel:
travelFileDescription: 'Travel id { travelId }' travelFileDescription: 'Travel id { travelId }'
file: File file: File
item: item:
pageTitles:
items: Items
list: List
diary: Diary
tags: Tags
create: Create
buyRequest: Buy requests
fixedPrice: Fixed prices
wasteBreakdown: Waste breakdown
itemCreate: New item
barcode: Barcodes
tax: Tax
log: Log
botanical: Botanical
shelving: Shelving
itemTypeCreate: New item type
family: Item Type
lastEntries: Last entries
descriptor: descriptor:
item: Item item: Item
buyer: Buyer buyer: Buyer
@ -1270,22 +1250,6 @@ item:
minSalesQuantity: 'Cantidad mínima de venta' minSalesQuantity: 'Cantidad mínima de venta'
genus: 'Genus' genus: 'Genus'
specie: 'Specie' specie: 'Specie'
item/itemType:
pageTitles:
itemType: Item type
basicData: Basic data
summary: Summary
monitor:
pageTitles:
monitors: Monitors
list: List
zone:
pageTitles:
zones: Zones
zonesList: Zones
deliveryList: Delivery days
upcomingList: Upcoming deliveries
components: components:
topbar: {} topbar: {}
itemsFilterPanel: itemsFilterPanel:

View File

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

View File

@ -3,7 +3,6 @@ import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelect from 'src/components/common/VnSelect.vue';
import FormModel from 'components/FormModel.vue'; import FormModel from 'components/FormModel.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 { ref, watch } from 'vue'; import { ref, watch } from 'vue';

View File

@ -6,8 +6,8 @@ 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 AccountDescriptorMenu from './AccountDescriptorMenu.vue'; import AccountDescriptorMenu from './AccountDescriptorMenu.vue';
import { useSession } from 'src/composables/useSession';
import FetchData from 'src/components/FetchData.vue'; import FetchData from 'src/components/FetchData.vue';
import VnImg from 'src/components/ui/VnImg.vue';
const $props = defineProps({ const $props = defineProps({
id: { id: {
@ -19,7 +19,6 @@ const $props = defineProps({
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const { getTokenMultimedia } = useSession();
const entityId = computed(() => { const entityId = computed(() => {
return $props.id || route.params.id; return $props.id || route.params.id;
}); });
@ -31,10 +30,6 @@ const filter = {
fields: ['id', 'nickname', 'name', 'role'], fields: ['id', 'nickname', 'name', 'role'],
include: { relation: 'role', scope: { fields: ['id', 'name'] } }, include: { relation: 'role', scope: { fields: ['id', 'name'] } },
}; };
function getAccountAvatar() {
const token = getTokenMultimedia();
return `/api/Images/user/160x160/${entityId.value}/download?access_token=${token}`;
}
const hasAccount = ref(false); const hasAccount = ref(false);
</script> </script>
@ -54,25 +49,12 @@ const hasAccount = ref(false);
:title="data.title" :title="data.title"
:subtitle="data.subtitle" :subtitle="data.subtitle"
> >
<template #header-extra-action>
<QBtn
round
flat
size="md"
color="white"
icon="face"
:to="{ name: 'AccountList' }"
>
<QTooltip>
{{ t('Go to module index') }}
</QTooltip>
</QBtn>
</template>
<template #menu> <template #menu>
<AccountDescriptorMenu :has-account="hasAccount" /> <AccountDescriptorMenu :has-account="hasAccount" />
</template> </template>
<template #before> <template #before>
<QImg :src="getAccountAvatar()" class="photo"> <!-- falla id :id="entityId.value" collection="user" size="160x160" -->
<VnImg :id="entityId" collection="user" size="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"
@ -87,7 +69,7 @@ const hasAccount = ref(false);
</div> </div>
</div> </div>
</template> </template>
</QImg> </VnImg>
</template> </template>
<template #body="{ entity }"> <template #body="{ entity }">
<VnLv :label="t('account.card.nickname')" :value="entity.nickname" /> <VnLv :label="t('account.card.nickname')" :value="entity.nickname" />

View File

@ -30,6 +30,7 @@ const filter = {
<template> <template>
<CardSummary <CardSummary
data-key="AccountSummary"
ref="AccountSummary" ref="AccountSummary"
url="VnUsers/preview" url="VnUsers/preview"
:filter="filter" :filter="filter"

View File

@ -15,6 +15,10 @@ const $props = defineProps({
required: false, required: false,
default: null, default: null,
}, },
summary: {
type: Object,
default: null,
},
}); });
const route = useRoute(); const route = useRoute();
@ -60,14 +64,14 @@ const removeRole = () => {
<template> <template>
<CardDescriptor <CardDescriptor
ref="descriptor" :url="`VnRoles/${entityId}`"
:url="`VnRoles`"
:filter="filter" :filter="filter"
module="Role" module="Role"
@on-fetch="setData" @on-fetch="setData"
data-key="accountData" data-key="accountData"
:title="data.title" :title="data.title"
:subtitle="data.subtitle" :subtitle="data.subtitle"
:summary="$props.summary"
> >
<template #menu> <template #menu>
<QItem v-ripple clickable @click="removeRole()"> <QItem v-ripple clickable @click="removeRole()">

View File

@ -0,0 +1,17 @@
<script setup>
import RoleDescriptor from './RoleDescriptor.vue';
import RoleSummary from './RoleSummary.vue';
const $props = defineProps({
id: {
type: Number,
required: true,
},
});
</script>
<template>
<QPopupProxy>
<RoleDescriptor v-if="$props.id" :id="$props.id" :summary="RoleSummary" />
</QPopupProxy>
</template>

View File

@ -30,6 +30,7 @@ const filter = {
:url="`VnRoles`" :url="`VnRoles`"
:filter="filter" :filter="filter"
@on-fetch="(data) => (role = data)" @on-fetch="(data) => (role = data)"
data-key="RoleSummary"
> >
<template #header> {{ role.id }} - {{ role.name }} </template> <template #header> {{ role.id }} - {{ role.name }} </template>
<template #body> <template #body>

View File

@ -1,72 +0,0 @@
<script setup>
import VnPaginate from 'src/components/ui/VnPaginate.vue';
import CardList from 'src/components/ui/CardList.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import { useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const router = useRouter();
function navigate(id) {
router.push({ path: `/agency/${id}` });
}
function exprBuilder(param, value) {
if (!value) return;
if (param !== 'search') return;
if (!isNaN(value)) return { id: value };
return { name: { like: `%${value}%` } };
}
</script>
<template>
<VnSearchbar
:info="t('You can search by name')"
:label="t('Search agency')"
data-key="AgencyList"
url="Agencies"
/>
<QPage class="column items-center q-pa-md">
<div class="vn-card-list">
<VnPaginate
data-key="AgencyList"
url="Agencies"
order="name"
:expr-builder="exprBuilder"
>
<template #body="{ rows }">
<CardList
:id="row.id"
:key="row.id"
:title="row.name"
@click="navigate(row.id)"
v-for="row of rows"
>
<template #list-items>
<QCheckbox
:label="t('isOwn')"
v-model="row.isOwn"
:disable="true"
/>
<QCheckbox
:label="t('isAnyVolumeAllowed')"
v-model="row.isAnyVolumeAllowed"
:disable="true"
/>
</template>
</CardList>
</template>
</VnPaginate>
</div>
</QPage>
</template>
<i18n>
es:
isOwn: Tiene propietario
isAnyVolumeAllowed: Permite cualquier volumen
Search agency: Buscar agencia
You can search by name: Puedes buscar por nombre
en:
isOwn: Has owner
isAnyVolumeAllowed: Allows any volume
</i18n>

View File

@ -33,8 +33,8 @@ const DEFAULT_MAX_RESPONSABILITY = 5;
const DEFAULT_MIN_RESPONSABILITY = 1; const DEFAULT_MIN_RESPONSABILITY = 1;
const arrayData = useArrayData('claimData'); const arrayData = useArrayData('claimData');
const marker_labels = [ const marker_labels = [
{ value: DEFAULT_MIN_RESPONSABILITY, label: t('claim.summary.company') }, { value: DEFAULT_MIN_RESPONSABILITY, label: t('claim.company') },
{ value: DEFAULT_MAX_RESPONSABILITY, label: t('claim.summary.person') }, { value: DEFAULT_MAX_RESPONSABILITY, label: t('claim.person') },
]; ];
const multiplicatorValue = ref(); const multiplicatorValue = ref();
const loading = ref(false); const loading = ref(false);
@ -209,12 +209,12 @@ async function post(query, params) {
<QItem class="justify-between"> <QItem class="justify-between">
<QItemLabel class="slider-container"> <QItemLabel class="slider-container">
<p class="text-primary"> <p class="text-primary">
{{ t('claim.summary.actions') }} {{ t('claim.actions') }}
</p> </p>
<QSlider <QSlider
class="responsibility { 'background-color:primary': quasar.platform.is.mobile }" class="responsibility { 'background-color:primary': quasar.platform.is.mobile }"
v-model="claim.responsibility" v-model="claim.responsibility"
:label-value="t('claim.summary.responsibility')" :label-value="t('claim.responsibility')"
@change="(value) => save({ responsibility: value })" @change="(value) => save({ responsibility: value })"
label-always label-always
color="primary" color="primary"

View File

@ -10,12 +10,13 @@ import VnInput from 'src/components/common/VnInput.vue';
import VnInputDate from 'components/common/VnInputDate.vue'; import VnInputDate from 'components/common/VnInputDate.vue';
import axios from 'axios'; import axios from 'axios';
import { useSession } from 'src/composables/useSession'; // import { useSession } from 'src/composables/useSession';
import VnImg from 'src/components/ui/VnImg.vue';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const { getTokenMultimedia } = useSession(); // const { getTokenMultimedia } = useSession();
const token = getTokenMultimedia(); // const token = getTokenMultimedia();
const claimStates = ref([]); const claimStates = ref([]);
const claimStatesCopy = ref([]); const claimStatesCopy = ref([]);
@ -29,7 +30,7 @@ function setClaimStates(data) {
} }
async function getEnumValues() { async function getEnumValues() {
optionsList.value = [{ id: null, description: t('claim.basicData.null') }]; optionsList.value = [{ id: null, description: t('claim.null') }];
const { data } = await axios.get(`Applications/get-enum-values`, { const { data } = await axios.get(`Applications/get-enum-values`, {
params: { params: {
schema: 'vn', schema: 'vn',
@ -38,7 +39,7 @@ async function getEnumValues() {
}, },
}); });
for (let value of data) for (let value of data)
optionsList.value.push({ id: value, description: t(`claim.basicData.${value}`) }); optionsList.value.push({ id: value, description: t(`claim.${value}`) });
} }
getEnumValues(); getEnumValues();
@ -76,17 +77,14 @@ const statesFilter = {
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">
<VnInput <VnInput
v-model="data.client.name" v-model="data.client.name"
:label="t('claim.basicData.customer')" :label="t('claim.customer')"
disable disable
/> />
<VnInputDate <VnInputDate v-model="data.created" :label="t('claim.created')" />
v-model="data.created"
:label="t('claim.basicData.created')"
/>
</VnRow> </VnRow>
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">
<VnSelect <VnSelect
:label="t('claim.basicData.assignedTo')" :label="t('claim.assignedTo')"
v-model="data.workerFk" v-model="data.workerFk"
:options="workersOptions" :options="workersOptions"
option-value="id" option-value="id"
@ -97,9 +95,11 @@ const statesFilter = {
> >
<template #before> <template #before>
<QAvatar color="orange"> <QAvatar color="orange">
<QImg <VnImg
v-if="data.workerFk" v-if="data.workerFk"
:src="`/api/Images/user/160x160/${data.workerFk}/download?access_token=${token}`" :size="'160x160'"
:id="data.workerFk"
collection="user"
spinner-color="white" spinner-color="white"
/> />
</QAvatar> </QAvatar>
@ -111,7 +111,7 @@ const statesFilter = {
option-value="id" option-value="id"
option-label="description" option-label="description"
emit-value emit-value
:label="t('claim.basicData.state')" :label="t('claim.state')"
map-options map-options
use-input use-input
@filter="(value, update) => filter(value, update, statesFilter)" @filter="(value, update) => filter(value, update, statesFilter)"
@ -133,7 +133,7 @@ const statesFilter = {
option-value="id" option-value="id"
option-label="description" option-label="description"
emit-value emit-value
:label="t('claim.basicData.pickup')" :label="t('claim.pickup')"
map-options map-options
use-input use-input
:input-debounce="0" :input-debounce="0"

View File

@ -2,7 +2,7 @@
import { ref, computed, onMounted } from 'vue'; import { ref, computed, onMounted } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { toDate, toPercentage } from 'src/filters'; import { toDateHourMinSec, toPercentage } from 'src/filters';
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
import TicketDescriptorProxy from 'pages/Ticket/Card/TicketDescriptorProxy.vue'; import TicketDescriptorProxy from 'pages/Ticket/Card/TicketDescriptorProxy.vue';
import ClaimDescriptorMenu from 'pages/Claim/Card/ClaimDescriptorMenu.vue'; import ClaimDescriptorMenu from 'pages/Claim/Card/ClaimDescriptorMenu.vue';
@ -42,7 +42,7 @@ function stateColor(code) {
const data = ref(useCardDescription()); const data = ref(useCardDescription());
const setData = (entity) => { const setData = (entity) => {
if (!entity) return; if (!entity) return;
data.value = useCardDescription(entity.client.name, entity.id); data.value = useCardDescription(entity?.client?.name, entity.id);
state.set('ClaimDescriptor', entity); state.set('ClaimDescriptor', entity);
}; };
onMounted(async () => { onMounted(async () => {
@ -52,12 +52,10 @@ onMounted(async () => {
<template> <template>
<CardDescriptor <CardDescriptor
ref="descriptor"
:url="`Claims/${entityId}`" :url="`Claims/${entityId}`"
:filter="filter" :filter="filter"
module="Claim" module="Claim"
:title="data.title" title="client.name"
:subtitle="data.subtitle"
@on-fetch="setData" @on-fetch="setData"
data-key="Claim" data-key="Claim"
> >
@ -65,7 +63,7 @@ onMounted(async () => {
<ClaimDescriptorMenu :claim="entity" /> <ClaimDescriptorMenu :claim="entity" />
</template> </template>
<template #body="{ entity }"> <template #body="{ entity }">
<VnLv v-if="entity.claimState" :label="t('claim.card.state')"> <VnLv v-if="entity.claimState" :label="t('claim.state')">
<template #value> <template #value>
<QBadge <QBadge
:color="stateColor(entity.claimState.code)" :color="stateColor(entity.claimState.code)"
@ -76,8 +74,8 @@ onMounted(async () => {
</QBadge> </QBadge>
</template> </template>
</VnLv> </VnLv>
<VnLv :label="t('claim.card.created')" :value="toDate(entity.created)" /> <VnLv :label="t('claim.created')" :value="toDateHourMinSec(entity.created)" />
<VnLv :label="t('claim.card.commercial')"> <VnLv :label="t('claim.commercial')">
<template #value> <template #value>
<VnUserLink <VnUserLink
:name="entity.client?.salesPersonUser?.name" :name="entity.client?.salesPersonUser?.name"
@ -87,17 +85,17 @@ onMounted(async () => {
</VnLv> </VnLv>
<VnLv <VnLv
v-if="entity.worker" v-if="entity.worker"
:label="t('claim.card.attendedBy')" :label="t('claim.attendedBy')"
:value="entity.worker.user.name" :value="entity.worker.user.name"
> >
<template #value> <template #value>
<VnUserLink <VnUserLink
:name="entity.worker.user.nickname" :name="entity.worker.user.name"
:worker-id="entity.worker.id" :worker-id="entity.worker.id"
/> />
</template> </template>
</VnLv> </VnLv>
<VnLv :label="t('claim.card.zone')"> <VnLv :label="t('claim.zone')">
<template #value> <template #value>
<span class="link"> <span class="link">
{{ entity.ticket?.zone?.name }} {{ entity.ticket?.zone?.name }}
@ -106,10 +104,10 @@ onMounted(async () => {
</template> </template>
</VnLv> </VnLv>
<VnLv <VnLv
:label="t('claim.card.province')" :label="t('claim.province')"
:value="entity.ticket?.address?.province?.name" :value="entity.ticket?.address?.province?.name"
/> />
<VnLv :label="t('claim.card.ticketId')"> <VnLv :label="t('claim.ticketId')">
<template #value> <template #value>
<span class="link"> <span class="link">
{{ entity.ticketFk }} {{ entity.ticketFk }}
@ -131,7 +129,7 @@ onMounted(async () => {
color="primary" color="primary"
:to="{ name: 'CustomerCard', params: { id: entity.clientFk } }" :to="{ name: 'CustomerCard', params: { id: entity.clientFk } }"
> >
<QTooltip>{{ t('claim.card.customerSummary') }}</QTooltip> <QTooltip>{{ t('claim.customerSummary') }}</QTooltip>
</QBtn> </QBtn>
<QBtn <QBtn
size="md" size="md"
@ -139,7 +137,7 @@ onMounted(async () => {
color="primary" color="primary"
:to="{ name: 'TicketCard', params: { id: entity.ticketFk } }" :to="{ name: 'TicketCard', params: { id: entity.ticketFk } }"
> >
<QTooltip>{{ t('claim.card.claimedTicket') }}</QTooltip> <QTooltip>{{ t('claim.claimedTicket') }}</QTooltip>
</QBtn> </QBtn>
<QBtn <QBtn
size="md" size="md"
@ -147,7 +145,7 @@ onMounted(async () => {
color="primary" color="primary"
:href="salixUrl + 'ticket/' + entity.ticketFk + '/sale-tracking'" :href="salixUrl + 'ticket/' + entity.ticketFk + '/sale-tracking'"
> >
<QTooltip>{{ t('claim.card.saleTracking') }}</QTooltip> <QTooltip>{{ t('claim.saleTracking') }}</QTooltip>
</QBtn> </QBtn>
<QBtn <QBtn
size="md" size="md"
@ -155,7 +153,7 @@ onMounted(async () => {
color="primary" color="primary"
:href="salixUrl + 'ticket/' + entity.ticketFk + '/tracking/index'" :href="salixUrl + 'ticket/' + entity.ticketFk + '/tracking/index'"
> >
<QTooltip>{{ t('claim.card.ticketTracking') }}</QTooltip> <QTooltip>{{ t('claim.ticketTracking') }}</QTooltip>
</QBtn> </QBtn>
</QCardActions> </QCardActions>
</template> </template>

View File

@ -9,7 +9,7 @@ const state = useState();
const user = state.getUser(); const user = state.getUser();
const $props = defineProps({ const $props = defineProps({
id: { type: Number, default: null }, id: { type: [Number, String], default: null },
addNote: { type: Boolean, default: true }, addNote: { type: Boolean, default: true },
}); });
const claimId = computed(() => $props.id || route.params.id); const claimId = computed(() => $props.id || route.params.id);

View File

@ -18,7 +18,7 @@ const claimId = computed(() => router.currentRoute.value.params.id);
const claimDms = ref([ const claimDms = ref([
{ {
dmsFk: 1, dmsFk: claimId,
}, },
]); ]);
const client = ref({}); const client = ref({});
@ -113,7 +113,7 @@ async function create() {
warehouseId: config.value.warehouseFk, warehouseId: config.value.warehouseFk,
companyId: config.value.companyFk, companyId: config.value.companyFk,
dmsTypeId: dmsType.value.id, dmsTypeId: dmsType.value.id,
description: t('claim.photo.fileDescription', { description: t('claim.fileDescription', {
claimId: claimId.value, claimId: claimId.value,
clientName: client.value.name, clientName: client.value.name,
clientId: client.value.id, clientId: client.value.id,
@ -177,7 +177,7 @@ function onDrag() {
> >
<QIcon size="xl" name="file_download" /> <QIcon size="xl" name="file_download" />
<h5> <h5>
{{ t('claim.photo.dragDrop') }} {{ t('claim.dragDrop') }}
</h5> </h5>
</div> </div>
<div <div
@ -188,7 +188,7 @@ function onDrag() {
<QIcon size="xl" name="image"></QIcon> <QIcon size="xl" name="image"></QIcon>
<QIcon size="xl" name="movie"></QIcon> <QIcon size="xl" name="movie"></QIcon>
<h5> <h5>
{{ t('claim.photo.noData') }} {{ t('claim.noData') }}
</h5> </h5>
</div> </div>
<div class="multimediaParent bg-transparent" v-if="claimDms?.length && !dragFile"> <div class="multimediaParent bg-transparent" v-if="claimDms?.length && !dragFile">

View File

@ -1,20 +1,25 @@
<script setup> <script setup>
import axios from 'axios';
import { onMounted, ref, computed } from 'vue'; import { onMounted, ref, computed } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { toDate, toCurrency } from 'src/filters'; import { toDate, toCurrency } from 'src/filters';
import CardSummary from 'components/ui/CardSummary.vue'; import dashIfEmpty from 'src/filters/dashIfEmpty';
import FetchData from 'components/FetchData.vue';
import { getUrl } from 'src/composables/getUrl'; import { getUrl } from 'src/composables/getUrl';
import { useSession } from 'src/composables/useSession'; import { useSession } from 'src/composables/useSession';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import ClaimNotes from 'src/pages/Claim/Card/ClaimNotes.vue';
import VnUserLink from 'src/components/ui/VnUserLink.vue'; import VnUserLink from 'src/components/ui/VnUserLink.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import VnTitle from 'src/components/common/VnTitle.vue'; import VnTitle from 'src/components/common/VnTitle.vue';
import FetchData from 'components/FetchData.vue';
import CardSummary from 'components/ui/CardSummary.vue';
import ClaimSummaryAction from 'src/pages/Claim/Card/ClaimSummaryAction.vue';
import ClaimNotes from 'src/pages/Claim/Card/ClaimNotes.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue'; import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue';
import axios from 'axios'; import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import dashIfEmpty from 'src/filters/dashIfEmpty';
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
@ -34,6 +39,9 @@ const ClaimStates = ref([]);
const claimUrl = ref(); const claimUrl = ref();
const salixUrl = ref(); const salixUrl = ref();
const claimDmsRef = ref(); const claimDmsRef = ref();
const claimDms = ref([]);
const multimediaDialog = ref();
const multimediaSlide = ref();
const claimDmsFilter = ref({ const claimDmsFilter = ref({
include: [ include: [
{ {
@ -42,34 +50,29 @@ const claimDmsFilter = ref({
], ],
}); });
onMounted(async () => {
salixUrl.value = await getUrl('');
claimUrl.value = salixUrl.value + `claim/${entityId.value}/`;
});
const detailsColumns = ref([ const detailsColumns = ref([
{ {
name: 'item', name: 'item',
label: 'claim.summary.item', label: 'claim.item',
field: (row) => row.sale.itemFk, field: (row) => row.sale.itemFk,
sortable: true, sortable: true,
}, },
{ {
name: 'landed', name: 'landed',
label: 'claim.summary.landed', label: 'claim.landed',
field: (row) => row.sale.ticket.landed, field: (row) => row.sale.ticket.landed,
format: (value) => toDate(value), format: (value) => toDate(value),
sortable: true, sortable: true,
}, },
{ {
name: 'quantity', name: 'quantity',
label: 'claim.summary.quantity', label: 'claim.quantity',
field: (row) => row.sale.quantity, field: (row) => row.sale.quantity,
sortable: true, sortable: true,
}, },
{ {
name: 'claimed', name: 'claimed',
label: 'claim.summary.claimed', label: 'claim.claimed',
field: (row) => row.quantity, field: (row) => row.quantity,
sortable: true, sortable: true,
}, },
@ -80,32 +83,38 @@ const detailsColumns = ref([
}, },
{ {
name: 'price', name: 'price',
label: 'claim.summary.price', label: 'claim.price',
field: (row) => row.sale.price, field: (row) => row.sale.price,
sortable: true, sortable: true,
}, },
{ {
name: 'discount', name: 'discount',
label: 'claim.summary.discount', label: 'claim.discount',
field: (row) => row.sale.discount, field: (row) => row.sale.discount,
format: (value) => `${value} %`, format: (value) => `${value} %`,
sortable: true, sortable: true,
}, },
{ {
name: 'total', name: 'total',
label: 'claim.summary.total', label: 'claim.total',
field: ({ sale }) => field: ({ sale }) =>
toCurrency(sale.quantity * sale.price * ((100 - sale.discount) / 100)), toCurrency(sale.quantity * sale.price * ((100 - sale.discount) / 100)),
sortable: true, sortable: true,
}, },
]); ]);
const markerLabels = [
{ value: 1, label: t('claim.company') },
{ value: 5, label: t('claim.person') },
];
const STATE_COLOR = { const STATE_COLOR = {
pending: 'warning', pending: 'warning',
incomplete: 'info', incomplete: 'info',
resolved: 'positive', resolved: 'positive',
canceled: 'negative', canceled: 'negative',
}; };
function stateColor(code) { function stateColor(code) {
return STATE_COLOR[code]; return STATE_COLOR[code];
} }
@ -113,38 +122,40 @@ function stateColor(code) {
const developmentColumns = ref([ const developmentColumns = ref([
{ {
name: 'claimReason', name: 'claimReason',
label: 'claim.summary.reason', label: 'claim.reason',
field: (row) => row.claimReason.description, field: (row) => row.claimReason.description,
sortable: true, sortable: true,
}, },
{ {
name: 'claimResult', name: 'claimResult',
label: 'claim.summary.result', label: 'claim.result',
field: (row) => row.claimResult.description, field: (row) => row.claimResult.description,
sortable: true, sortable: true,
}, },
{ {
name: 'claimResponsible', name: 'claimResponsible',
label: 'claim.summary.responsible', label: 'claim.responsible',
field: (row) => row.claimResponsible.description, field: (row) => row.claimResponsible.description,
sortable: true, sortable: true,
}, },
{ {
name: 'worker', name: 'worker',
label: 'claim.summary.worker', label: 'claim.worker',
field: (row) => row.worker?.user.nickname, field: (row) => row.worker?.user.nickname,
sortable: true, sortable: true,
}, },
{ {
name: 'claimRedelivery', name: 'claimRedelivery',
label: 'claim.summary.redelivery', label: 'claim.redelivery',
field: (row) => row.claimRedelivery.description, field: (row) => row.claimRedelivery.description,
sortable: true, sortable: true,
}, },
]); ]);
const claimDms = ref([]);
const multimediaDialog = ref(); onMounted(async () => {
const multimediaSlide = ref(); salixUrl.value = await getUrl('');
claimUrl.value = salixUrl.value + `claim/${entityId.value}/`;
});
async function getClaimDms() { async function getClaimDms() {
claimDmsFilter.value.where = { claimFk: entityId.value }; claimDmsFilter.value.where = { claimFk: entityId.value };
@ -185,6 +196,7 @@ async function changeState(value) {
:url="`Claims/${entityId}/getSummary`" :url="`Claims/${entityId}/getSummary`"
:entity-id="entityId" :entity-id="entityId"
@on-fetch="getClaimDms" @on-fetch="getClaimDms"
data-key="claimSummary"
> >
<template #header="{ entity: { claim } }"> <template #header="{ entity: { claim } }">
{{ claim.id }} - {{ claim.client.name }} ({{ claim.client.id }}) {{ claim.id }} - {{ claim.client.name }} ({{ claim.client.id }})
@ -199,7 +211,7 @@ async function changeState(value) {
> >
<QList> <QList>
<QVirtualScroll <QVirtualScroll
style="max-height: 300px" class="max-container-height"
:items="ClaimStates" :items="ClaimStates"
separator separator
v-slot="{ item, index }" v-slot="{ item, index }"
@ -220,16 +232,13 @@ async function changeState(value) {
</QBtnDropdown> </QBtnDropdown>
</template> </template>
<template #body="{ entity: { claim, salesClaimed, developments } }"> <template #body="{ entity: { claim, salesClaimed, developments } }">
<QCard class="vn-one"> <QCard class="vn-one" v-if="$route.name != 'ClaimSummary'">
<VnTitle <VnTitle
:url="`#/claim/${entityId}/basic-data`" :url="`#/claim/${entityId}/basic-data`"
:text="t('claim.pageTitles.basicData')" :text="t('globals.pageTitles.basicData')"
/> />
<VnLv <VnLv :label="t('claim.created')" :value="toDate(claim.created)" />
:label="t('claim.summary.created')" <VnLv :label="t('claim.state')">
:value="toDate(claim.created)"
/>
<VnLv :label="t('claim.summary.state')">
<template #value> <template #value>
<QChip :color="stateColor(claim.claimState.code)" dense> <QChip :color="stateColor(claim.claimState.code)" dense>
{{ claim.claimState.description }} {{ claim.claimState.description }}
@ -244,7 +253,7 @@ async function changeState(value) {
/> />
</template> </template>
</VnLv> </VnLv>
<VnLv :label="t('claim.summary.attendedBy')"> <VnLv :label="t('claim.attendedBy')">
<template #value> <template #value>
<VnUserLink <VnUserLink
:name="claim.worker?.user?.nickname" :name="claim.worker?.user?.nickname"
@ -252,7 +261,7 @@ async function changeState(value) {
/> />
</template> </template>
</VnLv> </VnLv>
<VnLv :label="t('claim.summary.customer')"> <VnLv :label="t('claim.customer')">
<template #value> <template #value>
<span class="link cursor-pointer"> <span class="link cursor-pointer">
{{ claim.client?.name }} {{ claim.client?.name }}
@ -261,27 +270,63 @@ async function changeState(value) {
</template> </template>
</VnLv> </VnLv>
<VnLv <VnLv
:label="t('claim.basicData.pickup')" :label="t('claim.pickup')"
:value="`${dashIfEmpty(claim.pickup)}`" :value="`${dashIfEmpty(claim.pickup)}`"
/> />
</QCard> </QCard>
<QCard class="vn-three"> <QCard class="vn-two">
<VnTitle <VnTitle :url="`#/claim/${entityId}/notes`" :text="t('claim.notes')" />
:url="`#/claim/${entityId}/notes`"
:text="t('claim.summary.notes')"
/>
<ClaimNotes <ClaimNotes
:id="entityId" :id="entityId"
:add-note="false" :add-note="false"
style="max-height: 300px" class="max-container-height"
order="created ASC" order="created ASC"
/> />
</QCard> </QCard>
<QCard class="vn-two" v-if="salesClaimed.length > 0"> <QCard class="vn-two" v-if="claimDms?.length">
<VnTitle <VnTitle :url="`#/claim/${entityId}/photos`" :text="t('claim.photos')" />
:url="`#/claim/${entityId}/lines`" <div class="container max-container-height" style="overflow: auto">
:text="t('claim.summary.details')" <div
/> class="multimedia-container"
v-for="(media, index) of claimDms"
:key="index"
>
<div class="relative-position">
<QIcon
name="play_circle"
color="primary"
size="xl"
class="absolute-center zindex"
v-if="media.isVideo"
@click.stop="openDialog(media.dmsFk)"
>
<QTooltip>Video</QTooltip>
</QIcon>
<QCard
class="multimedia relative-position"
style="max-height: 128px"
>
<QImg
:src="media.url"
class="rounded-borders cursor-pointer fit"
@click="openDialog(media.dmsFk)"
v-if="!media.isVideo"
>
</QImg>
<video
:src="media.url"
class="rounded-borders cursor-pointer fit"
muted="muted"
v-if="media.isVideo"
@click="openDialog(media.dmsFk)"
/>
</QCard>
</div>
</div>
</div>
</QCard>
<QCard class="vn-max" v-if="salesClaimed.length > 0">
<VnTitle :url="`#/claim/${entityId}/lines`" :text="t('claim.details')" />
<QTable <QTable
:columns="detailsColumns" :columns="detailsColumns"
:rows="salesClaimed" :rows="salesClaimed"
@ -319,53 +364,8 @@ async function changeState(value) {
</template> </template>
</QTable> </QTable>
</QCard> </QCard>
<QCard class="vn-two" v-if="claimDms.length > 0"> <QCard class="vn-max" v-if="developments.length > 0">
<VnTitle <VnTitle :url="claimUrl + 'development'" :text="t('claim.development')" />
:url="`#/claim/${entityId}/photos`"
:text="t('claim.summary.photos')"
/>
<div class="container">
<div
class="multimedia-container"
v-for="(media, index) of claimDms"
:key="index"
>
<div class="relative-position">
<QIcon
name="play_circle"
color="primary"
size="xl"
class="absolute-center zindex"
v-if="media.isVideo"
@click.stop="openDialog(media.dmsFk)"
>
<QTooltip>Video</QTooltip>
</QIcon>
<QCard class="multimedia relative-position">
<QImg
:src="media.url"
class="rounded-borders cursor-pointer fit"
@click="openDialog(media.dmsFk)"
v-if="!media.isVideo"
>
</QImg>
<video
:src="media.url"
class="rounded-borders cursor-pointer fit"
muted="muted"
v-if="media.isVideo"
@click="openDialog(media.dmsFk)"
/>
</QCard>
</div>
</div>
</div>
</QCard>
<QCard class="vn-two" v-if="developments.length > 0">
<VnTitle
:url="claimUrl + 'development'"
:text="t('claim.summary.development')"
/>
<QTable <QTable
:columns="developmentColumns" :columns="developmentColumns"
:rows="developments" :rows="developments"
@ -381,27 +381,31 @@ async function changeState(value) {
</QTh> </QTh>
</QTr> </QTr>
</template> </template>
<template #body-cell-worker="props">
<QTd :props="props" class="link">
{{ props.value }}
<WorkerDescriptorProxy :id="props.row.worker.id" />
</QTd>
</template>
</QTable> </QTable>
</QCard> </QCard>
<QCard class="vn-max"> <QCard class="vn-max">
<VnTitle :url="claimUrl + 'action'" :text="t('claim.summary.actions')" /> <VnTitle :url="claimUrl + 'action'" :text="t('claim.actions')" />
<div id="slider-container" class="q-px-xl q-py-md"> <div id="slider-container" class="q-px-xl q-py-md">
<QSlider <QSlider
v-model="claim.responsibility" v-model="claim.responsibility"
label label
:label-value="t('claim.summary.responsibility')" :label-value="t('claim.responsibility')"
label-always label-always
color="var()" color="var()"
markers markers
:marker-labels="[ :marker-labels="markerLabels"
{ value: 1, label: t('claim.summary.company') },
{ value: 5, label: t('claim.summary.person') },
]"
:min="1" :min="1"
:max="5" :max="5"
readonly readonly
/> />
</div> </div>
<ClaimSummaryAction :id="entityId" />
</QCard> </QCard>
<QDialog <QDialog
v-model="multimediaDialog" v-model="multimediaDialog"
@ -457,7 +461,7 @@ async function changeState(value) {
gap: 15px; gap: 15px;
} }
.multimedia-container { .multimedia-container {
flex: 1 0 21%; flex: 0 0 128px;
} }
.multimedia { .multimedia {
transition: all 0.5s; transition: all 0.5s;
@ -490,4 +494,8 @@ async function changeState(value) {
.change-state { .change-state {
width: 10%; width: 10%;
} }
.max-container-height {
max-height: 300px;
}
</style> </style>

View File

@ -0,0 +1,97 @@
<script setup>
import { useI18n } from 'vue-i18n';
import { toDate, toPercentage } from 'filters/index';
import VnTable from 'src/components/VnTable/VnTable.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import TicketDescriptorProxy from 'src/pages/Ticket/Card/TicketDescriptorProxy.vue';
const { t } = useI18n();
const $props = defineProps({
id: {
type: [Number, String],
required: true,
},
});
const columns = [
{
name: 'itemFk',
label: t('Id item'),
columnFilter: false,
align: 'left',
},
{
name: 'ticketFk',
label: t('Ticket'),
columnFilter: false,
align: 'left',
},
{
name: 'claimDestinationFk',
label: t('Destination'),
columnFilter: false,
align: 'left',
},
{
name: 'landed',
label: t('Landed'),
format: (row) => toDate(row.landed),
align: 'left',
},
{
name: 'quantity',
label: t('Quantity'),
align: 'left',
},
{
name: 'concept',
label: t('Description'),
align: 'left',
},
{
name: 'price',
label: t('Price'),
align: 'left',
},
{
name: 'discount',
label: t('Discount'),
format: ({ discount }) => toPercentage(discount / 100),
align: 'left',
},
{
name: 'total',
label: t('Total'),
align: 'left',
},
];
</script>
<template>
<VnTable
data-key="ClaimEndsTable"
url="ClaimEnds/filter"
:right-search="false"
:column-search="false"
:disable-option="{ card: true, table: true }"
search-url="actions"
:filter="{ where: { claimFk: $props.id } }"
:columns="columns"
:limit="0"
:without-header="true"
auto-load
>
<template #column-itemFk="{ row }">
<span class="link">
{{ row.itemFk }}
<ItemDescriptorProxy :id="row.itemFk" />
</span>
</template>
<template #column-ticketFk="{ row }">
<span class="link">
{{ row.ticketFk }}
<TicketDescriptorProxy :id="row.ticketFk" />
</span>
</template>
</VnTable>
</template>

View File

@ -16,19 +16,14 @@ const props = defineProps({
}, },
}); });
const workers = ref(); const states = ref([]);
const states = ref();
defineExpose({ states });
</script> </script>
<template> <template>
<FetchData url="ClaimStates" @on-fetch="(data) => (states = data)" auto-load /> <FetchData url="ClaimStates" @on-fetch="(data) => (states = data)" auto-load />
<FetchData <VnFilterPanel :data-key="props.dataKey" :search-button="true" search-url="table">
url="Workers/activeWithInheritedRole"
:filter="{ where: { role: 'salesPerson' } }"
@on-fetch="(data) => (workers = data)"
auto-load
/>
<VnFilterPanel :data-key="props.dataKey" :search-button="true">
<template #tags="{ tag, formatFn }"> <template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs"> <div class="q-gutter-x-xs">
<strong>{{ t(`params.${tag.label}`) }}: </strong> <strong>{{ t(`params.${tag.label}`) }}: </strong>
@ -36,156 +31,110 @@ const states = ref();
</div> </div>
</template> </template>
<template #body="{ params, searchFn }"> <template #body="{ params, searchFn }">
<QItem class="q-my-sm"> <div class="q-pa-sm q-gutter-y-sm">
<QItemSection> <VnInput
<VnInput :label="t('claim.customerId')"
:label="t('Customer ID')" v-model="params.clientFk"
v-model="params.clientFk" lazy-rules
lazy-rules is-outlined
is-outlined >
> <template #prepend> <QIcon name="badge" size="xs" /></template>
<template #prepend> </VnInput>
<QIcon name="badge" size="xs"></QIcon> </template <VnInput
></VnInput> :label="t('Client Name')"
</QItemSection> v-model="params.clientName"
</QItem> lazy-rules
<QItem class="q-mb-sm"> is-outlined
<QItemSection> />
<VnInput <VnSelect
:label="t('Client Name')" :label="t('Salesperson')"
v-model="params.clientName" v-model="params.salesPersonFk"
lazy-rules @update:model-value="searchFn()"
is-outlined url="Workers/activeWithInheritedRole"
/> :filter="{ where: { role: 'salesPerson' } }"
</QItemSection> :use-like="false"
</QItem> option-value="id"
<QItem class="q-mb-sm"> option-label="name"
<QItemSection v-if="!workers"> option-filter="firstName"
<QSkeleton type="QInput" class="full-width" /> dense
</QItemSection> outlined
<QItemSection v-if="workers"> rounded
<VnSelect />
:label="t('Salesperson')" <VnSelect
v-model="params.salesPersonFk" :label="t('claim.attendedBy')"
@update:model-value="searchFn()" v-model="params.attenderFk"
:options="workers" @update:model-value="searchFn()"
option-value="id" url="Workers/activeWithInheritedRole"
option-label="name" :filter="{ where: { role: 'salesPerson' } }"
emit-value :use-like="false"
map-options option-value="id"
use-input option-label="name"
hide-selected option-filter="firstName"
dense dense
outlined outlined
rounded rounded
:input-debounce="0" />
/> <VnSelect
</QItemSection> :label="t('claim.state')"
</QItem> v-model="params.claimStateFk"
<QItem class="q-mb-sm"> @update:model-value="searchFn()"
<QItemSection v-if="!workers"> :options="states"
<QSkeleton type="QInput" class="full-width" /> option-value="id"
</QItemSection> option-label="description"
<QItemSection v-if="workers"> dense
<VnSelect outlined
:label="t('Attender')" rounded
v-model="params.attenderFk" />
@update:model-value="searchFn()" <VnInputDate
:options="workers" v-model="params.created"
option-value="id" @update:model-value="searchFn()"
option-label="name" :label="t('claim.created')"
emit-value outlined
map-options rounded
use-input dense
hide-selected />
dense <VnSelect
outlined :label="t('Item')"
rounded v-model="params.itemFk"
:input-debounce="0" @update:model-value="searchFn()"
/> url="Items/withName"
</QItemSection> option-value="id"
</QItem> option-label="name"
<QItem class="q-mb-sm"> sort-by="id DESC"
<QItemSection v-if="!workers"> outlined
<QSkeleton type="QInput" class="full-width" /> rounded
</QItemSection> dense
<QItemSection v-if="workers"> >
<VnSelect <template #option="scope">
:label="t('Responsible')" <QItem v-bind="scope.itemProps">
v-model="params.claimResponsibleFk" <QItemSection>
@update:model-value="searchFn()" <QItemLabel> #{{ scope.opt?.id }} </QItemLabel>
:options="workers" <QItemLabel caption>{{ scope.opt?.name }}</QItemLabel>
option-value="id" </QItemSection>
option-label="name" </QItem>
emit-value </template>
map-options </VnSelect>
use-input <VnSelect
hide-selected :label="t('claim.responsible')"
dense v-model="params.claimResponsibleFk"
outlined @update:model-value="searchFn()"
rounded url="Workers/activeWithInheritedRole"
:input-debounce="0" :filter="{ where: { role: 'salesPerson' } }"
/> :use-like="false"
</QItemSection> option-value="id"
</QItem> option-label="name"
<QItem class="q-mb-sm"> option-filter="firstName"
<QItemSection v-if="!states"> dense
<QSkeleton type="QInput" class="full-width" /> outlined
</QItemSection> rounded
<QItemSection v-if="states"> />
<VnSelect <QCheckbox
:label="t('State')" v-model="params.myTeam"
v-model="params.claimStateFk" :label="t('params.myTeam')"
@update:model-value="searchFn()" @update:model-value="searchFn()"
:options="states" toggle-indeterminate
option-value="id" />
option-label="description" </div>
emit-value
map-options
hide-selected
dense
outlined
rounded
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QCheckbox
v-model="params.myTeam"
:label="t('myTeam')"
toggle-indeterminate
/>
</QItemSection>
</QItem>
<QSeparator />
<QExpansionItem :label="t('More options')" expand-separator>
<!-- <QItem>
<QItemSection>
<qSelect
:label="t('Item')"
v-model="params.itemFk"
:options="items"
:loading="loading"
@filter="filterFn"
@virtual-scroll="onScroll"
option-value="id"
option-label="name"
emit-value
map-options
/>
</QItemSection>
</QItem> -->
<QItem>
<QItemSection>
<VnInputDate
v-model="params.created"
:label="t('Created')"
is-outlined
/>
</QItemSection>
</QItem>
</QExpansionItem>
</template> </template>
</VnFilterPanel> </VnFilterPanel>
</template> </template>
@ -201,7 +150,8 @@ en:
claimResponsibleFk: Responsible claimResponsibleFk: Responsible
claimStateFk: State claimStateFk: State
created: Created created: Created
myTeam: My team myTeam: My team
itemFk: Item
es: es:
params: params:
search: Contiene search: Contiene
@ -212,14 +162,9 @@ es:
claimResponsibleFk: Responsable claimResponsibleFk: Responsable
claimStateFk: Estado claimStateFk: Estado
created: Creada created: Creada
Customer ID: ID cliente myTeam: Mi equipo
itemFk: Artículo
Client Name: Nombre del cliente Client Name: Nombre del cliente
Salesperson: Comercial Salesperson: Comercial
Attender: Asistente
Responsible: Responsable
State: Estado
Item: Artículo Item: Artículo
Created: Creada
More options: Más opciones
myTeam: Mi equipo
</i18n> </i18n>

View File

@ -1,38 +1,114 @@
<script setup> <script setup>
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import { toDate } from 'filters/index'; import { toDate } from 'filters/index';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
import VnSearchbar from 'components/ui/VnSearchbar.vue'; import VnSearchbar from 'components/ui/VnSearchbar.vue';
import ClaimFilter from './ClaimFilter.vue'; import ClaimFilter from './ClaimFilter.vue';
import VnLv from 'src/components/ui/VnLv.vue';
import CardList from 'src/components/ui/CardList.vue';
import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue'; import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue';
import VnUserLink from 'src/components/ui/VnUserLink.vue'; import VnUserLink from 'src/components/ui/VnUserLink.vue';
import ClaimSummary from './Card/ClaimSummary.vue'; import ClaimSummary from './Card/ClaimSummary.vue';
import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import RightMenu from 'src/components/common/RightMenu.vue'; import RightMenu from 'src/components/common/RightMenu.vue';
import VnTable from 'src/components/VnTable/VnTable.vue';
const router = useRouter();
const { t } = useI18n(); const { t } = useI18n();
const { viewSummary } = useSummaryDialog(); const { viewSummary } = useSummaryDialog();
const claimFilterRef = ref();
const columns = computed(() => [
{
align: 'left',
name: 'id',
label: t('customer.extendedList.tableVisibleColumns.id'),
chip: {
condition: () => true,
},
isId: true,
},
{
align: 'left',
label: t('customer.extendedList.tableVisibleColumns.name'),
name: 'clientName',
isTitle: true,
visible: false,
},
{
align: 'left',
label: t('claim.customer'),
name: 'clientFk',
cardVisible: true,
columnFilter: {
component: 'select',
attrs: {
url: 'Clients',
fields: ['id', 'name'],
},
},
class: 'expand',
},
{
align: 'left',
label: t('claim.attendedBy'),
name: 'attendedBy',
cardVisible: true,
columnFilter: {
component: 'select',
attrs: {
url: 'Workers/activeWithInheritedRole',
fields: ['id', 'name'],
where: { role: 'salesPerson' },
useLike: false,
optionValue: 'id',
optionLabel: 'name',
optionFilter: 'firstName',
},
},
},
{
align: 'left',
label: t('claim.created'),
name: 'created',
format: ({ created }) => toDate(created),
cardVisible: true,
columnFilter: {
component: 'date',
},
},
{
align: 'left',
label: t('claim.state'),
name: 'stateCode',
chip: {
condition: () => true,
color: ({ stateCode }) => STATE_COLOR[stateCode] ?? 'bg-grey',
},
columnFilter: {
name: 'claimStateFk',
component: 'select',
attrs: {
options: claimFilterRef.value?.states,
optionLabel: 'description',
},
},
},
{
align: 'right',
name: 'tableActions',
actions: [
{
title: t('Client ticket list'),
icon: 'preview',
action: (row) => viewSummary(row.id, ClaimSummary),
},
],
},
]);
const STATE_COLOR = { const STATE_COLOR = {
pending: 'warning', pending: 'bg-warning',
managed: 'info', managed: 'bg-info',
resolved: 'positive', resolved: 'bg-positive',
}; };
function getApiUrl() {
return new URL(window.location).origin;
}
function stateColor(code) {
return STATE_COLOR[code];
}
function navigate(event, id) {
if (event.ctrlKey || event.metaKey)
return window.open(`${getApiUrl()}/#/claim/${id}/summary`);
router.push({ path: `/claim/${id}` });
}
</script> </script>
<template> <template>
@ -43,85 +119,39 @@ function navigate(event, id) {
/> />
<RightMenu> <RightMenu>
<template #right-panel> <template #right-panel>
<ClaimFilter data-key="ClaimList" /> <ClaimFilter data-key="ClaimList" ref="claimFilterRef" />
</template> </template>
</RightMenu> </RightMenu>
<QPage class="column items-center q-pa-md"> <VnTable
<div class="vn-card-list"> data-key="ClaimList"
<VnPaginate url="Claims/filter"
data-key="ClaimList" :order="['priority ASC', 'created DESC']"
url="Claims/filter" :columns="columns"
:order="['priority ASC', 'created DESC']" redirect="claim"
auto-load :right-search="false"
> auto-load
<template #body="{ rows }"> >
<CardList <template #column-clientFk="{ row }">
:id="row.id" <span class="link" @click.stop>
:key="row.id" {{ row.clientName }}
:title="row.clientName" <CustomerDescriptorProxy :id="row.clientFk" />
@click="navigate($event, row.id)" </span>
v-for="row of rows" </template>
> <template #column-attendedBy="{ row }">
<template #list-items> <span @click.stop>
<VnLv :label="t('claim.list.customer')"> <VnUserLink :name="row.workerName" :worker-id="row.workerFk" />
<template #value> </span>
<span class="link" @click.stop> </template>
{{ row.clientName }} </VnTable>
<CustomerDescriptorProxy :id="row.clientFk" />
</span>
</template>
</VnLv>
<VnLv :label="t('claim.list.assignedTo')">
<template #value>
<span @click.stop>
<VnUserLink
:name="row.workerName"
:worker-id="row.workerFk"
/>
</span>
</template>
</VnLv>
<VnLv
:label="t('claim.list.created')"
:value="toDate(row.created)"
/>
<VnLv :label="t('claim.list.state')">
<template #value>
<QBadge
text-color="black"
:color="stateColor(row.stateCode)"
dense
>
{{ row.stateDescription }}
</QBadge>
</template>
</VnLv>
</template>
<template #actions>
<QBtn
:label="t('globals.description')"
@click.stop
outline
style="margin-top: 15px"
>
<CustomerDescriptorProxy :id="row.clientFk" />
</QBtn>
<QBtn
:label="t('components.smartCard.openSummary')"
@click.stop="viewSummary(row.id, ClaimSummary)"
color="primary"
style="margin-top: 15px"
/>
</template>
</CardList>
</template>
</VnPaginate>
</div>
</QPage>
</template> </template>
<i18n> <i18n>
es: es:
Search claim: Buscar reclamación Search claim: Buscar reclamación
You can search by claim id or customer name: Puedes buscar por id de la reclamación o nombre del cliente You can search by claim id or customer name: Puedes buscar por id de la reclamación o nombre del cliente
params:
stateCode: Estado
en:
params:
stateCode: State
</i18n> </i18n>

View File

@ -0,0 +1,46 @@
claim:
customer: Customer
code: Code
records: records
claimId: Claim ID
attendedBy: Attended by
ticketId: Ticket ID
customerSummary: Customer summary
claimedTicket: Claimed ticket
saleTracking: Sale tracking
ticketTracking: Ticket tracking
commercial: Commercial
province: Province
zone: Zone
customerId: client ID
assignedTo: Assigned
created: Created
details: Details
item: Item
landed: Landed
quantity: Quantity
claimed: Claimed
price: Price
discount: Discount
total: Total
actions: Actions
responsibility: Responsibility
company: Company
person: Employee/Customer
notes: Notes
photos: Photos
development: Development
reason: Reason
result: Result
responsible: Responsible
worker: Worker
redelivery: Redelivery
changeState: Change state
state: State
pickup: Pick up
null: No
agency: Agency
delivery: Delivery
fileDescription: 'Claim id {claimId} from client {clientName} id {clientId}'
noData: 'There are no images/videos, click here or drag and drop the file'
dragDrop: Drag and drop it here

View File

@ -1,2 +1,48 @@
Search claim: Buscar reclamación Search claim: Buscar reclamación
You can search by claim id or customer name: Puedes buscar por id de la reclamación o nombre del cliente You can search by claim id or customer name: Puedes buscar por id de la reclamación o nombre del cliente
claim:
customer: Cliente
code: Código
records: Registros
claimId: ID de reclamación
attendedBy: Atendido por
ticketId: ID de ticket
customerSummary: Resumen del cliente
claimedTicket: Ticket reclamado
saleTracking: Seguimiento de ventas
ticketTracking: Seguimiento de tickets
commercial: Comercial
province: Provincia
zone: Zona
customerId: ID de cliente
assignedTo: Asignado a
created: Creado
details: Detalles
item: Artículo
landed: Llegado
quantity: Cantidad
claimed: Reclamado
price: Precio
discount: Descuento
total: Total
actions: Acciones
responsibility: Responsabilidad
company: Empresa
person: Empleado/Cliente
notes: Notas
photos: Fotos
development: Trazabilidad
reason: Razón
result: Resultado
responsible: Responsable
worker: Trabajador
redelivery: Reentrega
changeState: Cambiar estado
state: Estado
pickup: Recoger
null: No
agency: Agencia
delivery: Entrega
fileDescription: 'ID de reclamación {claimId} del cliente {clientName} con ID {clientId}'
noData: 'No hay imágenes/videos, haz clic aquí o arrastra y suelta el archivo'
dragDrop: Arrastra y suelta aquí

View File

@ -3,16 +3,14 @@ import { ref } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useSession } from 'src/composables/useSession';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import FormModel from 'components/FormModel.vue'; import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import VnImg from 'src/components/ui/VnImg.vue';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const { getTokenMultimedia } = useSession();
const token = getTokenMultimedia();
const workers = ref([]); const workers = ref([]);
const workersCopy = ref([]); const workersCopy = ref([]);
@ -71,7 +69,7 @@ const filterOptions = {
<template #form="{ data, validate, filter }"> <template #form="{ data, validate, filter }">
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">
<VnInput <VnInput
:label="t('Comercial name')" :label="t('globals.name')"
:rules="validate('client.socialName')" :rules="validate('client.socialName')"
autofocus autofocus
clearable clearable
@ -143,10 +141,11 @@ const filterOptions = {
> >
<template #prepend> <template #prepend>
<QAvatar color="orange"> <QAvatar color="orange">
<QImg <VnImg
:src="`/api/Images/user/160x160/${data.salesPersonFk}/download?access_token=${token}`"
spinner-color="white"
v-if="data.salesPersonFk" v-if="data.salesPersonFk"
:id="user.id"
collection="user"
spinner-color="white"
/> />
</QAvatar> </QAvatar>
</template> </template>

View File

@ -32,7 +32,7 @@ const entityId = computed(() => {
}); });
const data = ref(useCardDescription()); const data = ref(useCardDescription());
const setData = (entity) => (data.value = useCardDescription(entity.name, entity.id)); const setData = (entity) => (data.value = useCardDescription(entity?.name, entity?.id));
</script> </script>
<template> <template>
@ -45,20 +45,6 @@ const setData = (entity) => (data.value = useCardDescription(entity.name, entity
:summary="$props.summary" :summary="$props.summary"
data-key="customerData" data-key="customerData"
> >
<template #header-extra-action>
<QBtn
round
flat
size="sm"
icon="vn:Person"
color="white"
:to="{ name: 'CustomerList' }"
>
<QTooltip>
{{ t('Go to module index') }}
</QTooltip>
</QBtn>
</template>
<template #menu="{ entity }"> <template #menu="{ entity }">
<CustomerDescriptorMenu :customer="entity" /> <CustomerDescriptorMenu :customer="entity" />
</template> </template>

View File

@ -147,6 +147,7 @@ function handleLocation(data, location) {
:label="t('Electronic invoice')" :label="t('Electronic invoice')"
v-model="data.hasElectronicInvoice" v-model="data.hasElectronicInvoice"
/> />
<QCheckbox :label="t('Daily invoice')" v-model="data.hasDailyInvoice" />
</VnRow> </VnRow>
</template> </template>
</FormModel> </FormModel>
@ -176,6 +177,7 @@ es:
onlyLetters: Sólo se pueden usar letras, números y espacios onlyLetters: Sólo se pueden usar letras, números y espacios
whenActivatingIt: Al activarlo, no informar el código del país en el campo nif whenActivatingIt: Al activarlo, no informar el código del país en el campo nif
inOrderToInvoice: Para facturar no se consulta este campo, sino el RE de consignatario. Al modificar este campo si no esta marcada la casilla Facturar por consignatario, se propagará automaticamente el cambio a todos lo consignatarios, en caso contrario preguntará al usuario si quiere o no propagar inOrderToInvoice: Para facturar no se consulta este campo, sino el RE de consignatario. Al modificar este campo si no esta marcada la casilla Facturar por consignatario, se propagará automaticamente el cambio a todos lo consignatarios, en caso contrario preguntará al usuario si quiere o no propagar
Daily invoice: Facturación diaria
en: en:
onlyLetters: Only letters, numbers and spaces can be used onlyLetters: Only letters, numbers and spaces can be used
whenActivatingIt: When activating it, do not enter the country code in the ID field whenActivatingIt: When activating it, do not enter the country code in the ID field

View File

@ -13,7 +13,7 @@ const { t } = useI18n();
const route = useRoute(); const route = useRoute();
const stateStore = computed(() => useStateStore()); const stateStore = computed(() => useStateStore());
const rows = ref([]); const rows = ref([]);
const totalAmount = ref(0); const totalAmount = ref();
const filter = { const filter = {
include: [ include: [
@ -75,7 +75,7 @@ const columns = computed(() => [
}, },
{ {
align: 'left', align: 'left',
field: (value) => value.user.name, field: (value) => value?.user?.name,
label: t('Created by'), label: t('Created by'),
name: 'createdBy', name: 'createdBy',
}, },
@ -87,7 +87,7 @@ const columns = computed(() => [
}, },
{ {
align: 'left', align: 'left',
field: (value) => value.greugeType.name, field: (value) => value?.greugeType?.name,
label: t('Type'), label: t('Type'),
name: 'type', name: 'type',
}, },
@ -108,26 +108,9 @@ const setRows = (data) => {
<template> <template>
<FetchData :filter="filter" @on-fetch="setRows" auto-load url="greuges" /> <FetchData :filter="filter" @on-fetch="setRows" auto-load url="greuges" />
<template v-if="stateStore.isHeaderMounted()">
<Teleport to="#actions-append">
<div class="row q-gutter-x-sm">
<QBtn
flat
@click="stateStore.toggleRightDrawer()"
round
dense
icon="menu"
>
<QTooltip bottom anchor="bottom right">
{{ t('globals.collapseMenu') }}
</QTooltip>
</QBtn>
</div>
</Teleport>
</template>
<QDrawer v-model="stateStore.rightDrawer" side="right" :width="300" show-if-above> <QDrawer v-model="stateStore.rightDrawer" side="right" :width="300" show-if-above>
<QCard class="full-width q-pa-sm"> <QCard class="full-width q-pa-sm">
<h6 class="flex justify-end q-my-lg q-pr-lg" v-if="totalAmount"> <h6 class="flex justify-end q-my-lg q-pr-lg" v-if="totalAmount !== undefined">
<span class="color-vn-label q-mr-md">{{ t('Total') }}:</span> <span class="color-vn-label q-mr-md">{{ t('Total') }}:</span>
{{ toCurrency(totalAmount) }} {{ toCurrency(totalAmount) }}
</h6> </h6>

View File

@ -61,7 +61,11 @@ const creditWarning = computed(() => {
</script> </script>
<template> <template>
<CardSummary ref="summary" :url="`Clients/${entityId}/summary`"> <CardSummary
ref="summary"
:url="`Clients/${entityId}/summary`"
data-key="CustomerSummary"
>
<template #body="{ entity }"> <template #body="{ entity }">
<QCard class="vn-one"> <QCard class="vn-one">
<VnTitle <VnTitle
@ -69,7 +73,7 @@ const creditWarning = computed(() => {
:text="t('customer.summary.basicData')" :text="t('customer.summary.basicData')"
/> />
<VnLv :label="t('customer.summary.customerId')" :value="entity.id" /> <VnLv :label="t('customer.summary.customerId')" :value="entity.id" />
<VnLv :label="t('customer.summary.name')" :value="entity.name" /> <VnLv :label="t('globals.name')" :value="entity.name" />
<VnLv :label="t('customer.summary.contact')" :value="entity.contact" /> <VnLv :label="t('customer.summary.contact')" :value="entity.contact" />
<VnLv :value="entity.phone"> <VnLv :value="entity.phone">
<template #label> <template #label>

View File

@ -38,10 +38,13 @@ const columns = computed(() => [
}, },
{ {
align: 'left', align: 'left',
label: t('customer.extendedList.tableVisibleColumns.name'), label: t('globals.name'),
name: 'name', name: 'name',
isTitle: true, isTitle: true,
create: true, create: true,
columnField: {
class: 'expand',
},
}, },
{ {
align: 'left', align: 'left',
@ -49,6 +52,9 @@ const columns = computed(() => [
label: t('customer.extendedList.tableVisibleColumns.socialName'), label: t('customer.extendedList.tableVisibleColumns.socialName'),
isTitle: true, isTitle: true,
create: true, create: true,
columnField: {
class: 'expand',
},
}, },
{ {
align: 'left', align: 'left',
@ -65,6 +71,8 @@ const columns = computed(() => [
url: 'Workers/activeWithInheritedRole', url: 'Workers/activeWithInheritedRole',
fields: ['id', 'name'], fields: ['id', 'name'],
where: { role: 'salesPerson' }, where: { role: 'salesPerson' },
optionFilter: 'firstName',
useLike: false,
}, },
create: true, create: true,
columnField: { columnField: {
@ -76,8 +84,8 @@ const columns = computed(() => [
align: 'left', align: 'left',
label: t('customer.extendedList.tableVisibleColumns.credit'), label: t('customer.extendedList.tableVisibleColumns.credit'),
name: 'credit', name: 'credit',
component: 'number',
columnFilter: { columnFilter: {
component: 'number',
inWhere: true, inWhere: true,
}, },
}, },
@ -85,8 +93,8 @@ const columns = computed(() => [
align: 'left', align: 'left',
label: t('customer.extendedList.tableVisibleColumns.creditInsurance'), label: t('customer.extendedList.tableVisibleColumns.creditInsurance'),
name: 'creditInsurance', name: 'creditInsurance',
component: 'number',
columnFilter: { columnFilter: {
component: 'number',
inWhere: true, inWhere: true,
}, },
}, },
@ -128,6 +136,9 @@ const columns = computed(() => [
columnFilter: { columnFilter: {
inWhere: true, inWhere: true,
}, },
columnField: {
class: 'expand',
},
}, },
{ {
align: 'left', align: 'left',
@ -177,8 +188,8 @@ const columns = computed(() => [
label: t('customer.extendedList.tableVisibleColumns.created'), label: t('customer.extendedList.tableVisibleColumns.created'),
name: 'created', name: 'created',
format: ({ created }) => toDate(created), format: ({ created }) => toDate(created),
component: 'date',
columnFilter: { columnFilter: {
component: 'date',
alias: 'c', alias: 'c',
inWhere: true, inWhere: true,
}, },
@ -403,7 +414,6 @@ function handleLocation(data, location) {
}" }"
order="id DESC" order="id DESC"
:columns="columns" :columns="columns"
default-mode="table"
redirect="customer" redirect="customer"
auto-load auto-load
> >

View File

@ -91,7 +91,6 @@ const tableColumnComponents = {
props: (prop) => ({ props: (prop) => ({
disable: true, disable: true,
'model-value': prop.value, 'model-value': prop.value,
class: 'disabled-checkbox',
}), }),
event: () => {}, event: () => {},
}, },

View File

@ -119,7 +119,7 @@ const departments = ref();
emit-value emit-value
hide-selected hide-selected
map-options map-options
option-label="country" option-label="name"
option-value="id" option-value="id"
outlined outlined
rounded rounded

View File

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

View File

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

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref, computed, watch } from 'vue'; import { ref, computed, watch, onMounted } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@ -10,6 +10,7 @@ import useCardDescription from 'src/composables/useCardDescription';
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
import { toDate } from 'src/filters'; import { toDate } from 'src/filters';
import { usePrintService } from 'composables/usePrintService'; import { usePrintService } from 'composables/usePrintService';
import { getUrl } from 'src/composables/getUrl';
const $props = defineProps({ const $props = defineProps({
id: { id: {
@ -17,10 +18,6 @@ const $props = defineProps({
required: false, required: false,
default: null, default: null,
}, },
summary: {
type: Object,
default: null,
},
}); });
const route = useRoute(); const route = useRoute();
@ -28,6 +25,7 @@ const { t } = useI18n();
const { openReport } = usePrintService(); const { openReport } = usePrintService();
const state = useState(); const state = useState();
const entryDescriptorRef = ref(null); const entryDescriptorRef = ref(null);
const url = ref();
const entryFilter = { const entryFilter = {
include: [ include: [
@ -69,10 +67,13 @@ const entryFilter = {
const entityId = computed(() => { const entityId = computed(() => {
return $props.id || route.params.id; return $props.id || route.params.id;
}); });
onMounted(async () => {
url.value = await getUrl('');
});
const data = ref(useCardDescription()); const data = ref(useCardDescription());
const setData = (entity) => const setData = (entity) =>
(data.value = useCardDescription(entity.supplier.nickname, entity.id)); (data.value = useCardDescription(entity.supplier?.nickname, entity.id));
const currentEntry = computed(() => state.get('entry')); const currentEntry = computed(() => state.get('entry'));
@ -114,6 +115,7 @@ watch;
:subtitle="data.subtitle" :subtitle="data.subtitle"
@on-fetch="setData" @on-fetch="setData"
data-key="entry" data-key="entry"
:summary="$attrs"
> >
<template #menu="{ entity }"> <template #menu="{ entity }">
<QItem v-ripple clickable @click="showEntryReport(entity)"> <QItem v-ripple clickable @click="showEntryReport(entity)">

View File

@ -1,5 +1,6 @@
<script setup> <script setup>
import EntryDescriptor from './EntryDescriptor.vue'; import EntryDescriptor from './EntryDescriptor.vue';
import EntrySummary from './EntrySummary.vue';
const $props = defineProps({ const $props = defineProps({
id: { id: {
@ -11,6 +12,6 @@ const $props = defineProps({
<template> <template>
<QPopupProxy> <QPopupProxy>
<EntryDescriptor v-if="$props.id" :id="$props.id" /> <EntryDescriptor v-if="$props.id" :id="$props.id" :summary="EntrySummary" />
</QPopupProxy> </QPopupProxy>
</template> </template>

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { onMounted, ref, computed, onUpdated } from 'vue'; import { onMounted, ref, computed } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
@ -11,8 +11,6 @@ import { toDate, toCurrency } from 'src/filters';
import { getUrl } from 'src/composables/getUrl'; import { getUrl } from 'src/composables/getUrl';
import axios from 'axios'; import axios from 'axios';
onUpdated(() => summaryRef.value.fetch());
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
@ -161,6 +159,7 @@ const fetchEntryBuys = async () => {
ref="summaryRef" ref="summaryRef"
:url="`Entries/${entityId}/getEntry`" :url="`Entries/${entityId}/getEntry`"
@on-fetch="(data) => setEntryData(data)" @on-fetch="(data) => setEntryData(data)"
data-key="EntrySummary"
> >
<template #header-left> <template #header-left>
<router-link <router-link
@ -184,15 +183,10 @@ const fetchEntryBuys = async () => {
{{ t('globals.summary.basicData') }} {{ t('globals.summary.basicData') }}
<QIcon name="open_in_new" /> <QIcon name="open_in_new" />
</router-link> </router-link>
<VnLv :label="t('entry.summary.commission')" :value="entry.commission" /> <VnLv :label="t('entry.summary.commission')" :value="entry.commission" />
<VnLv :label="t('entry.summary.currency')" :value="entry.currency.name" /> <VnLv :label="t('entry.summary.currency')" :value="entry.currency.name" />
<VnLv :label="t('entry.summary.company')" :value="entry.company.code" /> <VnLv :label="t('entry.summary.company')" :value="entry.company.code" />
<VnLv :label="t('entry.summary.reference')" :value="entry.reference" /> <VnLv :label="t('entry.summary.reference')" :value="entry.reference" />
<VnLv <VnLv
:label="t('entry.summary.invoiceNumber')" :label="t('entry.summary.invoiceNumber')"
:value="entry.invoiceNumber" :value="entry.invoiceNumber"
@ -206,7 +200,6 @@ const fetchEntryBuys = async () => {
{{ t('globals.summary.basicData') }} {{ t('globals.summary.basicData') }}
<QIcon name="open_in_new" /> <QIcon name="open_in_new" />
</router-link> </router-link>
<VnLv :label="t('entry.summary.travelReference')"> <VnLv :label="t('entry.summary.travelReference')">
<template #value> <template #value>
<span class="link"> <span class="link">
@ -215,31 +208,25 @@ const fetchEntryBuys = async () => {
</span> </span>
</template> </template>
</VnLv> </VnLv>
<VnLv <VnLv
:label="t('entry.summary.travelAgency')" :label="t('entry.summary.travelAgency')"
:value="entry.travel.agency.name" :value="entry.travel.agency.name"
/> />
<VnLv :label="t('shipped')" :value="toDate(entry.travel.shipped)" /> <VnLv :label="t('shipped')" :value="toDate(entry.travel.shipped)" />
<VnLv <VnLv
:label="t('entry.summary.travelWarehouseOut')" :label="t('entry.summary.travelWarehouseOut')"
:value="entry.travel.warehouseOut.name" :value="entry.travel.warehouseOut.name"
/> />
<QCheckbox <QCheckbox
:label="t('entry.summary.travelDelivered')" :label="t('entry.summary.travelDelivered')"
v-model="entry.travel.isDelivered" v-model="entry.travel.isDelivered"
:disable="true" :disable="true"
/> />
<VnLv :label="t('landed')" :value="toDate(entry.travel.landed)" /> <VnLv :label="t('landed')" :value="toDate(entry.travel.landed)" />
<VnLv <VnLv
:label="t('entry.summary.travelWarehouseIn')" :label="t('entry.summary.travelWarehouseIn')"
:value="entry.travel.warehouseIn.name" :value="entry.travel.warehouseIn.name"
/> />
<QCheckbox <QCheckbox
:label="t('entry.summary.travelReceived')" :label="t('entry.summary.travelReceived')"
v-model="entry.travel.isReceived" v-model="entry.travel.isReceived"

View File

@ -0,0 +1,120 @@
<script setup>
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { QBtn } from 'quasar';
import VnPaginate from 'src/components/ui/VnPaginate.vue';
import { usePrintService } from 'composables/usePrintService';
const { openReport } = usePrintService();
const route = useRoute();
const { t } = useI18n();
const $props = defineProps({
id: {
type: String,
required: false,
default: null,
},
});
const entityId = computed(() => $props.id || route.params.id);
const entriesTableColumns = computed(() => [
{
align: 'left',
name: 'itemFk',
field: 'itemFk',
label: t('globals.id'),
},
{
align: 'left',
name: 'item',
label: t('entry.summary.item'),
field: (row) => row.item.name,
},
{
align: 'left',
name: 'packagingFk',
label: t('entry.summary.package'),
field: 'packagingFk',
},
{
align: 'left',
name: 'stickers',
label: t('entry.summary.stickers'),
field: 'stickers',
},
{
align: 'left',
name: 'packing',
label: t('entry.summary.packing'),
field: 'packing',
},
{
align: 'left',
name: 'grouping',
label: t('entry.summary.grouping'),
field: 'grouping',
},
]);
</script>
<template>
<QDialog ref="dialogRef">
<QCard style="min-width: 800px">
<QCardSection class="row items-center q-pb-none">
<QAvatar
:icon="icon"
color="primary"
text-color="white"
size="xl"
v-if="icon"
/>
<span class="text-h6 text-grey">{{ title }}</span>
<QSpace />
<QBtn icon="close" :disable="isLoading" flat round dense v-close-popup />
</QCardSection>
<QCardActions align="right">
<QBtn
:label="t('Print buys')"
color="primary"
icon="print"
:loading="isLoading"
@click="openReport(`Entries/${entityId}/buy-label`)"
unelevated
autofocus
/>
</QCardActions>
<QCardSection class="row items-center">
<VnPaginate
ref="entryBuysPaginateRef"
:limit="0"
data-key="EntryBuys"
:url="`Entries/${entityId}/getBuys`"
auto-load
>
<template #body="{ rows }">
<QTable
:rows="rows"
:columns="entriesTableColumns"
row-key="id"
flat
dense
class="q-ml-lg"
:grid="$q.screen.lt.md"
:no-data-label="t('globals.noResults')"
>
<template #body="props">
<QTr>
<QTd v-for="col in props.cols" :key="col.name">
{{ col.value }}
</QTd>
</QTr>
</template>
</QTable>
</template>
</VnPaginate>
</QCardSection>
</QCard>
</QDialog>
</template>

View File

@ -63,7 +63,7 @@ const redirectToEntryBasicData = (_, { id }) => {
<VnSearchbar <VnSearchbar
url="Entries/filter" url="Entries/filter"
custom-route-redirect-name="EntrySummary" custom-route-redirect-name="EntrySummary"
data-key="EntrySummary" data-key="Entry"
:label="t('Search entries')" :label="t('Search entries')"
:info="t('You can search by entry reference')" :info="t('You can search by entry reference')"
/> />

View File

@ -1,765 +1,199 @@
<script setup> <script setup>
import { onMounted, ref, computed, reactive, onUnmounted } from 'vue'; import { onMounted, onUnmounted } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router';
import FetchData from 'components/FetchData.vue';
import FetchedTags from 'components/ui/FetchedTags.vue';
import EntryDescriptorProxy from './Card/EntryDescriptorProxy.vue';
import TableVisibleColumns from 'src/components/common/TableVisibleColumns.vue';
import EditTableCellValueForm from 'src/components/EditTableCellValueForm.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import EntryLatestBuysFilter from './EntryLatestBuysFilter.vue';
import ItemDescriptorProxy from '../Item/Card/ItemDescriptorProxy.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import { useStateStore } from 'stores/useStateStore';
import { toDate, toCurrency } from 'src/filters';
import { useSession } from 'composables/useSession';
import { dashIfEmpty } from 'src/filters';
import { useArrayData } from 'composables/useArrayData';
import RightMenu from 'src/components/common/RightMenu.vue'; import RightMenu from 'src/components/common/RightMenu.vue';
import VnTable from 'components/VnTable/VnTable.vue';
const router = useRouter(); import EntryLatestBuysFilter from './EntryLatestBuysFilter.vue';
const { getTokenMultimedia } = useSession(); import { useStateStore } from 'stores/useStateStore';
const token = getTokenMultimedia();
const stateStore = useStateStore(); const stateStore = useStateStore();
const { t } = useI18n(); const { t } = useI18n();
import { toDate } from 'src/filters';
import VnImg from 'src/components/ui/VnImg.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
const rowsFetchDataRef = ref(null); const columns = [
const itemTypesOptions = ref([]);
const originsOptions = ref([]);
const itemFamiliesOptions = ref([]);
const intrastatOptions = ref([]);
const packagingsOptions = ref([]);
const editTableCellDialogRef = ref(null);
const visibleColumns = ref([]);
const allColumnNames = ref([]);
const exprBuilder = (param, value) => {
switch (param) {
case 'id':
case 'size':
case 'weightByPiece':
case 'isActive':
case 'family':
case 'minPrice':
case 'packingOut':
return { [`i.${param}`]: value };
case 'name':
case 'description':
return { [`i.${param}`]: { like: `%${value}%` } };
case 'code':
return { 'it.code': value };
case 'intrastat':
return { 'intr.description': value };
case 'origin':
return { 'ori.code': value };
case 'landing':
return { [`lb.${param}`]: value };
case 'packing':
case 'grouping':
case 'quantity':
case 'entryFk':
case 'buyingValue':
case 'freightValue':
case 'comissionValue':
case 'packageValue':
case 'isIgnored':
case 'price2':
case 'price3':
case 'ektFk':
case 'weight':
case 'packagingFk':
return { [`b.${param}`]: value };
}
};
const params = reactive({});
const arrayData = useArrayData('EntryLatestBuys', {
url: 'Buys/latestBuysFilter',
order: ['itemFk DESC'],
exprBuilder: exprBuilder,
});
const store = arrayData.store;
const rows = computed(() => store.data);
const rowsSelected = ref([]);
const getInputEvents = (col) => {
return col.columnFilter.type === 'select'
? { 'update:modelValue': () => applyColumnFilter(col) }
: {
'keyup.enter': () => applyColumnFilter(col),
};
};
const columns = computed(() => [
{ {
label: t('entry.latestBuys.picture'), align: 'center',
name: 'picture', label: t('entry.latestBuys.tableVisibleColumns.image'),
align: 'left', name: 'image',
columnField: {
component: VnImg,
attrs: (id) => {
return {
id,
width: '50px',
};
},
},
columnFilter: false,
}, },
{ {
label: t('entry.latestBuys.itemFk'), align: 'left',
label: t('entry.latestBuys.tableVisibleColumns.itemFk'),
name: 'itemFk', name: 'itemFk',
field: 'itemFk', isTitle: true,
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
}, },
{ {
label: t('entry.latestBuys.packing'), align: 'left',
field: 'packing', label: t('entry.latestBuys.tableVisibleColumns.packing'),
name: 'packing', name: 'packing',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => dashIfEmpty(val),
}, },
{ {
label: t('entry.latestBuys.grouping'), align: 'left',
field: 'grouping', label: t('entry.latestBuys.tableVisibleColumns.grouping'),
name: 'grouping', name: 'grouping',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => dashIfEmpty(val),
}, },
{ {
label: t('entry.latestBuys.quantity'), align: 'left',
field: 'quantity', label: t('entry.latestBuys.tableVisibleColumns.quantity'),
name: 'quantity', name: 'quantity',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
}, },
{ {
label: t('entry.latestBuys.description'), align: 'left',
field: 'description', label: t('entry.latestBuys.tableVisibleColumns.description'),
name: 'description', name: 'description',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => dashIfEmpty(val),
}, },
{ {
label: t('entry.latestBuys.size'), align: 'left',
field: 'size', label: t('entry.latestBuys.tableVisibleColumns.size'),
name: 'size', name: 'size',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
}, },
{ {
label: t('entry.latestBuys.tags'), align: 'left',
label: t('entry.latestBuys.tableVisibleColumns.tags'),
name: 'tags', name: 'tags',
align: 'left',
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
}, },
{ {
label: t('entry.latestBuys.type'), align: 'left',
field: 'code', label: t('entry.latestBuys.tableVisibleColumns.type'),
name: 'type', name: 'type',
align: 'left',
sortable: true,
columnFilter: {
component: VnSelect,
type: 'select',
filterValue: null,
event: getInputEvents,
attrs: {
options: itemTypesOptions.value,
'option-value': 'code',
'option-label': 'code',
dense: true,
},
},
}, },
{ {
label: t('entry.latestBuys.intrastat'), align: 'left',
field: 'intrastat', label: t('entry.latestBuys.tableVisibleColumns.intrastat'),
name: 'intrastat', name: 'intrastat',
align: 'left',
sortable: true,
columnFilter: {
component: VnSelect,
type: 'select',
filterValue: null,
event: getInputEvents,
attrs: {
options: intrastatOptions.value,
'option-value': 'description',
'option-label': 'description',
dense: true,
},
},
}, },
{ {
label: t('entry.latestBuys.origin'), align: 'left',
field: 'origin', label: t('entry.latestBuys.tableVisibleColumns.origin'),
name: 'origin', name: 'origin',
align: 'left',
sortable: true,
columnFilter: {
component: VnSelect,
type: 'select',
filterValue: null,
event: getInputEvents,
attrs: {
options: originsOptions.value,
'option-value': 'code',
'option-label': 'code',
dense: true,
},
},
}, },
{ {
label: t('entry.latestBuys.weightByPiece'), align: 'left',
field: 'weightByPiece', label: t('entry.latestBuys.tableVisibleColumns.weightByPiece'),
name: 'weightByPiece', name: 'weightByPiece',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => dashIfEmpty(val),
}, },
{ {
label: t('entry.latestBuys.isActive'), align: 'left',
field: 'isActive', label: t('entry.latestBuys.tableVisibleColumns.isActive'),
name: 'isActive', name: 'isActive',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
}, },
{ {
label: t('entry.latestBuys.family'), align: 'left',
field: 'family', label: t('entry.latestBuys.tableVisibleColumns.family'),
name: 'family', name: 'family',
align: 'left',
sortable: true,
columnFilter: {
component: VnSelect,
type: 'select',
filterValue: null,
event: getInputEvents,
attrs: {
options: itemFamiliesOptions.value,
'option-value': 'code',
'option-label': 'code',
dense: true,
},
},
}, },
{ {
label: t('entry.latestBuys.entryFk'), align: 'left',
field: 'entryFk', label: t('entry.latestBuys.tableVisibleColumns.entryFk'),
name: 'entryFk', name: 'entryFk',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
}, },
{ {
label: t('entry.latestBuys.buyingValue'), align: 'left',
field: 'buyingValue', label: t('entry.latestBuys.tableVisibleColumns.buyingValue'),
name: 'buyingValue', name: 'buyingValue',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => toCurrency(val),
}, },
{ {
label: t('entry.latestBuys.freightValue'), align: 'left',
field: 'freightValue', label: t('entry.latestBuys.tableVisibleColumns.freightValue'),
name: 'freightValue', name: 'freightValue',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => toCurrency(val),
}, },
{ {
label: t('entry.latestBuys.comissionValue'), align: 'left',
field: 'comissionValue', label: t('entry.latestBuys.tableVisibleColumns.comissionValue'),
name: 'comissionValue', name: 'comissionValue',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => toCurrency(val),
}, },
{ {
label: t('entry.latestBuys.packageValue'), align: 'left',
field: 'packageValue', label: t('entry.latestBuys.tableVisibleColumns.packageValue'),
name: 'packageValue', name: 'packageValue',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => toCurrency(val),
}, },
{ {
label: t('entry.latestBuys.isIgnored'), align: 'left',
field: 'isIgnored', label: t('entry.latestBuys.tableVisibleColumns.isIgnored'),
name: 'isIgnored', name: 'isIgnored',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
}, },
{ {
label: t('entry.latestBuys.price2'), align: 'left',
field: 'price2', label: t('entry.latestBuys.tableVisibleColumns.price2'),
name: 'price2', name: 'price2',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => toCurrency(val),
}, },
{ {
label: t('entry.latestBuys.price3'), align: 'left',
field: 'price3', label: t('entry.latestBuys.tableVisibleColumns.price3'),
name: 'price3', name: 'price3',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => toCurrency(val),
}, },
{ {
label: t('entry.latestBuys.minPrice'), align: 'left',
field: 'minPrice', label: t('entry.latestBuys.tableVisibleColumns.minPrice'),
name: 'minPrice', name: 'minPrice',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => toCurrency(val),
}, },
{ {
label: t('entry.latestBuys.ektFk'), align: 'left',
field: 'ektFk', label: t('entry.latestBuys.tableVisibleColumns.ektFk'),
name: 'ektFk', name: 'ektFk',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => dashIfEmpty(val),
}, },
{ {
label: t('entry.latestBuys.weight'), align: 'left',
field: 'weight', label: t('entry.latestBuys.tableVisibleColumns.weight'),
name: 'weight', name: 'weight',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
}, },
{ {
label: t('entry.latestBuys.packagingFk'), align: 'left',
field: 'packagingFk', label: t('entry.latestBuys.tableVisibleColumns.packagingFk'),
name: 'packagingFk', name: 'packagingFk',
align: 'left',
sortable: true,
columnFilter: {
component: VnSelect,
type: 'select',
filterValue: null,
event: getInputEvents,
attrs: {
options: packagingsOptions.value,
'option-value': 'id',
'option-label': 'id',
dense: true,
},
},
}, },
{ {
label: t('entry.latestBuys.packingOut'), align: 'left',
field: 'packingOut', label: t('entry.latestBuys.tableVisibleColumns.packingOut'),
name: 'packingOut', name: 'packingOut',
align: 'left',
sortable: true,
columnFilter: {
component: VnInput,
type: 'text',
filterValue: null,
event: getInputEvents,
attrs: {
dense: true,
},
},
format: (val) => dashIfEmpty(val),
}, },
{ {
label: t('entry.latestBuys.landing'),
field: 'landing',
name: 'landing',
align: 'left', align: 'left',
sortable: true, label: t('entry.latestBuys.tableVisibleColumns.landing'),
columnFilter: { name: 'landing',
component: VnInput, component: 'date',
type: 'text', columnField: {
filterValue: null, component: null,
event: getInputEvents,
attrs: {
dense: true,
},
}, },
format: (val) => toDate(val), format: (row, dashIfEmpty) => dashIfEmpty(toDate(row.landing)),
}, },
]);
const editTableCellFormFieldsOptions = [
{ field: 'packing', label: t('entry.latestBuys.packing') },
{ field: 'grouping', label: t('entry.latestBuys.grouping') },
{ field: 'packageValue', label: t('entry.latestBuys.packageValue') },
{ field: 'weight', label: t('entry.latestBuys.weight') },
{ field: 'description', label: t('globals.description') },
{ field: 'size', label: t('entry.latestBuys.size') },
{ field: 'weightByPiece', label: t('entry.latestBuys.weightByPiece') },
{ field: 'packingOut', label: t('entry.latestBuys.packingOut') },
{ field: 'landing', label: t('entry.latestBuys.landing') },
]; ];
const openEditTableCellDialog = () => {
editTableCellDialogRef.value.show();
};
const onEditCellDataSaved = async () => {
rowsSelected.value = [];
await rowsFetchDataRef.value.fetch();
};
const redirectToEntryBuys = (entryFk) => {
router.push({ name: 'EntryBuys', params: { id: entryFk } });
};
const applyColumnFilter = async (col) => {
try {
params[col.field] = col.columnFilter.filterValue;
await arrayData.addFilter({ params });
} catch (err) {
console.error('Error applying column filter', err);
}
};
onMounted(async () => { onMounted(async () => {
stateStore.rightDrawer = true; stateStore.rightDrawer = true;
const filteredColumns = columns.value.filter((col) => col.name !== 'picture');
allColumnNames.value = filteredColumns.map((col) => col.name);
await arrayData.fetch({ append: false });
}); });
onUnmounted(() => (stateStore.rightDrawer = false)); onUnmounted(() => (stateStore.rightDrawer = false));
</script> </script>
<template> <template>
<FetchData
url="ItemTypes"
:filter="{ fields: ['code'], order: 'code ASC', limit: 30 }"
auto-load
@on-fetch="(data) => (itemTypesOptions = data)"
/>
<FetchData
url="Origins"
:filter="{ fields: ['code'], order: 'code ASC', limit: 30 }"
auto-load
@on-fetch="(data) => (originsOptions = data)"
/>
<FetchData
url="ItemFamilies"
:filter="{ fields: ['code'], order: 'code ASC', limit: 30 }"
auto-load
@on-fetch="(data) => (itemFamiliesOptions = data)"
/>
<FetchData
url="Packagings"
:filter="{ fields: ['id'], order: 'id ASC', limit: 30 }"
auto-load
@on-fetch="(data) => (packagingsOptions = data)"
/>
<FetchData
url="Intrastats"
:filter="{ fields: ['description'], order: 'description ASC', limit: 30 }"
auto-load
@on-fetch="(data) => (intrastatOptions = data)"
/>
<VnSubToolbar>
<template #st-data>
<TableVisibleColumns
:all-columns="allColumnNames"
table-code="latestBuys"
labels-traductions-path="entry.latestBuys"
@on-config-saved="visibleColumns = ['picture', ...$event]"
/>
</template>
</VnSubToolbar>
<RightMenu> <RightMenu>
<template #right-panel> <template #right-panel>
<EntryLatestBuysFilter data-key="EntryLatestBuys" /> <EntryLatestBuysFilter data-key="LatestBuys" />
</template> </template>
</RightMenu> </RightMenu>
<Teleport to="#actions-append"> <VnSubToolbar />
<div class="row q-gutter-x-sm"> <VnTable
<QBtn flat @click="stateStore.toggleRightDrawer()" round dense icon="menu"> ref="tableRef"
<QTooltip bottom anchor="bottom right"> data-key="LatestBuys"
{{ t('globals.collapseMenu') }} url="Buys/latestBuysFilter"
</QTooltip> order="id DESC"
</QBtn> :columns="columns"
</div> redirect="entry"
</Teleport> auto-load
<QPage class="column items-center q-pa-md"> :right-search="false"
<QTable />
:rows="rows"
:columns="columns"
selection="multiple"
row-key="id"
class="full-width q-mt-md"
:visible-columns="visibleColumns"
v-model:selected="rowsSelected"
:no-data-label="t('globals.noResults')"
@row-click="(_, row) => redirectToEntryBuys(row.entryFk)"
>
<template #top-row="{ cols }">
<QTr>
<QTd />
<QTd
v-for="(col, index) in cols"
:key="index"
style="max-width: 100px"
>
<component
:is="col.columnFilter.component"
v-if="col.name !== 'picture'"
v-model="col.columnFilter.filterValue"
v-bind="col.columnFilter.attrs"
v-on="col.columnFilter.event(col)"
dense
/>
</QTd>
</QTr>
</template>
<template #body-cell-picture="{ row }">
<QTd>
<QImg
:src="`/api/Images/catalog/50x50/${row.itemFk}/download?access_token=${token}`"
spinner-color="primary"
:ratio="1"
height="50px"
width="50px"
class="image"
/>
</QTd>
</template>
<template #body-cell-itemFk="{ row }">
<QTd @click.stop>
<QBtn flat color="primary">
{{ row.itemFk }}
</QBtn>
<ItemDescriptorProxy :id="row.itemFk" />
</QTd>
</template>
<template #body-cell-tags="{ row }">
<QTd>
<FetchedTags :item="row" :max-length="6" />
</QTd>
</template>
<template #body-cell-entryFk="{ row }">
<QTd @click.stop>
<QBtn flat color="primary">
<EntryDescriptorProxy :id="row.entryFk" />
{{ row.entryFk }}
</QBtn>
</QTd>
</template>
<template #body-cell-isIgnored="{ row }">
<QTd>
<QIcon
:name="row.isIgnored ? `check` : `close`"
:color="row.isIgnored ? `positive` : `negative`"
size="sm"
/>
</QTd>
</template>
<template #body-cell-isActive="{ row }">
<QTd>
<QIcon
:name="row.isActive ? `check` : `close`"
:color="row.isActive ? `positive` : `negative`"
size="sm"
/>
</QTd>
</template>
</QTable>
<QPageSticky v-if="rowsSelected.length > 0" :offset="[20, 20]">
<QBtn @click="openEditTableCellDialog()" color="primary" fab icon="edit" />
<QTooltip>
{{ t('Edit buy(s)') }}
</QTooltip>
</QPageSticky>
<QDialog ref="editTableCellDialogRef">
<EditTableCellValueForm
edit-url="Buys/editLatestBuys"
:rows="rowsSelected"
:fields-options="editTableCellFormFieldsOptions"
@on-data-saved="onEditCellDataSaved()"
/>
</QDialog>
</QPage>
</template> </template>
<i18n> <i18n>

View File

@ -1,31 +1,174 @@
<script setup> <script setup>
import { onMounted } from 'vue'; import { onMounted, ref, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } 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 EntrySummary from './Card/EntrySummary.vue';
import EntryFilter from './EntryFilter.vue'; import EntryFilter from './EntryFilter.vue';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue'; import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import { toDate } from 'src/filters/index'; import VnTable from 'components/VnTable/VnTable.vue';
import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import RightMenu from 'src/components/common/RightMenu.vue'; import RightMenu from 'src/components/common/RightMenu.vue';
import { toDate } from 'src/filters';
const stateStore = useStateStore(); const stateStore = useStateStore();
const router = useRouter();
const { t } = useI18n(); const { t } = useI18n();
const { viewSummary } = useSummaryDialog(); const tableRef = ref();
function navigate(id) { const entryFilter = {
router.push({ path: `/entry/${id}` }); include: [
} {
relation: 'suppliers',
const redirectToCreateView = () => { scope: {
router.push({ name: 'EntryCreate' }); fields: ['id', 'name'],
},
},
{
relation: 'travels',
scope: {
fields: ['id', 'ref'],
},
},
{
relation: 'companies',
scope: {
fields: ['id', 'code'],
},
},
],
}; };
const columns = computed(() => [
{
align: 'left',
label: t('entry.list.tableVisibleColumns.id'),
name: 'id',
isTitle: true,
cardVisible: true,
},
{
align: 'left',
label: t('entry.list.tableVisibleColumns.reference'),
name: 'reference',
isTitle: true,
component: 'input',
columnField: {
component: null,
},
create: true,
cardVisible: true,
},
{
align: 'left',
label: t('entry.list.tableVisibleColumns.created'),
name: 'created',
create: true,
cardVisible: true,
component: 'date',
columnField: {
component: null,
},
format: (row, dashIfEmpty) => dashIfEmpty(toDate(row.created)),
},
{
align: 'left',
label: t('entry.list.tableVisibleColumns.supplierFk'),
name: 'supplierFk',
create: true,
cardVisible: true,
component: 'select',
attrs: {
url: 'suppliers',
fields: ['id', 'name'],
},
columnField: {
component: null,
},
format: (row, dashIfEmpty) => dashIfEmpty(row.supplierName),
},
{
align: 'left',
label: t('entry.list.tableVisibleColumns.isBooked'),
name: 'isBooked',
cardVisible: true,
create: true,
component: 'checkbox',
},
{
align: 'left',
label: t('entry.list.tableVisibleColumns.isConfirmed'),
name: 'isConfirmed',
cardVisible: true,
create: true,
component: 'checkbox',
},
{
align: 'left',
label: t('entry.list.tableVisibleColumns.isOrdered'),
name: 'isOrdered',
cardVisible: true,
create: true,
component: 'checkbox',
},
{
align: 'left',
label: t('entry.list.tableVisibleColumns.companyFk'),
name: 'companyFk',
component: 'select',
attrs: {
url: 'companies',
fields: ['id', 'code'],
optionLabel: 'code',
optionValue: 'id',
},
columnField: {
component: null,
},
create: true,
format: (row, dashIfEmpty) => dashIfEmpty(row.companyCode),
},
{
align: 'left',
label: t('entry.list.tableVisibleColumns.travelFk'),
name: 'travelFk',
component: 'select',
attrs: {
url: 'travels',
fields: ['id', 'ref'],
optionLabel: 'ref',
optionValue: 'id',
},
columnField: {
component: null,
},
create: true,
format: (row, dashIfEmpty) => dashIfEmpty(row.travelRef),
},
{
align: 'left',
label: t('entry.list.tableVisibleColumns.isExcludedFromAvailable'),
name: 'isExcludedFromAvailable',
chip: {
color: null,
condition: (value) => value,
icon: 'vn:inventory',
},
columnFilter: {
inWhere: true,
},
},
{
align: 'left',
label: t('entry.list.tableVisibleColumns.isRaid'),
name: 'isRaid',
chip: {
color: null,
condition: (value) => value,
icon: 'vn:net',
},
columnFilter: {
inWhere: true,
},
},
]);
onMounted(async () => { onMounted(async () => {
stateStore.rightDrawer = true; stateStore.rightDrawer = true;
}); });
@ -42,83 +185,23 @@ onMounted(async () => {
<EntryFilter data-key="EntryList" /> <EntryFilter data-key="EntryList" />
</template> </template>
</RightMenu> </RightMenu>
<QPage class="column items-center q-pa-md"> <VnTable
<div class="vn-card-list"> ref="tableRef"
<VnPaginate data-key="EntryList"
data-key="EntryList" url="Entries/filter"
url="Entries/filter" :filter="entryFilter"
:order="['landed DESC', 'id DESC']" :create="{
auto-load urlCreate: 'Entries',
> title: 'Create entry',
<template #body="{ rows }"> onDataSaved: ({ id }) => tableRef.redirect(id),
<CardList formInitialData: {},
v-for="row of rows" }"
:key="row.id" order="id DESC"
:title="row.reference" :columns="columns"
@click="navigate(row.id)" redirect="entry"
:id="row.id" auto-load
:has-info-icons="!!row.isExcludedFromAvailable || !!row.isRaid" :right-search="false"
> />
<template #info-icons>
<QIcon
v-if="row.isExcludedFromAvailable"
name="vn:inventory"
color="primary"
size="xs"
>
<QTooltip>{{ t('Inventory entry') }}</QTooltip>
</QIcon>
<QIcon
v-if="row.isRaid"
name="vn:net"
color="primary"
size="xs"
>
<QTooltip>{{ t('Virtual entry') }}</QTooltip>
</QIcon>
</template>
<template #list-items>
<VnLv :label="t('landed')" :value="toDate(row.landed)" />
<VnLv
:label="t('entry.list.booked')"
:value="!!row.isBooked"
/>
<VnLv
:label="t('entry.list.invoiceNumber')"
:value="row.invoiceNumber"
/>
<VnLv
:label="t('entry.list.confirmed')"
:value="!!row.isConfirmed"
/>
<VnLv
:label="t('entry.list.supplier')"
:value="row.supplierName"
/>
<VnLv
:label="t('entry.list.ordered')"
:value="!!row.isOrdered"
/>
</template>
<template #actions>
<QBtn
:label="t('components.smartCard.openSummary')"
@click.stop="viewSummary(row.id, EntrySummary)"
color="primary"
type="submit"
/>
</template>
</CardList>
</template>
</VnPaginate>
</div>
</QPage>
<QPageSticky :offset="[20, 20]">
<QBtn fab icon="add" color="primary" @click="redirectToCreateView()" />
<QTooltip>
{{ t('entry.list.newEntry') }}
</QTooltip>
</QPageSticky>
</template> </template>
<i18n> <i18n>

View File

@ -0,0 +1,124 @@
<script setup>
import { computed, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import VnSearchbar from 'src/components/ui/VnSearchbar.vue';
import { useStateStore } from 'stores/useStateStore';
import { toDate } from 'src/filters/index';
import { useQuasar } from 'quasar';
import EntryBuysTableDialog from './EntryBuysTableDialog.vue';
import VnTable from 'components/VnTable/VnTable.vue';
import VnInput from 'src/components/common/VnInput.vue';
const stateStore = useStateStore();
const { t } = useI18n();
const quasar = useQuasar();
onMounted(async () => {
stateStore.rightDrawer = true;
});
const columns = computed(() => [
{
align: 'left',
name: 'id',
label: t('customer.extendedList.tableVisibleColumns.id'),
chip: {
condition: () => true,
},
isId: true,
isTitle: false,
},
{
align: 'left',
label: t('shipped'),
name: 'shipped',
isTitle: false,
create: true,
cardVisible: true,
component: 'date',
columnField: {
component: null,
},
format: ({ shipped }) => toDate(shipped),
},
{
align: 'left',
label: t('landed'),
name: 'landed',
isTitle: false,
create: true,
cardVisible: false,
component: 'date',
columnField: {
component: null,
},
format: ({ landed }) => toDate(landed),
},
{
align: 'left',
label: t('globals.wareHouseIn'),
name: 'warehouseInName',
isTitle: false,
cardVisible: true,
create: false,
},
{
align: 'right',
name: 'tableActions',
computed,
actions: [
{
title: t('printBuys'),
icon: 'print',
action: (row) => printBuys(row.id),
},
],
},
]);
const printBuys = (rowId) => {
quasar.dialog({
component: EntryBuysTableDialog,
componentProps: {
id: rowId,
},
});
};
</script>
<template>
<VnSearchbar
data-key="EntryList"
url="Entries/filter"
:label="t('Search entries')"
:info="t('You can search by entry reference')"
/>
<QPage class="column items-center q-pa-md">
<div class="vn-card-list">
<VnTable
ref="myEntriesRef"
data-key="myEntriesList"
url="Entries/filter"
:order="['landed DESC', 'id DESC']"
:columns="columns"
default-mode="card"
auto-load
:right-search="true"
>
<template #moreFilterPanel="{ params }">
<VnInput
:label="t('globals.daysOnward')"
v-model="params.days"
class="q-px-xs row"
dense
filled
outlined
></VnInput>
</template>
</VnTable>
</div>
</QPage>
</template>
<i18n>
You can search by entry reference: Puedes buscar por referencia de la entrada
</i18n>

View File

@ -8,3 +8,4 @@ entryFilter:
reference: Reference reference: Reference
landed: Landed landed: Landed
shipped: Shipped shipped: Shipped
printBuys: Print buys

View File

@ -1,5 +1,6 @@
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
entryList: entryList:
list: list:
inventoryEntry: Es inventario inventoryEntry: Es inventario
@ -11,3 +12,4 @@ entryFilter:
landed: F. llegada landed: F. llegada
shipped: F. salida shipped: F. salida
Print buys: Imprimir etiquetas

View File

@ -1,5 +1,6 @@
<script setup> <script setup>
import InvoiceInDescriptor from "pages/InvoiceIn/Card/InvoiceInDescriptor.vue"; import InvoiceInDescriptor from 'pages/InvoiceIn/Card/InvoiceInDescriptor.vue';
import InvoiceInSummary from './InvoiceInSummary.vue';
const $props = defineProps({ const $props = defineProps({
id: { id: {
@ -10,6 +11,10 @@ const $props = defineProps({
</script> </script>
<template> <template>
<QPopupProxy> <QPopupProxy>
<InvoiceInDescriptor v-if="$props.id" :id="$props.id" /> <InvoiceInDescriptor
v-if="$props.id"
:id="$props.id"
:summary="InvoiceInSummary"
/>
</QPopupProxy> </QPopupProxy>
</template> </template>

View File

@ -209,7 +209,7 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
<QCardSection class="q-pa-none"> <QCardSection class="q-pa-none">
<VnTitle <VnTitle
:url="getLink('basic-data')" :url="getLink('basic-data')"
:text="t('invoiceIn.pageTitles.basicData')" :text="t('globals.pageTitles.basicData')"
/> />
</QCardSection> </QCardSection>
<VnLv <VnLv
@ -240,7 +240,7 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
<QCardSection class="q-pa-none"> <QCardSection class="q-pa-none">
<VnTitle <VnTitle
:url="getLink('basic-data')" :url="getLink('basic-data')"
:text="t('invoiceIn.pageTitles.basicData')" :text="t('globals.pageTitles.basicData')"
/> />
</QCardSection> </QCardSection>
<VnLv <VnLv
@ -265,7 +265,7 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
<QCardSection class="q-pa-none"> <QCardSection class="q-pa-none">
<VnTitle <VnTitle
:url="getLink('basic-data')" :url="getLink('basic-data')"
:text="t('invoiceIn.pageTitles.basicData')" :text="t('globals.pageTitles.basicData')"
/> />
</QCardSection> </QCardSection>
<VnLv <VnLv
@ -289,7 +289,7 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
<QCardSection class="q-pa-none"> <QCardSection class="q-pa-none">
<VnTitle <VnTitle
:url="getLink('basic-data')" :url="getLink('basic-data')"
:text="t('invoiceIn.pageTitles.basicData')" :text="t('globals.pageTitles.basicData')"
/> />
</QCardSection> </QCardSection>
<QCardSection class="q-pa-none"> <QCardSection class="q-pa-none">

View File

@ -63,8 +63,8 @@ const setData = (entity) => (data.value = useCardDescription(entity.ref, entity.
@on-fetch="setData" @on-fetch="setData"
data-key="invoiceOutData" data-key="invoiceOutData"
> >
<template #menu="{ entity }"> <template #menu="{ entity, menuRef }">
<InvoiceOutDescriptorMenu :invoice-out-data="entity" /> <InvoiceOutDescriptorMenu :invoice-out-data="entity" :menu-ref="menuRef" />
</template> </template>
<template #body="{ entity }"> <template #body="{ entity }">
<VnLv :label="t('invoiceOut.card.issued')" :value="toDate(entity.issued)" /> <VnLv :label="t('invoiceOut.card.issued')" :value="toDate(entity.issued)" />

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref } from 'vue'; import { ref, onBeforeMount } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
@ -11,13 +11,20 @@ import useNotify from 'src/composables/useNotify';
import { useSession } from 'src/composables/useSession'; import { useSession } from 'src/composables/useSession';
import { usePrintService } from 'composables/usePrintService'; import { usePrintService } from 'composables/usePrintService';
import { useVnConfirm } from 'composables/useVnConfirm'; import { useVnConfirm } from 'composables/useVnConfirm';
import { getUrl } from 'src/composables/getUrl';
import axios from 'axios'; import axios from 'axios';
onBeforeMount(async () => (salixUrl.value = await getUrl('')));
const $props = defineProps({ const $props = defineProps({
invoiceOutData: { invoiceOutData: {
type: Object, type: Object,
default: () => {}, default: () => {},
}, },
menuRef: {
type: Object,
default: () => {},
},
}); });
const { notify } = useNotify(); const { notify } = useNotify();
@ -28,8 +35,7 @@ const { t } = useI18n();
const { openReport, sendEmail } = usePrintService(); const { openReport, sendEmail } = usePrintService();
const { openConfirmationModal } = useVnConfirm(); const { openConfirmationModal } = useVnConfirm();
const quasar = useQuasar(); const quasar = useQuasar();
const salixUrl = ref();
const transferInvoiceDialogRef = ref();
const invoiceFormType = ref('pdf'); const invoiceFormType = ref('pdf');
const defaultEmailAddress = ref($props.invoiceOutData.client?.email); const defaultEmailAddress = ref($props.invoiceOutData.client?.email);
@ -115,6 +121,7 @@ const refundInvoice = async (withWarehouse) => {
try { try {
const params = { ref: $props.invoiceOutData.ref, withWarehouse: withWarehouse }; const params = { ref: $props.invoiceOutData.ref, withWarehouse: withWarehouse };
const { data } = await axios.post('InvoiceOuts/refund', params); const { data } = await axios.post('InvoiceOuts/refund', params);
location.href = window.origin + `/#/ticket/${data[0].id}/sale`;
notify( notify(
t('refundInvoiceSuccessMessage', { t('refundInvoiceSuccessMessage', {
refundTicket: data[0].id, refundTicket: data[0].id,
@ -125,11 +132,22 @@ const refundInvoice = async (withWarehouse) => {
console.error('Error generating invoice out pdf', err); console.error('Error generating invoice out pdf', err);
} }
}; };
const showTransferInvoiceForm = async () => {
quasar.dialog({
component: TransferInvoiceForm,
componentProps: {
invoiceOutData: $props.invoiceOutData,
},
});
};
</script> </script>
<template> <template>
<QItem v-ripple clickable @click="transferInvoiceDialogRef.show()"> <QItem v-ripple clickable>
<QItemSection>{{ t('Transfer invoice to...') }}</QItemSection> <QItemSection @click="showTransferInvoiceForm()">{{
t('Transfer invoice to...')
}}</QItemSection>
</QItem> </QItem>
<QItem v-ripple clickable> <QItem v-ripple clickable>
<QItemSection>{{ t('Show invoice...') }}</QItemSection> <QItemSection>{{ t('Show invoice...') }}</QItemSection>
@ -222,10 +240,6 @@ const refundInvoice = async (withWarehouse) => {
{{ t('Create a single ticket with all the content of the current invoice') }} {{ t('Create a single ticket with all the content of the current invoice') }}
</QTooltip> </QTooltip>
</QItem> </QItem>
<QDialog ref="transferInvoiceDialogRef">
<TransferInvoiceForm :invoice-out-data="invoiceOutData" />
</QDialog>
</template> </template>
<i18n> <i18n>

View File

@ -3,7 +3,7 @@ import { onMounted, ref, computed } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import axios from 'axios'; import axios from 'axios';
import { toCurrency, toDate } from 'src/filters'; import { toCurrency, toDate, toPercentage } from 'src/filters';
import CardSummary from 'components/ui/CardSummary.vue'; import CardSummary from 'components/ui/CardSummary.vue';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import { getUrl } from 'src/composables/getUrl'; import { getUrl } from 'src/composables/getUrl';
@ -57,12 +57,14 @@ const taxColumns = ref([
name: 'quantity', name: 'quantity',
label: 'invoiceOut.summary.rate', label: 'invoiceOut.summary.rate',
field: (row) => row.rate, field: (row) => row.rate,
format: (value) => toPercentage(value / 100),
sortable: true, sortable: true,
}, },
{ {
name: 'invoiceOuted', name: 'invoiceOuted',
label: 'invoiceOut.summary.fee', label: 'invoiceOut.summary.fee',
field: (row) => row.vat, field: (row) => row.vat,
format: (value) => toCurrency(value),
sortable: true, sortable: true,
}, },
]); ]);
@ -106,13 +108,14 @@ const ticketsColumns = ref([
ref="summary" ref="summary"
:url="`InvoiceOuts/${entityId}/summary`" :url="`InvoiceOuts/${entityId}/summary`"
:entity-id="entityId" :entity-id="entityId"
data-key="InvoiceOutSummary"
> >
<template #header="{ entity: { invoiceOut } }"> <template #header="{ entity: { invoiceOut } }">
<div>{{ invoiceOut.ref }} - {{ invoiceOut.client?.socialName }}</div> <div>{{ invoiceOut.ref }} - {{ invoiceOut.client?.socialName }}</div>
</template> </template>
<template #body="{ entity: { invoiceOut } }"> <template #body="{ entity: { invoiceOut } }">
<QCard class="vn-one"> <QCard class="vn-one">
<VnTitle :text="t('invoiceOut.pageTitles.basicData')" /> <VnTitle :text="t('globals.pageTitles.basicData')" />
<VnLv <VnLv
:label="t('invoiceOut.summary.issued')" :label="t('invoiceOut.summary.issued')"
:value="toDate(invoiceOut.issued)" :value="toDate(invoiceOut.issued)"
@ -163,7 +166,7 @@ const ticketsColumns = ref([
</template> </template>
<template #body-cell-item="{ value }"> <template #body-cell-item="{ value }">
<QTd> <QTd>
<QBtn flat color="primary"> <QBtn flat class="link">
{{ value }} {{ value }}
<TicketDescriptorProxy :id="value" /> <TicketDescriptorProxy :id="value" />
</QBtn> </QBtn>
@ -171,7 +174,7 @@ const ticketsColumns = ref([
</template> </template>
<template #body-cell-quantity="{ value, row }"> <template #body-cell-quantity="{ value, row }">
<QTd> <QTd>
<QBtn class="no-uppercase" flat color="primary" dense> <QBtn class="no-uppercase link" flat dense>
{{ value }} {{ value }}
<CustomerDescriptorProxy :id="row.id" /> <CustomerDescriptorProxy :id="row.id" />
</QBtn> </QBtn>

View File

@ -41,7 +41,7 @@ function setWorkers(data) {
<span>{{ formatFn(tag.value) }}</span> <span>{{ formatFn(tag.value) }}</span>
</div> </div>
</template> </template>
<template #body="{ params, searchFn }"> <template #body="{ params }">
<QItem> <QItem>
<QItemSection> <QItemSection>
<VnInput <VnInput
@ -93,7 +93,6 @@ function setWorkers(data) {
<QItemSection> <QItemSection>
<QCheckbox <QCheckbox
:label="t('Has PDF')" :label="t('Has PDF')"
@update:model-value="searchFn()"
toggle-indeterminate toggle-indeterminate
v-model="params.hasPdf" v-model="params.hasPdf"
/> />

View File

@ -30,7 +30,7 @@ const selectedCustomerId = ref(null);
const tableColumnComponents = { const tableColumnComponents = {
clientId: { clientId: {
component: QBtn, component: QBtn,
props: { flat: true, color: 'blue' }, props: { flat: true, class: 'link' },
event: (prop) => selectCustomerId(prop.value), event: (prop) => selectCustomerId(prop.value),
}, },
clientName: { clientName: {
@ -52,7 +52,12 @@ const tableColumnComponents = {
}; };
const columns = computed(() => [ const columns = computed(() => [
{ label: 'Id', field: 'clientId', name: 'clientId', align: 'left' }, {
align: 'left',
name: 'clientId',
label: 'Id',
field: 'clientId',
},
{ {
label: t('invoiceOut.globalInvoices.table.client'), label: t('invoiceOut.globalInvoices.table.client'),
field: 'clientName', field: 'clientName',

View File

@ -48,7 +48,7 @@ const getStatus = computed({
onMounted(async () => { onMounted(async () => {
await invoiceOutGlobalStore.init(); await invoiceOutGlobalStore.init();
formData.value = { ...formInitialData.value }; formData.value = formInitialData.value.invoiceDate;
}); });
</script> </script>

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