Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix-front into 8036-arrayData_exprBuilder_after
gitea/salix-front/pipeline/pr-dev This commit looks good Details

This commit is contained in:
Alex Moreno 2024-10-24 07:58:00 +02:00
commit 99549c6787
56 changed files with 986 additions and 156 deletions

View File

@ -2,9 +2,11 @@ import axios from 'axios';
import { useSession } from 'src/composables/useSession'; import { useSession } from 'src/composables/useSession';
import { Router } from 'src/router'; import { Router } from 'src/router';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
import { useStateQueryStore } from 'src/stores/useStateQueryStore';
const session = useSession(); const session = useSession();
const { notify } = useNotify(); const { notify } = useNotify();
const stateQuery = useStateQueryStore();
const baseUrl = '/api/'; const baseUrl = '/api/';
axios.defaults.baseURL = baseUrl; axios.defaults.baseURL = baseUrl;
@ -15,7 +17,7 @@ const onRequest = (config) => {
if (token.length && !config.headers.Authorization) { if (token.length && !config.headers.Authorization) {
config.headers.Authorization = token; config.headers.Authorization = token;
} }
stateQuery.add(config);
return config; return config;
}; };
@ -24,10 +26,10 @@ const onRequestError = (error) => {
}; };
const onResponse = (response) => { const onResponse = (response) => {
const { method } = response.config; const config = response.config;
stateQuery.remove(config);
const isSaveRequest = method === 'patch'; if (config.method === 'patch') {
if (isSaveRequest) {
notify('globals.dataSaved', 'positive'); notify('globals.dataSaved', 'positive');
} }
@ -35,6 +37,8 @@ const onResponse = (response) => {
}; };
const onResponseError = (error) => { const onResponseError = (error) => {
stateQuery.remove(error.config);
let message = ''; let message = '';
const response = error.response; const response = error.response;

View File

@ -79,14 +79,20 @@ async function onProvinceCreated(data) {
watch( watch(
() => [postcodeFormData.countryFk], () => [postcodeFormData.countryFk],
async (newCountryFk, oldValueFk) => { async (newCountryFk, oldValueFk) => {
if (!!oldValueFk[0] && newCountryFk[0] !== oldValueFk[0]) { if (Array.isArray(newCountryFk)) {
newCountryFk = newCountryFk[0];
}
if (Array.isArray(oldValueFk)) {
oldValueFk = oldValueFk[0];
}
if (!!oldValueFk && newCountryFk !== oldValueFk) {
postcodeFormData.provinceFk = null; postcodeFormData.provinceFk = null;
postcodeFormData.townFk = null; postcodeFormData.townFk = null;
} }
if ((newCountryFk, newCountryFk !== postcodeFormData.countryFk)) { if (oldValueFk !== newCountryFk) {
await provincesFetchDataRef.value.fetch({ await provincesFetchDataRef.value.fetch({
where: { where: {
countryFk: newCountryFk[0], countryFk: newCountryFk,
}, },
}); });
await townsFetchDataRef.value.fetch({ await townsFetchDataRef.value.fetch({
@ -103,9 +109,12 @@ watch(
watch( watch(
() => postcodeFormData.provinceFk, () => postcodeFormData.provinceFk,
async (newProvinceFk) => { async (newProvinceFk) => {
if (newProvinceFk[0] && newProvinceFk[0] !== postcodeFormData.provinceFk) { if (Array.isArray(newProvinceFk)) {
newProvinceFk = newProvinceFk[0];
}
if (newProvinceFk !== postcodeFormData.provinceFk) {
await townsFetchDataRef.value.fetch({ await townsFetchDataRef.value.fetch({
where: { provinceFk: newProvinceFk[0] }, where: { provinceFk: newProvinceFk },
}); });
} }
} }
@ -125,16 +134,26 @@ async function handleCountries(data) {
<FetchData <FetchData
ref="provincesFetchDataRef" ref="provincesFetchDataRef"
@on-fetch="handleProvinces" @on-fetch="handleProvinces"
:sort-by="['name ASC']"
:limit="30"
auto-load auto-load
url="Provinces/location" url="Provinces/location"
/> />
<FetchData <FetchData
ref="townsFetchDataRef" ref="townsFetchDataRef"
:sort-by="['name ASC']"
:limit="30"
@on-fetch="handleTowns" @on-fetch="handleTowns"
auto-load auto-load
url="Towns/location" url="Towns/location"
/> />
<FetchData @on-fetch="handleCountries" auto-load url="Countries" /> <FetchData
@on-fetch="handleCountries"
:sort-by="['name ASC']"
:limit="30"
auto-load
url="Countries"
/>
<FormModelPopup <FormModelPopup
url-create="postcodes" url-create="postcodes"
model="postcode" model="postcode"

View File

@ -46,6 +46,8 @@ const onDataSaved = (dataSaved, requestResponse) => {
}, },
}" }"
url="Autonomies/location" url="Autonomies/location"
:sort-by="['name ASC']"
:limit="30"
/> />
<FormModelPopup <FormModelPopup
:title="t('New province')" :title="t('New province')"

View File

@ -3,6 +3,7 @@ import { onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import { useStateQueryStore } from 'src/stores/useStateQueryStore';
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';
@ -12,6 +13,7 @@ import VnAvatar from './ui/VnAvatar.vue';
const { t } = useI18n(); const { t } = useI18n();
const stateStore = useStateStore(); const stateStore = useStateStore();
const quasar = useQuasar(); const quasar = useQuasar();
const stateQuery = useStateQueryStore();
const state = useState(); const state = useState();
const user = state.getUser(); const user = state.getUser();
const appName = 'Lilium'; const appName = 'Lilium';
@ -50,6 +52,14 @@ const pinnedModulesRef = ref();
</QBtn> </QBtn>
</RouterLink> </RouterLink>
<VnBreadcrumbs v-if="$q.screen.gt.sm" /> <VnBreadcrumbs v-if="$q.screen.gt.sm" />
<QSpinner
color="primary"
class="q-ml-md"
:class="{
'no-visible': !stateQuery.isLoading().value,
}"
size="xs"
/>
<QSpace /> <QSpace />
<div id="searchbar" class="searchbar"></div> <div id="searchbar" class="searchbar"></div>
<QSpace /> <QSpace />

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref, watch } from 'vue'; import { ref } from 'vue';
import { useValidator } from 'src/composables/useValidator'; import { useValidator } from 'src/composables/useValidator';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';

View File

@ -316,6 +316,7 @@ defineExpose({
selected, selected,
CrudModelRef, CrudModelRef,
params, params,
tableRef,
}); });
function handleOnDataSaved(_) { function handleOnDataSaved(_) {
@ -416,6 +417,7 @@ function handleScroll() {
ref="tableRef" ref="tableRef"
v-bind="table" v-bind="table"
class="vnTable" class="vnTable"
:class="{ 'last-row-sticky': $props.footer }"
:columns="splittedColumns.columns" :columns="splittedColumns.columns"
:rows="rows" :rows="rows"
v-model:selected="selected" v-model:selected="selected"
@ -815,6 +817,7 @@ es:
top: 0; top: 0;
} }
} }
.vnTable { .vnTable {
thead tr th { thead tr th {
position: sticky; position: sticky;
@ -853,6 +856,9 @@ es:
table tbody th { table tbody th {
position: relative; position: relative;
} }
}
.last-row-sticky {
tbody:nth-last-child(1) { tbody:nth-last-child(1) {
@extend .bg-header; @extend .bg-header;
position: sticky; position: sticky;

View File

@ -0,0 +1,19 @@
<script setup>
import VnSelect from './VnSelect.vue';
defineProps({
selectProps: { type: Object, required: true },
promise: { type: Function, default: () => {} },
});
</script>
<template>
<QBtnDropdown v-bind="$attrs" color="primary">
<VnSelect
v-bind="selectProps"
hide-selected
hide-dropdown-icon
focus-on-mount
@update:model-value="promise"
/>
</QBtnDropdown>
</template>

View File

@ -141,6 +141,7 @@ function findKeyInOptions() {
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));
emit('update:options', data);
} }
function filter(val, options) { function filter(val, options) {

View File

@ -0,0 +1,33 @@
<script setup>
import { useRoute } from 'vue-router';
import { defineProps } from 'vue';
const props = defineProps({
routeName: {
type: String,
required: true,
},
entityId: {
type: [String, Number],
required: true,
},
url: {
type: String,
default: null,
},
});
const route = useRoute();
const id = props.entityId;
</script>
<template>
<router-link
v-if="route?.name !== routeName"
:to="{ name: routeName, params: { id: id } }"
class="header link"
:href="url"
>
<QIcon name="open_in_new" color="white" size="sm" />
</router-link>
</template>

View File

@ -0,0 +1,55 @@
<script setup>
import { defineProps, ref } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const props = defineProps({
usesMana: {
type: Boolean,
required: true,
},
manaCode: {
type: String,
required: true,
},
manaVal: {
type: String,
default: 'mana',
},
manaLabel: {
type: String,
default: 'Promotion mana',
},
manaClaimVal: {
type: String,
default: 'manaClaim',
},
claimLabel: {
type: String,
default: 'Claim mana',
},
});
const manaCode = ref(props.manaCode);
</script>
<template>
<div class="column q-gutter-y-sm q-mt-sm">
<QRadio
v-model="manaCode"
dense
:val="manaVal"
:label="t(manaLabel)"
:dark="true"
class="q-mb-sm"
/>
<QRadio
v-model="manaCode"
dense
:val="manaClaimVal"
:label="t(claimLabel)"
:dark="true"
class="q-mb-sm"
/>
</div>
</template>

View File

@ -288,3 +288,7 @@ input::-webkit-inner-spin-button {
color: $info; color: $info;
} }
} }
.no-visible {
visibility: hidden;
}

View File

@ -304,12 +304,14 @@ globals:
from: From from: From
To: To To: To
stateFk: State stateFk: State
departmentFk: Department
email: Email email: Email
SSN: SSN SSN: SSN
fi: FI fi: FI
myTeam: My team
departmentFk: Department
changePass: Change password changePass: Change password
deleteConfirmTitle: Delete selected elements deleteConfirmTitle: Delete selected elements
changeState: Change state
errors: errors:
statusUnauthorized: Access denied statusUnauthorized: Access denied
statusInternalServerError: An internal server error has ocurred statusInternalServerError: An internal server error has ocurred
@ -556,7 +558,6 @@ ticket:
package: Package package: Package
taxClass: Tax class taxClass: Tax class
services: Services services: Services
changeState: Change state
requester: Requester requester: Requester
atender: Atender atender: Atender
request: Request request: Request

View File

@ -312,8 +312,10 @@ globals:
email: Correo email: Correo
SSN: NSS SSN: NSS
fi: NIF fi: NIF
myTeam: Mi equipo
changePass: Cambiar contraseña changePass: Cambiar contraseña
deleteConfirmTitle: Eliminar los elementos seleccionados deleteConfirmTitle: Eliminar los elementos seleccionados
changeState: Cambiar estado
errors: errors:
statusUnauthorized: Acceso denegado statusUnauthorized: Acceso denegado
statusInternalServerError: Ha ocurrido un error interno del servidor statusInternalServerError: Ha ocurrido un error interno del servidor
@ -565,7 +567,6 @@ ticket:
package: Embalaje package: Embalaje
taxClass: Tipo IVA taxClass: Tipo IVA
services: Servicios services: Servicios
changeState: Cambiar estado
requester: Solicitante requester: Solicitante
atender: Comprador atender: Comprador
request: Petición de compra request: Petición de compra

View File

@ -204,7 +204,7 @@ function claimUrl(section) {
top top
color="black" color="black"
text-color="white" text-color="white"
:label="t('ticket.summary.changeState')" :label="t('globals.changeState')"
> >
<QList> <QList>
<QVirtualScroll <QVirtualScroll

View File

@ -11,6 +11,7 @@ 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';
import FetchedTags from 'src/components/ui/FetchedTags.vue'; import FetchedTags from 'src/components/ui/FetchedTags.vue';
import VnToSummary from 'src/components/ui/VnToSummary.vue';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
@ -163,14 +164,12 @@ const fetchEntryBuys = async () => {
data-key="EntrySummary" data-key="EntrySummary"
> >
<template #header-left> <template #header-left>
<router-link <VnToSummary
v-if="route?.name !== 'EntrySummary'" v-if="route?.name !== 'EntrySummary'"
:to="{ name: 'EntrySummary', params: { id: entityId } }" :route-name="'EntrySummary'"
class="header link" :entity-id="entityId"
:href="entryUrl" :url="entryUrl"
> />
<QIcon name="open_in_new" color="white" size="sm" />
</router-link>
</template> </template>
<template #header> <template #header>
<span>{{ entry.id }} - {{ entry.supplier.nickname }}</span> <span>{{ entry.id }} - {{ entry.supplier.nickname }}</span>

View File

@ -78,7 +78,7 @@ const ticketsColumns = ref([
align: 'left', align: 'left',
}, },
{ {
name: 'quantity', name: 'nickname',
label: t('invoiceOut.summary.nickname'), label: t('invoiceOut.summary.nickname'),
field: (row) => row.nickname, field: (row) => row.nickname,
sortable: true, sortable: true,
@ -172,11 +172,11 @@ const ticketsColumns = ref([
</QBtn> </QBtn>
</QTd> </QTd>
</template> </template>
<template #body-cell-quantity="{ value, row }"> <template #body-cell-nickname="{ value, row }">
<QTd> <QTd>
<QBtn class="no-uppercase link" flat dense> <QBtn class="no-uppercase link" flat dense>
{{ value }} {{ value }}
<CustomerDescriptorProxy :id="row.id" /> <CustomerDescriptorProxy :id="row.clientFk" />
</QBtn> </QBtn>
</QTd> </QTd>
</template> </template>

View File

@ -229,7 +229,7 @@ onBeforeMount(() => {
> >
<template #body-cell-id="{ row }"> <template #body-cell-id="{ row }">
<QTd> <QTd>
<QBtn flat color="primary"> {{ row.ticketFk }}</QBtn> <QBtn flat class="link"> {{ row.ticketFk }}</QBtn>
<TicketDescriptorProxy :id="row.ticketFk" /> <TicketDescriptorProxy :id="row.ticketFk" />
</QTd> </QTd>
</template> </template>
@ -251,22 +251,35 @@ onBeforeMount(() => {
</template> </template>
<template #body-cell-requester="{ row }"> <template #body-cell-requester="{ row }">
<QTd> <QTd>
<QBtn flat dense color="primary"> {{ row.requesterName }}</QBtn> <QBtn flat dense class="link"> {{ row.requesterName }}</QBtn>
<WorkerDescriptorProxy :id="row.requesterFk" /> <WorkerDescriptorProxy :id="row.requesterFk" />
</QTd> </QTd>
</template> </template>
<template #body-cell-attender="{ row }"> <template #body-cell-attender="{ row }">
<QTd> <QTd>
<VnSelect <VnSelect
url="Workers/search"
v-model="row.attenderFk" v-model="row.attenderFk"
:where="{ role: 'buyer' }" :params="{ departmentCodes: ['shopping'] }"
sort-by="id" :fields="['id', 'nickname']"
url="Workers" sort-by="nickname ASC"
hide-selected hide-selected
option-label="firstName" option-label="nickname"
option-value="id" option-value="id"
dense dense
/> >
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{ scope.opt?.name }}</QItemLabel>
<QItemLabel caption
>{{ scope.opt?.nickname }},
{{ scope.opt?.code }}</QItemLabel
>
</QItemSection>
</QItem>
</template>
</VnSelect>
</QTd> </QTd>
</template> </template>
<template #body-cell-item="{ row }"> <template #body-cell-item="{ row }">
@ -292,7 +305,7 @@ onBeforeMount(() => {
</template> </template>
<template #body-cell-concept="{ row }"> <template #body-cell-concept="{ row }">
<QTd> <QTd>
<QBtn flat dense color="primary"> {{ row.itemDescription }}</QBtn> <QBtn flat dense class="link"> {{ row.itemDescription }}</QBtn>
<ItemDescriptorProxy :id="row.itemFk" /> <ItemDescriptorProxy :id="row.itemFk" />
</QTd> </QTd>
</template> </template>

View File

@ -174,6 +174,16 @@ const decrement = (paramsObj, key) => {
</VnSelect> </VnSelect>
</QItemSection> </QItemSection>
</QItem> </QItem>
<QItem>
<QItemSection>
<QCheckbox
v-model="params.myTeam"
:label="t('params.myTeam')"
@update:model-value="searchFn()"
toggle-indeterminate
/>
</QItemSection>
</QItem>
<QCard bordered> <QCard bordered>
<QItem> <QItem>
<QItemSection> <QItemSection>
@ -274,11 +284,11 @@ en:
to: To to: To
mine: For me mine: For me
state: State state: State
myTeam: My team
dateFiltersTooltip: Cannot choose a range of dates and days onward at the same time dateFiltersTooltip: Cannot choose a range of dates and days onward at the same time
denied: Denied denied: Denied
accepted: Accepted accepted: Accepted
pending: Pending pending: Pending
es: es:
params: params:
search: Búsqueda general search: Búsqueda general
@ -291,6 +301,7 @@ es:
to: Hasta to: Hasta
mine: Para mi mine: Para mi
state: Estado state: Estado
myTeam: Mi equipo
dateFiltersTooltip: No se puede seleccionar un rango de fechas y días en adelante a la vez dateFiltersTooltip: No se puede seleccionar un rango de fechas y días en adelante a la vez
denied: Denegada denied: Denegada
accepted: Aceptada accepted: Aceptada

View File

@ -52,15 +52,27 @@ const redirectToItemTypeBasicData = (_, { id }) => {
</VnRow> </VnRow>
<VnRow> <VnRow>
<VnSelect <VnSelect
url="Workers/search"
v-model="data.workerFk" v-model="data.workerFk"
:label="t('itemType.shared.worker')" :label="t('shared.worker')"
url="Workers" sort-by="nickname ASC"
sort-by="firstName ASC" :fields="['id', 'nickname']"
:fields="['id', 'firstName']" :params="{ departmentCodes: ['shopping'] }"
option-label="nickname"
option-value="id" option-value="id"
option-label="firstName"
hide-selected hide-selected
/> ><template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{ scope.opt?.name }}</QItemLabel>
<QItemLabel caption
>{{ scope.opt?.nickname }},
{{ scope.opt?.code }}</QItemLabel
>
</QItemSection>
</QItem>
</template>
</VnSelect>
<VnSelect <VnSelect
v-model="data.categoryFk" v-model="data.categoryFk"
:label="t('itemType.shared.category')" :label="t('itemType.shared.category')"

View File

@ -41,15 +41,27 @@ const temperaturesOptions = ref([]);
</VnRow> </VnRow>
<VnRow> <VnRow>
<VnSelect <VnSelect
url="Workers/search"
v-model="data.workerFk" v-model="data.workerFk"
:label="t('shared.worker')" :label="t('shared.worker')"
url="Workers" sort-by="nickname ASC"
sort-by="firstName ASC" :fields="['id', 'nickname']"
:fields="['id', 'firstName']" :params="{ departmentCodes: ['shopping'] }"
option-label="nickname"
option-value="id" option-value="id"
option-label="firstName"
hide-selected hide-selected
/> ><template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{ scope.opt?.name }}</QItemLabel>
<QItemLabel caption
>{{ scope.opt?.nickname }},
{{ scope.opt?.code }}</QItemLabel
>
</QItemSection>
</QItem>
</template></VnSelect
>
<VnSelect <VnSelect
v-model="data.categoryFk" v-model="data.categoryFk"
:label="t('shared.category')" :label="t('shared.category')"

View File

@ -6,6 +6,7 @@ import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.v
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 VnToSummary from 'src/components/ui/VnToSummary.vue';
onUpdated(() => summaryRef.value.fetch()); onUpdated(() => summaryRef.value.fetch());
@ -55,6 +56,11 @@ async function setItemTypeData(data) {
> >
<QIcon name="open_in_new" color="white" size="sm" /> <QIcon name="open_in_new" color="white" size="sm" />
</router-link> </router-link>
<VnToSummary
v-if="route?.name !== 'ItemTypeSummary'"
:route-name="'ItemTypeSummary'"
:entity-id="entityId"
/>
</template> </template>
<template #header> <template #header>
<span> <span>

View File

@ -12,6 +12,7 @@ import VnInputTime from 'components/common/VnInputTime.vue';
import axios from 'axios'; import axios from 'axios';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
import { useAcl } from 'src/composables/useAcl';
import { useValidator } from 'src/composables/useValidator'; import { useValidator } from 'src/composables/useValidator';
import { toTimeFormat } from 'filters/date.js'; import { toTimeFormat } from 'filters/date.js';
@ -28,14 +29,17 @@ const { validate } = useValidator();
const { notify } = useNotify(); const { notify } = useNotify();
const router = useRouter(); const router = useRouter();
const { t } = useI18n(); const { t } = useI18n();
const agencyFetchRef = ref(null); const canEditZone = useAcl().hasAny([
const zonesFetchRef = ref(null); { model: 'Ticket', props: 'editZone', accessType: 'WRITE' },
]);
const agencyFetchRef = ref();
const warehousesOptions = ref([]); const warehousesOptions = ref([]);
const companiesOptions = ref([]); const companiesOptions = ref([]);
const agenciesOptions = ref([]); const agenciesOptions = ref([]);
const zonesOptions = ref([]); const zonesOptions = ref([]);
const addresses = ref([]); const addresses = ref([]);
const zoneSelectRef = ref();
const formData = ref($props.formData); const formData = ref($props.formData);
watch( watch(
@ -44,6 +48,8 @@ watch(
{ deep: true } { deep: true }
); );
onMounted(() => onFormModelInit());
const agencyByWarehouseFilter = computed(() => ({ const agencyByWarehouseFilter = computed(() => ({
fields: ['id', 'name'], fields: ['id', 'name'],
order: 'name ASC', order: 'name ASC',
@ -52,18 +58,16 @@ const agencyByWarehouseFilter = computed(() => ({
}, },
})); }));
function zoneWhere() { const zoneWhere = computed(() => {
if (formData?.value?.agencyModeFk) { return formData.value?.agencyModeFk
return formData.value?.agencyModeFk ? {
? { shipped: formData.value?.shipped,
shipped: formData.value?.shipped, addressFk: formData.value?.addressFk,
addressFk: formData.value?.addressFk, agencyModeFk: formData.value?.agencyModeFk,
agencyModeFk: formData.value?.agencyModeFk, warehouseFk: formData.value?.warehouseFk,
warehouseFk: formData.value?.warehouseFk, }
} : {};
: {}; });
}
}
const getLanded = async (params) => { const getLanded = async (params) => {
try { try {
@ -270,7 +274,17 @@ const redirectToCustomerAddress = () => {
}); });
}; };
onMounted(() => onFormModelInit()); async function getZone(options) {
if (!zoneId.value) return;
const zone = options.find((z) => z.id == zoneId.value);
if (zone) return;
const { data } = await axios.get('Zones/' + zoneId.value, {
params: { filter: JSON.stringify({ fields: ['id', 'name'] }) },
});
zoneSelectRef.value.opts.push(data);
}
</script> </script>
<template> <template>
<FetchData <FetchData
@ -416,6 +430,7 @@ onMounted(() => onFormModelInit());
:rules="validate('basicData.agency')" :rules="validate('basicData.agency')"
/> />
<VnSelect <VnSelect
ref="zoneSelectRef"
:label="t('basicData.zone')" :label="t('basicData.zone')"
v-model="zoneId" v-model="zoneId"
option-value="id" option-value="id"
@ -424,11 +439,10 @@ onMounted(() => onFormModelInit());
:fields="['id', 'name']" :fields="['id', 'name']"
sort-by="id" sort-by="id"
:where="zoneWhere" :where="zoneWhere"
hide-selected
map-options
:required="true"
@focus="zonesFetchRef.fetch()"
:rules="validate('basicData.zone')" :rules="validate('basicData.zone')"
:required="true"
:disable="!canEditZone"
@update:options="getZone"
> >
<template #option="scope"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem v-bind="scope.itemProps">

View File

@ -3,7 +3,7 @@ import axios from 'axios';
import { ref, toRefs } from 'vue'; import { ref, toRefs } from 'vue';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { usePrintService } from 'composables/usePrintService'; import { usePrintService } from 'composables/usePrintService';
import SendEmailDialog from 'components/common/SendEmailDialog.vue'; import SendEmailDialog from 'components/common/SendEmailDialog.vue';
import VnConfirm from 'components/ui/VnConfirm.vue'; import VnConfirm from 'components/ui/VnConfirm.vue';
@ -23,6 +23,7 @@ const props = defineProps({
required: true, required: true,
}, },
}); });
const route = useRoute();
const { push, currentRoute } = useRouter(); const { push, currentRoute } = useRouter();
const { dialog, notify } = useQuasar(); const { dialog, notify } = useQuasar();
@ -40,6 +41,8 @@ const isEditable = ref();
const hasInvoicing = useAcl('invoicing'); const hasInvoicing = useAcl('invoicing');
const hasPdf = ref(); const hasPdf = ref();
const weight = ref(); const weight = ref();
const hasDocuwareFile = ref();
const quasar = useQuasar();
const actions = { const actions = {
clone: async () => { clone: async () => {
const opts = { message: t('Ticket cloned'), type: 'positive' }; const opts = { message: t('Ticket cloned'), type: 'positive' };
@ -331,10 +334,49 @@ async function handleInvoiceOutData() {
}); });
hasPdf.value = data[0]?.hasPdf; hasPdf.value = data[0]?.hasPdf;
} }
async function docuwareDownload() {
await axios.get(`Tickets/${ticketId}/docuwareDownload`);
}
async function hasDocuware() {
const { data } = await axios.post(`Docuwares/${ticketId}/checkFile`, {
fileCabinet: 'deliveryNote',
signed: true,
});
hasDocuwareFile.value = data;
}
async function uploadDocuware(force) {
console.log('force: ', force);
if (!force)
return quasar
.dialog({
component: VnConfirm,
componentProps: {
title: t('Send PDF to tablet'),
message: t('Are you sure you want to replace this delivery note?'),
},
})
.onOk(async () => {
uploadDocuware(true);
});
const { data } = await axios.post(`Docuwares/upload`, {
fileCabinet: 'deliveryNote',
ticketIds: [parseInt(ticketId)],
});
if (data) notify({ message: t('PDF sent!'), type: 'positive' });
}
</script> </script>
<template> <template>
<FetchData <FetchData
:url="`Tickets/${ticketId}/isEditable`" :url="
route.path.startsWith('/ticket')
? `Tickets/${ticketId}/isEditable`
: `Tickets/${ticket}/isEditable`
"
auto-load auto-load
@on-fetch="handleFetchData" @on-fetch="handleFetchData"
/> />
@ -452,7 +494,13 @@ async function handleInvoiceOutData() {
<QItemSection side> <QItemSection side>
<QIcon name="keyboard_arrow_right" /> <QIcon name="keyboard_arrow_right" />
</QItemSection> </QItemSection>
<QMenu anchor="top end" self="top start" auto-close bordered> <QMenu
anchor="top end"
self="top start"
auto-close
bordered
@click="hasDocuware()"
>
<QList> <QList>
<QItem @click="openDeliveryNote('deliveryNote')" v-ripple clickable> <QItem @click="openDeliveryNote('deliveryNote')" v-ripple clickable>
<QItemSection>{{ t('as PDF') }}</QItemSection> <QItemSection>{{ t('as PDF') }}</QItemSection>
@ -460,6 +508,14 @@ async function handleInvoiceOutData() {
<QItem @click="openDeliveryNote('withoutPrices')" v-ripple clickable> <QItem @click="openDeliveryNote('withoutPrices')" v-ripple clickable>
<QItemSection>{{ t('as PDF without prices') }}</QItemSection> <QItemSection>{{ t('as PDF without prices') }}</QItemSection>
</QItem> </QItem>
<QItem
v-if="hasDocuwareFile"
@click="docuwareDownload()"
v-ripple
clickable
>
<QItemSection>{{ t('as PDF signed') }}</QItemSection>
</QItem>
<QItem <QItem
@click="openDeliveryNote('deliveryNote', 'csv')" @click="openDeliveryNote('deliveryNote', 'csv')"
v-ripple v-ripple
@ -478,7 +534,7 @@ async function handleInvoiceOutData() {
<QItemSection side> <QItemSection side>
<QIcon name="keyboard_arrow_right" /> <QIcon name="keyboard_arrow_right" />
</QItemSection> </QItemSection>
<QMenu anchor="top end" self="top start" auto-close> <QMenu anchor="top end" self="top start" auto-close @click="hasDocuware()">
<QList> <QList>
<QItem <QItem
@click="sendDeliveryNoteConfirmation('deliveryNote')" @click="sendDeliveryNoteConfirmation('deliveryNote')"
@ -487,11 +543,7 @@ async function handleInvoiceOutData() {
> >
<QItemSection>{{ t('Send PDF') }}</QItemSection> <QItemSection>{{ t('Send PDF') }}</QItemSection>
</QItem> </QItem>
<QItem <QItem @click="uploadDocuware(!hasDocuwareFile)" v-ripple clickable>
@click="sendDeliveryNoteConfirmation('withoutPrices')"
v-ripple
clickable
>
<QItemSection>{{ t('Send PDF to tablet') }}</QItemSection> <QItemSection>{{ t('Send PDF to tablet') }}</QItemSection>
</QItem> </QItem>
<QItem <QItem
@ -695,4 +747,6 @@ es:
invoiceIds: "Se han generado las facturas con los siguientes ids: {invoiceIds}" invoiceIds: "Se han generado las facturas con los siguientes ids: {invoiceIds}"
This ticket will be removed from current route! Continue anyway?: ¡Se eliminará el ticket de la ruta actual! ¿Continuar de todas formas? This ticket will be removed from current route! Continue anyway?: ¡Se eliminará el ticket de la ruta actual! ¿Continuar de todas formas?
You are going to delete this ticket: Vas a eliminar este ticket You are going to delete this ticket: Vas a eliminar este ticket
as PDF signed: como PDF firmado
Are you sure you want to replace this delivery note?: ¿Seguro que quieres reemplazar este albarán?
</i18n> </i18n>

View File

@ -1,8 +1,8 @@
<script setup> <script setup>
import { ref } from 'vue'; import { ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { toCurrency } from 'src/filters'; import { toCurrency } from 'src/filters';
import VnUsesMana from 'components/ui/VnUsesMana.vue';
const $props = defineProps({ const $props = defineProps({
mana: { mana: {
@ -13,12 +13,21 @@ const $props = defineProps({
type: Number, type: Number,
default: 0, default: 0,
}, },
usesMana: {
type: Boolean,
default: false,
},
manaCode: {
type: String,
default: 'mana',
},
}); });
const emit = defineEmits(['save', 'cancel']); const emit = defineEmits(['save', 'cancel']);
const { t } = useI18n(); const { t } = useI18n();
const QPopupProxyRef = ref(null); const QPopupProxyRef = ref(null);
const manaCode = ref($props.manaCode);
const save = () => { const save = () => {
emit('save'); emit('save');
@ -47,6 +56,9 @@ const cancel = () => {
</div> </div>
</div> </div>
</div> </div>
<div v-if="usesMana" class="column q-gutter-y-sm q-mt-sm">
<VnUsesMana :mana-code="manaCode" />
</div>
<div class="row"> <div class="row">
<QBtn <QBtn
color="primary" color="primary"

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { onMounted, ref, computed, onUnmounted, watch } from 'vue'; import { onMounted, ref, computed, onUnmounted } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
@ -15,6 +15,8 @@ import useNotify from 'src/composables/useNotify.js';
import { toDateTimeFormat } from 'src/filters/date'; import { toDateTimeFormat } from 'src/filters/date';
import axios from 'axios'; import axios from 'axios';
import VnTable from 'src/components/VnTable/VnTable.vue'; import VnTable from 'src/components/VnTable/VnTable.vue';
import VnBtnSelect from 'src/components/common/VnBtnSelect.vue';
import FetchData from 'src/components/FetchData.vue';
const route = useRoute(); const route = useRoute();
const stateStore = useStateStore(); const stateStore = useStateStore();
@ -23,50 +25,24 @@ const { notify } = useNotify();
const { openConfirmationModal } = useVnConfirm(); const { openConfirmationModal } = useVnConfirm();
const newTicketDialogRef = ref(null); const newTicketDialogRef = ref(null);
const logsTableDialogRef = ref(null); const logsTableDialogRef = ref(null);
const tableRef = ref(); const vnTableRef = ref();
const expeditionsLogsData = ref([]); const expeditionsLogsData = ref([]);
const selectedExpeditions = ref([]); const selectedExpeditions = ref([]);
const allColumnNames = ref([]); const allColumnNames = ref([]);
const newTicketWithRoute = ref(false); const newTicketWithRoute = ref(false);
const selectedRows = ref([]); const selectedRows = ref([]);
const hasSelectedRows = computed(() => selectedRows.value.length > 0); const hasSelectedRows = computed(() => selectedRows.value.length > 0);
const expeditionStateTypes = ref([]);
const exprBuilder = (param, value) => {
switch (param) {
case 'expeditionFk':
return { id: value };
case 'packageItemName':
return { packagingItemFk: value };
}
};
const expeditionsFilter = computed(() => ({ const expeditionsFilter = computed(() => ({
where: { ticketFk: route.params.id }, where: { ticketFk: route.params.id },
order: ['created DESC'], order: ['created DESC'],
})); }));
const expeditionsArrayData = useArrayData('ticketExpeditions', {
url: 'Expeditions/filter',
filter: expeditionsFilter.value,
exprBuilder: exprBuilder,
});
const ticketArrayData = useArrayData('ticketData'); const ticketArrayData = useArrayData('ticketData');
const ticketStore = ticketArrayData.store; const ticketStore = ticketArrayData.store;
const ticketData = computed(() => ticketStore.data); const ticketData = computed(() => ticketStore.data);
const refetchExpeditions = async () => {
await expeditionsArrayData.applyFilter({
filter: expeditionsFilter.value,
});
};
watch(
() => route.params.id,
async () => await refetchExpeditions(),
{ immediate: true }
);
const columns = computed(() => [ const columns = computed(() => [
{ {
align: 'left', align: 'left',
@ -188,12 +164,10 @@ const showNewTicketDialog = (withRoute = false) => {
const deleteExpedition = async () => { const deleteExpedition = async () => {
try { try {
const expeditionIds = selectedExpeditions.value.map( const expeditionIds = selectedRows.value.map((expedition) => expedition.id);
(expedition) => expedition.id
);
const params = { expeditionIds }; const params = { expeditionIds };
await axios.post('Expeditions/deleteExpeditions', params); await axios.post('Expeditions/deleteExpeditions', params);
await refetchExpeditions(); vnTableRef.value.reload();
selectedExpeditions.value = []; selectedExpeditions.value = [];
notify(t('expedition.expeditionRemoved'), 'positive'); notify(t('expedition.expeditionRemoved'), 'positive');
} catch (error) { } catch (error) {
@ -216,17 +190,11 @@ const getExpeditionState = async (expedition) => {
const { data: expeditionStates } = await axios.get(`ExpeditionStates/filter`, { const { data: expeditionStates } = await axios.get(`ExpeditionStates/filter`, {
params: { filter: JSON.stringify(filter) }, params: { filter: JSON.stringify(filter) },
}); });
const { data: scannedStates } = await axios.get(`ExpeditionStates`, {
params: { filter: JSON.stringify(filter), fields: ['id', 'isScanned'] },
});
expeditionsLogsData.value = expeditionStates.map((state) => { expeditionsLogsData.value = expeditionStates.map((state) => ({
const scannedState = scannedStates.find((s) => s.id === state.id); ...state,
return { isScanned: !!state.isScanned,
...state, }));
isScanned: scannedState ? scannedState.isScanned : false,
};
});
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }
@ -242,9 +210,35 @@ onUnmounted(() => (stateStore.rightDrawer = false));
</script> </script>
<template> <template>
<FetchData
url="expeditionStateTypes"
@on-fetch="(data) => (expeditionStateTypes = data)"
auto-load
/>
<VnSubToolbar> <VnSubToolbar>
<template #st-actions> <template #st-actions>
<QBtnGroup push class="q-gutter-x-sm" flat> <QBtnGroup push class="q-gutter-x-sm" flat>
<VnBtnSelect
:disable="!hasSelectedRows"
color="primary"
:label="t('globals.changeState')"
:select-props="{
options: expeditionStateTypes,
optionLabel: 'description',
optionValue: 'code',
}"
:promise="
async (stateCode) => {
await axios.post('ExpeditionStates/addExpeditionState', {
expeditions: selectedRows.map(({ id }) => {
return { expeditionFk: id, stateCode };
}),
});
vnTableRef.tableRef.clearSelection();
vnTableRef.reload();
}
"
/>
<QBtnDropdown <QBtnDropdown
ref="btnDropdownRef" ref="btnDropdownRef"
color="primary" color="primary"
@ -298,11 +292,11 @@ onUnmounted(() => (stateStore.rightDrawer = false));
</QBtnGroup> </QBtnGroup>
</template> </template>
</VnSubToolbar> </VnSubToolbar>
<VnTable <VnTable
ref="tableRef" ref="vnTableRef"
data-key="TicketExpedition" data-key="TicketExpedition"
url="Expeditions/filter" url="Expeditions/filter"
search-url="expeditions"
:columns="columns" :columns="columns"
:filter="expeditionsFilter" :filter="expeditionsFilter"
v-model:selected="selectedRows" v-model:selected="selectedRows"
@ -311,6 +305,16 @@ onUnmounted(() => (stateStore.rightDrawer = false));
selection: 'multiple', selection: 'multiple',
}" }"
auto-load auto-load
:expr-builder="
(param, value) => {
switch (param) {
case 'expeditionFk':
return { id: value };
case 'packageItemName':
return { packagingItemFk: value };
}
}
"
order="created DESC" order="created DESC"
> >
<template #column-packagingItemFk="{ row }"> <template #column-packagingItemFk="{ row }">
@ -324,7 +328,7 @@ onUnmounted(() => (stateStore.rightDrawer = false));
<ExpeditionNewTicket <ExpeditionNewTicket
:ticket="ticketData" :ticket="ticketData"
:with-route="newTicketWithRoute" :with-route="newTicketWithRoute"
:selected-expeditions="selectedExpeditions" :selected-expeditions="selectedRows"
/> />
</QDialog> </QDialog>
<QDialog ref="logsTableDialogRef" transition-show="scale" transition-hide="scale"> <QDialog ref="logsTableDialogRef" transition-show="scale" transition-hide="scale">
@ -345,11 +349,7 @@ onUnmounted(() => (stateStore.rightDrawer = false));
</template> </template>
<template #body-cell-isScanned="{ row }"> <template #body-cell-isScanned="{ row }">
<QTd style="text-align: center"> <QTd style="text-align: center">
<QCheckbox disable v-model="row.isScanned"> <QCheckbox disable v-model="row.isScanned" />
{{
row.isScanned === 1 ? t('expedition.yes') : t('expedition.no')
}}
</QCheckbox>
</QTd> </QTd>
</template> </template>
</QTable> </QTable>

View File

@ -22,6 +22,7 @@ import { useVnConfirm } from 'composables/useVnConfirm';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
import axios from 'axios'; import axios from 'axios';
import VnTable from 'src/components/VnTable/VnTable.vue'; import VnTable from 'src/components/VnTable/VnTable.vue';
import VnUsesMana from 'src/components/ui/VnUsesMana.vue';
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
@ -768,6 +769,8 @@ watch(
<TicketEditManaProxy <TicketEditManaProxy
:mana="mana" :mana="mana"
:new-price="getNewPrice" :new-price="getNewPrice"
:uses-mana="usesMana"
:mana-code="manaCode"
@save="changeDiscount(row)" @save="changeDiscount(row)"
> >
<VnInput <VnInput
@ -775,6 +778,9 @@ watch(
:label="t('ticketSale.discount')" :label="t('ticketSale.discount')"
type="number" type="number"
/> />
<div v-if="usesMana" class="column q-gutter-y-sm q-mt-sm">
<VnUsesMana :mana-code="manaCode" />
</div>
</TicketEditManaProxy> </TicketEditManaProxy>
</template> </template>
<span v-else>{{ toPercentage(row.discount / 100) }}</span> <span v-else>{{ toPercentage(row.discount / 100) }}</span>

View File

@ -19,6 +19,8 @@ import VnTitle from 'src/components/common/VnTitle.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import ZoneDescriptorProxy from 'src/pages/Zone/Card/ZoneDescriptorProxy.vue'; import ZoneDescriptorProxy from 'src/pages/Zone/Card/ZoneDescriptorProxy.vue';
import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelect from 'src/components/common/VnSelect.vue';
import TicketDescriptorMenu from './TicketDescriptorMenu.vue';
import VnToSummary from 'src/components/ui/VnToSummary.vue';
const route = useRoute(); const route = useRoute();
const { notify } = useNotify(); const { notify } = useNotify();
@ -68,7 +70,7 @@ function isEditable() {
async function changeState(value) { async function changeState(value) {
try { try {
stateBtnDropdownRef.value.hide(); stateBtnDropdownRef.value?.hide();
const formData = { const formData = {
ticketFk: entityId.value, ticketFk: entityId.value,
code: value, code: value,
@ -85,6 +87,10 @@ async function changeState(value) {
function toTicketUrl(section) { function toTicketUrl(section) {
return '#/ticket/' + entityId.value + '/' + section; return '#/ticket/' + entityId.value + '/' + section;
} }
function isOnTicketCard() {
const currentPath = route.path;
return currentPath.startsWith('/ticket');
}
</script> </script>
<template> <template>
@ -99,6 +105,14 @@ function toTicketUrl(section) {
:url="`Tickets/${entityId}/summary`" :url="`Tickets/${entityId}/summary`"
data-key="TicketSummary" data-key="TicketSummary"
> >
<template #header-left>
<VnToSummary
v-if="route?.name !== 'TicketSummary'"
:route-name="'TicketSummary'"
:entity-id="entityId"
:url="ticketUrl"
/>
</template>
<template #header="{ entity }"> <template #header="{ entity }">
<div> <div>
Ticket #{{ entity.id }} - {{ entity.client?.name }} ({{ Ticket #{{ entity.id }} - {{ entity.client?.name }} ({{
@ -112,7 +126,7 @@ function toTicketUrl(section) {
ref="stateBtnDropdownRef" ref="stateBtnDropdownRef"
color="black" color="black"
text-color="white" text-color="white"
:label="t('ticket.summary.changeState')" :label="t('globals.changeState')"
:disable="!isEditable()" :disable="!isEditable()"
> >
<VnSelect <VnSelect

View File

@ -463,6 +463,9 @@ onMounted(async () => {
userParams.dateToAdvance = today; userParams.dateToAdvance = today;
userParams.scopeDays = 1; userParams.scopeDays = 1;
userParams.warehouseFk = user.value.warehouseFk; userParams.warehouseFk = user.value.warehouseFk;
userParams.ipt = 'H';
userParams.futureIpt = 'H';
userParams.isFullMovable = true;
const filter = { limit: 0 }; const filter = { limit: 0 };
await arrayData.addFilter({ filter, userParams }); await arrayData.addFilter({ filter, userParams });
}); });

View File

@ -11,7 +11,7 @@ import axios from 'axios';
import { onMounted } from 'vue'; import { onMounted } from 'vue';
import VnInputNumber from 'src/components/common/VnInputNumber.vue'; import VnInputNumber from 'src/components/common/VnInputNumber.vue';
const { t } = useI18n(); const { t, te } = useI18n();
const props = defineProps({ const props = defineProps({
dataKey: { dataKey: {
type: String, type: String,
@ -43,6 +43,11 @@ const getItemPackingTypes = async () => {
} }
}; };
const getLocale = (val) => {
const param = `params.${val}`;
return te(param) ? t(param) : t(`globals.${param}`);
};
onMounted(async () => await getItemPackingTypes()); onMounted(async () => await getItemPackingTypes());
</script> </script>
@ -60,7 +65,7 @@ onMounted(async () => await getItemPackingTypes());
> >
<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>{{ getLocale(tag.label) }}: </strong>
<span>{{ formatFn(tag.value) }}</span> <span>{{ formatFn(tag.value) }}</span>
</div> </div>
</template> </template>
@ -136,6 +141,19 @@ onMounted(async () => await getItemPackingTypes());
/> />
</QItemSection> </QItemSection>
</QItem> </QItem>
<QItem>
<QItemSection>
<VnSelect
:label="t('globals.params.departmentFk')"
v-model="params.departmentFk"
url="Departments"
:fields="['id', 'name']"
dense
outlined
rounded
/>
</QItemSection>
</QItem>
<QItem> <QItem>
<QItemSection> <QItemSection>
<VnSelect <VnSelect

View File

@ -20,11 +20,31 @@ const provinces = ref([]);
const states = ref([]); const states = ref([]);
const agencies = ref([]); const agencies = ref([]);
const warehouses = ref([]); const warehouses = ref([]);
const groupedStates = ref([]);
const getGroupedStates = (data) => {
for (const state of data) {
groupedStates.value.push({
id: state.id,
name: t(`${state.code}`),
code: state.code,
});
}
};
</script> </script>
<template> <template>
<FetchData url="Provinces" @on-fetch="(data) => (provinces = data)" auto-load /> <FetchData url="Provinces" @on-fetch="(data) => (provinces = data)" auto-load />
<FetchData url="States" @on-fetch="(data) => (states = data)" auto-load /> <FetchData url="States" @on-fetch="(data) => (states = data)" auto-load />
<FetchData
url="AlertLevels"
@on-fetch="
(data) => {
getGroupedStates(data);
}
"
auto-load
/>
<FetchData url="AgencyModes" @on-fetch="(data) => (agencies = data)" auto-load /> <FetchData url="AgencyModes" @on-fetch="(data) => (agencies = data)" auto-load />
<FetchData url="Warehouses" @on-fetch="(data) => (warehouses = data)" auto-load /> <FetchData url="Warehouses" @on-fetch="(data) => (warehouses = data)" auto-load />
<VnFilterPanel :data-key="props.dataKey" :search-button="true" search-url="table"> <VnFilterPanel :data-key="props.dataKey" :search-button="true" search-url="table">
@ -90,12 +110,35 @@ const warehouses = ref([]);
option-label="name" option-label="name"
emit-value emit-value
map-options map-options
use-input
dense dense
outlined outlined
rounded rounded
/> />
</QItemSection> </QItemSection>
</QItem> </QItem>
<QItem>
<QItemSection v-if="!groupedStates">
<QSkeleton type="QInput" class="full-width" />
</QItemSection>
<QItemSection v-if="groupedStates">
<QSelect
:label="t('Grouped state')"
v-model="params.groupedStates"
@update:model-value="searchFn()"
:options="groupedStates"
option-value="id"
option-label="name"
emit-value
map-options
use-input
dense
outlined
rounded
sort-by="name ASC"
/>
</QItemSection>
</QItem>
<QItem> <QItem>
<QItemSection> <QItemSection>
<VnInput <VnInput
@ -114,6 +157,15 @@ const warehouses = ref([]);
/> />
</QItemSection> </QItemSection>
</QItem> </QItem>
<QItem>
<QItemSection>
<VnInput
v-model="params.nickname"
:label="t('Nickname')"
is-outlined
/>
</QItemSection>
</QItem>
<QItem> <QItem>
<QItemSection> <QItemSection>
<QCheckbox <QCheckbox
@ -176,6 +228,7 @@ const warehouses = ref([]);
option-label="name" option-label="name"
emit-value emit-value
map-options map-options
use-input
dense dense
outlined outlined
rounded rounded
@ -196,6 +249,7 @@ const warehouses = ref([]);
option-label="name" option-label="name"
emit-value emit-value
map-options map-options
use-input
dense dense
outlined outlined
rounded rounded
@ -216,12 +270,22 @@ const warehouses = ref([]);
option-label="name" option-label="name"
emit-value emit-value
map-options map-options
use-input
dense dense
outlined outlined
rounded rounded
/> />
</QItemSection> </QItemSection>
</QItem> </QItem>
<QItem>
<QItemSection>
<VnInput
v-model="params.collectionFk"
:label="t('Collection')"
is-outlined
/>
</QItemSection>
</QItem>
</QExpansionItem> </QExpansionItem>
</template> </template>
</VnFilterPanel> </VnFilterPanel>
@ -245,6 +309,11 @@ en:
provinceFk: Province provinceFk: Province
agencyModeFk: Agency agencyModeFk: Agency
warehouseFk: Warehouse warehouseFk: Warehouse
FREE: Free
ON_PREPARATION: On preparation
PACKED: Packed
DELIVERED: Delivered
ON_PREVIOUS: ON_PREVIOUS
es: es:
params: params:
search: Contiene search: Contiene
@ -278,4 +347,12 @@ es:
Yes: Si Yes: Si
No: No No: No
Days onward: Días adelante Days onward: Días adelante
Grouped state: Estado agrupado
FREE: Libre
ON_PREPARATION: En preparación
PACKED: Encajado
DELIVERED: Servido
ON_PREVIOUS: ON_PREVIOUS
Collection: Colección
Nickname: Nombre mostrado
</i18n> </i18n>

View File

@ -550,7 +550,7 @@ function setReference(data) {
</template> </template>
<template #column-salesPersonFk="{ row }"> <template #column-salesPersonFk="{ row }">
<span class="link" @click.stop> <span class="link" @click.stop>
{{ dashIfEmpty(row.salesPerson) }} {{ dashIfEmpty(row.userName) }}
<CustomerDescriptorProxy :id="row.salesPersonFk" /> <CustomerDescriptorProxy :id="row.salesPersonFk" />
</span> </span>
</template> </template>

View File

@ -100,7 +100,7 @@ weeklyTickets:
advanceTickets: advanceTickets:
preparation: Preparación preparation: Preparación
origin: Origen origin: Origen
destination: Destinatario destination: Destino
originAgency: 'Agencia origen: {agency}' originAgency: 'Agencia origen: {agency}'
destinationAgency: 'Agencia destino: {agency}' destinationAgency: 'Agencia destino: {agency}'
ticketId: ID ticketId: ID

View File

@ -0,0 +1,31 @@
import { ref, computed } from 'vue';
import { defineStore } from 'pinia';
export const useStateQueryStore = defineStore('stateQueryStore', () => {
const queries = ref(new Set());
function add(query) {
queries.value.add(query);
return query;
}
function isLoading() {
return computed(() => queries.value.size);
}
function remove(query) {
queries.value.delete(query);
}
function reset() {
queries.value = new Set();
}
return {
add,
isLoading,
remove,
queries,
reset,
};
});

View File

@ -0,0 +1,13 @@
/// <reference types="cypress" />
describe('Client consignee', () => {
beforeEach(() => {
cy.viewport(1280, 720);
cy.login('developer');
cy.visit('#/customer/1110/address', {
timeout: 5000,
});
});
it('Should load layout', () => {
cy.get('.q-card').should('be.visible');
});
});

View File

@ -0,0 +1,13 @@
/// <reference types="cypress" />
describe('Client balance', () => {
beforeEach(() => {
cy.viewport(1280, 720);
cy.login('developer');
cy.visit('#/customer/1101/balance', {
timeout: 5000,
});
});
it('Should load layout', () => {
cy.get('.q-card').should('be.visible');
});
});

View File

@ -0,0 +1,13 @@
/// <reference types="cypress" />
describe('Client basic data', () => {
beforeEach(() => {
cy.viewport(1280, 720);
cy.login('developer');
cy.visit('#/customer/1110/basic-data', {
timeout: 5000,
});
});
it('Should load layout', () => {
cy.get('.q-card').should('be.visible');
});
});

View File

@ -0,0 +1,13 @@
/// <reference types="cypress" />
describe('Client billing data', () => {
beforeEach(() => {
cy.viewport(1280, 720);
cy.login('developer');
cy.visit('#/customer/1110/billing-data', {
timeout: 5000,
});
});
it('Should load layout', () => {
cy.get('.q-card').should('be.visible');
});
});

View File

@ -0,0 +1,13 @@
/// <reference types="cypress" />
describe('Client credits', () => {
beforeEach(() => {
cy.viewport(1280, 720);
cy.login('developer');
cy.visit('#/customer/1110/credits', {
timeout: 5000,
});
});
it('Should load layout', () => {
cy.get('.q-card').should('be.visible');
});
});

View File

@ -0,0 +1,13 @@
/// <reference types="cypress" />
describe('Client fiscal data', () => {
beforeEach(() => {
cy.viewport(1280, 720);
cy.login('developer');
cy.visit('#/customer/1110/fiscal-data', {
timeout: 5000,
});
});
it('Should load layout', () => {
cy.get('.q-card').should('be.visible');
});
});

View File

@ -0,0 +1,13 @@
/// <reference types="cypress" />
describe('Client greuges', () => {
beforeEach(() => {
cy.viewport(1280, 720);
cy.login('developer');
cy.visit('#/customer/1101/greuges', {
timeout: 5000,
});
});
it('Should load layout', () => {
cy.get('.q-card').should('be.visible');
});
});

View File

@ -0,0 +1,63 @@
/// <reference types="cypress" />
describe('Client list', () => {
beforeEach(() => {
cy.viewport(1280, 720);
cy.login('developer');
cy.visit('/#/customer/list', {
timeout: 5000,
onBeforeLoad(win) {
cy.stub(win, 'open');
},
});
});
it('Client list create new client', () => {
cy.get('.q-page-sticky > div > .q-btn > .q-btn__content > .q-icon').click();
const data = {
Name: { val: 'Name 1' },
'Social name': { val: 'TEST 1' },
'Tax number': { val: '20852113Z' },
'Web user': { val: 'user_test_1' },
Street: { val: 'C/ STREET 1' },
Email: { val: 'user.test@1.com' },
'Business type': { val: 'Otros', type: 'select' },
'Sales person': { val: 'salesboss', type: 'select' },
Location: { val: '46000, Valencia(Province one), España', type: 'select' },
};
cy.fillInForm(data);
cy.get('.q-mt-lg > .q-btn--standard').click();
cy.checkNotification('created');
cy.url().should('include', '/summary');
});
it('Client list search client', () => {
const search = 'Jessica Jones';
cy.searchByLabel('Name', search);
cy.get('.title > span').should('have.text', search);
let id = null;
cy.get('.q-item > .q-item__label').then((text) => {
id = text.text().trim().split('#')[1];
cy.get('.q-item > .q-item__label').should('have.text', ` #${id}`);
cy.url().should('include', `/customer/${id}/summary`);
});
});
it('Client founded create ticket', () => {
const search = 'Jessica Jones';
cy.searchByLabel('Name', search);
cy.clickButtonsDescriptor(2);
cy.waitForElement('#formModel');
cy.waitForElement('.q-form');
cy.checkValueForm(1, search);
});
it('Client founded create order', () => {
const search = 'Jessica Jones';
cy.searchByLabel('Name', search);
cy.clickButtonsDescriptor(4);
cy.waitForElement('#formModel');
cy.waitForElement('.q-form');
cy.checkValueForm(2, search);
});
});

View File

@ -0,0 +1,13 @@
/// <reference types="cypress" />
describe('Client notes', () => {
beforeEach(() => {
cy.viewport(1280, 720);
cy.login('developer');
cy.visit('#/customer/1110/notes', {
timeout: 5000,
});
});
it('Should load layout', () => {
cy.get('.q-card').should('be.visible');
});
});

View File

@ -0,0 +1,13 @@
/// <reference types="cypress" />
describe('Client recoveries', () => {
beforeEach(() => {
cy.viewport(1280, 720);
cy.login('developer');
cy.visit('#/customer/1101/recoveries', {
timeout: 5000,
});
});
it('Should load layout', () => {
cy.get('.q-card').should('be.visible');
});
});

View File

@ -0,0 +1,13 @@
/// <reference types="cypress" />
describe('Client web-access', () => {
beforeEach(() => {
cy.viewport(1280, 720);
cy.login('developer');
cy.visit('#/customer/1110/web-access', {
timeout: 5000,
});
});
it('Should load layout', () => {
cy.get('.q-card').should('be.visible');
});
});

View File

@ -0,0 +1,13 @@
/// <reference types="cypress" />
describe('Client credit opinion', () => {
beforeEach(() => {
cy.viewport(1280, 720);
cy.login('developer');
cy.visit('#/customer/1101/credit-management/credit-contracts', {
timeout: 5000,
});
});
it('Should load layout', () => {
cy.get('.q-card').should('be.visible');
});
});

View File

@ -0,0 +1,13 @@
/// <reference types="cypress" />
describe('Client credit opinion', () => {
beforeEach(() => {
cy.viewport(1280, 720);
cy.login('developer');
cy.visit('#/customer/1110/credit-management/credit-opinion', {
timeout: 5000,
});
});
it('Should load layout', () => {
cy.get('.q-card').should('be.visible');
});
});

View File

@ -0,0 +1,13 @@
/// <reference types="cypress" />
describe('Client consumption', () => {
beforeEach(() => {
cy.viewport(1280, 720);
cy.login('developer');
cy.visit('#/customer/1101/others/consumption', {
timeout: 5000,
});
});
it('Should load layout', () => {
cy.get('.q-card').should('be.visible');
});
});

View File

@ -0,0 +1,13 @@
/// <reference types="cypress" />
describe('Client contacts', () => {
beforeEach(() => {
cy.viewport(1280, 720);
cy.login('developer');
cy.visit('#/customer/1101/others/contacts', {
timeout: 5000,
});
});
it('Should load layout', () => {
cy.get('.q-card').should('be.visible');
});
});

View File

@ -0,0 +1,13 @@
/// <reference types="cypress" />
describe('Client mandates', () => {
beforeEach(() => {
cy.viewport(1280, 720);
cy.login('developer');
cy.visit('#/customer/1110/others/mandates', {
timeout: 5000,
});
});
it('Should load layout', () => {
cy.get('.q-card').should('be.visible');
});
});

View File

@ -0,0 +1,13 @@
/// <reference types="cypress" />
describe('Client samples', () => {
beforeEach(() => {
cy.viewport(1280, 720);
cy.login('developer');
cy.visit('#/customer/1101/others/samples', {
timeout: 5000,
});
});
it('Should load layout', () => {
cy.get('.q-card').should('be.visible');
});
});

View File

@ -0,0 +1,13 @@
/// <reference types="cypress" />
describe('Client unpaid', () => {
beforeEach(() => {
cy.viewport(1280, 720);
cy.login('developer');
cy.visit('#/customer/1110/others/unpaid', {
timeout: 5000,
});
});
it('Should load layout', () => {
cy.get('.q-card').should('be.visible');
});
});

View File

@ -0,0 +1,13 @@
/// <reference types="cypress" />
describe('Client web payments', () => {
beforeEach(() => {
cy.viewport(1280, 720);
cy.login('developer');
cy.visit('#/customer/1101/others/web-payments', {
timeout: 5000,
});
});
it('Should load layout', () => {
cy.get('.q-card').should('be.visible');
});
});

View File

@ -0,0 +1,28 @@
/// <reference types="cypress" />
describe('Ticket expedtion', () => {
const tableContent = '.q-table .q-virtual-scroll__content';
const stateTd = 'td:nth-child(9)';
beforeEach(() => {
cy.login('developer');
cy.viewport(1920, 1080);
});
it('should change the state', () => {
cy.visit('#/ticket/1/expedition');
cy.intercept('GET', /\/api\/Expeditions\/filter/).as('expeditions');
cy.intercept('POST', /\/api\/Expeditions\/crud/).as('crud');
cy.wait('@expeditions');
cy.selectRows([1, 2]);
cy.get('#subToolbar [aria-controls]:nth-child(1)').click();
cy.get('.q-menu .q-item').contains('Perdida').click();
cy.wait('@crud');
cy.get(`${tableContent} tr:nth-child(-n+2) ${stateTd}`).each(($el) => {
cy.wrap($el).contains('Perdida');
});
});
});

View File

@ -152,6 +152,14 @@ Cypress.Commands.add('notificationHas', (selector, text) => {
cy.get(selector).should('have.text', text); cy.get(selector).should('have.text', text);
}); });
Cypress.Commands.add('selectRows', (rows) => {
rows.forEach((row) => {
cy.get('.q-table .q-virtual-scroll__content tr .q-checkbox__inner')
.eq(row - 1)
.click();
});
});
Cypress.Commands.add('fillRow', (rowSelector, data) => { Cypress.Commands.add('fillRow', (rowSelector, data) => {
// Usar el selector proporcionado para obtener la fila deseada // Usar el selector proporcionado para obtener la fila deseada
cy.waitForElement('tbody'); cy.waitForElement('tbody');

View File

@ -7,41 +7,46 @@ vi.mock('src/composables/useSession', () => ({
getToken: () => 'DEFAULT_TOKEN', getToken: () => 'DEFAULT_TOKEN',
isLoggedIn: () => vi.fn(), isLoggedIn: () => vi.fn(),
destroy: () => vi.fn(), destroy: () => vi.fn(),
}) }),
}));
vi.mock('src/stores/useStateQueryStore', () => ({
useStateQueryStore: () => ({
add: () => vi.fn(),
remove: () => vi.fn(),
}),
})); }));
describe('Axios boot', () => { describe('Axios boot', () => {
describe('onRequest()', async () => { describe('onRequest()', async () => {
it('should set the "Authorization" property on the headers', async () => { it('should set the "Authorization" property on the headers', async () => {
const config = { headers: {} }; const config = { headers: {} };
const resultConfig = onRequest(config); const resultConfig = onRequest(config);
expect(resultConfig).toEqual(expect.objectContaining({ expect(resultConfig).toEqual(
headers: { expect.objectContaining({
Authorization: 'DEFAULT_TOKEN' headers: {
} Authorization: 'DEFAULT_TOKEN',
})); },
})
);
}); });
}) });
describe('onResponseError()', async () => { describe('onResponseError()', async () => {
it('should call to the Notify plugin with a message error for an status code "500"', async () => { it('should call to the Notify plugin with a message error for an status code "500"', async () => {
Notify.create = vi.fn() Notify.create = vi.fn();
const error = { const error = {
response: { response: {
status: 500 status: 500,
} },
}; };
const result = onResponseError(error); const result = onResponseError(error);
expect(result).rejects.toEqual(expect.objectContaining(error));
expect(result).rejects.toEqual(
expect.objectContaining(error)
);
expect(Notify.create).toHaveBeenCalledWith( expect(Notify.create).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
message: 'An internal server error has ocurred', message: 'An internal server error has ocurred',
@ -51,25 +56,22 @@ describe('Axios boot', () => {
}); });
it('should call to the Notify plugin with a message from the response property', async () => { it('should call to the Notify plugin with a message from the response property', async () => {
Notify.create = vi.fn() Notify.create = vi.fn();
const error = { const error = {
response: { response: {
status: 401, status: 401,
data: { data: {
error: { error: {
message: 'Invalid user or password' message: 'Invalid user or password',
} },
} },
} },
}; };
const result = onResponseError(error); const result = onResponseError(error);
expect(result).rejects.toEqual(expect.objectContaining(error));
expect(result).rejects.toEqual(
expect.objectContaining(error)
);
expect(Notify.create).toHaveBeenCalledWith( expect(Notify.create).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
message: 'Invalid user or password', message: 'Invalid user or password',
@ -77,5 +79,5 @@ describe('Axios boot', () => {
}) })
); );
}); });
}) });
}); });

View File

@ -0,0 +1,58 @@
import { describe, expect, it, beforeEach, beforeAll } from 'vitest';
import { createWrapper } from 'app/test/vitest/helper';
import { useStateQueryStore } from 'src/stores/useStateQueryStore';
describe('useStateQueryStore', () => {
beforeAll(() => {
createWrapper({}, {});
});
const stateQueryStore = useStateQueryStore();
const { add, isLoading, remove, reset } = useStateQueryStore();
const firstQuery = { url: 'myQuery' };
function getQueries() {
return stateQueryStore.queries;
}
beforeEach(() => {
reset();
expect(getQueries().size).toBeFalsy();
});
it('should add two queries', async () => {
expect(getQueries().size).toBeFalsy();
add(firstQuery);
expect(getQueries().size).toBeTruthy();
expect(getQueries().has(firstQuery)).toBeTruthy();
add();
expect(getQueries().size).toBe(2);
});
it('should add and remove loading state', async () => {
expect(isLoading().value).toBeFalsy();
add(firstQuery);
expect(isLoading().value).toBeTruthy();
remove(firstQuery);
expect(isLoading().value).toBeFalsy();
});
it('should add and remove query', async () => {
const secondQuery = { ...firstQuery };
const thirdQuery = { ...firstQuery };
add(firstQuery);
add(secondQuery);
const beforeCount = getQueries().size;
add(thirdQuery);
expect(getQueries().has(thirdQuery)).toBeTruthy();
remove(thirdQuery);
expect(getQueries().has(thirdQuery)).toBeFalsy();
expect(getQueries().size).toBe(beforeCount);
});
});