From c621ccb5a6e35552a25bc203575554c852d9f8d2 Mon Sep 17 00:00:00 2001
From: jcasado <jcasado.verdnatura@gmail.com>
Date: Tue, 23 Apr 2024 13:19:21 +0200
Subject: [PATCH 01/27] refs #7113 fix test

---
 test/cypress/support/commands.js | 29 ++++++++++++++++++++---------
 1 file changed, 20 insertions(+), 9 deletions(-)

diff --git a/test/cypress/support/commands.js b/test/cypress/support/commands.js
index f075d500ff..13103950cc 100755
--- a/test/cypress/support/commands.js
+++ b/test/cypress/support/commands.js
@@ -38,6 +38,15 @@ Cypress.Commands.add('login', (user) => {
         },
     }).then((response) => {
         window.localStorage.setItem('token', response.body.token);
+        cy.request({
+            method: 'GET',
+            url: '/api/VnUsers/ShareToken',
+            headers: {
+                Authorization: window.localStorage.getItem('token'),
+            },
+        }).then(({ body }) => {
+            window.localStorage.setItem('tokenMultimedia', body.multimediaToken.id);
+        });
     });
 });
 
@@ -52,16 +61,18 @@ Cypress.Commands.add('getValue', (selector) => {
         }
         // Si es un QSelect
         if ($el.find('.q-select__dropdown-icon').length) {
-            return cy.get(
-                selector +
-                    '> .q-field > .q-field__inner > .q-field__control > .q-field__control-container > .q-field__native > input'
-            ).invoke('val')
+            return cy
+                .get(
+                    selector +
+                        '> .q-field > .q-field__inner > .q-field__control > .q-field__control-container > .q-field__native > input'
+                )
+                .invoke('val');
         }
         // Si es un QSelect
         if ($el.find('span').length) {
-            return cy.get(
-                selector + ' span'
-            ).then(($span) => { return $span[0].innerText })
+            return cy.get(selector + ' span').then(($span) => {
+                return $span[0].innerText;
+            });
         }
         // Puedes añadir un log o lanzar un error si el elemento no es reconocido
         cy.log('Elemento no soportado');
@@ -132,13 +143,13 @@ Cypress.Commands.add('validateRow', (rowSelector, expectedValues) => {
     cy.get(rowSelector).within(() => {
         for (const [index, value] of expectedValues.entries()) {
             cy.log('CHECKING ', index, value);
-            if(value === undefined) continue
+            if (value === undefined) continue;
             if (typeof value == 'boolean') {
                 const prefix = value ? '' : 'not.';
                 cy.getValue(`:nth-child(${index + 1})`).should(`${prefix}be.checked`);
                 continue;
             }
-            cy.getValue(`:nth-child(${index + 1})`).should('equal', value)
+            cy.getValue(`:nth-child(${index + 1})`).should('equal', value);
         }
     });
 });

From 934db329733c81741074a22cdb39ea37446350d6 Mon Sep 17 00:00:00 2001
From: wbuezas <wbuezas@verdnatura.es>
Date: Wed, 24 Apr 2024 10:31:33 -0300
Subject: [PATCH 02/27] WIP

---
 src/components/CrudModel.vue     |   1 +
 src/css/app.scss                 |   5 +
 src/pages/Item/Card/ItemTags.vue | 176 ++++++++++++++++++++++++++++++-
 src/router/modules/item.js       |   4 +-
 4 files changed, 183 insertions(+), 3 deletions(-)

diff --git a/src/components/CrudModel.vue b/src/components/CrudModel.vue
index fb3ac10c38..deec0e4feb 100644
--- a/src/components/CrudModel.vue
+++ b/src/components/CrudModel.vue
@@ -81,6 +81,7 @@ defineExpose({
     hasChanges,
     saveChanges,
     getChanges,
+    formData,
 });
 
 async function fetch(data) {
diff --git a/src/css/app.scss b/src/css/app.scss
index 25b4846497..9037802ada 100644
--- a/src/css/app.scss
+++ b/src/css/app.scss
@@ -119,6 +119,11 @@ select:-webkit-autofill {
     font-variation-settings: 'FILL' 1;
 }
 
+.fill-icon-on-hover:hover {
+    font-variation-settings: 'FILL' 1;
+    cursor: pointer;
+}
+
 .vn-table-separation-row {
     height: 16px !important;
     background-color: var(--vn-section-color) !important;
diff --git a/src/pages/Item/Card/ItemTags.vue b/src/pages/Item/Card/ItemTags.vue
index 95f4380e44..bf18c682c0 100644
--- a/src/pages/Item/Card/ItemTags.vue
+++ b/src/pages/Item/Card/ItemTags.vue
@@ -1 +1,175 @@
-<template>Item tags (CREAR CUANDO SE DESARROLLE EL MODULO DE ITEMS)</template>
+<script setup>
+import { ref, onMounted, computed } from 'vue';
+import { useRoute } from 'vue-router';
+import { useI18n } from 'vue-i18n';
+
+import CrudModel from 'components/CrudModel.vue';
+import VnRow from 'components/ui/VnRow.vue';
+import VnInput from 'src/components/common/VnInput.vue';
+import FetchData from 'components/FetchData.vue';
+import VnSelectFilter from 'src/components/common/VnSelectFilter.vue';
+
+import axios from 'axios';
+
+const route = useRoute();
+const { t } = useI18n();
+
+const itemTagsRef = ref(null);
+const tagOptions = ref([]);
+
+// const getHighestPriority = () => {
+//     let max = 0;
+//     console.log('formData:: ', itemTagsRef.value.formData);
+//     itemTagsRef.value.formData.forEach((tag) => {
+//         if (tag.priority > max) max = tag.priority;
+//     });
+//     return max + 1;
+// };
+
+const getHighestPriority = computed(() => {
+    let max = 0;
+    if (!itemTagsRef.value || !itemTagsRef.value.length) return max;
+    console.log('formData:: ', itemTagsRef.value.formData);
+    itemTagsRef.value.formData.forEach((tag) => {
+        if (tag.priority > max) max = tag.priority;
+    });
+    return max + 1;
+});
+
+const getSelectedTagValues = async (tag) => {
+    try {
+        tag.value = null;
+        const filter = {
+            fields: ['value'],
+            order: 'value ASC',
+        };
+
+        const params = { filter: JSON.stringify(filter) };
+        const { data } = await axios.get(`Tags/${tag.selectedTag.id}/filterValue`, {
+            params,
+        });
+        tag.valueOptions = data;
+    } catch (err) {
+        console.error('Error getting selected tag values');
+    }
+};
+
+onMounted(() => {
+    if (itemTagsRef.value) itemTagsRef.value.reload();
+});
+</script>
+
+<template>
+    <FetchData
+        url="Tags"
+        :filter="{ fields: ['id', 'name', 'isFree', 'sourceTable'] }"
+        @on-fetch="(data) => (tagOptions = data)"
+        auto-load
+    />
+    <div class="full-width flex justify-center">
+        <QPage class="card-width q-pa-lg">
+            <CrudModel
+                ref="itemTagsRef"
+                :data-required="{
+                    itemFk: route.params.id,
+                    priority: getHighestPriority,
+                    tag: {
+                        isFree: undefined,
+                    },
+                }"
+                :default-remove="false"
+                :filter="{
+                    fields: ['id', 'itemFk', 'tagFk', 'value', 'priority'],
+                    where: { itemFk: route.params.id },
+                    order: 'priority ASC',
+                    include: {
+                        relation: 'tag',
+                        scope: {
+                            fields: ['id', 'name', 'isFree', 'sourceTable'],
+                        },
+                    },
+                }"
+                data-key="ItemTags"
+                model="ItemTags"
+                url="ItemTags"
+                save-url="Tags/onSubmit"
+            >
+                <template #body="{ rows }">
+                    <QCard class="q-pl-lg q-py-md">
+                        <VnRow
+                            v-for="(row, index) in rows"
+                            :key="index"
+                            class="row q-gutter-md q-mb-md"
+                        >
+                            <VnSelectFilter
+                                :label="t('Tag')"
+                                v-model="row.tagFk"
+                                :options="tagOptions"
+                                option-label="name"
+                                option-value="id"
+                                hide-selected
+                                @update:model-value="getSelectedTagValues(value)"
+                            />
+                            <VnSelectFilter
+                                v-if="row.tag?.isFree === false"
+                                :label="t('Value')"
+                                v-model="row.value"
+                                option-value="value"
+                                option-label="value"
+                                emit-value
+                                use-input
+                                :is-clearable="false"
+                            />
+                            <VnInput
+                                v-if="row.tag?.isFree || row.tag?.isFree == undefined"
+                                v-model="row.value"
+                                :label="t('Value')"
+                                :is-clearable="false"
+                            />
+                            <VnInput
+                                :label="t('Relevancy')"
+                                type="number"
+                                v-model="row.priority"
+                            />
+                            <div class="col-1 row justify-center items-center">
+                                <QIcon
+                                    @click="itemTagsRef.remove([row])"
+                                    class="fill-icon-on-hover"
+                                    color="primary"
+                                    name="delete"
+                                    size="sm"
+                                >
+                                    <QTooltip>
+                                        {{ t('Remove tag') }}
+                                    </QTooltip>
+                                </QIcon>
+                            </div>
+                        </VnRow>
+                        <VnRow>
+                            <QIcon
+                                @click="itemTagsRef.insert()"
+                                class="cursor-pointer"
+                                color="primary"
+                                name="add"
+                                size="sm"
+                            >
+                                <QTooltip>
+                                    {{ t('Add tag') }}
+                                </QTooltip>
+                            </QIcon>
+                        </VnRow>
+                    </QCard>
+                </template>
+            </CrudModel>
+        </QPage>
+    </div>
+</template>
+
+<i18n>
+es:
+    Remove tag: Quitar etiqueta
+    Add tag: Añadir etiqueta
+    Tag: Etiqueta
+    Value: Valor
+    Relevancy: Relevancia
+</i18n>
diff --git a/src/router/modules/item.js b/src/router/modules/item.js
index 70d49c56cb..1b582d8e40 100644
--- a/src/router/modules/item.js
+++ b/src/router/modules/item.js
@@ -12,7 +12,7 @@ export default {
     redirect: { name: 'ItemMain' },
     menus: {
         main: ['ItemList', 'WasteBreakdown', 'ItemTypeList'],
-        card: ['ItemBasicData'],
+        card: ['ItemBasicData', 'ItemTags'],
     },
     children: [
         {
@@ -98,7 +98,7 @@ export default {
                     path: 'tags',
                     name: 'ItemTags',
                     meta: {
-                        title: 'Tags',
+                        title: 'tags',
                         icon: 'vn:tags',
                     },
                     component: () => import('src/pages/Item/Card/ItemTags.vue'),

From 42591d078119e6a1051669a104c977f7c6b4abc2 Mon Sep 17 00:00:00 2001
From: Javier Segarra <jsegarra@verdnatura.es>
Date: Thu, 25 Apr 2024 14:27:56 +0200
Subject: [PATCH 03/27] feat #7271 router

---
 src/router/modules/index.js      |   2 +
 src/router/modules/zone.js       | 139 +++++++++++++++++++++++++++++++
 src/router/routes.js             |   2 +
 src/stores/useNavigationStore.js |   1 +
 4 files changed, 144 insertions(+)
 create mode 100644 src/router/modules/zone.js

diff --git a/src/router/modules/index.js b/src/router/modules/index.js
index 302ba7fe0b..2fe40038f3 100644
--- a/src/router/modules/index.js
+++ b/src/router/modules/index.js
@@ -15,6 +15,7 @@ import Department from './department';
 import Entry from './entry';
 import roadmap from './roadmap';
 import Parking from './parking';
+import Zone from './zone';
 
 export default [
     Item,
@@ -34,4 +35,5 @@ export default [
     Entry,
     roadmap,
     Parking,
+    Zone,
 ];
diff --git a/src/router/modules/zone.js b/src/router/modules/zone.js
new file mode 100644
index 0000000000..be11ced11a
--- /dev/null
+++ b/src/router/modules/zone.js
@@ -0,0 +1,139 @@
+import { RouterView } from 'vue-router';
+
+export default {
+    path: '/zone',
+    name: 'Zone',
+    meta: {
+        title: 'zones',
+        icon: 'vn:zone',
+        moduleName: 'Zone',
+    },
+    component: RouterView,
+    redirect: { name: 'ZoneMain' },
+    menus: {
+        main: ['ZoneList', 'ZoneDeliveryList', 'ZoneUpcomingList'],
+        card: [],
+    },
+    children: [
+        {
+            path: '/zone',
+            name: 'ZoneMain',
+            component: () => import('src/pages/Zone/ZoneMain.vue'),
+            redirect: { name: 'ZoneList' },
+            children: [
+                {
+                    path: 'list',
+                    name: 'ZoneList',
+                    meta: {
+                        title: 'zonesList',
+                        icon: 'vn:zone',
+                    },
+                    component: () => import('src/pages/Zone/ZoneList.vue'),
+                },
+                {
+                    path: 'create',
+                    name: 'ZoneCreate',
+                    meta: {
+                        title: 'zoneCreate',
+                        icon: 'create',
+                    },
+                    component: () => import('src/pages/Zone/ZoneCreate.vue'),
+                },
+                {
+                    path: ':id/edit',
+                    name: 'ZoneEdit',
+                    meta: {
+                        title: 'zoneEdit',
+                        icon: 'edit',
+                    },
+                    component: () => import('src/pages/Zone/ZoneCreate.vue'),
+                },
+                {
+                    path: 'counter',
+                    name: 'ZoneCounter',
+                    meta: {
+                        title: 'zoneCounter',
+                        icon: 'add_circle',
+                    },
+                    component: () => import('src/pages/Zone/ZoneCounter.vue'),
+                },
+            ],
+        },
+        {
+            path: '/zone/delivery',
+            name: 'ZoneDeliveryMain',
+            component: () => import('src/pages/Zone/ZoneMain.vue'),
+            redirect: { name: 'ZoneDeliveryList' },
+            children: [
+                {
+                    path: 'list',
+                    name: 'ZoneDeliveryList',
+                    meta: {
+                        title: 'deliveryList',
+                        icon: 'today',
+                    },
+                    component: () =>
+                        import('src/pages/Zone/Delivery/ZoneDeliveryList.vue'),
+                },
+                {
+                    path: 'create',
+                    name: 'ZoneDeliveryCreate',
+                    meta: {
+                        title: 'deliveryCreate',
+                        icon: 'create',
+                    },
+                    component: () =>
+                        import('src/pages/Zone/Delivery/ZoneDeliveryCreate.vue'),
+                },
+                {
+                    path: ':id/edit',
+                    name: 'ZoneDeliveryEdit',
+                    meta: {
+                        title: 'deliveryEdit',
+                        icon: 'edit',
+                    },
+                    component: () =>
+                        import('src/pages/Zone/Delivery/ZoneDeliveryCreate.vue'),
+                },
+            ],
+        },
+        {
+            path: '/zone/upcoming',
+            name: 'ZoneUpcomingMain',
+            component: () => import('src/pages/Zone/ZoneMain.vue'),
+            redirect: { name: 'ZoneUpcomingList' },
+            children: [
+                {
+                    path: 'list',
+                    name: 'ZoneUpcomingList',
+                    meta: {
+                        title: 'upcomingList',
+                        icon: 'today',
+                    },
+                    component: () =>
+                        import('src/pages/Zone/Upcoming/ZoneUpcomingList.vue'),
+                },
+                {
+                    path: 'create',
+                    name: 'ZoneUpcomingCreate',
+                    meta: {
+                        title: 'upcomingCreate',
+                        icon: 'create',
+                    },
+                    component: () =>
+                        import('src/pages/Zone/Upcoming/ZoneUpcomingCreate.vue'),
+                },
+                {
+                    path: ':id/edit',
+                    name: 'ZoneUpcomingEdit',
+                    meta: {
+                        title: 'upcomingEdit',
+                        icon: 'edit',
+                    },
+                    component: () =>
+                        import('src/pages/Zone/Upcoming/ZoneUpcomingCreate.vue'),
+                },
+            ],
+        },
+    ],
+};
diff --git a/src/router/routes.js b/src/router/routes.js
index 51e726a62f..14bf6665f8 100644
--- a/src/router/routes.js
+++ b/src/router/routes.js
@@ -15,6 +15,7 @@ import order from 'src/router/modules/order';
 import entry from 'src/router/modules/entry';
 import roadmap from 'src/router/modules/roadmap';
 import parking from 'src/router/modules/parking';
+import zone from 'src/router/modules/zone';
 
 const routes = [
     {
@@ -71,6 +72,7 @@ const routes = [
             roadmap,
             entry,
             parking,
+            zone,
             {
                 path: '/:catchAll(.*)*',
                 name: 'NotFound',
diff --git a/src/stores/useNavigationStore.js b/src/stores/useNavigationStore.js
index f075301f66..ee1e04e9b6 100644
--- a/src/stores/useNavigationStore.js
+++ b/src/stores/useNavigationStore.js
@@ -21,6 +21,7 @@ export const useNavigationStore = defineStore('navigationStore', () => {
         'ticket',
         'worker',
         'wagon',
+        'zone',
     ];
     const pinnedModules = ref([]);
     const role = useRole();

From b4e3157887e6ace9072ba5f686b9d26046bf2360 Mon Sep 17 00:00:00 2001
From: Javier Segarra <jsegarra@verdnatura.es>
Date: Thu, 25 Apr 2024 14:28:06 +0200
Subject: [PATCH 04/27] feat #7271 i18n

---
 src/i18n/locale/en.yml | 6 ++++++
 src/i18n/locale/es.yml | 8 +++++++-
 2 files changed, 13 insertions(+), 1 deletion(-)

diff --git a/src/i18n/locale/en.yml b/src/i18n/locale/en.yml
index ff57bf9680..aa65ce08c7 100644
--- a/src/i18n/locale/en.yml
+++ b/src/i18n/locale/en.yml
@@ -1165,6 +1165,12 @@ item:
         type: Type
         intrastat: Intrastat
         origin: Origin
+zone:
+    pageTitles:
+        zones: Zone
+        zonesList: Zones
+        deliveryList: Delivery days
+        upcomingList: Upcoming deliveries
 components:
     topbar: {}
     itemsFilterPanel:
diff --git a/src/i18n/locale/es.yml b/src/i18n/locale/es.yml
index f9278a9b0b..da421432de 100644
--- a/src/i18n/locale/es.yml
+++ b/src/i18n/locale/es.yml
@@ -287,7 +287,7 @@ customer:
             hasSepaVnl: Recibido B2B VNL
 entry:
     pageTitles:
-        entries: Entradas
+        entries: Entrasdadas
         list: Listado
         summary: Resumen
         basicData: Datos básicos
@@ -1164,6 +1164,12 @@ item:
         type: Tipo
         intrastat: Intrastat
         origin: Origen
+zone:
+    pageTitles:
+        zones: Zona
+        zonesList: Zonas
+        deliveryList: Días de entrega
+        upcomingList: Próximos repartos
 components:
     topbar: {}
     itemsFilterPanel:

From b1871c33fd0ad1eb86f7ca6985af6ec41a770709 Mon Sep 17 00:00:00 2001
From: Javier Segarra <jsegarra@verdnatura.es>
Date: Thu, 25 Apr 2024 14:30:16 +0200
Subject: [PATCH 05/27] feat #7271 Zone Components boilerplate

---
 src/pages/Zone/Card/ZoneCard.vue              |   6 +
 .../Zone/Delivery/ZoneDeliveryCreate.vue      | 432 ++++++++++++++++++
 src/pages/Zone/Delivery/ZoneDeliveryList.vue  |  81 ++++
 .../Zone/Upcoming/ZoneUpcomingCreate.vue      | 432 ++++++++++++++++++
 src/pages/Zone/Upcoming/ZoneUpcomingList.vue  |  81 ++++
 src/pages/Zone/ZoneCreate.vue                 | 184 ++++++++
 src/pages/Zone/ZoneList.vue                   |  97 ++++
 src/pages/Zone/ZoneMain.vue                   |  17 +
 8 files changed, 1330 insertions(+)
 create mode 100644 src/pages/Zone/Card/ZoneCard.vue
 create mode 100644 src/pages/Zone/Delivery/ZoneDeliveryCreate.vue
 create mode 100644 src/pages/Zone/Delivery/ZoneDeliveryList.vue
 create mode 100644 src/pages/Zone/Upcoming/ZoneUpcomingCreate.vue
 create mode 100644 src/pages/Zone/Upcoming/ZoneUpcomingList.vue
 create mode 100644 src/pages/Zone/ZoneCreate.vue
 create mode 100644 src/pages/Zone/ZoneList.vue
 create mode 100644 src/pages/Zone/ZoneMain.vue

diff --git a/src/pages/Zone/Card/ZoneCard.vue b/src/pages/Zone/Card/ZoneCard.vue
new file mode 100644
index 0000000000..948636c55a
--- /dev/null
+++ b/src/pages/Zone/Card/ZoneCard.vue
@@ -0,0 +1,6 @@
+<script setup>
+import VnCard from 'components/common/VnCard.vue';
+</script>
+<template>
+    <VnCard data-key="Zone" base-url="Zones" />
+</template>
diff --git a/src/pages/Zone/Delivery/ZoneDeliveryCreate.vue b/src/pages/Zone/Delivery/ZoneDeliveryCreate.vue
new file mode 100644
index 0000000000..a48eaf2787
--- /dev/null
+++ b/src/pages/Zone/Delivery/ZoneDeliveryCreate.vue
@@ -0,0 +1,432 @@
+<script setup>
+import { computed, ref, onMounted, onUpdated } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+import { useQuasar } from 'quasar';
+
+import VnInput from 'src/components/common/VnInput.vue';
+
+import { useI18n } from 'vue-i18n';
+import axios from 'axios';
+
+onMounted(() => fetch());
+onUpdated(() => fetch());
+
+const { t } = useI18n();
+const route = useRoute();
+const quasar = useQuasar();
+const router = useRouter();
+const $props = defineProps({
+    id: {
+        type: Number,
+        required: false,
+        default: null,
+    },
+});
+const entityId = computed(() => $props.id || route.params.id);
+
+const zone = ref([]);
+const divisible = ref(false);
+const name = ref('');
+const colorPickerActive = ref(false);
+let originalData = { trays: [] };
+let zoneConfig;
+let zoneDeliveryColors;
+let currentTrayColorPicked;
+
+async function fetch() {
+    try {
+        await axios.get('ZoneConfigs').then(async (res) => {
+            if (res.data) {
+                zoneConfig = res.data[0];
+            }
+        });
+
+        await axios.get(`ZoneDeliveryColors`).then(async (res) => {
+            if (res.data) {
+                zoneDeliveryColors = res.data;
+                if (!entityId.value)
+                    zone.value.push({
+                        id: 0,
+                        position: 0,
+                        color: { ...zoneDeliveryColors[0] },
+                        action: 'add',
+                    });
+                else {
+                    await axios
+                        .get(`ZoneDeliveryTrays`, {
+                            params: { filter: { where: { typeFk: entityId.value } } },
+                        })
+                        .then(async (res) => {
+                            if (res.data) {
+                                for (let i = 0; i < res.data.length; i++) {
+                                    const tray = res.data[i];
+                                    zone.value.push({
+                                        id: res.data.length - i - 1,
+                                        position: tray.height,
+                                        color: {
+                                            ...zoneDeliveryColors.find((color) => {
+                                                return color.id === tray.colorFk;
+                                            }),
+                                        },
+                                        action: tray.height == 0 ? 'add' : 'delete',
+                                    });
+                                }
+                                zone.value.forEach((value) => {
+                                    originalData.trays.push({ ...value });
+                                });
+                            }
+                        });
+                }
+            }
+        });
+
+        if (entityId.value) {
+            await axios.get(`ZoneDeliverys/${entityId.value}`).then((res) => {
+                if (res.data) {
+                    originalData.name = name.value = res.data.name;
+                    originalData.divisible = divisible.value = res.data.divisible;
+                }
+            });
+        }
+    } catch (e) {
+        //
+    }
+}
+
+function addTray() {
+    if (
+        zone.value.find((tray) => {
+            return tray.position == null;
+        })
+    ) {
+        quasar.notify({
+            message: t('zone.warnings.uncompleteTrays'),
+            type: 'warning',
+        });
+        return;
+    }
+
+    if (zone.value.length < zoneConfig.maxTrays) {
+        zone.value.unshift({
+            id: zone.value.length,
+            position: null,
+            color: { ...zoneDeliveryColors[0] },
+            action: 'delete',
+        });
+    } else {
+        quasar.notify({
+            message: t('zone.warnings.maxTrays'),
+            type: 'warning',
+        });
+    }
+}
+
+function deleteTray(trayToDelete) {
+    zone.value = zone.value.filter((tray) => tray.id !== trayToDelete.id);
+    reorderIds();
+}
+
+function reorderIds() {
+    for (let index = zone.value.length - 1; index >= 0; index--) {
+        zone.value[index].id = index;
+    }
+}
+
+async function onSubmit() {
+    try {
+        const path = entityId.value
+            ? 'ZoneDeliverys/editZoneDelivery'
+            : 'ZoneDeliverys/createZoneDelivery';
+
+        const params = {
+            id: entityId.value,
+            name: name.value,
+            divisible: divisible.value,
+            trays: zone.value,
+        };
+
+        await axios.patch(path, params).then((res) => {
+            if (res.status == 204) router.push({ path: `/zone/type/list` });
+        });
+    } catch (error) {
+        //
+    }
+}
+
+function onReset() {
+    name.value = entityId.value ? originalData.name : null;
+    divisible.value = entityId.value ? originalData.divisible : false;
+    zone.value = entityId.value
+        ? [...originalData.trays]
+        : [
+              {
+                  id: 0,
+                  position: 0,
+                  color: { ...zoneDeliveryColors[0] },
+                  action: 'add',
+              },
+          ];
+}
+
+function doAction(tray) {
+    if (tray.action == 'add') {
+        addTray();
+    } else {
+        deleteTray(tray);
+    }
+}
+
+function showColorPicker(tray) {
+    colorPickerActive.value = true;
+    currentTrayColorPicked = zone.value.findIndex((val) => {
+        return val.id === tray.id;
+    });
+}
+
+function updateColor(newColor) {
+    zone.value[currentTrayColorPicked].color = {
+        ...zoneDeliveryColors.find((color) => {
+            return color.rgb === newColor;
+        }),
+    };
+}
+
+function onPositionBlur(tray) {
+    if (tray.position) {
+        if (tray.position == '' || tray.position < 0) {
+            tray.position = null;
+            return;
+        }
+        tray.position = parseInt(tray.position);
+        zone.value.sort((a, b) => b.position - a.position);
+        reorderIds();
+        for (let index = zone.value.length - 1; index > 0; index--) {
+            if (exceedMaxHeight(index - 1)) continue;
+            if (
+                zone.value[index - 1].position - zone.value[index].position >=
+                zoneConfig.minHeightBetweenTrays
+            ) {
+                continue;
+            } else {
+                zone.value[index - 1].position +=
+                    zoneConfig.minHeightBetweenTrays -
+                    (zone.value[index - 1].position - zone.value[index].position);
+
+                quasar.notify({
+                    message:
+                        t('zone.warnings.minHeightBetweenTrays') +
+                        zoneConfig.minHeightBetweenTrays +
+                        ' cm',
+                    type: 'warning',
+                });
+
+                exceedMaxHeight(index - 1);
+            }
+        }
+    }
+}
+
+function exceedMaxHeight(pos) {
+    if (zone.value[pos].position > zoneConfig.maxZoneHeight) {
+        zone.value.splice(pos, 1);
+        quasar.notify({
+            message: t('zone.warnings.maxZoneHeight') + zoneConfig.maxZoneHeight + ' cm',
+            type: 'warning',
+        });
+        return true;
+    }
+    return false;
+}
+</script>
+
+<template>
+    <QPage class="q-pa-sm q-mx-xl">
+        <QForm @submit="onSubmit()" @reset="onReset()" class="q-pa-sm">
+            <QCard class="q-pa-md">
+                <VnInput
+                    filled
+                    v-model="name"
+                    :label="t('zone.delivery.name')"
+                    :rules="[(val) => !!val || t('zone.warnings.nameNotEmpty')]"
+                />
+                <QCheckbox class="q-mb-sm" v-model="divisible" label="Divisible" />
+                <div class="zone-tray q-mx-lg" v-for="tray in zone" :key="tray.id">
+                    <div class="position">
+                        <QInput
+                            autofocus
+                            filled
+                            type="number"
+                            :class="{ isVisible: tray.action == 'add' }"
+                            v-model="tray.position"
+                            @blur="onPositionBlur(tray)"
+                        >
+                            <QTooltip :delay="2000">
+                                {{
+                                    t('zone.warnings.minHeightBetweenTrays') +
+                                    zoneConfig.minHeightBetweenTrays +
+                                    ' cm'
+                                }}
+                                <QSpace />
+                                {{
+                                    t('zone.warnings.maxZoneHeight') +
+                                    zoneConfig.maxZoneHeight +
+                                    ' cm'
+                                }}
+                            </QTooltip>
+                        </QInput>
+                    </div>
+                    <div class="shelving">
+                        <div class="shelving-half">
+                            <div class="shelving-up"></div>
+                            <div
+                                class="shelving-down"
+                                :style="{ backgroundColor: tray.color.rgb }"
+                                @click="showColorPicker(tray)"
+                            ></div>
+                        </div>
+                        <div
+                            class="shelving-divisible"
+                            :class="{ isVisible: !divisible }"
+                        ></div>
+                        <div class="shelving-half">
+                            <div class="shelving-up"></div>
+                            <div
+                                class="shelving-down"
+                                :style="{ backgroundColor: tray.color.rgb }"
+                                @click="showColorPicker(tray)"
+                            ></div>
+                        </div>
+                    </div>
+                    <div class="action-button">
+                        <QBtn
+                            flat
+                            round
+                            color="primary"
+                            :icon="tray.action"
+                            @click="doAction(tray)"
+                        />
+                    </div>
+                </div>
+                <div class="q-mb-sm wheels">
+                    <QIcon color="grey-6" name="trip_origin" size="xl" />
+                    <QIcon color="grey-6" name="trip_origin" size="xl" />
+                </div>
+                <QDialog
+                    v-model="colorPickerActive"
+                    position="right"
+                    :no-backdrop-dismiss="false"
+                >
+                    <QCard>
+                        <QCardSection>
+                            <div class="text-h6">{{ t('zone.delivery.trayColor') }}</div>
+                        </QCardSection>
+                        <QCardSection class="row items-center no-wrap">
+                            <QColor
+                                flat
+                                v-model="zone[currentTrayColorPicked].color.rgb"
+                                no-header
+                                no-footer
+                                default-view="palette"
+                                :palette="
+                                    zoneDeliveryColors.map((color) => {
+                                        return color.rgb;
+                                    })
+                                "
+                                @change="updateColor($event)"
+                            />
+                            <QBtn flat round icon="close" v-close-popup />
+                        </QCardSection>
+                    </QCard>
+                </QDialog>
+            </QCard>
+            <div class="q-mt-md">
+                <QBtn :label="t('zone.delivery.submit')" type="submit" color="primary" />
+                <QBtn
+                    :label="t('zone.delivery.reset')"
+                    type="reset"
+                    color="primary"
+                    flat
+                    class="q-ml-sm"
+                />
+            </div>
+        </QForm>
+    </QPage>
+</template>
+
+<style lang="scss" scoped>
+.q-page {
+    display: flex;
+    justify-content: center;
+    align-items: flex-start;
+}
+
+.q-form {
+    width: 70%;
+}
+
+.q-dialog {
+    .q-card {
+        width: 100%;
+    }
+}
+
+.wheels {
+    margin-left: 5%;
+    display: flex;
+    justify-content: space-around;
+}
+
+.zone-tray {
+    display: flex;
+    height: 6rem;
+
+    .position {
+        width: 20%;
+        border-right: 1rem solid gray;
+        display: flex;
+        align-items: flex-end;
+        justify-content: flex-end;
+        padding-right: 1rem;
+    }
+
+    .shelving {
+        display: flex;
+        width: 75%;
+
+        .shelving-half {
+            width: 50%;
+            height: 100%;
+
+            .shelving-up {
+                height: 80%;
+                width: 100%;
+            }
+
+            .shelving-down {
+                height: 20%;
+                width: 100%;
+            }
+        }
+
+        .shelving-divisible {
+            width: 1%;
+            height: 100%;
+            border-left: 0.5rem dashed grey;
+            border-right: 0.5rem dashed grey;
+        }
+    }
+
+    .action-button {
+        width: 10%;
+        border-left: 1rem solid gray;
+        display: flex;
+        align-items: flex-end;
+        justify-content: flex-start;
+        padding-left: 1rem;
+    }
+
+    .isVisible {
+        display: none;
+    }
+}
+</style>
diff --git a/src/pages/Zone/Delivery/ZoneDeliveryList.vue b/src/pages/Zone/Delivery/ZoneDeliveryList.vue
new file mode 100644
index 0000000000..c7a3cbcdbc
--- /dev/null
+++ b/src/pages/Zone/Delivery/ZoneDeliveryList.vue
@@ -0,0 +1,81 @@
+<script setup>
+import axios from 'axios';
+import { useQuasar } from 'quasar';
+import VnPaginate from 'src/components/ui/VnPaginate.vue';
+import { useArrayData } from 'src/composables/useArrayData';
+import { useI18n } from 'vue-i18n';
+import { useRouter } from 'vue-router';
+import CardList from 'components/ui/CardList.vue';
+import VnLv from 'components/ui/VnLv.vue';
+
+const quasar = useQuasar();
+const arrayData = useArrayData('ZoneDeliveryList');
+const store = arrayData.store;
+const router = useRouter();
+const { t } = useI18n();
+
+function navigate(id) {
+    router.push({ path: `/zone/type/${id}/edit` });
+}
+
+function create() {
+    router.push({ path: `/zone/type/create` });
+}
+
+async function remove(row) {
+    try {
+        const id = row.id;
+        await axios
+            .delete(`ZoneDeliverys/deleteZoneDelivery`, { params: { id } })
+            .then(async () => {
+                quasar.notify({
+                    message: t('zone.delivery.removeItem'),
+                    type: 'positive',
+                });
+                store.data.splice(store.data.indexOf(row), 1);
+            });
+    } catch (error) {
+        //
+    }
+}
+</script>
+
+<template>
+    <QPage class="column items-center q-pa-md">
+        <div class="vn-card-list">
+            <VnPaginate
+                data-key="ZoneDeliveryList"
+                url="/ZoneDeliverys"
+                order="id DESC"
+                auto-load
+            >
+                <template #body="{ rows }">
+                    <CardList
+                        v-for="row of rows"
+                        :key="row.id"
+                        :title="(row.name || '').toString()"
+                        :id="row.id"
+                        @click="navigate(row.id)"
+                    >
+                        <template #actions>
+                            <QBtn
+                                :label="t('components.smartCard.openCard')"
+                                @click.stop="navigate(row.id)"
+                                outline
+                            />
+                            <QBtn
+                                :label="t('zone.list.remove')"
+                                @click.stop="remove(row)"
+                                color="primary"
+                                style="margin-top: 15px"
+                            />
+                        </template>
+                    </CardList>
+                </template>
+            </VnPaginate>
+        </div>
+        <QPageSticky position="bottom-right" :offset="[18, 18]">
+            <QBtn @click="create" fab icon="add" color="primary" />
+        </QPageSticky>
+    </QPage>
+</template>
diff --git a/src/pages/Zone/Upcoming/ZoneUpcomingCreate.vue b/src/pages/Zone/Upcoming/ZoneUpcomingCreate.vue
new file mode 100644
index 0000000000..6bc04c4282
--- /dev/null
+++ b/src/pages/Zone/Upcoming/ZoneUpcomingCreate.vue
@@ -0,0 +1,432 @@
+<script setup>
+import { computed, ref, onMounted, onUpdated } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+import { useQuasar } from 'quasar';
+
+import VnInput from 'src/components/common/VnInput.vue';
+
+import { useI18n } from 'vue-i18n';
+import axios from 'axios';
+
+onMounted(() => fetch());
+onUpdated(() => fetch());
+
+const { t } = useI18n();
+const route = useRoute();
+const quasar = useQuasar();
+const router = useRouter();
+const $props = defineProps({
+    id: {
+        type: Number,
+        required: false,
+        default: null,
+    },
+});
+const entityId = computed(() => $props.id || route.params.id);
+
+const zone = ref([]);
+const divisible = ref(false);
+const name = ref('');
+const colorPickerActive = ref(false);
+let originalData = { trays: [] };
+let zoneConfig;
+let zoneUpcomingColors;
+let currentTrayColorPicked;
+
+async function fetch() {
+    try {
+        await axios.get('ZoneConfigs').then(async (res) => {
+            if (res.data) {
+                zoneConfig = res.data[0];
+            }
+        });
+
+        await axios.get(`ZoneUpcomingColors`).then(async (res) => {
+            if (res.data) {
+                zoneUpcomingColors = res.data;
+                if (!entityId.value)
+                    zone.value.push({
+                        id: 0,
+                        position: 0,
+                        color: { ...zoneUpcomingColors[0] },
+                        action: 'add',
+                    });
+                else {
+                    await axios
+                        .get(`ZoneUpcomingTrays`, {
+                            params: { filter: { where: { typeFk: entityId.value } } },
+                        })
+                        .then(async (res) => {
+                            if (res.data) {
+                                for (let i = 0; i < res.data.length; i++) {
+                                    const tray = res.data[i];
+                                    zone.value.push({
+                                        id: res.data.length - i - 1,
+                                        position: tray.height,
+                                        color: {
+                                            ...zoneUpcomingColors.find((color) => {
+                                                return color.id === tray.colorFk;
+                                            }),
+                                        },
+                                        action: tray.height == 0 ? 'add' : 'delete',
+                                    });
+                                }
+                                zone.value.forEach((value) => {
+                                    originalData.trays.push({ ...value });
+                                });
+                            }
+                        });
+                }
+            }
+        });
+
+        if (entityId.value) {
+            await axios.get(`ZoneUpcomings/${entityId.value}`).then((res) => {
+                if (res.data) {
+                    originalData.name = name.value = res.data.name;
+                    originalData.divisible = divisible.value = res.data.divisible;
+                }
+            });
+        }
+    } catch (e) {
+        //
+    }
+}
+
+function addTray() {
+    if (
+        zone.value.find((tray) => {
+            return tray.position == null;
+        })
+    ) {
+        quasar.notify({
+            message: t('zone.warnings.uncompleteTrays'),
+            type: 'warning',
+        });
+        return;
+    }
+
+    if (zone.value.length < zoneConfig.maxTrays) {
+        zone.value.unshift({
+            id: zone.value.length,
+            position: null,
+            color: { ...zoneUpcomingColors[0] },
+            action: 'delete',
+        });
+    } else {
+        quasar.notify({
+            message: t('zone.warnings.maxTrays'),
+            type: 'warning',
+        });
+    }
+}
+
+function deleteTray(trayToDelete) {
+    zone.value = zone.value.filter((tray) => tray.id !== trayToDelete.id);
+    reorderIds();
+}
+
+function reorderIds() {
+    for (let index = zone.value.length - 1; index >= 0; index--) {
+        zone.value[index].id = index;
+    }
+}
+
+async function onSubmit() {
+    try {
+        const path = entityId.value
+            ? 'ZoneUpcomings/editZoneUpcoming'
+            : 'ZoneUpcomings/createZoneUpcoming';
+
+        const params = {
+            id: entityId.value,
+            name: name.value,
+            divisible: divisible.value,
+            trays: zone.value,
+        };
+
+        await axios.patch(path, params).then((res) => {
+            if (res.status == 204) router.push({ path: `/zone/type/list` });
+        });
+    } catch (error) {
+        //
+    }
+}
+
+function onReset() {
+    name.value = entityId.value ? originalData.name : null;
+    divisible.value = entityId.value ? originalData.divisible : false;
+    zone.value = entityId.value
+        ? [...originalData.trays]
+        : [
+              {
+                  id: 0,
+                  position: 0,
+                  color: { ...zoneUpcomingColors[0] },
+                  action: 'add',
+              },
+          ];
+}
+
+function doAction(tray) {
+    if (tray.action == 'add') {
+        addTray();
+    } else {
+        deleteTray(tray);
+    }
+}
+
+function showColorPicker(tray) {
+    colorPickerActive.value = true;
+    currentTrayColorPicked = zone.value.findIndex((val) => {
+        return val.id === tray.id;
+    });
+}
+
+function updateColor(newColor) {
+    zone.value[currentTrayColorPicked].color = {
+        ...zoneUpcomingColors.find((color) => {
+            return color.rgb === newColor;
+        }),
+    };
+}
+
+function onPositionBlur(tray) {
+    if (tray.position) {
+        if (tray.position == '' || tray.position < 0) {
+            tray.position = null;
+            return;
+        }
+        tray.position = parseInt(tray.position);
+        zone.value.sort((a, b) => b.position - a.position);
+        reorderIds();
+        for (let index = zone.value.length - 1; index > 0; index--) {
+            if (exceedMaxHeight(index - 1)) continue;
+            if (
+                zone.value[index - 1].position - zone.value[index].position >=
+                zoneConfig.minHeightBetweenTrays
+            ) {
+                continue;
+            } else {
+                zone.value[index - 1].position +=
+                    zoneConfig.minHeightBetweenTrays -
+                    (zone.value[index - 1].position - zone.value[index].position);
+
+                quasar.notify({
+                    message:
+                        t('zone.warnings.minHeightBetweenTrays') +
+                        zoneConfig.minHeightBetweenTrays +
+                        ' cm',
+                    type: 'warning',
+                });
+
+                exceedMaxHeight(index - 1);
+            }
+        }
+    }
+}
+
+function exceedMaxHeight(pos) {
+    if (zone.value[pos].position > zoneConfig.maxZoneHeight) {
+        zone.value.splice(pos, 1);
+        quasar.notify({
+            message: t('zone.warnings.maxZoneHeight') + zoneConfig.maxZoneHeight + ' cm',
+            type: 'warning',
+        });
+        return true;
+    }
+    return false;
+}
+</script>
+
+<template>
+    <QPage class="q-pa-sm q-mx-xl">
+        <QForm @submit="onSubmit()" @reset="onReset()" class="q-pa-sm">
+            <QCard class="q-pa-md">
+                <VnInput
+                    filled
+                    v-model="name"
+                    :label="t('zone.upcoming.name')"
+                    :rules="[(val) => !!val || t('zone.warnings.nameNotEmpty')]"
+                />
+                <QCheckbox class="q-mb-sm" v-model="divisible" label="Divisible" />
+                <div class="zone-tray q-mx-lg" v-for="tray in zone" :key="tray.id">
+                    <div class="position">
+                        <QInput
+                            autofocus
+                            filled
+                            type="number"
+                            :class="{ isVisible: tray.action == 'add' }"
+                            v-model="tray.position"
+                            @blur="onPositionBlur(tray)"
+                        >
+                            <QTooltip :delay="2000">
+                                {{
+                                    t('zone.warnings.minHeightBetweenTrays') +
+                                    zoneConfig.minHeightBetweenTrays +
+                                    ' cm'
+                                }}
+                                <QSpace />
+                                {{
+                                    t('zone.warnings.maxZoneHeight') +
+                                    zoneConfig.maxZoneHeight +
+                                    ' cm'
+                                }}
+                            </QTooltip>
+                        </QInput>
+                    </div>
+                    <div class="shelving">
+                        <div class="shelving-half">
+                            <div class="shelving-up"></div>
+                            <div
+                                class="shelving-down"
+                                :style="{ backgroundColor: tray.color.rgb }"
+                                @click="showColorPicker(tray)"
+                            ></div>
+                        </div>
+                        <div
+                            class="shelving-divisible"
+                            :class="{ isVisible: !divisible }"
+                        ></div>
+                        <div class="shelving-half">
+                            <div class="shelving-up"></div>
+                            <div
+                                class="shelving-down"
+                                :style="{ backgroundColor: tray.color.rgb }"
+                                @click="showColorPicker(tray)"
+                            ></div>
+                        </div>
+                    </div>
+                    <div class="action-button">
+                        <QBtn
+                            flat
+                            round
+                            color="primary"
+                            :icon="tray.action"
+                            @click="doAction(tray)"
+                        />
+                    </div>
+                </div>
+                <div class="q-mb-sm wheels">
+                    <QIcon color="grey-6" name="trip_origin" size="xl" />
+                    <QIcon color="grey-6" name="trip_origin" size="xl" />
+                </div>
+                <QDialog
+                    v-model="colorPickerActive"
+                    position="right"
+                    :no-backdrop-dismiss="false"
+                >
+                    <QCard>
+                        <QCardSection>
+                            <div class="text-h6">{{ t('zone.upcoming.trayColor') }}</div>
+                        </QCardSection>
+                        <QCardSection class="row items-center no-wrap">
+                            <QColor
+                                flat
+                                v-model="zone[currentTrayColorPicked].color.rgb"
+                                no-header
+                                no-footer
+                                default-view="palette"
+                                :palette="
+                                    zoneUpcomingColors.map((color) => {
+                                        return color.rgb;
+                                    })
+                                "
+                                @change="updateColor($event)"
+                            />
+                            <QBtn flat round icon="close" v-close-popup />
+                        </QCardSection>
+                    </QCard>
+                </QDialog>
+            </QCard>
+            <div class="q-mt-md">
+                <QBtn :label="t('zone.upcoming.submit')" type="submit" color="primary" />
+                <QBtn
+                    :label="t('zone.upcoming.reset')"
+                    type="reset"
+                    color="primary"
+                    flat
+                    class="q-ml-sm"
+                />
+            </div>
+        </QForm>
+    </QPage>
+</template>
+
+<style lang="scss" scoped>
+.q-page {
+    display: flex;
+    justify-content: center;
+    align-items: flex-start;
+}
+
+.q-form {
+    width: 70%;
+}
+
+.q-dialog {
+    .q-card {
+        width: 100%;
+    }
+}
+
+.wheels {
+    margin-left: 5%;
+    display: flex;
+    justify-content: space-around;
+}
+
+.zone-tray {
+    display: flex;
+    height: 6rem;
+
+    .position {
+        width: 20%;
+        border-right: 1rem solid gray;
+        display: flex;
+        align-items: flex-end;
+        justify-content: flex-end;
+        padding-right: 1rem;
+    }
+
+    .shelving {
+        display: flex;
+        width: 75%;
+
+        .shelving-half {
+            width: 50%;
+            height: 100%;
+
+            .shelving-up {
+                height: 80%;
+                width: 100%;
+            }
+
+            .shelving-down {
+                height: 20%;
+                width: 100%;
+            }
+        }
+
+        .shelving-divisible {
+            width: 1%;
+            height: 100%;
+            border-left: 0.5rem dashed grey;
+            border-right: 0.5rem dashed grey;
+        }
+    }
+
+    .action-button {
+        width: 10%;
+        border-left: 1rem solid gray;
+        display: flex;
+        align-items: flex-end;
+        justify-content: flex-start;
+        padding-left: 1rem;
+    }
+
+    .isVisible {
+        display: none;
+    }
+}
+</style>
diff --git a/src/pages/Zone/Upcoming/ZoneUpcomingList.vue b/src/pages/Zone/Upcoming/ZoneUpcomingList.vue
new file mode 100644
index 0000000000..5c417df8fd
--- /dev/null
+++ b/src/pages/Zone/Upcoming/ZoneUpcomingList.vue
@@ -0,0 +1,81 @@
+<script setup>
+import axios from 'axios';
+import { useQuasar } from 'quasar';
+import VnPaginate from 'src/components/ui/VnPaginate.vue';
+import { useArrayData } from 'src/composables/useArrayData';
+import { useI18n } from 'vue-i18n';
+import { useRouter } from 'vue-router';
+import CardList from 'components/ui/CardList.vue';
+import VnLv from 'components/ui/VnLv.vue';
+
+const quasar = useQuasar();
+const arrayData = useArrayData('ZoneUpcomingList');
+const store = arrayData.store;
+const router = useRouter();
+const { t } = useI18n();
+
+function navigate(id) {
+    router.push({ path: `/zone/type/${id}/edit` });
+}
+
+function create() {
+    router.push({ path: `/zone/type/create` });
+}
+
+async function remove(row) {
+    try {
+        const id = row.id;
+        await axios
+            .delete(`ZoneUpcomings/deleteZoneUpcoming`, { params: { id } })
+            .then(async () => {
+                quasar.notify({
+                    message: t('zone.upcoming.removeItem'),
+                    type: 'positive',
+                });
+                store.data.splice(store.data.indexOf(row), 1);
+            });
+    } catch (error) {
+        //
+    }
+}
+</script>
+
+<template>
+    <QPage class="column items-center q-pa-md">
+        <div class="vn-card-list">
+            <VnPaginate
+                data-key="ZoneUpcomingList"
+                url="/ZoneUpcomings"
+                order="id DESC"
+                auto-load
+            >
+                <template #body="{ rows }">
+                    <CardList
+                        v-for="row of rows"
+                        :key="row.id"
+                        :title="(row.name || '').toString()"
+                        :id="row.id"
+                        @click="navigate(row.id)"
+                    >
+                        <template #actions>
+                            <QBtn
+                                :label="t('components.smartCard.openCard')"
+                                @click.stop="navigate(row.id)"
+                                outline
+                            />
+                            <QBtn
+                                :label="t('zone.list.remove')"
+                                @click.stop="remove(row)"
+                                color="primary"
+                                style="margin-top: 15px"
+                            />
+                        </template>
+                    </CardList>
+                </template>
+            </VnPaginate>
+        </div>
+        <QPageSticky position="bottom-right" :offset="[18, 18]">
+            <QBtn @click="create" fab icon="add" color="primary" />
+        </QPageSticky>
+    </QPage>
+</template>
diff --git a/src/pages/Zone/ZoneCreate.vue b/src/pages/Zone/ZoneCreate.vue
new file mode 100644
index 0000000000..8c0ba8c176
--- /dev/null
+++ b/src/pages/Zone/ZoneCreate.vue
@@ -0,0 +1,184 @@
+<script setup>
+import { computed, onMounted, onUpdated, ref } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { QIcon, QInput, QItem, QItemSection, QSelect } from 'quasar';
+
+import VnInput from 'src/components/common/VnInput.vue';
+
+import { useRoute, useRouter } from 'vue-router';
+import axios from 'axios';
+
+onMounted(() => fetch());
+onUpdated(() => fetch());
+
+const { t } = useI18n();
+const route = useRoute();
+const router = useRouter();
+const $props = defineProps({
+    id: {
+        type: Number,
+        required: false,
+        default: null,
+    },
+});
+const entityId = computed(() => $props.id || route.params.id);
+
+let zoneTypes = [];
+let originalData = {};
+const zone = ref({});
+const filteredZoneTypes = ref(zoneTypes);
+
+async function onSubmit() {
+    try {
+        const params = {
+            id: entityId.value,
+            label: zone.value.label,
+            plate: zone.value.plate,
+            volume: zone.value.volume,
+            typeFk: zone.value.typeFk,
+        };
+        await axios.patch('Zones', params).then((res) => {
+            if (res.status == 200) router.push({ path: `/zone/list` });
+        });
+    } catch (error) {
+        //
+    }
+}
+
+async function onReset() {
+    if (entityId.value) {
+        zone.value = { ...originalData };
+    } else {
+        zone.value = {};
+    }
+}
+
+async function fetch() {
+    try {
+        await axios.get('ZoneTypes').then(async (res) => {
+            if (res.data) {
+                filteredZoneTypes.value = zoneTypes = res.data;
+            }
+        });
+        if (entityId.value) {
+            await axios.get(`Zones/${entityId.value}`).then(async (res) => {
+                const data = res.data;
+                if (data) {
+                    zone.value.label = data.label;
+                    zone.value.plate = data.plate;
+                    zone.value.volume = data.volume;
+                    zone.value.typeFk = data.typeFk;
+                    originalData = { ...zone.value };
+                }
+            });
+        }
+    } catch (e) {
+        //
+    }
+}
+
+function filterType(val, update) {
+    update(() => {
+        const needle = val.toLowerCase();
+        filteredZoneTypes.value = zoneTypes.filter(
+            (v) => v.name.toLowerCase().indexOf(needle) > -1
+        );
+    });
+}
+</script>
+
+<template>
+    <QPage class="q-pa-sm q-mx-xl">
+        <QForm @submit="onSubmit()" @reset="onReset()" class="q-pa-sm">
+            <QCard class="q-pa-md">
+                <div class="row q-col-gutter-md">
+                    <div class="col">
+                        <QInput
+                            filled
+                            v-model="zone.label"
+                            :label="t('zone.create.label')"
+                            type="number"
+                            min="0"
+                            :rules="[(val) => !!val || t('zone.warnings.labelNotEmpty')]"
+                        />
+                    </div>
+                    <div class="col">
+                        <VnInput
+                            filled
+                            v-model="zone.plate"
+                            :label="t('zone.create.plate')"
+                            :rules="[(val) => !!val || t('zone.warnings.plateNotEmpty')]"
+                        />
+                    </div>
+                </div>
+                <div class="row q-col-gutter-md">
+                    <div class="col">
+                        <QInput
+                            filled
+                            v-model="zone.volume"
+                            :label="t('zone.create.volume')"
+                            type="number"
+                            min="0"
+                            :rules="[(val) => !!val || t('zone.warnings.volumeNotEmpty')]"
+                        />
+                    </div>
+                    <div class="col">
+                        <QSelect
+                            filled
+                            v-model="zone.typeFk"
+                            use-input
+                            fill-input
+                            hide-selected
+                            input-debounce="0"
+                            option-label="name"
+                            option-value="id"
+                            emit-value
+                            map-options
+                            :label="t('zone.create.type')"
+                            :options="filteredZoneTypes"
+                            :rules="[(val) => !!val || t('zone.warnings.typeNotEmpty')]"
+                            @filter="filterType"
+                        >
+                            <template v-if="zone.typeFk" #append>
+                                <QIcon
+                                    name="cancel"
+                                    @click.stop.prevent="zone.typeFk = null"
+                                    class="cursor-pointer"
+                                />
+                            </template>
+                            <template #no-option>
+                                <QItem>
+                                    <QItemSection class="text-grey">
+                                        {{ t('zone.warnings.noData') }}
+                                    </QItemSection>
+                                </QItem>
+                            </template>
+                        </QSelect>
+                    </div>
+                </div>
+            </QCard>
+            <div class="q-mt-md">
+                <QBtn :label="t('zone.type.submit')" type="submit" color="primary" />
+                <QBtn
+                    :label="t('zone.type.reset')"
+                    type="reset"
+                    color="primary"
+                    flat
+                    class="q-ml-sm"
+                />
+            </div>
+        </QForm>
+    </QPage>
+</template>
+
+<style lang="scss" scoped>
+.q-page {
+    display: flex;
+    justify-content: center;
+    align-items: flex-start;
+}
+
+.q-form {
+    width: 70%;
+}
+</style>
diff --git a/src/pages/Zone/ZoneList.vue b/src/pages/Zone/ZoneList.vue
new file mode 100644
index 0000000000..00502e1f7c
--- /dev/null
+++ b/src/pages/Zone/ZoneList.vue
@@ -0,0 +1,97 @@
+<script setup>
+import axios from 'axios';
+import { useQuasar } from 'quasar';
+import VnPaginate from 'src/components/ui/VnPaginate.vue';
+import { useArrayData } from 'src/composables/useArrayData';
+import { useI18n } from 'vue-i18n';
+import { useRouter } from 'vue-router';
+import CardList from 'components/ui/CardList.vue';
+import VnLv from 'components/ui/VnLv.vue';
+
+const quasar = useQuasar();
+const arrayData = useArrayData('ZoneList');
+const store = arrayData.store;
+const router = useRouter();
+const { t } = useI18n();
+
+const filter = {
+    include: {
+        relation: 'type',
+        scope: {
+            fields: 'name',
+        },
+    },
+};
+
+function navigate(id) {
+    router.push({ path: `/zone/${id}/edit` });
+}
+
+function create() {
+    router.push({ path: `/zone/create` });
+}
+
+async function remove(row) {
+    try {
+        await axios.delete(`Zones/${row.id}`).then(async () => {
+            quasar.notify({
+                message: t('zone.list.removeItem'),
+                type: 'positive',
+            });
+            store.data.splice(store.data.indexOf(row), 1);
+        });
+    } catch (error) {
+        //
+    }
+}
+</script>
+
+<template>
+    <QPage class="column items-center q-pa-md">
+        <div class="vn-card-list">
+            <VnPaginate
+                data-key="ZoneList"
+                url="/Zones"
+                order="id DESC"
+                :filter="filter"
+                auto-load
+            >
+                <template #body="{ rows }">
+                    <CardList
+                        v-for="row of rows"
+                        :key="row.id"
+                        :title="(row.label || '').toString()"
+                        :id="row.id"
+                        @click="navigate(row.id)"
+                    >
+                        <template #list-items>
+                            <VnLv
+                                :label="t('zone.list.plate')"
+                                :title-label="t('zone.list.plate')"
+                                :value="row.plate"
+                            />
+                            <VnLv :label="t('zone.list.volume')" :value="row?.volume" />
+                            <VnLv :label="t('zone.list.type')" :value="row?.type?.name" />
+                        </template>
+                        <template #actions>
+                            <QBtn
+                                :label="t('components.smartCard.openCard')"
+                                @click.stop="navigate(row.id)"
+                                outline
+                            />
+                            <QBtn
+                                :label="t('zone.list.remove')"
+                                @click.stop="remove(row)"
+                                color="primary"
+                                style="margin-top: 15px"
+                            />
+                        </template>
+                    </CardList>
+                </template>
+            </VnPaginate>
+        </div>
+        <QPageSticky position="bottom-right" :offset="[18, 18]">
+            <QBtn @click="create" fab icon="add" color="primary" />
+        </QPageSticky>
+    </QPage>
+</template>
diff --git a/src/pages/Zone/ZoneMain.vue b/src/pages/Zone/ZoneMain.vue
new file mode 100644
index 0000000000..66ce78f232
--- /dev/null
+++ b/src/pages/Zone/ZoneMain.vue
@@ -0,0 +1,17 @@
+<script setup>
+import { useStateStore } from 'stores/useStateStore';
+import LeftMenu from 'src/components/LeftMenu.vue';
+
+const stateStore = useStateStore();
+</script>
+
+<template>
+    <QDrawer v-model="stateStore.leftDrawer" show-if-above :width="256">
+        <QScrollArea class="fit text-grey-8">
+            <LeftMenu />
+        </QScrollArea>
+    </QDrawer>
+    <QPageContainer>
+        <RouterView></RouterView>
+    </QPageContainer>
+</template>

From f14d6310514c949c81de6ac07083dd686e37e87b Mon Sep 17 00:00:00 2001
From: Javier Segarra <jsegarra@verdnatura.es>
Date: Thu, 25 Apr 2024 14:32:42 +0200
Subject: [PATCH 06/27] feat #7271 ZoneDescriptor

---
 src/pages/Zone/Card/ZoneDescriptor.vue        | 127 ++++++++++++++++++
 .../Zone/Card/ZoneDescriptorMenuItems.vue     | 108 +++++++++++++++
 src/pages/Zone/Card/ZoneDescriptorProxy.vue   |  16 +++
 3 files changed, 251 insertions(+)
 create mode 100644 src/pages/Zone/Card/ZoneDescriptor.vue
 create mode 100644 src/pages/Zone/Card/ZoneDescriptorMenuItems.vue
 create mode 100644 src/pages/Zone/Card/ZoneDescriptorProxy.vue

diff --git a/src/pages/Zone/Card/ZoneDescriptor.vue b/src/pages/Zone/Card/ZoneDescriptor.vue
new file mode 100644
index 0000000000..665cb6f0e6
--- /dev/null
+++ b/src/pages/Zone/Card/ZoneDescriptor.vue
@@ -0,0 +1,127 @@
+<script setup>
+import { ref, computed } from 'vue';
+import { useRoute } from 'vue-router';
+import { useI18n } from 'vue-i18n';
+
+import CardDescriptor from 'components/ui/CardDescriptor.vue';
+import VnLv from 'src/components/ui/VnLv.vue';
+import ZoneDescriptorMenuItems from './ZoneDescriptorMenuItems.vue';
+
+import useCardDescription from 'src/composables/useCardDescription';
+import { toDate } from 'src/filters';
+
+const $props = defineProps({
+    id: {
+        type: Number,
+        required: false,
+        default: null,
+    },
+});
+
+const route = useRoute();
+const { t } = useI18n();
+
+const filter = {
+    fields: [
+        'id',
+        'ref',
+        'shipped',
+        'landed',
+        'totalEntries',
+        'warehouseInFk',
+        'warehouseOutFk',
+        'cargoSupplierFk',
+        'agencyModeFk',
+    ],
+    include: [
+        {
+            relation: 'warehouseIn',
+            scope: {
+                fields: ['name'],
+            },
+        },
+        {
+            relation: 'warehouseOut',
+            scope: {
+                fields: ['name'],
+            },
+        },
+    ],
+};
+
+const entityId = computed(() => {
+    return $props.id || route.params.id;
+});
+
+const data = ref(useCardDescription());
+
+const setData = (entity) => {
+    data.value = useCardDescription(entity.ref, entity.id);
+};
+</script>
+
+<template>
+    <CardDescriptor
+        module="Zone"
+        :url="`Zones/${entityId}`"
+        :title="data.title"
+        :subtitle="data.subtitle"
+        :filter="filter"
+        @on-fetch="setData"
+        data-key="travelData"
+    >
+        <template #header-extra-action>
+            <QBtn
+                round
+                flat
+                dense
+                size="md"
+                icon="local_airport"
+                color="white"
+                class="link"
+                :to="{ name: 'ZoneList' }"
+            >
+                <QTooltip>
+                    {{ t('Go to module index') }}
+                </QTooltip>
+            </QBtn>
+        </template>
+        <template #menu="{ entity }">
+            <ZoneDescriptorMenuItems :travel="entity" />
+        </template>
+        <template #body="{ entity }">
+            <VnLv :label="t('globals.wareHouseIn')" :value="entity.warehouseIn.name" />
+            <VnLv :label="t('globals.wareHouseOut')" :value="entity.warehouseOut.name" />
+            <VnLv :label="t('globals.shipped')" :value="toDate(entity.shipped)" />
+            <VnLv :label="t('globals.landed')" :value="toDate(entity.landed)" />
+            <VnLv :label="t('globals.totalEntries')" :value="entity.totalEntries" />
+        </template>
+        <template #actions="{ entity }">
+            <QCardActions>
+                <QBtn
+                    :to="{
+                        name: 'ZoneList',
+                        query: {
+                            params: JSON.stringify({
+                                agencyModeFk: entity.agencyModeFk,
+                            }),
+                        },
+                    }"
+                    size="md"
+                    icon="local_airport"
+                    color="primary"
+                >
+                    <QTooltip>{{ t('All travels with current agency') }}</QTooltip>
+                </QBtn>
+            </QCardActions>
+        </template>
+    </CardDescriptor>
+</template>
+
+<i18n>
+es:
+    Go to module index: Ir al índice del módulo
+    The travel will be deleted: El envío será eliminado
+    Do you want to delete this travel?: ¿Quieres eliminar este envío?
+    All travels with current agency: Todos los envíos con la agencia actual
+</i18n>
diff --git a/src/pages/Zone/Card/ZoneDescriptorMenuItems.vue b/src/pages/Zone/Card/ZoneDescriptorMenuItems.vue
new file mode 100644
index 0000000000..920d83dfe9
--- /dev/null
+++ b/src/pages/Zone/Card/ZoneDescriptorMenuItems.vue
@@ -0,0 +1,108 @@
+<script setup>
+import { computed } from 'vue';
+import { useQuasar } from 'quasar';
+import { useRouter } from 'vue-router';
+import { useI18n } from 'vue-i18n';
+
+import VnConfirm from 'components/ui/VnConfirm.vue';
+
+import axios from 'axios';
+import useNotify from 'src/composables/useNotify.js';
+import { useRole } from 'src/composables/useRole';
+
+const $props = defineProps({
+    travel: {
+        type: Object,
+        default: () => {},
+    },
+});
+
+const { t } = useI18n();
+const router = useRouter();
+const quasar = useQuasar();
+const { notify } = useNotify();
+const role = useRole();
+
+const redirectToCreateView = (queryParams) => {
+    router.push({ name: 'ZoneCreate', query: { travelData: queryParams } });
+};
+
+const cloneZone = () => {
+    const stringifiedZoneData = JSON.stringify($props.travel);
+    redirectToCreateView(stringifiedZoneData);
+};
+
+const cloneZoneWithEntries = () => {
+    try {
+        axios.post(`Zones/${$props.travel.id}/cloneWithEntries`);
+        notify('globals.dataSaved', 'positive');
+    } catch (err) {
+        console.err('Error cloning travel with entries');
+    }
+};
+
+const isBuyer = computed(() => {
+    return role.hasAny(['buyer']);
+});
+
+const openDeleteEntryDialog = (id) => {
+    quasar
+        .dialog({
+            component: VnConfirm,
+            componentProps: {
+                title: t('The travel will be deleted'),
+                message: t('Do you want to delete this travel?'),
+            },
+        })
+        .onOk(async () => {
+            await deleteZone(id);
+        });
+};
+
+const deleteZone = async (id) => {
+    try {
+        await axios.delete(`Zones/${id}`);
+        router.push({ name: 'ZoneList' });
+        notify('globals.dataDeleted', 'positive');
+    } catch (err) {
+        console.error('Error deleting travel');
+    }
+};
+</script>
+
+<template>
+    <QItem v-ripple clickable @click="cloneZone(travel)">
+        <QItemSection>{{ t('travel.summary.cloneShipping') }}</QItemSection>
+    </QItem>
+    <QItem v-ripple clickable @click="cloneZoneWithEntries()">
+        <QItemSection>
+            {{ t('travel.summary.CloneZoneAndEntries') }}
+        </QItemSection>
+    </QItem>
+    <QItem
+        v-if="isBuyer && travel.totalEntries === 0"
+        v-ripple
+        clickable
+        @click="openDeleteEntryDialog(travel.id)"
+    >
+        <QItemSection>
+            {{ t('travel.summary.deleteZone') }}
+        </QItemSection>
+    </QItem>
+    <QItem v-ripple clickable>
+        <QItemSection>
+            <RouterLink
+                :to="{ name: 'EntryCreate', query: { travelFk: travel.id } }"
+                class="color-vn-text"
+            >
+                {{ t('travel.summary.AddEntry') }}
+            </RouterLink>
+        </QItemSection>
+    </QItem>
+</template>
+
+<i18n>
+es:
+    The travel will be deleted: El envío será eliminado
+    Do you want to delete this travel?: ¿Quieres eliminar este envío?
+</i18n>
diff --git a/src/pages/Zone/Card/ZoneDescriptorProxy.vue b/src/pages/Zone/Card/ZoneDescriptorProxy.vue
new file mode 100644
index 0000000000..15c5fb0e54
--- /dev/null
+++ b/src/pages/Zone/Card/ZoneDescriptorProxy.vue
@@ -0,0 +1,16 @@
+<script setup>
+import ZoneDescriptor from './ZoneDescriptor.vue';
+
+const $props = defineProps({
+    id: {
+        type: Number,
+        required: true,
+    },
+});
+</script>
+
+<template>
+    <QPopupProxy>
+        <ZoneDescriptor v-if="$props.id" :id="$props.id" />
+    </QPopupProxy>
+</template>

From ac219bfbb79d748b9f19d51d816d457af235368e Mon Sep 17 00:00:00 2001
From: wbuezas <wbuezas@verdnatura.es>
Date: Tue, 30 Apr 2024 16:15:48 -0300
Subject: [PATCH 07/27] Item shelvings

---
 src/i18n/locale/en.yml               |   1 +
 src/i18n/locale/es.yml               |   1 +
 src/pages/Item/Card/ItemShelving.vue | 275 +++++++++++++++++++++++++++
 src/pages/Item/locale/en.yml         |  14 ++
 src/pages/Item/locale/es.yml         |  14 ++
 src/router/modules/item.js           |  10 +
 6 files changed, 315 insertions(+)
 create mode 100644 src/pages/Item/Card/ItemShelving.vue
 create mode 100644 src/pages/Item/locale/en.yml
 create mode 100644 src/pages/Item/locale/es.yml

diff --git a/src/i18n/locale/en.yml b/src/i18n/locale/en.yml
index abe59fe18d..1000fd3089 100644
--- a/src/i18n/locale/en.yml
+++ b/src/i18n/locale/en.yml
@@ -1135,6 +1135,7 @@ item:
         tax: Tax
         barcode: Barcode
         botanical: Botanical
+        shelving: Shelving
     descriptor:
         item: Item
         buyer: Buyer
diff --git a/src/i18n/locale/es.yml b/src/i18n/locale/es.yml
index 06aa057e39..2727b91135 100644
--- a/src/i18n/locale/es.yml
+++ b/src/i18n/locale/es.yml
@@ -1134,6 +1134,7 @@ item:
         botanical: 'Botánico'
         barcode: 'Código de barras'
         log: Historial
+        shelving: Carros
     descriptor:
         item: Artículo
         buyer: Comprador
diff --git a/src/pages/Item/Card/ItemShelving.vue b/src/pages/Item/Card/ItemShelving.vue
new file mode 100644
index 0000000000..573c4be0c0
--- /dev/null
+++ b/src/pages/Item/Card/ItemShelving.vue
@@ -0,0 +1,275 @@
+<script setup>
+import { onMounted, ref, computed, reactive } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { useRoute } from 'vue-router';
+
+import FetchData from 'components/FetchData.vue';
+import VnInput from 'src/components/common/VnInput.vue';
+import VnSelect from 'src/components/common/VnSelect.vue';
+import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
+
+import { toDateFormat } from 'src/filters/date.js';
+import { dashIfEmpty } from 'src/filters';
+import { useArrayData } from 'src/composables/useArrayData';
+import useNotify from 'src/composables/useNotify.js';
+import { useVnConfirm } from 'composables/useVnConfirm';
+import axios from 'axios';
+
+const route = useRoute();
+const { t } = useI18n();
+const { notify } = useNotify();
+const { openConfirmationModal } = useVnConfirm();
+
+const rowsSelected = ref([]);
+const parkingsOptions = ref([]);
+const shelvingsOptions = ref([]);
+
+const exprBuilder = (param, value) => {
+    switch (param) {
+        case 'parking':
+        case 'shelving':
+        case 'label':
+        case 'packing':
+        case 'itemFk':
+            return { [param]: value };
+    }
+};
+
+const params = reactive({ itemFk: route.params.id });
+
+const arrayData = useArrayData('ItemShelvings', {
+    url: 'ItemShelvingPlacementSupplyStocks',
+    userParams: params,
+    exprBuilder: exprBuilder,
+});
+const rows = computed(() => arrayData.store.data || []);
+
+const applyColumnFilter = async (col) => {
+    try {
+        const paramKey = col.columnFilter?.filterParamKey || col.field;
+        params[paramKey] = col.columnFilter.filterValue;
+        await arrayData.addFilter({ filter: null, params });
+    } catch (err) {
+        console.error('Error applying column filter', err);
+    }
+};
+
+const getInputEvents = (col) => {
+    return col.columnFilter.type === 'select'
+        ? { 'update:modelValue': () => applyColumnFilter(col) }
+        : {
+              'keyup.enter': () => applyColumnFilter(col),
+          };
+};
+
+const columns = computed(() => [
+    {
+        label: t('shelvings.created'),
+        name: 'created',
+        field: 'created',
+        align: 'left',
+        sortable: true,
+        columnFilter: null,
+        format: (val) => toDateFormat(val),
+    },
+
+    {
+        label: t('shelvings.item'),
+        name: 'item',
+        field: 'itemFk',
+        align: 'left',
+        sortable: true,
+        columnFilter: null,
+    },
+    {
+        label: t('shelvings.concept'),
+        name: 'concept',
+        align: 'left',
+        sortable: true,
+        columnFilter: null,
+    },
+    {
+        label: t('shelvings.parking'),
+        name: 'parking',
+        field: 'parking',
+        align: 'left',
+        sortable: true,
+        format: (val) => dashIfEmpty(val),
+        columnFilter: {
+            component: VnSelect,
+            type: 'select',
+            filterValue: null,
+            event: getInputEvents,
+            attrs: {
+                options: parkingsOptions.value,
+                'option-value': 'code',
+                'option-label': 'code',
+                dense: true,
+            },
+        },
+    },
+    {
+        label: t('shelvings.shelving'),
+        name: 'shelving',
+        field: 'shelving',
+        align: 'left',
+        sortable: true,
+        format: (val) => dashIfEmpty(val),
+        columnFilter: {
+            component: VnSelect,
+            type: 'select',
+            filterValue: null,
+            event: getInputEvents,
+            attrs: {
+                options: shelvingsOptions.value,
+                'option-value': 'code',
+                'option-label': 'code',
+                dense: true,
+            },
+        },
+    },
+    {
+        label: t('shelvings.label'),
+        name: 'label',
+        align: 'left',
+        sortable: true,
+        format: (_, row) => (row.stock / row.packing).toFixed(2),
+        columnFilter: {
+            component: VnInput,
+            type: 'text',
+            filterParamKey: 'label',
+            filterValue: null,
+            event: getInputEvents,
+            attrs: {
+                dense: true,
+            },
+        },
+    },
+    {
+        label: t('shelvings.packing'),
+        field: 'packing',
+        name: 'packing',
+        align: 'left',
+        sortable: true,
+        columnFilter: {
+            component: VnInput,
+            type: 'text',
+            filterValue: null,
+            event: getInputEvents,
+            attrs: {
+                dense: true,
+            },
+        },
+        format: (val) => dashIfEmpty(val),
+    },
+]);
+
+const totalLabels = computed(() =>
+    rows.value.reduce((acc, row) => acc + row.stock / row.packing, 0).toFixed(2)
+);
+
+const removeLines = async () => {
+    try {
+        const itemShelvingIds = rowsSelected.value.map((row) => row.itemShelvingFk);
+        await axios.post('ItemShelvings/deleteItemShelvings', { itemShelvingIds });
+        rowsSelected.value = [];
+        notify('shelvings.shelvingsRemoved', 'positive');
+        await arrayData.fetch({ append: false });
+    } catch (err) {
+        console.error('Error removing lines', err);
+    }
+};
+onMounted(async () => {
+    await arrayData.fetch({ append: false });
+});
+</script>
+
+<template>
+    <FetchData
+        url="parkings"
+        :filter="{ fields: ['code'], order: 'code ASC' }"
+        auto-load
+        @on-fetch="(data) => (parkingsOptions = data)"
+    />
+    <FetchData
+        url="shelvings"
+        :filter="{ fields: ['code'], order: 'code ASC' }"
+        auto-load
+        @on-fetch="(data) => (shelvingsOptions = data)"
+    />
+    <QToolbar class="bg-vn-dark justify-end">
+        <div id="st-data" class="q-py-sm flex items-center">
+            <div class="q-pa-md q-mr-lg" style="border: 2px solid #222">
+                <QCardSection horizontal>
+                    <span class="text-weight-bold text-subtitle1 text-center full-width">
+                        {{ t('shelvings.total') }}
+                    </span>
+                </QCardSection>
+                <QCardSection class="column items-center" horizontal>
+                    <div>
+                        <span class="details-label"
+                            >{{ t('shelvings.totalLabels') }}
+                        </span>
+                        <span>: {{ totalLabels }}</span>
+                    </div>
+                </QCardSection>
+            </div>
+            <QBtn
+                color="primary"
+                icon="delete"
+                :disabled="!rowsSelected.length"
+                @click="
+                    openConfirmationModal(
+                        t('shelvings.removeConfirmTitle'),
+                        t('shelvings.removeConfirmSubtitle'),
+                        removeLines
+                    )
+                "
+            >
+                <QTooltip>
+                    {{ t('shelvings.removeLines') }}
+                </QTooltip>
+            </QBtn>
+        </div>
+        <QSpace />
+        <div id="st-actions"></div>
+    </QToolbar>
+    <QPage class="column items-center q-pa-md">
+        <QTable
+            :rows="rows"
+            :columns="columns"
+            row-key="id"
+            :pagination="{ rowsPerPage: 0 }"
+            class="full-width q-mt-md"
+            selection="multiple"
+            v-model:selected="rowsSelected"
+            :no-data-label="t('globals.noResults')"
+        >
+            <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.columnFilter"
+                            v-model="col.columnFilter.filterValue"
+                            v-bind="col.columnFilter.attrs"
+                            v-on="col.columnFilter.event(col)"
+                            dense
+                        />
+                    </QTd>
+                </QTr>
+            </template>
+            <template #body-cell-concept="{ row }">
+                <QTd @click.stop>
+                    <span class="link">{{ row.longName }}</span>
+                    <ItemDescriptorProxy :id="row.itemFk" />
+                </QTd>
+            </template>
+        </QTable>
+    </QPage>
+</template>
diff --git a/src/pages/Item/locale/en.yml b/src/pages/Item/locale/en.yml
new file mode 100644
index 0000000000..19bbd7f06c
--- /dev/null
+++ b/src/pages/Item/locale/en.yml
@@ -0,0 +1,14 @@
+shelvings:
+    created: Created
+    item: Item
+    concept: Concept
+    parking: Parking
+    shelving: Shelving
+    label: Label
+    packing: Packing
+    total: Total
+    totalLabels: Total labels
+    removeLines: Remove selected lines
+    shelvingsRemoved: ItemShelvings removed
+    removeConfirmTitle: Selected lines will be deleted
+    removeConfirmSubtitle: Are you sure you want to continue?
diff --git a/src/pages/Item/locale/es.yml b/src/pages/Item/locale/es.yml
new file mode 100644
index 0000000000..6f99bb4bec
--- /dev/null
+++ b/src/pages/Item/locale/es.yml
@@ -0,0 +1,14 @@
+shelvings:
+    created: Creado
+    item: Artículo
+    concept: Concepto
+    parking: Parking
+    shelving: Matrícula
+    label: Etiqueta
+    packing: Packing
+    total: Total
+    totalLabels: Total etiquetas
+    removeLines: Eliminar líneas seleccionadas
+    shelvingsRemoved: Carros eliminados
+    removeConfirmTitle: Las líneas seleccionadas serán eliminadas
+    removeConfirmSubtitle: ¿Seguro que quieres continuar?
diff --git a/src/router/modules/item.js b/src/router/modules/item.js
index bc1e72a948..59bd07d209 100644
--- a/src/router/modules/item.js
+++ b/src/router/modules/item.js
@@ -20,6 +20,7 @@ export default {
             'ItemTax',
             'ItemBotanical',
             'ItemBarcode',
+            'ItemShelving',
         ],
     },
     children: [
@@ -157,6 +158,15 @@ export default {
                     },
                     component: () => import('src/pages/Item/Card/ItemBotanical.vue'),
                 },
+                {
+                    path: 'shelving',
+                    name: 'ItemShelving',
+                    meta: {
+                        title: 'shelving',
+                        icon: 'vn:inventory',
+                    },
+                    component: () => import('src/pages/Item/Card/ItemShelving.vue'),
+                },
             ],
         },
     ],

From 554b9e814b9af83e65c091cf1d0a30c61d6ead3e Mon Sep 17 00:00:00 2001
From: wbuezas <wbuezas@verdnatura.es>
Date: Thu, 2 May 2024 09:08:03 -0300
Subject: [PATCH 08/27] WIP

---
 src/components/FilterItemForm.vue           |  11 +-
 src/pages/Entry/Card/EntryBuysImport.vue    |   1 +
 src/pages/Item/Card/CreateIntrastatForm.vue |  52 +++++
 src/pages/Item/Card/ItemBasicData.vue       | 240 +++++++++++++++++++-
 src/pages/Item/locale/en.yml                |  17 ++
 src/pages/Item/locale/es.yml                |  17 ++
 6 files changed, 334 insertions(+), 4 deletions(-)
 create mode 100644 src/pages/Item/Card/CreateIntrastatForm.vue

diff --git a/src/components/FilterItemForm.vue b/src/components/FilterItemForm.vue
index e031999e2d..00659f8fd2 100644
--- a/src/components/FilterItemForm.vue
+++ b/src/components/FilterItemForm.vue
@@ -1,7 +1,6 @@
 <script setup>
 import { ref, reactive, computed } from 'vue';
 import { useI18n } from 'vue-i18n';
-import { useRoute } from 'vue-router';
 
 import VnRow from 'components/ui/VnRow.vue';
 import FetchData from 'components/FetchData.vue';
@@ -12,10 +11,16 @@ import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
 import axios from 'axios';
 import { dashIfEmpty } from 'src/filters';
 
+const props = defineProps({
+    url: {
+        type: String,
+        required: true,
+    },
+});
+
 const emit = defineEmits(['itemSelected']);
 
 const { t } = useI18n();
-const route = useRoute();
 
 const itemFilter = {
     include: [
@@ -100,7 +105,7 @@ const fetchResults = async () => {
         }
         filter.where = where;
 
-        const { data } = await axios.get(`Entries/${route.params.id}/lastItemBuys`, {
+        const { data } = await axios.get(props.url, {
             params: { filter: JSON.stringify(filter) },
         });
         tableRows.value = data;
diff --git a/src/pages/Entry/Card/EntryBuysImport.vue b/src/pages/Entry/Card/EntryBuysImport.vue
index 705f56b682..6d856f0689 100644
--- a/src/pages/Entry/Card/EntryBuysImport.vue
+++ b/src/pages/Entry/Card/EntryBuysImport.vue
@@ -251,6 +251,7 @@ const redirectToBuysView = () => {
                                 >
                                     <template #form>
                                         <FilterItemForm
+                                            :url="`Entries/${route.params.id}/lastItemBuys`"
                                             @item-selected="row[col.field] = $event"
                                         />
                                     </template>
diff --git a/src/pages/Item/Card/CreateIntrastatForm.vue b/src/pages/Item/Card/CreateIntrastatForm.vue
new file mode 100644
index 0000000000..3fd1ffe22c
--- /dev/null
+++ b/src/pages/Item/Card/CreateIntrastatForm.vue
@@ -0,0 +1,52 @@
+<script setup>
+import { reactive, ref, onMounted, nextTick } from 'vue';
+import { useI18n } from 'vue-i18n';
+import { useRoute } from 'vue-router';
+
+import VnInput from 'src/components/common/VnInput.vue';
+import VnRow from 'components/ui/VnRow.vue';
+import FormModelPopup from 'components/FormModelPopup.vue';
+
+const { t } = useI18n();
+const emit = defineEmits(['onDataSaved']);
+const route = useRoute();
+
+const identifierInputRef = ref(null);
+const intrastatFormData = reactive({});
+
+const onDataSaved = (formData, requestResponse) => {
+    emit('onDataSaved', formData, requestResponse);
+};
+
+onMounted(async () => {
+    await nextTick();
+    identifierInputRef.value.focus();
+});
+</script>
+
+<template>
+    <FormModelPopup
+        :url-update="`Items/${route.params.id}/createIntrastat`"
+        model="itemGenus"
+        :title="t('createIntrastatForm.title')"
+        :form-initial-data="intrastatFormData"
+        @on-data-saved="onDataSaved"
+    >
+        <template #form-inputs="{ data }">
+            <VnRow class="row q-gutter-md q-mb-md">
+                <VnInput
+                    ref="identifierInputRef"
+                    :label="t('createIntrastatForm.identifier')"
+                    type="number"
+                    v-model.number="data.intrastatId"
+                    :required="true"
+                />
+                <VnInput
+                    :label="t('createIntrastatForm.description')"
+                    v-model="data.description"
+                    :required="true"
+                />
+            </VnRow>
+        </template>
+    </FormModelPopup>
+</template>
diff --git a/src/pages/Item/Card/ItemBasicData.vue b/src/pages/Item/Card/ItemBasicData.vue
index 334cf049d5..dc6868fba5 100644
--- a/src/pages/Item/Card/ItemBasicData.vue
+++ b/src/pages/Item/Card/ItemBasicData.vue
@@ -1 +1,239 @@
-<template>Item basic data</template>
+<script setup>
+import { ref } from 'vue';
+import { useRoute } from 'vue-router';
+import { useI18n } from 'vue-i18n';
+
+import FetchData from 'components/FetchData.vue';
+import FormModel from 'components/FormModel.vue';
+import VnRow from 'components/ui/VnRow.vue';
+import VnInput from 'src/components/common/VnInput.vue';
+import VnSelect from 'src/components/common/VnSelect.vue';
+import VnSelectDialog from 'src/components/common/VnSelectDialog.vue';
+import FilterItemForm from 'src/components/FilterItemForm.vue';
+import CreateIntrastatForm from './CreateIntrastatForm.vue';
+
+const route = useRoute();
+const { t } = useI18n();
+
+const itemTypesOptions = ref([]);
+const itemsWithNameOptions = ref([]);
+const intrastatsOptions = ref([]);
+const expensesOptions = ref([]);
+
+const onIntrastatCreated = (response, formData) => {
+    intrastatsOptions.value = [...intrastatsOptions.value, response];
+    formData.intrastatFk = response.id;
+};
+</script>
+<template>
+    <FetchData
+        url="ItemTypes"
+        :filter="{
+            fields: ['id', 'name', 'categoryFk'],
+            include: 'category',
+            order: 'name ASC',
+        }"
+        @on-fetch="(data) => (itemTypesOptions = data)"
+        auto-load
+    />
+    <FetchData
+        url="Items/withName"
+        :filter="{
+            fields: ['id', 'name'],
+            order: 'id DESC',
+        }"
+        @on-fetch="(data) => (itemsWithNameOptions = data)"
+        auto-load
+    />
+    <FetchData
+        url="Intrastats"
+        :filter="{
+            fields: ['id', 'description'],
+            order: 'description ASC',
+        }"
+        @on-fetch="(data) => (intrastatsOptions = data)"
+        auto-load
+    />
+    <FetchData
+        url="Expenses"
+        :filter="{
+            fields: ['id', 'name'],
+            order: 'name ASC',
+        }"
+        @on-fetch="(data) => (expensesOptions = data)"
+        auto-load
+    />
+    <FormModel
+        :url="`Items/${route.params.id}`"
+        :url-update="`Items/${route.params.id}`"
+        model="item"
+        auto-load
+        :clear-store-on-unmount="false"
+    >
+        <template #form="{ data }">
+            <VnRow class="row q-gutter-md q-mb-md">
+                <VnSelect
+                    :label="t('basicData.type')"
+                    v-model="data.typeFk"
+                    :options="itemTypesOptions"
+                    option-value="id"
+                    option-label="name"
+                    hide-selected
+                    map-options
+                >
+                    <template #option="scope">
+                        <QItem v-bind="scope.itemProps">
+                            <QItemSection>
+                                <QItemLabel>{{ scope.opt?.name }}</QItemLabel>
+                                <QItemLabel caption>
+                                    {{ scope.opt?.category?.name }}
+                                </QItemLabel>
+                            </QItemSection>
+                        </QItem>
+                    </template>
+                </VnSelect>
+                <VnInput :label="t('basicData.reference')" v-model="data.comment" />
+                <VnInput :label="t('basicData.relevancy')" v-model="data.relevancy" />
+            </VnRow>
+            <VnRow class="row q-gutter-md q-mb-md">
+                <VnInput :label="t('basicData.stems')" v-model="data.stems" />
+                <VnInput
+                    :label="t('basicData.multiplier')"
+                    v-model="data.stemMultiplier"
+                />
+                <VnSelectDialog
+                    :label="t('basicData.generic')"
+                    v-model="data.genericFk"
+                    :options="itemsWithNameOptions"
+                    option-value="id"
+                    option-label="name"
+                    map-options
+                    hide-selected
+                    action-icon="filter_alt"
+                >
+                    <template #form>
+                        <FilterItemForm
+                            url="Items/withName"
+                            @item-selected="data.genericFk = $event"
+                        />
+                    </template>
+                    <template #option="scope">
+                        <QItem v-bind="scope.itemProps">
+                            <QItemSection>
+                                <QItemLabel>{{ scope.opt?.name }}</QItemLabel>
+                                <QItemLabel caption> #{{ scope.opt?.id }} </QItemLabel>
+                            </QItemSection>
+                        </QItem>
+                    </template>
+                </VnSelectDialog>
+            </VnRow>
+            <VnRow class="row q-gutter-md q-mb-md">
+                <VnSelectDialog
+                    :label="t('basicData.intrastat')"
+                    v-model="data.intrastatFk"
+                    :options="intrastatsOptions"
+                    option-value="id"
+                    option-label="description"
+                    map-options
+                    hide-selected
+                >
+                    <template #form>
+                        <CreateIntrastatForm
+                            @on-data-saved="
+                                (_, requestResponse) =>
+                                    onIntrastatCreated(requestResponse, data)
+                            "
+                        />
+                    </template>
+                    <template #option="scope">
+                        <QItem v-bind="scope.itemProps">
+                            <QItemSection>
+                                <QItemLabel>{{ scope.opt?.description }}</QItemLabel>
+                                <QItemLabel caption> #{{ scope.opt?.id }} </QItemLabel>
+                            </QItemSection>
+                        </QItem>
+                    </template>
+                </VnSelectDialog>
+                <div class="col">
+                    <VnSelect
+                        :label="t('basicData.expense')"
+                        v-model="data.expenseFk"
+                        :options="expensesOptions"
+                        option-value="id"
+                        option-label="name"
+                        hide-selected
+                        map-options
+                    />
+                </div>
+            </VnRow>
+            <VnRow class="row q-gutter-md q-mb-md">
+                <VnInput
+                    :label="t('basicData.weightByPiece')"
+                    v-model.number="data.weightByPiece"
+                    :min="0"
+                    type="number"
+                />
+                <VnInput
+                    :label="t('basicData.boxUnits')"
+                    v-model.number="data.packingOut"
+                    :min="0"
+                    type="number"
+                />
+                <VnInput
+                    :label="t('basicData.recycledPlastic')"
+                    v-model.number="data.recycledPlastic"
+                    :min="0"
+                    type="number"
+                />
+                <VnInput
+                    :label="t('basicData.nonRecycledPlastic')"
+                    v-model.number="data.nonRecycledPlastic"
+                    :min="0"
+                    type="number"
+                />
+            </VnRow>
+            <!-- <VnRow class="row q-gutter-md q-mb-md">
+                <div class="col">
+                    <QInput
+                        :label="t('entry.basicData.observation')"
+                        type="textarea"
+                        v-model="data.observation"
+                        :maxlength="45"
+                        counter
+                        fill-input
+                    />
+                </div>
+            </VnRow> -->
+            <!-- <VnRow class="row q-gutter-md q-mb-md">
+                <div class="col">
+                    <QCheckbox
+                        v-model="data.isOrdered"
+                        :label="t('entry.basicData.ordered')"
+                    />
+                </div>
+                <div class="col">
+                    <QCheckbox
+                        v-model="data.isConfirmed"
+                        :label="t('entry.basicData.confirmed')"
+                    />
+                </div>
+                <div class="col">
+                    <QCheckbox
+                        v-model="data.isExcludedFromAvailable"
+                        :label="t('entry.basicData.excludedFromAvailable')"
+                    />
+                </div>
+                <div class="col">
+                    <QCheckbox v-model="data.isRaid" :label="t('entry.basicData.raid')" />
+                </div>
+                <div class="col">
+                    <QCheckbox
+                        v-if="isAdministrative()"
+                        v-model="data.isBooked"
+                        :label="t('entry.basicData.booked')"
+                    />
+                </div>
+            </VnRow> -->
+        </template>
+    </FormModel>
+</template>
diff --git a/src/pages/Item/locale/en.yml b/src/pages/Item/locale/en.yml
index ec3b134e81..410ed5edcf 100644
--- a/src/pages/Item/locale/en.yml
+++ b/src/pages/Item/locale/en.yml
@@ -11,3 +11,20 @@ itemDiary:
     showBefore: Show what's before the inventory
     since: Since
     warehouse: Warehouse
+basicData:
+    type: Type
+    reference: Reference
+    relevancy: Relevancy
+    stems: Stems
+    multiplier: Multiplier
+    generic: Generic
+    intrastat: Intrastat
+    expense: Expense
+    weightByPiece: Weight/Piece
+    boxUnits: Units/Box
+    recycledPlastic: Recycled plastic
+    nonRecycledPlastic: Non recycled plastic
+createIntrastatForm:
+    title: New intrastat
+    identifier: Identifier
+    description: Description
diff --git a/src/pages/Item/locale/es.yml b/src/pages/Item/locale/es.yml
index 4f76313fa6..6540c36c86 100644
--- a/src/pages/Item/locale/es.yml
+++ b/src/pages/Item/locale/es.yml
@@ -11,3 +11,20 @@ itemDiary:
     showBefore: Mostrar lo anterior al inventario
     since: Desde
     warehouse: Almacén
+basicData:
+    type: Tipo
+    reference: Referencia
+    relevancy: Relevancia
+    stems: Tallos
+    multiplier: Multiplicador
+    generic: Genérico
+    intrastat: Intrastat
+    expense: Gasto
+    weightByPiece: Peso (gramos)/tallo
+    boxUnits: Unidades/caja
+    recycledPlastic: Plástico reciclado
+    nonRecycledPlastic: Plástico no reciclado
+createIntrastatForm:
+    title: Nuevo intrastat
+    identifier: Identificador
+    description: Descripción

From cb0d7214650b02e1ab5a7af4e348e037a03b8e84 Mon Sep 17 00:00:00 2001
From: Javier Segarra <jsegarra@verdnatura.es>
Date: Thu, 2 May 2024 14:40:56 +0200
Subject: [PATCH 09/27] feat: zoneSummary

---
 src/pages/Zone/Card/ZoneSummary.vue | 94 +++++++++++++++++++++++++++++
 1 file changed, 94 insertions(+)
 create mode 100644 src/pages/Zone/Card/ZoneSummary.vue

diff --git a/src/pages/Zone/Card/ZoneSummary.vue b/src/pages/Zone/Card/ZoneSummary.vue
new file mode 100644
index 0000000000..00df03cb01
--- /dev/null
+++ b/src/pages/Zone/Card/ZoneSummary.vue
@@ -0,0 +1,94 @@
+<script setup>
+import { ref, onMounted, computed } from 'vue';
+import { useRoute } from 'vue-router';
+import { useI18n } from 'vue-i18n';
+import { dashIfEmpty } from 'src/filters';
+import { getUrl } from 'src/composables/getUrl';
+import VnLv from 'src/components/ui/VnLv.vue';
+import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue';
+import CardSummary from 'components/ui/CardSummary.vue';
+import VnUserLink from 'src/components/ui/VnUserLink.vue';
+import VnTitle from 'src/components/common/VnTitle.vue';
+
+const route = useRoute();
+const { t } = useI18n();
+
+const $props = defineProps({
+    id: {
+        type: Number,
+        default: 0,
+    },
+});
+
+const entityId = computed(() => $props.id || route.params.id);
+const zoneUrl = ref();
+
+onMounted(async () => {
+    zoneUrl.value = (await getUrl('')) + `zone/${entityId.value}/`;
+});
+
+const filter = computed(() => {
+    return { where: { id: entityId.value } };
+});
+</script>
+
+<template>
+    <CardSummary
+        data-key="zoneData"
+        ref="summary"
+        :url="`Zones/summary`"
+        :filter="filter"
+    >
+        <template #header="{ entity }">
+            <div>{{ entity.id }} - {{ entity.firstName }} {{ entity.lastName }}</div>
+        </template>
+        <template #body="{ entity: zone }">
+            <QCard class="vn-one">
+                <VnTitle
+                    :url="zoneUrl + `basic-data`"
+                    :text="t('zone.summary.basicData')"
+                />
+                <VnLv :label="t('zone.card.name')" :value="zone.user?.nickname" />
+                <VnLv
+                    :label="t('zone.list.department')"
+                    :value="zone.department?.department?.name"
+                />
+                <VnLv :label="t('zone.list.email')" :value="zone.user.email" copy />
+                <VnLv :label="t('zone.summary.boss')" link>
+                    <template #value>
+                        <VnUserLink
+                            v-if="zone.boss"
+                            :name="dashIfEmpty(zone.boss?.name)"
+                            :zone-id="zone.bossFk"
+                        />
+                    </template>
+                </VnLv>
+                <VnLv :value="zone.mobileExtension">
+                    <template #label>
+                        {{ t('zone.summary.phoneExtension') }}
+                        <VnLinkPhone :phone-number="zone.mobileExtension" />
+                    </template>
+                </VnLv>
+                <VnLv :value="zone.phone">
+                    <template #label>
+                        {{ t('zone.summary.entPhone') }}
+                        <VnLinkPhone :phone-number="zone.phone" />
+                    </template>
+                </VnLv>
+                <VnLv :label="t('zone.summary.locker')" :value="zone.locker" />
+            </QCard>
+            <QCard class="vn-one">
+                <VnTitle :text="t('zone.summary.userData')" />
+                <VnLv :label="t('zone.summary.userId')" :value="zone.user.id" />
+                <VnLv :label="t('zone.card.name')" :value="zone.user.nickname" />
+                <VnLv :label="t('zone.summary.role')" :value="zone.user.role.name" />
+                <VnLv :value="zone?.sip?.extension">
+                    <template #label>
+                        {{ t('zone.summary.sipExtension') }}
+                        <VnLinkPhone :phone-number="zone?.sip?.extension" />
+                    </template>
+                </VnLv>
+            </QCard>
+        </template>
+    </CardSummary>
+</template>

From d68934a5269ff18f69ef8398e32914228bccab22 Mon Sep 17 00:00:00 2001
From: wbuezas <wbuezas@verdnatura.es>
Date: Thu, 2 May 2024 11:51:04 -0300
Subject: [PATCH 10/27] Item basic data

---
 src/pages/Item/Card/ItemBasicData.vue | 66 ++++++++++++---------------
 src/pages/Item/locale/en.yml          |  7 +++
 src/pages/Item/locale/es.yml          |  7 +++
 3 files changed, 44 insertions(+), 36 deletions(-)

diff --git a/src/pages/Item/Card/ItemBasicData.vue b/src/pages/Item/Card/ItemBasicData.vue
index dc6868fba5..7e8fa1d923 100644
--- a/src/pages/Item/Card/ItemBasicData.vue
+++ b/src/pages/Item/Card/ItemBasicData.vue
@@ -192,48 +192,42 @@ const onIntrastatCreated = (response, formData) => {
                     type="number"
                 />
             </VnRow>
-            <!-- <VnRow class="row q-gutter-md q-mb-md">
-                <div class="col">
-                    <QInput
-                        :label="t('entry.basicData.observation')"
-                        type="textarea"
-                        v-model="data.observation"
-                        :maxlength="45"
-                        counter
-                        fill-input
-                    />
-                </div>
-            </VnRow> -->
-            <!-- <VnRow class="row q-gutter-md q-mb-md">
-                <div class="col">
+            <VnRow class="row q-gutter-md q-mb-md">
+                <QCheckbox v-model="data.isActive" :label="t('basicData.isActive')" />
+                <QCheckbox v-model="data.hasKgPrice" :label="t('basicData.hasKgPrice')" />
+                <div>
                     <QCheckbox
-                        v-model="data.isOrdered"
-                        :label="t('entry.basicData.ordered')"
+                        v-model="data.isFragile"
+                        :label="t('basicData.isFragile')"
+                        class="q-mr-sm"
                     />
+                    <QIcon name="info" class="cursor-pointer" size="xs">
+                        <QTooltip max-width="300px">
+                            {{ t('basicData.isFragileTooltip') }}
+                        </QTooltip>
+                    </QIcon>
                 </div>
-                <div class="col">
+                <div>
                     <QCheckbox
-                        v-model="data.isConfirmed"
-                        :label="t('entry.basicData.confirmed')"
+                        v-model="data.isPhotoRequested"
+                        :label="t('basicData.isPhotoRequested')"
+                        class="q-mr-sm"
                     />
+                    <QIcon name="info" class="cursor-pointer" size="xs">
+                        <QTooltip>
+                            {{ t('basicData.isPhotoRequestedTooltip') }}
+                        </QTooltip>
+                    </QIcon>
                 </div>
-                <div class="col">
-                    <QCheckbox
-                        v-model="data.isExcludedFromAvailable"
-                        :label="t('entry.basicData.excludedFromAvailable')"
-                    />
-                </div>
-                <div class="col">
-                    <QCheckbox v-model="data.isRaid" :label="t('entry.basicData.raid')" />
-                </div>
-                <div class="col">
-                    <QCheckbox
-                        v-if="isAdministrative()"
-                        v-model="data.isBooked"
-                        :label="t('entry.basicData.booked')"
-                    />
-                </div>
-            </VnRow> -->
+            </VnRow>
+            <VnRow class="row q-gutter-md q-mb-md">
+                <QInput
+                    :label="t('basicData.description')"
+                    type="textarea"
+                    v-model="data.description"
+                    fill-input
+                />
+            </VnRow>
         </template>
     </FormModel>
 </template>
diff --git a/src/pages/Item/locale/en.yml b/src/pages/Item/locale/en.yml
index 410ed5edcf..35d65b2dab 100644
--- a/src/pages/Item/locale/en.yml
+++ b/src/pages/Item/locale/en.yml
@@ -24,6 +24,13 @@ basicData:
     boxUnits: Units/Box
     recycledPlastic: Recycled plastic
     nonRecycledPlastic: Non recycled plastic
+    description: Description
+    isActive: Active
+    hasKgPrice: Price in kg
+    isFragile: Fragile
+    isFragileTooltip: Is shown at website, app that this item cannot travel (wreath, palms, ...)
+    isPhotoRequested: Do photo
+    isPhotoRequestedTooltip: This item does need a photo
 createIntrastatForm:
     title: New intrastat
     identifier: Identifier
diff --git a/src/pages/Item/locale/es.yml b/src/pages/Item/locale/es.yml
index 6540c36c86..498520c269 100644
--- a/src/pages/Item/locale/es.yml
+++ b/src/pages/Item/locale/es.yml
@@ -24,6 +24,13 @@ basicData:
     boxUnits: Unidades/caja
     recycledPlastic: Plástico reciclado
     nonRecycledPlastic: Plástico no reciclado
+    description: Descripción
+    isActive: Activo
+    hasKgPrice: Precio en kg
+    isFragile: Frágil
+    isFragileTooltip: Se muestra en la web app, que este artículo no puede viajar (coronas, palmas, ...)
+    isPhotoRequested: Hacer foto
+    isPhotoRequestedTooltip: Este artículo necesita una foto
 createIntrastatForm:
     title: Nuevo intrastat
     identifier: Identificador

From 1a5f2fbea2a7c821ca7d9e70fa5224f6a82dc22f Mon Sep 17 00:00:00 2001
From: Jon <jon@verdnatura.es>
Date: Fri, 3 May 2024 12:30:53 +0200
Subject: [PATCH 11/27] feat: refs #7271 advanced structure

---
 src/i18n/locale/en.yml                       |   4 +
 src/i18n/locale/es.yml                       |   6 +-
 src/pages/Zone/Card/ZoneBasicData.vue        | 103 +++++++++++++++++++
 src/pages/Zone/Card/ZoneCalendar.vue         |   0
 src/pages/Zone/Card/ZoneLocations.vue        |   0
 src/pages/Zone/Card/ZoneLog.vue              |   6 ++
 src/pages/Zone/Card/ZoneSummary.vue          |   3 +
 src/pages/Zone/Card/ZoneWarehouses.vue       |  59 +++++++++++
 src/pages/Zone/Delivery/ZoneDeliveryList.vue |   3 +-
 src/pages/Zone/Upcoming/ZoneUpcomingList.vue |   3 +-
 src/pages/Zone/ZoneCreate.vue                |   8 +-
 src/pages/Zone/ZoneDeliveryDays.vue          |   0
 src/pages/Zone/ZoneFilterPanel.vue           |  55 ++++++++++
 src/pages/Zone/ZoneList.vue                  |  67 +++++++-----
 src/pages/Zone/ZoneUpcoming.vue              |  53 ++++++++++
 src/pages/Zone/locale/en.yml                 |  19 ++++
 src/pages/Zone/locale/es.yml                 |  19 ++++
 17 files changed, 376 insertions(+), 32 deletions(-)
 create mode 100644 src/pages/Zone/Card/ZoneBasicData.vue
 create mode 100644 src/pages/Zone/Card/ZoneCalendar.vue
 create mode 100644 src/pages/Zone/Card/ZoneLocations.vue
 create mode 100644 src/pages/Zone/Card/ZoneLog.vue
 create mode 100644 src/pages/Zone/Card/ZoneSummary.vue
 create mode 100644 src/pages/Zone/Card/ZoneWarehouses.vue
 create mode 100644 src/pages/Zone/ZoneDeliveryDays.vue
 create mode 100644 src/pages/Zone/ZoneFilterPanel.vue
 create mode 100644 src/pages/Zone/ZoneUpcoming.vue
 create mode 100644 src/pages/Zone/locale/en.yml
 create mode 100644 src/pages/Zone/locale/es.yml

diff --git a/src/i18n/locale/en.yml b/src/i18n/locale/en.yml
index aa65ce08c7..a580f25536 100644
--- a/src/i18n/locale/en.yml
+++ b/src/i18n/locale/en.yml
@@ -90,6 +90,10 @@ globals:
         basicData: Basic data
         log: Logs
         parkingList: Parkings list
+        zones: Zones
+        zonesList: Zones
+        deliveryList: Delivery days
+        upcomingList: Upcoming deliveries
     created: Created
     worker: Worker
     now: Now
diff --git a/src/i18n/locale/es.yml b/src/i18n/locale/es.yml
index da421432de..ef7ebf22b2 100644
--- a/src/i18n/locale/es.yml
+++ b/src/i18n/locale/es.yml
@@ -90,6 +90,10 @@ globals:
         basicData: Datos básicos
         log: Historial
         parkingList: Listado de parkings
+        zones: Zonas
+        zonesList: Zonas
+        deliveryList: Días de entrega
+        upcomingList: Próximos repartos
     created: Fecha creación
     worker: Trabajador
     now: Ahora
@@ -287,7 +291,7 @@ customer:
             hasSepaVnl: Recibido B2B VNL
 entry:
     pageTitles:
-        entries: Entrasdadas
+        entries: Entradas
         list: Listado
         summary: Resumen
         basicData: Datos básicos
diff --git a/src/pages/Zone/Card/ZoneBasicData.vue b/src/pages/Zone/Card/ZoneBasicData.vue
new file mode 100644
index 0000000000..5d57b920e6
--- /dev/null
+++ b/src/pages/Zone/Card/ZoneBasicData.vue
@@ -0,0 +1,103 @@
+<script setup>
+import { useRoute } from 'vue-router';
+import { useI18n } from 'vue-i18n';
+
+import FetchData from 'components/FetchData.vue';
+import FormModel from 'src/components/FormModel.vue';
+import VnRow from 'components/ui/VnRow.vue';
+import VnInput from 'src/components/common/VnInput.vue';
+import { QCheckbox } from 'quasar';
+
+const route = useRoute();
+const { t } = useI18n();
+const zoneFilter = {
+    include: [
+        {
+            relation: 'agency',
+            scope: {
+                fields: ['name'],
+                include: { relation: 'agencyModeFk', scope: { fields: ['id'] } },
+            },
+        },
+        { relation: 'sip', scope: { fields: ['extension', 'secret'] } },
+        { relation: 'department', scope: { include: { relation: 'department' } } },
+        { relation: 'client', scope: { fields: ['phone'] } },
+    ],
+};
+const agencyFilter = {
+    fields: ['id', 'name'],
+    order: 'name ASC',
+    limit: 30,
+};
+</script>
+
+<template>
+    <FetchData
+        :filter="agencyFilter"
+        @on-fetch="(data) => (agencyOptions = data)"
+        auto-load
+        url="agencies"
+    />
+    <FetchData
+        :filter="zoneFilter"
+        @on-fetch="(data) => (zoneOptions = data)"
+        auto-load
+        url="zones"
+    />
+
+    <FormModel
+        :filter="zoneFilter"
+        :url="`zone/${route.params.id}/basic-data`"
+        auto-load
+        model="Zone"
+    >
+        <template #form="{ data }">
+            <VnRow class="row q-gutter-md q-mb-md">
+                <VnInput :label="t('Name')" clearable v-model="data.zone.name" />
+            </VnRow>
+
+            <VnRow class="row q-gutter-md q-mb-md">
+                <VnInput v-model="data.agency.name" :label="t('Agency')" clearable />
+                <VnInput v-model="data.zone.itemMaxSize" :label="t('Max m³')" clearable />
+                <VnInput v-model="data.zone.m3Max" :label="t('Maximum m³')" clearable />
+            </VnRow>
+
+            <VnRow class="row q-gutter-md q-mb-md">
+                <VnInput
+                    v-model="data.zone.travelingDays"
+                    :label="t('Traveling days')"
+                    clearable
+                />
+                <VnInput v-model="data.zone.hour" :label="t('Closing')" clearable />
+            </VnRow>
+
+            <VnRow class="row q-gutter-md q-mb-md">
+                <VnInput v-model="data.zone.price" :label="t('Price')" clearable />
+                <VnInput v-model="data.zone.bonus" :label="t('Bonus')" clearable />
+            </VnRow>
+
+            <VnRow class="row q-gutter-md q-mb-md">
+                <VnInput
+                    v-model="data.zone.inflation"
+                    :label="t('Inflation')"
+                    clearable
+                />
+                <QCheckbox v-model="data.zone.isVolumetric" :label="t('Volumetric')" />
+            </VnRow>
+        </template>
+    </FormModel>
+</template>
+
+<i18n>
+es:
+    Name: Nombre
+    Agency: Agencia
+    Max m³: Medida máxima
+    Maximum m³: M³ maximo
+    Traveling days: Dias de viaje
+    Closing: Cierre
+    Price: Precio
+    Bonus: Bonificación
+    Inflation: Inflación
+    Volumetric: Volumétrico
+</i18n>
diff --git a/src/pages/Zone/Card/ZoneCalendar.vue b/src/pages/Zone/Card/ZoneCalendar.vue
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/pages/Zone/Card/ZoneLocations.vue b/src/pages/Zone/Card/ZoneLocations.vue
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/pages/Zone/Card/ZoneLog.vue b/src/pages/Zone/Card/ZoneLog.vue
new file mode 100644
index 0000000000..373d210b57
--- /dev/null
+++ b/src/pages/Zone/Card/ZoneLog.vue
@@ -0,0 +1,6 @@
+<script setup>
+import VnLog from 'src/components/common/VnLog.vue';
+</script>
+<template>
+    <VnLog model="Zone" url="/ZoneLogs"></VnLog>
+</template>
diff --git a/src/pages/Zone/Card/ZoneSummary.vue b/src/pages/Zone/Card/ZoneSummary.vue
new file mode 100644
index 0000000000..63090b5d4d
--- /dev/null
+++ b/src/pages/Zone/Card/ZoneSummary.vue
@@ -0,0 +1,3 @@
+<template>
+    <div>Si</div>
+</template>
diff --git a/src/pages/Zone/Card/ZoneWarehouses.vue b/src/pages/Zone/Card/ZoneWarehouses.vue
new file mode 100644
index 0000000000..67a81ba4d2
--- /dev/null
+++ b/src/pages/Zone/Card/ZoneWarehouses.vue
@@ -0,0 +1,59 @@
+<script setup>
+import { useRoute } from 'vue-router';
+import VnPaginate from 'components/ui/VnPaginate.vue';
+import CardList from 'components/CardList.vue';
+import VnLv from 'components/ui/VnLv.vue';
+
+const route = useRoute();
+
+function deleteWarehouse() {
+    let row = this.deleteRow;
+    if (!row) return;
+    return this.$http.delete(`${this.path}/${row.id}`).then(() => {
+        let index = this.$.data.indexOf(row);
+        if (index !== -1) this.$.data.splice(index, 1);
+        this.deleteRow = null;
+    });
+}
+</script>
+
+<template>
+    <QPage class="column items-center q-pa-md">
+        <div class="vn-card-list">
+            <VnPaginate
+                data-key="ZoneWarehouses"
+                :url="`Zones/${route.params.id}/warehouses`"
+                auto-load
+            >
+                <template #body="{ rows }">
+                    <CardList
+                        v-for="row of rows"
+                        :key="row.id"
+                        :title="row.name"
+                        :id="row.id"
+                    >
+                        <template #list-items>
+                            <VnLv :value="row.name" />
+                            <QIcon
+                                name="delete"
+                                size="sm"
+                                class="cursor-pointer"
+                                color="primary"
+                                @click="deleteWarehouse()"
+                            >
+                                <QTooltip>
+                                    {{ t('Remove row') }}
+                                </QTooltip>
+                            </QIcon>
+                        </template>
+                    </CardList>
+                </template>
+            </VnPaginate>
+        </div>
+    </QPage>
+</template>
+
+<i18n>
+    es:
+        Remove row: Eliminar fila
+</i18n>
diff --git a/src/pages/Zone/Delivery/ZoneDeliveryList.vue b/src/pages/Zone/Delivery/ZoneDeliveryList.vue
index c7a3cbcdbc..695388a9b1 100644
--- a/src/pages/Zone/Delivery/ZoneDeliveryList.vue
+++ b/src/pages/Zone/Delivery/ZoneDeliveryList.vue
@@ -6,7 +6,6 @@ import { useArrayData } from 'src/composables/useArrayData';
 import { useI18n } from 'vue-i18n';
 import { useRouter } from 'vue-router';
 import CardList from 'components/ui/CardList.vue';
-import VnLv from 'components/ui/VnLv.vue';
 
 const quasar = useQuasar();
 const arrayData = useArrayData('ZoneDeliveryList');
@@ -45,7 +44,7 @@ async function remove(row) {
         <div class="vn-card-list">
             <VnPaginate
                 data-key="ZoneDeliveryList"
-                url="/ZoneDeliverys"
+                url="/Zones/getEvents"
                 order="id DESC"
                 auto-load
             >
diff --git a/src/pages/Zone/Upcoming/ZoneUpcomingList.vue b/src/pages/Zone/Upcoming/ZoneUpcomingList.vue
index 5c417df8fd..2d3016f258 100644
--- a/src/pages/Zone/Upcoming/ZoneUpcomingList.vue
+++ b/src/pages/Zone/Upcoming/ZoneUpcomingList.vue
@@ -6,7 +6,6 @@ import { useArrayData } from 'src/composables/useArrayData';
 import { useI18n } from 'vue-i18n';
 import { useRouter } from 'vue-router';
 import CardList from 'components/ui/CardList.vue';
-import VnLv from 'components/ui/VnLv.vue';
 
 const quasar = useQuasar();
 const arrayData = useArrayData('ZoneUpcomingList');
@@ -45,7 +44,7 @@ async function remove(row) {
         <div class="vn-card-list">
             <VnPaginate
                 data-key="ZoneUpcomingList"
-                url="/ZoneUpcomings"
+                url="/Zones/getUpcomingDeliveries"
                 order="id DESC"
                 auto-load
             >
diff --git a/src/pages/Zone/ZoneCreate.vue b/src/pages/Zone/ZoneCreate.vue
index 8c0ba8c176..93ea9589b1 100644
--- a/src/pages/Zone/ZoneCreate.vue
+++ b/src/pages/Zone/ZoneCreate.vue
@@ -96,7 +96,7 @@ function filterType(val, update) {
                         <QInput
                             filled
                             v-model="zone.label"
-                            :label="t('zone.create.label')"
+                            :label="t('zone.create.name')"
                             type="number"
                             min="0"
                             :rules="[(val) => !!val || t('zone.warnings.labelNotEmpty')]"
@@ -106,7 +106,7 @@ function filterType(val, update) {
                         <VnInput
                             filled
                             v-model="zone.plate"
-                            :label="t('zone.create.plate')"
+                            :label="t('zone.create.agency')"
                             :rules="[(val) => !!val || t('zone.warnings.plateNotEmpty')]"
                         />
                     </div>
@@ -116,7 +116,7 @@ function filterType(val, update) {
                         <QInput
                             filled
                             v-model="zone.volume"
-                            :label="t('zone.create.volume')"
+                            :label="t('zone.create.close')"
                             type="number"
                             min="0"
                             :rules="[(val) => !!val || t('zone.warnings.volumeNotEmpty')]"
@@ -134,7 +134,7 @@ function filterType(val, update) {
                             option-value="id"
                             emit-value
                             map-options
-                            :label="t('zone.create.type')"
+                            :label="t('zone.create.price')"
                             :options="filteredZoneTypes"
                             :rules="[(val) => !!val || t('zone.warnings.typeNotEmpty')]"
                             @filter="filterType"
diff --git a/src/pages/Zone/ZoneDeliveryDays.vue b/src/pages/Zone/ZoneDeliveryDays.vue
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/pages/Zone/ZoneFilterPanel.vue b/src/pages/Zone/ZoneFilterPanel.vue
new file mode 100644
index 0000000000..94765919d0
--- /dev/null
+++ b/src/pages/Zone/ZoneFilterPanel.vue
@@ -0,0 +1,55 @@
+<script setup>
+import { ref } from 'vue';
+import { useI18n } from 'vue-i18n';
+import VnInput from 'components/common/VnInput.vue';
+import FetchData from 'components/FetchData.vue';
+import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
+import VnSelectFilter from 'components/common/VnSelectFilter.vue';
+
+const { t } = useI18n();
+const props = defineProps({
+    dataKey: {
+        type: String,
+        required: true,
+    },
+    exprBuilder: {
+        type: Function,
+        default: null,
+    },
+});
+const agencies = ref([]);
+</script>
+
+<template>
+    <FetchData
+        url="agencies"
+        limit="30"
+        @on-fetch="(data) => (agencies = data)"
+        auto-load
+    />
+    <VnFilterPanel :data-key="props.dataKey" :search-button="true">
+        <template #body="{ params }">
+            <QItem>
+                <QItemSection>
+                    <VnInput :label="t('Name')" v-model="params.name" is-outlined />
+                </QItemSection>
+            </QItem>
+            <QItem>
+                <QItemSection>
+                    <VnSelectFilter
+                        :label="t('Agency')"
+                        v-model="params.agencyModefK"
+                        :options="agencies"
+                        option-value="id"
+                        option-label="name"
+                        @input-value="agencies.fetch()"
+                        dense
+                        outlined
+                        rounded
+                    >
+                    </VnSelectFilter>
+                </QItemSection>
+            </QItem>
+        </template>
+    </VnFilterPanel>
+</template>
diff --git a/src/pages/Zone/ZoneList.vue b/src/pages/Zone/ZoneList.vue
index 00502e1f7c..2140c5e140 100644
--- a/src/pages/Zone/ZoneList.vue
+++ b/src/pages/Zone/ZoneList.vue
@@ -7,21 +7,15 @@ import { useI18n } from 'vue-i18n';
 import { useRouter } from 'vue-router';
 import CardList from 'components/ui/CardList.vue';
 import VnLv from 'components/ui/VnLv.vue';
+import FetchData from 'src/components/FetchData.vue';
+import { useSummaryDialog } from 'src/composables/useSummaryDialog';
 
 const quasar = useQuasar();
 const arrayData = useArrayData('ZoneList');
 const store = arrayData.store;
 const router = useRouter();
 const { t } = useI18n();
-
-const filter = {
-    include: {
-        relation: 'type',
-        scope: {
-            fields: 'name',
-        },
-    },
-};
+const { viewSummary } = useSummaryDialog();
 
 function navigate(id) {
     router.push({ path: `/zone/${id}/edit` });
@@ -44,18 +38,25 @@ async function remove(row) {
         //
     }
 }
+
+function extractHour(dateTime) {
+    const date = new Date(dateTime);
+    const hours = date.getHours().toString().padStart(2, '0');
+    const minutes = date.getMinutes().toString().padStart(2, '0');
+    return `${hours}:${minutes}`;
+}
 </script>
 
 <template>
+    <FetchData
+        url="/Agencies"
+        @on-fetch="(data) => (agencyOptions = data)"
+        :filter="{ fields: ['id', 'name'] }"
+        auto-load
+    />
     <QPage class="column items-center q-pa-md">
         <div class="vn-card-list">
-            <VnPaginate
-                data-key="ZoneList"
-                url="/Zones"
-                order="id DESC"
-                :filter="filter"
-                auto-load
-            >
+            <VnPaginate data-key="ZoneList" url="/Zones" order="id DESC" auto-load>
                 <template #body="{ rows }">
                     <CardList
                         v-for="row of rows"
@@ -66,12 +67,23 @@ async function remove(row) {
                     >
                         <template #list-items>
                             <VnLv
-                                :label="t('zone.list.plate')"
-                                :title-label="t('zone.list.plate')"
-                                :value="row.plate"
+                                :label="t('zone.list.id')"
+                                :title-label="t('zone.list.id')"
+                                :value="row.id"
                             />
-                            <VnLv :label="t('zone.list.volume')" :value="row?.volume" />
-                            <VnLv :label="t('zone.list.type')" :value="row?.type?.name" />
+                            <VnLv :label="t('zone.list.name')" :value="row?.name" />
+                            <VnLv
+                                :label="t('zone.list.agency')"
+                                :options="agencyOptions"
+                                option-value="id"
+                                option-label="name"
+                                :value="row?.agencyFk"
+                            />
+                            <VnLv
+                                :label="t('zone.list.close')"
+                                :value="extractHour(row?.hour)"
+                            />
+                            <VnLv :label="t('zone.list.price')" :value="row?.price" />
                         </template>
                         <template #actions>
                             <QBtn
@@ -80,7 +92,14 @@ async function remove(row) {
                                 outline
                             />
                             <QBtn
-                                :label="t('zone.list.remove')"
+                                :label="t('zone.list.openSummary')"
+                                @click.stop="viewSummary(row.id, ZoneSummary)"
+                                color="primary"
+                                style="margin-top: 15px"
+                            />
+                            <!--AQUI PONER BOTÓN CLONAR-->
+                            <QBtn
+                                :label="t('zone.list.clone')"
                                 @click.stop="remove(row)"
                                 color="primary"
                                 style="margin-top: 15px"
@@ -91,7 +110,9 @@ async function remove(row) {
             </VnPaginate>
         </div>
         <QPageSticky position="bottom-right" :offset="[18, 18]">
-            <QBtn @click="create" fab icon="add" color="primary" />
+            <QBtn @click="create" fab icon="add" color="primary">
+                <QTooltip>{{ t('zone.list.create') }}</QTooltip>
+            </QBtn>
         </QPageSticky>
     </QPage>
 </template>
diff --git a/src/pages/Zone/ZoneUpcoming.vue b/src/pages/Zone/ZoneUpcoming.vue
new file mode 100644
index 0000000000..d405c95f6b
--- /dev/null
+++ b/src/pages/Zone/ZoneUpcoming.vue
@@ -0,0 +1,53 @@
+<script setup>
+import { ref, computed } from 'vue';
+import ZoneFilterPanel from 'components/InvoiceOutNegativeFilter.vue';
+import VnSubToolbar from 'components/ui/VnSubToolbar.vue';
+import { useI18n } from 'vue-i18n';
+
+const { t } = useI18n();
+const arrayData = ref(null);
+const rows = computed(() => arrayData.value.store.data);
+
+const columns = computed(() => [
+    {
+        label: t('Province'),
+        //field: '',
+        //name: '',
+        align: 'left',
+    },
+    {
+        label: t('Closing'),
+        //field: '',
+        //name: '',
+        align: 'left',
+    },
+    {
+        label: t('Id'),
+        //field: '',
+        //name: '',
+        align: 'left',
+    },
+]);
+
+function getWeekDay(jsonDate) {
+    const weekDay = new Date(jsonDate).getDay();
+
+    return this.days[weekDay].locale;
+}
+</script>
+
+<template>
+    <QDrawer v-model="stateStore.rightDrawer" side="right" :width="256" show-if-above>
+        <QScrollArea class="fit text-grey-8">
+            <ZoneFilterPanel data-key="ZoneUpcoming" />
+        </QScrollArea>
+    </QDrawer>
+    <VnSubToolbar />
+    <QPage class="column items-center q-pa-md">
+        <span>
+            {{ t(`${getWeekDay(/*detail.shipped*/)}`) }} -
+            {{ t /*'detail.shipped'*/() }}
+        </span>
+        <QTable :columns="columns" :rows="rows" class="full-width q-mt-md"> </QTable>
+    </QPage>
+</template>
diff --git a/src/pages/Zone/locale/en.yml b/src/pages/Zone/locale/en.yml
new file mode 100644
index 0000000000..e62111d577
--- /dev/null
+++ b/src/pages/Zone/locale/en.yml
@@ -0,0 +1,19 @@
+zone:
+    list:
+        volume: Volume
+        clone: Clone
+        id: Id
+        name: Name
+        agency: Agency
+        close: Close
+        price: Price
+        create: Create zone
+        openSummary: Details
+    create:
+        name: Name
+        agency: Agency
+        close: Close
+        price: Price
+    type:
+        submit: Save
+        reset: Reset
diff --git a/src/pages/Zone/locale/es.yml b/src/pages/Zone/locale/es.yml
new file mode 100644
index 0000000000..5d7a265bfd
--- /dev/null
+++ b/src/pages/Zone/locale/es.yml
@@ -0,0 +1,19 @@
+zone:
+    list:
+        volume: Volumen
+        clone: Clonar
+        id: Id
+        name: Nombre
+        agency: Agencia
+        close: Cierre
+        price: Precio
+        create: Crear zona
+        openSummary: Detalles
+    create:
+        name: Nombre
+        agency: Agencia
+        close: Cierre
+        price: Precio
+    type:
+        submit: Guardar
+        reset: Reiniciar

From 710b45f43ab0ecf3a90142708f060bfe3151f782 Mon Sep 17 00:00:00 2001
From: carlossa <carlossa@verdnatura.es>
Date: Fri, 3 May 2024 14:49:50 +0200
Subject: [PATCH 12/27] refs #6842 isFreelance

---
 src/pages/Worker/Card/WorkerCalendar.vue | 16 ++++++++++++++--
 1 file changed, 14 insertions(+), 2 deletions(-)

diff --git a/src/pages/Worker/Card/WorkerCalendar.vue b/src/pages/Worker/Card/WorkerCalendar.vue
index 288e78dcf4..c0a70571e2 100644
--- a/src/pages/Worker/Card/WorkerCalendar.vue
+++ b/src/pages/Worker/Card/WorkerCalendar.vue
@@ -13,7 +13,7 @@ import axios from 'axios';
 const stateStore = useStateStore();
 const route = useRoute();
 const { t } = useI18n();
-
+const workerIsFreelance = ref();
 const workerCalendarFilterRef = ref(null);
 const workerCalendarRef = ref(null);
 const absenceType = ref(null);
@@ -32,6 +32,12 @@ const onFetchActiveContract = (data) => {
     hasWorkCenter.value = Boolean(data?.workCenterFk);
 };
 
+const isFreelance = async () => {
+    const { data } = await axios.get(`Workers/${route.params.id}`);
+
+    workerIsFreelance.value = data.isFreelance;
+};
+
 const addEvent = (day, newEvent, isFestive = false) => {
     const timestamp = new Date(day).getTime();
     let event = eventsMap.value[timestamp];
@@ -128,6 +134,7 @@ const refreshData = () => {
     updateYearHolidays();
     updateContractHolidays();
     getAbsences();
+    isFreelance();
 };
 
 const onDeletedEvent = (timestamp) => {
@@ -151,6 +158,11 @@ watch([year, businessFk], () => refreshData());
         @on-fetch="(data) => (isSubordinate = data)"
         auto-load
     />
+    <FetchData
+        :url="`Workers/${route.params.id}`"
+        @on-fetch="(data) => (workerIsFreelance = data.isFreelance)"
+        auto-load
+    />
     <template v-if="stateStore.isHeaderMounted()">
         <Teleport to="#actions-append">
             <div class="row q-gutter-x-sm">
@@ -181,7 +193,7 @@ watch([year, businessFk], () => refreshData());
         </QScrollArea>
     </QDrawer>
     <QPage class="column items-center">
-        <QCard v-if="!hasWorkCenter">
+        <QCard v-if="workerIsFreelance">
             <QCardSection class="text-center">
                 {{ t('Autonomous worker') }}
             </QCardSection>

From 743faef9567f4c7f942a3ebd1d27b45d0f510aec Mon Sep 17 00:00:00 2001
From: Javier Segarra <jsegarra@verdnatura.es>
Date: Fri, 3 May 2024 15:05:37 +0200
Subject: [PATCH 13/27] fix: #6842 Not update calendar correctly

---
 src/pages/Worker/Card/WorkerCalendar.vue   | 13 ++++++++++++-
 src/pages/Worker/Card/WorkerDescriptor.vue |  1 -
 2 files changed, 12 insertions(+), 2 deletions(-)

diff --git a/src/pages/Worker/Card/WorkerCalendar.vue b/src/pages/Worker/Card/WorkerCalendar.vue
index c0a70571e2..6677cb351f 100644
--- a/src/pages/Worker/Card/WorkerCalendar.vue
+++ b/src/pages/Worker/Card/WorkerCalendar.vue
@@ -1,5 +1,5 @@
 <script setup>
-import { ref, watch } from 'vue';
+import { nextTick, ref, watch } from 'vue';
 import { useI18n } from 'vue-i18n';
 import { useRoute } from 'vue-router';
 
@@ -10,6 +10,8 @@ import WorkerCalendarItem from 'pages/Worker/Card/WorkerCalendarItem.vue';
 import { useStateStore } from 'stores/useStateStore';
 import axios from 'axios';
 
+import { useRouter } from 'vue-router';
+const router = useRouter();
 const stateStore = useStateStore();
 const route = useRoute();
 const { t } = useI18n();
@@ -143,12 +145,21 @@ const onDeletedEvent = (timestamp) => {
     if (festiveEventsMap.value[timestamp])
         eventsMap.value[timestamp] = festiveEventsMap.value[timestamp];
 };
+const activeContractRef = ref(null);
 
+watch(
+    () => router.currentRoute.value.params.id,
+    async () => {
+        await nextTick();
+        await activeContractRef.value.fetch();
+    }
+);
 watch([year, businessFk], () => refreshData());
 </script>
 
 <template>
     <FetchData
+        ref="activeContractRef"
         :url="`Workers/${route.params.id}/activeContract`"
         @on-fetch="onFetchActiveContract"
         auto-load
diff --git a/src/pages/Worker/Card/WorkerDescriptor.vue b/src/pages/Worker/Card/WorkerDescriptor.vue
index 6f05fcdfb9..922c31d999 100644
--- a/src/pages/Worker/Card/WorkerDescriptor.vue
+++ b/src/pages/Worker/Card/WorkerDescriptor.vue
@@ -36,7 +36,6 @@ const filter = computed(() => {
 });
 
 const sip = ref(null);
-
 watch(
     () => [worker.value?.sip?.extension, state.get('extension')],
     ([newWorkerSip, newStateExtension], [oldWorkerSip, oldStateExtension]) => {

From f3b1de1ee41777981e0bc7e6cbc654f075745693 Mon Sep 17 00:00:00 2001
From: wbuezas <wbuezas@verdnatura.es>
Date: Fri, 3 May 2024 17:19:13 -0300
Subject: [PATCH 14/27] Item tags

---
 src/pages/Item/Card/ItemTags.vue | 119 +++++++++++++++----------------
 src/pages/Item/locale/en.yml     |   6 ++
 src/pages/Item/locale/es.yml     |   6 ++
 3 files changed, 69 insertions(+), 62 deletions(-)

diff --git a/src/pages/Item/Card/ItemTags.vue b/src/pages/Item/Card/ItemTags.vue
index 3067c97683..01df1009bc 100644
--- a/src/pages/Item/Card/ItemTags.vue
+++ b/src/pages/Item/Card/ItemTags.vue
@@ -1,5 +1,5 @@
 <script setup>
-import { ref, onMounted, computed } from 'vue';
+import { ref } from 'vue';
 import { useRoute } from 'vue-router';
 import { useI18n } from 'vue-i18n';
 
@@ -10,65 +10,58 @@ import FetchData from 'components/FetchData.vue';
 import VnSelect from 'src/components/common/VnSelect.vue';
 
 import axios from 'axios';
-import { useArrayData } from 'composables/useArrayData';
 
 const route = useRoute();
 const { t } = useI18n();
 
 const itemTagsRef = ref(null);
 const tagOptions = ref([]);
-
-const arrayData = useArrayData('ItemTags');
-const itemTags = computed(() => {
-    console.log('arrayData.store.data:: ', arrayData.store.data);
-    let map = new Map();
-    (arrayData.store.data || []).forEach((tag) => {
-        map.set(tag.id, tag);
-    });
-    return map;
-});
-// const getHighestPriority = () => {
-//     let max = 0;
-//     console.log('formData:: ', itemTagsRef.value.formData);
-//     itemTagsRef.value.formData.forEach((tag) => {
-//         if (tag.priority > max) max = tag.priority;
-//     });
-//     return max + 1;
-// };
-
-const getHighestPriority = computed(() => {
-    let max = 0;
-    if (!itemTagsRef.value || !itemTagsRef.value.length) return max;
-    console.log('formData:: ', itemTagsRef.value.formData);
-    itemTagsRef.value.formData.forEach((tag) => {
-        if (tag.priority > max) max = tag.priority;
-    });
-    return max + 1;
-});
+const valueOptionsMap = ref(new Map());
 
 const getSelectedTagValues = async (tag) => {
     try {
-        console.log('tag:: ', tag);
-        tag.value = null;
+        if (!tag.tagFk && tag.tag.isFree) return;
         const filter = {
             fields: ['value'],
             order: 'value ASC',
         };
 
         const params = { filter: JSON.stringify(filter) };
-        const { data } = await axios.get(`Tags/${tag.selectedTag.id}/filterValue`, {
+        const { data } = await axios.get(`Tags/${tag.tagFk}/filterValue`, {
             params,
         });
-        tag.valueOptions = data;
+        valueOptionsMap.value.set(tag.tagFk, data);
     } catch (err) {
         console.error('Error getting selected tag values');
     }
 };
 
-onMounted(() => {
-    // if (itemTagsRef.value) itemTagsRef.value.reload();
-    console.log('itemTagsRef:: ', itemTagsRef.value.formData);
-});
+const onItemTagsFetched = async (itemTags) => {
+    (itemTags || []).forEach((tag) => {
+        getSelectedTagValues(tag);
+    });
+};
+
+const handleTagSelected = (rows, index, tag) => {
+    rows[index].tag = tag;
+    rows[index].tagFk = tag.id;
+    rows[index].value = null;
+    getSelectedTagValues(rows[index]);
+};
+
+const getHighestPriority = (rows) => {
+    let max = 0;
+    rows.forEach((tag) => {
+        if (tag.priority > max) max = tag.priority;
+    });
+    return max + 1;
+};
+
+const insertTag = (rows) => {
+    itemTagsRef.value.insert();
+    itemTagsRef.value.formData[itemTagsRef.value.formData.length - 1].priority =
+        getHighestPriority(rows);
+};
 </script>
 
 <template>
@@ -85,14 +78,17 @@ onMounted(() => {
                 data-key="ItemTags"
                 model="ItemTags"
                 url="ItemTags"
-                save-url="Tags/onSubmit"
-                auto-load
+                update-url="Tags/onSubmit"
                 :data-required="{
+                    $index: undefined,
                     itemFk: route.params.id,
-                    priority: getHighestPriority,
+                    priority: undefined,
                     tag: {
                         isFree: undefined,
+                        value: undefined,
+                        name: undefined,
                     },
+                    tagFk: undefined,
                 }"
                 :default-remove="false"
                 :filter="{
@@ -106,6 +102,8 @@ onMounted(() => {
                         },
                     },
                 }"
+                auto-load
+                @on-fetch="onItemTagsFetched"
             >
                 <template #body="{ rows }">
                     <QCard class="q-pl-lg q-py-md">
@@ -115,32 +113,39 @@ onMounted(() => {
                             class="row q-gutter-md q-mb-md"
                         >
                             <VnSelect
-                                :label="t('Tag')"
-                                v-model="row.tagFk"
+                                :label="t('itemTags.tag')"
                                 :options="tagOptions"
+                                :model-value="row.tag"
                                 option-label="name"
-                                option-value="id"
                                 hide-selected
-                                @update:model-value="getSelectedTagValues(row)"
+                                @update:model-value="
+                                    ($event) => handleTagSelected(rows, index, $event)
+                                "
                             />
                             <VnSelect
                                 v-if="row.tag?.isFree === false"
+                                :key="row.tagFk"
                                 :label="t('Value')"
                                 v-model="row.value"
-                                option-value="value"
+                                :options="valueOptionsMap.get(row.tagFk)"
                                 option-label="value"
+                                option-value="value"
                                 emit-value
                                 use-input
+                                class="col"
                                 :is-clearable="false"
                             />
                             <VnInput
-                                v-if="row.tag?.isFree || row.tag?.isFree == undefined"
+                                v-else-if="
+                                    row.tag?.isFree || row.tag?.isFree == undefined
+                                "
                                 v-model="row.value"
-                                :label="t('Value')"
+                                :label="t('itemTags.value')"
                                 :is-clearable="false"
+                                style="width: 100%"
                             />
                             <VnInput
-                                :label="t('Relevancy')"
+                                :label="t('itemTags.relevancy')"
                                 type="number"
                                 v-model="row.priority"
                             />
@@ -153,37 +158,27 @@ onMounted(() => {
                                     size="sm"
                                 >
                                     <QTooltip>
-                                        {{ t('Remove tag') }}
+                                        {{ t('itemTags.removeTag') }}
                                     </QTooltip>
                                 </QIcon>
                             </div>
                         </VnRow>
                         <VnRow>
                             <QIcon
-                                @click="itemTagsRef.insert()"
+                                @click="insertTag(rows)"
                                 class="cursor-pointer"
                                 color="primary"
                                 name="add"
                                 size="sm"
                             >
                                 <QTooltip>
-                                    {{ t('Add tag') }}
+                                    {{ t('itemTags.addTag') }}
                                 </QTooltip>
                             </QIcon>
                         </VnRow>
                     </QCard>
                 </template>
             </CrudModel>
-            <pre>{{ itemTags }}</pre>
         </QPage>
     </div>
 </template>
-
-<i18n>
-es:
-    Remove tag: Quitar etiqueta
-    Add tag: Añadir etiqueta
-    Tag: Etiqueta
-    Value: Valor
-    Relevancy: Relevancia
-</i18n>
diff --git a/src/pages/Item/locale/en.yml b/src/pages/Item/locale/en.yml
index 9acd1de4d8..2ffcd36618 100644
--- a/src/pages/Item/locale/en.yml
+++ b/src/pages/Item/locale/en.yml
@@ -34,3 +34,9 @@ lastEntries:
     package: Package
     freight: Freight
     comission: Comission
+itemTags:
+    removeTag: Remove tag
+    addTag: Add tag
+    tag: Tag
+    value: Value
+    relevancy: Relevancy
diff --git a/src/pages/Item/locale/es.yml b/src/pages/Item/locale/es.yml
index 46e0d9eb79..3047239f2b 100644
--- a/src/pages/Item/locale/es.yml
+++ b/src/pages/Item/locale/es.yml
@@ -34,3 +34,9 @@ lastEntries:
     package: Embalaje
     freight: Porte
     comission: Comisión
+itemTags:
+    removeTag: Quitar etiqueta
+    addTag: Añadir etiqueta
+    tag: Etiqueta
+    value: Valor
+    relevancy: Relevancia

From 0f5d06614d2c1e0e21a433be89957ea340c43b49 Mon Sep 17 00:00:00 2001
From: Javier Segarra <jsegarra@verdnatura.es>
Date: Mon, 6 May 2024 07:24:52 +0200
Subject: [PATCH 15/27] feat: VnInput numberValidation

---
 src/components/common/VnInput.vue | 14 ++++++++++++++
 1 file changed, 14 insertions(+)

diff --git a/src/components/common/VnInput.vue b/src/components/common/VnInput.vue
index 96028862ad..0d7d6edca7 100644
--- a/src/components/common/VnInput.vue
+++ b/src/components/common/VnInput.vue
@@ -52,6 +52,12 @@ const focus = () => {
 defineExpose({
     focus,
 });
+
+const inputRules = (val) => {
+    const { min } = vnInputRef.value.$attrs;
+    if (min >= 0)
+        if (val.toString().indexOf('.') < min) return t('inputMin', { value: min });
+};
 </script>
 
 <template>
@@ -68,6 +74,8 @@ defineExpose({
             :class="{ required: $attrs.required }"
             @keyup.enter="onEnterPress()"
             :clearable="false"
+            :rules="[inputRules]"
+            :lazy-rules="true"
         >
             <template v-if="$slots.prepend" #prepend>
                 <slot name="prepend" />
@@ -85,3 +93,9 @@ defineExpose({
         </QInput>
     </div>
 </template>
+<i18n>
+    en:
+        inputMin: Must be more than {value}
+    es:
+        inputMin: Debe ser mayor a {value}
+</i18n>

From f85665b271e1c01a665a1369052446a67f916ba0 Mon Sep 17 00:00:00 2001
From: Javier Segarra <jsegarra@verdnatura.es>
Date: Mon, 6 May 2024 08:37:41 +0200
Subject: [PATCH 16/27] fix: replace QToolbar by Teleport

---
 src/pages/Item/Card/ItemShelving.vue | 76 +++++++++++++++-------------
 1 file changed, 41 insertions(+), 35 deletions(-)

diff --git a/src/pages/Item/Card/ItemShelving.vue b/src/pages/Item/Card/ItemShelving.vue
index 573c4be0c0..830628f3b1 100644
--- a/src/pages/Item/Card/ItemShelving.vue
+++ b/src/pages/Item/Card/ItemShelving.vue
@@ -14,6 +14,9 @@ import { useArrayData } from 'src/composables/useArrayData';
 import useNotify from 'src/composables/useNotify.js';
 import { useVnConfirm } from 'composables/useVnConfirm';
 import axios from 'axios';
+import { useStateStore } from 'stores/useStateStore';
+
+const stateStore = useStateStore();
 
 const route = useRoute();
 const { t } = useI18n();
@@ -197,43 +200,46 @@ onMounted(async () => {
         auto-load
         @on-fetch="(data) => (shelvingsOptions = data)"
     />
-    <QToolbar class="bg-vn-dark justify-end">
-        <div id="st-data" class="q-py-sm flex items-center">
-            <div class="q-pa-md q-mr-lg" style="border: 2px solid #222">
-                <QCardSection horizontal>
-                    <span class="text-weight-bold text-subtitle1 text-center full-width">
-                        {{ t('shelvings.total') }}
-                    </span>
-                </QCardSection>
-                <QCardSection class="column items-center" horizontal>
-                    <div>
-                        <span class="details-label"
-                            >{{ t('shelvings.totalLabels') }}
+    <template v-if="stateStore.isHeaderMounted()">
+        <Teleport to="#st-data">
+            <div class="q-py-sm flex items-center">
+                <div class="q-pa-md q-mr-lg" style="border: 2px solid #222">
+                    <QCardSection horizontal>
+                        <span
+                            class="text-weight-bold text-subtitle1 text-center full-width"
+                        >
+                            {{ t('shelvings.total') }}
                         </span>
-                        <span>: {{ totalLabels }}</span>
-                    </div>
-                </QCardSection>
+                    </QCardSection>
+                    <QCardSection class="column items-center" horizontal>
+                        <div>
+                            <span class="details-label"
+                                >{{ t('shelvings.totalLabels') }}
+                            </span>
+                            <span>: {{ totalLabels }}</span>
+                        </div></QCardSection
+                    >
+                </div>
+                <QBtn
+                    color="primary"
+                    icon="delete"
+                    :disabled="!rowsSelected.length"
+                    @click="
+                        openConfirmationModal(
+                            t('shelvings.removeConfirmTitle'),
+                            t('shelvings.removeConfirmSubtitle'),
+                            removeLines
+                        )
+                    "
+                >
+                    <QTooltip>
+                        {{ t('shelvings.removeLines') }}
+                    </QTooltip>
+                </QBtn>
             </div>
-            <QBtn
-                color="primary"
-                icon="delete"
-                :disabled="!rowsSelected.length"
-                @click="
-                    openConfirmationModal(
-                        t('shelvings.removeConfirmTitle'),
-                        t('shelvings.removeConfirmSubtitle'),
-                        removeLines
-                    )
-                "
-            >
-                <QTooltip>
-                    {{ t('shelvings.removeLines') }}
-                </QTooltip>
-            </QBtn>
-        </div>
-        <QSpace />
-        <div id="st-actions"></div>
-    </QToolbar>
+        </Teleport>
+    </template>
+
     <QPage class="column items-center q-pa-md">
         <QTable
             :rows="rows"

From 62bf968b0bb056c4669d377cec72589891ee7439 Mon Sep 17 00:00:00 2001
From: Javier Segarra <jsegarra@verdnatura.es>
Date: Mon, 6 May 2024 08:41:12 +0200
Subject: [PATCH 17/27] change action to st-actions

---
 src/pages/Item/Card/ItemShelving.vue | 62 ++++++++++++++--------------
 1 file changed, 30 insertions(+), 32 deletions(-)

diff --git a/src/pages/Item/Card/ItemShelving.vue b/src/pages/Item/Card/ItemShelving.vue
index 830628f3b1..7e7faab361 100644
--- a/src/pages/Item/Card/ItemShelving.vue
+++ b/src/pages/Item/Card/ItemShelving.vue
@@ -202,42 +202,40 @@ onMounted(async () => {
     />
     <template v-if="stateStore.isHeaderMounted()">
         <Teleport to="#st-data">
-            <div class="q-py-sm flex items-center">
-                <div class="q-pa-md q-mr-lg" style="border: 2px solid #222">
-                    <QCardSection horizontal>
-                        <span
-                            class="text-weight-bold text-subtitle1 text-center full-width"
-                        >
-                            {{ t('shelvings.total') }}
+            <div class="q-pa-md q-mr-lg q-ma-xs" style="border: 2px solid #222">
+                <QCardSection horizontal>
+                    <span class="text-weight-bold text-subtitle1 text-center full-width">
+                        {{ t('shelvings.total') }}
+                    </span>
+                </QCardSection>
+                <QCardSection class="column items-center" horizontal>
+                    <div>
+                        <span class="details-label"
+                            >{{ t('shelvings.totalLabels') }}
                         </span>
-                    </QCardSection>
-                    <QCardSection class="column items-center" horizontal>
-                        <div>
-                            <span class="details-label"
-                                >{{ t('shelvings.totalLabels') }}
-                            </span>
-                            <span>: {{ totalLabels }}</span>
-                        </div></QCardSection
-                    >
-                </div>
-                <QBtn
-                    color="primary"
-                    icon="delete"
-                    :disabled="!rowsSelected.length"
-                    @click="
-                        openConfirmationModal(
-                            t('shelvings.removeConfirmTitle'),
-                            t('shelvings.removeConfirmSubtitle'),
-                            removeLines
-                        )
-                    "
+                        <span>: {{ totalLabels }}</span>
+                    </div></QCardSection
                 >
-                    <QTooltip>
-                        {{ t('shelvings.removeLines') }}
-                    </QTooltip>
-                </QBtn>
             </div>
         </Teleport>
+        <Teleport to="#st-actions">
+            <QBtn
+                color="primary"
+                icon="delete"
+                :disabled="!rowsSelected.length"
+                @click="
+                    openConfirmationModal(
+                        t('shelvings.removeConfirmTitle'),
+                        t('shelvings.removeConfirmSubtitle'),
+                        removeLines
+                    )
+                "
+            >
+                <QTooltip>
+                    {{ t('shelvings.removeLines') }}
+                </QTooltip>
+            </QBtn>
+        </Teleport>
     </template>
 
     <QPage class="column items-center q-pa-md">

From 6a81076874456a8a31510bf8f77af13e107eff1e Mon Sep 17 00:00:00 2001
From: Javier Segarra <jsegarra@verdnatura.es>
Date: Mon, 6 May 2024 08:45:29 +0200
Subject: [PATCH 18/27] fix: replace inputMin rule

---
 src/components/common/VnInput.vue | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/components/common/VnInput.vue b/src/components/common/VnInput.vue
index 0d7d6edca7..3f7e46367b 100644
--- a/src/components/common/VnInput.vue
+++ b/src/components/common/VnInput.vue
@@ -55,8 +55,7 @@ defineExpose({
 
 const inputRules = (val) => {
     const { min } = vnInputRef.value.$attrs;
-    if (min >= 0)
-        if (val.toString().indexOf('.') < min) return t('inputMin', { value: min });
+    if (min >= 0) if (Math.floor(val) < min) return t('inputMin', { value: min });
 };
 </script>
 

From 49b6c57ad4b34505f7add33c21449d04b229aade Mon Sep 17 00:00:00 2001
From: Javier Segarra <jsegarra@verdnatura.es>
Date: Mon, 6 May 2024 08:53:12 +0200
Subject: [PATCH 19/27] fix: show Color

---
 src/components/FilterItemForm.vue | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/components/FilterItemForm.vue b/src/components/FilterItemForm.vue
index 00659f8fd2..7af9adf939 100644
--- a/src/components/FilterItemForm.vue
+++ b/src/components/FilterItemForm.vue
@@ -78,7 +78,7 @@ const tableColumns = computed(() => [
     {
         label: t('entry.buys.color'),
         name: 'ink',
-        field: 'inkName',
+        field: (row) => row?.ink?.name,
         align: 'left',
     },
 ]);

From 4d490652ea4c009e3ddf9b80bc1aea26f8767a0d Mon Sep 17 00:00:00 2001
From: Javier Segarra <jsegarra@verdnatura.es>
Date: Mon, 6 May 2024 10:11:07 +0200
Subject: [PATCH 20/27] feat #7271: add template

---
 src/pages/Zone/Card/ZoneLocations.vue  |  1 +
 src/pages/Zone/Card/ZoneWarehouses.vue | 12 +++---------
 src/pages/Zone/ZoneDeliveryDays.vue    |  1 +
 3 files changed, 5 insertions(+), 9 deletions(-)

diff --git a/src/pages/Zone/Card/ZoneLocations.vue b/src/pages/Zone/Card/ZoneLocations.vue
index e69de29bb2..e4305c8983 100644
--- a/src/pages/Zone/Card/ZoneLocations.vue
+++ b/src/pages/Zone/Card/ZoneLocations.vue
@@ -0,0 +1 @@
+<template>Zone Locations</template>
diff --git a/src/pages/Zone/Card/ZoneWarehouses.vue b/src/pages/Zone/Card/ZoneWarehouses.vue
index 67a81ba4d2..7ff73a5e95 100644
--- a/src/pages/Zone/Card/ZoneWarehouses.vue
+++ b/src/pages/Zone/Card/ZoneWarehouses.vue
@@ -6,15 +6,9 @@ import VnLv from 'components/ui/VnLv.vue';
 
 const route = useRoute();
 
-function deleteWarehouse() {
-    let row = this.deleteRow;
-    if (!row) return;
-    return this.$http.delete(`${this.path}/${row.id}`).then(() => {
-        let index = this.$.data.indexOf(row);
-        if (index !== -1) this.$.data.splice(index, 1);
-        this.deleteRow = null;
-    });
-}
+const deleteWarehouse = () => {
+    return true;
+};
 </script>
 
 <template>
diff --git a/src/pages/Zone/ZoneDeliveryDays.vue b/src/pages/Zone/ZoneDeliveryDays.vue
index e69de29bb2..485500dbaa 100644
--- a/src/pages/Zone/ZoneDeliveryDays.vue
+++ b/src/pages/Zone/ZoneDeliveryDays.vue
@@ -0,0 +1 @@
+<template>Zone Delivery days</template>

From 4caf27b4f88b6430001c2aedbc1f3b0f5150d966 Mon Sep 17 00:00:00 2001
From: alexm <alexm@verdnatura.es>
Date: Tue, 7 May 2024 08:37:52 +0200
Subject: [PATCH 21/27] deploy: init version 2422

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 063cf6de4a..7be20a8428 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
     "name": "salix-front",
-    "version": "24.20.0",
+    "version": "24.22.0",
     "description": "Salix frontend",
     "productName": "Salix",
     "author": "Verdnatura",

From ac0993aa11e40a72126ac232d713269ab706c9f0 Mon Sep 17 00:00:00 2001
From: carlossa <carlossa@verdnatura.es>
Date: Tue, 7 May 2024 10:20:26 +0200
Subject: [PATCH 22/27] refs #6842 pr changes

---
 src/pages/Worker/Card/WorkerCalendar.vue | 10 +++-------
 1 file changed, 3 insertions(+), 7 deletions(-)

diff --git a/src/pages/Worker/Card/WorkerCalendar.vue b/src/pages/Worker/Card/WorkerCalendar.vue
index 6677cb351f..2e525aa303 100644
--- a/src/pages/Worker/Card/WorkerCalendar.vue
+++ b/src/pages/Worker/Card/WorkerCalendar.vue
@@ -16,6 +16,7 @@ const stateStore = useStateStore();
 const route = useRoute();
 const { t } = useI18n();
 const workerIsFreelance = ref();
+const WorkerFreelanceRef = ref();
 const workerCalendarFilterRef = ref(null);
 const workerCalendarRef = ref(null);
 const absenceType = ref(null);
@@ -34,12 +35,6 @@ const onFetchActiveContract = (data) => {
     hasWorkCenter.value = Boolean(data?.workCenterFk);
 };
 
-const isFreelance = async () => {
-    const { data } = await axios.get(`Workers/${route.params.id}`);
-
-    workerIsFreelance.value = data.isFreelance;
-};
-
 const addEvent = (day, newEvent, isFestive = false) => {
     const timestamp = new Date(day).getTime();
     let event = eventsMap.value[timestamp];
@@ -136,7 +131,7 @@ const refreshData = () => {
     updateYearHolidays();
     updateContractHolidays();
     getAbsences();
-    isFreelance();
+    WorkerFreelanceRef.value.fetch();
 };
 
 const onDeletedEvent = (timestamp) => {
@@ -172,6 +167,7 @@ watch([year, businessFk], () => refreshData());
     <FetchData
         :url="`Workers/${route.params.id}`"
         @on-fetch="(data) => (workerIsFreelance = data.isFreelance)"
+        ref="WorkerFreelanceRef"
         auto-load
     />
     <template v-if="stateStore.isHeaderMounted()">

From c0dd140a3ab0a22d475be5216421e50186721128 Mon Sep 17 00:00:00 2001
From: Javier Segarra <jsegarra@verdnatura.es>
Date: Tue, 7 May 2024 11:38:12 +0200
Subject: [PATCH 23/27] fix: validTag

---
 src/components/common/VnSelect.vue | 3 ++-
 src/pages/Item/Card/ItemTags.vue   | 9 ++++++++-
 2 files changed, 10 insertions(+), 2 deletions(-)

diff --git a/src/components/common/VnSelect.vue b/src/components/common/VnSelect.vue
index 05c74f00a0..e8083dec29 100644
--- a/src/components/common/VnSelect.vue
+++ b/src/components/common/VnSelect.vue
@@ -57,7 +57,7 @@ const $props = defineProps({
 });
 
 const { t } = useI18n();
-const requiredFieldRule = (val) => !!val || t('globals.fieldRequired');
+const requiredFieldRule = (val) => val ?? t('globals.fieldRequired');
 
 const { optionLabel, optionValue, options, modelValue } = toRefs($props);
 const myOptions = ref([]);
@@ -167,6 +167,7 @@ watch(modelValue, (newValue) => {
         hide-selected
         fill-input
         ref="vnSelectRef"
+        lazy-rules
         :class="{ required: $attrs.required }"
         :rules="$attrs.required ? [requiredFieldRule] : null"
         virtual-scroll-slice-size="options.length"
diff --git a/src/pages/Item/Card/ItemTags.vue b/src/pages/Item/Card/ItemTags.vue
index 01df1009bc..b3cdfffb18 100644
--- a/src/pages/Item/Card/ItemTags.vue
+++ b/src/pages/Item/Card/ItemTags.vue
@@ -105,7 +105,7 @@ const insertTag = (rows) => {
                 auto-load
                 @on-fetch="onItemTagsFetched"
             >
-                <template #body="{ rows }">
+                <template #body="{ rows, validate }">
                     <QCard class="q-pl-lg q-py-md">
                         <VnRow
                             v-for="(row, index) in rows"
@@ -121,6 +121,8 @@ const insertTag = (rows) => {
                                 @update:model-value="
                                     ($event) => handleTagSelected(rows, index, $event)
                                 "
+                                :required="true"
+                                :rules="validate('itemTag.tagFk')"
                             />
                             <VnSelect
                                 v-if="row.tag?.isFree === false"
@@ -134,6 +136,8 @@ const insertTag = (rows) => {
                                 use-input
                                 class="col"
                                 :is-clearable="false"
+                                :required="false"
+                                :rules="validate('itemTag.tagFk')"
                             />
                             <VnInput
                                 v-else-if="
@@ -148,6 +152,8 @@ const insertTag = (rows) => {
                                 :label="t('itemTags.relevancy')"
                                 type="number"
                                 v-model="row.priority"
+                                :required="true"
+                                :rules="validate('itemTag.priority')"
                             />
                             <div class="col-1 row justify-center items-center">
                                 <QIcon
@@ -167,6 +173,7 @@ const insertTag = (rows) => {
                             <QIcon
                                 @click="insertTag(rows)"
                                 class="cursor-pointer"
+                                :disable="!validRow"
                                 color="primary"
                                 name="add"
                                 size="sm"

From 5712810ae28548f97727adaeefed9aceb2015ca1 Mon Sep 17 00:00:00 2001
From: jorgep <jorgep@verdnatura.es>
Date: Tue, 7 May 2024 12:00:20 +0200
Subject: [PATCH 24/27] fix: refs #6938 rollback phone

---
 src/pages/Worker/Card/WorkerBasicData.vue  |  3 --
 src/pages/Worker/Card/WorkerDescriptor.vue | 26 +++++++++++-
 src/pages/Worker/Card/WorkerSummary.vue    | 49 ++++++++++++++++++++--
 3 files changed, 69 insertions(+), 9 deletions(-)

diff --git a/src/pages/Worker/Card/WorkerBasicData.vue b/src/pages/Worker/Card/WorkerBasicData.vue
index 7754720658..c59f4281dc 100644
--- a/src/pages/Worker/Card/WorkerBasicData.vue
+++ b/src/pages/Worker/Card/WorkerBasicData.vue
@@ -27,7 +27,6 @@ const workerFilter = {
         },
         { relation: 'sip', scope: { fields: ['extension', 'secret'] } },
         { relation: 'department', scope: { include: { relation: 'department' } } },
-        { relation: 'client', scope: {fields:['phone']} },
     ],
 };
 const workersFilter = {
@@ -87,7 +86,6 @@ const maritalStatus = [
                     :label="t('Mobile extension')"
                     clearable
                 />
-                <VnInput v-model="data.client.phone" :label="t('Personal phone')" clearable />
             </VnRow>
 
             <VnRow class="row q-gutter-md q-mb-md">
@@ -159,7 +157,6 @@ es:
     Last name: Apellidos
     Business phone: Teléfono de empresa
     Mobile extension: Extensión móvil
-    Personal phone: Teléfono personal
     Boss: Jefe
     Marital status: Estado civil
     Married: Casado/a
diff --git a/src/pages/Worker/Card/WorkerDescriptor.vue b/src/pages/Worker/Card/WorkerDescriptor.vue
index 6f876b8fa7..a20ad55469 100644
--- a/src/pages/Worker/Card/WorkerDescriptor.vue
+++ b/src/pages/Worker/Card/WorkerDescriptor.vue
@@ -31,7 +31,29 @@ const entityId = computed(() => {
 });
 
 const worker = ref();
-const filter = { where: { id: entityId } };
+const filter = {
+    include: [
+        {
+            relation: 'user',
+            scope: {
+                fields: ['email', 'name', 'nickname'],
+            },
+        },
+        {
+            relation: 'department',
+            scope: {
+                include: [
+                    {
+                        relation: 'department',
+                    },
+                ],
+            },
+        },
+        {
+            relation: 'sip',
+        },
+    ],
+};
 
 const sip = ref(null);
 
@@ -60,7 +82,7 @@ const setData = (entity) => {
     <CardDescriptor
         module="Worker"
         data-key="workerData"
-        url="Workers/summary"
+        :url="`Workers/${entityId}`"
         :filter="filter"
         :title="data.title"
         :subtitle="data.subtitle"
diff --git a/src/pages/Worker/Card/WorkerSummary.vue b/src/pages/Worker/Card/WorkerSummary.vue
index 43c493565b..dad21cda5d 100644
--- a/src/pages/Worker/Card/WorkerSummary.vue
+++ b/src/pages/Worker/Card/WorkerSummary.vue
@@ -10,7 +10,7 @@ import CardSummary from 'components/ui/CardSummary.vue';
 import VnUserLink from 'src/components/ui/VnUserLink.vue';
 import VnTitle from 'src/components/common/VnTitle.vue';
 
-const { params } = useRoute();
+const route = useRoute();
 const { t } = useI18n();
 
 const $props = defineProps({
@@ -20,18 +20,53 @@ const $props = defineProps({
     },
 });
 
-const entityId = computed(() => $props.id || params.id);
+const entityId = computed(() => $props.id || route.params.id);
 const workerUrl = ref();
 
 onMounted(async () => {
     workerUrl.value = (await getUrl('')) + `worker/${entityId.value}/`;
 });
 
-const filter = { where: { id: entityId.value } };
+const filter = {
+    include: [
+        {
+            relation: 'user',
+            scope: {
+                fields: ['email', 'name', 'nickname', 'roleFk'],
+                include: {
+                    relation: 'role',
+                    scope: {
+                        fields: ['name'],
+                    },
+                },
+            },
+        },
+        {
+            relation: 'department',
+            scope: {
+                include: {
+                    relation: 'department',
+                    scope: {
+                        fields: ['name'],
+                    },
+                },
+            },
+        },
+        {
+            relation: 'boss',
+        },
+        {
+            relation: 'client',
+        },
+        {
+            relation: 'sip',
+        },
+    ],
+};
 </script>
 
 <template>
-    <CardSummary ref="summary" :url="`Workers/summary`" :filter="filter">
+    <CardSummary ref="summary" :url="`Workers/${entityId}`" :filter="filter">
         <template #header="{ entity }">
             <div>{{ entity.id }} - {{ entity.firstName }} {{ entity.lastName }}</div>
         </template>
@@ -68,6 +103,12 @@ const filter = { where: { id: entityId.value } };
                         <VnLinkPhone :phone-number="worker.phone" />
                     </template>
                 </VnLv>
+                <VnLv :value="worker.client?.phone">
+                    <template #label>
+                        {{ t('worker.summary.personalPhone') }}
+                        <VnLinkPhone :phone-number="worker.client?.phone" />
+                    </template>
+                </VnLv>
                 <VnLv :label="t('worker.summary.locker')" :value="worker.locker" />
             </QCard>
             <QCard class="vn-one">

From 0a71060f603ead9badd05d1f09b0424f4a14e012 Mon Sep 17 00:00:00 2001
From: jorgep <jorgep@verdnatura.es>
Date: Tue, 7 May 2024 15:28:00 +0200
Subject: [PATCH 25/27] fix: refs #6938 workerCard

---
 src/pages/Worker/Card/WorkerCard.vue | 5 +----
 1 file changed, 1 insertion(+), 4 deletions(-)

diff --git a/src/pages/Worker/Card/WorkerCard.vue b/src/pages/Worker/Card/WorkerCard.vue
index e0047bf9ef..d76ef6f59e 100644
--- a/src/pages/Worker/Card/WorkerCard.vue
+++ b/src/pages/Worker/Card/WorkerCard.vue
@@ -1,15 +1,12 @@
 <script setup>
 import VnCard from 'components/common/VnCard.vue';
 import WorkerDescriptor from './WorkerDescriptor.vue';
-
-const filter = { where: {} };
 </script>
 <template>
     <VnCard
         data-key="Worker"
-        custom-url="Workers/Summary"
+        base-url="Workers"
         :descriptor="WorkerDescriptor"
-        :filter="filter"
         searchbar-data-key="WorkerList"
         searchbar-url="Workers/filter"
         searchbar-label="Search worker"

From 5e0db81a211b29b50ec27878434c9d63e76dc3fe Mon Sep 17 00:00:00 2001
From: jorgep <jorgep@verdnatura.es>
Date: Tue, 7 May 2024 15:51:55 +0200
Subject: [PATCH 26/27] fix: refs #6938 conflicts

---
 src/pages/Worker/Card/WorkerBasicData.vue | 7 -------
 1 file changed, 7 deletions(-)

diff --git a/src/pages/Worker/Card/WorkerBasicData.vue b/src/pages/Worker/Card/WorkerBasicData.vue
index 0b4250c687..4f1786a67b 100644
--- a/src/pages/Worker/Card/WorkerBasicData.vue
+++ b/src/pages/Worker/Card/WorkerBasicData.vue
@@ -86,13 +86,6 @@ const maritalStatus = [
                     :label="t('Mobile extension')"
                     clearable
                 />
-                <<<<<<< HEAD
-                <VnInput
-                    v-model="data.client.phone"
-                    :label="t('Personal phone')"
-                    clearable
-                />
-                ======= >>>>>>> 095e62ebac4b6b88bc431f7c87455a0bb0989e80
             </VnRow>
 
             <VnRow class="row q-gutter-md q-mb-md">

From 9a6dd6577460fb2fca8c486f62339670e4b900f8 Mon Sep 17 00:00:00 2001
From: Javier Segarra <jsegarra@verdnatura.es>
Date: Wed, 8 May 2024 07:52:18 +0200
Subject: [PATCH 27/27] feat: remove deprecatedHour fn

---
 src/pages/Zone/Card/ZoneDescriptor.vue | 11 ++---------
 src/pages/Zone/ZoneList.vue            | 10 ++--------
 2 files changed, 4 insertions(+), 17 deletions(-)

diff --git a/src/pages/Zone/Card/ZoneDescriptor.vue b/src/pages/Zone/Card/ZoneDescriptor.vue
index 486e4f063d..93e951801b 100644
--- a/src/pages/Zone/Card/ZoneDescriptor.vue
+++ b/src/pages/Zone/Card/ZoneDescriptor.vue
@@ -5,7 +5,7 @@ import { useI18n } from 'vue-i18n';
 
 import CardDescriptor from 'components/ui/CardDescriptor.vue';
 import VnLv from 'src/components/ui/VnLv.vue';
-import ZoneDescriptorMenuItems from './ZoneDescriptorMenuItems.vue';
+import { toTimeFormat } from 'src/filters/date';
 
 import useCardDescription from 'src/composables/useCardDescription';
 
@@ -40,13 +40,6 @@ const data = ref(useCardDescription());
 const setData = (entity) => {
     data.value = useCardDescription(entity.ref, entity.id);
 };
-
-function extractHour(dateTime) {
-    const date = new Date(dateTime);
-    const hours = date.getHours().toString().padStart(2, '0');
-    const minutes = date.getMinutes().toString().padStart(2, '0');
-    return `${hours}:${minutes}`;
-}
 </script>
 
 <template>
@@ -81,7 +74,7 @@ function extractHour(dateTime) {
         <template #body="{ entity }">
             {{ console.log('entity', entity) }}
             <VnLv :label="t('Agency')" :value="entity.agencyMode.name" />
-            <VnLv :label="t('Closing hour')" :value="extractHour(entity.hour)" />
+            <VnLv :label="t('Closing hour')" :value="toTimeFormat(entity.hour)" />
             <VnLv :label="t('zoneing days')" :value="entity.zoneingDays" />
             <VnLv :label="t('Price')" :value="entity.price" />
             <VnLv :label="t('Bonus')" :value="entity.bonus" />
diff --git a/src/pages/Zone/ZoneList.vue b/src/pages/Zone/ZoneList.vue
index 2140c5e140..f260eb1342 100644
--- a/src/pages/Zone/ZoneList.vue
+++ b/src/pages/Zone/ZoneList.vue
@@ -9,6 +9,7 @@ import CardList from 'components/ui/CardList.vue';
 import VnLv from 'components/ui/VnLv.vue';
 import FetchData from 'src/components/FetchData.vue';
 import { useSummaryDialog } from 'src/composables/useSummaryDialog';
+import { toTimeFormat } from 'src/filters/date';
 
 const quasar = useQuasar();
 const arrayData = useArrayData('ZoneList');
@@ -38,13 +39,6 @@ async function remove(row) {
         //
     }
 }
-
-function extractHour(dateTime) {
-    const date = new Date(dateTime);
-    const hours = date.getHours().toString().padStart(2, '0');
-    const minutes = date.getMinutes().toString().padStart(2, '0');
-    return `${hours}:${minutes}`;
-}
 </script>
 
 <template>
@@ -81,7 +75,7 @@ function extractHour(dateTime) {
                             />
                             <VnLv
                                 :label="t('zone.list.close')"
-                                :value="extractHour(row?.hour)"
+                                :value="toTimeFormat(row?.hour)"
                             />
                             <VnLv :label="t('zone.list.price')" :value="row?.price" />
                         </template>