Compare commits

..

28 Commits

  1. 3
      .idea/timetrack.iml
  2. BIN
      assets/img/gitlab.png
  3. BIN
      assets/img/jira.png
  4. BIN
      assets/img/redmine.png
  5. 17
      css/app.css
  6. 1050
      index.html
  7. 241
      js/app.js

3
.idea/timetrack.iml

@ -5,5 +5,8 @@
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="izitoast" level="application" /> <orderEntry type="library" name="izitoast" level="application" />
<orderEntry type="library" name="quill" level="application" />
<orderEntry type="library" name="quill.snow" level="application" />
<orderEntry type="library" name="quill.bubble" level="application" />
</component> </component>
</module> </module>

BIN
assets/img/gitlab.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
assets/img/jira.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
assets/img/redmine.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

17
css/app.css

@ -1,4 +1,5 @@
body { body {
background-color: #e7e7e7;
overflow-y: auto; overflow-y: auto;
} }
@ -221,3 +222,19 @@ nav, .card {
bottom: 0; bottom: 0;
left: 0; left: 0;
} }
.finished-task {
text-decoration: line-through;
}
#trackerTasksModal a li {
color: black !important;
}
#trackerTasksModal input[type=range] {
width: 100%;
}
.ticket-icon {
max-width: 24px;
}

1050
index.html

File diff suppressed because it is too large Load Diff

241
js/app.js

@ -1,3 +1,7 @@
Array.prototype.pushToBeginning = function (toPush) {
return this.unshift.apply(this, [toPush]);
}
const TimeTrack = { const TimeTrack = {
data() { data() {
return { return {
@ -7,8 +11,7 @@ const TimeTrack = {
snippetSpace: false, snippetSpace: false,
portalSwitcher: true portalSwitcher: true
}, },
view: 'trackers', view: 'board',
showHistory: false,
theme: 'materia', theme: 'materia',
themes: null, themes: null,
dashboardLogo: 'assets/img/logo.png', dashboardLogo: 'assets/img/logo.png',
@ -21,6 +24,7 @@ const TimeTrack = {
}, },
tickets: [], tickets: [],
archive: [], archive: [],
trashed: {},
worktimeTracker: { worktimeTracker: {
tracking: false, tracking: false,
number: 'Worktime', number: 'Worktime',
@ -53,7 +57,8 @@ const TimeTrack = {
snippets: [], snippets: [],
codeMirrors: [], codeMirrors: [],
customBookingValue: '', customBookingValue: '',
customDateForPastDays: '' customDateForPastDays: '',
newTaskInput: ''
} }
}, },
mounted() { mounted() {
@ -61,7 +66,9 @@ const TimeTrack = {
this.loadStorage(); this.loadStorage();
this.fetchThemes(); this.fetchThemes();
moment.locale('de'); moment.locale('de');
this.customDateForPastDays = moment().format();
setInterval(() => { setInterval(() => {
vue.$forceUpdate(); vue.$forceUpdate();
@ -153,7 +160,7 @@ const TimeTrack = {
this.updateStorage(); this.updateStorage();
}, },
createTracker() { createTracker() {
this.tickets.push({ this.tickets.pushToBeginning({
tracking: false, tracking: false,
number: '#', number: '#',
trackingStarted: null, trackingStarted: null,
@ -164,7 +171,7 @@ const TimeTrack = {
this.updateStorage(); this.updateStorage();
}, },
createTimeBox() { createTimeBox() {
this.tickets.push({ this.tickets.pushToBeginning({
tracking: false, tracking: false,
number: 'Timebox ', number: 'Timebox ',
trackingStarted: null, trackingStarted: null,
@ -204,6 +211,7 @@ const TimeTrack = {
}, },
stopTracking(ticket) { stopTracking(ticket) {
// console.log(ticket); // console.log(ticket);
let component = this;
ticket.trackingStopped = moment(); ticket.trackingStopped = moment();
ticket.tracking = false; ticket.tracking = false;
@ -218,7 +226,6 @@ const TimeTrack = {
manually: false, manually: false,
minutes: Math.round(minutesSpent) minutes: Math.round(minutesSpent)
}; };
// console.log(historyEntry);
if (this.experimental.trackWorktime) { if (this.experimental.trackWorktime) {
if (ticket.paused) { if (ticket.paused) {
@ -226,7 +233,34 @@ const TimeTrack = {
} }
} }
ticket.history.push(historyEntry); if (this.ticketSystemUrl && this.isTicketNumber(ticket.number)) {
let finishedTasks = ticket.tasks.filter((task) => {
return task.finished > ticket.trackingStarted && task.finished < ticket.trackingStopped;
}).map((task) => {
return task.name;
});
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)+
'&time_entry[activity_id]=9&time_entry[comments]='+finishedTasks.join(', '),
'_blank'
);
}
}, toast, 'buttonName');
}, true]
]
});
}
ticket.history.pushToBeginning(historyEntry);
} }
ticket.trackingStarted = null; ticket.trackingStarted = null;
@ -242,6 +276,28 @@ const TimeTrack = {
this.stopTracking(ticket); this.stopTracking(ticket);
}, },
sendLastBookingToTicketSystem(ticket) {
let component = this;
let latestHistoryItem = ticket.history[0];
if (this.ticketSystemUrl && this.isTicketNumber(ticket.number)) {
let finishedTasks = [];
if (ticket.tasks && ticket.tasks.length > 0) {
finishedTasks = ticket.tasks.filter((task) => {
return task.finished > latestHistoryItem.trackingStarted && task.finished < latestHistoryItem.trackingStopped;
}).map((task) => {
return task.name;
});
}
window.open(
component.ticketSystemUrl + ticket.number.replace('#', '').trim()+'/time_entries/new?time_entry[hours]='+(Math.round(latestHistoryItem.minutes)/60)+
'&time_entry[activity_id]=9&time_entry[comments]='+finishedTasks.join(', '),
'_blank'
);
}
},
resumeTracking(ticket) { resumeTracking(ticket) {
ticket.trackingStarted = moment(); ticket.trackingStarted = moment();
ticket.tracking = true; ticket.tracking = true;
@ -336,12 +392,16 @@ const TimeTrack = {
return number.indexOf('#') >= 0; return number.indexOf('#') >= 0;
}, },
deleteTracker(index, archive = false) { deleteTracker(index, archive = false) {
let component = this;
let message = ''; let message = '';
if (archive) { if (archive) {
Object.assign(this.trashed, this.archive[index]);
let name = this.archive[index].number; let name = this.archive[index].number;
message = 'Tracker "' + name + '" wurde gelöscht'; message = 'Tracker "' + name + '" wurde gelöscht';
this.archive.splice(index, 1); this.archive.splice(index, 1);
} else { } else {
Object.assign(this.trashed, this.tickets[index]);
let name = this.tickets[index].number; let name = this.tickets[index].number;
message = 'Tracker "' + name + '" wurde gelöscht'; message = 'Tracker "' + name + '" wurde gelöscht';
this.tickets.splice(index, 1); this.tickets.splice(index, 1);
@ -350,29 +410,39 @@ const TimeTrack = {
iziToast.show({ iziToast.show({
message: message, message: message,
color: 'blue', color: 'blue',
position: 'topCenter' 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(); this.updateStorage();
}, },
restoreTrashed() {
let restoredTracker = {};
Object.assign(restoredTracker, this.trashed);
this.trashed = {};
this.tickets.pushToBeginning(restoredTracker);
this.updateStorage();
},
archiveTracker(index) { archiveTracker(index) {
if (this.tickets[index].tracking) { if (this.tickets[index].tracking) {
this.stopActiveTracker(); this.stopActiveTracker();
} }
iziToast.show({ this.archive.pushToBeginning(this.tickets[index]);
title: 'Tracker archiviert',
color: 'green',
position: 'topCenter'
});
this.archive.push(this.tickets[index]);
this.tickets.splice(index, 1); this.tickets.splice(index, 1);
this.updateStorage(); this.updateStorage();
}, },
reactivateTicket(index) { reactivateTicket(index) {
this.view = 'trackers'; this.tickets.pushToBeginning(this.archive[index]);
this.tickets.push(this.archive[index]);
this.archive.splice(index, 1); this.archive.splice(index, 1);
this.updateStorage(); this.updateStorage();
}, },
@ -385,7 +455,7 @@ const TimeTrack = {
this.updateStorage(); this.updateStorage();
}, },
bookTimeManually(ticket, minutes) { bookTimeManually(ticket, minutes) {
ticket.history.push({ ticket.history.pushToBeginning({
trackingStarted: moment(), trackingStarted: moment(),
trackingStopped: moment(), trackingStopped: moment(),
manually: true, manually: true,
@ -423,9 +493,9 @@ const TimeTrack = {
ticketCollection.forEach((ticket) => { ticketCollection.forEach((ticket) => {
if (ticket.archived || (ticket.active && ticket.active === false)) { if (ticket.archived || (ticket.active && ticket.active === false)) {
archive.push(ticket); archive.pushToBeginning(ticket);
} else { } else {
tickets.push(ticket) tickets.pushToBeginning(ticket)
} }
}); });
@ -450,15 +520,22 @@ const TimeTrack = {
console.log(error); console.log(error);
}); });
}, },
showHistoryForTracker(ticket) { showHistoryForTracker(tracker) {
this.selectedTracker = ticket; this.selectedTracker = tracker;
this.showHistory = true;
this.$forceUpdate(); this.$forceUpdate();
setTimeout(() => { setTimeout(() => {
let historyModal = new bootstrap.Modal(document.getElementById('historyModal')); let historyModal = new bootstrap.Modal(document.getElementById('historyModal'));
historyModal.toggle(); historyModal.toggle();
}, 50); }, 50);
}, },
openTasksForTracker(tracker) {
this.selectedTracker = tracker;
this.$forceUpdate();
setTimeout(() => {
let tasksModal = new bootstrap.Modal(document.getElementById('trackerTasksModal'));
tasksModal.toggle();
}, 50);
},
showCustomBookingForTracker(ticket) { showCustomBookingForTracker(ticket) {
this.selectedTracker = ticket; this.selectedTracker = ticket;
this.$forceUpdate(); this.$forceUpdate();
@ -497,8 +574,17 @@ const TimeTrack = {
this.tickets.forEach((ticket) => { this.tickets.forEach((ticket) => {
ticket.history.forEach((historyEntry) => { ticket.history.forEach((historyEntry) => {
if (moment(historyEntry.trackingStarted).format("MMM Do YY") === day) { if (moment(historyEntry.trackingStarted).format("MMM Do YY") === day) {
historyEntry.ticket = ticket.number; let newEntry = {};
collection.push(historyEntry); 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);
}
} }
}); });
}); });
@ -506,15 +592,34 @@ const TimeTrack = {
this.archive.forEach((ticket) => { this.archive.forEach((ticket) => {
ticket.history.forEach((historyEntry) => { ticket.history.forEach((historyEntry) => {
if (moment(historyEntry.trackingStarted).format("MMM Do YY") === day) { if (moment(historyEntry.trackingStarted).format("MMM Do YY") === day) {
historyEntry.ticket = ticket.number; let newEntry = {};
historyEntry.archive = true; Object.assign(newEntry, historyEntry);
collection.push(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; return collection;
}, },
getCollectionItemWithValue(collection, property, value) {
let found = false;
collection.forEach((item) => {
if (item[property] === value) {
found = item;
}
})
return found;
},
sendPortalChangeRequest() { sendPortalChangeRequest() {
let vue = this; let vue = this;
let publicDBParam = this.publicDB ? '&publicDB=1' : ''; let publicDBParam = this.publicDB ? '&publicDB=1' : '';
@ -557,7 +662,7 @@ const TimeTrack = {
id: getRandomID() id: getRandomID()
}; };
this.snippets.push(newSnippet); this.snippets.pushToBeginning(newSnippet);
this.loadSnippet(this.snippets[this.snippets.length-1]); this.loadSnippet(this.snippets[this.snippets.length-1]);
this.updateStorage(); this.updateStorage();
}, },
@ -634,10 +739,71 @@ const TimeTrack = {
tellJoke(category, language = 'en') { tellJoke(category, language = 'en') {
let jokeService = new JokeService(category ?? undefined, language); let jokeService = new JokeService(category ?? undefined, language);
jokeService.tell(); 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() { beforeDestroy() {
this.stopTrackingTicket(); this.stopActiveTracker();
}, },
watch: { watch: {
publicDB() { publicDB() {
@ -675,6 +841,21 @@ const TimeTrack = {
fun: this.fun, fun: this.fun,
theme: this.theme 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';
}
}
} }
} }
}; };