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 @@ @@ -5,5 +5,8 @@
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<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>
</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 @@ @@ -1,4 +1,5 @@
body {
background-color: #e7e7e7;
overflow-y: auto;
}
@ -221,3 +222,19 @@ nav, .card { @@ -221,3 +222,19 @@ nav, .card {
bottom: 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 @@ @@ -1,3 +1,7 @@
Array.prototype.pushToBeginning = function (toPush) {
return this.unshift.apply(this, [toPush]);
}
const TimeTrack = {
data() {
return {
@ -7,8 +11,7 @@ const TimeTrack = { @@ -7,8 +11,7 @@ const TimeTrack = {
snippetSpace: false,
portalSwitcher: true
},
view: 'trackers',
showHistory: false,
view: 'board',
theme: 'materia',
themes: null,
dashboardLogo: 'assets/img/logo.png',
@ -21,6 +24,7 @@ const TimeTrack = { @@ -21,6 +24,7 @@ const TimeTrack = {
},
tickets: [],
archive: [],
trashed: {},
worktimeTracker: {
tracking: false,
number: 'Worktime',
@ -53,7 +57,8 @@ const TimeTrack = { @@ -53,7 +57,8 @@ const TimeTrack = {
snippets: [],
codeMirrors: [],
customBookingValue: '',
customDateForPastDays: ''
customDateForPastDays: '',
newTaskInput: ''
}
},
mounted() {
@ -61,7 +66,9 @@ const TimeTrack = { @@ -61,7 +66,9 @@ const TimeTrack = {
this.loadStorage();
this.fetchThemes();
moment.locale('de');
this.customDateForPastDays = moment().format();
setInterval(() => {
vue.$forceUpdate();
@ -153,7 +160,7 @@ const TimeTrack = { @@ -153,7 +160,7 @@ const TimeTrack = {
this.updateStorage();
},
createTracker() {
this.tickets.push({
this.tickets.pushToBeginning({
tracking: false,
number: '#',
trackingStarted: null,
@ -164,7 +171,7 @@ const TimeTrack = { @@ -164,7 +171,7 @@ const TimeTrack = {
this.updateStorage();
},
createTimeBox() {
this.tickets.push({
this.tickets.pushToBeginning({
tracking: false,
number: 'Timebox ',
trackingStarted: null,
@ -204,6 +211,7 @@ const TimeTrack = { @@ -204,6 +211,7 @@ const TimeTrack = {
},
stopTracking(ticket) {
// console.log(ticket);
let component = this;
ticket.trackingStopped = moment();
ticket.tracking = false;
@ -218,7 +226,6 @@ const TimeTrack = { @@ -218,7 +226,6 @@ const TimeTrack = {
manually: false,
minutes: Math.round(minutesSpent)
};
// console.log(historyEntry);
if (this.experimental.trackWorktime) {
if (ticket.paused) {
@ -226,7 +233,34 @@ const TimeTrack = { @@ -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;
@ -242,6 +276,28 @@ const TimeTrack = { @@ -242,6 +276,28 @@ const TimeTrack = {
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) {
ticket.trackingStarted = moment();
ticket.tracking = true;
@ -336,12 +392,16 @@ const TimeTrack = { @@ -336,12 +392,16 @@ const TimeTrack = {
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);
@ -350,29 +410,39 @@ const TimeTrack = { @@ -350,29 +410,39 @@ const TimeTrack = {
iziToast.show({
message: message,
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();
},
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();
}
iziToast.show({
title: 'Tracker archiviert',
color: 'green',
position: 'topCenter'
});
this.archive.push(this.tickets[index]);
this.archive.pushToBeginning(this.tickets[index]);
this.tickets.splice(index, 1);
this.updateStorage();
},
reactivateTicket(index) {
this.view = 'trackers';
this.tickets.push(this.archive[index]);
this.tickets.pushToBeginning(this.archive[index]);
this.archive.splice(index, 1);
this.updateStorage();
},
@ -385,7 +455,7 @@ const TimeTrack = { @@ -385,7 +455,7 @@ const TimeTrack = {
this.updateStorage();
},
bookTimeManually(ticket, minutes) {
ticket.history.push({
ticket.history.pushToBeginning({
trackingStarted: moment(),
trackingStopped: moment(),
manually: true,
@ -423,9 +493,9 @@ const TimeTrack = { @@ -423,9 +493,9 @@ const TimeTrack = {
ticketCollection.forEach((ticket) => {
if (ticket.archived || (ticket.active && ticket.active === false)) {
archive.push(ticket);
archive.pushToBeginning(ticket);
} else {
tickets.push(ticket)
tickets.pushToBeginning(ticket)
}
});
@ -450,15 +520,22 @@ const TimeTrack = { @@ -450,15 +520,22 @@ const TimeTrack = {
console.log(error);
});
},
showHistoryForTracker(ticket) {
this.selectedTracker = ticket;
this.showHistory = true;
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();
@ -497,8 +574,17 @@ const TimeTrack = { @@ -497,8 +574,17 @@ const TimeTrack = {
this.tickets.forEach((ticket) => {
ticket.history.forEach((historyEntry) => {
if (moment(historyEntry.trackingStarted).format("MMM Do YY") === day) {
historyEntry.ticket = ticket.number;
collection.push(historyEntry);
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);
}
}
});
});
@ -506,15 +592,34 @@ const TimeTrack = { @@ -506,15 +592,34 @@ const TimeTrack = {
this.archive.forEach((ticket) => {
ticket.history.forEach((historyEntry) => {
if (moment(historyEntry.trackingStarted).format("MMM Do YY") === day) {
historyEntry.ticket = ticket.number;
historyEntry.archive = true;
collection.push(historyEntry);
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' : '';
@ -557,7 +662,7 @@ const TimeTrack = { @@ -557,7 +662,7 @@ const TimeTrack = {
id: getRandomID()
};
this.snippets.push(newSnippet);
this.snippets.pushToBeginning(newSnippet);
this.loadSnippet(this.snippets[this.snippets.length-1]);
this.updateStorage();
},
@ -634,10 +739,71 @@ const TimeTrack = { @@ -634,10 +739,71 @@ const TimeTrack = {
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.stopTrackingTicket();
this.stopActiveTracker();
},
watch: {
publicDB() {
@ -675,6 +841,21 @@ const TimeTrack = { @@ -675,6 +841,21 @@ const TimeTrack = {
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';
}
}
}
}
};