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/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 = {