Version Changes
This commit is contained in:
commit
df89539710
259
app.js
Normal file
259
app.js
Normal file
@ -0,0 +1,259 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
M.AutoInit();
|
||||||
|
});
|
||||||
|
/* $(document).ready(function () {
|
||||||
|
$('.modal').modal();
|
||||||
|
$('.tabs').tabs();
|
||||||
|
$('.fixed-action-btn').floatingActionButton();
|
||||||
|
}); */
|
||||||
|
|
||||||
|
//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 rules = JSON.parse(localStorage.getItem(RULE_STORAGE_KEY)) || Rule.defaults();
|
||||||
|
ruleStorage.uid = rules.length;
|
||||||
|
return rules;
|
||||||
|
},
|
||||||
|
save: function (rules) {
|
||||||
|
'use strict';
|
||||||
|
var json = JSON.stringify(rules);
|
||||||
|
localStorage.setItem(RULE_STORAGE_KEY, json);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var app = new Vue({
|
||||||
|
el: '#app',
|
||||||
|
data: {
|
||||||
|
shifts: shiftStorage.fetch(),
|
||||||
|
rules: ruleStorage.fetch(),
|
||||||
|
icsFile: null,
|
||||||
|
blob: null,
|
||||||
|
dp_sheet: '',
|
||||||
|
deletedShift: '',
|
||||||
|
remaining: shiftStorage.count(),
|
||||||
|
selectedShift: '',
|
||||||
|
selectedRule: '',
|
||||||
|
saveto: 'dienstplan.ics',
|
||||||
|
uploadFileName: "",
|
||||||
|
config: {
|
||||||
|
moment: {
|
||||||
|
parse_formats: [
|
||||||
|
"ddd, DD/ MMM. 'YY HH:mm",
|
||||||
|
"ddd, DD/ MMM. YYYY HH:mm"
|
||||||
|
],
|
||||||
|
parse_language: 'en',
|
||||||
|
display_language: 'de'
|
||||||
|
},
|
||||||
|
toast_length: 3000
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
shifts: {
|
||||||
|
handler: function (shifts) {
|
||||||
|
'use strict';
|
||||||
|
shiftStorage.save(shifts);
|
||||||
|
this.remaining = shifts.length;
|
||||||
|
M.toast({html:"Änderungen gespeichert.", displayLength:this.config.toast_length});
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
handler: function (rules) {
|
||||||
|
'use strict';
|
||||||
|
ruleStorage.save(rules);
|
||||||
|
M.toast({html:"Änderungen gespeichert.", displayLength:this.config.toast_length});
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
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]";
|
||||||
|
M.toast({html:this.uploadFileName + " ausgewählt", displayLength:this.config.toast_length});
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
if(workbook.Sheets["Dienstplan"] == null){
|
||||||
|
file.status = "error";
|
||||||
|
} else {
|
||||||
|
vm.parseWorksheet(workbook.Sheets["Dienstplan"]);
|
||||||
|
file.status = "success";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
reader.readAsBinaryString(file);
|
||||||
|
M.toast({html:this.uploadFileName + " wird eingelesen",displayLength: this.config.toast_length});
|
||||||
|
},
|
||||||
|
parseWorksheet: function (dp) {
|
||||||
|
var arr = XLSX.utils.sheet_to_row_object_array(dp, {
|
||||||
|
range: 1
|
||||||
|
});
|
||||||
|
var vm = this;
|
||||||
|
var day;
|
||||||
|
arr.forEach(element => {
|
||||||
|
moment.locale(vm.config.moment.parse_language);
|
||||||
|
if (element.hasOwnProperty('Datum')) {
|
||||||
|
day = moment(element.Datum);
|
||||||
|
}
|
||||||
|
if (element.hasOwnProperty('Dienst')) {
|
||||||
|
var time = moment(element.Zeit);
|
||||||
|
var date = day.hour(time.hour()).minute(time.minute());
|
||||||
|
var bemerkung = !element.Bemerkung ? "" : element.Bemerkung.trim();
|
||||||
|
var art = !element.Dienst ? "" : element.Dienst.trim();
|
||||||
|
var name = !element.__EMPTY ? "" : element.__EMPTY.trim();
|
||||||
|
console.log({day, element});
|
||||||
|
vm.addShift(new Shift(art, name, date, bemerkung));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
addShift: function (shift) {
|
||||||
|
this.shifts.push(shift);
|
||||||
|
},
|
||||||
|
removeShift: function (shift) {
|
||||||
|
this.shifts.splice(this.shifts.indexOf(shift), 1);
|
||||||
|
M.toast({html:shift.VEventTitle + " gelöscht",displayLength: this.config.toast_length});
|
||||||
|
},
|
||||||
|
cleanStorage: function () {
|
||||||
|
this.shifts = [];
|
||||||
|
M.toast({html:"Alle Einträge gelöscht",displayLength: this.config.toast_length});
|
||||||
|
},
|
||||||
|
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);
|
||||||
|
M.toast({html:this.saveto + " erstellt.",displayLength: this.config.toast_length});
|
||||||
|
},
|
||||||
|
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();
|
||||||
|
$('#shiftModal').modal('open');
|
||||||
|
},
|
||||||
|
saveChanges: function (changedShift) {
|
||||||
|
this.shifts.splice(this.shifts.indexOf(this.keepShift), 1, changedShift);
|
||||||
|
|
||||||
|
$('#shiftModal').modal('close');
|
||||||
|
this.keepShift = '';
|
||||||
|
this.selectedShift = '';
|
||||||
|
|
||||||
|
},
|
||||||
|
discardChanges: function (changedShift) {
|
||||||
|
$('#shiftModal').modal('close');
|
||||||
|
this.keepShift = '';
|
||||||
|
this.selectedShift = '';
|
||||||
|
},
|
||||||
|
selectRule: function (rule) {
|
||||||
|
this.selectedRule = new Rule(rule.duration, rule.art, rule.title);
|
||||||
|
this.keepRule = rule;
|
||||||
|
|
||||||
|
$('#rule_arten').chips({
|
||||||
|
data: this.selectedRule.Arten
|
||||||
|
});
|
||||||
|
$('#rule_titel').chips({
|
||||||
|
data: this.selectedRule.Titel
|
||||||
|
});
|
||||||
|
$('#ruleModal').modal('open');
|
||||||
|
},
|
||||||
|
saveRule: function (changedRule) {
|
||||||
|
changedRule.Arten = $('#rule_arten').chips('data');
|
||||||
|
changedRule.Titel = $('#rule_titel').chips('data');
|
||||||
|
|
||||||
|
this.rules.splice(this.rules.indexOf(this.keepRule), 1, changedRule);
|
||||||
|
|
||||||
|
$('#ruleModal').modal('close');
|
||||||
|
this.keepRule = '';
|
||||||
|
this.selectedRule = '';
|
||||||
|
},
|
||||||
|
discardRule: function (changedRule) {
|
||||||
|
this.rules.splice(this.rules.indexOf(this.keepRule), 1, this.keepRule);
|
||||||
|
|
||||||
|
$('#ruleModal').modal('close');
|
||||||
|
this.keepRule = '';
|
||||||
|
this.selectedRule = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
directives: {
|
||||||
|
'edit-focus': function (el, value) {
|
||||||
|
if (value) {
|
||||||
|
el.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
44
css/page.css
Normal file
44
css/page.css
Normal 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%;
|
||||||
|
}
|
||||||
217
index.html
Normal file
217
index.html
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
<!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="css/page.css">
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
<div class="navbar-fixed">
|
||||||
|
<nav class="nav-extended indigo">
|
||||||
|
<div class="nav-wrapper container ">
|
||||||
|
<span class="brand-logo">Dienstplan Converter</span>
|
||||||
|
<a href="#" data-activates="mobile-demo" class="button-collapse"><i class="material-icons">menu</i></a>
|
||||||
|
<ul id="nav-mobile" class="right hide-on-med-and-down">
|
||||||
|
<li> </li>
|
||||||
|
</ul>
|
||||||
|
<ul class="side-nav" id="mobile-demo">
|
||||||
|
</ul>
|
||||||
|
<ul class="tabs tabs-transparent">
|
||||||
|
<li class="tab"><a class="active" href="#Dienste">Dienste</a></li>
|
||||||
|
<li class="tab"><a href="#Einstellungen">Regeln</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div id="body">
|
||||||
|
<main class="container">
|
||||||
|
<div class="card" id="Dienste">
|
||||||
|
<div class="card-content" v-if="shifts.length > 0">
|
||||||
|
<h3 class="card-title">Dienste</h3>
|
||||||
|
<table class="highlight" >
|
||||||
|
<thead class="">
|
||||||
|
<tr>
|
||||||
|
<th>Datum</th>
|
||||||
|
<th>Wochentag</th>
|
||||||
|
<th>Titel</th>
|
||||||
|
<th>Uhrzeit</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="s in shifts" :key="s.id" @click="selectShift(s)">
|
||||||
|
<td class="text-xs-right align-middle">
|
||||||
|
{{s.FormattedDatum}}
|
||||||
|
</td>
|
||||||
|
<td class="">
|
||||||
|
{{s.Wochentag}}
|
||||||
|
</td>
|
||||||
|
<td class="">
|
||||||
|
{{s.VEventTitle}}
|
||||||
|
</td>
|
||||||
|
<td class="">
|
||||||
|
{{s.Beginn}} - {{s.Ende}}
|
||||||
|
</td>
|
||||||
|
<td class="">
|
||||||
|
<a class="btn-flat red-text wave" @click.stop="removeShift(s)"><i class="material-icons">delete</i></a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<th colspan="5" class="text-right"> {{shifts.length }} Dienste</th>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="card-content" v-if="shifts.length < 1">
|
||||||
|
Noch keine Dienste da.
|
||||||
|
</div>
|
||||||
|
<div class="card-action" v-if="shifts.length < 1">
|
||||||
|
<label class="btn green tooltipped" data-position="left" data-tooltip="Dienstplan einlesen" for="fileInput">
|
||||||
|
<i class="material-icons">publish</i>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="fixed-action-btn">
|
||||||
|
<a class="btn-floating btn-large orange darken-2 waves-effect">
|
||||||
|
<i class="large material-icons">work</i>
|
||||||
|
</a>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<label class="btn-floating green tooltipped" data-position="left" data-tooltip="Dienstplan einlesen" for="fileInput">
|
||||||
|
<i class="material-icons">publish</i>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button class="btn-floating blue tooltipped" data-position="left" data-tooltip="Kalenderdatei erstellen" @click="createDownloadFile" :disabled=" (remaining > 0) ? null : 'disabled'">
|
||||||
|
<i class="material-icons">description</i>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="btn-floating yellow tooltipped" data-position="left" data-tooltip="Kalenderdatei herunterladen" :href="icsFile" v-bind:class="[ icsFile ? '' : 'disabled']" download="dienstplan.ics" @click="downloadFile">
|
||||||
|
<i class="material-icons">get_app</i>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="btn-floating red tooltipped" data-position="left" data-tooltip="Tabelle leeren" @click="cleanStorage" :disabled=" (remaining > 0) ? null : 'disabled'"><i class="material-icons">clear_all</i></a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="card" id="Einstellungen">
|
||||||
|
<div class="card-content">
|
||||||
|
<h3 class="card-title">Regeln für die Dauer</h3>
|
||||||
|
<div>
|
||||||
|
<span class="chip orange">Titel der Veranstaltung</span>
|
||||||
|
<span class="chip indigo">Art des Dienstes</span>
|
||||||
|
</div>
|
||||||
|
<ul class="collection highlight">
|
||||||
|
<li class="collection-item" v-for="r in rules" :key="rules.indexOf(r)" @click="selectRule(r)">
|
||||||
|
<span v-if="rules.indexOf(r) != 0" class="title">sonst </span>
|
||||||
|
<span class="title" v-if="rules.indexOf(r) < rules.length -1">wenn: </span>
|
||||||
|
<span class="secondary-content">{{ r.duration }} min</span>
|
||||||
|
<div>
|
||||||
|
<span v-if="Array.isArray(r.art)">
|
||||||
|
<span class="chip indigo" :class="{'lighten-3' : r.art.length > 1, 'lighten-2' : r.art.length == 1}" v-for="a in r.art" :key="r.art.indexOf(a)">{{a}}</span>
|
||||||
|
</span>
|
||||||
|
<span v-if="Array.isArray(r.title)">
|
||||||
|
<span class="chip orange" :class="{'lighten-3' : r.title.length > 1, 'lighten-2' : r.title.length == 1}" v-for="t in r.title" :key="r.title.indexOf(t)">{{t}}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
<div id="ruleModal" class="modal bottom-sheet active">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h4>Regel anpassen</h4>
|
||||||
|
<div class="row">
|
||||||
|
<div class="range-field col m6 s12">
|
||||||
|
<label class="active" for="rule_duration">Dauer in Minuten: {{selectedRule.Dauer}}</label>
|
||||||
|
<input id="rule_duration" type="range" v-model="selectedRule.duration" min="30" max="300" step="10">
|
||||||
|
</div>
|
||||||
|
<div class="input-field col s12 m8 l6">
|
||||||
|
<label class="active" for="shift_kind">Arten</label>
|
||||||
|
<div id="rule_arten" class="chips-initial"></div>
|
||||||
|
</div>
|
||||||
|
<div class="input-field col m8 l6 s12">
|
||||||
|
<label class="active" for="rule_titel">Titel</label>
|
||||||
|
<div id="rule_titel" class="chips-inital"></div>
|
||||||
|
</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 bottom-sheet">
|
||||||
|
<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">Titel</label>
|
||||||
|
</div>
|
||||||
|
<div class="input-field col s6 ">
|
||||||
|
<input id="shift_date" type="date" class="datepicker" v-model="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="selectedShift.Beginn" placeholder="Uhrzeit" class="timepicker" :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="selectedShift.Ende" placeholder="Ende" class="timepicker">
|
||||||
|
<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">Titel</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>
|
||||||
|
</div>
|
||||||
|
<input type="file"
|
||||||
|
name="fileInput"
|
||||||
|
id="fileInput"
|
||||||
|
@change="onFileChange"
|
||||||
|
accept="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||||
|
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/materialize/1.0.0/js/materialize.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>
|
||||||
207
js/shift.js
Normal file
207
js/shift.js
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
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 () {
|
||||||
|
var chips = [];
|
||||||
|
this.forEach(function (el, index, array) {
|
||||||
|
chips.push({
|
||||||
|
tag: el,
|
||||||
|
id: index
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return chips;
|
||||||
|
}
|
||||||
|
Array.prototype.fromChipData = function () {
|
||||||
|
var strings = [];
|
||||||
|
this.forEach(function (el, id, array) {
|
||||||
|
strings.push(el.tag);
|
||||||
|
});
|
||||||
|
return strings;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Rule(duration, arten, titel) {
|
||||||
|
this.duration = duration;
|
||||||
|
this.art = arten;
|
||||||
|
this.title = titel;
|
||||||
|
}
|
||||||
|
|
||||||
|
Rule.prototype = {
|
||||||
|
get Dauer() {
|
||||||
|
return this.duration;
|
||||||
|
},
|
||||||
|
set Dauer(value) {
|
||||||
|
this.duration = value;
|
||||||
|
},
|
||||||
|
get Arten() {
|
||||||
|
return this.art.asChipData();
|
||||||
|
},
|
||||||
|
set Arten(value) {
|
||||||
|
this.art = value.fromChipData();
|
||||||
|
},
|
||||||
|
get Titel() {
|
||||||
|
return this.title.asChipData();
|
||||||
|
},
|
||||||
|
set Titel(value) {
|
||||||
|
this.title = value.fromChipData();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Rule.prototype.fits = function (art, title) {
|
||||||
|
var artMatch = false;
|
||||||
|
var nameMatch = false;
|
||||||
|
|
||||||
|
if (this.art.length == 0) artMatch = true;
|
||||||
|
else {
|
||||||
|
this.art.forEach(function (el, i) {
|
||||||
|
if (art.includes(el.toLowerCase())) artMatch = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.title.length == 0) nameMatch = true;
|
||||||
|
else {
|
||||||
|
this.title.forEach(function (el, i) {
|
||||||
|
if (name.includes(el.toLowerCase())) nameMatch = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return artMatch && nameMatch;
|
||||||
|
}
|
||||||
|
Rule.defaults = function () {
|
||||||
|
var rules = [];
|
||||||
|
rules.push(new Rule(60, ['VS'], ['Kinderkonzert']));
|
||||||
|
rules.push(new Rule(120, ['VS'], ["expeditionskonzert", "sinfoniekonzert"]));
|
||||||
|
rules.push(new Rule(150, ['Oa', 'GP'], ['Expeditionskonzert']));
|
||||||
|
rules.push(new Rule(60, ['Oa', 'GP'], ['Expeditionskonzert']));
|
||||||
|
rules.push(new Rule(150, ['Oa', 'OSP'], []));
|
||||||
|
rules.push(new Rule(180, ["VS", "BO", "OHP", "HP", "GP", "Prem", "WA"], []));
|
||||||
|
rules.push(new Rule(60, [], []));
|
||||||
|
rules.push(new Rule(60, [], []));
|
||||||
|
rules.push(new Rule(60, [], []));
|
||||||
|
rules.push(new Rule(60, [], []));
|
||||||
|
rules.push(new Rule(60, [], []));
|
||||||
|
|
||||||
|
return rules;
|
||||||
|
}
|
||||||
|
var DURATION_RULES = Rule.defaults();
|
||||||
|
|
||||||
|
function Shift(kind, name, date, info) {
|
||||||
|
'use strict';
|
||||||
|
this.Datum = date.format(DATE_INPUT_FORMAT);
|
||||||
|
this.Beginn = date.format(TIME_FORMAT);
|
||||||
|
this.Art = kind;
|
||||||
|
this.Beschreibung = info;
|
||||||
|
this.Name = name;
|
||||||
|
Shift.setDurationFromRules(this);
|
||||||
|
}
|
||||||
|
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() {
|
||||||
|
return this._begin.clone().add(this._duration).format(TIME_FORMAT);
|
||||||
|
},
|
||||||
|
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 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) {
|
||||||
|
'use strict';
|
||||||
|
var art = shift.Art.toLowerCase();
|
||||||
|
var name = shift.Name.toLowerCase();
|
||||||
|
var duration = 60;
|
||||||
|
for (var rIndex in DURATION_RULES) {
|
||||||
|
var rule = DURATION_RULES[rIndex];
|
||||||
|
|
||||||
|
if (rule.fits(art, name)) {
|
||||||
|
duration = rule.Dauer;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
shift.Dauer = moment.duration(duration, 'm').locale(MOMENT_LOCALE);
|
||||||
|
}
|
||||||
|
Shift.prototype.toVEvent = function () {
|
||||||
|
'use strict';
|
||||||
|
var end = this._begin.clone().add(this._duration);
|
||||||
|
return new VEvent(TIMEZONE_NAME, this._begin, end, this.VEventTitle, this.Beschreibung);
|
||||||
|
};
|
||||||
|
Shift.thaw = function (jsonShift) {
|
||||||
|
'use strict';
|
||||||
|
moment.locale(MOMENT_LOCALE);
|
||||||
|
var begin = moment(jsonShift._begin);
|
||||||
|
var shift = new Shift(jsonShift._kind, jsonShift._name, begin, jsonShift._description);
|
||||||
|
shift.id = jsonShift.id;
|
||||||
|
shift.Dauer = moment.duration(jsonShift._duration);
|
||||||
|
return shift;
|
||||||
|
};
|
||||||
128
js/vcal.js
Normal file
128
js/vcal.js
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
var VCAL_DATETIME_FORMAT = "YMMDD[T]HHmmss"; //20160216T130500
|
||||||
|
var UID_FORMAT = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx';
|
||||||
|
var NEW_LINE = "\r\n";
|
||||||
|
|
||||||
|
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', window.location.href);
|
||||||
|
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(timezoneName, startMoment, endMoment, title, description) {
|
||||||
|
VMeta.call(this);
|
||||||
|
this.tag = "VEVENT";
|
||||||
|
|
||||||
|
this.set('DTSTART;TZID=' + timezoneName, VMeta.formatDate(startMoment));
|
||||||
|
this.set('DTEND;TZID=' + timezoneName, VMeta.formatDate(endMoment));
|
||||||
|
this.set('DTSTAMP', VMeta.formatDate(moment()) + "Z");
|
||||||
|
this.set('UID', VMeta.generateUID());
|
||||||
|
this.set('SUMMARY', title);
|
||||||
|
this.set('DESCRIPTION', description);
|
||||||
|
this.set('CLASS', "PUBLIC");
|
||||||
|
this.set('TRANSP', "OPAQUE");
|
||||||
|
this.set('STATUS', "CONFIRMED");
|
||||||
|
this.set('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;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user