mirror of
https://github.com/pretix/pretix.git
synced 2026-05-06 15:24:02 +00:00
Visualize custom check-in rules (#2053)
This commit is contained in:
@@ -1,484 +1,122 @@
|
||||
$(document).ready(function () {
|
||||
var TYPEOPS = {
|
||||
// Every change to our supported JSON logic must be done
|
||||
// * in pretix.base.services.checkin
|
||||
// * in pretix.base.models.checkin
|
||||
// * in checkinrules.js
|
||||
// * in libpretixsync
|
||||
'product': {
|
||||
'inList': {
|
||||
'label': gettext('is one of'),
|
||||
'cardinality': 2,
|
||||
}
|
||||
},
|
||||
'variation': {
|
||||
'inList': {
|
||||
'label': gettext('is one of'),
|
||||
'cardinality': 2,
|
||||
}
|
||||
},
|
||||
'datetime': {
|
||||
'isBefore': {
|
||||
'label': gettext('is before'),
|
||||
'cardinality': 2,
|
||||
},
|
||||
'isAfter': {
|
||||
'label': gettext('is after'),
|
||||
'cardinality': 2,
|
||||
},
|
||||
},
|
||||
'int': {
|
||||
'<': {
|
||||
'label': '<',
|
||||
'cardinality': 2,
|
||||
},
|
||||
'<=': {
|
||||
'label': '≤',
|
||||
'cardinality': 2,
|
||||
},
|
||||
'>': {
|
||||
'label': '>',
|
||||
'cardinality': 2,
|
||||
},
|
||||
'>=': {
|
||||
'label': '≥',
|
||||
'cardinality': 2,
|
||||
},
|
||||
'==': {
|
||||
'label': '=',
|
||||
'cardinality': 2,
|
||||
},
|
||||
'!=': {
|
||||
'label': '≠',
|
||||
'cardinality': 2,
|
||||
},
|
||||
},
|
||||
};
|
||||
var VARS = {
|
||||
'product': {
|
||||
'label': gettext('Product'),
|
||||
'type': 'product',
|
||||
},
|
||||
'variation': {
|
||||
'label': gettext('Product variation'),
|
||||
'type': 'variation',
|
||||
},
|
||||
'now': {
|
||||
'label': gettext('Current date and time'),
|
||||
'type': 'datetime',
|
||||
},
|
||||
'entries_number': {
|
||||
'label': gettext('Number of previous entries'),
|
||||
'type': 'int',
|
||||
},
|
||||
'entries_today': {
|
||||
'label': gettext('Number of previous entries since midnight'),
|
||||
'type': 'int',
|
||||
},
|
||||
'entries_days': {
|
||||
'label': gettext('Number of days with a previous entry'),
|
||||
'type': 'int',
|
||||
},
|
||||
};
|
||||
var TYPEOPS = {
|
||||
// Every change to our supported JSON logic must be done
|
||||
// * in pretix.base.services.checkin
|
||||
// * in pretix.base.models.checkin
|
||||
// * in checkinrules.js
|
||||
// * in libpretixsync
|
||||
'product': {
|
||||
'inList': {
|
||||
'label': gettext('is one of'),
|
||||
'cardinality': 2,
|
||||
}
|
||||
},
|
||||
'variation': {
|
||||
'inList': {
|
||||
'label': gettext('is one of'),
|
||||
'cardinality': 2,
|
||||
}
|
||||
},
|
||||
'datetime': {
|
||||
'isBefore': {
|
||||
'label': gettext('is before'),
|
||||
'cardinality': 2,
|
||||
},
|
||||
'isAfter': {
|
||||
'label': gettext('is after'),
|
||||
'cardinality': 2,
|
||||
},
|
||||
},
|
||||
'int': {
|
||||
'<': {
|
||||
'label': '<',
|
||||
'cardinality': 2,
|
||||
},
|
||||
'<=': {
|
||||
'label': '≤',
|
||||
'cardinality': 2,
|
||||
},
|
||||
'>': {
|
||||
'label': '>',
|
||||
'cardinality': 2,
|
||||
},
|
||||
'>=': {
|
||||
'label': '≥',
|
||||
'cardinality': 2,
|
||||
},
|
||||
'==': {
|
||||
'label': '=',
|
||||
'cardinality': 2,
|
||||
},
|
||||
'!=': {
|
||||
'label': '≠',
|
||||
'cardinality': 2,
|
||||
},
|
||||
},
|
||||
};
|
||||
var VARS = {
|
||||
'product': {
|
||||
'label': gettext('Product'),
|
||||
'type': 'product',
|
||||
},
|
||||
'variation': {
|
||||
'label': gettext('Product variation'),
|
||||
'type': 'variation',
|
||||
},
|
||||
'now': {
|
||||
'label': gettext('Current date and time'),
|
||||
'type': 'datetime',
|
||||
},
|
||||
'entries_number': {
|
||||
'label': gettext('Number of previous entries'),
|
||||
'type': 'int',
|
||||
},
|
||||
'entries_today': {
|
||||
'label': gettext('Number of previous entries since midnight'),
|
||||
'type': 'int',
|
||||
},
|
||||
'entries_days': {
|
||||
'label': gettext('Number of days with a previous entry'),
|
||||
'type': 'int',
|
||||
},
|
||||
};
|
||||
|
||||
Vue.component("datetimefield", {
|
||||
props: ["required", "value"],
|
||||
template: ('<input class="form-control">'),
|
||||
mounted: function () {
|
||||
var vm = this;
|
||||
var multiple = this.multiple;
|
||||
$(this.$el)
|
||||
.datetimepicker(this.opts())
|
||||
.trigger("change")
|
||||
.on("dp.change", function (e) {
|
||||
vm.$emit("input", $(this).data('DateTimePicker').date().toISOString());
|
||||
});
|
||||
if (!vm.value) {
|
||||
$(this.$el).data("DateTimePicker").viewDate(moment().hour(0).minute(0).second(0).millisecond(0));
|
||||
} else {
|
||||
$(this.$el).data("DateTimePicker").date(moment(vm.value));
|
||||
}
|
||||
Vue.component('checkin-rule', CheckinRule.default);
|
||||
var app = new Vue({
|
||||
el: '#rules-editor',
|
||||
components: {
|
||||
CheckinRulesEditor: CheckinRulesEditor.default,
|
||||
CheckinRulesVisualization: CheckinRulesVisualization.default,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
rules: {},
|
||||
TYPEOPS: TYPEOPS,
|
||||
VARS: VARS,
|
||||
texts: {
|
||||
and: gettext('All of the conditions below (AND)'),
|
||||
or: gettext('At least one of the conditions below (OR)'),
|
||||
date_from: gettext('Event start'),
|
||||
date_to: gettext('Event end'),
|
||||
date_admission: gettext('Event admission'),
|
||||
date_custom: gettext('custom time'),
|
||||
date_tolerance: gettext('Tolerance (minutes)'),
|
||||
condition_add: gettext('Add condition'),
|
||||
minutes: gettext('minutes'),
|
||||
},
|
||||
methods: {
|
||||
opts: function () {
|
||||
return {
|
||||
format: $("body").attr("data-datetimeformat"),
|
||||
locale: $("body").attr("data-datetimelocale"),
|
||||
useCurrent: false,
|
||||
showClear: this.required,
|
||||
icons: {
|
||||
time: 'fa fa-clock-o',
|
||||
date: 'fa fa-calendar',
|
||||
up: 'fa fa-chevron-up',
|
||||
down: 'fa fa-chevron-down',
|
||||
previous: 'fa fa-chevron-left',
|
||||
next: 'fa fa-chevron-right',
|
||||
today: 'fa fa-screenshot',
|
||||
clear: 'fa fa-trash',
|
||||
close: 'fa fa-remove'
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value: function (val) {
|
||||
$(this.$el).data('DateTimePicker').date(moment(val));
|
||||
},
|
||||
},
|
||||
destroyed: function () {
|
||||
$(this.$el)
|
||||
.off()
|
||||
.datetimepicker("destroy");
|
||||
hasRules: false,
|
||||
};
|
||||
},
|
||||
created: function () {
|
||||
this.rules = JSON.parse($("#id_rules").val());
|
||||
},
|
||||
watch: {
|
||||
rules: {
|
||||
deep: true,
|
||||
handler: function (newval) {
|
||||
$("#id_rules").val(JSON.stringify(newval));
|
||||
}
|
||||
});
|
||||
|
||||
Vue.component("lookup-select2", {
|
||||
props: ["required", "value", "placeholder", "url", "multiple"],
|
||||
template: ('<select>\n' +
|
||||
' <slot></slot>\n' +
|
||||
' </select>'),
|
||||
mounted: function () {
|
||||
var vm = this;
|
||||
var multiple = this.multiple;
|
||||
$(this.$el)
|
||||
.select2(this.opts())
|
||||
.val(this.value)
|
||||
.trigger("change")
|
||||
// emit event on change.
|
||||
.on("change", function (e) {
|
||||
vm.$emit("input", $(this).select2('data'));
|
||||
});
|
||||
if (vm.value) {
|
||||
for (var i = 0; i < vm.value["objectList"].length; i++) {
|
||||
var option = new Option(vm.value["objectList"][i]["lookup"][2], vm.value["objectList"][i]["lookup"][1], true, true);
|
||||
$(vm.$el).append(option);
|
||||
}
|
||||
}
|
||||
$(vm.$el).trigger("change");
|
||||
},
|
||||
methods: {
|
||||
opts: function () {
|
||||
return {
|
||||
theme: "bootstrap",
|
||||
delay: 100,
|
||||
width: '100%',
|
||||
multiple: true,
|
||||
allowClear: this.required,
|
||||
language: $("body").attr("data-select2-locale"),
|
||||
ajax: {
|
||||
url: this.url,
|
||||
data: function (params) {
|
||||
return {
|
||||
query: params.term,
|
||||
page: params.page || 1
|
||||
}
|
||||
}
|
||||
},
|
||||
templateResult: function (res) {
|
||||
if (!res.id) {
|
||||
return res.text;
|
||||
}
|
||||
var $ret = $("<span>").append(
|
||||
$("<span>").addClass("primary").append($("<div>").text(res.text).html())
|
||||
);
|
||||
return $ret;
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
placeholder: function (val) {
|
||||
$(this.$el).empty().select2(this.opts());
|
||||
this.build();
|
||||
},
|
||||
required: function (val) {
|
||||
$(this.$el).empty().select2(this.opts());
|
||||
this.build();
|
||||
},
|
||||
url: function (val) {
|
||||
$(this.$el).empty().select2(this.opts());
|
||||
this.build();
|
||||
},
|
||||
},
|
||||
destroyed: function () {
|
||||
$(this.$el)
|
||||
.off()
|
||||
.select2("destroy");
|
||||
}
|
||||
});
|
||||
|
||||
Vue.component('checkin-rule', {
|
||||
template: ('<div v-bind:class="classObject">'
|
||||
+ '<div class="btn-group pull-right">'
|
||||
+ '<button type="button" class="checkin-rule-remove btn btn-xs btn-default" @click.prevent="wrapWithOR">OR</button>'
|
||||
+ '<button type="button" class="checkin-rule-remove btn btn-xs btn-default" @click.prevent="wrapWithAND">AND</button> '
|
||||
+ '<button type="button" class="checkin-rule-remove btn btn-xs btn-default" @click.prevent="cutOut" v-if="operands && operands.length == 1 && (operator === \'or\' || operator == \'and\')"><span class="fa fa-cut"></span></button>'
|
||||
+ '<button type="button" class="checkin-rule-remove btn btn-xs btn-default" @click.prevent="remove" v-if="level > 0"><span class="fa fa-trash"></span></button>'
|
||||
+ '</div>'
|
||||
+ '<select v-bind:value="variable" v-on:input="setVariable" required class="form-control">'
|
||||
+ '<option value="and">' + gettext('All of the conditions below (AND)') + '</option>'
|
||||
+ '<option value="or">' + gettext('At least one of the conditions below (OR)') + '</option>'
|
||||
+ '<option v-for="(v, name) in vars" :value="name">{{ v.label }}</option>'
|
||||
+ '</select> '
|
||||
+ '<select v-bind:value="operator" v-on:input="setOperator" required class="form-control" v-if="operator !== \'or\' && operator !== \'and\'">'
|
||||
+ '<option></option>'
|
||||
+ '<option v-for="(v, name) in operators" :value="name">{{ v.label }}</option>'
|
||||
+ '</select> '
|
||||
+ '<select v-bind:value="timeType" v-on:input="setTimeType" required class="form-control" v-if="vartype == \'datetime\'">'
|
||||
+ '<option value="date_from">' + gettext('Event start') + '</option>'
|
||||
+ '<option value="date_to">' + gettext('Event end') + '</option>'
|
||||
+ '<option value="date_admission">' + gettext('Event admission') + '</option>'
|
||||
+ '<option value="custom">' + gettext('custom time') + '</option>'
|
||||
+ '</select> '
|
||||
+ '<datetimefield v-if="vartype == \'datetime\' && timeType == \'custom\'" :value="timeValue" v-on:input="setTimeValue"></datetimefield>'
|
||||
+ '<input class="form-control" required type="number" v-if="vartype == \'datetime\' && timeType && timeType != \'custom\'" v-bind:value="timeTolerance" v-on:input="setTimeTolerance" placeholder="' + gettext('Tolerance (minutes)') + '">'
|
||||
+ '<input class="form-control" required type="number" v-if="vartype == \'int\' && cardinality > 1" v-bind:value="rightoperand" v-on:input="setRightOperandNumber">'
|
||||
+ '<lookup-select2 required v-if="vartype == \'product\' && operator == \'inList\'" :multiple="true" :value="rightoperand" v-on:input="setRightOperandProductList" :url="productSelectURL"></lookup-select2>'
|
||||
+ '<lookup-select2 required v-if="vartype == \'variation\' && operator == \'inList\'" :multiple="true" :value="rightoperand" v-on:input="setRightOperandVariationList" :url="variationSelectURL"></lookup-select2>'
|
||||
+ '<div class="checkin-rule-childrules" v-if="operator === \'or\' || operator === \'and\'">'
|
||||
+ '<div v-for="(op, opi) in operands">'
|
||||
+ '<checkin-rule :rule="op" :index="opi" :level="level + 1" v-if="typeof op === \'object\'"></checkin-rule>'
|
||||
+ '</div>'
|
||||
+ '<button type="button" class="checkin-rule-addchild btn btn-xs btn-default" @click.prevent="addOperand"><span class="fa fa-plus-circle"></span> ' + gettext('Add condition') + '</button>'
|
||||
+ '</div>'
|
||||
+ '</div>'
|
||||
),
|
||||
computed: {
|
||||
variable: function () {
|
||||
var op = this.operator;
|
||||
if (op === "and" || op === "or") {
|
||||
return op;
|
||||
} else if (this.rule[op] && this.rule[op][0]) {
|
||||
return this.rule[op][0]["var"];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
rightoperand: function () {
|
||||
var op = this.operator;
|
||||
if (op === "and" || op === "or") {
|
||||
return null;
|
||||
} else if (this.rule[op] && typeof this.rule[op][1] !== "undefined") {
|
||||
return this.rule[op][1];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
operator: function () {
|
||||
return Object.keys(this.rule)[0];
|
||||
},
|
||||
operands: function () {
|
||||
return this.rule[this.operator];
|
||||
},
|
||||
classObject: function () {
|
||||
var c = {
|
||||
'checkin-rule': true
|
||||
};
|
||||
c['checkin-rule-' + this.variable] = true;
|
||||
return c;
|
||||
},
|
||||
vartype: function () {
|
||||
if (this.variable && VARS[this.variable]) {
|
||||
return VARS[this.variable]['type'];
|
||||
}
|
||||
},
|
||||
timeType: function () {
|
||||
if (this.rightoperand && this.rightoperand['buildTime']) {
|
||||
return this.rightoperand['buildTime'][0];
|
||||
}
|
||||
},
|
||||
timeTolerance: function () {
|
||||
var op = this.operator;
|
||||
if ((op === "isBefore" || op === "isAfter") && this.rule[op] && typeof this.rule[op][2] !== "undefined") {
|
||||
return this.rule[op][2];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
timeValue: function () {
|
||||
if (this.rightoperand && this.rightoperand['buildTime']) {
|
||||
return this.rightoperand['buildTime'][1];
|
||||
}
|
||||
},
|
||||
cardinality: function () {
|
||||
if (this.vartype && TYPEOPS[this.vartype] && TYPEOPS[this.vartype][this.operator]) {
|
||||
return TYPEOPS[this.vartype][this.operator]['cardinality'];
|
||||
}
|
||||
},
|
||||
operators: function () {
|
||||
return TYPEOPS[this.vartype];
|
||||
},
|
||||
productSelectURL: function () {
|
||||
return $("#product-select2").text();
|
||||
},
|
||||
variationSelectURL: function () {
|
||||
return $("#variations-select2").text();
|
||||
},
|
||||
vars: function () {
|
||||
return VARS;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setVariable: function (event) {
|
||||
var current_op = Object.keys(this.rule)[0];
|
||||
var current_val = this.rule[current_op];
|
||||
|
||||
if (event.target.value === "and" || event.target.value === "or") {
|
||||
if (current_val[0] && current_val[0]["var"]) {
|
||||
current_val = [];
|
||||
}
|
||||
this.$set(this.rule, event.target.value, current_val);
|
||||
this.$delete(this.rule, current_op);
|
||||
} else {
|
||||
if (current_val !== "and" && current_val !== "or" && current_val[0] && VARS[event.target.value]['type'] === this.vartype) {
|
||||
this.$set(this.rule[current_op][0], "var", event.target.value);
|
||||
} else {
|
||||
this.$delete(this.rule, current_op);
|
||||
this.$set(this.rule, "!!", [{"var": event.target.value}]);
|
||||
}
|
||||
}
|
||||
},
|
||||
setOperator: function (event) {
|
||||
var current_op = Object.keys(this.rule)[0];
|
||||
var current_val = this.rule[current_op];
|
||||
this.$delete(this.rule, current_op);
|
||||
this.$set(this.rule, event.target.value, current_val);
|
||||
},
|
||||
setRightOperandNumber: function (event) {
|
||||
if (this.rule[this.operator].length === 1) {
|
||||
this.rule[this.operator].push(parseInt(event.target.value));
|
||||
} else {
|
||||
this.$set(this.rule[this.operator], 1, parseInt(event.target.value));
|
||||
}
|
||||
},
|
||||
setTimeTolerance: function (event) {
|
||||
if (this.rule[this.operator].length === 2) {
|
||||
this.rule[this.operator].push(parseInt(event.target.value));
|
||||
} else {
|
||||
this.$set(this.rule[this.operator], 2, parseInt(event.target.value));
|
||||
}
|
||||
},
|
||||
setTimeType: function (event) {
|
||||
var time = {
|
||||
"buildTime": [event.target.value]
|
||||
};
|
||||
if (this.rule[this.operator].length === 1) {
|
||||
this.rule[this.operator].push(time);
|
||||
} else {
|
||||
this.$set(this.rule[this.operator], 1, time);
|
||||
}
|
||||
if (event.target.value === "custom") {
|
||||
this.$set(this.rule[this.operator], 2, 0);
|
||||
}
|
||||
},
|
||||
setTimeValue: function (val) {
|
||||
console.log(val);
|
||||
this.$set(this.rule[this.operator][1]["buildTime"], 1, val);
|
||||
},
|
||||
setRightOperandProductList: function (val) {
|
||||
var products = {
|
||||
"objectList": []
|
||||
};
|
||||
for (var i = 0; i < val.length; i++) {
|
||||
products["objectList"].push({
|
||||
"lookup": [
|
||||
"product",
|
||||
val[i].id,
|
||||
val[i].text
|
||||
]
|
||||
});
|
||||
}
|
||||
if (this.rule[this.operator].length === 1) {
|
||||
this.rule[this.operator].push(products);
|
||||
} else {
|
||||
this.$set(this.rule[this.operator], 1, products);
|
||||
}
|
||||
},
|
||||
setRightOperandVariationList: function (val) {
|
||||
var products = {
|
||||
"objectList": []
|
||||
};
|
||||
for (var i = 0; i < val.length; i++) {
|
||||
products["objectList"].push({
|
||||
"lookup": [
|
||||
"variation",
|
||||
val[i].id,
|
||||
val[i].text
|
||||
]
|
||||
});
|
||||
}
|
||||
if (this.rule[this.operator].length === 1) {
|
||||
this.rule[this.operator].push(products);
|
||||
} else {
|
||||
this.$set(this.rule[this.operator], 1, products);
|
||||
}
|
||||
},
|
||||
addOperand: function () {
|
||||
this.rule[this.operator].push({"": []});
|
||||
},
|
||||
wrapWithOR: function () {
|
||||
var r = JSON.parse(JSON.stringify(this.rule));
|
||||
this.$delete(this.rule, this.operator);
|
||||
this.$set(this.rule, "or", [r]);
|
||||
},
|
||||
wrapWithAND: function () {
|
||||
var r = JSON.parse(JSON.stringify(this.rule));
|
||||
this.$delete(this.rule, this.operator);
|
||||
this.$set(this.rule, "and", [r]);
|
||||
},
|
||||
cutOut: function () {
|
||||
var cop = Object.keys(this.operands[0])[0];
|
||||
var r = this.operands[0][cop];
|
||||
this.$delete(this.rule, this.operator);
|
||||
this.$set(this.rule, cop, r);
|
||||
},
|
||||
remove: function () {
|
||||
this.$parent.rule[this.$parent.operator].splice(this.index, 1);
|
||||
},
|
||||
},
|
||||
props: {
|
||||
rule: Object,
|
||||
level: Number,
|
||||
index: Number,
|
||||
}
|
||||
});
|
||||
|
||||
Vue.component('checkin-rules-editor', {
|
||||
template: ('<div class="checkin-rules-editor">'
|
||||
+ '<checkin-rule :rule="this.$root.rules" :level="0" :index="0" v-if="hasRules"></checkin-rule>'
|
||||
+ '<button type="button" class="checkin-rule-addchild btn btn-xs btn-default" v-if="!hasRules" @click.prevent="addRule"><span class="fa fa-plus-circle"></span> ' + gettext('Add condition') + '</button>'
|
||||
+ '</div>'
|
||||
),
|
||||
computed: {
|
||||
hasRules: function () {
|
||||
return hasRules = !!Object.keys(this.$root.rules).length;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addRule: function () {
|
||||
this.$set(this.$root.rules, "and", []);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
var app = new Vue({
|
||||
el: '#rules-editor',
|
||||
data: function () {
|
||||
return {
|
||||
rules: {},
|
||||
hasRules: false,
|
||||
};
|
||||
},
|
||||
created: function () {
|
||||
this.rules = JSON.parse($("#id_rules").val());
|
||||
},
|
||||
watch: {
|
||||
rules: {
|
||||
deep: true,
|
||||
handler: function (newval) {
|
||||
$("#id_rules").val(JSON.stringify(newval));
|
||||
}
|
||||
},
|
||||
}
|
||||
})
|
||||
});
|
||||
},
|
||||
}
|
||||
})
|
||||
});
|
||||
@@ -0,0 +1,261 @@
|
||||
<template>
|
||||
<div v-bind:class="classObject">
|
||||
<div class="btn-group pull-right">
|
||||
<button type="button" class="checkin-rule-remove btn btn-xs btn-default" @click.prevent="wrapWithOR">OR
|
||||
</button>
|
||||
<button type="button" class="checkin-rule-remove btn btn-xs btn-default" @click.prevent="wrapWithAND">AND
|
||||
</button>
|
||||
<button type="button" class="checkin-rule-remove btn btn-xs btn-default" @click.prevent="cutOut"
|
||||
v-if="operands && operands.length == 1 && (operator === 'or' || operator == 'and')"><span
|
||||
class="fa fa-cut"></span></button>
|
||||
<button type="button" class="checkin-rule-remove btn btn-xs btn-default" @click.prevent="remove"
|
||||
v-if="level > 0"><span class="fa fa-trash"></span></button>
|
||||
</div>
|
||||
<select v-bind:value="variable" v-on:input="setVariable" required class="form-control">
|
||||
<option value="and">{{texts.and}}</option>
|
||||
<option value="or">{{texts.or}}</option>
|
||||
<option v-for="(v, name) in vars" :value="name">{{ v.label }}</option>
|
||||
</select>
|
||||
<select v-bind:value="operator" v-on:input="setOperator" required class="form-control"
|
||||
v-if="operator !== 'or' && operator !== 'and'">
|
||||
<option></option>
|
||||
<option v-for="(v, name) in operators" :value="name">{{ v.label }}</option>
|
||||
</select>
|
||||
<select v-bind:value="timeType" v-on:input="setTimeType" required class="form-control"
|
||||
v-if="vartype == 'datetime'">
|
||||
<option value="date_from">{{texts.date_from}}</option>
|
||||
<option value="date_to">{{texts.date_to}}</option>
|
||||
<option value="date_admission">{{texts.date_admission}}</option>
|
||||
<option value="custom">{{texts.date_custom}}</option>
|
||||
</select>
|
||||
<datetimefield v-if="vartype == 'datetime' && timeType == 'custom'" :value="timeValue"
|
||||
v-on:input="setTimeValue"></datetimefield>
|
||||
<input class="form-control" required type="number"
|
||||
v-if="vartype == 'datetime' && timeType && timeType != 'custom'" v-bind:value="timeTolerance"
|
||||
v-on:input="setTimeTolerance" :placeholder="texts.date_tolerance">
|
||||
<input class="form-control" required type="number" v-if="vartype == 'int' && cardinality > 1"
|
||||
v-bind:value="rightoperand" v-on:input="setRightOperandNumber">
|
||||
<lookup-select2 required v-if="vartype == 'product' && operator == 'inList'" :multiple="true"
|
||||
:value="rightoperand" v-on:input="setRightOperandProductList"
|
||||
:url="productSelectURL"></lookup-select2>
|
||||
<lookup-select2 required v-if="vartype == 'variation' && operator == 'inList'" :multiple="true"
|
||||
:value="rightoperand" v-on:input="setRightOperandVariationList"
|
||||
:url="variationSelectURL"></lookup-select2>
|
||||
<div class="checkin-rule-childrules" v-if="operator === 'or' || operator === 'and'">
|
||||
<div v-for="(op, opi) in operands">
|
||||
<checkin-rule :rule="op" :index="opi" :level="level + 1" v-if="typeof op === 'object'"></checkin-rule>
|
||||
</div>
|
||||
<button type="button" class="checkin-rule-addchild btn btn-xs btn-default" @click.prevent="addOperand"><span
|
||||
class="fa fa-plus-circle"></span> {{ texts.condition_add }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
components: {
|
||||
LookupSelect2: LookupSelect2.default,
|
||||
Datetimefield: Datetimefield.default,
|
||||
},
|
||||
props: {
|
||||
rule: Object,
|
||||
level: Number,
|
||||
index: Number,
|
||||
},
|
||||
computed: {
|
||||
texts: function () {
|
||||
return this.$root.texts;
|
||||
},
|
||||
variable: function () {
|
||||
var op = this.operator;
|
||||
if (op === "and" || op === "or") {
|
||||
return op;
|
||||
} else if (this.rule[op] && this.rule[op][0]) {
|
||||
return this.rule[op][0]["var"];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
rightoperand: function () {
|
||||
var op = this.operator;
|
||||
if (op === "and" || op === "or") {
|
||||
return null;
|
||||
} else if (this.rule[op] && typeof this.rule[op][1] !== "undefined") {
|
||||
return this.rule[op][1];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
operator: function () {
|
||||
return Object.keys(this.rule)[0];
|
||||
},
|
||||
operands: function () {
|
||||
return this.rule[this.operator];
|
||||
},
|
||||
classObject: function () {
|
||||
var c = {
|
||||
'checkin-rule': true
|
||||
};
|
||||
c['checkin-rule-' + this.variable] = true;
|
||||
return c;
|
||||
},
|
||||
vartype: function () {
|
||||
if (this.variable && this.$root.VARS[this.variable]) {
|
||||
return this.$root.VARS[this.variable]['type'];
|
||||
}
|
||||
},
|
||||
timeType: function () {
|
||||
if (this.rightoperand && this.rightoperand['buildTime']) {
|
||||
return this.rightoperand['buildTime'][0];
|
||||
}
|
||||
},
|
||||
timeTolerance: function () {
|
||||
var op = this.operator;
|
||||
if ((op === "isBefore" || op === "isAfter") && this.rule[op] && typeof this.rule[op][2] !== "undefined") {
|
||||
return this.rule[op][2];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
timeValue: function () {
|
||||
if (this.rightoperand && this.rightoperand['buildTime']) {
|
||||
return this.rightoperand['buildTime'][1];
|
||||
}
|
||||
},
|
||||
cardinality: function () {
|
||||
if (this.vartype && this.$root.TYPEOPS[this.vartype] && this.$root.TYPEOPS[this.vartype][this.operator]) {
|
||||
return this.$root.TYPEOPS[this.vartype][this.operator]['cardinality'];
|
||||
}
|
||||
},
|
||||
operators: function () {
|
||||
return this.$root.TYPEOPS[this.vartype];
|
||||
},
|
||||
productSelectURL: function () {
|
||||
return $("#product-select2").text();
|
||||
},
|
||||
variationSelectURL: function () {
|
||||
return $("#variations-select2").text();
|
||||
},
|
||||
vars: function () {
|
||||
return this.$root.VARS;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
setVariable: function (event) {
|
||||
var current_op = Object.keys(this.rule)[0];
|
||||
var current_val = this.rule[current_op];
|
||||
|
||||
if (event.target.value === "and" || event.target.value === "or") {
|
||||
if (current_val[0] && current_val[0]["var"]) {
|
||||
current_val = [];
|
||||
}
|
||||
this.$set(this.rule, event.target.value, current_val);
|
||||
this.$delete(this.rule, current_op);
|
||||
} else {
|
||||
if (current_val !== "and" && current_val !== "or" && current_val[0] && this.$root.VARS[event.target.value]['type'] === this.vartype) {
|
||||
this.$set(this.rule[current_op][0], "var", event.target.value);
|
||||
} else {
|
||||
this.$delete(this.rule, current_op);
|
||||
this.$set(this.rule, "!!", [{"var": event.target.value}]);
|
||||
}
|
||||
}
|
||||
},
|
||||
setOperator: function (event) {
|
||||
var current_op = Object.keys(this.rule)[0];
|
||||
var current_val = this.rule[current_op];
|
||||
this.$delete(this.rule, current_op);
|
||||
this.$set(this.rule, event.target.value, current_val);
|
||||
},
|
||||
setRightOperandNumber: function (event) {
|
||||
if (this.rule[this.operator].length === 1) {
|
||||
this.rule[this.operator].push(parseInt(event.target.value));
|
||||
} else {
|
||||
this.$set(this.rule[this.operator], 1, parseInt(event.target.value));
|
||||
}
|
||||
},
|
||||
setTimeTolerance: function (event) {
|
||||
if (this.rule[this.operator].length === 2) {
|
||||
this.rule[this.operator].push(parseInt(event.target.value));
|
||||
} else {
|
||||
this.$set(this.rule[this.operator], 2, parseInt(event.target.value));
|
||||
}
|
||||
},
|
||||
setTimeType: function (event) {
|
||||
var time = {
|
||||
"buildTime": [event.target.value]
|
||||
};
|
||||
if (this.rule[this.operator].length === 1) {
|
||||
this.rule[this.operator].push(time);
|
||||
} else {
|
||||
this.$set(this.rule[this.operator], 1, time);
|
||||
}
|
||||
if (event.target.value === "custom") {
|
||||
this.$set(this.rule[this.operator], 2, 0);
|
||||
}
|
||||
},
|
||||
setTimeValue: function (val) {
|
||||
this.$set(this.rule[this.operator][1]["buildTime"], 1, val);
|
||||
},
|
||||
setRightOperandProductList: function (val) {
|
||||
var products = {
|
||||
"objectList": []
|
||||
};
|
||||
for (var i = 0; i < val.length; i++) {
|
||||
products["objectList"].push({
|
||||
"lookup": [
|
||||
"product",
|
||||
val[i].id,
|
||||
val[i].text
|
||||
]
|
||||
});
|
||||
}
|
||||
if (this.rule[this.operator].length === 1) {
|
||||
this.rule[this.operator].push(products);
|
||||
} else {
|
||||
this.$set(this.rule[this.operator], 1, products);
|
||||
}
|
||||
},
|
||||
setRightOperandVariationList: function (val) {
|
||||
var products = {
|
||||
"objectList": []
|
||||
};
|
||||
for (var i = 0; i < val.length; i++) {
|
||||
products["objectList"].push({
|
||||
"lookup": [
|
||||
"variation",
|
||||
val[i].id,
|
||||
val[i].text
|
||||
]
|
||||
});
|
||||
}
|
||||
if (this.rule[this.operator].length === 1) {
|
||||
this.rule[this.operator].push(products);
|
||||
} else {
|
||||
this.$set(this.rule[this.operator], 1, products);
|
||||
}
|
||||
},
|
||||
addOperand: function () {
|
||||
this.rule[this.operator].push({"": []});
|
||||
},
|
||||
wrapWithOR: function () {
|
||||
var r = JSON.parse(JSON.stringify(this.rule));
|
||||
this.$delete(this.rule, this.operator);
|
||||
this.$set(this.rule, "or", [r]);
|
||||
},
|
||||
wrapWithAND: function () {
|
||||
var r = JSON.parse(JSON.stringify(this.rule));
|
||||
this.$delete(this.rule, this.operator);
|
||||
this.$set(this.rule, "and", [r]);
|
||||
},
|
||||
cutOut: function () {
|
||||
var cop = Object.keys(this.operands[0])[0];
|
||||
var r = this.operands[0][cop];
|
||||
this.$delete(this.rule, this.operator);
|
||||
this.$set(this.rule, cop, r);
|
||||
},
|
||||
remove: function () {
|
||||
this.$parent.rule[this.$parent.operator].splice(this.index, 1);
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,25 @@
|
||||
<template>
|
||||
<div class="checkin-rules-editor">
|
||||
<checkin-rule :rule="this.$root.rules" :level="0" :index="0" v-if="hasRules"></checkin-rule>
|
||||
<button type="button" class="checkin-rule-addchild btn btn-xs btn-default" v-if="!hasRules"
|
||||
@click.prevent="addRule"><span class="fa fa-plus-circle"></span> {{ this.$root.texts.condition_add }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
components: {
|
||||
CheckinRule: CheckinRule.default,
|
||||
},
|
||||
computed: {
|
||||
hasRules: function () {
|
||||
return !!Object.keys(this.$root.rules).length;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
addRule: function () {
|
||||
this.$set(this.$root.rules, "and", []);
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,268 @@
|
||||
<template>
|
||||
<div :class="'checkin-rules-visualization ' + (maximized ? 'maximized' : '')">
|
||||
<div class="tools">
|
||||
<button v-if="maximized" class="btn btn-default" type="button" @click.prevent="maximized = false"><span class="fa fa-window-close"></span></button>
|
||||
<button v-if="!maximized" class="btn btn-default" type="button" @click.prevent="maximized = true"><span class="fa fa-window-maximize"></span></button>
|
||||
</div>
|
||||
<svg :width="graph.columns * (boxWidth + marginX) + 2 * paddingX" :height="graph.height * (boxHeight + marginY)"
|
||||
:viewBox="viewBox" ref="svg">
|
||||
<g :transform="zoomTransform.toString()">
|
||||
<viz-node v-for="(node, nodeid) in graph.nodes_by_id" :key="nodeid" :node="node"
|
||||
:children="node.children.map(n => graph.nodes_by_id[n])" :nodeid="nodeid"
|
||||
:boxWidth="boxWidth" :boxHeight="boxHeight" :marginX="marginX" :marginY="marginY"
|
||||
:paddingX="paddingX"></viz-node>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
components: {
|
||||
VizNode: VizNode.default,
|
||||
},
|
||||
computed: {
|
||||
boxWidth() {
|
||||
return 300
|
||||
},
|
||||
boxHeight() {
|
||||
return 62
|
||||
},
|
||||
paddingX() {
|
||||
return 50
|
||||
},
|
||||
marginX() {
|
||||
return 50
|
||||
},
|
||||
marginY() {
|
||||
return 20
|
||||
},
|
||||
contentWidth() {
|
||||
return this.graph.columns * (this.boxWidth + this.marginX) + 2 * this.paddingX
|
||||
},
|
||||
contentHeight() {
|
||||
return this.graph.height * (this.boxHeight + this.marginY)
|
||||
},
|
||||
viewBox() {
|
||||
return `0 0 ${this.contentWidth} ${this.contentHeight}`
|
||||
},
|
||||
graph() {
|
||||
/**
|
||||
* Converts a JSON logic rule into a "flow chart".
|
||||
*
|
||||
* A JSON logic rule has a structure like an operator tree:
|
||||
*
|
||||
* OR
|
||||
* |-- AND
|
||||
* |-- A
|
||||
* |-- B
|
||||
* |-- AND
|
||||
* |-- OR
|
||||
* |-- C
|
||||
* |-- D
|
||||
* |-- E
|
||||
*
|
||||
* For our visualization, we want to visualize that tree as a graph one can follow along to reach a
|
||||
* decision, which has the structure of a directed graph:
|
||||
*
|
||||
* --- A --- B --- OK!
|
||||
* /
|
||||
* /
|
||||
* /
|
||||
* --
|
||||
* \
|
||||
* \ --- C ---
|
||||
* \ / \
|
||||
* --- --- E --- OK!
|
||||
* \ /
|
||||
* --- D ---
|
||||
*/
|
||||
const graph = {
|
||||
nodes_by_id: {},
|
||||
children: [],
|
||||
columns: -1,
|
||||
}
|
||||
|
||||
// Step 1: Start building the graph by finding all nodes and edges
|
||||
let counter = 0;
|
||||
const _add_to_graph = (rule) => { // returns [heads, tails]
|
||||
if (typeof rule !== 'object' || rule === null) {
|
||||
const node_id = (counter++).toString()
|
||||
graph.nodes_by_id[node_id] = {
|
||||
rule: rule,
|
||||
column: -1,
|
||||
height: -1,
|
||||
children: [],
|
||||
}
|
||||
return [[node_id], [node_id]]
|
||||
}
|
||||
|
||||
const operator = Object.keys(rule)[0]
|
||||
const operands = rule[operator]
|
||||
|
||||
if (operator === "and") {
|
||||
let children = []
|
||||
let tails = null
|
||||
operands.reverse()
|
||||
for (let operand of operands) {
|
||||
let [new_children, new_tails] = _add_to_graph(operand)
|
||||
for (let new_child of new_tails) {
|
||||
graph.nodes_by_id[new_child].children.push(...children)
|
||||
}
|
||||
if (tails === null) {
|
||||
tails = new_tails
|
||||
}
|
||||
children = new_children
|
||||
}
|
||||
return [children, tails]
|
||||
} else if (operator === "or") {
|
||||
const children = []
|
||||
const tails = []
|
||||
for (let operand of operands) {
|
||||
let [new_children, new_tails] = _add_to_graph(operand)
|
||||
children.push(...new_children)
|
||||
tails.push(...new_tails)
|
||||
}
|
||||
return [children, tails]
|
||||
} else {
|
||||
const node_id = (counter++).toString()
|
||||
graph.nodes_by_id[node_id] = {
|
||||
rule: rule,
|
||||
column: -1,
|
||||
height: -1,
|
||||
children: [],
|
||||
}
|
||||
return [[node_id], [node_id]]
|
||||
}
|
||||
|
||||
}
|
||||
graph.children = _add_to_graph(JSON.parse(JSON.stringify(this.$root.rules)))[0]
|
||||
|
||||
// Step 2: We compute the "column" of every node, which is the maximum number of hops required to reach the
|
||||
// node from the root node
|
||||
const _set_column_to_min = (nodes, mincol) => {
|
||||
for (let node of nodes) {
|
||||
if (mincol > node.column) {
|
||||
node.column = mincol
|
||||
graph.columns = Math.max(mincol + 1, graph.columns)
|
||||
_set_column_to_min(node.children.map(nid => graph.nodes_by_id[nid]), mincol + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
_set_column_to_min(graph.children.map(nid => graph.nodes_by_id[nid]), 0)
|
||||
|
||||
// Step 3: We compute the "height" of every node, which is the maximum number of descendent nodes in
|
||||
// the same column
|
||||
const _get_all_descendents = (node) => {
|
||||
let result = [...node.children]
|
||||
for (let cid of node.children) {
|
||||
result.push(..._get_all_descendents(graph.nodes_by_id[cid]))
|
||||
}
|
||||
result = result.filter((v, idx, self) => self.indexOf(v) === idx) // ensure uniqueness
|
||||
return result
|
||||
}
|
||||
for (let node of [...Object.values(graph.nodes_by_id), graph]) {
|
||||
const descendents = _get_all_descendents(node)
|
||||
const descendents_by_column = {}
|
||||
for (let descid of descendents) {
|
||||
const col = graph.nodes_by_id[descid].column
|
||||
descendents_by_column[col] = (descendents_by_column[col] || 0) + 1
|
||||
}
|
||||
node.height = Math.max(1, ...Object.values(descendents_by_column))
|
||||
}
|
||||
|
||||
// Step 4: Align each node on a grid. The x position is already given by the column computed above, but we still
|
||||
// need the y position. This part of the algorithm is opinionated and probably not yet the nicest solution we
|
||||
// can use!
|
||||
const _set_y = (node, offset) => {
|
||||
if (typeof node.y === "undefined") {
|
||||
// We only take the first value we found for each node
|
||||
node.y = offset
|
||||
}
|
||||
|
||||
let used = 0
|
||||
for (let cid of node.children) {
|
||||
used += Math.max(0, _set_y(graph.nodes_by_id[cid], offset + used) - 1)
|
||||
used++
|
||||
}
|
||||
return used
|
||||
}
|
||||
_set_y(graph, 0)
|
||||
|
||||
return graph
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.createZoom()
|
||||
},
|
||||
created() {
|
||||
window.addEventListener('resize', this.createZoom)
|
||||
},
|
||||
destroyed() {
|
||||
window.removeEventListener('resize', this.createZoom)
|
||||
},
|
||||
watch: {
|
||||
maximized() {
|
||||
this.$nextTick(() => {
|
||||
this.createZoom()
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
createZoom() {
|
||||
if (!this.$refs.svg) return
|
||||
|
||||
const viewportHeight = this.$refs.svg.clientHeight
|
||||
const viewportWidth = this.$refs.svg.clientWidth
|
||||
this.defaultScale = 1
|
||||
|
||||
this.zoom = d3
|
||||
.zoom()
|
||||
.scaleExtent([Math.min(this.defaultScale * 0.5, 1), Math.max(5, this.contentHeight / viewportHeight, this.contentWidth / viewportWidth)])
|
||||
.extent([[0, 0], [viewportWidth, viewportHeight]])
|
||||
.filter(event => {
|
||||
const wheeled = event.type === 'wheel'
|
||||
const mouseDrag =
|
||||
event.type === 'mousedown' ||
|
||||
event.type === 'mouseup' ||
|
||||
event.type === 'mousemove'
|
||||
const touch =
|
||||
event.type === 'touchstart' ||
|
||||
event.type === 'touchmove' ||
|
||||
event.type === 'touchstop'
|
||||
return (wheeled || mouseDrag || touch) && this.maximized
|
||||
})
|
||||
.wheelDelta(event => {
|
||||
// In contrast to default implementation, do not use a factor 10 if ctrl is pressed
|
||||
return -event.deltaY * (event.deltaMode === 1 ? 0.05 : event.deltaMode ? 1 : 0.002)
|
||||
})
|
||||
.on('zoom', (event) => {
|
||||
this.zoomTransform = event.transform
|
||||
})
|
||||
|
||||
const initTransform = d3.zoomIdentity
|
||||
.scale(this.defaultScale)
|
||||
.translate(
|
||||
0,
|
||||
0
|
||||
)
|
||||
this.zoomTransform = initTransform
|
||||
|
||||
// This sets correct d3 internal state for the initial centering
|
||||
d3.select(this.$refs.svg)
|
||||
.call(this.zoom.transform, initTransform)
|
||||
|
||||
const svg = d3.select(this.$refs.svg).call(this.zoom)
|
||||
svg.on('touchmove.zoom', null)
|
||||
// TODO touch support
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
maximized: false,
|
||||
zoom: null,
|
||||
defaultScale: 1,
|
||||
zoomTransform: d3.zoomTransform({k: 1, x: 0, y: 0}),
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<input class="form-control">
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ["required", "value"],
|
||||
template: (''),
|
||||
mounted: function () {
|
||||
var vm = this;
|
||||
var multiple = this.multiple;
|
||||
$(this.$el)
|
||||
.datetimepicker(this.opts())
|
||||
.trigger("change")
|
||||
.on("dp.change", function (e) {
|
||||
vm.$emit("input", $(this).data('DateTimePicker').date().toISOString());
|
||||
});
|
||||
if (!vm.value) {
|
||||
$(this.$el).data("DateTimePicker").viewDate(moment().hour(0).minute(0).second(0).millisecond(0));
|
||||
} else {
|
||||
$(this.$el).data("DateTimePicker").date(moment(vm.value));
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
opts: function () {
|
||||
return {
|
||||
format: $("body").attr("data-datetimeformat"),
|
||||
locale: $("body").attr("data-datetimelocale"),
|
||||
useCurrent: false,
|
||||
showClear: this.required,
|
||||
icons: {
|
||||
time: 'fa fa-clock-o',
|
||||
date: 'fa fa-calendar',
|
||||
up: 'fa fa-chevron-up',
|
||||
down: 'fa fa-chevron-down',
|
||||
previous: 'fa fa-chevron-left',
|
||||
next: 'fa fa-chevron-right',
|
||||
today: 'fa fa-screenshot',
|
||||
clear: 'fa fa-trash',
|
||||
close: 'fa fa-remove'
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
value: function (val) {
|
||||
$(this.$el).data('DateTimePicker').date(moment(val));
|
||||
},
|
||||
},
|
||||
destroyed: function () {
|
||||
$(this.$el)
|
||||
.off()
|
||||
.datetimepicker("destroy");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,81 @@
|
||||
<template>
|
||||
<select>
|
||||
<slot></slot>
|
||||
</select>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: ["required", "value", "placeholder", "url", "multiple"],
|
||||
template: ('<select>\n' +
|
||||
' <slot></slot>\n' +
|
||||
' </select>'),
|
||||
mounted: function () {
|
||||
var vm = this;
|
||||
var multiple = this.multiple;
|
||||
$(this.$el)
|
||||
.select2(this.opts())
|
||||
.val(this.value)
|
||||
.trigger("change")
|
||||
// emit event on change.
|
||||
.on("change", function (e) {
|
||||
vm.$emit("input", $(this).select2('data'));
|
||||
});
|
||||
if (vm.value) {
|
||||
for (var i = 0; i < vm.value["objectList"].length; i++) {
|
||||
var option = new Option(vm.value["objectList"][i]["lookup"][2], vm.value["objectList"][i]["lookup"][1], true, true);
|
||||
$(vm.$el).append(option);
|
||||
}
|
||||
}
|
||||
$(vm.$el).trigger("change");
|
||||
},
|
||||
methods: {
|
||||
opts: function () {
|
||||
return {
|
||||
theme: "bootstrap",
|
||||
delay: 100,
|
||||
width: '100%',
|
||||
multiple: true,
|
||||
allowClear: this.required,
|
||||
language: $("body").attr("data-select2-locale"),
|
||||
ajax: {
|
||||
url: this.url,
|
||||
data: function (params) {
|
||||
return {
|
||||
query: params.term,
|
||||
page: params.page || 1
|
||||
}
|
||||
}
|
||||
},
|
||||
templateResult: function (res) {
|
||||
if (!res.id) {
|
||||
return res.text;
|
||||
}
|
||||
var $ret = $("<span>").append(
|
||||
$("<span>").addClass("primary").append($("<div>").text(res.text).html())
|
||||
);
|
||||
return $ret;
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
placeholder: function (val) {
|
||||
$(this.$el).empty().select2(this.opts());
|
||||
this.build();
|
||||
},
|
||||
required: function (val) {
|
||||
$(this.$el).empty().select2(this.opts());
|
||||
this.build();
|
||||
},
|
||||
url: function (val) {
|
||||
$(this.$el).empty().select2(this.opts());
|
||||
this.build();
|
||||
},
|
||||
},
|
||||
destroyed: function () {
|
||||
$(this.$el)
|
||||
.off()
|
||||
.select2("destroy");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
145
src/pretix/static/pretixcontrol/js/ui/checkinrules/viz-node.vue
Normal file
145
src/pretix/static/pretixcontrol/js/ui/checkinrules/viz-node.vue
Normal file
@@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<g>
|
||||
<path v-for="e in edges" :d="e" class="edge"></path>
|
||||
<path v-if="rootEdge" :d="rootEdge" class="edge"></path>
|
||||
<path v-if="!node.children.length" :d="checkEdge" class="edge"></path>
|
||||
<rect :width="boxWidth" :height="boxHeight" :x="x" :y="y" class="node" rx="5">
|
||||
</rect>
|
||||
<foreignObject :width="boxWidth - 10" :height="boxHeight - 10" :x="x + 5" :y="y + 5">
|
||||
<div xmlns="http://www.w3.org/1999/xhtml" class="text">
|
||||
<span v-if="vardata.type === 'int'">
|
||||
<span v-if="variable.startsWith('entries_')" class="fa fa-sign-in"></span>
|
||||
{{ vardata.label }}
|
||||
<br>
|
||||
<strong>
|
||||
{{ op.label }} {{ rightoperand }}
|
||||
</strong>
|
||||
</span>
|
||||
<span v-else-if="variable === 'now'">
|
||||
<span class="fa fa-clock-o"></span> {{ vardata.label }}<br>
|
||||
<strong>
|
||||
{{ op.label }}<br>
|
||||
<span v-if="rightoperand.buildTime[0] === 'custom'">
|
||||
{{ df(rightoperand.buildTime[1]) }}
|
||||
</span>
|
||||
<span v-else>
|
||||
{{ this.$root.texts[rightoperand.buildTime[0]] }}
|
||||
</span>
|
||||
<span v-if="operands[2]">
|
||||
<span v-if="operator === 'isBefore'">+</span>
|
||||
<span v-else>-</span>
|
||||
{{ operands[2] }}
|
||||
{{ this.$root.texts.minutes }}
|
||||
</span>
|
||||
</strong>
|
||||
</span>
|
||||
<span v-else-if="operator === 'inList'">
|
||||
<span class="fa fa-ticket"></span> {{ vardata.label }}<br>
|
||||
<strong>
|
||||
{{ rightoperand.objectList.map((o) => o.lookup[2]).join(", ") }}
|
||||
</strong>
|
||||
</span>
|
||||
</div>
|
||||
</foreignObject>
|
||||
|
||||
<g v-if="!node.children.length" :transform="`translate(${x + boxWidth + 25}, ${y + boxHeight/2 - 15})`">
|
||||
<path d="m 25.078125,11.835938 c 0,-0.332032 -0.117188,-0.664063 -0.351563,-0.898438 L 22.949219,9.1796875 c -0.234375,-0.234375 -0.546875,-0.3710937 -0.878907,-0.3710937 -0.332031,0 -0.644531,0.1367187 -0.878906,0.3710937 L 13.222656,17.128906 8.8085938,12.714844 C 8.5742188,12.480469 8.2617188,12.34375 7.9296875,12.34375 c -0.3320313,0 -0.6445313,0.136719 -0.8789063,0.371094 l -1.7773437,1.757812 c -0.234375,0.234375 -0.3515625,0.566407 -0.3515625,0.898438 0,0.332031 0.1171875,0.644531 0.3515625,0.878906 l 7.0703125,7.070312 c 0.234375,0.234375 0.566406,0.371094 0.878906,0.371094 0.332032,0 0.664063,-0.136719 0.898438,-0.371094 L 24.726562,12.714844 c 0.234375,-0.234375 0.351563,-0.546875 0.351563,-0.878906 z M 30,15 C 30,23.28125 23.28125,30 15,30 6.71875,30 0,23.28125 0,15 0,6.71875 6.71875,0 15,0 23.28125,0 30,6.71875 30,15 Z"
|
||||
class="check"/>
|
||||
</g>
|
||||
</g>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
node: Object,
|
||||
nodeid: String,
|
||||
children: Array,
|
||||
boxWidth: Number,
|
||||
boxHeight: Number,
|
||||
marginX: Number,
|
||||
marginY: Number,
|
||||
paddingX: Number,
|
||||
},
|
||||
computed: {
|
||||
x() {
|
||||
return this.node.column * (this.boxWidth + this.marginX) + this.marginX / 2 + this.paddingX
|
||||
},
|
||||
y() {
|
||||
return this.node.y * (this.boxHeight + this.marginY) + this.marginY / 2
|
||||
},
|
||||
edges() {
|
||||
const startX = this.x + this.boxWidth + 1
|
||||
const startY = this.y + this.boxHeight / 2
|
||||
return this.children.map((c) => {
|
||||
const endX = (c.column * (this.boxWidth + this.marginX) + this.marginX / 2 + this.paddingX) - 1
|
||||
const endY = (c.y * (this.boxHeight + this.marginY) + this.marginY / 2) + this.boxHeight / 2
|
||||
|
||||
return `
|
||||
M ${startX} ${startY}
|
||||
L ${endX - 50} ${startY}
|
||||
C ${endX - 25} ${startY} ${endX - 25} ${startY} ${endX - 25} ${startY + 25 * Math.sign(endY - startY)}
|
||||
L ${endX - 25} ${endY - 25 * Math.sign(endY - startY)}
|
||||
C ${endX - 25} ${endY} ${endX - 25} ${endY} ${endX} ${endY}
|
||||
`
|
||||
})
|
||||
},
|
||||
checkEdge() {
|
||||
const startX = this.x + this.boxWidth + 1
|
||||
const startY = this.y + this.boxHeight / 2
|
||||
|
||||
return `M ${startX} ${startY} L ${startX + 25} ${startY}`
|
||||
},
|
||||
rootEdge() {
|
||||
if (this.node.column > 0) {
|
||||
return
|
||||
}
|
||||
const startX = 0
|
||||
const startY = this.boxHeight / 2 + this.marginY / 2
|
||||
const endX = this.x - 1
|
||||
const endY = this.y + this.boxHeight / 2
|
||||
|
||||
return `
|
||||
M ${startX} ${startY}
|
||||
L ${endX - 50} ${startY}
|
||||
C ${endX - 25} ${startY} ${endX - 25} ${startY} ${endX - 25} ${startY + 25 * Math.sign(endY - startY)}
|
||||
L ${endX - 25} ${endY - 25 * Math.sign(endY - startY)}
|
||||
C ${endX - 25} ${endY} ${endX - 25} ${endY} ${endX} ${endY}
|
||||
`
|
||||
},
|
||||
variable () {
|
||||
const op = this.operator;
|
||||
if (this.node.rule[op] && this.node.rule[op][0]) {
|
||||
return this.node.rule[op][0]["var"];
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
},
|
||||
vardata () {
|
||||
return this.$root.VARS[this.variable];
|
||||
},
|
||||
rightoperand () {
|
||||
const op = this.operator;
|
||||
if (this.node.rule[op] && typeof this.node.rule[op][1] !== "undefined") {
|
||||
return this.node.rule[op][1];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
op: function () {
|
||||
return this.$root.TYPEOPS[this.vardata.type][this.operator]
|
||||
},
|
||||
operands: function () {
|
||||
return this.node.rule[this.operator]
|
||||
},
|
||||
operator: function () {
|
||||
return Object.keys(this.node.rule)[0];
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
df (val) {
|
||||
const format = $("body").attr("data-datetimeformat")
|
||||
return moment(val).format(format)
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -642,18 +642,81 @@ table td > .checkbox input[type="checkbox"] {
|
||||
|
||||
#rules-editor {
|
||||
.checkin-rule {
|
||||
border-left: 4px solid $brand-primary;
|
||||
border-left: 4px solid transparentize($brand-primary, .5);
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
padding: 5px 15px 5px 15px;
|
||||
margin: 5px 0;
|
||||
position: relative;
|
||||
&:hover {
|
||||
border-left-color: $brand-primary;
|
||||
}
|
||||
}
|
||||
|
||||
.checkin-rule-and {
|
||||
border-left: 4px solid $brand-danger;
|
||||
border-left: 4px solid transparentize($brand-danger, .5);
|
||||
&:hover {
|
||||
border-left-color: $brand-danger;
|
||||
}
|
||||
}
|
||||
.checkin-rule-or {
|
||||
border-left: 4px solid $brand-success;
|
||||
border-left: 4px solid transparentize($brand-success, .5);
|
||||
&:hover {
|
||||
border-left-color: $brand-success;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.checkin-rules-visualization {
|
||||
svg {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
.node {
|
||||
stroke: $gray-light;
|
||||
stroke-width: 2px;
|
||||
fill: #fff;
|
||||
&:hover {
|
||||
stroke: $brand-primary;
|
||||
}
|
||||
}
|
||||
.edge {
|
||||
stroke: $gray-light;
|
||||
stroke-width: 2px;
|
||||
fill: none;
|
||||
}
|
||||
.check {
|
||||
fill: $brand-success;
|
||||
}
|
||||
.text {
|
||||
font-size: 12px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
cursor: default;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
position: relative;
|
||||
.tools {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
&.maximized {
|
||||
background: white;
|
||||
position: fixed;
|
||||
top: 51px;
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
max-height: none;
|
||||
height: 100vh;
|
||||
height: calc(100vh - 51px);
|
||||
z-index: 90000;
|
||||
|
||||
svg {
|
||||
height: 100%
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user