You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
This repo is archived. You can view files and clone it, but cannot push or open issues/pull-requests.
 
 
 

858 lines
29 KiB

Array.prototype.pushToBeginning = function (toPush) {
return this.unshift.apply(this, [toPush]);
}
const TimeTrack = {
data() {
return {
experimental: {
trackWorktime: false,
boardView: false,
snippetSpace: false,
portalSwitcher: true
},
view: 'board',
theme: 'materia',
themes: null,
dashboardLogo: 'assets/img/logo.png',
ticketSystemUrl: '',
showPT: true,
dontShowMinutes: false,
linkTarget: '_blank',
inputs: {
importJson: ''
},
tickets: [],
archive: [],
trashed: {},
worktimeTracker: {
tracking: false,
number: 'Worktime',
trackingStarted: null,
trackingStopped: null,
history: []
},
selectedTracker: null,
searchQuery: '',
portal: '',
portals: null,
importStringForPortals: '',
publicDB: false,
fun: false,
sounds: {
bad: [
'alert',
'wilhelm',
// 'wtf',
// 'jesus_wtf'
],
animals: [
// 'meow',
// 'moo',
// 'quack',
// 'pika',
'transition'
]
},
snippets: [],
codeMirrors: [],
customBookingValue: '',
customDateForPastDays: '',
newTaskInput: ''
}
},
mounted() {
let vue = this;
this.loadStorage();
this.fetchThemes();
moment.locale('de');
this.customDateForPastDays = moment().format();
setInterval(() => {
vue.$forceUpdate();
}, 1000 * 60);
vue.loadTooltips();
setInterval(() => {
vue.loadTooltips();
}, 5000);
if (this.experimental.snippetSpace) {
this.snippets.forEach((snippet) => {
vue.loadSnippet(snippet);
});
setTimeout(() => {
vue.updateStorage();
}, 5000);
}
setInterval(() => {
vue.checkTimeBoxes();
}, 10000);
if (this.fun && localStorage.getItem('noJokes') === null) {
let jokeService = new JokeService();
setInterval(() => {
jokeService.tell();
}, 1_800_000)
}
},
methods: {
loadTooltips() {
let tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'))
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl)
});
},
loadStorage() {
let storedTickets = JSON.parse(localStorage.getItem('tickets'));
this.tickets = storedTickets == null ? [] : storedTickets;
let storedArchive = JSON.parse(localStorage.getItem('archive'));
this.archive = storedArchive == null ? [] : storedArchive;
let storedTicketSystemUrl = localStorage.getItem('ticketSystemUrl');
this.ticketSystemUrl = storedTicketSystemUrl == null ? '' : storedTicketSystemUrl;
let storedShowPT = localStorage.getItem('showPT');
this.showPT = (storedShowPT == null || storedShowPT === 'false') ? true : storedShowPT;
let storedDontShowMinutes = localStorage.getItem('dontShowMinutes');
this.dontShowMinutes = (storedDontShowMinutes == null || storedDontShowMinutes === 'false') ? false : Boolean(storedDontShowMinutes);
let storedPublicDB = localStorage.getItem('publicDB');
storedPublicDB = storedPublicDB === 'true';
this.publicDB = (storedPublicDB == null || storedPublicDB === 'false') ? false : storedPublicDB;
let storedTheme = localStorage.getItem('theme');
this.theme = storedTheme == null ? 'materia' : storedTheme;
let storedPortal = localStorage.getItem('portal');
this.portal = storedPortal == null ? '' : storedPortal;
let storedPortals = JSON.parse(localStorage.getItem('portals'));
this.portals = storedPortals == null ? [] : storedPortals;
let storedFun = localStorage.getItem('fun');
this.fun = storedFun == null || storedFun === 'false' ? false : storedFun;
// let storedSnippets = JSON.parse(localStorage.getItem('snippets'));
// this.snippets = storedSnippets == null ? [] : storedSnippets;
},
updateStorage() {
localStorage.setItem('tickets', JSON.stringify(this.tickets));
localStorage.setItem('archive', JSON.stringify(this.archive));
localStorage.setItem('portals', JSON.stringify(this.portals));
localStorage.setItem('ticketSystemUrl', this.ticketSystemUrl);
localStorage.setItem('showPT', this.showPT);
localStorage.setItem('dontShowMinutes', Boolean(this.dontShowMinutes));
localStorage.setItem('publicDB', this.publicDB);
localStorage.setItem('fun', this.fun);
localStorage.setItem('theme', this.theme);
localStorage.setItem('portal', this.portal);
this.$forceUpdate();
},
resetToDefault() {
this.updateStorage();
},
createTracker() {
this.tickets.pushToBeginning({
tracking: false,
number: '#',
trackingStarted: null,
trackingStopped: null,
history: []
});
this.updateStorage();
},
createTimeBox() {
this.tickets.pushToBeginning({
tracking: false,
number: 'Timebox ',
trackingStarted: null,
trackingStopped: null,
isTimeBox: true,
history: []
});
this.updateStorage();
},
startTimeBox(ticket, minutes) {
Notification.requestPermission();
this.startTracking(ticket, false, minutes);
},
startTracking(ticket, individual = false, timeBoxMinutes = null) {
if (!individual) {
this.stopActiveTracker();
}
if (timeBoxMinutes) {
ticket.timeBoxMinutes = timeBoxMinutes;
}
ticket.status = 'wip';
ticket.trackingStarted = moment();
ticket.tracking = true;
let noNoTickets = ['#1920', '#3110', '#2492', '#2419', '#1256'];
if (this.fun && noNoTickets.includes(ticket.number)) {
playSound(oneOf(this.sounds.bad));
}
this.$forceUpdate();
this.updateStorage();
},
stopTracking(ticket) {
// console.log(ticket);
let component = this;
ticket.trackingStopped = moment();
ticket.tracking = false;
let minutesSpent = moment.duration(
ticket.trackingStopped.diff(ticket.trackingStarted)
).as('minutes');
if (minutesSpent > 0) {
let historyEntry = {
trackingStarted: ticket.trackingStarted,
trackingStopped: ticket.trackingStopped,
manually: false,
minutes: Math.round(minutesSpent)
};
// console.log(historyEntry);
if (this.experimental.trackWorktime) {
if (ticket.paused) {
historyEntry.pause = true;
}
}
if (this.ticketSystemUrl && this.isTicketNumber(ticket.number)) {
iziToast.show({
message: 'Buchung gespeichert',
color: 'blue',
buttons: [
['<button><img src="'+component.ticketSystemIcon+'" class="ticket-icon"/></button>', function (instance, toast) {
instance.hide({
transitionOut: 'fadeOutUp',
onClosing: function () {
window.open(
component.ticketSystemUrl + ticket.number.replace('#', '').trim()+'/time_entries/new?time_entry[hours]='+(Math.round(minutesSpent)/60),
'_blank'
);
}
}, toast, 'buttonName');
}, true]
]
});
}
ticket.history.pushToBeginning(historyEntry);
}
ticket.trackingStarted = null;
ticket.trackingStopped = null;
this.$forceUpdate();
this.updateStorage();
},
pauseTracking(ticket) {
ticket.trackingStopped = moment();
ticket.tracking = false;
ticket.paused = true;
this.stopTracking(ticket);
},
resumeTracking(ticket) {
ticket.trackingStarted = moment();
ticket.tracking = true;
ticket.paused = false;
let noNoTickets = ['#1920', '#3110', '#2492', '#2419', '#1256'];
if (this.fun && noNoTickets.includes(ticket.number)) {
playSound(oneOf(this.sounds.bad))
}
this.$forceUpdate();
this.updateStorage();
},
formattedDate(date) {
return moment(date).format('llll');
},
exactTimestamp(date) {
return moment(date).format('LTS');
},
currentTrackingRunningFor(ticket) {
return this.timeWithPostFix(Math.round(moment.duration(moment().diff(ticket.trackingStarted)).as('minutes')));
},
getTotalTime(ticket, raw = false) {
let totalTime = 0;
if (ticket.history.length > 0) {
ticket.history.forEach(function (historyEntry) {
totalTime += Math.round(historyEntry.minutes);
});
}
if (ticket.tracking) {
totalTime += Math.round(moment.duration(moment().diff(ticket.trackingStarted)).as('minutes'));
}
if (raw) {
return totalTime;
} else {
return this.timeWithPostFix(totalTime);
}
},
getTotalTimeToday(ticket) {
let totalTime = 0;
if (ticket.history.length > 0) {
ticket.history.forEach(function (historyEntry) {
if (moment(historyEntry.trackingStarted).format("MMM Do YY") === moment().format("MMM Do YY")) {
totalTime += Math.round(historyEntry.minutes);
}
});
}
if (ticket.tracking) {
totalTime += Math.round(moment.duration(moment().diff(ticket.trackingStarted)).as('minutes'));
}
return this.timeWithPostFix(totalTime);
},
timeWithPostFix(time) {
let postFix = ' Minute';
if (time >= 480 && this.showPT) {
postFix = ' PT';
time = (time / 480).toFixed(1);
} else if (time >= 60 || this.dontShowMinutes) {
postFix = ' Stunde';
time = (time / 60).toFixed(2);
}
let plural = '';
if (((time > 1 || time <= 0) || this.dontShowMinutes) && postFix !== ' PT') {
plural = 'n'
}
return time + postFix + plural;
},
stopActiveTracker() {
let vue = this;
vue.tickets.forEach(function (ticket) {
if (ticket.tracking === true) {
vue.stopTracking(ticket);
}
})
},
getTrackingStartTime(ticket) {
return moment(ticket.trackingStarted).format('LT');
},
isTicketNumber(number) {
return number.indexOf('#') >= 0;
},
deleteTracker(index, archive = false) {
let component = this;
let message = '';
if (archive) {
Object.assign(this.trashed, this.archive[index]);
let name = this.archive[index].number;
message = 'Tracker "' + name + '" wurde gelöscht';
this.archive.splice(index, 1);
} else {
Object.assign(this.trashed, this.tickets[index]);
let name = this.tickets[index].number;
message = 'Tracker "' + name + '" wurde gelöscht';
this.tickets.splice(index, 1);
}
iziToast.show({
message: message,
color: 'blue',
buttons: [
['<button><i class="fas fa-undo"></i></button>', function (instance, toast) {
instance.hide({
transitionOut: 'fadeOutUp',
onClosing: function(instance, toast, closedBy){
component.restoreTrashed();
}
}, toast, 'buttonName');
}, true]
]
});
this.updateStorage();
},
restoreTrashed() {
let restoredTracker = {};
Object.assign(restoredTracker, this.trashed);
this.trashed = {};
this.tickets.pushToBeginning(restoredTracker);
this.updateStorage();
},
archiveTracker(index) {
if (this.tickets[index].tracking) {
this.stopActiveTracker();
}
this.archive.pushToBeginning(this.tickets[index]);
this.tickets.splice(index, 1);
this.updateStorage();
},
reactivateTicket(index) {
this.tickets.pushToBeginning(this.archive[index]);
this.archive.splice(index, 1);
this.updateStorage();
},
deleteHistoryEntry(ticketIndex, historyIndex) {
if (ticketIndex) {
this.tickets[ticketIndex].history.splice(historyIndex, 1);
} else {
this.selectedTracker.history.splice(historyIndex, 1);
}
this.updateStorage();
},
bookTimeManually(ticket, minutes) {
ticket.history.pushToBeginning({
trackingStarted: moment(),
trackingStopped: moment(),
manually: true,
minutes: Math.round(minutes)
});
this.updateStorage();
},
importData() {
let json = JSON.parse(this.inputs.importJson);
if (json.trackedTickets) {
this.tickets = this.extractTicketsFromLegacyJson(json.trackedTickets);
this.archive = this.extractArchivedTicketsFromLegacyJson(json.trackedTickets);
} else {
this.tickets = json.tickets;
this.archive = json.archive ?? [];
}
this.ticketSystemUrl = json.redmineUrl ?? json.ticketSystemUrl;
this.showPT = json.showPT;
this.theme = json.theme;
this.updateStorage();
location.reload();
},
extractTicketsFromLegacyJson(tickets) {
return this.extractTickets(tickets);
},
extractArchivedTicketsFromLegacyJson(tickets) {
return this.extractTickets(tickets, true);
},
extractTickets(ticketCollection, forArchive = false) {
let tickets = [];
let archive = [];
ticketCollection.forEach((ticket) => {
if (ticket.archived || (ticket.active && ticket.active === false)) {
archive.pushToBeginning(ticket);
} else {
tickets.pushToBeginning(ticket)
}
});
return forArchive ? archive : tickets;
},
copy2Clipboard() {
let copyText = document.getElementById("exportJsonInput");
copyText.select();
copyText.setSelectionRange(0, 99999);
document.execCommand("copy");
},
fetchThemes() {
let vue = this;
axios.get(
'https://bootswatch.com/api/5.json'
).then((response) => {
vue.themes = response.data.themes;
}).catch((error) => {
console.log(error);
});
},
showHistoryForTracker(tracker) {
this.selectedTracker = tracker;
this.$forceUpdate();
setTimeout(() => {
let historyModal = new bootstrap.Modal(document.getElementById('historyModal'));
historyModal.toggle();
}, 50);
},
openTasksForTracker(tracker) {
this.selectedTracker = tracker;
this.$forceUpdate();
setTimeout(() => {
let tasksModal = new bootstrap.Modal(document.getElementById('trackerTasksModal'));
tasksModal.toggle();
}, 50);
},
showCustomBookingForTracker(ticket) {
this.selectedTracker = ticket;
this.$forceUpdate();
setTimeout(() => {
let customBookingModal = new bootstrap.Modal(document.getElementById('customBookingModal'));
customBookingModal.toggle();
}, 50);
},
makeCustomBooking(subtract = false) {
if (subtract) {
this.customBookingValue -= (this.customBookingValue * 2)
}
iziToast.show({
message: 'Buchung erfolgreich',
color: 'green'
});
this.bookTimeManually(this.selectedTracker, this.customBookingValue);
},
getPortalLink (test = false) {
let finalPortalName = this.portal.replaceAll('_', '-');
finalPortalName.replaceAll('-test', '');
finalPortalName += test ? '-test' : '';
return 'https://' + finalPortalName + '.vemap.com';
},
collectDataForDay(subtractDays = 0, customDate = false) {
let day = moment().subtract(subtractDays, "days").format("MMM Do YY");
let collection = [];
if (customDate) {
day = moment(customDate).format("MMM Do YY");
}
this.tickets.forEach((ticket) => {
ticket.history.forEach((historyEntry) => {
if (moment(historyEntry.trackingStarted).format("MMM Do YY") === day) {
let newEntry = {};
Object.assign(newEntry, historyEntry);
newEntry.ticket = ticket.number;
let existingEntry = this.getCollectionItemWithValue(collection, 'ticket', ticket.number);
if (existingEntry) {
existingEntry.minutes = Number(existingEntry.minutes) + Number(newEntry.minutes);
} else {
collection.pushToBeginning(newEntry);
}
}
});
});
this.archive.forEach((ticket) => {
ticket.history.forEach((historyEntry) => {
if (moment(historyEntry.trackingStarted).format("MMM Do YY") === day) {
let newEntry = {};
Object.assign(newEntry, historyEntry);
newEntry.ticket = ticket.number;
let existingEntry = this.getCollectionItemWithValue(collection, 'ticket', ticket.number);
if (existingEntry) {
existingEntry.minutes = Number(existingEntry.minutes) + Number(newEntry.minutes);
} else {
collection.pushToBeginning(newEntry);
}
}
});
});
return collection;
},
getCollectionItemWithValue(collection, property, value) {
let found = false;
collection.forEach((item) => {
if (item[property] === value) {
found = item;
}
})
return found;
},
sendPortalChangeRequest() {
let vue = this;
let publicDBParam = this.publicDB ? '&publicDB=1' : '';
axios.get(
'https://settings.vemap.docker/?portal2change=' + this.portal + publicDBParam
).then((response) => {
// console.log(response)
}).catch((error) => {
// An error is expected here due to apache restarting
iziToast.show({
message: 'Portal-Wechsel erfolgreich',
color: 'green'
});
if (vue.fun) {
playSound(oneOf(vue.sounds.animals));
}
vue.updateStorage();
})
},
importPortalsJson() {
let vue = this;
if (this.importStringForPortals !== '') {
this.portals = JSON.parse(this.importStringForPortals);
this.importStringForPortals = '';
iziToast.show({
message: 'Portalnamen importiert',
color: 'green'
});
this.updateStorage();
}
},
createSnippet() {
let newSnippet = {
content: '',
mode: 'htmlmixed',
id: getRandomID()
};
this.snippets.pushToBeginning(newSnippet);
this.loadSnippet(this.snippets[this.snippets.length-1]);
this.updateStorage();
},
loadSnippet(snippet) {
setTimeout(() => {
let element = document.getElementById(snippet.id);
if (!element) {
console.log('Textarea with id '+snippet.id+' not found');
return false;
}
snippet.mirrorInstance = CodeMirror.fromTextArea(element, {
theme: 'darcula',
lineNumbers: true,
mode: snippet.mode,
value: snippet.content
});
setInterval(() => {
snippet.content = snippet.mirrorInstance.doc.getValue();
}, 1000);
}, 100);
},
deleteSnippet(index) {
this.snippets.splice(index, 1);
iziToast.show({
message: 'Snippet wurde gelöscht',
color: 'blue'
});
this.updateStorage();
},
copySnippet(snippet) {
let code = document.getElementById(snippet.id);
code.select();
code.setSelectionRange(0, 99999);
document.execCommand("copy");
iziToast.show({
message: 'Code kopiert!',
color: 'green'
});
},
checkTimeBoxes() {
let vue = this;
this.tickets.forEach((ticket) => {
if (ticket.isTimeBox && !ticket.notificated && vue.timeBoxTimeLeft(ticket) <= 0) {
ticket.notificated = true;
vue.stopTracking(ticket);
// alert('Zeit für "'+ticket.number+'" ist abgelaufen!');
if (Notification.permission === 'granted') {
new Notification('Timebox zu Ende', {
body: 'Zeit für "'+ticket.number+'" ist abgelaufen!',
icon: '/timetrack/assets/img/favicon.ico'
});
} else {
iziToast.show({
message: 'Zeit für "'+ticket.number+'" ist abgelaufen!',
color: 'red'
});
}
}
});
},
timeBoxTimeLeft(ticket) {
return Number(ticket.timeBoxMinutes - this.getTotalTime(ticket, true));
},
validateBooleans(value) {
return value === 'true' || value === true;
},
tellJoke(category, language = 'en') {
let jokeService = new JokeService(category ?? undefined, language);
jokeService.tell();
},
addTask() {
if (this.newTaskInput.length <= 0 || this.newTaskInput.trim() === '') {
return false;
}
if (!this.selectedTracker.tasks) {
this.selectedTracker.tasks = [];
}
this.selectedTracker.tasks.pushToBeginning({
name: this.newTaskInput,
done: false,
created: moment(),
percentDone: 0,
finished: null
});
this.newTaskInput = '';
this.updateStorage();
},
deleteTask(taskIndex) {
this.selectedTracker.tasks.splice(taskIndex, 1)
this.updateStorage();
},
toggleTask(task) {
task.done = !task.done;
if (task.done) {
task.finished = moment();
task.percentDone = 100;
} else {
task.finished = null;
task.percentDone = 0;
}
this.updateStorage();
},
checkForCompletionOfTask(task) {
task.done = task.percentDone == 100;
this.$forceUpdate();
this.updateStorage();
},
getOpenTasksForTracker(tasks) {
if (!tasks) {
return 0;
}
let counter = 0;
tasks.forEach((task) => {
if (!task.done) {
counter++;
}
});
return counter;
},
showOpenTasksForTracker(tasks) {
let count = this.getOpenTasksForTracker(tasks);
return count > 0 ? ' ('+count+' offen)' : '';
}
},
beforeDestroy() {
this.stopActiveTracker();
},
watch: {
publicDB() {
this.publicDB = this.validateBooleans(this.publicDB)
this.updateStorage();
this.$forceUpdate();
},
showPT() {
this.showPT = this.validateBooleans(this.showPT)
this.updateStorage();
this.$forceUpdate();
},
dontShowMinutes() {
this.dontShowMinutes = this.validateBooleans(this.dontShowMinutes)
this.updateStorage();
this.$forceUpdate();
},
theme() {
this.updateStorage();
this.$forceUpdate();
},
fun() {
this.fun = this.validateBooleans(this.fun)
this.updateStorage();
this.$forceUpdate();
}
},
computed: {
exportJson() {
return JSON.stringify({
tickets: this.tickets,
archive: this.archive,
ticketSystemUrl: this.ticketSystemUrl,
showPT: this.showPT,
fun: this.fun,
theme: this.theme
});
},
ticketSystemIcon() {
if (this.ticketSystemUrl) {
if (this.ticketSystemUrl.search('redmine')) {
return 'assets/img/redmine.png';
}
if (this.ticketSystemUrl.search('jira')) {
return 'assets/img/jira.png';
}
if (this.ticketSystemUrl.search('gitlab')) {
return 'assets/img/gitlab.png';
}
}
}
}
};
function oneOf(collection) {
return collection[Math.floor(Math.random()*collection.length)];
}
function playSound(sound, extension = null) {
let audio = new Audio('/timetrack/assets/audio/' + sound + (extension ?? '.mp3'));
audio.play();
}
function getRandomID() {
let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 10; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
const TimeTrackApp = Vue.createApp(TimeTrack).mount('#root');
function iHateToHaveFun() {
localStorage.setItem('noJokes', 1);
}