From df8953971070df91c466c8cd1d43021b8477688a Mon Sep 17 00:00:00 2001 From: Christian Seyfferth Date: Mon, 14 Jan 2019 09:57:57 +0100 Subject: [PATCH 01/26] Version Changes --- app.js | 259 +++++++++++++++++++++++++++++++++++++++++++++++++++ css/page.css | 44 +++++++++ index.html | 217 ++++++++++++++++++++++++++++++++++++++++++ js/shift.js | 207 ++++++++++++++++++++++++++++++++++++++++ js/vcal.js | 128 +++++++++++++++++++++++++ 5 files changed, 855 insertions(+) create mode 100644 app.js create mode 100644 css/page.css create mode 100644 index.html create mode 100644 js/shift.js create mode 100644 js/vcal.js diff --git a/app.js b/app.js new file mode 100644 index 0000000..33beb47 --- /dev/null +++ b/app.js @@ -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(); + } + } + } +}) diff --git a/css/page.css b/css/page.css new file mode 100644 index 0000000..ddd794c --- /dev/null +++ b/css/page.css @@ -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%; +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..db2b02b --- /dev/null +++ b/index.html @@ -0,0 +1,217 @@ + + + + + + Dienstplan Converter + + + + + + + + + + + + + +
+ +
+
+
+
+

Dienste

+ + + + + + + + + + + + + + + + + + + + + + + + +
DatumWochentagTitelUhrzeit
+ {{s.FormattedDatum}} + + {{s.Wochentag}} + + {{s.VEventTitle}} + + {{s.Beginn}} - {{s.Ende}} + + delete +
{{shifts.length }} Dienste
+
+
+ Noch keine Dienste da. +
+
+ +
+
+
+ + work + + +
+
+
+

Regeln für die Dauer

+
+ Titel der Veranstaltung + Art des Dienstes +
+
    +
  • + sonst + wenn: + {{ r.duration }} min +
    + + {{a}} + + + {{t}} + +
    + + +
  • +
+
+
+ +
+ + +
+ +
+ + + + + + + + + + + diff --git a/js/shift.js b/js/shift.js new file mode 100644 index 0000000..6b18b20 --- /dev/null +++ b/js/shift.js @@ -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; +}; diff --git a/js/vcal.js b/js/vcal.js new file mode 100644 index 0000000..a581319 --- /dev/null +++ b/js/vcal.js @@ -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; +} From 0cad7ccab488c4843685e15cb3f6743b5ee89b2f Mon Sep 17 00:00:00 2001 From: Christian Seyfferth Date: Tue, 15 Jan 2019 11:35:56 +0100 Subject: [PATCH 02/26] changes for newest dp --- app.js | 3 +-- index.html | 17 +++++++++-------- js/shift.js | 5 +++++ js/vcal.js | 3 ++- 4 files changed, 17 insertions(+), 11 deletions(-) diff --git a/app.js b/app.js index 33beb47..4b7d598 100644 --- a/app.js +++ b/app.js @@ -156,12 +156,11 @@ var app = new Vue({ day = moment(element.Datum); } if (element.hasOwnProperty('Dienst')) { - var time = moment(element.Zeit); + var time = element.Zeit ? moment(element.Zeit) : day; 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)); } }); diff --git a/index.html b/index.html index db2b02b..e45db3e 100644 --- a/index.html +++ b/index.html @@ -46,7 +46,7 @@ Datum Wochentag Titel - Uhrzeit + Dauer @@ -61,11 +61,14 @@ {{s.VEventTitle}} - + {{s.Beginn}} - {{s.Ende}} + + ganzer Tag + - delete + delete @@ -80,9 +83,9 @@ Noch keine Dienste da.
- +
@@ -130,8 +133,6 @@ {{t}}
- - diff --git a/js/shift.js b/js/shift.js index 6b18b20..11ffdd8 100644 --- a/js/shift.js +++ b/js/shift.js @@ -178,6 +178,11 @@ Shift.prototype = { } Shift.setDurationFromRules = function (shift) { '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 name = shift.Name.toLowerCase(); var duration = 60; diff --git a/js/vcal.js b/js/vcal.js index a581319..1fb0b4a 100644 --- a/js/vcal.js +++ b/js/vcal.js @@ -1,6 +1,7 @@ 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(); @@ -63,7 +64,7 @@ function VCalendar(calendarName) { this.tag = "VCALENDAR"; this.set('X-WR-CALNAME', calendarName); this.set('VERSION', '2.0'); - this.set('PRODID', window.location.href); + this.set('PRODID', productID); this.set('METHOD', "PUBLISH"); this.set('X-WR-TIMEZONE', "Europe/Berlin"); this.set('CALSCALE', "GREGORIAN"); From 921bc18061135d8e4c43ed1009c42e7b89e6f255 Mon Sep 17 00:00:00 2001 From: Christian Seyfferth Date: Tue, 15 Jan 2019 12:17:01 +0100 Subject: [PATCH 03/26] slight changes in layout --- index.html | 122 ++++++++++++++++++++++++++++++++--------------------- 1 file changed, 74 insertions(+), 48 deletions(-) diff --git a/index.html b/index.html index e45db3e..6b4b998 100644 --- a/index.html +++ b/index.html @@ -37,55 +37,81 @@
-
-
-

Dienste

- - - - - - - - - - - - - - - - - - - - - - - - - -
DatumWochentagTitelDauer
- {{s.FormattedDatum}} - - {{s.Wochentag}} - - {{s.VEventTitle}} - - {{s.Beginn}} - {{s.Ende}} - - ganzer Tag - - delete -
{{shifts.length }} Dienste
+
+
+
+

Dienste

+ + + + + + + + + + + + + + + + + + + + + + + + + +
DatumWochentagTitelDauer
+ {{s.FormattedDatum}} + + {{s.Wochentag}} + + {{s.VEventTitle}} + + {{s.Beginn}} - {{s.Ende}} + + ganzer Tag + + delete +
{{shifts.length }} Dienste
+
+
-
- Noch keine Dienste da. -
-
- +
+
+

+ Noch keine Dienste da +

+ +
+
+ +
From a89539c9bbf54d8d91a43aa4c63a715a0444ca38 Mon Sep 17 00:00:00 2001 From: Christian Seyfferth Date: Wed, 16 Jan 2019 10:35:46 +0100 Subject: [PATCH 04/26] basic import and export working --- .vscode/launch.json | 22 ++++++++++++ app.js | 82 ++++++++++++++++++++++++++++++++++++--------- index.html | 13 +++---- js/shift.js | 68 ++++++++++++++++++++++++++++++------- js/vcal.js | 41 ++++++++++++++++++----- 5 files changed, 184 insertions(+), 42 deletions(-) create mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..97f0ec7 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,22 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "chrome", + "request": "attach", + "name": "Attach to Chrome", + "port": 9222, + "webRoot": "${workspaceFolder}" + }, + { + "type": "chrome", + "request": "launch", + "name": "Launch Chrome against localhost", + "url": "http://localhost:5500", + "webRoot": "${workspaceFolder}" + }, + ] +} \ No newline at end of file diff --git a/app.js b/app.js index 4b7d598..14dd4e8 100644 --- a/app.js +++ b/app.js @@ -118,38 +118,48 @@ var app = new Vue({ } }, methods: { + makeToast(message) { + M.toast({ + html: message, + displayLength: this.config.toast_length + }); + }, + 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}); + 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, }); - if(workbook.Sheets["Dienstplan"] == null){ - file.status = "error"; + var isErfurterDienstplan = workbook.SheetNames.indexOf("Dienstplan") > -1; + + if(isErfurterDienstplan){ + vm.parseForErfurt(workbook.Sheets["Dienstplan"]); } else { - vm.parseWorksheet(workbook.Sheets["Dienstplan"]); - file.status = "success"; + var sheetName = workbook.SheetNames[0]; + vm.parseForStuttgart(workbook.Sheets[sheetName]); } }; reader.readAsBinaryString(file); - M.toast({html:this.uploadFileName + " wird eingelesen",displayLength: this.config.toast_length}); }, - parseWorksheet: function (dp) { + parseForErfurt: function (dp) { var arr = XLSX.utils.sheet_to_row_object_array(dp, { range: 1 }); var vm = this; var day; + + this.makeToast("Erfurter Dienstplan erkannt."); + arr.forEach(element => { moment.locale(vm.config.moment.parse_language); if (element.hasOwnProperty('Datum')) { @@ -157,11 +167,51 @@ var app = new Vue({ } if (element.hasOwnProperty('Dienst')) { var time = element.Zeit ? moment(element.Zeit) : day; - 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(); - vm.addShift(new Shift(art, name, date, bemerkung)); + var termin = { + datum: day.clone().hour(time.hour()).minute(time.minute()), + art: element.Dienst ? element.Dienst.trim() : "", + beschreibung: element.Bemerkung ? element.Bemerkung.trim(): "", + name: element.__EMPTY ? element.__EMPTY.trim() : "" + } + vm.addShift(new Shift(termin)); + } + }); + }, + 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 time = day.clone(); + var end = null; + if (typeof(element.D) === "object") { + time = moment(element.D); + } else if(element.D.indexOf("-") > -1 ) { + var tArray = element.D.split(" - "); + time = moment(tArray[0],"HH:mm"); + end = moment(tArray[1], "HH:mm"); + } + var termin = { + datum: day.clone().hour(time.hour()).minute(time.minute()), + ende: end ? end.format("HH:mm"): null, + ort: element.H ? element.H.trim() : "", + art: element.E ? element.E.trim() : "", + beschreibung: element.H ? element.H.trim(): "", + name: element.F ? element.F.trim() : "" + } + vm.addShift(new Shift(termin)); + } }); }, @@ -170,11 +220,11 @@ var app = new Vue({ }, removeShift: function (shift) { this.shifts.splice(this.shifts.indexOf(shift), 1); - M.toast({html:shift.VEventTitle + " gelöscht",displayLength: this.config.toast_length}); + this.makeToast(shift.VEventTitle + " gelöscht"); }, cleanStorage: function () { this.shifts = []; - M.toast({html:"Alle Einträge gelöscht",displayLength: this.config.toast_length}); + this.makeToast("Alle Einträge gelöscht"); }, createDownloadFile: function () { var vCal = new VCalendar("Dienstplan Kalender"); @@ -191,7 +241,7 @@ var app = new Vue({ } this.icsFile = window.URL.createObjectURL(this.blob); - M.toast({html:this.saveto + " erstellt.",displayLength: this.config.toast_length}); + this.makeToast(this.saveto + " erstellt."); }, downloadFile: function () { if (window.navigator.msSaveOrOpenBlob) { diff --git a/index.html b/index.html index 6b4b998..8dbe512 100644 --- a/index.html +++ b/index.html @@ -29,7 +29,7 @@
@@ -37,10 +37,10 @@
-
+
-

Dienste

+

Termine

@@ -61,6 +61,7 @@ - +
{{s.VEventTitle}} + {{ s.Ort }} {{s.Beginn}} - {{s.Ende}} @@ -75,7 +76,7 @@
{{shifts.length }} Dienste {{shifts.length }} Dienste
@@ -96,14 +97,14 @@ - Dienste löschen + Termine löschen

- Noch keine Dienste da + Noch keine Termine da

diff --git a/js/shift.js b/js/shift.js index 11ffdd8..d0230a3 100644 --- a/js/shift.js +++ b/js/shift.js @@ -88,15 +88,42 @@ Rule.defaults = function () { } 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); +function Shift() { + 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 = { + 'art' : "", + 'name' : "DUMMY", + 'datum' : 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.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); + } + } + } + Shift.prototype = { get Wochentag() { return this._date.clone().locale(MOMENT_LOCALE).format(WEEKDAY_FORMAT); @@ -153,6 +180,13 @@ Shift.prototype = { 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; }, @@ -197,15 +231,25 @@ Shift.setDurationFromRules = function (shift) { 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); + return new VEvent({ + startMoment: this._begin, + endMoment: end, + title: this.VEventTitle, + description: this.Beschreibung, + location: this.Ort + }); }; 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); + var shift = new Shift( + { + art: jsonShift._kind, + name: jsonShift._name, + datum: begin, + beschreibung: jsonShift._description + }); shift.id = jsonShift.id; shift.Dauer = moment.duration(jsonShift._duration); return shift; diff --git a/js/vcal.js b/js/vcal.js index 1fb0b4a..b8e4c78 100644 --- a/js/vcal.js +++ b/js/vcal.js @@ -72,21 +72,46 @@ function VCalendar(calendarName) { VCalendar.prototype = Object.create(VMeta.prototype); VCalendar.prototype.constructor = VCalendar; -function VEvent(timezoneName, startMoment, endMoment, title, description) { +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=' + 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('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', ""); + this.set('ORGANIZER', options.organizer); } + VEvent.prototype = Object.create(VMeta.prototype); VEvent.prototype.constructor = VEvent; From f64ac02ec396d3a3718a88c5fe894aba19781ec0 Mon Sep 17 00:00:00 2001 From: Christian Seyfferth Date: Wed, 16 Jan 2019 11:06:18 +0100 Subject: [PATCH 05/26] =?UTF-8?q?Stuttgarter=20Termine=20haben=20jetzt=20n?= =?UTF-8?q?ur=20ende=20wenn=20an=20Konstruktor=20=C3=BCbergeben?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.js | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/app.js b/app.js index 14dd4e8..0c67038 100644 --- a/app.js +++ b/app.js @@ -193,23 +193,22 @@ var app = new Vue({ day = moment(element.C); } if (element.hasOwnProperty('D') && moment(element.D, "HH:mm").isValid()) { - var time = day.clone(); - var end = null; - if (typeof(element.D) === "object") { - time = moment(element.D); - } else if(element.D.indexOf("-") > -1 ) { - var tArray = element.D.split(" - "); - time = moment(tArray[0],"HH:mm"); - end = moment(tArray[1], "HH:mm"); - } var termin = { - datum: day.clone().hour(time.hour()).minute(time.minute()), - ende: end ? end.format("HH:mm"): null, 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)); } From e9c129e2eed054cb2e312b88c150b3332572c82d Mon Sep 17 00:00:00 2001 From: Christian Seyfferth Date: Wed, 16 Jan 2019 11:19:30 +0100 Subject: [PATCH 06/26] Timepicker Init-Options --- app.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/app.js b/app.js index 0c67038..a0a55ff 100644 --- a/app.js +++ b/app.js @@ -23,15 +23,18 @@ if (!FileReader.prototype.readAsBinaryString) { reader.readAsArrayBuffer(file); } } + +var options = { + timepicker: { + twelveHour: false, + } +}; + document.addEventListener('DOMContentLoaded', function() { M.AutoInit(); + var elems = document.querySelectorAll('.timepicker'); + var timepickers = M.Timepicker.init(elems, options.timepicker); }); -/* $(document).ready(function () { - $('.modal').modal(); - $('.tabs').tabs(); - $('.fixed-action-btn').floatingActionButton(); -}); */ - //localStorage persistence var SHIFT_STORAGE_KEY = "dienstplan_chrosey"; From b1b60ee95f8cc49da16a1dfca38ae2ca04aae320 Mon Sep 17 00:00:00 2001 From: Christian Seyfferth Date: Wed, 16 Jan 2019 11:19:52 +0100 Subject: [PATCH 07/26] Termin-Modal changes --- index.html | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/index.html b/index.html index 8dbe512..ab5ed96 100644 --- a/index.html +++ b/index.html @@ -199,23 +199,27 @@
- +
- +
- +
- + +
+
+ +
From 0624a29b0b7173e1fe1ae694703ffbe3549a0ac4 Mon Sep 17 00:00:00 2001 From: Christian Seyfferth Date: Wed, 16 Jan 2019 17:34:00 +0100 Subject: [PATCH 08/26] refactoring and reworking how to edit rules --- app.js | 122 +++++++++++++++++++++++++++++++++++++++++----------- index.html | 59 +++++++++++++++---------- js/shift.js | 105 +++++++++++++++++++++++++------------------- 3 files changed, 193 insertions(+), 93 deletions(-) diff --git a/app.js b/app.js index a0a55ff..e2faed7 100644 --- a/app.js +++ b/app.js @@ -64,7 +64,14 @@ var shiftStorage = { var ruleStorage = { fetch: function () { 'use strict'; - var rules = JSON.parse(localStorage.getItem(RULE_STORAGE_KEY)) || Rule.defaults(); + 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; }, @@ -75,6 +82,60 @@ var ruleStorage = { } }; +Vue.component('chip-input', { + template: ` +
+ `, + 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: { @@ -85,8 +146,10 @@ var app = new Vue({ dp_sheet: '', deletedShift: '', remaining: shiftStorage.count(), - selectedShift: '', - selectedRule: '', + selectedShift: new Shift({}), + selectedShiftIndex: -1, + selectedRule: new Rule({}), + selectedRuleIndex: -1, saveto: 'dienstplan.ics', uploadFileName: "", config: { @@ -99,7 +162,7 @@ var app = new Vue({ display_language: 'de' }, toast_length: 3000 - } + }, }, watch: { shifts: { @@ -107,7 +170,7 @@ var app = new Vue({ 'use strict'; shiftStorage.save(shifts); this.remaining = shifts.length; - M.toast({html:"Änderungen gespeichert.", displayLength:this.config.toast_length}); + this.makeToast("Änderungen gespeichert."); }, deep: true }, @@ -115,12 +178,19 @@ var app = new Vue({ handler: function (rules) { 'use strict'; ruleStorage.save(rules); - M.toast({html:"Änderungen gespeichert.", displayLength:this.config.toast_length}); + this.makeToast("Änderungen gespeichert."); }, deep: true } }, methods: { + updateArten(value){ + this.selectedRule.Arten = value; + }, + updateTitel(value){ + this.selectedRule.Titel = value; + }, + makeToast(message) { M.toast({ html: message, @@ -134,6 +204,7 @@ var app = new Vue({ 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; @@ -154,6 +225,7 @@ var app = new Vue({ }; reader.readAsBinaryString(file); }, + parseForErfurt: function (dp) { var arr = XLSX.utils.sheet_to_row_object_array(dp, { range: 1 @@ -180,6 +252,7 @@ var app = new Vue({ } }); }, + parseForStuttgart: function (dp) { var arr = XLSX.utils.sheet_to_json(dp, { header: "A", @@ -217,17 +290,21 @@ var app = new Vue({ } }); }, + 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) { @@ -245,58 +322,51 @@ var app = new Vue({ 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(); $('#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 - }); + editRule: function (rule) { + this.selectedRule = Rule.thaw(rule); + this.selectedRuleIndex = this.rules.indexOf(rule); $('#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); + saveRule: function () { + this.rules[this.selectedRuleIndex] = this.selectedRule; $('#ruleModal').modal('close'); - this.keepRule = ''; - this.selectedRule = ''; + this.selectedRule = new Rule(); }, - discardRule: function (changedRule) { - this.rules.splice(this.rules.indexOf(this.keepRule), 1, this.keepRule); + discardRule: function () { $('#ruleModal').modal('close'); - this.keepRule = ''; - this.selectedRule = ''; + + this.selectedRule = new Rule(); } }, diff --git a/index.html b/index.html index ab5ed96..2fa7980 100644 --- a/index.html +++ b/index.html @@ -38,6 +38,7 @@
+

Termine

@@ -140,28 +141,36 @@
-
-
-

Regeln für die Dauer

+
+
+

Regeln für die Dauer

Titel der Veranstaltung Art des Dienstes
- +
+ + {{a.tag}} + + + + {{t.tag}} + + +
+
+
@@ -170,17 +179,21 @@