added zone prices, radio-group and calendar refactor #1320
gitea/salix/dev This commit looks good Details

This commit is contained in:
Joan Sanchez 2019-06-26 13:35:38 +02:00
parent b0c34f8e29
commit 8ff61a5d3a
36 changed files with 816 additions and 327 deletions

View File

@ -1,5 +1,4 @@
const UserError = require('vn-loopback/util/user-error'); const UserError = require('vn-loopback/util/user-error');
const fs = require('fs-extra');
module.exports = Self => { module.exports = Self => {
Self.remoteMethodCtx('downloadFile', { Self.remoteMethodCtx('downloadFile', {
@ -8,7 +7,7 @@ module.exports = Self => {
accepts: [ accepts: [
{ {
arg: 'id', arg: 'id',
type: 'String', type: 'Number',
description: 'The document id', description: 'The document id',
http: {source: 'path'} http: {source: 'path'}
} }

View File

@ -1,6 +1,5 @@
const UserError = require('vn-loopback/util/user-error'); const UserError = require('vn-loopback/util/user-error');
const fs = require('fs-extra'); const fs = require('fs-extra');
const md5 = require('md5');
module.exports = Self => { module.exports = Self => {
Self.remoteMethodCtx('uploadFile', { Self.remoteMethodCtx('uploadFile', {

View File

@ -0,0 +1,3 @@
ALTER TABLE `vn`.`zoneCalendar`
ADD COLUMN `price` DOUBLE NOT NULL AFTER `delivered`,
ADD COLUMN `bonus` DOUBLE NOT NULL AFTER `price`;

View File

@ -26,37 +26,55 @@
<vn-vertical class="body"> <vn-vertical class="body">
<vn-horizontal class="weekdays"> <vn-horizontal class="weekdays">
<section class="day" ng-click="$ctrl.selectAll(1)"> <section title="{{'Monday' | translate}}"
ng-click="$ctrl.selectAll(1)">
<span>L</span> <span>L</span>
</section> </section>
<section class="day" ng-click="$ctrl.selectAll(2)"> <section title="{{'Tuesday' | translate}}"
ng-click="$ctrl.selectAll(2)">
<span>M</span> <span>M</span>
</section> </section>
<section class="day" ng-click="$ctrl.selectAll(3)"> <section title="{{'Wednesday' | translate}}"
ng-click="$ctrl.selectAll(3)">
<span>X</span> <span>X</span>
</section> </section>
<section class="day" ng-click="$ctrl.selectAll(4)"> <section title="{{'Thursday' | translate}}"
ng-click="$ctrl.selectAll(4)">
<span>J</span> <span>J</span>
</section> </section>
<section class="day" ng-click="$ctrl.selectAll(5)"> <section title="{{'Friday' | translate}}"
ng-click="$ctrl.selectAll(5)">
<span>V</span> <span>V</span>
</section> </section>
<section class="day" ng-click="$ctrl.selectAll(6)"> <section title="{{'Saturday' | translate}}"
ng-click="$ctrl.selectAll(6)">
<span>S</span> <span>S</span>
</section> </section>
<section class="day" ng-click="$ctrl.selectAll(0)"> <section title="{{'Sunday' | translate}}"
ng-click="$ctrl.selectAll(0)">
<span>D</span> <span>D</span>
</section> </section>
</vn-horizontal> </vn-horizontal>
<vn-horizontal class="days"> <vn-horizontal class="days">
<section ng-repeat="day in $ctrl.days" class="day {{day.event.className || day.className}}" <section ng-repeat="day in $ctrl.days" class="day"
ng-click="$ctrl.select($index)" ng-class="{'primary': day.events.length > 0}">
ng-style="{'color': day.event.style.color}"> <div class="content">
<span ng-if="day.event" vn-tooltip="{{day.event.title}}" <div class="day-number"
ng-style="{'background-color': day.event.style.background}"> title="{{(day.events[0].description || day.events[0].name) | translate}}"
{{::day.dated | date: 'd'}} ng-style="$ctrl.renderStyle(day.style || day.events[0].style)"
</span> ng-click="$ctrl.select($index)">
<span ng-if="!day.event">{{::day.dated | date: 'd'}}</span> {{::day.dated | date: 'd'}}
</div>
<div ng-if="day.events" class="events">
<div ng-repeat="event in day.events" class="event"
title="{{(event.description || event.name) | translate}}">
<span class="chip ellipsize"
ng-style="::$ctrl.renderStyle(event.style)">
{{::event.name}}
</span>
</div>
</div>
</div>
</section> </section>
</vn-horizontal> </vn-horizontal>
</vn-vertical> </vn-vertical>

View File

@ -13,6 +13,23 @@ export default class Calendar extends Component {
this.defaultDate = new Date(); this.defaultDate = new Date();
this.displayControls = true; this.displayControls = true;
this.skip = 1; this.skip = 1;
this.window.addEventListener('resize', () => {
this.checkSize();
});
}
/**
* Resizes the calendar
* based on component height
*/
checkSize() {
const height = this.$element[0].clientHeight;
if (height < 530)
this.$element.addClass('small');
else
this.$element.removeClass('small');
} }
/** /**
@ -49,8 +66,10 @@ export default class Calendar extends Component {
this.addEvent(event); this.addEvent(event);
}); });
if (value.length && this.defaultDate) if (value.length && this.defaultDate) {
this.repaint(); this.repaint();
this.checkSize();
}
} }
/** /**
@ -165,16 +184,28 @@ export default class Calendar extends Component {
* @param {Date} dated - Date of month * @param {Date} dated - Date of month
* @param {String} className - Default class style * @param {String} className - Default class style
*/ */
insertDay(dated, className = '') { insertDay(dated) {
let event = this.events.find(event => { let events = this.events.filter(event => {
return event.dated >= dated && event.dated <= dated; return event.dated >= dated && event.dated <= dated;
}); });
// Weeekends const params = {dated, events};
if (dated.getMonth() === this.currentMonth.getMonth() && dated.getDay() == 0)
className = 'red';
this.days.push({dated, className, event}); const isSaturday = dated.getDay() === 6;
const isSunday = dated.getDay() === 0;
const isCurrentMonth = dated.getMonth() === this.currentMonth.getMonth();
const hasEvents = events.length > 0;
if (isCurrentMonth && isSunday && !hasEvents)
params.style = {color: '#f42121'};
if (isCurrentMonth && isSaturday && !hasEvents)
params.style = {color: '#666666'};
if (!isCurrentMonth && !hasEvents)
params.style = {color: '#9b9b9b'};
this.days.push(params);
} }
/** /**
@ -182,7 +213,7 @@ export default class Calendar extends Component {
* *
* @param {Object} options - Event params * @param {Object} options - Event params
* @param {Date} options.dated - Day to add event * @param {Date} options.dated - Day to add event
* @param {String} options.title - Tooltip description * @param {String} options.name - Tooltip description
* @param {String} options.className - ClassName style * @param {String} options.className - ClassName style
* @param {Object} options.style - Style properties * @param {Object} options.style - Style properties
* @param {Boolean} options.isRemovable - True if is removable by users * @param {Boolean} options.isRemovable - True if is removable by users
@ -194,12 +225,7 @@ export default class Calendar extends Component {
options.dated = new Date(options.dated); options.dated = new Date(options.dated);
options.dated.setHours(0, 0, 0, 0); options.dated.setHours(0, 0, 0, 0);
const event = this.events.findIndex(event => { this.events.push(options);
return event.dated >= options.dated && event.dated <= options.dated;
});
if (event < 0)
this.events.push(options);
} }
/** /**
@ -274,6 +300,16 @@ export default class Calendar extends Component {
} }
this.emit('selection', {values: selected}); this.emit('selection', {values: selected});
} }
renderStyle(style) {
if (style) {
return {
'background-color': style.backgroundColor,
'font-weight': style.fontWeight,
'color': style.color
};
}
}
} }
Calendar.$inject = ['$element', '$scope']; Calendar.$inject = ['$element', '$scope'];

View File

@ -20,8 +20,8 @@ describe('Component vnCalendar', () => {
let currentDate = new Date().toString(); let currentDate = new Date().toString();
controller.data = [ controller.data = [
{dated: currentDate, title: 'Event 1'}, {dated: currentDate, name: 'Event 1'},
{dated: currentDate, title: 'Event 2'}, {dated: currentDate, name: 'Event 2'},
]; ];
expect(controller.events[0].dated instanceof Object).toBeTruthy(); expect(controller.events[0].dated instanceof Object).toBeTruthy();
@ -34,12 +34,11 @@ describe('Component vnCalendar', () => {
controller.events = []; controller.events = [];
controller.addEvent({ controller.addEvent({
dated: new Date(), dated: new Date(),
title: 'My event', name: 'My event'
className: 'color'
}); });
const firstEvent = controller.events[0]; const firstEvent = controller.events[0];
expect(firstEvent.title).toEqual('My event'); expect(firstEvent.name).toEqual('My event');
expect(firstEvent.isRemovable).toBeDefined(); expect(firstEvent.isRemovable).toBeDefined();
expect(firstEvent.isRemovable).toBeTruthy(); expect(firstEvent.isRemovable).toBeTruthy();
}); });
@ -50,19 +49,17 @@ describe('Component vnCalendar', () => {
controller.events = [{ controller.events = [{
dated: curDate, dated: curDate,
title: 'My event 1', name: 'My event 1'
className: 'color'
}]; }];
controller.addEvent({ controller.addEvent({
dated: curDate, dated: curDate,
title: 'My event 2', name: 'My event 2'
className: 'color'
}); });
const firstEvent = controller.events[0]; const firstEvent = controller.events[0];
expect(controller.events.length).toEqual(1); expect(controller.events.length).toEqual(2);
expect(firstEvent.title).toEqual('My event 1'); expect(firstEvent.name).toEqual('My event 1');
}); });
}); });
@ -71,7 +68,7 @@ describe('Component vnCalendar', () => {
const curDate = new Date(); const curDate = new Date();
controller._events = [{ controller._events = [{
dated: curDate, dated: curDate,
title: 'My event 1', name: 'My event 1',
className: 'color' className: 'color'
}]; }];
controller.removeEvent(curDate); controller.removeEvent(curDate);

View File

@ -1,143 +1,123 @@
@import "variables"; @import "variables";
vn-calendar.small {
.events {
display: none
}
}
vn-calendar { vn-calendar {
display: block; display: block;
max-width: 250px;
.header vn-one { .header vn-one {
text-align: center; text-align: center;
padding: 0.2em 0 padding: 0.2em 0;
height: 1.5em
} }
.body {
.days { .weekdays {
justify-content: flex-start; color: $color-font-secondary;
align-items: flex-start; margin-bottom: 0.5em;
flex-wrap: wrap; padding: 0.5em 0;
font-weight: bold;
font-size: 0.8em;
}
.weekdays section {
cursor: pointer
}
.weekdays section, .day {
position: relative;
text-align: center;
box-sizing: border-box;
width: 14.28%;
outline: 0;
}
.days {
justify-content: flex-start;
align-items: flex-start;
flex-wrap: wrap;
}
.day {
.content {
position: absolute;
bottom: 0;
right: 0;
left: 0;
top: 0
} }
.weekdays { .day-number {
border-bottom: 1px solid $color-hover-cd; transition: background-color 0.3s;
border-top: 1px solid $color-hover-cd; text-align:center;
color: $color-font-secondary; float:inline-end;
font-weight: bold margin: 0 auto;
border-radius: 50%;
font-size: 0.85em;
width:2.2em;
height: 1.2em;
padding: 0.5em 0;
cursor: pointer;
outline: 0
} }
.day { .day-number:hover {
box-sizing: border-box; background-color: lighten($color-font-secondary, 20%);
padding: 0.1em; opacity: 0.8
width: 14.2857143%;
line-height: 1.5em;
outline: 0;
span {
transition: background-color 0.3s;
text-align: center;
font-size: .8em;
border-radius: 50%;
display: block;
padding: 0.2em;
cursor: pointer
}
} }
}
.day:hover span { .day::after {
background-color: #DDD content: "";
display: block;
padding-top: 100%;
}
.day.primary .day-number {
background-color: $color-main;
color: $color-font-bg;
}
.events {
margin-top: 0.5em;
font-size: 0.6em
}
.events {
color: $color-font-secondary;
.event {
margin-bottom: .1em;
} }
}
.day.gray { .chip {
background-color: $color-main;
color: $color-font-bg;
display: inline-block;
border-radius: .3em;
padding: 0.3em .8em;
max-width: 5em;
}
.day.gray {
.day-number {
color: $color-font-secondary color: $color-font-secondary
} }
}
.day.orange {
font-weight: bold;
color: $color-main;
}
.day.orange-circle {
color: $color-font;
& > span {
background-color: $color-main
}
}
.day.orange-circle:hover { .day.sunday {
& > span { .day-number {
background-color: $color-main-medium color: $color-alert;
} font-weight: bold
}
.day.light-orange {
color: $color-main-medium
}
.day.green {
font-weight: bold;
color: $color-success;
}
.day.green-circle {
color: $color-font;
& > span {
background-color: $color-success
}
}
.day.green-circle:hover {
& > span {
background-color: $color-success-medium
}
}
.day.light-green {
font-weight: bold;
color: $color-success-medium
}
.day.blue {
font-weight: bold;
color: $color-notice;
}
.day.blue-circle {
color: $color-font;
& > span {
background-color: $color-notice
}
}
.day.blue-circle:hover {
& > span {
background-color: $color-notice-medium
}
}
.day.light-blue {
font-weight: bold;
color: $color-notice-medium
}
.day.red {
font-weight: bold;
color: $color-alert
}
.day.red-circle {
color: $color-font;
& > span {
background-color: $color-alert
}
}
.day.red-circle:hover {
& > span {
background-color: $color-alert-medium
}
}
.day.light-red {
font-weight: bold;
color: $color-alert-medium;
} }
} }
} }

View File

@ -45,8 +45,7 @@ export default class Dialog extends Component {
this.element.style.display = 'flex'; this.element.style.display = 'flex';
this.transitionTimeout = setTimeout(() => this.$element.addClass('shown'), 30); this.transitionTimeout = setTimeout(() => this.$element.addClass('shown'), 30);
if (this.onOpen) this.emit('open');
this.onOpen();
} }
/** /**
@ -55,6 +54,7 @@ export default class Dialog extends Component {
hide() { hide() {
this.fireResponse(); this.fireResponse();
this.realHide(); this.realHide();
this.emit('close');
} }
/** /**
@ -120,7 +120,6 @@ ngModule.component('vnDialog', {
buttons: '?tplButtons' buttons: '?tplButtons'
}, },
bindings: { bindings: {
onOpen: '&?',
onResponse: '&?' onResponse: '&?'
}, },
controller: Dialog controller: Dialog

View File

@ -7,7 +7,7 @@ describe('Component vnDialog', () => {
beforeEach(angular.mock.inject($componentController => { beforeEach(angular.mock.inject($componentController => {
$element = angular.element('<vn-dialog></vn-dialog>'); $element = angular.element('<vn-dialog></vn-dialog>');
controller = $componentController('vnDialog', {$element, $transclude: null}); controller = $componentController('vnDialog', {$element, $transclude: null});
controller.onOpen = jasmine.createSpy('onOpen'); controller.emit = jasmine.createSpy('emit');
})); }));
describe('show()', () => { describe('show()', () => {
@ -17,15 +17,15 @@ describe('Component vnDialog', () => {
controller.show(); controller.show();
expect(controller.element.style.display).toEqual('none'); expect(controller.element.style.display).toEqual('none');
expect(controller.onOpen).not.toHaveBeenCalledWith(); expect(controller.emit).not.toHaveBeenCalledWith('open');
}); });
it(`should set shown on the controller, set style.display on the element and call onOpen()`, () => { it(`should set shown on the controller, set style.display on the element and emit onOpen() event`, () => {
controller.show(); controller.show();
expect(controller.element.style.display).toEqual('flex'); expect(controller.element.style.display).toEqual('flex');
expect(controller.shown).toBeTruthy(); expect(controller.shown).toBeTruthy();
expect(controller.onOpen).toHaveBeenCalledWith(); expect(controller.emit).toHaveBeenCalledWith('open');
}); });
}); });

View File

@ -20,7 +20,7 @@ import './multi-check/multi-check';
import './date-picker/date-picker'; import './date-picker/date-picker';
import './button/button'; import './button/button';
import './check/check'; import './check/check';
import './radio/radio'; import './radio-group/radio-group';
import './textarea/textarea'; import './textarea/textarea';
import './icon-button/icon-button'; import './icon-button/icon-button';
import './submit/submit'; import './submit/submit';

View File

@ -22,7 +22,10 @@
ng-focus="$ctrl.hasFocus = true" ng-focus="$ctrl.hasFocus = true"
ng-blur="$ctrl.hasFocus = false" ng-blur="$ctrl.hasFocus = false"
tabindex="{{$ctrl.input.tabindex}}"/> tabindex="{{$ctrl.input.tabindex}}"/>
<label class="label" translate>{{::$ctrl.label}}</label> <label class="label">
<span translate>{{::$ctrl.label}}</span>
<span translate ng-show="::$ctrl.required">*</span>
</label>
</div> </div>
<div class="underline"></div> <div class="underline"></div>
<div class="selected underline"></div> <div class="selected underline"></div>

View File

@ -192,6 +192,7 @@ ngModule.component('vnInputNumber', {
label: '@?', label: '@?',
name: '@?', name: '@?',
disabled: '<?', disabled: '<?',
required: '@?',
min: '<?', min: '<?',
max: '<?', max: '<?',
step: '<?', step: '<?',

View File

@ -0,0 +1,8 @@
<md-radio-group ng-model="$ctrl.model">
<md-radio-button aria-label="::option.label"
ng-repeat="option in $ctrl.options"
ng-value="option.value"
ng-disabled="$ctrl.disabled">
<span translate>{{::option.label}}</span>
</md-radio-button>
</md-radio-group>

View File

@ -0,0 +1,41 @@
import ngModule from '../../module';
import Component from '../../lib/component';
import './style.scss';
export default class Controller extends Component {
constructor($element, $scope, $attrs) {
super($element, $scope);
this.hasInfo = Boolean($attrs.info);
this.info = $attrs.info || null;
}
get model() {
return this._model;
}
set model(value) {
this._model = value;
}
get field() {
return this._model;
}
set field(value) {
this._model = value;
}
}
Controller.$inject = ['$element', '$scope', '$attrs'];
ngModule.component('vnRadioGroup', {
template: require('./radio-group.html'),
controller: Controller,
bindings: {
field: '=?',
options: '<?',
disabled: '<?',
checked: '<?'
}
});

View File

@ -0,0 +1,11 @@
@import "variables";
md-radio-group md-radio-button.md-checked .md-container {
.md-on {
background-color: $color-main
}
.md-off {
border-color: $color-main
}
}

View File

@ -1,7 +0,0 @@
<input
type="radio"
class="*[className]*"
name="*[name]*"
ng-model="*[model]*.*[name]*"
*[enabled]*>
<span class="mdl-radio__label" translate>*[text]*</span>

View File

@ -1,15 +0,0 @@
import ngModule from '../../module';
import template from './radio.html';
directive.$inject = ['vnTemplate'];
export default function directive(vnTemplate) {
return {
restrict: 'E',
template: (_, $attrs) =>
vnTemplate.get(template, $attrs, {
enabled: 'true',
className: 'mdl-radio mdl-js-radio mdl-js-ripple-effect'
})
};
}
ngModule.directive('vnRadio', directive);

View File

@ -16,7 +16,7 @@
tabindex="{{$ctrl.input.tabindex}}"/> tabindex="{{$ctrl.input.tabindex}}"/>
<label class="label"> <label class="label">
<span translate>{{::$ctrl.label}}</span> <span translate>{{::$ctrl.label}}</span>
<span translate ng-show="::$ctrl.required">(*)</span> <span translate vn-tooltip="Required" ng-show="::$ctrl.required">*</span>
</label> </label>
</div> </div>
<div class="underline"></div> <div class="underline"></div>

View File

@ -52,6 +52,8 @@ $color-hover-cd: rgba(0, 0, 0, .1);
$color-hover-dc: .7; $color-hover-dc: .7;
$color-disabled: .6; $color-disabled: .6;
$color-font-link-medium: lighten($color-font-link, 20%);
$color-font-link-light: lighten($color-font-link, 35%);
$color-main-medium: lighten($color-main, 20%); $color-main-medium: lighten($color-main, 20%);
$color-main-light: lighten($color-main, 35%); $color-main-light: lighten($color-main, 35%);
$color-success-medium: lighten($color-success, 20%); $color-success-medium: lighten($color-success, 20%);

View File

@ -0,0 +1,81 @@
const mergeFilters = require('vn-loopback/util/filter').mergeFilters;
module.exports = Self => {
Self.remoteMethod('editPrices', {
description: 'Changes the price and bonus of a delivery day',
accessType: 'WRITE',
accepts: [{
arg: 'id',
type: 'Number',
description: 'The zone id',
http: {source: 'path'}
},
{
arg: 'delivered',
type: 'Date',
required: true,
},
{
arg: 'price',
type: 'Number',
required: true,
},
{
arg: 'bonus',
type: 'Number',
required: true,
},
{
arg: 'option',
type: 'String',
required: true,
}],
returns: {
type: 'object',
root: true
},
http: {
path: `/:id/editPrices`,
verb: 'POST'
}
});
Self.editPrices = async(id, delivered, price, bonus, option) => {
const models = Self.app.models;
let filter = {
where: {
zoneFk: id
}
};
let where;
let shouldPropagate = true;
if (option == 'Only this day') {
shouldPropagate = false;
where = {delivered};
} else if (option == 'From this day') {
where = {
delivered: {
gte: delivered
}
};
}
filter = mergeFilters(filter, {where});
const days = await models.ZoneCalendar.find(filter);
const areAllFromSameZone = days.every(day => day.zoneFk === id);
if (!areAllFromSameZone)
throw new UserError('All delivery days must belong to the same zone');
if (shouldPropagate) {
const zone = await models.Zone.findById(id);
zone.updateAttributes({price, bonus});
}
return models.ZoneCalendar.updateAll(filter.where, {price, bonus});
};
};

View File

@ -0,0 +1,84 @@
const app = require('vn-loopback/server/server');
describe('agency editPrices()', () => {
const zoneId = 1;
let originalZone;
beforeAll(async done => {
originalZone = await app.models.Zone.findById(zoneId);
done();
});
afterAll(async done => {
await await app.models.ZoneCalendar.updateAll({zoneFk: zoneId}, {
price: originalZone.price,
bonus: originalZone.bonus
});
done();
});
it('should apply price and bonus for a selected day', async() => {
const delivered = new Date();
delivered.setHours(0, 0, 0, 0);
delivered.setDate(delivered.getDate() + 1);
await app.models.Zone.editPrices(zoneId, delivered, 4.00, 2.00, 'Only this day');
const editedDays = await app.models.ZoneCalendar.find({
where: {
zoneFk: zoneId,
delivered: delivered
}
});
const firstEditedDay = editedDays[0];
expect(editedDays.length).toEqual(1);
expect(firstEditedDay.price).toEqual(4.00);
expect(firstEditedDay.bonus).toEqual(2.00);
});
it('should apply price and bonus for all delivery days starting from selected day', async() => {
const delivered = new Date();
delivered.setHours(0, 0, 0, 0);
delivered.setDate(delivered.getDate() + 1);
await app.models.Zone.editPrices(1, delivered, 5.50, 1.00, 'From this day');
const editedDays = await app.models.ZoneCalendar.find({
where: {
zoneFk: zoneId,
delivered: {
gte: delivered
}
}
});
const firstEditedDay = editedDays[0];
const lastEditedDay = editedDays[editedDays.length - 1];
expect(editedDays.length).toEqual(4);
expect(firstEditedDay.price).toEqual(5.50);
expect(firstEditedDay.bonus).toEqual(1.00);
expect(lastEditedDay.price).toEqual(5.50);
expect(lastEditedDay.bonus).toEqual(1.00);
});
it('should apply price and bonus for all delivery days', async() => {
const delivered = new Date();
delivered.setHours(0, 0, 0, 0);
delivered.setDate(delivered.getDate() + 1);
await app.models.Zone.editPrices(1, delivered, 7.00, 0.00, 'All days');
const editedDays = await app.models.ZoneCalendar.find({
where: {
zoneFk: zoneId
}
});
const firstEditedDay = editedDays[0];
const lastEditedDay = editedDays[editedDays.length - 1];
expect(editedDays.length).toEqual(5);
expect(firstEditedDay.price).toEqual(7.00);
expect(firstEditedDay.bonus).toEqual(0.00);
expect(lastEditedDay.price).toEqual(7.00);
expect(lastEditedDay.bonus).toEqual(0.00);
});
});

View File

@ -14,6 +14,12 @@
"delivered": { "delivered": {
"id": true, "id": true,
"type": "Date" "type": "Date"
},
"price": {
"type": "Number"
},
"bonus": {
"type": "Number"
} }
}, },
"relations": { "relations": {

View File

@ -1,5 +1,6 @@
module.exports = Self => { module.exports = Self => {
require('../methods/zone/clone')(Self); require('../methods/zone/clone')(Self);
require('../methods/zone/editPrices')(Self);
Self.validatesPresenceOf('warehouseFk', { Self.validatesPresenceOf('warehouseFk', {
message: `Warehouse cannot be blank` message: `Warehouse cannot be blank`

View File

@ -1,21 +1,74 @@
<vn-crud-model <vn-crud-model
vn-id="model" vn-id="model"
url="/agency/api/ZoneCalendars" url="/agency/api/ZoneCalendars"
fields="['zoneFk', 'delivered']"
link="{zoneFk: $ctrl.$stateParams.id}" link="{zoneFk: $ctrl.$stateParams.id}"
data="$ctrl.data" data="$ctrl.data"
primary-key="zoneFk" primary-key="zoneFk"
auto-load="true"> auto-load="true">
</vn-crud-model> </vn-crud-model>
<vn-calendar pad-small vn-id="stMonth" skip="2" <vn-watcher
data="$ctrl.events" vn-id="watcher"
on-selection="$ctrl.onSelection(stMonth, values)" data="$ctrl.selectedDay">
on-move-next="$ctrl.onMoveNext(ndMonth)" </vn-watcher>
on-move-previous="$ctrl.onMovePrevious(ndMonth)">
</vn-calendar> <vn-card pad-large>
<vn-calendar pad-small vn-id="ndMonth" skip="2" <vn-horizontal pad-medium style="justify-content: space-between;">
data="$ctrl.events" <vn-icon-button icon="keyboard_arrow_left"
display-controls="false" ng-click="$ctrl.onMovePrevious([stMonth, ndMonth])"
on-selection="$ctrl.onSelection(ndMonth, values)" vn-tooltip="Previous">
default-date="$ctrl.ndMonthDate"> </vn-icon-button>
</vn-calendar> <vn-icon-button icon="keyboard_arrow_right"
ng-click="$ctrl.onMoveNext([stMonth, ndMonth])"
vn-tooltip="Next">
</vn-icon-button>
</vn-horizontal>
<vn-horizontal>
<vn-calendar vn-id="stMonth" vn-one pad-medium
data="$ctrl.events"
display-controls="false"
on-selection="$ctrl.onSelection(values)"
skip="2">
</vn-calendar>
<vn-calendar vn-id="ndMonth" vn-one pad-medium
data="$ctrl.events"
display-controls="false"
on-selection="$ctrl.onSelection(values)"
default-date="$ctrl.ndMonthDate"
skip="2">
</vn-calendar>
</vn-horizontal>
</vn-card>
<!-- Edit price dialog -->
<vn-dialog class="edit"
vn-id="priceDialog"
on-close="$ctrl.onClose()"
on-response="$ctrl.onResponse(response)">
<tpl-body>
<h5 pad-small-v translate>Edit price</h5>
<vn-horizontal>
<vn-input-number vn-one
label="Price"
model="$ctrl.selectedDay.price"
min="0" step="0.01"
required="true">
</vn-input-number>
<vn-input-number vn-one
label="Bonus"
model="$ctrl.selectedDay.bonus"
min="0" step="0.01"
required="true">
</vn-input-number>
</vn-horizontal>
<vn-horizontal>
<vn-radio-group vn-one
field="$ctrl.selectedDay.option"
options="$ctrl.options">
</vn-radio-group>
</vn-horizontal>
</tpl-body>
<tpl-buttons>
<input type="button" response="CANCEL" translate-attr="{value: 'Cancel'}"/>
<button response="ACCEPT" translate>Save</button>
</tpl-buttons>
</vn-dialog>

View File

@ -1,49 +1,26 @@
import ngModule from '../module'; import ngModule from '../module';
import './style.scss';
class Controller { class Controller {
constructor($element, $scope, $stateParams, $http) { constructor($element, $scope, $http, $filter, $translate, $stateParams, vnApp) {
this.$element = $element; this.$element = $element;
this.$stateParams = $stateParams; this.$ = $scope;
this.$scope = $scope;
this.$http = $http; this.$http = $http;
this.$filter = $filter;
this.$translate = $translate;
this.$stateParams = $stateParams;
this.vnApp = vnApp;
this.stMonthDate = new Date(); this.stMonthDate = new Date();
this.ndMonthDate = new Date(); this.ndMonthDate = new Date();
this.ndMonthDate.setMonth(this.ndMonthDate.getMonth() + 1); this.ndMonthDate.setMonth(this.ndMonthDate.getMonth() + 1);
this.selectedDay = {};
} }
$postLink() { $postLink() {
this.stMonth = this.$scope.stMonth; this.stMonth = this.$.stMonth;
this.ndMonth = this.$scope.ndMonth; this.ndMonth = this.$.ndMonth;
} }
// Disabled until implementation
// of holidays by node
/* get zone() {
return this._zone;
}
set zone(value) {
this._zone = value;
if (!value) return;
let query = '/agency/api/LabourHolidays/getByWarehouse';
this.$http.get(query, {params: {warehouseFk: value.warehouseFk}}).then(res => {
if (!res.data) return;
const events = [];
res.data.forEach(holiday => {
events.push({
date: holiday.dated,
className: 'red',
title: holiday.description || holiday.name,
isRemovable: false
});
});
this.events = this.events.concat(events);
});
} */
get data() { get data() {
return this._data; return this._data;
} }
@ -52,97 +29,106 @@ class Controller {
this._data = value; this._data = value;
if (!value) return; if (!value) return;
const events = []; const events = [];
value.forEach(event => { value.forEach(event => {
events.push({ events.push({
name: `P: ${this.$filter('currency')(event.price)}`,
description: 'Price',
dated: event.delivered, dated: event.delivered,
className: 'green-circle', style: {backgroundColor: '#a3d131'},
title: 'Has delivery' data: {price: event.price}
});
events.push({
name: `B: ${this.$filter('currency')(event.bonus)}`,
description: 'Bonus',
dated: event.delivered,
data: {bonus: event.bonus}
}); });
}); });
this.events = events; this.events = events;
} }
onSelection(calendar, values) { onSelection(values) {
let totalEvents = 0; if (values.length > 1) return false;
values.forEach(day => {
const exists = calendar.events.findIndex(event => {
return event.dated >= day.dated && event.dated <= day.dated
&& event.isRemovable;
});
if (exists > -1) totalEvents++; this.options = [
{label: 'Only this day', value: 'Only this day'},
{label: 'From this day', value: 'From this day'},
{label: 'All days', value: 'All days'}
];
const selection = values[0];
const events = selection.events;
const hasEvents = events.length > 0;
if (!hasEvents)
return this.vnApp.showMessage(this.$translate.instant(`There's no delivery for this day`));
this.selectedDay = {
delivered: selection.dated,
option: 'Only this day'
};
events.forEach(event => {
this.selectedDay = Object.assign(this.selectedDay, event.data);
}); });
this.$.priceDialog.show();
if (totalEvents > (values.length / 2))
this.removeEvents(calendar, values);
else
this.insertEvents(calendar, values);
} }
insertEvents(calendar, days) { onResponse(response) {
days.forEach(day => { if (response == 'ACCEPT') {
const event = calendar.events.find(event => { try {
return event.dated >= day.dated && event.dated <= day.dated; const data = {
}); delivered: this.selectedDay.delivered,
price: this.selectedDay.price,
bonus: this.selectedDay.bonus,
option: this.selectedDay.option
};
if (event) return false; this.$.watcher.check();
this.$scope.model.insert({ const path = `/api/Zones/${this.zone.id}/editPrices`;
zoneFk: this.zone.id, this.$http.post(path, data).then(() => {
delivered: day.dated this.vnApp.showSuccess(this.$translate.instant('Data saved!'));
}); this.$.model.refresh();
this.card.reload();
calendar.addEvent({ });
dated: day.dated, } catch (e) {
className: 'green-circle', this.vnApp.showError(this.$translate.instant(e.message));
title: 'Has delivery'
});
});
this.$scope.model.save().then(() => {
this.events = calendar.events;
});
}
removeEvents(calendar, days) {
let dates = [];
days.forEach(day => {
const event = calendar.events.find(event => {
return event.dated >= day.dated && event.dated <= day.dated;
});
if (event && !event.isRemovable)
return false; return false;
}
}
dates.push(day.dated); return this.onClose();
}
calendar.removeEvent(day.dated); onClose() {
}); this.$.watcher.updateOriginalData();
}
if (dates.length == 0) return; onMoveNext(calendars) {
const params = {zoneFk: this.zone.id, dates}; calendars.forEach(calendar => {
this.$http.post('/agency/api/zoneCalendars/removeByDate', params).then(() => { calendar.moveNext(2);
this.events = calendar.events;
}); });
} }
onMoveNext(calendar) { onMovePrevious(calendars) {
calendar.moveNext(2); calendars.forEach(calendar => {
} calendar.movePrevious(2);
});
onMovePrevious(calendar) {
calendar.movePrevious(2);
} }
} }
Controller.$inject = ['$element', '$scope', '$stateParams', '$http']; Controller.$inject = ['$element', '$scope', '$http', '$filter', '$translate', '$stateParams', 'vnApp'];
ngModule.component('vnZoneCalendar', { ngModule.component('vnZoneCalendar', {
template: require('./index.html'), template: require('./index.html'),
controller: Controller, controller: Controller,
bindings: { bindings: {
zone: '<' zone: '<'
},
require: {
card: '^vnZoneCard'
} }
}); });

View File

@ -0,0 +1,6 @@
Prices: Precios
Edit price: Modificar precio
Only this day: Solo este día
From this day: A partir de este día
All days: Todos los días
There's no delivery for this day: No hay reparto para este día

View File

@ -0,0 +1,3 @@
vn-calendar:nth-child(2n + 1) {
border-right:1px solid #ddd
}

View File

@ -8,4 +8,5 @@ import './search-panel';
import './create'; import './create';
import './basic-data'; import './basic-data';
import './location'; import './location';
import './location/calendar';
import './calendar'; import './calendar';

View File

@ -0,0 +1,21 @@
<vn-crud-model
vn-id="model"
url="/agency/api/ZoneCalendars"
fields="['zoneFk', 'delivered']"
link="{zoneFk: $ctrl.$stateParams.id}"
data="$ctrl.data"
primary-key="zoneFk"
auto-load="true">
</vn-crud-model>
<vn-calendar pad-small vn-id="stMonth" skip="2"
data="$ctrl.events"
on-selection="$ctrl.onSelection(values, stMonth)"
on-move-next="$ctrl.onMoveNext(ndMonth)"
on-move-previous="$ctrl.onMovePrevious(ndMonth)">
</vn-calendar>
<vn-calendar pad-small vn-id="ndMonth" skip="2"
data="$ctrl.events"
display-controls="false"
on-selection="$ctrl.onSelection(values, ndMonth)"
default-date="$ctrl.ndMonthDate">
</vn-calendar>

View File

@ -0,0 +1,150 @@
import ngModule from '../module';
class Controller {
constructor($element, $scope, $stateParams, $http) {
this.$element = $element;
this.$stateParams = $stateParams;
this.$scope = $scope;
this.$http = $http;
this.stMonthDate = new Date();
this.ndMonthDate = new Date();
this.ndMonthDate.setMonth(this.ndMonthDate.getMonth() + 1);
}
$postLink() {
this.stMonth = this.$scope.stMonth;
this.ndMonth = this.$scope.ndMonth;
}
// Disabled until implementation
// of holidays by node
/* get zone() {
return this._zone;
}
set zone(value) {
this._zone = value;
if (!value) return;
let query = '/agency/api/LabourHolidays/getByWarehouse';
this.$http.get(query, {params: {warehouseFk: value.warehouseFk}}).then(res => {
if (!res.data) return;
const events = [];
res.data.forEach(holiday => {
events.push({
date: holiday.dated,
className: 'red',
title: holiday.description || holiday.name,
isRemovable: false
});
});
this.events = this.events.concat(events);
});
} */
get data() {
return this._data;
}
set data(value) {
this._data = value;
if (!value) return;
const events = [];
value.forEach(event => {
events.push({
name: 'Has delivery',
dated: event.delivered,
style: {backgroundColor: '#a3d131'}
});
});
this.events = events;
}
onSelection(values, calendar) {
let totalEvents = 0;
values.forEach(day => {
const exists = calendar.events.findIndex(event => {
return event.dated >= day.dated && event.dated <= day.dated
&& event.isRemovable;
});
if (exists > -1) totalEvents++;
});
if (totalEvents > (values.length / 2))
this.removeEvents(calendar, values);
else
this.insertEvents(calendar, values);
}
insertEvents(calendar, days) {
days.forEach(day => {
const event = calendar.events.find(event => {
return event.dated >= day.dated && event.dated <= day.dated;
});
if (event) return false;
this.$scope.model.insert({
zoneFk: this.zone.id,
delivered: day.dated,
price: this.zone.price,
bonus: this.zone.bonus
});
calendar.addEvent({
name: 'Has delivery',
dated: day.dated,
style: {backgroundColor: '#a3d131'}
});
});
this.$scope.model.save().then(() => {
this.events = calendar.events;
});
}
removeEvents(calendar, days) {
let dates = [];
days.forEach(day => {
const event = calendar.events.find(event => {
return event.dated >= day.dated && event.dated <= day.dated;
});
if (event && !event.isRemovable)
return false;
dates.push(day.dated);
calendar.removeEvent(day.dated);
});
if (dates.length == 0) return;
const params = {zoneFk: this.zone.id, dates};
this.$http.post('/agency/api/zoneCalendars/removeByDate', params).then(() => {
this.events = calendar.events;
});
}
onMoveNext(calendar) {
calendar.moveNext(2);
}
onMovePrevious(calendar) {
calendar.movePrevious(2);
}
}
Controller.$inject = ['$element', '$scope', '$stateParams', '$http'];
ngModule.component('vnZoneLocationCalendar', {
template: require('./calendar.html'),
controller: Controller,
bindings: {
zone: '<'
}
});

View File

@ -18,6 +18,6 @@
</vn-treeview> </vn-treeview>
</vn-card> </vn-card>
<vn-side-menu side="right"> <vn-side-menu side="right">
<vn-zone-calendar zone="::$ctrl.zone"></vn-zone-calendar> <vn-zone-location-calendar zone="::$ctrl.zone"></vn-zone-location-calendar>
</vn-side-menu> </vn-side-menu>
</div> </div>

View File

@ -6,7 +6,8 @@
"dependencies": ["worker"], "dependencies": ["worker"],
"menu": [ "menu": [
{"state": "zone.card.basicData", "icon": "settings"}, {"state": "zone.card.basicData", "icon": "settings"},
{"state": "zone.card.location", "icon": "my_location"} {"state": "zone.card.location", "icon": "my_location"},
{"state": "zone.card.calendar"}
], ],
"routes": [ "routes": [
{ {
@ -15,27 +16,31 @@
"abstract": true, "abstract": true,
"component": "ui-view", "component": "ui-view",
"description": "Zones" "description": "Zones"
}, { },
{
"url": "/index?q", "url": "/index?q",
"state": "zone.index", "state": "zone.index",
"component": "vn-zone-index", "component": "vn-zone-index",
"description": "Zones" "description": "Zones"
}, { },
{
"url": "/create", "url": "/create",
"state": "zone.create", "state": "zone.create",
"component": "vn-zone-create", "component": "vn-zone-create",
"description": "New zone" "description": "New zone"
}, { },
{
"url": "/:id", "url": "/:id",
"state": "zone.card", "state": "zone.card",
"component": "vn-zone-card", "component": "vn-zone-card",
"abstract": true, "abstract": true,
"description": "Detail" "description": "Detail"
}, { },
"url": "/location?q", {
"state": "zone.card.location", "url": "/summary",
"component": "vn-zone-location", "state": "zone.card.summary",
"description": "Locations", "component": "vn-zone-summary",
"description": "Summary",
"params": { "params": {
"zone": "$ctrl.zone" "zone": "$ctrl.zone"
} }
@ -50,10 +55,19 @@
} }
}, },
{ {
"url": "/summary", "url": "/location?q",
"state": "zone.card.summary", "state": "zone.card.location",
"component": "vn-zone-summary", "component": "vn-zone-location",
"description": "Summary", "description": "Locations",
"params": {
"zone": "$ctrl.zone"
}
},
{
"url": "/calendar",
"state": "zone.card.calendar",
"component": "vn-zone-calendar",
"description": "Prices",
"params": { "params": {
"zone": "$ctrl.zone" "zone": "$ctrl.zone"
} }

View File

@ -3,7 +3,7 @@
data="$ctrl.absenceTypes" auto-load="true"> data="$ctrl.absenceTypes" auto-load="true">
</vn-crud-model> </vn-crud-model>
<div class="main-with-right-menu"> <div class="main-with-right-menu">
<vn-card compact pad-large> <vn-card pad-large>
<vn-horizontal class="calendar-list"> <vn-horizontal class="calendar-list">
<section class="calendar" ng-repeat="month in $ctrl.months"> <section class="calendar" ng-repeat="month in $ctrl.months">
<vn-calendar <vn-calendar

View File

@ -45,10 +45,11 @@ class Controller {
const holidayName = holidayDetail || holidayType; const holidayName = holidayDetail || holidayType;
events.push({ events.push({
name: holidayName,
description: holidayName,
dated: holiday.dated, dated: holiday.dated,
className: 'red', isRemovable: false,
title: holidayName, style: {backgroundColor: '#FFFF00'}
isRemovable: false
}); });
}); });
this.events = this.events.concat(events); this.events = this.events.concat(events);
@ -62,11 +63,10 @@ class Controller {
absences.forEach(absence => { absences.forEach(absence => {
const absenceType = absence.absenceType; const absenceType = absence.absenceType;
events.push({ events.push({
name: absenceType.name,
description: absenceType.name,
dated: absence.dated, dated: absence.dated,
title: absenceType.name, style: {backgroundColor: absenceType.rgb}
style: {
background: absenceType.rgb
}
}); });
}); });
this.events = this.events.concat(events); this.events = this.events.concat(events);

View File

@ -93,7 +93,7 @@ describe('Worker', () => {
controller.setHolidays(data); controller.setHolidays(data);
expect(controller.events.length).toEqual(2); expect(controller.events.length).toEqual(2);
expect(controller.events[0].title).toEqual('New year'); expect(controller.events[0].name).toEqual('New year');
expect(controller.events[0].isRemovable).toEqual(false); expect(controller.events[0].isRemovable).toEqual(false);
}); });
}); });
@ -107,9 +107,9 @@ describe('Worker', () => {
controller.setWorkerCalendar(data); controller.setWorkerCalendar(data);
expect(controller.events.length).toEqual(2); expect(controller.events.length).toEqual(2);
expect(controller.events[0].title).toEqual('Holiday'); expect(controller.events[0].name).toEqual('Holiday');
expect(controller.events[0].style).toBeDefined(); expect(controller.events[0].style).toBeDefined();
expect(controller.events[1].title).toEqual('Leave'); expect(controller.events[1].name).toEqual('Leave');
expect(controller.events[1].style).toBeDefined(); expect(controller.events[1].style).toBeDefined();
}); });
}); });

View File

@ -1,5 +1,13 @@
@import "variables"; @import "variables";
.calendar-list .calendar {
border-bottom:1px solid #ddd
}
.calendar-list .calendar:nth-child(2n + 1) {
border-right:1px solid #ddd
}
.calendar-list { .calendar-list {
align-items: flex-start; align-items: flex-start;
flex-wrap: wrap; flex-wrap: wrap;
@ -9,7 +17,7 @@
box-sizing: border-box; box-sizing: border-box;
padding: $pad-medium; padding: $pad-medium;
overflow: hidden; overflow: hidden;
width: 20em width: 50%
} }
} }