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.
 
 
 

568 lines
18 KiB

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,
linkTarget: '_blank',
inputs: {
importJson: ''
},
tickets: [],
archive: [],
worktimeTracker: {
tracking: false,
number: 'Worktime',
trackingStarted: null,
trackingStopped: null,
history: []
},
selectedTicket: 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: []
}
},
mounted() {
let vue = this;
this.loadStorage();
this.fetchThemes();
moment.locale('de');
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);
}
},
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 || false) ? true : storedShowPT;
let storedPublicDB = localStorage.getItem('publicDB');
this.publicDB = (storedPublicDB == null || 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 ? 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('publicDB', this.publicDB);
localStorage.setItem('fun', this.fun);
localStorage.setItem('theme', this.theme);
localStorage.setItem('portal', this.portal);
this.$forceUpdate();
},
resetToDefault() {
this.updateStorage();
},
addTrackedTicket() {
let newTicket = {
tracking: false,
number: '#',
trackingStarted: null,
trackingStopped: null,
history: []
};
this.tickets.push(newTicket);
this.updateStorage();
},
startTracking(ticket, individual = false) {
if (!individual) {
this.stopTrackingTicket();
}
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);
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;
}
}
ticket.history.push(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) {
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'));
}
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) {
postFix = ' Stunde';
time = (time / 60).toFixed(2);
}
let plural = '';
if ((time > 1 || time <= 0) && postFix !== ' PT') {
plural = 'n'
}
return time + postFix + plural;
},
stopTrackingTicket() {
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;
},
deleteTicket(index, archive = false) {
if (archive) {
let name = this.archive[index].number;
this.archive.splice(index, 1);
} else {
let name = this.tickets[index].number;
this.tickets.splice(index, 1);
}
iziToast.show({
message: 'Ticket "' + name + '" wurde gelöscht',
color: 'blue'
});
this.updateStorage();
},
archiveTicket(index) {
if (this.tickets[index].tracking) {
this.stopTrackingTicket();
}
this.archive.push(this.tickets[index]);
this.tickets.splice(index, 1);
this.updateStorage();
},
reactivateTicket(index) {
this.tickets.push(this.archive[index]);
this.archive.splice(index, 1);
this.updateStorage();
},
deleteHistoryEntry(ticketIndex, historyIndex) {
if (ticketIndex) {
this.tickets[ticketIndex].history.splice(historyIndex, 1);
} else {
this.selectedTicket.history.splice(historyIndex, 1);
}
this.updateStorage();
},
bookTimeManually(ticket, minutes) {
ticket.history.push({
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.push(ticket);
} else {
tickets.push(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);
});
},
showHistoryForTicket(ticket) {
this.selectedTicket = ticket;
this.$forceUpdate();
setTimeout(() => {
let historyModal = new bootstrap.Modal(document.getElementById('historyModal'));
historyModal.toggle();
}, 50);
},
getPortalLink (test = false) {
let finalPortalName = this.portal.replaceAll('_', '-');
finalPortalName.replaceAll('-test', '');
finalPortalName += test ? '-test' : '';
return 'https://' + finalPortalName + '.vemap.com';
},
collectDataForDay() {
let collection = [];
this.tickets.forEach((ticket) => {
ticket.history.forEach((historyEntry) => {
if (moment(historyEntry.trackingStarted).format("MMM Do YY") === moment().format("MMM Do YY")) {
collection.push(historyEntry);
}
});
});
return collection;
},
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 = '';
console.log(vue.portals.length);
iziToast.show({
message: 'Portalnamen importiert',
color: 'green'
});
this.updateStorage();
}
},
createSnippet() {
let newSnippet = {
content: '',
mode: 'htmlmixed',
id: getRandomID()
};
this.snippets.push(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'
});
}
},
watch: {
publicDB() {
this.updateStorage();
this.$forceUpdate();
},
showPT() {
this.updateStorage();
this.$forceUpdate();
},
theme() {
this.updateStorage();
this.$forceUpdate();
},
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
});
}
}
};
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');