From 5803b8392b6821bb1fa1a9168aff7fea5fe505be Mon Sep 17 00:00:00 2001
From: jorgep <jorgep@verdnatura.es>
Date: Fri, 15 Mar 2024 16:30:12 +0100
Subject: [PATCH 1/7] feat: refs #6951 clone ticket

---
 .../Ticket/Card/TicketDescriptorMenu.vue      | 90 ++++++++++++-------
 .../ticket/ticketDescriptor.spec.js           | 27 ++++++
 2 files changed, 87 insertions(+), 30 deletions(-)
 create mode 100644 test/cypress/integration/ticket/ticketDescriptor.spec.js

diff --git a/src/pages/Ticket/Card/TicketDescriptorMenu.vue b/src/pages/Ticket/Card/TicketDescriptorMenu.vue
index 95f6a94d9..61167d722 100644
--- a/src/pages/Ticket/Card/TicketDescriptorMenu.vue
+++ b/src/pages/Ticket/Card/TicketDescriptorMenu.vue
@@ -3,7 +3,7 @@ import axios from 'axios';
 import { ref } from 'vue';
 import { useQuasar } from 'quasar';
 import { useI18n } from 'vue-i18n';
-import { useRouter, useRoute } from 'vue-router';
+import { useRouter } from 'vue-router';
 import { usePrintService } from 'composables/usePrintService';
 import SendEmailDialog from 'components/common/SendEmailDialog.vue';
 import VnConfirm from 'components/ui/VnConfirm.vue';
@@ -17,13 +17,14 @@ const props = defineProps({
     },
 });
 
-const router = useRouter();
-const route = useRoute();
-const quasar = useQuasar();
+const { push, currentRoute } = useRouter();
+const { dialog, notify } = useQuasar();
 const { t } = useI18n();
 const { openReport, sendEmail } = usePrintService();
 
 const ticket = ref(props.ticket);
+const ticketId = currentRoute.value.params.id;
+const actions = { remove: remove, clone: clone };
 
 function openDeliveryNote(type = 'deliveryNote', documentType = 'pdf') {
     const path = `Tickets/${ticket.value.id}/delivery-note-${documentType}`;
@@ -35,7 +36,7 @@ function openDeliveryNote(type = 'deliveryNote', documentType = 'pdf') {
 
 function sendDeliveryNoteConfirmation(type = 'deliveryNote', documentType = 'pdf') {
     const customer = ticket.value.client;
-    quasar.dialog({
+    dialog({
         component: SendEmailDialog,
         componentProps: {
             data: {
@@ -67,7 +68,7 @@ function showSmsDialog(template, customData) {
     const address = ticket.value.address;
     const client = ticket.value.client;
     const phone =
-        route.params.phone ||
+        currentRoute.value.params.phone ||
         address.mobile ||
         address.phone ||
         client.mobile ||
@@ -82,7 +83,7 @@ function showSmsDialog(template, customData) {
         Object.assign(data, customData);
     }
 
-    quasar.dialog({
+    dialog({
         component: VnSmsDialog,
         componentProps: {
             phone: phone,
@@ -95,43 +96,63 @@ function showSmsDialog(template, customData) {
 }
 
 async function showSmsDialogWithChanges() {
-    const query = `TicketLogs/${route.params.id}/getChanges`;
+    const query = `TicketLogs/${ticketId}/getChanges`;
     const response = await axios.get(query);
 
     showSmsDialog('orderChanges', { changes: response.data });
 }
 
 async function sendSms(body) {
-    await axios.post(`Tickets/${route.params.id}/sendSms`, body);
-    quasar.notify({
+    await axios.post(`Tickets/${ticketId}/sendSms`, body);
+    notify({
         message: 'Notification sent',
         type: 'positive',
     });
 }
 
-function confirmDelete() {
-    quasar
-        .dialog({
-            component: VnConfirm,
-            componentProps: {
-                promise: remove,
-            },
-        })
-        .onOk(async () => await router.push({ name: 'TicketList' }));
+function openConfirmDialog(callback) {
+    dialog({
+        component: VnConfirm,
+        componentProps: {
+            promise: actions[callback],
+        },
+    });
+}
+
+async function clone() {
+    const opts = { message: t('Ticket cloned'), type: 'positive' };
+    let clonedTicketId;
+
+    try {
+        const { data } = await axios.post(`Tickets/${ticketId}/clone`, {
+            shipped: ticket.value.shipped,
+        });
+        clonedTicketId = data;
+    } catch (e) {
+        opts.message = t('It was not able to clone the ticket');
+        opts.type = 'negative';
+    } finally {
+        notify(opts);
+
+        if (clonedTicketId)
+            push({ name: 'TicketSummary', params: { id: clonedTicketId } });
+    }
 }
 
 async function remove() {
-    const id = route.params.id;
-    await axios.post(`Tickets/${id}/setDeleted`);
+    try {
+        await axios.post(`Tickets/${ticketId}/setDeleted`);
 
-    quasar.notify({
-        message: t('Ticket deleted'),
-        type: 'positive',
-    });
-    quasar.notify({
-        message: t('You can undo this action within the first hour'),
-        icon: 'info',
-    });
+        notify({ message: t('Ticket deleted'), type: 'positive' });
+        notify({
+            message: t('You can undo this action within the first hour'),
+            icon: 'info',
+        });
+
+        push({ name: 'TicketList' });
+    } catch (e) {
+        notify({ message: e.message, type: 'negative' });
+    }
 }
 </script>
 <template>
@@ -227,9 +248,15 @@ async function remove() {
             </QList>
         </QMenu>
     </QItem>
+    <QItem @click="openConfirmDialog('clone')" v-ripple clickable>
+        <QItemSection avatar>
+            <QIcon name="content_copy" />
+        </QItemSection>
+        <QItemSection>{{ t('To clone ticket') }}</QItemSection>
+    </QItem>
     <template v-if="!ticket.isDeleted">
         <QSeparator />
-        <QItem @click="confirmDelete()" v-ripple clickable>
+        <QItem @click="openConfirmDialog('remove')" v-ripple clickable>
             <QItemSection avatar>
                 <QIcon name="delete" />
             </QItemSection>
@@ -253,4 +280,7 @@ es:
     Order changes: Cambios del pedido
     Ticket deleted: Ticket eliminado
     You can undo this action within the first hour: Puedes deshacer esta acción dentro de la primera hora
+    To clone ticket: Clonar ticket
+    Ticket cloned: Ticked clonado
+    It was not able to clone the ticket: No se pudo clonar el ticket
 </i18n>
diff --git a/test/cypress/integration/ticket/ticketDescriptor.spec.js b/test/cypress/integration/ticket/ticketDescriptor.spec.js
new file mode 100644
index 000000000..c212d3b4a
--- /dev/null
+++ b/test/cypress/integration/ticket/ticketDescriptor.spec.js
@@ -0,0 +1,27 @@
+/// <reference types="cypress" />
+describe('Ticket descriptor', () => {
+    const toCloneOpt = '.q-list > :nth-child(5)';
+    const warehouseValue = '.summaryBody > :nth-child(2) > :nth-child(6) > .value > span';
+    const summaryHeader = '.summaryHeader > div';
+
+    beforeEach(() => {
+        const ticketId = 1;
+
+        cy.login('developer');
+        cy.visit(`/#/ticket/${ticketId}/summary`);
+    });
+
+    it('should clone the ticket without warehouse', () => {
+        cy.openLeftMenu();
+        cy.openActionsDescriptor();
+        cy.get(toCloneOpt).click();
+        cy.clickConfirm();
+        cy.get(warehouseValue).contains('-');
+        cy.get(summaryHeader)
+            .invoke('text')
+            .then((text) => {
+                const [, owner] = text.split('-');
+                cy.wrap(owner.trim()).should('eq', 'Bruce Wayne (1101)');
+            });
+    });
+});

From d7d83fd886fedb8eb09f03fd49ae7bbd6a99b3ca Mon Sep 17 00:00:00 2001
From: Javier Segarra <jsegarra@verdnatura.es>
Date: Thu, 21 Mar 2024 14:56:39 +0100
Subject: [PATCH 2/7] refs #7124 feat: autofocus without property

---
 quasar.config.js                  |  2 +-
 src/boot/qformMixin.js            | 32 +++++++++++++++++++++++++++++++
 src/boot/quasar.js                |  6 ++++++
 src/components/ui/VnSearchbar.vue |  2 +-
 4 files changed, 40 insertions(+), 2 deletions(-)
 create mode 100644 src/boot/qformMixin.js
 create mode 100644 src/boot/quasar.js

diff --git a/quasar.config.js b/quasar.config.js
index 2d8289508..80ddc3759 100644
--- a/quasar.config.js
+++ b/quasar.config.js
@@ -29,7 +29,7 @@ module.exports = configure(function (/* ctx */) {
         // app boot file (/src/boot)
         // --> boot files are part of "main.js"
         // https://v2.quasar.dev/quasar-cli/boot-files
-        boot: ['i18n', 'axios', 'vnDate', 'validations'],
+        boot: ['i18n', 'axios', 'vnDate', 'validations', 'quasar'],
 
         // https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
         css: ['app.scss'],
diff --git a/src/boot/qformMixin.js b/src/boot/qformMixin.js
new file mode 100644
index 000000000..37beda4f0
--- /dev/null
+++ b/src/boot/qformMixin.js
@@ -0,0 +1,32 @@
+import { QForm } from 'quasar';
+import { getCurrentInstance } from 'vue';
+
+export default {
+    inject: { QForm },
+    component: { QForm },
+    components: { QForm },
+    extends: { QForm },
+    mounted: function () {
+        const vm = getCurrentInstance();
+        if (vm.type.name === 'QForm')
+            if (![ 'searchbarForm'].includes(this.$el?.id)) {
+                let that = this;
+
+                // AUTOFOCUS
+                const elementsArray = Array.from(this.$el.elements);
+                const index = elementsArray.findIndex(element => element.classList.contains('q-field__native'));
+
+                if (index !== -1) {
+                    const firstInputElement = elementsArray[index];
+                    firstInputElement.focus();
+                }
+
+                // KEYUP Event
+                document.addEventListener('keyup', function (evt) {
+                    if (evt.keyCode === 13) {
+                        that.onSubmit();
+                    }
+                });
+            }
+    },
+};
diff --git a/src/boot/quasar.js b/src/boot/quasar.js
new file mode 100644
index 000000000..a8d9b7ad9
--- /dev/null
+++ b/src/boot/quasar.js
@@ -0,0 +1,6 @@
+import { boot } from 'quasar/wrappers';
+import qFormMixin from './qformMixin';
+
+export default boot(({ app }) => {
+    app.mixin(qFormMixin);
+});
diff --git a/src/components/ui/VnSearchbar.vue b/src/components/ui/VnSearchbar.vue
index 143efcd0f..d86b02166 100644
--- a/src/components/ui/VnSearchbar.vue
+++ b/src/components/ui/VnSearchbar.vue
@@ -108,7 +108,7 @@ async function search() {
 </script>
 
 <template>
-    <QForm @submit="search">
+    <QForm @submit="search" id="searchbarForm">
         <VnInput
             id="searchbar"
             v-model="searchText"

From 561a7a52869ffb8cdd86603d82051047aea7c5bb Mon Sep 17 00:00:00 2001
From: Javier Segarra <jsegarra@verdnatura.es>
Date: Thu, 21 Mar 2024 15:03:48 +0100
Subject: [PATCH 3/7] refs #7124 perf: whitespace

---
 src/boot/qformMixin.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/boot/qformMixin.js b/src/boot/qformMixin.js
index 37beda4f0..a3e2543c4 100644
--- a/src/boot/qformMixin.js
+++ b/src/boot/qformMixin.js
@@ -9,7 +9,7 @@ export default {
     mounted: function () {
         const vm = getCurrentInstance();
         if (vm.type.name === 'QForm')
-            if (![ 'searchbarForm'].includes(this.$el?.id)) {
+            if (!['searchbarForm'].includes(this.$el?.id)) {
                 let that = this;
 
                 // AUTOFOCUS

From c4f7b5e7c0659468487c86d5b9221193f8dfaae0 Mon Sep 17 00:00:00 2001
From: Javier Segarra <jsegarra@verdnatura.es>
Date: Fri, 22 Mar 2024 07:32:21 +0100
Subject: [PATCH 4/7] refs #7124 perf: use find instead findIndex

---
 src/boot/qformMixin.js | 17 ++---------------
 1 file changed, 2 insertions(+), 15 deletions(-)

diff --git a/src/boot/qformMixin.js b/src/boot/qformMixin.js
index a3e2543c4..71f5e9f6d 100644
--- a/src/boot/qformMixin.js
+++ b/src/boot/qformMixin.js
@@ -1,11 +1,6 @@
-import { QForm } from 'quasar';
 import { getCurrentInstance } from 'vue';
 
 export default {
-    inject: { QForm },
-    component: { QForm },
-    components: { QForm },
-    extends: { QForm },
     mounted: function () {
         const vm = getCurrentInstance();
         if (vm.type.name === 'QForm')
@@ -14,19 +9,11 @@ export default {
 
                 // AUTOFOCUS
                 const elementsArray = Array.from(this.$el.elements);
-                const index = elementsArray.findIndex(element => element.classList.contains('q-field__native'));
+                const firstInputElement = elementsArray.find(element => element.classList.contains('q-field__native'));
 
-                if (index !== -1) {
-                    const firstInputElement = elementsArray[index];
+                if (firstInputElement) {
                     firstInputElement.focus();
                 }
-
-                // KEYUP Event
-                document.addEventListener('keyup', function (evt) {
-                    if (evt.keyCode === 13) {
-                        that.onSubmit();
-                    }
-                });
             }
     },
 };

From b11462e244d61b7857345233155284044845857b Mon Sep 17 00:00:00 2001
From: Javier Segarra <jsegarra@verdnatura.es>
Date: Fri, 22 Mar 2024 07:32:33 +0100
Subject: [PATCH 5/7] refs #7124 perf: avoid focus in VnFilterPanel

---
 src/boot/qformMixin.js              | 4 +---
 src/components/ui/VnFilterPanel.vue | 2 +-
 2 files changed, 2 insertions(+), 4 deletions(-)

diff --git a/src/boot/qformMixin.js b/src/boot/qformMixin.js
index 71f5e9f6d..66e3de4f5 100644
--- a/src/boot/qformMixin.js
+++ b/src/boot/qformMixin.js
@@ -4,9 +4,7 @@ export default {
     mounted: function () {
         const vm = getCurrentInstance();
         if (vm.type.name === 'QForm')
-            if (!['searchbarForm'].includes(this.$el?.id)) {
-                let that = this;
-
+            if (!['searchbarForm','filterPanelForm'].includes(this.$el?.id)) {
                 // AUTOFOCUS
                 const elementsArray = Array.from(this.$el.elements);
                 const firstInputElement = elementsArray.find(element => element.classList.contains('q-field__native'));
diff --git a/src/components/ui/VnFilterPanel.vue b/src/components/ui/VnFilterPanel.vue
index 722462d4a..96d097191 100644
--- a/src/components/ui/VnFilterPanel.vue
+++ b/src/components/ui/VnFilterPanel.vue
@@ -164,7 +164,7 @@ function formatValue(value) {
 </script>
 
 <template>
-    <QForm @submit="search">
+    <QForm @submit="search" id="filterPanelForm">
         <QList dense>
             <QItem class="q-mt-xs">
                 <QItemSection top>

From 8a82b7ab38a27925842c69dd771beb7bf40a0007 Mon Sep 17 00:00:00 2001
From: Javier Segarra <jsegarra@verdnatura.es>
Date: Fri, 22 Mar 2024 09:55:16 +0100
Subject: [PATCH 6/7] refs #7124 perf: avoid focus in disabled fields

---
 src/boot/qformMixin.js | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/boot/qformMixin.js b/src/boot/qformMixin.js
index 66e3de4f5..8c89c9202 100644
--- a/src/boot/qformMixin.js
+++ b/src/boot/qformMixin.js
@@ -1,5 +1,9 @@
 import { getCurrentInstance } from 'vue';
 
+const filterAvailableInput = element => element.classList.contains('q-field__native') && !element.disabled
+const filterAvailableText = element => element.__vueParentComponent.type.name === 'QInput' && element.__vueParentComponent?.attrs?.class !== 'vn-input-date';
+
+
 export default {
     mounted: function () {
         const vm = getCurrentInstance();
@@ -7,7 +11,7 @@ export default {
             if (!['searchbarForm','filterPanelForm'].includes(this.$el?.id)) {
                 // AUTOFOCUS
                 const elementsArray = Array.from(this.$el.elements);
-                const firstInputElement = elementsArray.find(element => element.classList.contains('q-field__native'));
+                const firstInputElement = elementsArray.filter(filterAvailableInput).find(filterAvailableText);
 
                 if (firstInputElement) {
                     firstInputElement.focus();

From 2bb95fa56816ac4dd603908ab73ee0dd3d983806 Mon Sep 17 00:00:00 2001
From: jorgep <jorgep@verdnatura.es>
Date: Fri, 22 Mar 2024 16:31:10 +0100
Subject: [PATCH 7/7] rafactor: refs #6951 actions descriptor & update
 changelog

---
 CHANGELOG.md                                  |  2 +
 .../Ticket/Card/TicketDescriptorMenu.vue      | 73 +++++++++----------
 2 files changed, 38 insertions(+), 37 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index dbf6bdcc3..51dd2010c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 ### Added
 
+-   (Tickets) => Se añade la opción de clonar ticket. #6951
+
 ### Changed
 
 ### Fixed
diff --git a/src/pages/Ticket/Card/TicketDescriptorMenu.vue b/src/pages/Ticket/Card/TicketDescriptorMenu.vue
index 61167d722..c7784bc2a 100644
--- a/src/pages/Ticket/Card/TicketDescriptorMenu.vue
+++ b/src/pages/Ticket/Card/TicketDescriptorMenu.vue
@@ -24,7 +24,42 @@ const { openReport, sendEmail } = usePrintService();
 
 const ticket = ref(props.ticket);
 const ticketId = currentRoute.value.params.id;
-const actions = { remove: remove, clone: clone };
+const actions = {
+    clone: async () => {
+        const opts = { message: t('Ticket cloned'), type: 'positive' };
+        let clonedTicketId;
+
+        try {
+            const { data } = await axios.post(`Tickets/${ticketId}/clone`, {
+                shipped: ticket.value.shipped,
+            });
+            clonedTicketId = data;
+        } catch (e) {
+            opts.message = t('It was not able to clone the ticket');
+            opts.type = 'negative';
+        } finally {
+            notify(opts);
+
+            if (clonedTicketId)
+                push({ name: 'TicketSummary', params: { id: clonedTicketId } });
+        }
+    },
+    remove: async () => {
+        try {
+            await axios.post(`Tickets/${ticketId}/setDeleted`);
+
+            notify({ message: t('Ticket deleted'), type: 'positive' });
+            notify({
+                message: t('You can undo this action within the first hour'),
+                icon: 'info',
+            });
+
+            push({ name: 'TicketList' });
+        } catch (e) {
+            notify({ message: e.message, type: 'negative' });
+        }
+    },
+};
 
 function openDeliveryNote(type = 'deliveryNote', documentType = 'pdf') {
     const path = `Tickets/${ticket.value.id}/delivery-note-${documentType}`;
@@ -118,42 +153,6 @@ function openConfirmDialog(callback) {
         },
     });
 }
-
-async function clone() {
-    const opts = { message: t('Ticket cloned'), type: 'positive' };
-    let clonedTicketId;
-
-    try {
-        const { data } = await axios.post(`Tickets/${ticketId}/clone`, {
-            shipped: ticket.value.shipped,
-        });
-        clonedTicketId = data;
-    } catch (e) {
-        opts.message = t('It was not able to clone the ticket');
-        opts.type = 'negative';
-    } finally {
-        notify(opts);
-
-        if (clonedTicketId)
-            push({ name: 'TicketSummary', params: { id: clonedTicketId } });
-    }
-}
-
-async function remove() {
-    try {
-        await axios.post(`Tickets/${ticketId}/setDeleted`);
-
-        notify({ message: t('Ticket deleted'), type: 'positive' });
-        notify({
-            message: t('You can undo this action within the first hour'),
-            icon: 'info',
-        });
-
-        push({ name: 'TicketList' });
-    } catch (e) {
-        notify({ message: e.message, type: 'negative' });
-    }
-}
 </script>
 <template>
     <QItem v-ripple clickable>