diff --git a/e2e/helpers/selectors.js b/e2e/helpers/selectors.js index 0f9618b95..e586f38ed 100644 --- a/e2e/helpers/selectors.js +++ b/e2e/helpers/selectors.js @@ -389,6 +389,7 @@ export default { moreMenuDeleteStowawayButton: '.vn-menu [name="deleteStowaway"]', moreMenuAddToTurn: '.vn-menu [name="addTurn"]', moreMenuDeleteTicket: '.vn-menu [name="deleteTicket"]', + moreMenuRestoreTicket: '.vn-menu [name="restoreTicket"]', moreMenuMakeInvoice: '.vn-menu [name="makeInvoice"]', moreMenuChangeShippedHour: '.vn-menu [name="changeShipped"]', changeShippedHourDialog: '.vn-dialog.shown', @@ -397,7 +398,7 @@ export default { shipButton: 'vn-ticket-descriptor vn-icon[icon="icon-stowaway"]', thursdayButton: '.vn-popup.shown vn-tool-bar > vn-button:nth-child(4)', saturdayButton: '.vn-popup.shown vn-tool-bar > vn-button:nth-child(6)', - acceptDeleteButton: '.vn-dialog.shown button[response="accept"]', + acceptDialog: '.vn-dialog.shown button[response="accept"]', acceptChangeHourButton: '.vn-dialog.shown button[response="accept"]', descriptorDeliveryDate: 'vn-ticket-descriptor slot-body > .attributes > vn-label-value:nth-child(3) > section > span', acceptInvoiceOutButton: '.vn-confirm.shown button[response="accept"]', diff --git a/e2e/paths/05-ticket/12_descriptor.spec.js b/e2e/paths/05-ticket/12_descriptor.spec.js index 9d67365a8..dab9a7558 100644 --- a/e2e/paths/05-ticket/12_descriptor.spec.js +++ b/e2e/paths/05-ticket/12_descriptor.spec.js @@ -41,26 +41,42 @@ describe('Ticket descriptor path', () => { it('should delete the ticket using the descriptor more menu', async() => { await page.waitToClick(selectors.ticketDescriptor.moreMenu); await page.waitToClick(selectors.ticketDescriptor.moreMenuDeleteTicket); - await page.waitToClick(selectors.ticketDescriptor.acceptDeleteButton); + await page.waitToClick(selectors.ticketDescriptor.acceptDialog); const message = await page.waitForSnackbar(); - expect(message.text).toBe('Ticket deleted'); + expect(message.text).toBe('Ticket deleted. You can undo this action within the first hour'); }); it('should have been relocated to the ticket index', async() => { await page.waitForState('ticket.index'); }); - it(`should search for the deleted ticket and check it's date`, async() => { + it(`should search for the deleted ticket and check the deletedTicket icon and it's date`, async() => { await page.write(selectors.ticketsIndex.topbarSearch, '18'); await page.waitToClick(selectors.globalItems.searchButton); await page.waitForState('ticket.card.summary'); + await page.waitForClassPresent(selectors.ticketDescriptor.isDeletedIcon, 'bright'); const result = await page.waitToGetProperty(selectors.ticketsIndex.searchResultDate, 'innerText'); expect(result).toContain(2000); }); }); + describe('Restore ticket', () => { + it('should restore the ticket using the descriptor more menu', async() => { + await page.waitToClick(selectors.ticketDescriptor.moreMenu); + await page.waitToClick(selectors.ticketDescriptor.moreMenuRestoreTicket); + await page.waitToClick(selectors.ticketDescriptor.acceptDialog); + const message = await page.waitForSnackbar(); + + expect(message.text).toBe('Data saved!'); + }); + + it('should make sure the ticketDeleted icon is no longer bright', async() => { + await page.waitForClassNotPresent(selectors.ticketDescriptor.isDeletedIcon, 'bright'); + }); + }); + describe('add stowaway', () => { it('should search for a ticket', async() => { await page.accessToSearchResult('16'); diff --git a/e2e/paths/05-ticket/14_create_ticket.spec.js b/e2e/paths/05-ticket/14_create_ticket.spec.js index 496cac161..da97d7584 100644 --- a/e2e/paths/05-ticket/14_create_ticket.spec.js +++ b/e2e/paths/05-ticket/14_create_ticket.spec.js @@ -74,7 +74,7 @@ describe('Ticket create path', () => { it('should delete the current ticket', async() => { await page.waitToClick(selectors.ticketDescriptor.moreMenu); await page.waitToClick(selectors.ticketDescriptor.moreMenuDeleteTicket); - await page.waitToClick(selectors.ticketDescriptor.acceptDeleteButton); + await page.waitToClick(selectors.ticketDescriptor.acceptDialog); const message = await page.waitForSnackbar(); expect(message.type).toBe('success'); diff --git a/loopback/locale/en.json b/loopback/locale/en.json index 4a970190d..04cc887b6 100644 --- a/loopback/locale/en.json +++ b/loopback/locale/en.json @@ -56,7 +56,6 @@ "Value has an invalid format": "Value has an invalid format", "The postcode doesn't exist. Please enter a correct one": "The postcode doesn't exist. Please enter a correct one", "Can't create stowaway for this ticket": "Can't create stowaway for this ticket", - "Has deleted the ticket id": "Has deleted the ticket id [#{{id}}]({{{url}}})", "Swift / BIC can't be empty": "Swift / BIC can't be empty", "MESSAGE_BOUGHT_UNITS": "Bought {{quantity}} units of {{concept}} (#{{itemId}}) for the ticket id [#{{ticketId}}]({{{url}}})", "MESSAGE_INSURANCE_CHANGE": "I have changed the insurence credit of client [{{clientName}} (#{{clientId}})]({{{url}}}) to *{{credit}} €*", @@ -68,7 +67,9 @@ "Incoterms is required for a non UEE member": "Incoterms is required for a non UEE member", "Client checked as validated despite of duplication": "Client checked as validated despite of duplication from client id {{clientId}}", "Landing cannot be lesser than shipment": "Landing cannot be lesser than shipment", - "NOT_ZONE_WITH_THIS_PARAMETERS": "NOT_ZONE_WITH_THIS_PARAMETERS", + "NOT_ZONE_WITH_THIS_PARAMETERS": "There's no zone available for this day", "Created absence": "The worker {{author}} has added an absence of type '{{absenceType}}' to {{employee}} for day {{dated}}.", - "Deleted absence": "The worker {{author}} has deleted an absence of type '{{absenceType}}' to {{employee}} for day {{dated}}." + "Deleted absence": "The worker {{author}} has deleted an absence of type '{{absenceType}}' to {{employee}} for day {{dated}}.", + "I have deleted the ticket id": "I have deleted the ticket id [#{{id}}]({{{url}}})", + "I have restored the ticket id": "I have restored the ticket id [#{{id}}]({{{url}}})" } \ No newline at end of file diff --git a/loopback/locale/es.json b/loopback/locale/es.json index 1f536c3ce..db17262b4 100644 --- a/loopback/locale/es.json +++ b/loopback/locale/es.json @@ -114,7 +114,6 @@ "You can't create a claim for a removed ticket": "No puedes crear una reclamación para un ticket eliminado", "You cannot delete a ticket that part of it is being prepared": "No puedes eliminar un ticket en el que una parte que está siendo preparada", "You must delete all the buy requests first": "Debes eliminar todas las peticiones de compra primero", - "Has deleted the ticket id": "Ha eliminado el ticket id [#{{id}}]({{{url}}})", "You should specify a date": "Debes especificar una fecha", "You should specify at least a start or end date": "Debes especificar al menos una fecha de inicio o de fín", "Start date should be lower than end date": "La fecha de inicio debe ser menor que la fecha de fín", @@ -146,5 +145,8 @@ "User already exists": "User already exists", "Absence change notification on the labour calendar": "Notificacion de cambio de ausencia en el calendario laboral", "Created absence": "El empleado {{author}} ha añadido una ausencia de tipo '{{absenceType}}' a {{employee}} para el día {{dated}}.", - "Deleted absence": "El empleado {{author}} ha eliminado una ausencia de tipo '{{absenceType}}' a {{employee}} del día {{dated}}." + "Deleted absence": "El empleado {{author}} ha eliminado una ausencia de tipo '{{absenceType}}' a {{employee}} del día {{dated}}.", + "I have deleted the ticket id": "He eliminado el ticket id [#{{id}}]({{{url}}})", + "I have restored the ticket id": "He restaurado el ticket id [#{{id}}]({{{url}}})", + "You can only restore a ticket within the first hour after deletion": "Únicamente puedes restaurar el ticket dentro de la primera hora después de su eliminación" } \ No newline at end of file diff --git a/modules/item/back/methods/item/getWasteDetail.js b/modules/item/back/methods/item/getWasteDetail.js index 906269044..b8b55b275 100644 --- a/modules/item/back/methods/item/getWasteDetail.js +++ b/modules/item/back/methods/item/getWasteDetail.js @@ -1,6 +1,6 @@ module.exports = Self => { Self.remoteMethod('getWasteDetail', { - description: 'Returns the ', + description: 'Returns the details of losses by worker', accessType: 'READ', accepts: [], returns: { diff --git a/modules/ticket/back/methods/ticket/restore.js b/modules/ticket/back/methods/ticket/restore.js new file mode 100644 index 000000000..19429f61b --- /dev/null +++ b/modules/ticket/back/methods/ticket/restore.js @@ -0,0 +1,56 @@ +const UserError = require('vn-loopback/util/user-error'); + +module.exports = Self => { + Self.remoteMethodCtx('restore', { + description: 'Restores a ticket within the first hour of deletion', + accessType: 'WRITE', + accepts: [{ + arg: 'id', + type: 'Number', + required: true, + description: 'The ticket id', + http: {source: 'path'} + }], + returns: { + type: 'string', + root: true + }, + http: { + path: `/:id/restore`, + verb: 'post' + } + }); + + Self.restore = async(ctx, id) => { + const models = Self.app.models; + const $t = ctx.req.__; // $translate + const ticket = await models.Ticket.findById(id, { + include: [{ + relation: 'client', + scope: { + fields: ['id', 'salesPersonFk'] + } + }] + }); + + const now = new Date(); + const maxDate = new Date(ticket.updated); + maxDate.setHours(maxDate.getHours() + 1); + + if (now > maxDate) + throw new UserError(`You can only restore a ticket within the first hour after deletion`); + + // Send notification to salesPerson + const salesPersonId = ticket.client().salesPersonFk; + if (salesPersonId) { + const origin = ctx.req.headers.origin; + const message = $t(`I have restored the ticket id`, { + id: id, + url: `${origin}/#!/ticket/${id}/summary` + }); + await models.Chat.sendCheckingPresence(ctx, salesPersonId, message); + } + + return ticket.updateAttribute('isDeleted', false); + }; +}; diff --git a/modules/ticket/back/methods/ticket/setDeleted.js b/modules/ticket/back/methods/ticket/setDeleted.js index 208333aad..9e8d202fa 100644 --- a/modules/ticket/back/methods/ticket/setDeleted.js +++ b/modules/ticket/back/methods/ticket/setDeleted.js @@ -102,7 +102,7 @@ module.exports = Self => { }] }); - // Change state to "fixing" if contains an stowaway and removed the link between them + // Change state to "fixing" if contains an stowaway and remove the link between them let otherTicketId; if (ticket.stowaway()) otherTicketId = ticket.stowaway().shipFk; @@ -121,7 +121,7 @@ module.exports = Self => { const salesPersonUser = ticket.client().salesPersonUser(); if (salesPersonUser) { const origin = ctx.req.headers.origin; - const message = $t(`Has deleted the ticket id`, { + const message = $t(`I have deleted the ticket id`, { id: id, url: `${origin}/#!/ticket/${id}/summary` }); diff --git a/modules/ticket/back/methods/ticket/specs/restore.spec.js b/modules/ticket/back/methods/ticket/specs/restore.spec.js new file mode 100644 index 000000000..1abb52dc5 --- /dev/null +++ b/modules/ticket/back/methods/ticket/specs/restore.spec.js @@ -0,0 +1,71 @@ +const app = require('vn-loopback/server/server'); +const models = app.models; + +describe('ticket restore()', () => { + const employeeUser = 110; + const ctx = { + req: { + accessToken: {userId: employeeUser}, + headers: { + origin: 'http://localhost:5000' + }, + __: () => {} + } + }; + let createdTicket; + + beforeEach(async done => { + try { + const sampleTicket = await models.Ticket.findById(11); + sampleTicket.id = undefined; + + createdTicket = await models.Ticket.create(sampleTicket); + } catch (error) { + console.error(error); + } + + done(); + }); + + afterEach(async done => { + try { + await models.Ticket.destroyById(createdTicket.id); + } catch (error) { + console.error(error); + } + + done(); + }); + + it('should throw an error if the given ticket has past the deletion time', async() => { + let error; + + const now = new Date(); + now.setHours(now.getHours() - 1); + + try { + const ticket = await models.Ticket.findById(createdTicket.id); + await ticket.updateAttributes({isDeleted: true, updated: now}); + await app.models.Ticket.restore(ctx, createdTicket.id); + } catch (e) { + error = e; + } + + expect(error.message).toContain('You can only restore a ticket within the first hour after deletion'); + }); + + it('should restore the ticket making its state no longer deleted', async() => { + const now = new Date(); + const ticketBeforeUpdate = await models.Ticket.findById(createdTicket.id); + await ticketBeforeUpdate.updateAttributes({isDeleted: true, updated: now}); + + const ticketAfterUpdate = await models.Ticket.findById(createdTicket.id); + + expect(ticketAfterUpdate.isDeleted).toBeTruthy(); + + await models.Ticket.restore(ctx, createdTicket.id); + const ticketAfterRestore = await models.Ticket.findById(createdTicket.id); + + expect(ticketAfterRestore.isDeleted).toBeFalsy(); + }); +}); diff --git a/modules/ticket/back/models/ticket.js b/modules/ticket/back/models/ticket.js index 1150b7367..fcdca2f13 100644 --- a/modules/ticket/back/models/ticket.js +++ b/modules/ticket/back/models/ticket.js @@ -12,6 +12,7 @@ module.exports = Self => { require('../methods/ticket/new')(Self); require('../methods/ticket/isEditable')(Self); require('../methods/ticket/setDeleted')(Self); + require('../methods/ticket/restore')(Self); require('../methods/ticket/getVAT')(Self); require('../methods/ticket/getSales')(Self); require('../methods/ticket/getSalesPersonMana')(Self); diff --git a/modules/ticket/back/models/ticket.json b/modules/ticket/back/models/ticket.json index df92d6861..ca4668b3a 100644 --- a/modules/ticket/back/models/ticket.json +++ b/modules/ticket/back/models/ticket.json @@ -34,8 +34,11 @@ "packages": { "type": "Number" }, - "created": { - "type": "Date" + "updated": { + "type": "Date", + "mysql": { + "columnName": "created" + } }, "isDeleted": { "type": "boolean" diff --git a/modules/ticket/front/descriptor/index.html b/modules/ticket/front/descriptor/index.html index c24b4c843..08f5c48a3 100644 --- a/modules/ticket/front/descriptor/index.html +++ b/modules/ticket/front/descriptor/index.html @@ -28,6 +28,13 @@ translate> Delete ticket + + Restore ticket + + + { @@ -86,7 +95,15 @@ class Controller extends Descriptor { return this.$http.post(`Tickets/${this.id}/setDeleted`) .then(() => { this.$state.go('ticket.index'); - this.vnApp.showSuccess(this.$t('Ticket deleted')); + this.vnApp.showSuccess(this.$t('Ticket deleted. You can undo this action within the first hour')); + }); + } + + restoreTicket() { + return this.$http.post(`Tickets/${this.id}/restore`) + .then(() => { + this.vnApp.showSuccess(this.$t('Data saved!')); + this.cardReload(); }); } @@ -124,7 +141,7 @@ class Controller extends Descriptor { sendImportSms() { const params = { ticketId: this.id, - created: this.ticket.created + created: this.ticket.updated }; this.showSMSDialog({ message: this.$params.message || this.$t('Minimum is needed', params) diff --git a/modules/ticket/front/descriptor/index.spec.js b/modules/ticket/front/descriptor/index.spec.js index a725a2d4a..5910b3cf1 100644 --- a/modules/ticket/front/descriptor/index.spec.js +++ b/modules/ticket/front/descriptor/index.spec.js @@ -37,6 +37,29 @@ describe('Ticket Component vnTicketDescriptor', () => { controller = $componentController('vnTicketDescriptor', {$element: null}, {ticket}); })); + describe('canRestoreTicket() getter', () => { + it('should return true for a ticket deleted within the last hour', () => { + controller.ticket.isDeleted = true; + controller.ticket.updated = new Date(); + + const result = controller.canRestoreTicket; + + expect(result).toBeTruthy(); + }); + + it('should return false for a ticket deleted more than one hour ago', () => { + const pastHour = new Date(); + pastHour.setHours(pastHour.getHours() - 2); + + controller.ticket.isDeleted = true; + controller.ticket.updated = pastHour; + + const result = controller.canRestoreTicket; + + expect(result).toBeFalsy(); + }); + }); + describe('addTurn()', () => { it('should make a query and call $.addTurn.hide() and vnApp.showSuccess()', () => { controller.$.addTurn = {hide: () => {}}; @@ -64,6 +87,20 @@ describe('Ticket Component vnTicketDescriptor', () => { }); }); + describe('restoreTicket()', () => { + it('should make a query to restore the ticket and call vnApp.showSuccess()', () => { + jest.spyOn(controller, 'cardReload'); + jest.spyOn(controller.vnApp, 'showSuccess'); + + $httpBackend.expectPOST(`Tickets/${ticket.id}/restore`).respond(); + controller.restoreTicket(); + $httpBackend.flush(); + + expect(controller.cardReload).toHaveBeenCalled(); + expect(controller.vnApp.showSuccess).toHaveBeenCalled(); + }); + }); + describe('showDeliveryNote()', () => { it('should open a new window showing a delivery note PDF document', () => { jest.spyOn(controller.vnReport, 'show'); diff --git a/modules/ticket/front/descriptor/locale/es.yml b/modules/ticket/front/descriptor/locale/es.yml index 0f98745bb..6524df353 100644 --- a/modules/ticket/front/descriptor/locale/es.yml +++ b/modules/ticket/front/descriptor/locale/es.yml @@ -24,8 +24,11 @@ You are going to regenerate the invoice: Vas a regenerar la factura Are you sure you want to regenerate the invoice?: ¿Seguro que quieres regenerar la factura? Invoice sent for a regeneration, will be available in a few minutes: La factura ha sido enviada para ser regenerada, estará disponible en unos minutos Shipped hour updated: Hora de envio modificada -Deleted ticket: Ticket eliminado +Deleted ticket: Ticket eliminado Recalculate components: Recalcular componentes Are you sure you want to recalculate the components?: ¿Seguro que quieres recalcular los componentes? SMS Minimum import: 'SMS Importe minimo' -SMS Pending payment: 'SMS Pago pendiente' \ No newline at end of file +SMS Pending payment: 'SMS Pago pendiente' +Restore ticket: Restaurar ticket +You are going to restore this ticket: Vas a restaurar este ticket +Are you sure you want to restore this ticket?: ¿Seguro que quieres restaurar el ticket? \ No newline at end of file diff --git a/modules/ticket/front/locale/es.yml b/modules/ticket/front/locale/es.yml index 494725bb7..5a84c331a 100644 --- a/modules/ticket/front/locale/es.yml +++ b/modules/ticket/front/locale/es.yml @@ -59,7 +59,7 @@ Freezed: Congelado Risk: Riesgo Invoice: Factura You are going to delete this ticket: Vas a eliminar este ticket -Ticket deleted: Ticket borrado +Ticket deleted. You can undo this action within the first hour: Ticket eliminado. Puedes deshacer esta acción durante la primera hora Search ticket by id or alias: Buscar tickets por identificador o alias ticket: ticket diff --git a/modules/ticket/front/sale/index.js b/modules/ticket/front/sale/index.js index e022e2351..384fd9b5f 100644 --- a/modules/ticket/front/sale/index.js +++ b/modules/ticket/front/sale/index.js @@ -363,7 +363,7 @@ class Controller extends Section { const notAvailables = items.join(', '); const params = { ticketFk: this.ticket.id, - created: this.ticket.created, + created: this.ticket.updated, notAvailables }; this.newSMS = {