This commit is contained in:
Carlos Jimenez Ruiz 2019-07-02 14:44:39 +02:00
commit 4b0a71644f
46 changed files with 601 additions and 200 deletions

View File

@ -1,2 +1,4 @@
INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`) INSERT INTO `salix`.`ACL` (`model`, `property`, `accessType`, `permission`, `principalType`, `principalId`)
VALUES ('Zone', 'editPrices', 'WRITE', 'ALLOW', 'ROLE', 'deliveryBoss'); VALUES
('Zone', 'editPrices', 'WRITE', 'ALLOW', 'ROLE', 'deliveryBoss'),
('Ticket', 'addSale', 'WRITE', 'ALLOW', 'ROLE', 'employee');

View File

@ -398,18 +398,18 @@ export default {
firstSaleText: 'vn-table div > vn-tbody > vn-tr:nth-child(1)', firstSaleText: 'vn-table div > vn-tbody > vn-tr:nth-child(1)',
firstSaleThumbnailImage: 'vn-ticket-sale:nth-child(1) vn-tr:nth-child(1) vn-td:nth-child(3) > img', firstSaleThumbnailImage: 'vn-ticket-sale:nth-child(1) vn-tr:nth-child(1) vn-td:nth-child(3) > img',
firstSaleZoomedImage: 'body > div > div > img', firstSaleZoomedImage: 'body > div > div > img',
firstSaleQuantity: 'vn-textfield[model="sale.quantity"]:nth-child(1) input', firstSaleQuantity: 'vn-input-number[model="sale.quantity"]:nth-child(1) input',
firstSaleQuantityCell: 'vn-ticket-sale vn-tr:nth-child(1) > vn-td-editable', firstSaleQuantityCell: 'vn-ticket-sale vn-tr:nth-child(1) > vn-td-editable:nth-child(5)',
firstSaleQuantityClearInput: 'vn-textfield[model="sale.quantity"] div.suffix > i', firstSaleQuantityClearInput: 'vn-textfield[model="sale.quantity"] div.suffix > i',
firstSaleID: 'vn-ticket-sale:nth-child(1) vn-td:nth-child(4) > span', firstSaleID: 'vn-ticket-sale:nth-child(1) vn-td-editable:nth-child(4) text > span',
firstSalePrice: 'vn-ticket-sale:nth-child(1) vn-tr:nth-child(1) > vn-td:nth-child(7)', firstSalePrice: 'vn-ticket-sale:nth-child(1) vn-tr:nth-child(1) > vn-td:nth-child(7)',
firstSalePriceInput: 'vn-ticket-sale:nth-child(1) vn-popover.edit.dialog-summary.ng-isolate-scope.vn-popover.shown vn-input-number input', firstSalePriceInput: 'vn-ticket-sale:nth-child(1) vn-popover.edit.dialog-summary.ng-isolate-scope.vn-popover.shown vn-input-number input',
firstSaleDiscount: 'vn-ticket-sale:nth-child(1) vn-td:nth-child(8)', firstSaleDiscount: 'vn-ticket-sale:nth-child(1) vn-td:nth-child(8)',
firstSaleDiscountInput: 'vn-ticket-sale:nth-child(1) vn-ticket-sale-edit-discount vn-input-number input', firstSaleDiscountInput: 'vn-ticket-sale:nth-child(1) vn-ticket-sale-edit-discount vn-input-number input',
firstSaleImport: 'vn-ticket-sale:nth-child(1) vn-td:nth-child(9)', firstSaleImport: 'vn-ticket-sale:nth-child(1) vn-td:nth-child(9)',
firstSaleReservedIcon: 'vn-ticket-sale vn-tr:nth-child(1) > vn-td:nth-child(2) > vn-icon:nth-child(3)', firstSaleReservedIcon: 'vn-ticket-sale vn-tr:nth-child(1) > vn-td:nth-child(2) > vn-icon:nth-child(3)',
firstSaleColour: 'vn-ticket-sale vn-tr:nth-child(1) vn-td:nth-child(6) section:nth-child(1)', firstSaleColour: 'vn-ticket-sale vn-tr:nth-child(1) vn-td-editable:nth-child(6) section:nth-child(1)',
firstSaleLength: 'vn-ticket-sale vn-tr:nth-child(1) vn-td:nth-child(6) section:nth-child(3)', firstSaleLength: 'vn-ticket-sale vn-tr:nth-child(1) vn-td-editable:nth-child(6) section:nth-child(3)',
firstSaleCheckbox: 'vn-ticket-sale vn-tr:nth-child(1) vn-check[field="sale.checked"] md-checkbox', firstSaleCheckbox: 'vn-ticket-sale vn-tr:nth-child(1) vn-check[field="sale.checked"] md-checkbox',
secondSaleColour: 'vn-ticket-sale vn-tr:nth-child(2) vn-td:nth-child(6) section:nth-child(5)', secondSaleColour: 'vn-ticket-sale vn-tr:nth-child(2) vn-td:nth-child(6) section:nth-child(5)',
secondSalePrice: 'vn-ticket-sale vn-tr:nth-child(2) vn-td:nth-child(7) > span', secondSalePrice: 'vn-ticket-sale vn-tr:nth-child(2) vn-td:nth-child(7) > span',

View File

@ -17,6 +17,7 @@ describe('Worker time control path', () => {
const scanTime = '07:00'; const scanTime = '07:00';
const result = await nightmare const result = await nightmare
.waitToClick(selectors.workerTimeControl.mondayAddTimeButton) .waitToClick(selectors.workerTimeControl.mondayAddTimeButton)
.clearInput(selectors.workerTimeControl.timeDialogInput)
.write(selectors.workerTimeControl.timeDialogInput, scanTime) .write(selectors.workerTimeControl.timeDialogInput, scanTime)
.waitToClick(selectors.workerTimeControl.confirmButton) .waitToClick(selectors.workerTimeControl.confirmButton)
.waitToGetProperty(selectors.workerTimeControl.firstEntryOfMonday, 'innerText'); .waitToGetProperty(selectors.workerTimeControl.firstEntryOfMonday, 'innerText');
@ -28,6 +29,7 @@ describe('Worker time control path', () => {
const scanTime = '10:00'; const scanTime = '10:00';
const result = await nightmare const result = await nightmare
.waitToClick(selectors.workerTimeControl.mondayAddTimeButton) .waitToClick(selectors.workerTimeControl.mondayAddTimeButton)
.clearInput(selectors.workerTimeControl.timeDialogInput)
.write(selectors.workerTimeControl.timeDialogInput, scanTime) .write(selectors.workerTimeControl.timeDialogInput, scanTime)
.waitToClick(selectors.workerTimeControl.confirmButton) .waitToClick(selectors.workerTimeControl.confirmButton)
.waitToGetProperty(selectors.workerTimeControl.secondEntryOfMonday, 'innerText'); .waitToGetProperty(selectors.workerTimeControl.secondEntryOfMonday, 'innerText');
@ -39,6 +41,7 @@ describe('Worker time control path', () => {
const scanTime = '15:00'; const scanTime = '15:00';
const result = await nightmare const result = await nightmare
.waitToClick(selectors.workerTimeControl.mondayAddTimeButton) .waitToClick(selectors.workerTimeControl.mondayAddTimeButton)
.clearInput(selectors.workerTimeControl.timeDialogInput)
.write(selectors.workerTimeControl.timeDialogInput, scanTime) .write(selectors.workerTimeControl.timeDialogInput, scanTime)
.waitToClick(selectors.workerTimeControl.confirmButton) .waitToClick(selectors.workerTimeControl.confirmButton)
.waitToGetProperty(selectors.workerTimeControl.thirdEntryOfMonday, 'innerText'); .waitToGetProperty(selectors.workerTimeControl.thirdEntryOfMonday, 'innerText');
@ -50,6 +53,7 @@ describe('Worker time control path', () => {
const scanTime = '10:20'; const scanTime = '10:20';
const result = await nightmare const result = await nightmare
.waitToClick(selectors.workerTimeControl.mondayAddTimeButton) .waitToClick(selectors.workerTimeControl.mondayAddTimeButton)
.clearInput(selectors.workerTimeControl.timeDialogInput)
.write(selectors.workerTimeControl.timeDialogInput, scanTime) .write(selectors.workerTimeControl.timeDialogInput, scanTime)
.waitToClick(selectors.workerTimeControl.confirmButton) .waitToClick(selectors.workerTimeControl.confirmButton)
.waitToGetProperty(selectors.workerTimeControl.fourthEntryOfMonday, 'innerText'); .waitToGetProperty(selectors.workerTimeControl.fourthEntryOfMonday, 'innerText');
@ -78,6 +82,7 @@ describe('Worker time control path', () => {
const scanTime = '08:00'; const scanTime = '08:00';
const result = await nightmare const result = await nightmare
.waitToClick(selectors.workerTimeControl.tuesdayAddTimeButton) .waitToClick(selectors.workerTimeControl.tuesdayAddTimeButton)
.clearInput(selectors.workerTimeControl.timeDialogInput)
.write(selectors.workerTimeControl.timeDialogInput, scanTime) .write(selectors.workerTimeControl.timeDialogInput, scanTime)
.waitToClick(selectors.workerTimeControl.confirmButton) .waitToClick(selectors.workerTimeControl.confirmButton)
.waitToGetProperty(selectors.workerTimeControl.firstEntryOfTuesday, 'innerText'); .waitToGetProperty(selectors.workerTimeControl.firstEntryOfTuesday, 'innerText');
@ -89,6 +94,7 @@ describe('Worker time control path', () => {
const scanTime = '10:00'; const scanTime = '10:00';
const result = await nightmare const result = await nightmare
.waitToClick(selectors.workerTimeControl.tuesdayAddTimeButton) .waitToClick(selectors.workerTimeControl.tuesdayAddTimeButton)
.clearInput(selectors.workerTimeControl.timeDialogInput)
.write(selectors.workerTimeControl.timeDialogInput, scanTime) .write(selectors.workerTimeControl.timeDialogInput, scanTime)
.waitToClick(selectors.workerTimeControl.confirmButton) .waitToClick(selectors.workerTimeControl.confirmButton)
.waitToGetProperty(selectors.workerTimeControl.secondEntryOfTuesday, 'innerText'); .waitToGetProperty(selectors.workerTimeControl.secondEntryOfTuesday, 'innerText');
@ -100,6 +106,7 @@ describe('Worker time control path', () => {
const scanTime = '10:20'; const scanTime = '10:20';
const result = await nightmare const result = await nightmare
.waitToClick(selectors.workerTimeControl.tuesdayAddTimeButton) .waitToClick(selectors.workerTimeControl.tuesdayAddTimeButton)
.clearInput(selectors.workerTimeControl.timeDialogInput)
.write(selectors.workerTimeControl.timeDialogInput, scanTime) .write(selectors.workerTimeControl.timeDialogInput, scanTime)
.waitToClick(selectors.workerTimeControl.confirmButton) .waitToClick(selectors.workerTimeControl.confirmButton)
.waitToGetProperty(selectors.workerTimeControl.thirdEntryOfTuesday, 'innerText'); .waitToGetProperty(selectors.workerTimeControl.thirdEntryOfTuesday, 'innerText');
@ -111,6 +118,7 @@ describe('Worker time control path', () => {
const scanTime = '16:00'; const scanTime = '16:00';
const result = await nightmare const result = await nightmare
.waitToClick(selectors.workerTimeControl.tuesdayAddTimeButton) .waitToClick(selectors.workerTimeControl.tuesdayAddTimeButton)
.clearInput(selectors.workerTimeControl.timeDialogInput)
.write(selectors.workerTimeControl.timeDialogInput, scanTime) .write(selectors.workerTimeControl.timeDialogInput, scanTime)
.waitToClick(selectors.workerTimeControl.confirmButton) .waitToClick(selectors.workerTimeControl.confirmButton)
.waitToGetProperty(selectors.workerTimeControl.fourthEntryOfTuesday, 'innerText'); .waitToGetProperty(selectors.workerTimeControl.fourthEntryOfTuesday, 'innerText');
@ -131,6 +139,7 @@ describe('Worker time control path', () => {
const scanTime = '09:00'; const scanTime = '09:00';
const result = await nightmare const result = await nightmare
.waitToClick(selectors.workerTimeControl.wednesdayAddTimeButton) .waitToClick(selectors.workerTimeControl.wednesdayAddTimeButton)
.clearInput(selectors.workerTimeControl.timeDialogInput)
.write(selectors.workerTimeControl.timeDialogInput, scanTime) .write(selectors.workerTimeControl.timeDialogInput, scanTime)
.waitToClick(selectors.workerTimeControl.confirmButton) .waitToClick(selectors.workerTimeControl.confirmButton)
.waitToGetProperty(selectors.workerTimeControl.firstEntryOfWednesday, 'innerText'); .waitToGetProperty(selectors.workerTimeControl.firstEntryOfWednesday, 'innerText');
@ -142,6 +151,7 @@ describe('Worker time control path', () => {
const scanTime = '10:00'; const scanTime = '10:00';
const result = await nightmare const result = await nightmare
.waitToClick(selectors.workerTimeControl.wednesdayAddTimeButton) .waitToClick(selectors.workerTimeControl.wednesdayAddTimeButton)
.clearInput(selectors.workerTimeControl.timeDialogInput)
.write(selectors.workerTimeControl.timeDialogInput, scanTime) .write(selectors.workerTimeControl.timeDialogInput, scanTime)
.waitToClick(selectors.workerTimeControl.confirmButton) .waitToClick(selectors.workerTimeControl.confirmButton)
.waitToGetProperty(selectors.workerTimeControl.secondEntryOfWednesday, 'innerText'); .waitToGetProperty(selectors.workerTimeControl.secondEntryOfWednesday, 'innerText');
@ -153,6 +163,7 @@ describe('Worker time control path', () => {
const scanTime = '10:20'; const scanTime = '10:20';
const result = await nightmare const result = await nightmare
.waitToClick(selectors.workerTimeControl.wednesdayAddTimeButton) .waitToClick(selectors.workerTimeControl.wednesdayAddTimeButton)
.clearInput(selectors.workerTimeControl.timeDialogInput)
.write(selectors.workerTimeControl.timeDialogInput, scanTime) .write(selectors.workerTimeControl.timeDialogInput, scanTime)
.waitToClick(selectors.workerTimeControl.confirmButton) .waitToClick(selectors.workerTimeControl.confirmButton)
.waitToGetProperty(selectors.workerTimeControl.thirdEntryOfWednesday, 'innerText'); .waitToGetProperty(selectors.workerTimeControl.thirdEntryOfWednesday, 'innerText');
@ -164,6 +175,7 @@ describe('Worker time control path', () => {
const scanTime = '17:00'; const scanTime = '17:00';
const result = await nightmare const result = await nightmare
.waitToClick(selectors.workerTimeControl.wednesdayAddTimeButton) .waitToClick(selectors.workerTimeControl.wednesdayAddTimeButton)
.clearInput(selectors.workerTimeControl.timeDialogInput)
.write(selectors.workerTimeControl.timeDialogInput, scanTime) .write(selectors.workerTimeControl.timeDialogInput, scanTime)
.waitToClick(selectors.workerTimeControl.confirmButton) .waitToClick(selectors.workerTimeControl.confirmButton)
.waitToGetProperty(selectors.workerTimeControl.fourthEntryOfWednesday, 'innerText'); .waitToGetProperty(selectors.workerTimeControl.fourthEntryOfWednesday, 'innerText');
@ -184,6 +196,7 @@ describe('Worker time control path', () => {
const scanTime = '09:59'; const scanTime = '09:59';
const result = await nightmare const result = await nightmare
.waitToClick(selectors.workerTimeControl.thursdayAddTimeButton) .waitToClick(selectors.workerTimeControl.thursdayAddTimeButton)
.clearInput(selectors.workerTimeControl.timeDialogInput)
.write(selectors.workerTimeControl.timeDialogInput, scanTime) .write(selectors.workerTimeControl.timeDialogInput, scanTime)
.waitToClick(selectors.workerTimeControl.confirmButton) .waitToClick(selectors.workerTimeControl.confirmButton)
.waitToGetProperty(selectors.workerTimeControl.firstEntryOfThursday, 'innerText'); .waitToGetProperty(selectors.workerTimeControl.firstEntryOfThursday, 'innerText');
@ -195,6 +208,7 @@ describe('Worker time control path', () => {
const scanTime = '10:00'; const scanTime = '10:00';
const result = await nightmare const result = await nightmare
.waitToClick(selectors.workerTimeControl.thursdayAddTimeButton) .waitToClick(selectors.workerTimeControl.thursdayAddTimeButton)
.clearInput(selectors.workerTimeControl.timeDialogInput)
.write(selectors.workerTimeControl.timeDialogInput, scanTime) .write(selectors.workerTimeControl.timeDialogInput, scanTime)
.waitToClick(selectors.workerTimeControl.confirmButton) .waitToClick(selectors.workerTimeControl.confirmButton)
.waitToGetProperty(selectors.workerTimeControl.secondEntryOfThursday, 'innerText'); .waitToGetProperty(selectors.workerTimeControl.secondEntryOfThursday, 'innerText');
@ -206,6 +220,7 @@ describe('Worker time control path', () => {
const scanTime = '10:20'; const scanTime = '10:20';
const result = await nightmare const result = await nightmare
.waitToClick(selectors.workerTimeControl.thursdayAddTimeButton) .waitToClick(selectors.workerTimeControl.thursdayAddTimeButton)
.clearInput(selectors.workerTimeControl.timeDialogInput)
.write(selectors.workerTimeControl.timeDialogInput, scanTime) .write(selectors.workerTimeControl.timeDialogInput, scanTime)
.waitToClick(selectors.workerTimeControl.confirmButton) .waitToClick(selectors.workerTimeControl.confirmButton)
.waitToGetProperty(selectors.workerTimeControl.thirdEntryOfThursday, 'innerText'); .waitToGetProperty(selectors.workerTimeControl.thirdEntryOfThursday, 'innerText');
@ -217,6 +232,7 @@ describe('Worker time control path', () => {
const scanTime = '17:59'; const scanTime = '17:59';
const result = await nightmare const result = await nightmare
.waitToClick(selectors.workerTimeControl.thursdayAddTimeButton) .waitToClick(selectors.workerTimeControl.thursdayAddTimeButton)
.clearInput(selectors.workerTimeControl.timeDialogInput)
.write(selectors.workerTimeControl.timeDialogInput, scanTime) .write(selectors.workerTimeControl.timeDialogInput, scanTime)
.waitToClick(selectors.workerTimeControl.confirmButton) .waitToClick(selectors.workerTimeControl.confirmButton)
.waitToGetProperty(selectors.workerTimeControl.fourthEntryOfThursday, 'innerText'); .waitToGetProperty(selectors.workerTimeControl.fourthEntryOfThursday, 'innerText');
@ -237,6 +253,7 @@ describe('Worker time control path', () => {
const scanTime = '07:30'; const scanTime = '07:30';
const result = await nightmare const result = await nightmare
.waitToClick(selectors.workerTimeControl.fridayAddTimeButton) .waitToClick(selectors.workerTimeControl.fridayAddTimeButton)
.clearInput(selectors.workerTimeControl.timeDialogInput)
.write(selectors.workerTimeControl.timeDialogInput, scanTime) .write(selectors.workerTimeControl.timeDialogInput, scanTime)
.waitToClick(selectors.workerTimeControl.confirmButton) .waitToClick(selectors.workerTimeControl.confirmButton)
.waitToGetProperty(selectors.workerTimeControl.firstEntryOfFriday, 'innerText'); .waitToGetProperty(selectors.workerTimeControl.firstEntryOfFriday, 'innerText');
@ -248,6 +265,7 @@ describe('Worker time control path', () => {
const scanTime = '10:00'; const scanTime = '10:00';
const result = await nightmare const result = await nightmare
.waitToClick(selectors.workerTimeControl.fridayAddTimeButton) .waitToClick(selectors.workerTimeControl.fridayAddTimeButton)
.clearInput(selectors.workerTimeControl.timeDialogInput)
.write(selectors.workerTimeControl.timeDialogInput, scanTime) .write(selectors.workerTimeControl.timeDialogInput, scanTime)
.waitToClick(selectors.workerTimeControl.confirmButton) .waitToClick(selectors.workerTimeControl.confirmButton)
.waitToGetProperty(selectors.workerTimeControl.secondEntryOfFriday, 'innerText'); .waitToGetProperty(selectors.workerTimeControl.secondEntryOfFriday, 'innerText');
@ -259,6 +277,7 @@ describe('Worker time control path', () => {
const scanTime = '10:20'; const scanTime = '10:20';
const result = await nightmare const result = await nightmare
.waitToClick(selectors.workerTimeControl.fridayAddTimeButton) .waitToClick(selectors.workerTimeControl.fridayAddTimeButton)
.clearInput(selectors.workerTimeControl.timeDialogInput)
.write(selectors.workerTimeControl.timeDialogInput, scanTime) .write(selectors.workerTimeControl.timeDialogInput, scanTime)
.waitToClick(selectors.workerTimeControl.confirmButton) .waitToClick(selectors.workerTimeControl.confirmButton)
.waitToGetProperty(selectors.workerTimeControl.thirdEntryOfFriday, 'innerText'); .waitToGetProperty(selectors.workerTimeControl.thirdEntryOfFriday, 'innerText');
@ -270,6 +289,7 @@ describe('Worker time control path', () => {
const scanTime = '15:30'; const scanTime = '15:30';
const result = await nightmare const result = await nightmare
.waitToClick(selectors.workerTimeControl.fridayAddTimeButton) .waitToClick(selectors.workerTimeControl.fridayAddTimeButton)
.clearInput(selectors.workerTimeControl.timeDialogInput)
.write(selectors.workerTimeControl.timeDialogInput, scanTime) .write(selectors.workerTimeControl.timeDialogInput, scanTime)
.waitToClick(selectors.workerTimeControl.confirmButton) .waitToClick(selectors.workerTimeControl.confirmButton)
.waitToGetProperty(selectors.workerTimeControl.fourthEntryOfFriday, 'innerText'); .waitToGetProperty(selectors.workerTimeControl.fourthEntryOfFriday, 'innerText');
@ -299,6 +319,7 @@ describe('Worker time control path', () => {
const scanTime = '06:00'; const scanTime = '06:00';
const result = await nightmare const result = await nightmare
.waitToClick(selectors.workerTimeControl.saturdayAddTimeButton) .waitToClick(selectors.workerTimeControl.saturdayAddTimeButton)
.clearInput(selectors.workerTimeControl.timeDialogInput)
.write(selectors.workerTimeControl.timeDialogInput, scanTime) .write(selectors.workerTimeControl.timeDialogInput, scanTime)
.waitToClick(selectors.workerTimeControl.confirmButton) .waitToClick(selectors.workerTimeControl.confirmButton)
.waitToGetProperty(selectors.workerTimeControl.firstEntryOfSaturday, 'innerText'); .waitToGetProperty(selectors.workerTimeControl.firstEntryOfSaturday, 'innerText');
@ -310,6 +331,7 @@ describe('Worker time control path', () => {
const scanTime = '13:40'; const scanTime = '13:40';
const result = await nightmare const result = await nightmare
.waitToClick(selectors.workerTimeControl.saturdayAddTimeButton) .waitToClick(selectors.workerTimeControl.saturdayAddTimeButton)
.clearInput(selectors.workerTimeControl.timeDialogInput)
.write(selectors.workerTimeControl.timeDialogInput, scanTime) .write(selectors.workerTimeControl.timeDialogInput, scanTime)
.waitToClick(selectors.workerTimeControl.confirmButton) .waitToClick(selectors.workerTimeControl.confirmButton)
.waitToGetProperty(selectors.workerTimeControl.secondEntryOfSaturday, 'innerText'); .waitToGetProperty(selectors.workerTimeControl.secondEntryOfSaturday, 'innerText');
@ -330,6 +352,7 @@ describe('Worker time control path', () => {
const scanTime = '05:00'; const scanTime = '05:00';
const result = await nightmare const result = await nightmare
.waitToClick(selectors.workerTimeControl.sundayAddTimeButton) .waitToClick(selectors.workerTimeControl.sundayAddTimeButton)
.clearInput(selectors.workerTimeControl.timeDialogInput)
.write(selectors.workerTimeControl.timeDialogInput, scanTime) .write(selectors.workerTimeControl.timeDialogInput, scanTime)
.waitToClick(selectors.workerTimeControl.confirmButton) .waitToClick(selectors.workerTimeControl.confirmButton)
.waitToGetProperty(selectors.workerTimeControl.firstEntryOfSunday, 'innerText'); .waitToGetProperty(selectors.workerTimeControl.firstEntryOfSunday, 'innerText');
@ -341,6 +364,7 @@ describe('Worker time control path', () => {
const scanTime = '12:40'; const scanTime = '12:40';
const result = await nightmare const result = await nightmare
.waitToClick(selectors.workerTimeControl.sundayAddTimeButton) .waitToClick(selectors.workerTimeControl.sundayAddTimeButton)
.clearInput(selectors.workerTimeControl.timeDialogInput)
.write(selectors.workerTimeControl.timeDialogInput, scanTime) .write(selectors.workerTimeControl.timeDialogInput, scanTime)
.waitToClick(selectors.workerTimeControl.confirmButton) .waitToClick(selectors.workerTimeControl.confirmButton)
.waitToGetProperty(selectors.workerTimeControl.secondEntryOfSunday, 'innerText'); .waitToGetProperty(selectors.workerTimeControl.secondEntryOfSunday, 'innerText');

View File

@ -148,6 +148,7 @@ describe('Ticket Edit sale path', () => {
it('should confirm the price have been updated', async() => { it('should confirm the price have been updated', async() => {
const result = await nightmare const result = await nightmare
.wait(1999)
.waitToGetProperty(`${selectors.ticketSales.firstSalePrice} span`, 'innerText'); .waitToGetProperty(`${selectors.ticketSales.firstSalePrice} span`, 'innerText');
expect(result).toContain('5.00'); expect(result).toContain('5.00');
@ -243,6 +244,7 @@ describe('Ticket Edit sale path', () => {
.waitToClick(selectors.ticketSales.thirdSaleCheckbox) .waitToClick(selectors.ticketSales.thirdSaleCheckbox)
.waitToClick(selectors.ticketSales.deleteSaleButton) .waitToClick(selectors.ticketSales.deleteSaleButton)
.waitToClick(selectors.ticketSales.acceptDeleteLineButton) .waitToClick(selectors.ticketSales.acceptDeleteLineButton)
.waitForSpinnerLoad()
.waitForLastSnackbar(); .waitForLastSnackbar();
expect(result).toEqual('Data saved!'); expect(result).toEqual('Data saved!');

View File

@ -30,6 +30,8 @@ export default class Autocomplete extends Input {
componentHandler.upgradeElement( componentHandler.upgradeElement(
this.element.querySelector('.mdl-textfield')); this.element.querySelector('.mdl-textfield'));
this.registerEvents();
} }
$postLink() { $postLink() {
@ -40,6 +42,15 @@ export default class Autocomplete extends Input {
this.refreshSelection(); this.refreshSelection();
} }
/**
* Registers all event emitters
*/
registerEvents() {
this.input.addEventListener('focus', event => {
this.emit('focus', {event});
});
}
get model() { get model() {
return this._model; return this._model;
} }
@ -223,7 +234,6 @@ export default class Autocomplete extends Input {
const value = item[this.valueField]; const value = item[this.valueField];
this.selection = item; this.selection = item;
this.setValue(value); this.setValue(value);
this.field = value;
} }
onClearClick(event) { onClearClick(event) {
@ -232,7 +242,7 @@ export default class Autocomplete extends Input {
} }
onKeyDown(event) { onKeyDown(event) {
if (event.defaultPrevented) return; // if (event.defaultPrevented) return;
switch (event.keyCode) { switch (event.keyCode) {
case 38: // Up case 38: // Up
@ -241,6 +251,7 @@ export default class Autocomplete extends Input {
this.showDropDown(); this.showDropDown();
break; break;
default: default:
console.log(event.key);
if (event.key.length == 1) if (event.key.length == 1)
this.showDropDown(event.key); this.showDropDown(event.key);
else else

View File

@ -6,10 +6,6 @@ vn-calendar.small {
} }
} }
vn-calendar[disabled] .day .day-number {
cursor: not-allowed
}
vn-calendar { vn-calendar {
display: block; display: block;

View File

@ -4,7 +4,7 @@
ng-disabled="::$ctrl.disabled" ng-disabled="::$ctrl.disabled"
ng-checked="$ctrl.isChecked" ng-checked="$ctrl.isChecked"
ng-model="$ctrl.model"> ng-model="$ctrl.model">
<span translate>{{::$ctrl.label}}</span> <span translate ng-if="::$ctrl.label">{{::$ctrl.label}}</span>
</md-checkbox> </md-checkbox>
<i class="material-icons" <i class="material-icons"
ng-if="::$ctrl.hasInfo" ng-if="::$ctrl.hasInfo"

View File

@ -16,6 +16,10 @@ vn-check {
} }
md-checkbox { md-checkbox {
margin-bottom: 0.8em margin-bottom: 0.8em;
.md-label:empty {
margin: 0
}
} }
} }

View File

@ -1,9 +1,5 @@
vn-multi-check { vn-multi-check {
md-checkbox { md-checkbox {
margin-bottom: 0.8em; margin-bottom: 0.8em
.md-label {
margin: 0
}
} }
} }

View File

@ -63,6 +63,10 @@ vn-table {
padding-bottom: .8em; padding-bottom: .8em;
} }
& > vn-th, & > vn-th,
& > vn-td {
overflow: hidden;
}
& > vn-th,
& > vn-td, & > vn-td,
& > vn-td-editable { & > vn-td-editable {
vertical-align: middle; vertical-align: middle;
@ -70,7 +74,6 @@ vn-table {
text-align: left; text-align: left;
padding: .6em .5em; padding: .6em .5em;
white-space: nowrap; white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
max-width: 5em; max-width: 5em;
@ -157,7 +160,7 @@ vn-table {
} }
vn-autocomplete { vn-autocomplete {
div.mdl-textfield { div.mdl-textfield {
padding: 0px !important; padding: 0 !important;
} }
label.mdl-textfield__label:after { label.mdl-textfield__label:after {
bottom: 0; bottom: 0;

View File

@ -1,42 +1,71 @@
import ngModule from '../../module'; import ngModule from '../../module';
import Component from '../../lib/component'; import Component from '../../lib/component';
import Input from '../../lib/input';
import './style.scss'; import './style.scss';
export default class Controller extends Component { export default class Controller extends Component {
constructor($element, $scope, $transclude, $timeout) { constructor($element, $scope, $transclude, $timeout) {
super($element, $scope); super($element, $scope);
this.$timeout = $timeout; this.$timeout = $timeout;
let element = $element[0]; this.element.tabIndex = 0;
element.tabIndex = 0;
element.addEventListener('focus', () => { this.element.addEventListener('focus', () => {
if (this.field || this.disabled) return; if (this.field || this.disabled) return;
$transclude((tClone, tScope) => { $transclude((tClone, tScope) => {
this.field = tClone; this.field = tClone;
this.tScope = tScope; this.tScope = tScope;
this.element.querySelector('.field').appendChild(this.field[0]); this.element.querySelector('.field').appendChild(this.field[0]);
element.tabIndex = -1; this.element.tabIndex = -1;
}, null, 'field'); }, null, 'field');
element.classList.add('selected'); this.element.classList.add('selected');
}); });
element.addEventListener('focusout', event => { this.element.addEventListener('focusout', event => this.hideField(event));
if (!this.field || this.disabled) return;
// this.destroyTimer();
this.lastEvent = event;
let target = event.relatedTarget;
while (target && target != element)
target = target.parentNode;
if (!target) { this.element.addEventListener('keyup', event => {
this.tScope.$destroy(); if (event.key === 'Enter')
this.field.remove(); this.hideField(event);
this.field = null; });
element.classList.remove('selected');
element.tabIndex = 0; this.element.addEventListener('click', event => {
if (this.disabled) return;
let target = event.target;
while (target) {
if (target == this.field[0]) return;
target = target.parentNode;
}
let inputCtrl = this.field[0].firstElementChild.$ctrl;
if (inputCtrl instanceof Input) {
let evt = new MouseEvent('click', {
bubbles: true,
cancelable: true,
view: window
});
inputCtrl.input.dispatchEvent(evt);
} }
}); });
} }
hideField(event) {
if (!this.field || !this.tScope) return;
this.lastEvent = event;
let target = event.relatedTarget;
while (target && target != this.element)
target = target.parentNode;
if (!target) {
this.tScope.$destroy();
this.tScope = null;
this.field.remove();
this.field = null;
this.element.classList.remove('selected');
this.element.tabIndex = 0;
}
}
destroyTimer() { destroyTimer() {
if (this.timer) { if (this.timer) {
this.$timeout.cancel(this.timer); this.$timeout.cancel(this.timer);
@ -47,6 +76,21 @@ export default class Controller extends Component {
$onDestroy() { $onDestroy() {
this.destroyTimer(); this.destroyTimer();
} }
get disabled() {
return this._disabled;
}
set disabled(value) {
this._disabled = value;
const classList = this.element.classList;
if (value)
classList.add('disabled');
else
classList.remove('disabled');
}
} }
Controller.$inject = ['$element', '$scope', '$transclude', '$timeout']; Controller.$inject = ['$element', '$scope', '$transclude', '$timeout'];

View File

@ -1,11 +1,21 @@
@import "variables"; @import "variables";
vn-td-editable { vn-td-editable {
cursor: pointer;
outline: none;
position: relative;
overflow: visible;
text { text {
border-bottom: 1px solid rgba(0,0,0,.12); border: 1px dashed rgba(0, 0, 0, .15);
border-radius: 1em;
padding: 5px 10px;
min-height: 15px; min-height: 15px;
cursor: pointer; display: block;
display: block overflow: hidden;
&:hover {
border-color: $color-main;
}
} }
text::after { text::after {
@ -13,25 +23,17 @@ vn-td-editable {
content: ''; content: '';
clear: both; clear: both;
} }
outline: none;
position: relative;
&:not([disabled="true"]) {
cursor: initial
}
&[disabled="true"] {
cursor: not-allowed;
}
&.selected > .text { &.selected > .text {
visibility: hidden; visibility: hidden;
} }
& > .field { & > .field {
display: none; display: none;
position: absolute;
top: 0;
left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
position: absolute;
z-index:10;
top: 0;
left: 0;
box-sizing: border-box; box-sizing: border-box;
align-items: center; align-items: center;
padding: .6em; padding: .6em;
@ -54,4 +56,11 @@ vn-td-editable {
&.selected > .field { &.selected > .field {
display: flex; display: flex;
} }
}
vn-td-editable.disabled {
cursor: initial;
}
vn-td-editable.disabled text {
border: none;
} }

View File

@ -1,13 +1,20 @@
import ngModule from '../module'; import ngModule from '../module';
export function focus(input) { export function focus(input) {
const element = input;
let selector = 'input, textarea, button, submit'; let selector = 'input, textarea, button, submit';
if (!input.matches(selector)) if (!input.matches(selector))
input = input.querySelector(selector); input = input.querySelector(selector);
if (!input) { if (!input) {
console.warn(`vnFocus: Can't find a focusable element`); const focusEvent = new MouseEvent('focus', {
bubbles: true,
cancelable: true,
view: window
});
element.dispatchEvent(focusEvent);
return; return;
} }

View File

@ -24,16 +24,9 @@ describe('Directive focus', () => {
let childHtml = '<input></input>'; let childHtml = '<input></input>';
compile(html, childHtml); compile(html, childHtml);
expect($element[0].firstChild.focus).toHaveBeenCalled(); expect($element[0].firstChild.focus).toHaveBeenCalledWith();
}); });
it('should print a warning message on console', () => {
let html = `<potato vn-focus></potato>`;
console.warn = jasmine.createSpy('warn');
compile(html);
expect(console.warn).toHaveBeenCalledWith(`vnFocus: Can't find a focusable element`);
});
it('should call focus function on the element', () => { it('should call focus function on the element', () => {
let html = `<input vn-focus></input>`; let html = `<input vn-focus></input>`;

View File

@ -9,7 +9,7 @@ export default function percentage() {
return function(input) { return function(input) {
if (input == null || input === '') if (input == null || input === '')
return null; return null;
return input * 100 + ' %'; return `${input} %`;
}; };
} }
ngModule.filter('percentage', percentage); ngModule.filter('percentage', percentage);

View File

@ -26,10 +26,6 @@
line-height: 0px; line-height: 0px;
} }
input:disabled, button:disabled {
cursor: not-allowed !important;
}
input[type="submit"]:disabled, button:disabled { input[type="submit"]:disabled, button:disabled {
opacity: 0.7; opacity: 0.7;
} }

View File

@ -49,5 +49,6 @@
"NO_ZONE_FOR_THIS_PARAMETERS": "NO_ZONE_FOR_THIS_PARAMETERS", "NO_ZONE_FOR_THIS_PARAMETERS": "NO_ZONE_FOR_THIS_PARAMETERS",
"This client can't be invoiced": "This client can't be invoiced", "This client can't be invoiced": "This client can't be invoiced",
"The introduced hour already exists": "The introduced hour already exists", "The introduced hour already exists": "The introduced hour already exists",
"Invalid parameters to create a new ticket": "Invalid parameters to create a new ticket" "Invalid parameters to create a new ticket": "Invalid parameters to create a new ticket",
"Concept cannot be blank": "Concept cannot be blank"
} }

View File

@ -76,7 +76,6 @@
"We weren't able to send this SMS": "No hemos podido enviar el SMS", "We weren't able to send this SMS": "No hemos podido enviar el SMS",
"This client can't be invoiced": "Este cliente no puede ser facturado", "This client can't be invoiced": "Este cliente no puede ser facturado",
"This ticket can't be invoiced": "Este ticket no puede ser facturado", "This ticket can't be invoiced": "Este ticket no puede ser facturado",
"That item is not available on that day": "El item no esta disponible para esa fecha",
"You cannot add or modify services to an invoiced ticket": "No puedes añadir o modificar servicios a un ticket facturado", "You cannot add or modify services to an invoiced ticket": "No puedes añadir o modificar servicios a un ticket facturado",
"This ticket can not be modified": "Este ticket no puede ser modificado", "This ticket can not be modified": "Este ticket no puede ser modificado",
"The introduced hour already exists": "Esta hora ya ha sido introducida", "The introduced hour already exists": "Esta hora ya ha sido introducida",
@ -92,5 +91,7 @@
"This item doesn't exists": "El artículo no existe", "This item doesn't exists": "El artículo no existe",
"NOT_ZONE_WITH_THIS_PARAMETERS": "Para este día no hay ninguna zona configurada", "NOT_ZONE_WITH_THIS_PARAMETERS": "Para este día no hay ninguna zona configurada",
"Extension format is invalid": "El formato de la extensión es inválido", "Extension format is invalid": "El formato de la extensión es inválido",
"Invalid parameters to create a new ticket": "Parámetros inválidos para crear un nuevo ticket" "Invalid parameters to create a new ticket": "Parámetros inválidos para crear un nuevo ticket",
"This item is not available": "Este artículo no está disponible",
"Concept cannot be blank": "Concept cannot be blank"
} }

View File

@ -197,7 +197,7 @@
"foreignKey": "clientFk" "foreignKey": "clientFk"
}, },
"claimsRatio": { "claimsRatio": {
"type": "hasMany", "type": "hasOne",
"model": "ClaimRatio", "model": "ClaimRatio",
"foreignKey": "clientFk" "foreignKey": "clientFk"
} }

View File

@ -155,13 +155,13 @@
value="{{$ctrl.summary.mana.mana | currency: 'EUR':2}}"> value="{{$ctrl.summary.mana.mana | currency: 'EUR':2}}">
</vn-label-value> </vn-label-value>
<vn-label-value label="Rate" <vn-label-value label="Rate"
value="{{$ctrl.summary.claimsRatio[0].priceIncreasing | percentage}}"> value="{{$ctrl.claimRate($ctrl.summary.claimsRatio.priceIncreasing) | percentage}}">
</vn-label-value> </vn-label-value>
<vn-label-value label="Average invoiced" <vn-label-value label="Average invoiced"
value="{{$ctrl.summary.averageInvoiced.invoiced | currency: 'EUR':2}}"> value="{{$ctrl.summary.averageInvoiced.invoiced | currency: 'EUR':2}}">
</vn-label-value> </vn-label-value>
<vn-label-value label="Claims" <vn-label-value label="Claims"
value="{{$ctrl.summary.claimsRatio[0].claimingRate | percentage}}"> value="{{$ctrl.claimingRate($ctrl.summary.claimsRatio.claimingRate) | percentage}}">
</vn-label-value> </vn-label-value>
</vn-one> </vn-one>
<vn-one> <vn-one>

View File

@ -29,6 +29,16 @@ class Controller {
}); });
return total; return total;
} }
claimRate(priceIncreasing) {
if (priceIncreasing)
return priceIncreasing * 100;
}
claimingRate(rate) {
if (rate)
return rate * 100;
}
} }
Controller.$inject = ['$http']; Controller.$inject = ['$http'];

View File

@ -1,7 +1,7 @@
<vn-horizontal> <vn-horizontal>
<vn-one title="{{::$ctrl.item.name}}">{{::$ctrl.item.name}}</vn-one> <vn-one title="{{$ctrl.name}}">{{$ctrl.name}}</vn-one>
<vn-one ng-if="$ctrl.subName"> <vn-one ng-if="$ctrl.subName">
<h3 title="{{::$ctrl.subName}}">{{::$ctrl.subName}}</h3> <h3 title="{{$ctrl.subName}}">{{$ctrl.subName}}</h3>
</vn-one> </vn-one>
<vn-auto> <vn-auto>
<section <section

View File

@ -6,6 +6,7 @@ ngModule.component('vnFetchedTags', {
bindings: { bindings: {
maxLength: '<', maxLength: '<',
item: '<', item: '<',
name: '<?',
subName: '<?' subName: '<?'
} }
}); });

View File

@ -75,6 +75,7 @@
<vn-fetched-tags <vn-fetched-tags
max-length="6" max-length="6"
item="::item" item="::item"
name="::item.name"
sub-name="::item.subName"> sub-name="::item.subName">
</vn-fetched-tags> </vn-fetched-tags>
</vn-td> </vn-td>

View File

@ -43,6 +43,7 @@
<vn-fetched-tags <vn-fetched-tags
max-length="6" max-length="6"
item="::row.item" item="::row.item"
name="::row.item.name"
sub-name="::row.item.subName"> sub-name="::row.item.subName">
</vn-fetched-tags> </vn-fetched-tags>
</vn-td> </vn-td>

View File

@ -72,6 +72,7 @@
<vn-fetched-tags <vn-fetched-tags
max-length="6" max-length="6"
item="::row.item" item="::row.item"
name="::row.item.name"
sub-name="::row.item.subName"> sub-name="::row.item.subName">
</vn-fetched-tags> </vn-fetched-tags>
</vn-td> </vn-td>

View File

@ -42,6 +42,7 @@
<vn-fetched-tags <vn-fetched-tags
max-length="6" max-length="6"
item="::row.item" item="::row.item"
name="::row.item.name"
sub-name="::row.item.subName"> sub-name="::row.item.subName">
</vn-fetched-tags> </vn-fetched-tags>
</vn-td> </vn-td>

View File

@ -19,7 +19,7 @@ module.exports = Self => {
} }
], ],
returns: { returns: {
type: 'string', type: 'Number',
root: true root: true
}, },
http: { http: {
@ -36,7 +36,6 @@ module.exports = Self => {
let options = {transaction: tx}; let options = {transaction: tx};
let filter = { let filter = {
fields: ['id', 'ticketFk', 'price'],
include: { include: {
relation: 'ticket', relation: 'ticket',
scope: { scope: {
@ -88,6 +87,8 @@ module.exports = Self => {
await Self.rawSql(query, [salesPerson], options); await Self.rawSql(query, [salesPerson], options);
await tx.commit(); await tx.commit();
return sale;
} catch (error) { } catch (error) {
await tx.rollback(); await tx.rollback();
throw error; throw error;

View File

@ -45,20 +45,15 @@ module.exports = Self => {
include: {relation: 'ticket'} include: {relation: 'ticket'}
}); });
let query = `CALL vn.getItemVisibleAvailable(?,?,?,?)`; let [[stock]] = await Self.rawSql(`CALL vn.getItemVisibleAvailable(?,?,?,?)`, [
let params = [
ctx.args.itemFk, ctx.args.itemFk,
request.ticket().shipped, request.ticket().shipped,
request.ticket().warehouseFk, request.ticket().warehouseFk,
false false
]; ]);
let [res] = await Self.rawSql(query, params);
let available = res[0].available;
if (!available)
throw new UserError(`That item is not available on that day`);
if (stock.available < quantity)
throw new UserError(`This item is not available`);
if (request.saleFk) { if (request.saleFk) {
let sale = await models.Sale.findById(request.saleFk); let sale = await models.Sale.findById(request.saleFk);

View File

@ -0,0 +1,71 @@
const UserError = require('vn-loopback/util/user-error');
module.exports = Self => {
Self.remoteMethod('addSale', {
description: 'Inserts a new sale for the current ticket',
accessType: 'WRITE',
accepts: [{
arg: 'id',
type: 'Number',
required: true,
description: 'The ticket id',
http: {source: 'path'}
},
{
arg: 'itemId',
type: 'Number',
required: true
},
{
arg: 'quantity',
type: 'Number',
required: true
}],
returns: {
type: 'Object',
root: true
},
http: {
path: `/:id/addSale`,
verb: 'POST'
}
});
Self.addSale = async(id, itemId, quantity) => {
const models = Self.app.models;
const isEditable = await models.Ticket.isEditable(id);
if (!isEditable)
throw new UserError(`The sales of this ticket can't be modified`);
const item = await models.Item.findById(itemId);
const ticket = await models.Ticket.findById(id);
const shouldRefresh = false;
const [[stock]] = await Self.rawSql(`CALL vn.getItemVisibleAvailable(?, ?, ?, ?)`, [
itemId,
ticket.shipped,
ticket.warehouseFk,
shouldRefresh
]);
if (stock.available < quantity)
throw new UserError(`This item is not available`);
const newSale = await models.Sale.create({
ticketFk: id,
itemFk: item.id,
concept: item.name,
quantity: quantity
});
await Self.rawSql('CALL vn.ticketCalculateSale(?)', [newSale.id]);
return models.Sale.findById(newSale.id, {
include: {
relation: 'item'
}
});
};
};

View File

@ -51,7 +51,7 @@ module.exports = Self => {
claimMap[claim.saleFk] = claim; claimMap[claim.saleFk] = claim;
for (line of lines) { for (line of lines) {
line.tags = map[line.itemFk]; line.item = map[line.itemFk];
line.claim = claimMap[line.id]; line.claim = claimMap[line.id];
} }

View File

@ -0,0 +1,49 @@
const app = require('vn-loopback/server/server');
describe('ticket addSale()', () => {
const ticketId = 13;
let newSale;
afterAll(async done => {
const sale = await app.models.Sale.findById(newSale.id);
await sale.destroy();
done();
});
it('should create a new sale for the ticket with id 13', async() => {
const itemId = 4;
const quantity = 10;
newSale = await app.models.Ticket.addSale(ticketId, itemId, quantity);
expect(newSale.itemFk).toEqual(4);
});
it('should not be able to add a sale if the item quantity is not available', async() => {
const itemId = 11;
const quantity = 10;
let error;
await app.models.Ticket.addSale(ticketId, itemId, quantity).catch(e => {
error = e;
}).finally(() => {
expect(error.message).toEqual(`This item is not available`);
});
expect(error).toBeDefined();
});
it('should not be able to add a sale if the ticket is not editable', async() => {
const notEditableTicketId = 1;
const itemId = 4;
const quantity = 10;
let error;
await app.models.Ticket.addSale(notEditableTicketId, itemId, quantity).catch(e => {
error = e;
}).finally(() => {
expect(error.message).toEqual(`The sales of this ticket can't be modified`);
});
expect(error).toBeDefined();
});
});

View File

@ -5,10 +5,10 @@ describe('ticket getSales()', () => {
let sales = await app.models.Ticket.getSales(16); let sales = await app.models.Ticket.getSales(16);
expect(sales.length).toEqual(4); expect(sales.length).toEqual(4);
expect(sales[0].tags).toBeDefined(); expect(sales[0].item).toBeDefined();
expect(sales[1].tags).toBeDefined(); expect(sales[1].item).toBeDefined();
expect(sales[2].tags).toBeDefined(); expect(sales[2].item).toBeDefined();
expect(sales[3].tags).toBeDefined(); expect(sales[3].item).toBeDefined();
expect(sales[0].claim).toBeDefined(); expect(sales[0].claim).toBeDefined();
}); });
}); });

View File

@ -6,4 +6,8 @@ module.exports = Self => {
require('../methods/sale/removes')(Self); require('../methods/sale/removes')(Self);
require('../methods/sale/updatePrice')(Self); require('../methods/sale/updatePrice')(Self);
require('../methods/sale/updateQuantity')(Self); require('../methods/sale/updateQuantity')(Self);
Self.validatesPresenceOf('concept', {
message: `Concept cannot be blank`
});
}; };

View File

@ -13,7 +13,8 @@
"description": "Identifier" "description": "Identifier"
}, },
"concept": { "concept": {
"type": "String" "type": "String",
"required": true
}, },
"quantity": { "quantity": {
"type": "Number" "type": "Number"

View File

@ -22,4 +22,5 @@ module.exports = Self => {
require('../methods/ticket/checkEmptiness')(Self); require('../methods/ticket/checkEmptiness')(Self);
require('../methods/ticket/updateDiscount')(Self); require('../methods/ticket/updateDiscount')(Self);
require('../methods/ticket/uploadFile')(Self); require('../methods/ticket/uploadFile')(Self);
require('../methods/ticket/addSale')(Self);
}; };

View File

@ -18,7 +18,8 @@
<td expand> <td expand>
<vn-fetched-tags <vn-fetched-tags
max-length="6" max-length="6"
item="::sale.item"> item="::sale.item"
name="::sale.concept">
</vn-fetched-tags> </vn-fetched-tags>
</td> </td>
<td number>{{::sale.quantity}}</td> <td number>{{::sale.quantity}}</td>

View File

@ -41,6 +41,7 @@
<vn-fetched-tags <vn-fetched-tags
max-length="6" max-length="6"
item="::sale.item" item="::sale.item"
name="::sale.concept"
sub-name="::sale.item.subName"> sub-name="::sale.item.subName">
</vn-fetched-tags> </vn-fetched-tags>
</td> </td>

View File

@ -37,6 +37,7 @@
<vn-fetched-tags <vn-fetched-tags
max-length="6" max-length="6"
item="::sale.item" item="::sale.item"
name="::sale.concept"
sub-name="::sale.item.subName"> sub-name="::sale.item.subName">
</vn-fetched-tags> </vn-fetched-tags>
</vn-td> </vn-td>

View File

@ -41,6 +41,7 @@
<vn-fetched-tags <vn-fetched-tags
max-length="6" max-length="6"
item="::sale.item" item="::sale.item"
name="::sale.concept"
sub-name="::sale.item.subName"> sub-name="::sale.item.subName">
</vn-fetched-tags> </vn-fetched-tags>
</vn-td> </vn-td>

View File

@ -93,66 +93,86 @@
</vn-icon> </vn-icon>
<vn-icon ng-show="sale.reserved" <vn-icon ng-show="sale.reserved"
icon="icon-reserve" icon="icon-reserve"
vn-tooltip="{{::$ctrl.$translate.instant('Reserved')}}"></vn-icon> vn-tooltip="{{::$ctrl.$translate.instant('Reserved')}}">
</vn-icon>
</vn-td> </vn-td>
<vn-td shrink> <vn-td shrink>
<img <img
ng-src="{{::$ctrl.imagesPath}}/50x50/{{::sale.image}}" ng-src="{{::$ctrl.imagesPath}}/50x50/{{sale.image}}"
zoom-image="{{::$ctrl.imagesPath}}/1600x900/{{::sale.image}}" zoom-image="{{::$ctrl.imagesPath}}/1600x900/{{sale.image}}"
on-error-src/> on-error-src/>
</vn-td> </vn-td>
<vn-td number> <vn-td-editable disabled="sale.itemFk" vn-focus number>
<span <text>
ng-click="$ctrl.showDescriptor($event, sale.itemFk)" <span class="link" ng-show="sale.itemFk"
class="link"> ng-click="$ctrl.showDescriptor($event, sale.itemFk)">
{{::sale.itemFk | zeroFill:6}} {{sale.itemFk | zeroFill:6}}
</span> </span>
</vn-td> </text>
<vn-td-editable editable="!$ctrl.isEditable"> <field>
<text>{{sale.quantity}}</text> <vn-autocomplete vn-focus vn-one
<field> url="/api/Items"
<vn-textfield vn-focus field="sale.itemFk"
model="sale.quantity" show-field="name"
on-change="$ctrl.updateQuantity(sale.id, sale.quantity)" value-field="id"
type="text"> search-function="{or: [{id: $search}, {name: {like: '%' + $search + '%'}}]}">
</vn-textfield> </vn-autocomplete>
</field> </field>
</vn-td-editable> </vn-td-editable>
<vn-td expand> <vn-td-editable disabled="!$ctrl.isEditable" number>
<text>{{sale.quantity}}</text>
<field>
<vn-input-number vn-focus
model="sale.quantity"
on-change="$ctrl.onChangeQuantity(sale)">
</vn-input-number>
</field>
</vn-td-editable>
<vn-td-editable disabled="!sale.id || !$ctrl.isEditable" expand>
<text>
<vn-fetched-tags <vn-fetched-tags
max-length="6" max-length="6"
item="::sale.tags" item="sale.item"
sub-name="::sale.subName"> name="sale.concept"
sub-name="sale.subName">
</vn-fetched-tags> </vn-fetched-tags>
</vn-td> </text>
<vn-td number ng-if="$ctrl.isEditable"> <field>
<span class="link" <vn-textfield vn-id="concept" vn-focus
vn-tooltip="Edit price" model="sale.concept"
ng-click="$ctrl.showEditPricePopover($event, sale)"> on-change="$ctrl.updateConcept(sale)">
{{sale.price | currency: 'EUR':2}} </vn-textfield>
</span> </field>
</vn-td> </vn-td-editable>
<vn-td number ng-if="!$ctrl.isEditable"> <vn-td number>
<span ng-class="{'link': $ctrl.isEditable}"
title="{{$ctrl.isEditable ? 'Edit price' : ''}}"
ng-click="$ctrl.showEditPricePopover($event, sale)">
{{sale.price | currency: 'EUR':2}} {{sale.price | currency: 'EUR':2}}
</vn-td> </span>
<vn-td number ng-if="!$ctrl.ticket.refFk && $ctrl.isEditable"> </vn-td>
<span class="link" <vn-td number>
vn-tooltip="Edit discount" <span ng-class="{'link': $ctrl.isEditable}"
ng-click="$ctrl.showEditPopover($event, sale)"> title="{{$ctrl.isEditable ? 'Edit discount' : ''}}"
{{sale.discount}} % ng-click="$ctrl.showEditDiscountPopover($event, sale)">
</span> {{sale.discount | percentage}}
</vn-td> </span>
<vn-td number </vn-td>
ng-if="!$ctrl.isEditable"> <vn-td number>
{{sale.discount}} % {{$ctrl.getSaleTotal(sale) | currency: 'EUR':2}}
</vn-td> </vn-td>
<vn-td number> </vn-tr>
{{$ctrl.getSaleTotal(sale) | currency: 'EUR':2}} </vn-tbody>
</vn-td> </vn-table>
</vn-tr> <vn-one>
</vn-tbody> <vn-icon-button vn-none
</vn-table> vn-tooltip="Add item"
</vn-vertical> vn-bind="+"
icon="add_circle"
ng-click="$ctrl.add()"
disabled="!$ctrl.isEditable">
</vn-icon-button>
</vn-one>
</vn-card> </vn-card>
<vn-item-descriptor-popover vn-id="descriptor" <vn-item-descriptor-popover vn-id="descriptor"
quicklinks="$ctrl.quicklinks"> quicklinks="$ctrl.quicklinks">
@ -248,7 +268,9 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr ng-if="$ctrl.lastThreeTickets.length === 0" ><td colspan="4" style="text-align: center" translate>No results</td></tr> <tr ng-if="$ctrl.lastThreeTickets.length === 0" >
<td colspan="4" style="text-align: center" translate>No results</td>
</tr>
<tr <tr
class="clickable" class="clickable"
ng-repeat="ticket in $ctrl.lastThreeTickets track by ticket.id" ng-repeat="ticket in $ctrl.lastThreeTickets track by ticket.id"
@ -305,7 +327,7 @@
ng-show="$ctrl.isEditable" ng-show="$ctrl.isEditable"
ng-click="$ctrl.newOrderFromTicket()" ng-click="$ctrl.newOrderFromTicket()"
icon="add" icon="add"
vn-tooltip="New item" vn-tooltip="Add item"
vn-bind="+" vn-bind="+"
fixed-bottom-right> fixed-bottom-right>
</vn-float-button> </vn-float-button>

View File

@ -17,9 +17,10 @@ class Controller {
{callback: this.createClaim, name: 'Add claim'}, {callback: this.createClaim, name: 'Add claim'},
{callback: this.showSMSDialog, name: 'Send SMS'} {callback: this.showSMSDialog, name: 'Send SMS'}
]; ];
this._sales = [];
this.imagesPath = '//verdnatura.es/vn-image-data/catalog'; this.imagesPath = '//verdnatura.es/vn-image-data/catalog';
} }
get sales() { get sales() {
return this._sales; return this._sales;
} }
@ -38,7 +39,6 @@ class Controller {
this.updateNewPrice(); this.updateNewPrice();
} }
refreshTotal() { refreshTotal() {
this.loadSubTotal(); this.loadSubTotal();
this.loadVAT(); this.loadVAT();
@ -52,6 +52,9 @@ class Controller {
} }
getSaleTotal(sale) { getSaleTotal(sale) {
if (!sale.quantity || !sale.price)
return;
return sale.quantity * sale.price * ((100 - sale.discount) / 100); return sale.quantity * sale.price * ((100 - sale.discount) / 100);
} }
@ -99,16 +102,45 @@ class Controller {
return false; return false;
} }
/**
* Returns checked instances
*
* @return {Array} Checked instances
*/
getCheckedLines() { getCheckedLines() {
let lines = []; if (!this.sales) return;
let data = this.sales;
if (data) { return this.sales.filter(sale => {
for (let i = 0; i < data.length; i++) { return sale.checked;
if (data[i].checked) });
lines.push({id: data[i].id, instance: i}); }
}
} /**
return lines; * Returns an array of indexes
* from checked instances
*
* @return {Array} Indexes of checked instances
*/
getCheckedLinesIndex() {
if (!this.sales) return;
let indexes = [];
this.sales.forEach((sale, index) => {
if (sale.checked) indexes.push(index);
});
return indexes;
}
removeCheckedLines() {
const sales = this.getCheckedLines();
sales.forEach(sale => {
const index = this.sales.indexOf(sale);
this.sales.splice(index, 1);
});
this.refreshTotal();
} }
onStateOkClick() { onStateOkClick() {
@ -130,21 +162,21 @@ class Controller {
onRemoveLinesClick(response) { onRemoveLinesClick(response) {
if (response === 'ACCEPT') { if (response === 'ACCEPT') {
let sales = this.getCheckedLines(); let sales = this.getCheckedLines();
// Remove unsaved instances
sales.forEach((sale, index) => {
if (!sale.id) sales.splice(index);
});
let params = {sales: sales, actualTicketFk: this.ticket.id}; let params = {sales: sales, actualTicketFk: this.ticket.id};
let query = `/api/Sales/removes`; let query = `/api/Sales/removes`;
this.$http.post(query, params).then(() => { this.$http.post(query, params).then(() => {
this.removeInstances(sales); this.removeCheckedLines();
this.vnApp.showSuccess(this.$translate.instant('Data saved!')); this.vnApp.showSuccess(this.$translate.instant('Data saved!'));
}); });
} }
} }
removeInstances(instances) {
for (let i of instances)
this.sales.splice(i.instance, 1);
this.refreshTotal();
}
showRemoveLinesDialog() { showRemoveLinesDialog() {
this.$scope.deleteLines.show(); this.$scope.deleteLines.show();
} }
@ -208,14 +240,12 @@ class Controller {
} }
createClaim() { createClaim() {
let claim = { const claim = {
ticketFk: this.ticket.id, ticketFk: this.ticket.id,
clientFk: this.ticket.clientFk, clientFk: this.ticket.clientFk,
ticketCreated: this.ticket.shipped ticketCreated: this.ticket.shipped
}; };
let sales = this.getCheckedLines(); const sales = this.getCheckedLines();
for (let i = 0; i < sales.length; i++)
sales[i].quantity = this.sales[sales[i].instance].quantity;
this.$http.post(`/api/Claims/createFromSales`, {claim: claim, sales: sales}).then(res => { this.$http.post(`/api/Claims/createFromSales`, {claim: claim, sales: sales}).then(res => {
this.$state.go('claim.card.basicData', {id: res.data.id}); this.$state.go('claim.card.basicData', {id: res.data.id});
}); });
@ -255,6 +285,7 @@ class Controller {
} }
showEditPricePopover(event, sale) { showEditPricePopover(event, sale) {
if (!this.isEditable) return;
this.sale = sale; this.sale = sale;
this.editedPrice = this.sale.price; this.editedPrice = this.sale.price;
this.edit = { this.edit = {
@ -268,10 +299,11 @@ class Controller {
updatePrice() { updatePrice() {
if (this.editedPrice != this.sale.price) { if (this.editedPrice != this.sale.price) {
this.$http.post(`/api/Sales/${this.edit.id}/updatePrice`, {newPrice: this.editedPrice}).then(() => { this.$http.post(`/api/Sales/${this.edit.id}/updatePrice`, {newPrice: this.editedPrice}).then(res => {
this.sale.price = this.edit.price; if (res.data)
this.sale.price = res.data.price;
this.vnApp.showSuccess(this.$translate.instant('Data saved!')); this.vnApp.showSuccess(this.$translate.instant('Data saved!'));
this.$scope.model.refresh();
}); });
} }
@ -282,7 +314,9 @@ class Controller {
this.newPrice = this.sale.quantity * this.editedPrice - ((this.sale.discount * (this.sale.quantity * this.editedPrice)) / 100); this.newPrice = this.sale.quantity * this.editedPrice - ((this.sale.discount * (this.sale.quantity * this.editedPrice)) / 100);
} }
showEditPopover(event, sale) { showEditDiscountPopover(event, sale) {
if (!this.isEditable) return;
this.sale = sale; this.sale = sale;
this.edit = [{ this.edit = [{
ticketFk: this.ticket.id, ticketFk: this.ticket.id,
@ -310,24 +344,14 @@ class Controller {
this.$scope.editPopover.hide(); this.$scope.editPopover.hide();
} }
updateQuantity(id, quantity) { /*
this.$http.post(`/api/Sales/${id}/updateQuantity`, {quantity: parseInt(quantity)}).then(() => {
this.vnApp.showSuccess(this.$translate.instant('Data saved!'));
}).catch(e => {
this.vnApp.showError(e.data.error.message);
}).finally(() => {
this.$scope.model.refresh();
});
}
/**
* Unmark sale as reserved * Unmark sale as reserved
*/ */
unmarkAsReserved() { unmarkAsReserved() {
this.setReserved(false); this.setReserved(false);
} }
/** /*
* Mark sale as reserved * Mark sale as reserved
*/ */
markAsReserved() { markAsReserved() {
@ -363,10 +387,9 @@ class Controller {
showSMSDialog() { showSMSDialog() {
const address = this.ticket.address; const address = this.ticket.address;
const lines = this.getCheckedLines(); const sales = this.getCheckedLines();
const items = lines.map(line => { const items = sales.map(sale => {
const instance = this.sales[line.instance]; return `${sale.quantity} ${sale.concept}`;
return `${instance.quantity} ${instance.concept}`;
}); });
const notAvailables = items.join(', '); const notAvailables = items.join(', ');
const params = { const params = {
@ -390,6 +413,79 @@ class Controller {
hasInvoice() { hasInvoice() {
return this.ticket.refFk !== null; return this.ticket.refFk !== null;
} }
/**
* Inserts a new instance
*/
add() {
this.$scope.model.insert({});
}
/*
* Creates a new sale if it's a new instance
* Updates the sale quantity for existing instance
*/
onChangeQuantity(sale) {
if (!sale.id)
this.addSale(sale);
else
this.updateQuantity(sale);
}
/*
* Updates a sale quantity
*/
updateQuantity(sale) {
const data = {quantity: parseInt(sale.quantity)};
const query = `/api/Sales/${sale.id}/updateQuantity`;
this.$http.post(query, data).then(() => {
this.vnApp.showSuccess(this.$translate.instant('Data saved!'));
}).catch(e => {
this.$scope.model.refresh();
throw e;
});
}
/*
* Updates a sale concept
*/
updateConcept(sale) {
const data = {concept: sale.concept};
const query = `/api/Sales/${sale.id}`;
this.$http.patch(query, data).then(() => {
this.vnApp.showSuccess(this.$translate.instant('Data saved!'));
}).catch(e => {
this.$scope.model.refresh();
throw e;
});
}
/*
* Adds a new sale
*/
addSale(sale) {
const data = {
itemId: sale.itemFk,
quantity: sale.quantity
};
const query = `/api/tickets/${this.ticket.id}/addSale`;
this.$http.post(query, data).then(res => {
if (!res.data) return;
const newSale = res.data;
sale.id = newSale.id;
sale.image = newSale.item.image;
sale.subName = newSale.item.subName;
sale.concept = newSale.concept;
sale.quantity = newSale.quantity;
sale.discount = newSale.discount;
sale.price = newSale.price;
sale.item = newSale.item;
this.vnApp.showSuccess(this.$translate.instant('Data saved!'));
});
}
} }
Controller.$inject = ['$scope', '$state', '$http', 'vnApp', '$translate']; Controller.$inject = ['$scope', '$state', '$http', 'vnApp', '$translate'];

View File

@ -1,4 +1,5 @@
New price: Nuevo precio New price: Nuevo precio
Add item: Añadir artículo
Add turn: Añadir a turno Add turn: Añadir a turno
Delete ticket: Borrar ticket Delete ticket: Borrar ticket
Mark as reserved: Marcar como reservado Mark as reserved: Marcar como reservado

View File

@ -64,8 +64,10 @@ describe('Ticket', () => {
it('should perform a query and call windows open', () => { it('should perform a query and call windows open', () => {
spyOn(controller.$state, 'go'); spyOn(controller.$state, 'go');
let res = {id: 1}; const claim = {id: 1};
$httpBackend.expectPOST(`/api/Claims/createFromSales`).respond(res); const sales = [{id: 1}, {id: 2}];
$httpBackend.when('POST', `/api/Claims/createFromSales`, {claim, sales}).respond(claim);
$httpBackend.expect('POST', `/api/Claims/createFromSales`).respond(claim);
controller.createClaim(); controller.createClaim();
$httpBackend.flush(); $httpBackend.flush();
@ -93,7 +95,7 @@ describe('Ticket', () => {
it('should make an array of the instances with the property checked true()', () => { it('should make an array of the instances with the property checked true()', () => {
let sale = controller.sales[1]; let sale = controller.sales[1];
sale.checked = true; sale.checked = true;
let expectedResult = [{id: sale.id, instance: 1}]; let expectedResult = [sale];
expect(controller.getCheckedLines()).toEqual(expectedResult); expect(controller.getCheckedLines()).toEqual(expectedResult);
}); });
@ -156,12 +158,10 @@ describe('Ticket', () => {
describe('setReserved()', () => { describe('setReserved()', () => {
it('should call getCheckedLines, $.index.accept and make a query ', () => { it('should call getCheckedLines, $.index.accept and make a query ', () => {
controller.sales[1].checked = true; const sale = controller.sales[1];
sale.checked = true;
let expectedRequest = { let expectedRequest = {
sales: [{ sales: [sale],
id: sales[1].id,
instance: 1
}],
ticketFk: ticket.id, ticketFk: ticket.id,
reserved: false reserved: false
}; };
@ -184,5 +184,55 @@ describe('Ticket', () => {
expect(controller.newSMS.message).not.toEqual(''); expect(controller.newSMS.message).not.toEqual('');
}); });
}); });
describe('updateQuantity()', () => {
it('should make a POST query saving sale quantity', () => {
const data = {quantity: 10};
const sale = sales[0];
sale.quantity = 10;
$httpBackend.when('POST', `/api/Sales/1/updateQuantity`, data).respond();
$httpBackend.expect('POST', `/api/Sales/1/updateQuantity`, data).respond();
controller.updateQuantity(sale);
$httpBackend.flush();
});
});
describe('updateConcept()', () => {
it('should make a POST query saving sale concept', () => {
const data = {concept: 'My new weapon'};
const sale = sales[0];
sale.concept = 'My new weapon';
$httpBackend.when('PATCH', `/api/Sales/1`, data).respond();
$httpBackend.expect('PATCH', `/api/Sales/1`, data).respond();
controller.updateConcept(sale);
$httpBackend.flush();
});
});
describe('addSale()', () => {
it('should make a POST query adding a new sale', () => {
const newSale = {itemFk: 4, quantity: 10};
const params = {itemId: 4, quantity: 10};
const expectedResult = {
id: 30,
quantity: 10,
discount: 0,
price: 0,
itemFk: 4,
item: {
subName: 'Item subName',
image: '30.png'
}
};
$httpBackend.when('POST', `/api/tickets/1/addSale`, params).respond(expectedResult);
$httpBackend.expect('POST', `/api/tickets/1/addSale`, params).respond(expectedResult);
controller.addSale(newSale);
$httpBackend.flush();
});
});
}); });
}); });

View File

@ -101,6 +101,7 @@
<vn-fetched-tags <vn-fetched-tags
max-length="6" max-length="6"
item="::sale.item" item="::sale.item"
name="::sale.concept"
sub-name="::sale.item.subName"/> sub-name="::sale.item.subName"/>
</vn-td> </vn-td>
<vn-td number>{{::sale.price | currency: 'EUR':2}}</vn-td> <vn-td number>{{::sale.price | currency: 'EUR':2}}</vn-td>

View File

@ -46,6 +46,7 @@
<vn-fetched-tags <vn-fetched-tags
max-length="6" max-length="6"
item="::sale.item" item="::sale.item"
name="::sale.concept"
sub-name="::sale.item.subName"/> sub-name="::sale.item.subName"/>
</vn-td> </vn-td>
<vn-td number>{{::sale.quantity}}</vn-td> <vn-td number>{{::sale.quantity}}</vn-td>