updated handling of entries in comment

This commit is contained in:
Christian Seyfferth 2022-07-30 12:35:06 +02:00
commit b82d9748ac
9 changed files with 1453 additions and 0 deletions

8
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

5
.idea/codeStyles/codeStyleConfig.xml generated Normal file
View File

@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/orchester_dienstplan.iml" filepath="$PROJECT_DIR$/.idea/orchester_dienstplan.iml" />
</modules>
</component>
</project>

8
.idea/orchester_dienstplan.iml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

633
app.js Normal file
View File

@ -0,0 +1,633 @@
if (!String.prototype.includes) {
String.prototype.includes = function () {
'use strict';
return String.prototype.indexOf.apply(this, arguments) !== -1;
};
}
//extend FileReader
if (!FileReader.prototype.readAsBinaryString) {
FileReader.prototype.readAsBinaryString = function (fileData) {
var binary = "";
var pt = this;
var reader = new FileReader();
reader.onload = function (e) {
var bytes = new Uint8Array(reader.result);
var length = bytes.byteLength;
for (var i = 0; i < length; i++) {
binary += String.fromCharCode(bytes[i]);
}
//pt.result - readonly so assign binary
pt.content = binary;
$(pt).trigger('onload');
}
reader.readAsArrayBuffer(file);
}
}
//localStorage persistence
var SHIFT_STORAGE_KEY = "dienstplan_chrosey";
var RULE_STORAGE_KEY = "regeln_chrosey";
var shiftStorage = {
fetch: function () {
'use strict';
var parsed = JSON.parse(localStorage.getItem(SHIFT_STORAGE_KEY) || '[]'),
shifts = [];
parsed.forEach(function (el, index) {
var shift = Shift.thaw(el);
shift.id = index;
shifts.push(shift);
});
shiftStorage.uid = shifts.length;
return shifts;
},
save : function (shifts) {
'use strict';
var json = JSON.stringify(shifts)
localStorage.setItem(SHIFT_STORAGE_KEY, json);
},
count: function () {
return JSON.parse(localStorage.getItem(SHIFT_STORAGE_KEY) || '[]').length;
}
};
var ruleStorage = {
fetch: function () {
'use strict';
var parsed = JSON.parse(localStorage.getItem(RULE_STORAGE_KEY)) | [];
var rules = parsed.length > 0 ?
parsed.map((e, i) => {
var r = Rule.thaw(e);
r.id = i;
return r;
}) :
Rule.defaults();
ruleStorage.uid = rules.length;
return rules;
},
save : function (rules) {
'use strict';
var json = JSON.stringify(rules);
localStorage.setItem(RULE_STORAGE_KEY, json);
}
};
Vue.component('ask-format-modal', {
template: `
<div id="ask-format-modal" class="modal">
<div class="modal-content">
<h4>Dienstplanformat</h4>
<p>Welches Format soll eingelesen werden?</p>
<label v-for="option in options" >
<input name="dpFormat" type="radio" :value="option" v-model="picked"/>
<span>{{ option }}</span>
</label>
<p>Ausgewählt: {{ picked }}</p>
</div>
<div class="modal-footer">
<a href="#!" class="modal-close waves-effect waves-green btn-flat" @click.stop="submitPick">Bastätigen</a>
</div>
</div>
`,
data() {
return {
picked: null
}
},
props : ["options"],
methods: {
submitPick() {
this.$emit('picked-format', this.picked);
$('#ask-format-modal').modal('close');
}
}
});
Vue.component('chip-input', {
template: `
<div class="chips no-autoinit" :id="name"></div>
`,
data() {
return {
instance: null,
chips : []
}
},
computed: {
chipsData() {
return this.instance.chipsData;
}
},
watch: {
initData: {
deep: true,
handler(n, o) {
if (n !== o) {
this.initialize();
this.$emit('init');
}
}
}
},
props: {
name : String,
initData: Array
},
methods: {
initialize() {
this.chips = this.initData.map(e => e);
var el = $('#' + this.name)[0];
this.instance = M.Chips.init(el, {
data : this.chips,
onChipAdd : () => {
this.$emit("change", this.chipsData);
},
onChipDelete: () => {
this.$emit("change", this.chipsData);
}
});
}
},
mounted() {
this.initialize();
}
});
var app = new Vue({
el : '#app',
data : {
shifts : shiftStorage.fetch(),
rules : ruleStorage.fetch(),
icsFile : null,
blob : null,
dp_sheet : '',
deletedShift : '',
format : '',
remaining : shiftStorage.count(),
selectedShift : new Shift({}),
selectedShiftIndex: -1,
selectedRule : new Rule({}),
selectedRuleIndex : -1,
saveto : 'dienstplan.ics',
uploadFileName : "",
availableFormats : ["Erfurt", "Stuttgart", "X"],
stepper : null,
timepickers : null,
config: {
moment : {
parse_formats : [
"ddd, DD/ MMM. 'YY HH:mm",
"ddd, DD/ MMM. YYYY HH:mm"
],
parse_language : 'en',
display_language: 'de'
},
stepper : {
firstActive: 0,
},
toast : {
displayLength: 3000
},
timepicker: {
twelveHour: false,
}
},
},
watch: {
shifts: {
handler: function (shifts) {
'use strict';
shiftStorage.save(shifts);
this.remaining = shifts.length;
this.icsFile = null;
this.blob = null;
this.makeToast("Änderungen gespeichert.");
},
deep : true
},
rules : {
handler: function (rules) {
'use strict';
ruleStorage.save(rules);
this.makeToast("Änderungen gespeichert.");
},
deep : true
}
},
computed: {
groupedTermine() {
return _.chain(this.shifts).sortBy(e => e.Datum).groupBy(e => e.Datum).value();
}
},
methods : {
updateArten(value) {
this.selectedRule.Arten = value;
},
updateTitel(value) {
this.selectedRule.Titel = value;
},
makeToast(message) {
var toastOptions = this.config.toast;
toastOptions.html = message;
M.toast(toastOptions);
},
openModal(elementID) {
var element = document.getElementById(elementID);
var modal = M.Modal.getInstance(element);
modal.open();
},
closeModal(elementID) {
var element = document.getElementById(elementID);
var modal = M.Modal.getInstance(element);
modal.close();
},
onFileChange: function (event) {
var files = event.target.files || event.dataTransfer.files;
this.handleInputFile(files[0]);
this.uploadFileName = files[0].name + " [" + Math.round(files[0].size / 1024) + " kB]";
this.makeToast(this.uploadFileName + " ausgewählt");
},
handleInputFile: function (file) {
var reader = new FileReader();
var vm = this;
reader.onload = (e) => {
var data = !e ? reader.content : e.target.result;
var workbook = XLSX.read(data, {
type : 'binary',
cellDates: true,
});
var isErfurterDienstplan = workbook.SheetNames.indexOf("Dienstplan") > -1;
if (isErfurterDienstplan) {
vm.parseForErfurt(workbook.Sheets["Dienstplan"]);
} else {
this.workbook = workbook;
this.askForDienstplanFormat();
}
};
reader.readAsBinaryString(file);
},
askForDienstplanFormat: function () {
this.makeToast("Dienstplanformat nicht erkannt.");
this.openModal('ask-format-modal');
},
parseForErfurt: function (dp) {
var arr = XLSX.utils.sheet_to_row_object_array(dp, {
range: 1
});
this.format = "Erfurt";
var vm = this;
var day;
this.makeToast("Erfurter Dienstplan erkannt.");
arr.forEach(element => {
moment.locale(vm.config.moment.parse_language);
if (element.hasOwnProperty('Datum')) {
day = moment(element.Datum);
}
if (element.hasOwnProperty('Bemerkung')) {
if (element.Bemerkung.toString().search(/\d\d:\d\d\s/) >= 0) {
// prüfe ob eine Uhrzeit drinnen steht
let sonderzeit = moment(element.Bemerkung, 'HH:mm');
let name = element.Bemerkung.toString().replace(/\d\d:\d\d\s/, '').trim();
let splittedName = name.split(' ');
let terminArt = '';
splittedName.forEach(function (item) {
vm.rules.forEach(function (rule) {
rule.Arten.forEach(function (art) {
if (art.tag == item) {
terminArt = art.tag;
name.replace(art.tag, '');
}
})
})
});
let termin = {
datum : day.clone().hour(sonderzeit.hour()).minute(sonderzeit.minute()),
art : terminArt,
beschreibung: '',
name : name.trim()
}
vm.addShift(new Shift(termin));
beschreibung = '';
}
}
if (element.hasOwnProperty('Dienst')) {
let times = [];
let art, beschreibung, name = "";
if (element.Zeit.toString().indexOf(' + ') > 0) {
// in der Zeitspalte stehen mehrere Uhrzeiten.
let tempTimes = element.Zeit.toString().split(' + ');
tempTimes.forEach(function (time) {
let mom = moment(time, 'HH:mm');
times.push([mom.hour(), mom.minute()]);
})
} else if (element.Zeit.toString().indexOf(' - ') > 0) {
// in der Zeitspalte stehen mehrere Uhrzeiten als Zeitspanne
let tempTimes = element.Zeit.toString().split(' - ');
let mom = moment(tempTimes[0], 'HH:mm');
let momEnd = moment(tempTimes[1], 'HH:mm');
times.push([mom.hour(), mom.minute(), momEnd.hour(), momEnd.minute()]);
} else {
if (element.Zeit) {
let mom = moment(element.Zeit);
times.push([mom.hour(), mom.minute()]);
} else {
times.push([0, 0]);
}
}
art = element.Dienst.trim();
if (element.hasOwnProperty('__EMPTY')) {
name = element.__EMPTY.trim();
}
if (element.hasOwnProperty('Bemerkung')
&& element.Bemerkung.toString().search(/\d\d:\d\d\s/) == -1) {
beschreibung = element.Bemerkung;
}
times.forEach(time => {
var termin = {
datum : day.clone().hour(time[0]).minute(time[1]),
art : art,
beschreibung: beschreibung,
name : name
}
if (time.length === 4) {
// Wenn die Zeit mehr Werte hat, dann behandle die nächsten 2 als ende
termin.end = day.clone().hour(time[2]).minute(time[3]);
termin.dontSetDurationFromRules = true;
}
vm.addShift(new Shift(termin));
});
}
});
},
changeTime: function (hours) {
let temp = this.shifts;
this.shifts = [];
temp.forEach(shift => {
shift.updateBeginn(hours);
this.addShift(shift);
});
},
updateBeginn: function (shift, hours) {
shift.updateBeginn(hours);
this.shifts.splice(this.shifts.indexOf(shift), 1, shift);
},
parseForStuttgart: function (dp) {
var arr = XLSX.utils.sheet_to_json(dp, {
header : "A",
blankrows: false,
});
var vm = this;
var day;
this.makeToast("Stuttgarter Dienstplan erkannt.");
arr.forEach(element => {
moment.locale(vm.config.moment.parse_language);
if (element.hasOwnProperty('C')) {
day = moment(element.C);
}
if (element.hasOwnProperty('D') && moment(element.D, "HH:mm").isValid()) {
var termin = {
ort : element.H ? element.H.trim() : "",
art : element.E ? element.E.trim() : "",
beschreibung: element.H ? element.H.trim() : "",
name : element.F ? element.F.trim() : ""
}
var time = day.clone();
if (typeof (element.D) === "object") {
time = moment(element.D);
termin.datum = day.clone().hour(time.hour()).minute(time.minute());
} else if (element.D.indexOf("-") > -1) {
var tArray = element.D.split(" - ");
time = moment(tArray[0], "HH:mm");
termin.datum = day.clone().hour(time.hour()).minute(time.minute());
termin.ende = moment(tArray[1], "HH:mm").format("HH:mm");
}
vm.addShift(new Shift(termin));
}
});
},
parseForX: function (dp, sheetName) {
moment.locale(this.config.moment.parse_language);
var arr = XLSX.utils.sheet_to_json(dp, {
header : "A",
blankrows: false,
});
var vm = this;
var month = moment(sheetName.substr(sheetName.indexOf(" ") + 1), "MMMM YYYY", "de");
var day;
arr.forEach(element => {
var art, name, beschreibung = null;
var time = moment().hour(0).minute(0);
try {
if (element.hasOwnProperty('A')) {
day = moment(element.A, "D.", "de");
day = Number(element.A);
}
if ((!element.F && !element.H && !element.J) || element.I == "Spielort/ Extras" || element.J == "Spielort/Extras") {
} else {
if (element.hasOwnProperty("D") && element.hasOwnProperty("E")) {
// Probe
time = moment(element.D);
art = element.E.trim();
name = element.F ? element.F.trim() : "";
beschreibung = typeof (element.I) != 'undefined' ? element.I.trim() :
typeof (element.J) != 'undefined' ? element.J.trim() :
"";
} else if (element.hasOwnProperty("G") && element.hasOwnProperty("H")) {
// Vorstellung
time = moment(element.G);
art = "VS";
name = element.H.trim();
beschreibung = typeof (element.I) != 'undefined' ? element.I.trim() :
typeof (element.J) != 'undefined' ? element.J.trim() :
"";
} else if (element.hasOwnProperty("I")) {
// Spielort/Extras
beschreibung = element.I.trim();
name = name ? name : "Spielort/Extras";
} else if (element.hasOwnProperty("J")) {
// Spielort/Extras Fallback
beschreibung = element.J.trim();
name = name ? name : "Spielort/Extras";
}
var datumStr = day + '.' + month.clone().format("MM.YY") + time.format(" HH:mm");
var termin = {
ort : "",
art : art,
beschreibung: beschreibung,
name : name,
datum : moment(datumStr, "D.MM.YY HH:mm")
}
vm.addShift(new Shift(termin));
}
} catch (error) {
console.error("Fehler beim Parsen", element, error);
}
});
},
parsePickedFormat: function (format) {
this.makeToast(`Versuche ${format} zu lesen.`);
switch (format) {
case "Erfurt":
this.parseForErfurt(this.workbook.Sheets["Dienstplan"]);
break;
case "Stuttgart":
var sheetName = this.workbook.SheetNames[0];
this.parseForStuttgart(this.workbook.Sheets[sheetName]);
break;
case "X":
var sheetName = this.workbook.SheetNames[0];
this.parseForX(this.workbook.Sheets[sheetName], sheetName);
break;
default:
break;
}
this.workbook = null;
},
addShift: function (shift) {
this.shifts.push(shift);
},
removeShift: function (shift) {
this.shifts.splice(this.shifts.indexOf(shift), 1);
this.makeToast(shift.VEventTitle + " gelöscht");
},
cleanStorage: function () {
this.shifts = [];
this.makeToast("Alle Einträge gelöscht");
},
createDownloadFile: function () {
var vCal = new VCalendar("Dienstplan Kalender");
this.shifts.forEach(function (shift) {
vCal.add(shift.toVEvent());
});
var calString = vCal.toString();
this.blob = new Blob([calString], {
type: 'text/plain'
});
if (this.icsFile !== null) {
window.URL.revokeObjectURL(this.icsFile);
}
this.icsFile = window.URL.createObjectURL(this.blob);
this.makeToast(this.saveto + " erstellt.");
},
downloadFile: function () {
if (window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveOrOpenBlob(this.blob, this.saveto);
}
},
selectShift: function (shift) {
this.selectedShift = Shift.thaw(shift);
this.keepShift = shift;
M.updateTextFields();
this.openModal('shiftModal');
},
saveChanges: function (changedShift) {
this.shifts.splice(this.shifts.indexOf(this.keepShift), 1, changedShift);
this.closeModal('shiftModal');
this.keepShift = '';
this.selectedShift = '';
},
discardChanges: function (changedShift) {
this.closeModal('shiftModal');
this.keepShift = '';
this.selectedShift = '';
},
editRule: function (rule) {
this.selectedRule = Rule.thaw(rule);
this.selectedRuleIndex = this.rules.indexOf(rule);
this.openModal('ruleModal');
},
saveRule: function () {
this.rules[this.selectedRuleIndex] = this.selectedRule;
this.closeModal('ruleModal');
this.selectedRule = new Rule();
},
discardRule: function () {
this.closeModal('ruleModal');
this.selectedRule = new Rule();
},
applyRules: function () {
var shifts = this.shifts;
shifts.forEach(function (shift) {
Shift.setDurationFromRules(shift, this.rules);
});
this.shifts = shifts;
}
},
directives: {
'edit-focus': function (el, value) {
if (value) {
el.focus();
}
}
},
mounted : function () {
M.AutoInit();
var els = document.querySelectorAll('.timepicker');
this.timepickers = M.Timepicker.init(els, this.config.timepicker);
var el = document.querySelector(".stepper");
this.stepper = new MStepper(el, this.config.stepper);
}
})

44
css/page.css Normal file
View File

@ -0,0 +1,44 @@
.custom-file-control:lang(de)::after {
content: "Datei wählen...";
}
.custom-file-control:lang(de)::before {
content: "Durchsuchen";
}
#drop {
padding: 3em;
outline: 2px dashed black;
outline-offset: -10px;
}
.edit {
display: none;
}
footer {
padding-bottom: 15px;
}
.click-me {
cursor: pointer;
}
#body {
display: flex;
min-height: calc(100vh - 64px);
flex-direction: column;
padding-top: 56px;
}
main {
flex: 1 0 auto;
}
.deleteMe {
cursor: pointer;
}
.modal {
max-height: 70%;
}

274
index.html Normal file
View File

@ -0,0 +1,274 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Dienstplan Converter</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<link rel="apple-touch-icon" sizes="180x180" href="/favicons/apple-touch-icon.png">
<link rel="icon" type="image/png" href="/favicons/favicon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="/favicons/favicon-16x16.png" sizes="16x16">
<link rel="mask-icon" href="/favicons/safari-pinned-tab.svg" color="#e6b300">
<link rel="stylesheet" href="https://fonts.googleapis.com/icon?family=Material+Icons">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/css/materialize.min.css">
<link rel="stylesheet" href="https://unpkg.com/materialize-stepper@3.1.0/dist/css/mstepper.min.css">
<link rel="stylesheet" href="css/page.css">
</head>
<body>
<div id="app">
<nav class="nav blue">
<div class="container">
<span class="brand-logo">
<img src="/img/logo.svg" height="30">
Dienstplan Converter
<link rel="apple-touch-icon" sizes="180x180" href="/favicons/apple-touch-icon.png">
<link rel="icon" type="image/png" href="/favicons/favicon-32x32.png" sizes="32x32">
<link rel="icon" type="image/png" href="/favicons/favicon-16x16.png" sizes="16x16">
<link rel="mask-icon" href="/favicons/safari-pinned-tab.svg" color="#e6b300">
</span>
</div>
</nav>
<div id="body">
<main class="container">
<ul class="stepper">
<li class="step active">
<div class="step-title waves-effect">Dienstplan-Datei auswählen</div>
<div class="step-content">
<label class="btn waves-effect waves-green" data-position="left"
data-tooltip="Dienstplan einlesen" for="fileInput">
Datei auswählen
</label>
<div class="card-panel" v-if="format">Erkanntes Format: {{format}}</div>
<div class="step-actions">
<button class="waves-effect waves-dark btn next-step">Weiter</button>
<a class="btn-flat red-text waves-effect" @click="cleanStorage"
:disabled=" (remaining > 0) ? null : 'disabled'">
{{ remaining }} Termine löschen
</a>
</div>
</div>
</li>
<li class="step">
<div class="step-title waves-effect" data-step-label="Termin-Dauer-Bestimmung">
Regeln prüfen
</div>
<div class="step-content">
<div>
<span class="chip orange">Titel der Veranstaltung</span>
<span class="chip indigo">Art des Dienstes</span>
</div>
<div class="collection">
<a class="collection-item avatar" v-for="(r,i) in rules" :key="'rule-'+i"
@click="editRule(r)">
<i class="circle">#{{ i+1 }}</i>
<span class="title">{{ r.Name}} </span>
<div class="secondary-content">
<span class="badge new" data-badge-caption="Minuten">{{ r.Dauer }}</span>
</div>
<div>
<span class="chip indigo" :class="'lighten-'+(r.Arten.length > 1 ? '3' : '1')"
v-for="a in r.Arten">
{{a.tag}}
</span>
<span class="chip orange" :class="'lighten-'+(r.Titel.length > 1 ? '3' : '1')"
v-for="t in r.Titel">
{{t.tag}}
</span>
</div>
</a>
</div>
<div class="step-actions">
<button class="waves-effect waves-dark btn-flat" @click.prevent="applyRules">
Regeln anwenden
</button>
<button class="waves-effect waves-dark btn next-step">Weiter</button>
</div>
</div>
</li>
<li class="step">
<div class="step-title waves-effect">Termine prüfen</div>
<div class="step-content">
<table class="highlight">
<thead>
<tr>
<th>Datum</th>
<th>Dauer</th>
<th>Info</th>
<th></th>
</tr>
</thead>
<tbody>
<template v-for="g in groupedTermine">
<tr v-for="(s,i) in g">
<th v-if="i == 0" :rowspan="g.length">
{{s.FormattedDatum}}
</th>
<td>
<button class="waves-effect waves-light btn-small btn-flat" @click.prevent="updateBeginn(s, -1)" title="1 Stunde früher">
<i class="material-icons">skip_previous</i>
</button>
{{ s.Beginn }} - {{ s.Ende }} Uhr
<button class="waves-effect waves-light btn-small btn-flat" @click.prevent="updateBeginn(s, 1)" title="1 Stunde später">
<i class="material-icons">skip_next</i>
</button>
</td>
<td>
<h6>{{ s.VEventTitle }}</h6>
<p v-if="s.Ort!=''">
<i class="material-icons red-text">location_on</i>
{{ s.Ort }}
</p>
<blockquote v-if="s.Ort != s.Beschreibung">
{{ s.Beschreibung }}
</blockquote>
</td>
<td>
<button class="btn-flat"
@click.prevent="removeShift(s)">löschen</button>
<button class="btn-flat"
@click.prevent="selectShift(s)">bearbeiten</button>
</td>
</tr>
</template>
</tbody>
</table>
<div class="step-actions">
<button class="waves-effect waves-dark btn blue" @click.prevent="changeTime(-1)">alle 1 h früher</button>
<button class="waves-effect waves-dark btn blue" @click.prevent="changeTime(1)">alle 1 h später</button>
<button class="waves-effect waves-dark btn next-step">Weiter</button>
</div>
</div>
</li>
<li class="step">
<div class="step-title waves-effect">Kalenderdatei speichern</div>
<div class="step-content">
<p>
Erst die Kalenderdatei erstellen. Danach kann sie gespeichert werden.
</p>
<div class="step-actions">
<button class="btn waves-effect" @click.prevent="createDownloadFile"
:disabled=" (remaining > 0) ? null : 'disabled'">
Kalenderdatei erstellen
</button>
<a class="btn waves-effect" :href="icsFile" :class="[ icsFile ? '' : 'disabled']"
download="dienstplan.ics" @click.stop="downloadFile">
Kalenderdatei speichern
</a>
</div>
</div>
</li>
</ul>
</main>
<div id="confirmModal" class="modal active">
<div class="modal-content">
<h4>Aktion bestätigen</h4>
</div>
<div class="modal-footer">
<button class="btn-flat" @click="true">Bestätigen</button>
<button class="btn-flat" @click="false">Abbrechen</button>
</div>
</div>
<div id="ruleModal" class="modal modal-fixed-footer active">
<div class="modal-content">
<h4>Regel anpassen</h4>
<div class="row">
<div class="range-field col s12 m6 l4">
<label class="active" for="rule_duration">Dauer in Minuten: {{selectedRule.Dauer}}</label>
<input id="rule_duration" type="range" v-model="selectedRule.Dauer" min="30" max="300"
step="10">
</div>
<div class="range-field col s12 m6 l8">
<label class="active" for="rule_name">Name</label>
<input id="rule_name" type="text" v-model="selectedRule.Name">
</div>
<div class="input-field col s12">
<label class="active" for="shift_kind">Arten</label>
<chip-input name="regel_arten" :init-data="selectedRule.Arten" @change="updateArten">
</div>
<div class="input-field col s12">
<label class="active" for="rule_titel">Titel</label>
<chip-input name="regel_titel" :init-data="selectedRule.Titel" @change="updateTitel">>
</div>
</div>
</div>
<div class="modal-footer">
<button class="waves-effect btn-flat waves-green" @click="saveRule(selectedRule)">Speichern</button>
<button class="waves-effect btn-flat waves-red"
@click="discardRule(selectedRule)">Verwerfen</button>
</div>
</div>
<div id="shiftModal" class="modal modal-fixed-footer">
<div class="modal-content">
<h4>{{ selectedShift.VEventTitle || 'kein Titel'}}</h4>
<div class="row">
<div class="input-field col s6 ">
<input id="shift_name" type="text" v-model="selectedShift.Name" placeholder="kein Titel">
<label class="active" for="shift_name">Titel</label>
</div>
<div class="input-field col s6 ">
<input id="shift_kind" type="text" v-model="selectedShift.Art" placeholder="keine Art">
<label class="active" for="shift_kind">Art</label>
</div>
<div class="input-field col s6 ">
<input id="shift_date" type="date" class="datepicker" v-model.lazy="selectedShift.Datum"
placeholder="Datum">
<label class="active" for="shift_date">Datum</label>
</div>
<div class="input-field col s6">
<input id="shift_begin" type="time" v-model.lazy="selectedShift.Beginn"
placeholder="Uhrzeit" class="timepicker no-autoinit"
:data-default="selectedShift.Beginn">
<label class="active" for="shift_begin">Anfang</label>
</div>
<div class="input-field col s6 ">
<input id="shift_end" type="time" v-model.lazy="selectedShift.Ende" placeholder="Ende"
class="timepicker no-autoinit">
<label class="active" for="shift_end">Ende</label>
</div>
<div class="input-field col s6 ">
<input id="shift_desc" type="text" v-model="selectedShift.Beschreibung"
placeholder="keine Beschreibung">
<label class="active" for="shift_desc">Beschreibung</label>
</div>
<div class="input-field col s6 ">
<input id="shift_location" type="text" v-model="selectedShift.Ort" placeholder="kein Ort">
<label class="active" for="shift_location">Ort</label>
</div>
</div>
</div>
<div class="modal-footer">
<button class="waves-effect btn-flat waves-green"
@click="saveChanges(selectedShift)">Speichern</button>
<button class="waves-effect btn-flat waves-red"
@click="discardChanges(selectedShift)">Verwerfen</button>
</div>
</div>
<ask-format-modal :options="availableFormats" @picked-format="parsePickedFormat"></ask-format-modal>
</div>
<input type="file" name="fileInput" id="fileInput" @change="onFileChange"
accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,application/vnd.ms-excel"
style="display:none;">
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0/js/materialize.min.js"></script>
<script src="https://unpkg.com/materialize-stepper@3.1.0/dist/js/mstepper.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.14.1/xlsx.full.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.23.0/moment-with-locales.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.22/dist/vue.js"></script>
<script src="js/shift.js"></script>
<script src="js/vcal.js"></script>
<script src="app.js"></script>
</body>
</html>

319
js/shift.js Normal file
View File

@ -0,0 +1,319 @@
var DATE_LOCALE_FORMAT = "D.M.YY"; //10.2.16
var DATE_INPUT_FORMAT = "YYYY-MM-DD"; //2016-02-10
var TIME_FORMAT = "HH:mm"; // 17:30
var WEEKDAY_FORMAT = "dddd"; //Monday
var MOMENT_LOCALE = "de";
var TIMEZONE_NAME = "Europe/Berlin";
//requires moment.js
Array.prototype.asChipData = function () {
return this.map((e, i) => {
return {
tag: e,
id : i
};
})
}
Array.prototype.fromChipData = function () {
return this.map(e => {
return e.tag;
})
}
function Rule() {
var options = {};
if (arguments[0]) options = arguments[0];
var default_args = {
'arten': [],
'dauer': 60,
'titel': [],
'name' : "Standard"
}
for (var index in default_args) {
if (typeof options[index] == "undefined") options[index] = default_args[index];
}
this._duration = options.dauer;
this._arten = options.arten;
this._titel = options.titel;
this._name = options.name;
}
Rule.prototype = {
get Dauer() {
return this._duration;
},
set Dauer(value) {
this._duration = value;
},
get Arten() {
return this._arten.asChipData();
},
set Arten(value) {
this._arten = value.fromChipData();
},
get Titel() {
return this._titel.asChipData();
},
set Titel(value) {
this._titel = value.fromChipData();
},
get Name() {
return this._name;
},
set Name(value) {
this._name = value;
},
};
Rule.prototype.fits = function (art, title) {
var artMatch = false;
var nameMatch = false;
if (this._arten.length == 0) artMatch = true;
else {
this._arten.forEach(function (el, i) {
if (art.includes(el.toLowerCase())) artMatch = true;
});
}
if (this._titel.length == 0) nameMatch = true;
else {
this._titel.forEach(function (el, i) {
if (name.includes(el.toLowerCase())) nameMatch = true;
});
}
return artMatch && nameMatch;
}
Rule.thaw = function (json) {
return new Rule({
dauer: json._duration,
name : json._name,
arten: json._arten,
titel: json._titel
});
}
Rule.defaults = function () {
var rules = [];
var input = [{
name : "EF #01",
dauer: 60,
arten: ['VS'],
titel: ['Kinderkonzert']
},
{
name : "EF #02",
dauer: 120,
arten: ['VS'],
titel: ["Expeditionskonzert", "Sinfoniekonzert"]
},
{
name : "EF #03",
dauer: 150,
arten: ['Oa', 'GP'],
titel: ['Expeditionskonzert']
},
{
name : "EF #04",
dauer: 60,
arten: ['Oa', 'GP'],
titel: ['Expeditionskonzert']
},
{
name : "EF #05",
dauer: 150,
arten: ['Oa', 'OSP']
},
{
name : "EF #06",
dauer: 180,
arten: ["VS", "BO", "OHP", "HP", "GP", "Prem", "WA"]
},
{
name : "Standard",
dauer: 60
}
];
input.forEach(e => {
rules.push(new Rule(e));
});
return rules;
}
var DURATION_RULES = Rule.defaults();
function Shift() {
var options = {};
if (arguments[0]) options = arguments[0];
var default_args = {
'art' : "",
'name' : "DUMMY",
'datum' : moment(),
'end' : moment(),
'beschreibung': "",
'ort' : "",
}
for (var index in default_args) {
if (typeof options[index] == "undefined") options[index] = default_args[index];
}
this.Datum = options.datum.format(DATE_INPUT_FORMAT);
this.Beginn = options.datum.format(TIME_FORMAT);
this.Ende = options.end.format(TIME_FORMAT);
this.Art = options.art;
this.Beschreibung = options.beschreibung;
this.Name = options.name;
this.Ort = options.ort;
if (typeof options.ende != "undefined") {
this.Ende = options.ende;
} else {
if (!options.dontSetDurationFromRules) {
Shift.setDurationFromRules(this, DURATION_RULES);
}
}
}
Shift.prototype = {
get Wochentag() {
return this._date.clone().locale(MOMENT_LOCALE).format(WEEKDAY_FORMAT);
},
set Wochentag(value) {
throw "kann nicht gesetzt werden.";
},
get FormattedDatum() {
return this._date.clone().format(DATE_LOCALE_FORMAT);
},
set FormattedDatum(value) {
var dateMoment = moment(value, DATE_LOCALE_FORMAT);
this._date = dateMoment;
},
get Datum() {
return this._date.clone().format(DATE_INPUT_FORMAT);
},
set Datum(value) {
var dateMoment = moment(value).startOf('day');
this._date = dateMoment.clone();
},
get Beginn() {
return this._begin.clone().format(TIME_FORMAT);
},
set Beginn(value) {
var dateMoment = moment(this.Datum + " " + value, DATE_INPUT_FORMAT + " " + TIME_FORMAT);
this._begin = dateMoment.clone();
},
get Ende() {
var ende = this._begin.clone().add(this._duration).format(TIME_FORMAT);
return ende;
},
set Ende(value) {
var dateMoment = moment(this.Datum + " " + value, DATE_INPUT_FORMAT + " " + TIME_FORMAT);
if (this._begin > dateMoment) {
dateMoment.add(1, 'd');
}
this._duration = moment.duration(dateMoment.diff(this._begin));
},
get Dauer() {
return this._duration;
},
set Dauer(duration) {
this._duration = duration;
},
get Name() {
return this._name;
},
set Name(value) {
this._name = value ? value.trim() : "";
},
get Ort() {
return this._ort;
},
set Ort(value) {
this._ort = value ? value.trim() : "";
},
get VEventTitle() {
return this.Name + " " + this.Art;
},
set VEventTitle(value) {
throw "kann nicht gesetzt werden.";
},
get Art() {
return this._kind;
},
set Art(value) {
this._kind = value ? value.trim() : "";
},
get Beschreibung() {
return this._description;
},
set Beschreibung(value) {
this._description = value ? value.trim() : "";
}
}
Shift.setDurationFromRules = function (shift, rules) {
'use strict';
var isAllDayEvent = shift.Beginn == "00:00";
if (isAllDayEvent) {
shift.Dauer = moment.duration(24, 'h').locale(MOMENT_LOCALE);
return;
}
var art = shift.Art.toLowerCase();
var titel = shift.Name.toLowerCase();
var duration = 60;
for (var rIndex in rules) {
var rule = rules[rIndex];
if (rule.fits(art, titel)) {
duration = rule.Dauer;
break;
}
}
shift.Dauer = moment.duration(duration, 'm').locale(MOMENT_LOCALE);
}
Shift.prototype.toVEvent = function () {
var end = this._begin.clone().add(this._duration);
return new VEvent({
startMoment: this._begin,
endMoment : end,
title : this.VEventTitle,
description: this.Beschreibung,
location : this.Ort
});
};
Shift.thaw = function (jsonShift) {
moment.locale(MOMENT_LOCALE);
var begin = moment(jsonShift._begin);
var shift = new Shift({
art : jsonShift._kind,
name : jsonShift._name,
datum : begin,
beschreibung: jsonShift._description,
ort : jsonShift._ort,
});
shift.id = jsonShift.id;
shift.Dauer = moment.duration(jsonShift._duration);
return shift;
};
Shift.prototype.updateBeginn = function (hour) {
this._begin = this._begin.add('hours', hour);
}

154
js/vcal.js Normal file
View File

@ -0,0 +1,154 @@
var VCAL_DATETIME_FORMAT = "YMMDD[T]HHmmss"; //20160216T130500
var UID_FORMAT = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx';
var NEW_LINE = "\r\n";
var productID = window.title;
function VMeta() {
this.properties = new Map();
this.children = [];
this.tag = "VMETA";
}
VMeta.formatDate = function (aMoment) {
'use strict';
return aMoment.format(VCAL_DATETIME_FORMAT);
};
VMeta.generateUID = function () {
'use strict';
return UID_FORMAT.replace(/[xy]/g, function (c) {
var r = Math.random() * 16 | 0,
v = c == 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
};
VMeta.prototype.set = function (key, value) {
'use strict';
this.properties.set(key, value);
};
VMeta.prototype.get = function (key) {
'use strict';
this.properties.get(key);
};
VMeta.prototype.add = function (child) {
'use strict';
this.children.push(child);
};
VMeta.prototype.mapToString = function () {
'use strict';
var output = "";
this.properties.forEach(function (value, key, map) {
output += (NEW_LINE + key + ":" + value);
});
return output;
};
VMeta.prototype.childrenToString = function () {
'use strict';
var output = "";
this.children.forEach(function (child) {
output += (NEW_LINE + child.toString());
});
return output;
};
VMeta.prototype.toString = function () {
'use strict';
var output = "BEGIN:" + this.tag;
output += this.mapToString();
output += this.childrenToString();
output += (NEW_LINE + "END:" + this.tag);
return output;
};
function VCalendar(calendarName) {
'use strict';
VMeta.call(this);
this.add(VTimeZone.Berlin());
this.tag = "VCALENDAR";
this.set('X-WR-CALNAME', calendarName);
this.set('VERSION', '2.0');
this.set('PRODID', productID);
this.set('METHOD', "PUBLISH");
this.set('X-WR-TIMEZONE', "Europe/Berlin");
this.set('CALSCALE', "GREGORIAN");
};
VCalendar.prototype = Object.create(VMeta.prototype);
VCalendar.prototype.constructor = VCalendar;
function VEvent() {
var evt = window.event || arguments[1] || arguments.callee.caller.arguments[0];
var target = evt.target || evt.srcElement;
var options = {};
if (arguments[0]) options = arguments[0];
var default_args = {
'tzName' : "Europe/Berlin",
'startMoment' : moment(),
'endMoment' : moment(),
'uid' : VMeta.generateUID(),
'dtStamp' : VMeta.formatDate(moment()) + "Z",
'title' : "",
'description' : "",
'location' : "",
'organizer' : "",
}
for (var index in default_args) {
if (typeof options[index] == "undefined") options[index] = default_args[index];
}
VMeta.call(this);
this.tag = "VEVENT";
this.set('DTSTART;TZID=' + options.tzName, VMeta.formatDate(options.startMoment));
this.set('DTEND;TZID=' + options.tzName, VMeta.formatDate(options.endMoment));
this.set('DTSTAMP', options.dtStamp);
this.set('UID', options.uid);
this.set('SUMMARY', options.title);
this.set('DESCRIPTION', options.description);
this.set('LOCATION', options.location)
this.set('CLASS', "PUBLIC");
this.set('TRANSP', "OPAQUE");
this.set('STATUS', "CONFIRMED");
this.set('ORGANIZER', options.organizer);
}
VEvent.prototype = Object.create(VMeta.prototype);
VEvent.prototype.constructor = VEvent;
function VTimeZone() {
VMeta.call(this);
this.tag = "VTIMEZONE";
}
VTimeZone.prototype = Object.create(VMeta.prototype);
VTimeZone.prototype.constructor = VTimeZone;
VTimeZone.Berlin = function () {
var tz = new VTimeZone();
tz.children = [VTimeDef.MESZ(), VTimeDef.MEZ()];
tz.set("TZID", "Europe/Berlin");
return tz;
}
function VTimeDef(name) {
VMeta.call(this);
this.tag = name;
}
VTimeDef.prototype = Object.create(VMeta.prototype);
VTimeDef.prototype.constructor = VTimeDef;
VTimeDef.MESZ = function () {
var timedef = new VTimeDef("DAYLIGHT");
timedef.set("TZOFFSETFROM", "+0100");
timedef.set("RRULE", "FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU");
timedef.set("DTSTART", "19819329T020000");
timedef.set("TZNAME", "MESZ");
timedef.set("TZOFFSETTO", "+0200");
return timedef;
}
VTimeDef.MEZ = function () {
var timedef = new VTimeDef("STANDARD");
timedef.set("TZOFFSETFROM", "+0200");
timedef.set("RRULE", "FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU");
timedef.set("DTSTART", "19961027T030000");
timedef.set("TZNAME", "MEZ");
timedef.set("TZOFFSETTO", "+0100");
return timedef;
}