commit
741716877b
26 changed files with 29964 additions and 0 deletions
@ -0,0 +1,23 @@ |
|||||||
|
.DS_Store |
||||||
|
node_modules |
||||||
|
/dist |
||||||
|
|
||||||
|
|
||||||
|
# local env files |
||||||
|
.env.local |
||||||
|
.env.*.local |
||||||
|
|
||||||
|
# Log files |
||||||
|
npm-debug.log* |
||||||
|
yarn-debug.log* |
||||||
|
yarn-error.log* |
||||||
|
pnpm-debug.log* |
||||||
|
|
||||||
|
# Editor directories and files |
||||||
|
.idea |
||||||
|
.vscode |
||||||
|
*.suo |
||||||
|
*.ntvs* |
||||||
|
*.njsproj |
||||||
|
*.sln |
||||||
|
*.sw? |
@ -0,0 +1,24 @@ |
|||||||
|
# timetrack-router |
||||||
|
|
||||||
|
## Project setup |
||||||
|
``` |
||||||
|
npm install |
||||||
|
``` |
||||||
|
|
||||||
|
### Compiles and hot-reloads for development |
||||||
|
``` |
||||||
|
npm run serve |
||||||
|
``` |
||||||
|
|
||||||
|
### Compiles and minifies for production |
||||||
|
``` |
||||||
|
npm run build |
||||||
|
``` |
||||||
|
|
||||||
|
### Lints and fixes files |
||||||
|
``` |
||||||
|
npm run lint |
||||||
|
``` |
||||||
|
|
||||||
|
### Customize configuration |
||||||
|
See [Configuration Reference](https://cli.vuejs.org/config/). |
@ -0,0 +1,5 @@ |
|||||||
|
module.exports = { |
||||||
|
presets: [ |
||||||
|
'@vue/cli-plugin-babel/preset' |
||||||
|
] |
||||||
|
} |
@ -0,0 +1,52 @@ |
|||||||
|
{ |
||||||
|
"name": "timetrack-router", |
||||||
|
"version": "0.1.0", |
||||||
|
"private": true, |
||||||
|
"scripts": { |
||||||
|
"serve": "vue-cli-service serve", |
||||||
|
"build": "vue-cli-service build", |
||||||
|
"lint": "vue-cli-service lint" |
||||||
|
}, |
||||||
|
"dependencies": { |
||||||
|
"@popperjs/core": "^2.11.0", |
||||||
|
"axios": "^0.24.0", |
||||||
|
"bootstrap": "^5.1.3", |
||||||
|
"core-js": "^3.6.5", |
||||||
|
"fontawesome": "^5.6.3", |
||||||
|
"izitoast": "^1.4.0", |
||||||
|
"moment": "^2.29.1", |
||||||
|
"vue": "^3.0.0", |
||||||
|
"vue-router": "^4.0.0-0", |
||||||
|
"vuex": "^4.0.0-0" |
||||||
|
}, |
||||||
|
"devDependencies": { |
||||||
|
"@vue/cli-plugin-babel": "~4.5.0", |
||||||
|
"@vue/cli-plugin-eslint": "~4.5.0", |
||||||
|
"@vue/cli-plugin-router": "~4.5.0", |
||||||
|
"@vue/cli-plugin-vuex": "~4.5.0", |
||||||
|
"@vue/cli-service": "~4.5.0", |
||||||
|
"@vue/compiler-sfc": "^3.0.0", |
||||||
|
"babel-eslint": "^10.1.0", |
||||||
|
"eslint": "^6.7.2", |
||||||
|
"eslint-plugin-vue": "^7.0.0" |
||||||
|
}, |
||||||
|
"eslintConfig": { |
||||||
|
"root": true, |
||||||
|
"env": { |
||||||
|
"node": true |
||||||
|
}, |
||||||
|
"extends": [ |
||||||
|
"plugin:vue/vue3-essential", |
||||||
|
"eslint:recommended" |
||||||
|
], |
||||||
|
"parserOptions": { |
||||||
|
"parser": "babel-eslint" |
||||||
|
}, |
||||||
|
"rules": {} |
||||||
|
}, |
||||||
|
"browserslist": [ |
||||||
|
"> 1%", |
||||||
|
"last 2 versions", |
||||||
|
"not dead" |
||||||
|
] |
||||||
|
} |
@ -0,0 +1,174 @@ |
|||||||
|
body { |
||||||
|
background-color: #e7e7e7; |
||||||
|
overflow-y: auto; |
||||||
|
} |
||||||
|
|
||||||
|
.container { |
||||||
|
margin: 20px auto 20px auto; |
||||||
|
} |
||||||
|
|
||||||
|
.tracker-action-button { |
||||||
|
margin-top: 10px; |
||||||
|
padding: 1px 5px 1px 5px; |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.delete-tracker-button { |
||||||
|
margin-top: 10px; |
||||||
|
margin-bottom: 15px; |
||||||
|
padding: 1px 5px 1px 5px; |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.side-button-right { |
||||||
|
font-size: 1.7em; |
||||||
|
} |
||||||
|
|
||||||
|
.add-tracker-button { |
||||||
|
font-size: 1.7em; |
||||||
|
} |
||||||
|
|
||||||
|
.navbar-brand { |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.logo-nav { |
||||||
|
max-height: 35px; |
||||||
|
margin: 5px; |
||||||
|
} |
||||||
|
|
||||||
|
nav { |
||||||
|
border-radius: 45px; |
||||||
|
} |
||||||
|
|
||||||
|
nav, .card { |
||||||
|
margin-bottom: 20px; |
||||||
|
} |
||||||
|
|
||||||
|
.modal-content { |
||||||
|
max-width: 1000px; |
||||||
|
} |
||||||
|
|
||||||
|
.btn-full-width { |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.timeTracking { |
||||||
|
height: 635px; |
||||||
|
overflow-y: scroll; |
||||||
|
margin-bottom: 0; |
||||||
|
padding-bottom: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.ticket-history { |
||||||
|
list-style: none; |
||||||
|
padding-left: 0; |
||||||
|
} |
||||||
|
|
||||||
|
.trackingNameField { |
||||||
|
max-height: 1em; |
||||||
|
margin-bottom: 1px; |
||||||
|
} |
||||||
|
|
||||||
|
#settingsModal .modal-dialog, |
||||||
|
#showTicketsModal .modal-dialog, |
||||||
|
#showArchivedTicketsModal .modal-dialog { |
||||||
|
max-width: 900px; |
||||||
|
} |
||||||
|
|
||||||
|
.btn { |
||||||
|
text-transform: initial !important; |
||||||
|
} |
||||||
|
|
||||||
|
.brand-title { |
||||||
|
margin-left: 2em; |
||||||
|
margin-top: 0.5em; |
||||||
|
} |
||||||
|
|
||||||
|
#settingsModal .form-group { |
||||||
|
margin-bottom: 0.5em; |
||||||
|
} |
||||||
|
|
||||||
|
.search-field { |
||||||
|
display: inline-block; |
||||||
|
max-width: 85%; |
||||||
|
margin-left: 5%; |
||||||
|
} |
||||||
|
|
||||||
|
#left-menu { |
||||||
|
position: fixed; |
||||||
|
top: 0; |
||||||
|
left: 0; |
||||||
|
height: 100%; |
||||||
|
width: 90px; |
||||||
|
|
||||||
|
align-content: center; |
||||||
|
background-color: #bdbdbd; |
||||||
|
border-right: 1px solid grey; |
||||||
|
padding: 0; |
||||||
|
} |
||||||
|
|
||||||
|
#content { |
||||||
|
margin-left: 90px; |
||||||
|
max-width: 85vw; |
||||||
|
} |
||||||
|
|
||||||
|
.bottom-menu-item { |
||||||
|
width: 100%; |
||||||
|
margin-bottom: 1em; |
||||||
|
} |
||||||
|
|
||||||
|
.btn-switcher { |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.container { |
||||||
|
margin-top: 1em; |
||||||
|
} |
||||||
|
|
||||||
|
.form-control { |
||||||
|
margin-bottom: 1em; |
||||||
|
} |
||||||
|
|
||||||
|
.switch-portal-button { |
||||||
|
margin-bottom: 0.5em; |
||||||
|
} |
||||||
|
|
||||||
|
.col-board-inner { |
||||||
|
border-right: 1px solid grey; |
||||||
|
} |
||||||
|
|
||||||
|
#board-view h5 { |
||||||
|
text-align: center; |
||||||
|
text-transform: uppercase; |
||||||
|
color: dimgrey; |
||||||
|
line-height: 0.7em; |
||||||
|
word-spacing: 1px; |
||||||
|
} |
||||||
|
|
||||||
|
.snippet-space-item { |
||||||
|
display: none; |
||||||
|
} |
||||||
|
|
||||||
|
.CodeMirror { |
||||||
|
border-radius: 5px; |
||||||
|
margin-top: 1em; |
||||||
|
} |
||||||
|
|
||||||
|
.btn-wide { |
||||||
|
width: 5em; |
||||||
|
margin-top: 1em; |
||||||
|
margin-left: 1em; |
||||||
|
} |
||||||
|
|
||||||
|
.finished-task { |
||||||
|
text-decoration: line-through; |
||||||
|
} |
||||||
|
|
||||||
|
#trackerTasksModal a li { |
||||||
|
color: black !important; |
||||||
|
} |
||||||
|
|
||||||
|
#trackerTasksModal input[type=range] { |
||||||
|
width: 100%; |
||||||
|
} |
After Width: | Height: | Size: 4.2 KiB |
@ -0,0 +1,20 @@ |
|||||||
|
<!DOCTYPE html> |
||||||
|
<html lang="de"> |
||||||
|
<head> |
||||||
|
<meta charset="utf-8"> |
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge"> |
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0"> |
||||||
|
<link rel="icon" href="<%= BASE_URL %>favicon.ico"> |
||||||
|
<link rel="stylesheet" href="/app.css"> |
||||||
|
<title><%= htmlWebpackPlugin.options.title %></title> |
||||||
|
</head> |
||||||
|
<body> |
||||||
|
<noscript> |
||||||
|
<strong>We're sorry but <%= htmlWebpackPlugin.options.title %> doesn't work properly without JavaScript enabled. |
||||||
|
Please enable it to continue.</strong> |
||||||
|
</noscript> |
||||||
|
<div id="app"></div> |
||||||
|
<!-- built files will be auto injected --> |
||||||
|
<script src="https://kit.fontawesome.com/b54a4cceff.js" crossorigin="anonymous"></script> |
||||||
|
</body> |
||||||
|
</html> |
@ -0,0 +1,436 @@ |
|||||||
|
<template> |
||||||
|
<div class="container"> |
||||||
|
<link rel="stylesheet" :href="'https://bootswatch.com/5/materia/bootstrap.min.css'"> |
||||||
|
<router-view/> |
||||||
|
</div> |
||||||
|
|
||||||
|
<Archive/> |
||||||
|
<Menu/> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import moment from 'moment' |
||||||
|
import iziToast from 'izitoast' |
||||||
|
import axios from 'axios' |
||||||
|
import bootstrap from 'bootstrap' |
||||||
|
import Menu from "./views/Menu"; |
||||||
|
import Archive from "./views/Archive"; |
||||||
|
|
||||||
|
export default { |
||||||
|
el: '#root', |
||||||
|
components: { |
||||||
|
Archive, |
||||||
|
Menu |
||||||
|
}, |
||||||
|
data() { |
||||||
|
return { |
||||||
|
sounds: { |
||||||
|
bad: [ |
||||||
|
'alert', |
||||||
|
'wilhelm', |
||||||
|
], |
||||||
|
animals: [ |
||||||
|
'transition' |
||||||
|
] |
||||||
|
}, |
||||||
|
theme: 'materia', |
||||||
|
customBookingValue: '', |
||||||
|
customDateForPastDays: '', |
||||||
|
newTaskInput: '' |
||||||
|
} |
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
let component = this; |
||||||
|
this.$store.commit('loadSavedData'); |
||||||
|
this.theme = this.$store.state.settings.theme; |
||||||
|
this.$forceUpdate(); |
||||||
|
|
||||||
|
moment.locale('de'); |
||||||
|
this.customDateForPastDays = moment().format(); |
||||||
|
|
||||||
|
setInterval(() => { |
||||||
|
component.$forceUpdate(); |
||||||
|
}, 1000 * 60); |
||||||
|
|
||||||
|
component.loadTooltips(); |
||||||
|
setInterval(() => { |
||||||
|
component.loadTooltips(); |
||||||
|
}, 5000); |
||||||
|
|
||||||
|
setInterval(() => { |
||||||
|
component.checkTimeBoxes(); |
||||||
|
}, 10000); |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
loadTooltips() { |
||||||
|
let tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')) |
||||||
|
tooltipTriggerList.map(function (tooltipTriggerEl) { |
||||||
|
return new bootstrap.Tooltip(tooltipTriggerEl) |
||||||
|
}); |
||||||
|
}, |
||||||
|
startTracking(tracker, individual = false, timeBoxMinutes = null) { |
||||||
|
if (!individual) { |
||||||
|
this.stopActiveTracker(); |
||||||
|
} |
||||||
|
|
||||||
|
if (timeBoxMinutes) { |
||||||
|
tracker.timeBoxMinutes = timeBoxMinutes; |
||||||
|
} |
||||||
|
|
||||||
|
tracker.status = 'wip'; |
||||||
|
|
||||||
|
tracker.trackingStarted = moment(); |
||||||
|
tracker.tracking = true; |
||||||
|
|
||||||
|
this.$forceUpdate(); |
||||||
|
this.$store.commit('saveTrackers'); |
||||||
|
}, |
||||||
|
formattedDate(date) { |
||||||
|
return moment(date).format('llll'); |
||||||
|
}, |
||||||
|
exactTimestamp(date) { |
||||||
|
return moment(date).format('LTS'); |
||||||
|
}, |
||||||
|
currentTrackingRunningFor(tracker) { |
||||||
|
return this.timeWithPostFix(Math.round(moment.duration(moment().diff(tracker.trackingStarted)).as('minutes'))); |
||||||
|
}, |
||||||
|
stopActiveTracker() { |
||||||
|
let component = this; |
||||||
|
|
||||||
|
component.trackers.forEach(function (tracker) { |
||||||
|
if (tracker.tracking === true) { |
||||||
|
component.stopTracking(tracker); |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
getTrackingStartTime(tracker) { |
||||||
|
return moment(tracker.trackingStarted).format('LT'); |
||||||
|
}, |
||||||
|
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.trackers[index]); |
||||||
|
let name = this.trackers[index].number; |
||||||
|
message = 'Tracker "' + name + '" wurde gelöscht'; |
||||||
|
this.trackers.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(){ |
||||||
|
component.restoreTrashed(); |
||||||
|
} |
||||||
|
}, toast, 'buttonName'); |
||||||
|
}, true] |
||||||
|
] |
||||||
|
}); |
||||||
|
|
||||||
|
this.$store.commit('saveTrackers'); |
||||||
|
}, |
||||||
|
deleteHistoryEntry(trackerIndex, historyIndex) { |
||||||
|
if (trackerIndex) { |
||||||
|
this.trackers[trackerIndex].history.splice(historyIndex, 1); |
||||||
|
} else { |
||||||
|
this.selectedTracker.history.splice(historyIndex, 1); |
||||||
|
} |
||||||
|
this.$store.commit('saveTrackers'); |
||||||
|
}, |
||||||
|
bookTimeManually(tracker, minutes) { |
||||||
|
tracker.history.pushToBeginning({ |
||||||
|
trackingStarted: moment(), |
||||||
|
trackingStopped: moment(), |
||||||
|
manually: true, |
||||||
|
minutes: Math.round(minutes) |
||||||
|
}); |
||||||
|
|
||||||
|
this.$store.commit('saveTrackers'); |
||||||
|
}, |
||||||
|
showCustomBookingForTracker(tracker) { |
||||||
|
this.selectedTracker = tracker; |
||||||
|
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.trackers.forEach((tracker) => { |
||||||
|
tracker.history.forEach((historyEntry) => { |
||||||
|
if (moment(historyEntry.trackingStarted).format("MMM Do YY") === day) { |
||||||
|
let newEntry = {}; |
||||||
|
Object.assign(newEntry, historyEntry); |
||||||
|
newEntry.tracker = tracker.number; |
||||||
|
|
||||||
|
let existingEntry = this.getCollectionItemWithValue(collection, 'tracker', tracker.number); |
||||||
|
|
||||||
|
if (existingEntry) { |
||||||
|
existingEntry.minutes = Number(existingEntry.minutes) + Number(newEntry.minutes); |
||||||
|
} else { |
||||||
|
collection.pushToBeginning(newEntry); |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
}); |
||||||
|
|
||||||
|
this.archive.forEach((tracker) => { |
||||||
|
tracker.history.forEach((historyEntry) => { |
||||||
|
if (moment(historyEntry.trackingStarted).format("MMM Do YY") === day) { |
||||||
|
let newEntry = {}; |
||||||
|
Object.assign(newEntry, historyEntry); |
||||||
|
newEntry.tracker = tracker.number; |
||||||
|
|
||||||
|
let existingEntry = this.getCollectionItemWithValue(collection, 'tracker', tracker.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 component = this; |
||||||
|
let publicDBParam = this.publicDB ? '&publicDB=1' : ''; |
||||||
|
axios.get( |
||||||
|
'https://settings.vemap.docker/?portal2change=' + component.portal + publicDBParam |
||||||
|
).then((response) => { |
||||||
|
console.log(response) |
||||||
|
}).catch((error) => { |
||||||
|
console.log(error); |
||||||
|
// An error is expected here due to apache restarting |
||||||
|
iziToast.show({ |
||||||
|
message: 'Portal-Wechsel erfolgreich', |
||||||
|
color: 'green' |
||||||
|
}); |
||||||
|
|
||||||
|
if (component.fun) { |
||||||
|
playSound(oneOf(component.sounds.animals)); |
||||||
|
} |
||||||
|
|
||||||
|
component.updateStorage(); |
||||||
|
}) |
||||||
|
}, |
||||||
|
importPortalsJson() { |
||||||
|
if (this.importStringForPortals !== '') { |
||||||
|
this.portals = JSON.parse(this.importStringForPortals); |
||||||
|
|
||||||
|
this.importStringForPortals = ''; |
||||||
|
|
||||||
|
iziToast.show({ |
||||||
|
message: 'Portalnamen importiert', |
||||||
|
color: 'green' |
||||||
|
}); |
||||||
|
this.$store.commit('saveTrackers'); |
||||||
|
} |
||||||
|
}, |
||||||
|
checkTimeBoxes() { |
||||||
|
let component = this; |
||||||
|
|
||||||
|
this.$store.state.trackers.forEach((tracker) => { |
||||||
|
if (tracker.isTimeBox && !tracker.notificated && component.timeBoxTimeLeft(tracker) <= 0) { |
||||||
|
tracker.notificated = true; |
||||||
|
component.stopTracking(tracker); |
||||||
|
|
||||||
|
if (Notification.permission === 'granted') { |
||||||
|
new Notification('Timebox zu Ende', { |
||||||
|
body: 'Zeit für "'+tracker.number+'" ist abgelaufen!', |
||||||
|
icon: '/timetrack/assets/img/favicon.ico' |
||||||
|
}); |
||||||
|
} else { |
||||||
|
iziToast.show({ |
||||||
|
message: 'Zeit für "'+tracker.number+'" ist abgelaufen!', |
||||||
|
color: 'red' |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
|
}); |
||||||
|
}, |
||||||
|
timeBoxTimeLeft(tracker) { |
||||||
|
return Number(tracker.timeBoxMinutes - this.getTotalTime(tracker, true)); |
||||||
|
}, |
||||||
|
getTotalTime(tracker, raw = false) { |
||||||
|
let totalTime = 0; |
||||||
|
|
||||||
|
if (tracker.history.length > 0) { |
||||||
|
tracker.history.forEach(function (historyEntry) { |
||||||
|
totalTime += Math.round(historyEntry.minutes); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
if (tracker.tracking) { |
||||||
|
totalTime += Math.round(moment.duration(moment().diff(tracker.trackingStarted)).as('minutes')); |
||||||
|
} |
||||||
|
|
||||||
|
if (raw) { |
||||||
|
return totalTime; |
||||||
|
} else { |
||||||
|
return this.timeWithPostFix(totalTime); |
||||||
|
} |
||||||
|
}, |
||||||
|
validateBooleans(value) { |
||||||
|
return value === 'true' || value === true; |
||||||
|
}, |
||||||
|
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.$store.commit('saveTrackers'); |
||||||
|
}, |
||||||
|
deleteTask(taskIndex) { |
||||||
|
this.selectedTracker.tasks.splice(taskIndex, 1) |
||||||
|
this.$store.commit('saveTrackers'); |
||||||
|
}, |
||||||
|
toggleTask(task) { |
||||||
|
task.done = !task.done; |
||||||
|
|
||||||
|
if (task.done) { |
||||||
|
task.finished = moment(); |
||||||
|
task.percentDone = 100; |
||||||
|
} else { |
||||||
|
task.finished = null; |
||||||
|
task.percentDone = 0; |
||||||
|
} |
||||||
|
|
||||||
|
this.$store.commit('saveTrackers'); |
||||||
|
}, |
||||||
|
checkForCompletionOfTask(task) { |
||||||
|
task.done = task.percentDone == 100; |
||||||
|
this.$forceUpdate(); |
||||||
|
this.$store.commit('saveTrackers'); |
||||||
|
}, |
||||||
|
}, |
||||||
|
beforeUnmount() { |
||||||
|
this.stopActiveTracker(); |
||||||
|
}, |
||||||
|
watch: { |
||||||
|
trackers: { |
||||||
|
handler() { |
||||||
|
this.$store.commit('saveTrackers'); |
||||||
|
}, |
||||||
|
deep: true |
||||||
|
}, |
||||||
|
publicDB() { |
||||||
|
this.publicDB = this.validateBooleans(this.publicDB) |
||||||
|
this.$store.commit('saveTrackers'); |
||||||
|
this.$forceUpdate(); |
||||||
|
}, |
||||||
|
showPT() { |
||||||
|
this.showPT = this.validateBooleans(this.showPT) |
||||||
|
this.$store.commit('saveTrackers'); |
||||||
|
this.$forceUpdate(); |
||||||
|
}, |
||||||
|
dontShowMinutes() { |
||||||
|
this.dontShowMinutes = this.validateBooleans(this.dontShowMinutes) |
||||||
|
this.$store.commit('saveTrackers'); |
||||||
|
this.$forceUpdate(); |
||||||
|
}, |
||||||
|
theme() { |
||||||
|
this.$store.commit('saveTrackers'); |
||||||
|
this.$forceUpdate(); |
||||||
|
}, |
||||||
|
fun() { |
||||||
|
this.fun = this.validateBooleans(this.fun) |
||||||
|
this.$store.commit('saveTrackers'); |
||||||
|
this.$forceUpdate(); |
||||||
|
} |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
exportJson() { |
||||||
|
return JSON.stringify({ |
||||||
|
trackers: this.$store.state.trackers, |
||||||
|
archive: this.$store.state.archive, |
||||||
|
trackerSystemUrl: this.$store.state.settings.trackerSystemUrl, |
||||||
|
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(); |
||||||
|
} |
||||||
|
|
||||||
|
</script> |
||||||
|
|
||||||
|
<style> |
||||||
|
body { |
||||||
|
background-color: #e7e7e7; |
||||||
|
overflow-y: auto; |
||||||
|
} |
||||||
|
|
||||||
|
.container { |
||||||
|
margin: 20px auto 20px 60px; |
||||||
|
} |
||||||
|
</style> |
After Width: | Height: | Size: 6.7 KiB |
@ -0,0 +1,18 @@ |
|||||||
|
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; |
||||||
|
} |
@ -0,0 +1,6 @@ |
|||||||
|
import { createApp } from 'vue' |
||||||
|
import App from './App.vue' |
||||||
|
import router from './router' |
||||||
|
import store from './store' |
||||||
|
|
||||||
|
createApp(App).use(store).use(router).mount('#app') |
@ -0,0 +1,27 @@ |
|||||||
|
import { createRouter, createWebHashHistory } from 'vue-router' |
||||||
|
import Trackers from "../views/Trackers"; |
||||||
|
import Settings from "../views/Settings"; |
||||||
|
|
||||||
|
Array.prototype.pushToBeginning = function (toPush) { |
||||||
|
return this.unshift.apply(this, [toPush]); |
||||||
|
} |
||||||
|
|
||||||
|
const routes = [ |
||||||
|
{ |
||||||
|
path: '/', |
||||||
|
name: 'Trackers', |
||||||
|
component: Trackers |
||||||
|
}, |
||||||
|
{ |
||||||
|
path: '/settings', |
||||||
|
name: 'Settings', |
||||||
|
component: Settings |
||||||
|
}, |
||||||
|
] |
||||||
|
|
||||||
|
const router = createRouter({ |
||||||
|
history: createWebHashHistory(), |
||||||
|
routes |
||||||
|
}) |
||||||
|
|
||||||
|
export default router |
@ -0,0 +1,96 @@ |
|||||||
|
import {createStore} from 'vuex' |
||||||
|
|
||||||
|
export default createStore({ |
||||||
|
state: { |
||||||
|
trackers: [], |
||||||
|
archive: [], |
||||||
|
settings: { |
||||||
|
theme: 'materia' |
||||||
|
}, |
||||||
|
trashed: {}, |
||||||
|
selectedTracker: {} |
||||||
|
}, |
||||||
|
mutations: { |
||||||
|
loadSavedData(state) { |
||||||
|
let storedTrackers = localStorage.getItem('trackers'); |
||||||
|
if (storedTrackers !== null && storedTrackers !== undefined) { |
||||||
|
state.trackers = JSON.parse(storedTrackers); |
||||||
|
} |
||||||
|
|
||||||
|
let storedArchive = localStorage.getItem('archive'); |
||||||
|
if (storedArchive !== null && storedArchive !== undefined) { |
||||||
|
state.archive = JSON.parse(storedArchive); |
||||||
|
} |
||||||
|
|
||||||
|
let storedSettings = localStorage.getItem('settings'); |
||||||
|
state.settings = storedSettings == null ? { |
||||||
|
theme: '', |
||||||
|
dashboardLogo: 'assets/img/logo.png', |
||||||
|
trackerSystemUrl: '', |
||||||
|
showPT: true, |
||||||
|
dontShowMinutes: false, |
||||||
|
linkTarget: '_blank', |
||||||
|
} : storedSettings; |
||||||
|
}, |
||||||
|
createTracker(state) { |
||||||
|
state.trackers.pushToBeginning({ |
||||||
|
tracking: false, |
||||||
|
number: '#', |
||||||
|
trackingStarted: null, |
||||||
|
trackingStopped: null, |
||||||
|
trashed: {}, |
||||||
|
history: [] |
||||||
|
}); |
||||||
|
}, |
||||||
|
createTimebox(state) { |
||||||
|
state.trackers.pushToBeginning({ |
||||||
|
tracking: false, |
||||||
|
number: 'Timebox ', |
||||||
|
trackingStarted: null, |
||||||
|
trackingStopped: null, |
||||||
|
isTimeBox: true, |
||||||
|
history: [] |
||||||
|
}); |
||||||
|
}, |
||||||
|
saveTrackers(state) { |
||||||
|
localStorage.setItem('trackers', JSON.stringify(state.trackers)); |
||||||
|
localStorage.setItem('archive', JSON.stringify(state.archive)); |
||||||
|
}, |
||||||
|
deleteTracker(state, index, archive) { |
||||||
|
let message = ''; |
||||||
|
|
||||||
|
if (archive) { |
||||||
|
Object.assign(state.trashed, state.archive[index]); |
||||||
|
let name = state.archive[index].number; |
||||||
|
message = 'Tracker "' + name + '" wurde gelöscht'; |
||||||
|
state.archive.splice(index, 1); |
||||||
|
} else { |
||||||
|
Object.assign(state.trashed, state.trackers[index]); |
||||||
|
let name = state.trackers[index].number; |
||||||
|
message = 'Tracker "' + name + '" wurde gelöscht'; |
||||||
|
state.trackers.splice(index, 1); |
||||||
|
} |
||||||
|
|
||||||
|
return message; |
||||||
|
}, |
||||||
|
restoreTrashed(state) { |
||||||
|
let restoredTracker = {}; |
||||||
|
Object.assign(restoredTracker, state.trashed); |
||||||
|
state.trashed = {}; |
||||||
|
|
||||||
|
state.trackers.pushToBeginning(restoredTracker); |
||||||
|
state.updateStorage(); |
||||||
|
}, |
||||||
|
archiveTracker(state, index) { |
||||||
|
state.archive.pushToBeginning(state.trackers[index]); |
||||||
|
state.trackers.splice(index, 1); |
||||||
|
}, |
||||||
|
reactivateTracker(state, index) { |
||||||
|
state.trackers.pushToBeginning(state.archive[index]); |
||||||
|
state.archive.splice(index, 1); |
||||||
|
}, |
||||||
|
selectTracker(state, tracker) { |
||||||
|
state.selectedTracker = tracker; |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
@ -0,0 +1,162 @@ |
|||||||
|
<template> |
||||||
|
<div class="modal modal-fullscreen fade" id="showArchivedTrackersModal" tabindex="-1" role="dialog" |
||||||
|
aria-labelledby="showArchivedTrackersModalLabel" |
||||||
|
aria-hidden="true"> |
||||||
|
<div class="modal-dialog showArchivedTrackersModalDialog" role="document"> |
||||||
|
<div class="modal-content"> |
||||||
|
<div class="modal-header"> |
||||||
|
<h5 class="modal-title"><i class="fas fa-archive"></i> Archivierte Tracker</h5> |
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
||||||
|
</div> |
||||||
|
<div class="modal-body"> |
||||||
|
<div class="row"> |
||||||
|
<div class="col-sm-6"></div> |
||||||
|
<div class="col-sm-6"> |
||||||
|
<i class="fas fa-search"></i> |
||||||
|
<input @keydown="$forceUpdate()" type="text" class="form-control search-field float-right" v-model="searchQuery" placeholder="Suche.."> |
||||||
|
</div> |
||||||
|
<template v-for="(tracker, trackerIndex) in $store.state.archive" v-bind:key="trackerIndex"> |
||||||
|
<div class="col-md-6" v-if="searchQuery === '' || tracker.number.search(searchQuery) >= 0 || (tracker.description && tracker.description.search(searchQuery)) >= 0"> |
||||||
|
<h6><span v-if="isTrackerNumber(tracker.number)"></span>{{ tracker.number }}</h6> |
||||||
|
<div v-if="tracker.description"> |
||||||
|
<p class="fst-italic">{{ tracker.description }}</p> |
||||||
|
</div> |
||||||
|
<div class="tracker-time-info"> |
||||||
|
<span class="float-end">{{ getTotalTime(tracker) }}</span> |
||||||
|
<span class="current-tracker-info">Gesamt: </span> |
||||||
|
</div> |
||||||
|
|
||||||
|
<span class="float-end">{{ getTotalTimeToday(tracker) }}</span> |
||||||
|
<span class="">Heute: </span> |
||||||
|
|
||||||
|
<br> |
||||||
|
<div class="row"> |
||||||
|
<div class="col"> |
||||||
|
<button class="btn btn-success tracker-action-button" data-bs-dismiss="modal" |
||||||
|
@click="reactivateTracker(trackerIndex)" title="Reaktivieren"> |
||||||
|
<i class="fas fa-power-off"></i> |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
<div class="col" v-if="tracker.history.length > 0"> |
||||||
|
<button class="btn btn-info tracker-action-button" data-bs-dismiss="modal" |
||||||
|
@click="showHistoryForTracker(tracker)" title="History"> |
||||||
|
<i class="fas fa-history"></i> |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
<div class="col"> |
||||||
|
<button class="btn btn-danger tracker-action-button" |
||||||
|
@click="deleteTracker(trackerIndex, true)" title="Löschen"> |
||||||
|
<i class="fas fa-trash"></i> |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
<div class="col" v-if="trackerSystemUrl"> |
||||||
|
<a v-if="isTrackerNumber(tracker.number)" :href="trackerSystemUrl + tracker.number.replace('#', '')" |
||||||
|
target="_blank" class="btn btn-dark tracker-action-button" title="Tracker"> |
||||||
|
<i class="fas fa-external-link-square-alt"></i> |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<br/> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import moment from "moment"; |
||||||
|
import bootstrap from "bootstrap"; |
||||||
|
|
||||||
|
export default { |
||||||
|
name: "Archive", |
||||||
|
data() { |
||||||
|
return { |
||||||
|
searchQuery: '' |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
isTrackerNumber(number) { |
||||||
|
return number.indexOf('#') >= 0; |
||||||
|
}, |
||||||
|
getTotalTime(tracker, raw = false) { |
||||||
|
let totalTime = 0; |
||||||
|
|
||||||
|
if (tracker.history.length > 0) { |
||||||
|
tracker.history.forEach(function (historyEntry) { |
||||||
|
totalTime += Math.round(historyEntry.minutes); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
if (tracker.tracking) { |
||||||
|
totalTime += Math.round(moment.duration(moment().diff(tracker.trackingStarted)).as('minutes')); |
||||||
|
} |
||||||
|
|
||||||
|
if (raw) { |
||||||
|
return totalTime; |
||||||
|
} else { |
||||||
|
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.$store.state.settings.dontShowMinutes) { |
||||||
|
postFix = ' Stunde'; |
||||||
|
time = (time / 60).toFixed(2); |
||||||
|
} |
||||||
|
|
||||||
|
let plural = ''; |
||||||
|
|
||||||
|
if (((time > 1 || time <= 0) || this.$store.state.settings.dontShowMinutes) && postFix !== ' PT') { |
||||||
|
plural = 'n' |
||||||
|
} |
||||||
|
|
||||||
|
return time + postFix + plural; |
||||||
|
}, |
||||||
|
getTotalTimeToday(tracker) { |
||||||
|
let totalTime = 0; |
||||||
|
|
||||||
|
if (tracker.history.length > 0) { |
||||||
|
tracker.history.forEach(function (historyEntry) { |
||||||
|
if (moment(historyEntry.trackingStarted).format("MMM Do YY") === moment().format("MMM Do YY")) { |
||||||
|
totalTime += Math.round(historyEntry.minutes); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
if (tracker.tracking) { |
||||||
|
totalTime += Math.round(moment.duration(moment().diff(tracker.trackingStarted)).as('minutes')); |
||||||
|
} |
||||||
|
|
||||||
|
return this.timeWithPostFix(totalTime); |
||||||
|
}, |
||||||
|
reactivateTracker(index) { |
||||||
|
this.$store.commit('reactivateTracker', index); |
||||||
|
this.$store.commit('saveTrackers'); |
||||||
|
}, |
||||||
|
showHistoryForTracker(tracker) { |
||||||
|
this.$store.commit('selectTracker', tracker); |
||||||
|
setTimeout(() => { |
||||||
|
let historyModal = new bootstrap.Modal(document.getElementById('historyModal')); |
||||||
|
historyModal.toggle(); |
||||||
|
}, 50); |
||||||
|
}, |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style scoped> |
||||||
|
|
||||||
|
.tracker-action-button { |
||||||
|
margin-top: 10px; |
||||||
|
padding: 1px 5px 1px 5px; |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
</style> |
@ -0,0 +1,149 @@ |
|||||||
|
<template> |
||||||
|
<div class="row" v-if="experimental.boardView && view === 'board'" id="board-view"> |
||||||
|
<div class="col-4 col-board-inner"> |
||||||
|
<h5>Todo</h5> |
||||||
|
<template v-for="(tracker, trackerIndex) in trackers"> |
||||||
|
</template> |
||||||
|
</div> |
||||||
|
<div class="col-4 col-board-inner"> |
||||||
|
<h5>On hold</h5> |
||||||
|
<template v-for="(tracker, trackerIndex) in trackers"> |
||||||
|
<div class="card bg-gradient-secondary" v-if="tracker.status === 'onhold'"> |
||||||
|
<div class="card-body"> |
||||||
|
<div class="card-text"> |
||||||
|
<input type="text" |
||||||
|
v-model="tracker.number" |
||||||
|
class="form-control trackingNameField" |
||||||
|
@keydown="updateStorage()"/> |
||||||
|
|
||||||
|
<div class="tracker-time-info"> |
||||||
|
<div v-if="tracker.tracking === true"> |
||||||
|
<div class="text-danger font-weight-bolder float-end"> |
||||||
|
<div class="spinner-grow spinner-grow-sm" role="status"> |
||||||
|
<span class="sr-only">Tracking...</span> |
||||||
|
</div> |
||||||
|
Tracking |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div v-if="tracker.tracking === true" class="tracker-time-info"> |
||||||
|
<span class="float-end">{{ getTrackingStartTime(tracker) }}</span> |
||||||
|
<span v-if="tracker.tracking === true">Gestartet: </span> |
||||||
|
<br/> |
||||||
|
<span class="float-end">{{ currentTrackingRunningFor(tracker) }}</span> |
||||||
|
<span v-if="tracker.tracking === true">Läuft seit: </span> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="tracker-time-info"> |
||||||
|
<span class="float-end">{{ getTotalTime(tracker) }}</span> |
||||||
|
<span class="current-tracker-info">Gesamt: </span> |
||||||
|
</div> |
||||||
|
|
||||||
|
<span class="float-end">{{ getTotalTimeToday(tracker) }}</span> |
||||||
|
<span class="">Heute: </span> |
||||||
|
|
||||||
|
<div class="row"> |
||||||
|
<div class="col-md-12" v-if="!tracker.tracking"> |
||||||
|
<button type="button" class="btn btn-info tracker-action-button" |
||||||
|
@click="startTracking(tracker)"> |
||||||
|
<i class="far fa-play-circle"></i> |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="col-md-12" v-else> |
||||||
|
<button type="button" class="btn btn-danger tracker-action-button" |
||||||
|
@click="stopTracking(tracker)"> |
||||||
|
<i class="far fa-stop-circle"></i> |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="col-md-12"> |
||||||
|
<button class="btn btn-warning tracker-action-button" data-bs-dismiss="modal" |
||||||
|
@click="archiveTracker(trackerIndex)" title="Archivieren"> |
||||||
|
<i class="fas fa-archive"></i> |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
</div> |
||||||
|
<div class="col-4"> |
||||||
|
<h5>Work in progress</h5> |
||||||
|
<template v-for="(tracker, trackerIndex) in trackers"> |
||||||
|
<div class="card bg-gradient-secondary" v-if="tracker.status === 'wip'"> |
||||||
|
<div class="card-body"> |
||||||
|
<div class="card-text"> |
||||||
|
<input type="text" |
||||||
|
v-model="tracker.number" |
||||||
|
class="form-control trackingNameField" |
||||||
|
@keydown="updateStorage()"/> |
||||||
|
|
||||||
|
<div class="tracker-time-info"> |
||||||
|
<div v-if="tracker.tracking === true"> |
||||||
|
<div class="text-danger font-weight-bolder float-end"> |
||||||
|
<div class="spinner-grow spinner-grow-sm" role="status"> |
||||||
|
<span class="sr-only">Tracking...</span> |
||||||
|
</div> |
||||||
|
Tracking |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div v-if="tracker.tracking === true" class="tracker-time-info"> |
||||||
|
<span class="float-end">{{ getTrackingStartTime(tracker) }}</span> |
||||||
|
<span v-if="tracker.tracking === true">Gestartet: </span> |
||||||
|
<br/> |
||||||
|
<span class="float-end">{{ currentTrackingRunningFor(tracker) }}</span> |
||||||
|
<span v-if="tracker.tracking === true">Läuft seit: </span> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="tracker-time-info"> |
||||||
|
<span class="float-end">{{ getTotalTime(tracker) }}</span> |
||||||
|
<span class="current-tracker-info">Gesamt: </span> |
||||||
|
</div> |
||||||
|
|
||||||
|
<span class="float-end">{{ getTotalTimeToday(tracker) }}</span> |
||||||
|
<span class="">Heute: </span> |
||||||
|
|
||||||
|
<div class="row"> |
||||||
|
<div class="col-md-12" v-if="!tracker.tracking"> |
||||||
|
<button type="button" class="btn btn-info tracker-action-button" |
||||||
|
@click="startTracking(tracker)"> |
||||||
|
<i class="far fa-play-circle"></i> |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="col-md-12" v-else> |
||||||
|
<button type="button" class="btn btn-danger tracker-action-button" |
||||||
|
@click="stopTracking(tracker)"> |
||||||
|
<i class="far fa-stop-circle"></i> |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="col-md-12"> |
||||||
|
<button class="btn btn-warning tracker-action-button" data-bs-dismiss="modal" |
||||||
|
@click="archiveTracker(trackerIndex)" title="Archivieren"> |
||||||
|
<i class="fas fa-archive"></i> |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
name: "Boardview" |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style scoped> |
||||||
|
|
||||||
|
</style> |
@ -0,0 +1,37 @@ |
|||||||
|
<template> |
||||||
|
<div class="modal fade" id="customBookingModal" tabindex="-1" role="dialog" aria-labelledby="customBookingModalLabel" |
||||||
|
aria-hidden="true" v-if="selectedTracker"> |
||||||
|
<div class="modal-dialog" role="document"> |
||||||
|
<div class="modal-content"> |
||||||
|
<div class="modal-header"> |
||||||
|
<h5 class="modal-title"><i class="fas fa-user-edit"></i> Buchung für {{selectedTracker.number}}</h5> |
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
||||||
|
</div> |
||||||
|
<div class="modal-body"> |
||||||
|
<div class="form-group"> |
||||||
|
<label>Zeit in Minuten:</label> |
||||||
|
<input type="text" class="form-control" v-model="customBookingValue"> |
||||||
|
</div> |
||||||
|
<div class="row"> |
||||||
|
<div class="col-6"> |
||||||
|
<a href="javascript:" class="btn btn-success btn-full-width" data-bs-dismiss="modal" @click="makeCustomBooking()">+ Hinzubuchen</a> |
||||||
|
</div> |
||||||
|
<div class="col-6"> |
||||||
|
<a href="javascript:" class="btn btn-danger btn-full-width" data-bs-dismiss="modal" @click="makeCustomBooking(true)">- Abbuchen</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
name: "CustomBookForTracker" |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style scoped> |
||||||
|
|
||||||
|
</style> |
@ -0,0 +1,41 @@ |
|||||||
|
<template> |
||||||
|
<div class="modal fade" id="pastDaysModal" tabindex="-1" role="dialog" aria-labelledby="pastDaysModalLabel" |
||||||
|
aria-hidden="true"> |
||||||
|
<div class="modal-dialog" role="document"> |
||||||
|
<div class="modal-content"> |
||||||
|
<div class="modal-header"> |
||||||
|
<h5 class="modal-title"><i class="fas fa-random"></i> Buchungsverlauf</h5> |
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
||||||
|
</div> |
||||||
|
<div class="modal-body"> |
||||||
|
<div class="row"> |
||||||
|
<div class="col-12"> |
||||||
|
<div class="form-group"> |
||||||
|
<div class="form-group"> |
||||||
|
<h6 for="date">Datum</h6> |
||||||
|
<input id="date" type="date" class="form-control" v-model="customDateForPastDays"> |
||||||
|
</div> |
||||||
|
<ul class="list-group"> |
||||||
|
<li class="list-group-item" v-for="entry in collectDataForDay(0, customDateForPastDays)" :style="entry.archive ? 'background-color: lightgrey;' : ''"> |
||||||
|
{{ entry.tracker }} |
||||||
|
<span class="float-end">{{ timeWithPostFix(entry.minutes) }}</span> |
||||||
|
</li> |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
name: "History" |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style scoped> |
||||||
|
|
||||||
|
</style> |
@ -0,0 +1,74 @@ |
|||||||
|
<template> |
||||||
|
<div class="modal fade" id="historyModal" tabindex="-1" role="dialog" aria-labelledby="historyModalLabel" |
||||||
|
aria-hidden="true" v-if="selectedTracker"> |
||||||
|
<div class="modal-dialog" role="document"> |
||||||
|
<div class="modal-content"> |
||||||
|
<div class="modal-header"> |
||||||
|
<h5 class="modal-title"><i class="fas fa-history"></i> History von {{ selectedTracker.number }}</h5> |
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
||||||
|
</div> |
||||||
|
<div class="modal-body"> |
||||||
|
<ul class="list-group tracker-history"> |
||||||
|
<template v-for="(tracker, historyIndex) in selectedTracker.history"> |
||||||
|
<li class="list-group-item" v-if="!tracker.manually"> |
||||||
|
<div> |
||||||
|
<div class="float-end" :title="exactTimestamp(tracker.trackingStarted)"> |
||||||
|
{{ formattedDate(tracker.trackingStarted) }} |
||||||
|
</div> |
||||||
|
Start: |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<div class="float-end" :title="exactTimestamp(tracker.trackingStopped)"> |
||||||
|
{{ formattedDate(tracker.trackingStopped) }} |
||||||
|
</div> |
||||||
|
Ende: |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<div class="float-end"> |
||||||
|
{{ timeWithPostFix(tracker.minutes) }} |
||||||
|
</div> |
||||||
|
Zeit: |
||||||
|
</div> |
||||||
|
<a href="javascript:" @click="deleteHistoryEntry(null, historyIndex)" class="float-end"> |
||||||
|
<i class="fas fa-trash"></i> |
||||||
|
</a> |
||||||
|
<br/> |
||||||
|
</li> |
||||||
|
</template> |
||||||
|
<template v-for="(tracker, historyIndex) in selectedTracker.history"> |
||||||
|
<li class="list-group-item bg-light" |
||||||
|
v-if="tracker.manually"> |
||||||
|
<div> |
||||||
|
<div class="float-end" :title="exactTimestamp(tracker.trackingStarted)"> |
||||||
|
{{ formattedDate(tracker.trackingStarted) }} |
||||||
|
</div> |
||||||
|
Manuell erfasst am: |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<div class="float-end"> |
||||||
|
{{ timeWithPostFix(tracker.minutes) }} |
||||||
|
</div> |
||||||
|
Zeit: |
||||||
|
</div> |
||||||
|
<a href="javascript:" @click="deleteHistoryEntry(trackerIndex, historyIndex)" class="float-end"> |
||||||
|
<i class="fas fa-trash"></i> |
||||||
|
</a> |
||||||
|
<br/> |
||||||
|
</li> |
||||||
|
</template> |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
name: "HistoryForTracker" |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style scoped> |
||||||
|
|
||||||
|
</style> |
@ -0,0 +1,112 @@ |
|||||||
|
<template> |
||||||
|
<div id="left-menu"> |
||||||
|
<div class="container"> |
||||||
|
<div class="row"> |
||||||
|
<div class="col"> |
||||||
|
<div class="dropup"> |
||||||
|
<button class="btn btn-success text-light bottom-menu-item" type="button" id="create-dropdown-menu" data-bs-toggle="dropdown" aria-expanded="false" title="Neu"> |
||||||
|
<i class="fas fa-plus"></i> |
||||||
|
</button> |
||||||
|
<ul class="dropdown-menu" aria-labelledby="create-dropdown-menu"> |
||||||
|
<li><button class="btn btn-light dropdown-menu-button" type="button" @click="createTracker()">Tracker</button></li> |
||||||
|
<li><button class="btn btn-light dropdown-menu-button" type="button" @click="createTimeBox()">Timebox</button></li> |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<router-link to="/" |
||||||
|
type="button" |
||||||
|
class="btn btn-dark text-light bottom-menu-item" |
||||||
|
data-bs-placement="left" |
||||||
|
title="Home"> |
||||||
|
<i class="fas fa-home"></i> |
||||||
|
</router-link> |
||||||
|
</div> |
||||||
|
<div class="" v-if="$store.state.trackers.length > 0"> |
||||||
|
<a type="button" |
||||||
|
class="btn btn-primary text-light bottom-menu-item" |
||||||
|
data-bs-toggle="modal" |
||||||
|
data-bs-target="#showTrackersModal" |
||||||
|
data-bs-placement="top" |
||||||
|
title="Alle Tracker"> |
||||||
|
<i class="fas fa-clock"></i> |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
<div class="" v-if="$store.state.archive.length > 0"> |
||||||
|
<a type="button" |
||||||
|
class="btn btn-warning bottom-menu-item" |
||||||
|
data-bs-toggle="modal" |
||||||
|
data-bs-target="#showArchivedTrackersModal" |
||||||
|
data-bs-placement="top" |
||||||
|
title="Archivierte Tracker"> |
||||||
|
<i class="fas fa-archive"></i> |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
<div class=""> |
||||||
|
<a type="button" |
||||||
|
class="btn btn-info text-light bottom-menu-item" |
||||||
|
data-bs-toggle="modal" |
||||||
|
data-bs-target="#pastDaysModal" |
||||||
|
data-bs-placement="top" |
||||||
|
title="Buchungsverlauf"> |
||||||
|
<i class="fas fa-history"></i> |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
<div class=""> |
||||||
|
|
||||||
|
<router-link to="/settings" |
||||||
|
type="button" |
||||||
|
class="btn btn-dark text-light bottom-menu-item" |
||||||
|
data-bs-placement="left" |
||||||
|
title="Einstellungen"> |
||||||
|
<i class="fas fa-sliders-h"></i> |
||||||
|
</router-link> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import bootstrap from "bootstrap"; |
||||||
|
|
||||||
|
export default { |
||||||
|
name: "Menu", |
||||||
|
props: { |
||||||
|
archive: Array |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
createTracker() { |
||||||
|
this.$store.commit('createTracker'); |
||||||
|
this.$store.commit('saveTrackers'); |
||||||
|
}, |
||||||
|
createTimeBox() { |
||||||
|
this.$store.commit('createTimebox'); |
||||||
|
this.$store.commit('saveTrackers'); |
||||||
|
}, |
||||||
|
openTasksForTracker(tracker) { |
||||||
|
this.selectedTracker = tracker; |
||||||
|
this.$forceUpdate(); |
||||||
|
setTimeout(() => { |
||||||
|
let tasksModal = new bootstrap.Modal(document.getElementById('trackerTasksModal')); |
||||||
|
tasksModal.toggle(); |
||||||
|
}, 50); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style scoped> |
||||||
|
.dropdown-menu { |
||||||
|
background: transparent; |
||||||
|
border: none; |
||||||
|
box-shadow: none; |
||||||
|
} |
||||||
|
|
||||||
|
.dropdown-menu-button { |
||||||
|
width: 10em; |
||||||
|
margin-bottom: 1em; |
||||||
|
} |
||||||
|
|
||||||
|
</style> |
@ -0,0 +1,157 @@ |
|||||||
|
<template> |
||||||
|
<h5 class="modal-title"><i class="fas fa-sliders-h"></i> Einstellungen</h5> |
||||||
|
<hr/> |
||||||
|
<div class="row"> |
||||||
|
<div class="col-md-6"> |
||||||
|
<h5>Allgemeine Einstellungen</h5> |
||||||
|
<div class="form-group"> |
||||||
|
<label>Logo-Pfad</label> |
||||||
|
<input type="text" v-model="dashboardLogo" class="form-control"> |
||||||
|
</div> |
||||||
|
<div class="form-group"> |
||||||
|
<label>Tracker-Link <small>Link zu einem Tracker ohne Trackernummer</small></label> |
||||||
|
<input type="text" v-model="trackerSystemUrl" class="form-control"> |
||||||
|
</div> |
||||||
|
<br/> |
||||||
|
<div class="form-group"> |
||||||
|
<label> |
||||||
|
<input type="checkbox" class="form-control-checkbox" v-model="showPT"> |
||||||
|
Ab 8 Stunden nurmehr PT anzeigen |
||||||
|
</label> |
||||||
|
</div> |
||||||
|
<br/> |
||||||
|
<div class="form-group"> |
||||||
|
<label> |
||||||
|
<input type="checkbox" class="form-control-checkbox" v-model="dontShowMinutes"> |
||||||
|
Zeit immer in Stunden anzeigen |
||||||
|
</label> |
||||||
|
</div> |
||||||
|
<br/> |
||||||
|
<div class="form-group"> |
||||||
|
<label>Design</label> |
||||||
|
<select v-model="theme" class="form-control"> |
||||||
|
<option v-for="(availableTheme, themeIndex) in themes" :value="availableTheme.name.toLowerCase()" |
||||||
|
v-bind:key="themeIndex">{{ availableTheme.name }} |
||||||
|
</option> |
||||||
|
</select> |
||||||
|
</div> |
||||||
|
<br/> |
||||||
|
</div> |
||||||
|
<div class="col-md-6"> |
||||||
|
<h5>Zurücksetzen & Löschen</h5> |
||||||
|
<div class="row"> |
||||||
|
<div class="col-md-6"> |
||||||
|
<button class="btn btn-outline-danger btn-full-width" @click="deleteAllData()">Alle Daten |
||||||
|
löschen |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<br/> |
||||||
|
<h5>Import & Export</h5> |
||||||
|
<div class="row"> |
||||||
|
<div class="col-md-6"> |
||||||
|
<h6>Export-Json</h6> |
||||||
|
<textarea class="form-control" rows="3" id="exportJsonInput" v-model="exportJson"></textarea> |
||||||
|
<button class="btn btn-success btn-full-width" @click="copy2Clipboard">Export-String kopieren |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
<div class="col-md-6"> |
||||||
|
<h6>Import</h6> |
||||||
|
<textarea class="form-control" rows="3" v-model="importJson"></textarea> |
||||||
|
<button class="btn btn-success btn-full-width" @click="importData">Import</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="modal-footer"> |
||||||
|
<button class="btn btn-primary" v-on:click="updateStorage()" data-bs-dismiss="modal">Speichern</button> |
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Schließen</button> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import axios from "axios"; |
||||||
|
|
||||||
|
export default { |
||||||
|
name: "Settings", |
||||||
|
data() { |
||||||
|
return { |
||||||
|
settings: {}, |
||||||
|
importJson: '', |
||||||
|
theme: '', |
||||||
|
themes: null, |
||||||
|
dashboardLogo: 'assets/img/logo.png', |
||||||
|
trackerSystemUrl: '', |
||||||
|
showPT: true, |
||||||
|
dontShowMinutes: false, |
||||||
|
linkTarget: '_blank', |
||||||
|
exportJson: '' |
||||||
|
} |
||||||
|
}, |
||||||
|
mounted() { |
||||||
|
this.fetchThemes(); |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
importData() { |
||||||
|
let json = JSON.parse(this.importJson); |
||||||
|
|
||||||
|
if (json.trackedTrackers) { |
||||||
|
this.trackers = this.extractTrackersFromLegacyJson(json.trackedTrackers); |
||||||
|
this.archive = this.extractArchivedTrackersFromLegacyJson(json.trackedTrackers); |
||||||
|
} else { |
||||||
|
this.trackers = json.trackers; |
||||||
|
this.archive = json.archive ?? []; |
||||||
|
} |
||||||
|
|
||||||
|
this.trackerSystemUrl = json.redmineUrl ?? json.trackerSystemUrl; |
||||||
|
this.showPT = json.showPT; |
||||||
|
this.theme = json.theme; |
||||||
|
this.updateStorage(); |
||||||
|
location.reload(); |
||||||
|
}, |
||||||
|
extractTrackersFromLegacyJson(trackers) { |
||||||
|
return this.extractTrackers(trackers); |
||||||
|
}, |
||||||
|
extractArchivedTrackersFromLegacyJson(trackers) { |
||||||
|
return this.extractTrackers(trackers, true); |
||||||
|
}, |
||||||
|
extractTrackers(trackerCollection, forArchive = false) { |
||||||
|
let trackers = []; |
||||||
|
let archive = []; |
||||||
|
|
||||||
|
trackerCollection.forEach((tracker) => { |
||||||
|
if (tracker.archived || (tracker.active && tracker.active === false)) { |
||||||
|
archive.pushToBeginning(tracker); |
||||||
|
} else { |
||||||
|
trackers.pushToBeginning(tracker) |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
return forArchive ? archive : trackers; |
||||||
|
}, |
||||||
|
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); |
||||||
|
}); |
||||||
|
}, |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style scoped> |
||||||
|
|
||||||
|
</style> |
@ -0,0 +1,59 @@ |
|||||||
|
<template> |
||||||
|
<div class="modal fade" id="switcherModal" tabindex="-1" role="dialog" aria-labelledby="switcherModalLabel" |
||||||
|
aria-hidden="true" v-if=""> |
||||||
|
<div class="modal-dialog" role="document"> |
||||||
|
<div class="modal-content"> |
||||||
|
<div class="modal-header"> |
||||||
|
<h5 class="modal-title"><i class="fas fa-random"></i> Portal Switcher</h5> |
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
||||||
|
</div> |
||||||
|
<div class="modal-body"> |
||||||
|
<div class="row"> |
||||||
|
<div class="col-12"> |
||||||
|
<div class="form-group"> |
||||||
|
<label class="text-muted">Portalname:</label> |
||||||
|
<input v-if="!portals || portals.length === 0" class="form-control" @change="updateStorage()" @keydown="updateStorage()" v-model="portal"/> |
||||||
|
<select v-else v-model="portal" class="form-control"> |
||||||
|
<option v-for="selectablePortal in portals" :value="selectablePortal">{{ selectablePortal }}</option> |
||||||
|
</select> |
||||||
|
<div class="form-group" v-if="experimental.portalSwitcher"> |
||||||
|
<label> |
||||||
|
<input type="checkbox" class="form-control-checkbox" v-model="publicDB"> |
||||||
|
Auf Standard-DB wechseln? |
||||||
|
</label> |
||||||
|
</div> |
||||||
|
<br/> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<template v-if="portal && portal !== ''"> |
||||||
|
<div class="col-12 switch-portal-button"> |
||||||
|
<a class="btn btn-danger btn-switcher" href="javascript:" @click="sendPortalChangeRequest"> |
||||||
|
<i class="fas fa-random"></i> Wechsle Portal |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
<div class="col"> |
||||||
|
<a class="btn btn-info btn-switcher" href="https://my.vemap.docker" target="_blank">Docker</a> |
||||||
|
</div> |
||||||
|
<div class="col"> |
||||||
|
<a class="btn btn-warning btn-switcher" :href="getPortalLink(true)" target="_blank">Test</a> |
||||||
|
</div> |
||||||
|
<div class="col"> |
||||||
|
<a class="btn btn-success btn-switcher" :href="getPortalLink()" target="_blank">Live</a> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
name: "Switcher" |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style scoped> |
||||||
|
|
||||||
|
</style> |
@ -0,0 +1,75 @@ |
|||||||
|
<template> |
||||||
|
<div class="modal modal-fullscreen fade" |
||||||
|
v-if="selectedTracker" |
||||||
|
id="trackerTasksModal" |
||||||
|
tabindex="-1" |
||||||
|
role="dialog" |
||||||
|
aria-labelledby="showTrackerTasksModal" |
||||||
|
aria-hidden="true"> |
||||||
|
<div class="modal-dialog" role="document"> |
||||||
|
<div class="modal-content"> |
||||||
|
<div class="modal-header"> |
||||||
|
<h5 class="modal-title"><i class="fas fa-clock"></i> Tasks für {{ selectedTracker.number }}</h5> |
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
||||||
|
</div> |
||||||
|
<div class="modal-body"> |
||||||
|
<div class="form-group"> |
||||||
|
<input type="text" id="newTaskInput" class="form-control" |
||||||
|
v-model="newTaskInput" placeholder="Neuer Task" v-on:keyup.enter="addTask()"/> |
||||||
|
</div> |
||||||
|
<ul class="list-group" v-if="selectedTracker.tasks && selectedTracker.tasks.length > 0"> |
||||||
|
<template v-for="(task, taskIndex) in selectedTracker.tasks"> |
||||||
|
<li class="list-group-item" v-if="!task.done"> |
||||||
|
<span class="float-end"> |
||||||
|
<a href="javascript:"> |
||||||
|
<i class="fas fa-trash" @click="deleteTask(taskIndex)"></i> |
||||||
|
</a> |
||||||
|
</span> |
||||||
|
<a href="javascript:" @click="toggleTask(task)"> |
||||||
|
<i class="far fa-square"></i> |
||||||
|
</a> {{ task.name }} |
||||||
|
<div class="form-group"> |
||||||
|
<div class="float-end"> |
||||||
|
{{ task.percentDone }}% erledigt |
||||||
|
</div> |
||||||
|
<input type="range" |
||||||
|
class="range range-success range-tasks" |
||||||
|
min="0" |
||||||
|
max="100" |
||||||
|
step="5" |
||||||
|
v-model="task.percentDone" |
||||||
|
@change="checkForCompletionOfTask(task)"> |
||||||
|
</div> |
||||||
|
</li> |
||||||
|
</template> |
||||||
|
</ul> |
||||||
|
<br/> |
||||||
|
<ul class="list-group" v-if="selectedTracker.tasks && selectedTracker.tasks.length > 0"> |
||||||
|
<template v-for="(task, taskIndex) in selectedTracker.tasks"> |
||||||
|
<li class="list-group-item" v-if="task.done"> |
||||||
|
<span class="float-end"> |
||||||
|
<a href="javascript:"> |
||||||
|
<i class="fas fa-trash" @click="deleteTask(taskIndex)"></i> |
||||||
|
</a> |
||||||
|
</span> |
||||||
|
<a href="javascript:" @click="toggleTask(task)"> |
||||||
|
<i class="far fa-check-square"></i> |
||||||
|
</a> <span class="finished-task">{{ task.name }}</span> |
||||||
|
</li> |
||||||
|
</template> |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
name: "TasksForTracker" |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style scoped> |
||||||
|
|
||||||
|
</style> |
@ -0,0 +1,374 @@ |
|||||||
|
<template> |
||||||
|
<div class="row"> |
||||||
|
<div class="col-md-12"> |
||||||
|
<div class="row"> |
||||||
|
<template v-for="(tracker, trackerIndex) in $store.state.trackers" v-bind:key="trackerIndex"> |
||||||
|
<div class="col-lg-4 col-md-6"> |
||||||
|
<div :class="'card ' + (tracker.isTimeBox ? 'bg-timebox' : 'bg-gradient-secondary')" |
||||||
|
:style="tracker.isTimeBox ? 'background: linear-gradient(90deg, grey ' + ((timeBoxTimeLeft(tracker) * 100) / tracker.timeBoxMinutes)+'% , black 100%':''"> |
||||||
|
<div class="card-body"> |
||||||
|
<div class="card-text"> |
||||||
|
<input type="text" |
||||||
|
v-model="tracker.number" |
||||||
|
class="form-control trackingNameField" |
||||||
|
@keydown="this.$store.commit('saveTrackers')"/> |
||||||
|
|
||||||
|
<div class="row"> |
||||||
|
<div v-if="tracker.tracking === true"> |
||||||
|
<div class="text-danger font-weight-bolder float-end tracker-time-info"> |
||||||
|
<div class="spinner-grow spinner-grow-sm" role="status"> |
||||||
|
<span class="sr-only">Tracking...</span> |
||||||
|
</div> |
||||||
|
Tracking |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<template v-if="!tracker.isTimeBox"> |
||||||
|
<div v-if="tracker.tracking === true"> |
||||||
|
<span class="float-end">{{ getTrackingStartTime(tracker) }}</span> |
||||||
|
<span v-if="tracker.tracking === true">Gestartet: </span> |
||||||
|
<br/> |
||||||
|
<span class="float-end">{{ currentTrackingRunningFor(tracker) }}</span> |
||||||
|
<span v-if="tracker.tracking === true">Läuft seit: </span> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div> |
||||||
|
<span class="float-end">{{ getTotalTime(tracker) }}</span> |
||||||
|
<span class="current-tracker-info">Gesamt: </span> |
||||||
|
</div> |
||||||
|
|
||||||
|
<span class="float-end">{{ getTotalTimeToday(tracker) }}</span> |
||||||
|
<span class="">Heute: </span> |
||||||
|
|
||||||
|
<div class="row"> |
||||||
|
<div class="col-md-6" v-if="!tracker.tracking"> |
||||||
|
<button type="button" class="btn btn-primary tracker-action-button" |
||||||
|
@click="startTracking(tracker)"> |
||||||
|
<i class="far fa-play-circle"></i> <small>Starten</small> |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="col-md-6" v-else> |
||||||
|
<button type="button" class="btn btn-danger tracker-action-button" |
||||||
|
@click="stopTracking(tracker)"> |
||||||
|
<i class="far fa-stop-circle"></i> <small>Stoppen</small> |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="col-6"> |
||||||
|
<button class="btn btn-warning tracker-action-button" |
||||||
|
data-bs-dismiss="modal" |
||||||
|
@click="archiveTracker(trackerIndex)" title="Archivieren"> |
||||||
|
<i class="fas fa-archive"></i> <small>Archivieren</small> |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="col-md-6"> |
||||||
|
<button type="button" class="btn btn-info tracker-action-button" |
||||||
|
@click="openTasksForTracker(tracker)" title="Tasks"> |
||||||
|
<i class="fas fa-clipboard -check"></i> <small>Tasks |
||||||
|
{{ showOpenTasksForTracker(tracker.tasks) }}</small> |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="col-6"> |
||||||
|
<button class="btn btn-danger tracker-action-button" |
||||||
|
@click="deleteTracker(trackerIndex)" title="Löschen"> |
||||||
|
<i class="fas fa-trash"></i> <small>Löschen</small> |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
<template v-else> |
||||||
|
<div class="row"> |
||||||
|
<div v-if="tracker.tracking === true"> |
||||||
|
<span class="float-end">{{ getTrackingStartTime(tracker) }}</span> |
||||||
|
<span v-if="tracker.tracking === true">Gestartet: </span> |
||||||
|
<br/> |
||||||
|
<span class="float-end">{{ currentTrackingRunningFor(tracker) }}</span> |
||||||
|
<span v-if="tracker.tracking === true">Läuft seit: </span> |
||||||
|
</div> |
||||||
|
|
||||||
|
<template v-if="!tracker.timeBoxMinutes"> |
||||||
|
<div class="col-12"> |
||||||
|
Minuten: |
||||||
|
</div> |
||||||
|
<div class="col"> |
||||||
|
<button type="button" class="btn btn-secondary tracker-action-button" |
||||||
|
@click="startTimeBox(tracker, 15)"> |
||||||
|
<i class="far fa-play-circle"></i> 15 |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="col"> |
||||||
|
<button type="button" class="btn btn-secondary tracker-action-button" |
||||||
|
@click="startTimeBox(tracker, 30)"> |
||||||
|
<i class="far fa-play-circle"></i> 30 |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="col"> |
||||||
|
<button type="button" class="btn btn-secondary tracker-action-button" |
||||||
|
@click="startTimeBox(tracker, 60)"> |
||||||
|
<i class="far fa-play-circle"></i> 60 |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<div class="col-12" |
||||||
|
v-if="tracker.timeBoxMinutes && timeBoxTimeLeft(tracker) > 0"> |
||||||
|
<span class="float-end">{{ timeBoxTimeLeft(tracker) }} Minuten</span> |
||||||
|
<span>Zeit übrig: </span> |
||||||
|
</div> |
||||||
|
<div class="col-12 text-center" v-if="timeBoxTimeLeft(tracker) <= 0"> |
||||||
|
<h5 class="text-danger text-bold">Abgeschlossen</h5> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="col-md-12" v-if="tracker.tracking"> |
||||||
|
<button type="button" class="btn btn-danger tracker-action-button" |
||||||
|
@click="stopTracking(tracker)"> |
||||||
|
<i class="far fa-stop-circle"></i> <small>Stoppen</small> |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
<div class="col-md-12" |
||||||
|
v-if="!tracker.tracking && tracker.timeBoxMinutes && timeBoxTimeLeft(tracker) > 0"> |
||||||
|
<button type="button" class="btn btn-primary tracker-action-button" |
||||||
|
@click="startTracking(tracker)"> |
||||||
|
<i class="far fa-play-circle"></i> <small>Starten</small> |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="col-md-12"> |
||||||
|
<button class="btn btn-danger tracker-action-button" |
||||||
|
@click="deleteTracker(trackerIndex)" title="Löschen"> |
||||||
|
<i class="fas fa-trash"></i> <small>Löschen</small> |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
import moment from "moment"; |
||||||
|
import iziToast from "izitoast"; |
||||||
|
|
||||||
|
export default { |
||||||
|
name: "Trackers", |
||||||
|
data() { |
||||||
|
return { |
||||||
|
} |
||||||
|
}, |
||||||
|
computed: { |
||||||
|
trackers() { |
||||||
|
return this.$store.state.trackers; |
||||||
|
} |
||||||
|
}, |
||||||
|
methods: { |
||||||
|
startTracking(tracker, individual = false, timeBoxMinutes = null) { |
||||||
|
if (!individual) { |
||||||
|
this.stopActiveTracker(); |
||||||
|
} |
||||||
|
|
||||||
|
if (timeBoxMinutes) { |
||||||
|
tracker.timeBoxMinutes = timeBoxMinutes; |
||||||
|
} |
||||||
|
|
||||||
|
tracker.status = 'wip'; |
||||||
|
|
||||||
|
tracker.trackingStarted = moment(); |
||||||
|
tracker.tracking = true; |
||||||
|
|
||||||
|
this.$forceUpdate(); |
||||||
|
this.$store.commit('saveTrackers'); |
||||||
|
}, |
||||||
|
stopTracking(tracker) { |
||||||
|
tracker.trackingStopped = moment(); |
||||||
|
tracker.tracking = false; |
||||||
|
|
||||||
|
let minutesSpent = moment.duration( |
||||||
|
tracker.trackingStopped.diff(tracker.trackingStarted) |
||||||
|
).as('minutes'); |
||||||
|
|
||||||
|
if (minutesSpent > 0) { |
||||||
|
let historyEntry = { |
||||||
|
trackingStarted: tracker.trackingStarted, |
||||||
|
trackingStopped: tracker.trackingStopped, |
||||||
|
manually: false, |
||||||
|
minutes: Math.round(minutesSpent) |
||||||
|
}; |
||||||
|
|
||||||
|
tracker.history.pushToBeginning(historyEntry); |
||||||
|
} |
||||||
|
|
||||||
|
tracker.trackingStarted = null; |
||||||
|
tracker.trackingStopped = null; |
||||||
|
|
||||||
|
this.$store.commit('saveTrackers'); |
||||||
|
}, |
||||||
|
archiveTracker(index) { |
||||||
|
this.stopActiveTracker(); |
||||||
|
this.$store.commit('archiveTracker', index); |
||||||
|
this.$store.commit('saveTrackers'); |
||||||
|
}, |
||||||
|
currentTrackingRunningFor(tracker) { |
||||||
|
return this.timeWithPostFix(Math.round(moment.duration(moment().diff(tracker.trackingStarted)).as('minutes'))); |
||||||
|
}, |
||||||
|
getTotalTime(tracker, raw = false) { |
||||||
|
let totalTime = 0; |
||||||
|
|
||||||
|
if (tracker.history.length > 0) { |
||||||
|
tracker.history.forEach(function (historyEntry) { |
||||||
|
totalTime += Math.round(historyEntry.minutes); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
if (tracker.tracking) { |
||||||
|
totalTime += Math.round(moment.duration(moment().diff(tracker.trackingStarted)).as('minutes')); |
||||||
|
} |
||||||
|
|
||||||
|
if (raw) { |
||||||
|
return totalTime; |
||||||
|
} else { |
||||||
|
return this.timeWithPostFix(totalTime); |
||||||
|
} |
||||||
|
}, |
||||||
|
getTotalTimeToday(tracker) { |
||||||
|
let totalTime = 0; |
||||||
|
|
||||||
|
if (tracker.history.length > 0) { |
||||||
|
tracker.history.forEach(function (historyEntry) { |
||||||
|
if (moment(historyEntry.trackingStarted).format("MMM Do YY") === moment().format("MMM Do YY")) { |
||||||
|
totalTime += Math.round(historyEntry.minutes); |
||||||
|
} |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
if (tracker.tracking) { |
||||||
|
totalTime += Math.round(moment.duration(moment().diff(tracker.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.$store.state.settings.dontShowMinutes) { |
||||||
|
postFix = ' Stunde'; |
||||||
|
time = (time / 60).toFixed(2); |
||||||
|
} |
||||||
|
|
||||||
|
let plural = ''; |
||||||
|
|
||||||
|
if (((time > 1 || time <= 0) || this.$store.state.settings.dontShowMinutes) && postFix !== ' PT') { |
||||||
|
plural = 'n' |
||||||
|
} |
||||||
|
|
||||||
|
return time + postFix + plural; |
||||||
|
}, |
||||||
|
stopActiveTracker() { |
||||||
|
let vue = this; |
||||||
|
|
||||||
|
vue.trackers.forEach(function (tracker) { |
||||||
|
if (tracker.tracking === true) { |
||||||
|
vue.stopTracking(tracker); |
||||||
|
} |
||||||
|
}) |
||||||
|
}, |
||||||
|
getTrackingStartTime(tracker) { |
||||||
|
return moment(tracker.trackingStarted).format('LT'); |
||||||
|
}, |
||||||
|
isTrackerNumber(number) { |
||||||
|
return number.indexOf('#') >= 0; |
||||||
|
}, |
||||||
|
deleteTracker(index, archive = false) { |
||||||
|
let message = this.$store.commit("deleteTracker", index, archive); |
||||||
|
|
||||||
|
iziToast.show({ |
||||||
|
message: message, |
||||||
|
color: 'blue', |
||||||
|
buttons: [ |
||||||
|
['<button><i class="fas fa-undo"></i></button>', function (instance, toast) { |
||||||
|
instance.hide({ |
||||||
|
transitionOut: 'fadeOutUp', |
||||||
|
onClosing: function(){ |
||||||
|
this.$store.commit('restoreTrashed'); |
||||||
|
} |
||||||
|
}, toast, 'buttonName'); |
||||||
|
}, true] |
||||||
|
] |
||||||
|
}); |
||||||
|
this.$store.commit('saveTrackers'); |
||||||
|
}, |
||||||
|
showOpenTasksForTracker(tasks) { |
||||||
|
let count = this.getOpenTasksForTracker(tasks); |
||||||
|
return count > 0 ? ' ('+count+' offen)' : ''; |
||||||
|
}, |
||||||
|
getOpenTasksForTracker(tasks) { |
||||||
|
if (!tasks) { |
||||||
|
return 0; |
||||||
|
} |
||||||
|
|
||||||
|
let counter = 0; |
||||||
|
|
||||||
|
tasks.forEach((task) => { |
||||||
|
if (!task.done) { |
||||||
|
counter++; |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
return counter; |
||||||
|
}, |
||||||
|
timeBoxTimeLeft(tracker) { |
||||||
|
return Number(tracker.timeBoxMinutes - this.getTotalTime(tracker, true)); |
||||||
|
}, |
||||||
|
startTimeBox(tracker, minutes) { |
||||||
|
Notification.requestPermission(); |
||||||
|
this.startTracking(tracker, false, minutes); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style scoped> |
||||||
|
.tracker-time-info { |
||||||
|
margin-top: 1em; |
||||||
|
clear: both; |
||||||
|
} |
||||||
|
|
||||||
|
.tracker-action-button { |
||||||
|
margin-top: 10px; |
||||||
|
padding: 1px 5px 1px 5px; |
||||||
|
width: 100%; |
||||||
|
} |
||||||
|
|
||||||
|
.trackingNameField { |
||||||
|
max-height: 1em; |
||||||
|
margin-bottom: 1px; |
||||||
|
} |
||||||
|
|
||||||
|
.bg-timebox { |
||||||
|
background-color: black; |
||||||
|
color: white; |
||||||
|
} |
||||||
|
|
||||||
|
.bg-timebox input { |
||||||
|
color: white; |
||||||
|
} |
||||||
|
|
||||||
|
.bg-timebox input:focus { |
||||||
|
color: lightgray; |
||||||
|
} |
||||||
|
|
||||||
|
</style> |
@ -0,0 +1,74 @@ |
|||||||
|
<template> |
||||||
|
<div class="modal modal-fullscreen fade" id="showTrackersModal" tabindex="-1" role="dialog" |
||||||
|
aria-labelledby="showTrackersModalLabel" |
||||||
|
aria-hidden="true"> |
||||||
|
<div class="modal-dialog showTrackersModalDialog" role="document"> |
||||||
|
<div class="modal-content"> |
||||||
|
<div class="modal-header"> |
||||||
|
<h5 class="modal-title"><i class="fas fa-clock"></i> Tracker</h5> |
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> |
||||||
|
</div> |
||||||
|
<div class="modal-body"> |
||||||
|
<div class="row"> |
||||||
|
<template v-for="(tracker, trackerIndex) in trackers"> |
||||||
|
<div class="col-md-6"> |
||||||
|
<h6><span v-if="isTrackerNumber(tracker.number)"></span>{{ tracker.number }}</h6> |
||||||
|
|
||||||
|
<div class="form-group"> |
||||||
|
<input type="text" class="form-control" v-model="tracker.description" @keydown="updateStorage()" placeholder="Beschreibung"> |
||||||
|
</div> |
||||||
|
|
||||||
|
<span v-if="getTotalTime(tracker) > 0">Gesamtzeit: {{getTotalTime(tracker)}}</span> |
||||||
|
<br> |
||||||
|
<div class="row"> |
||||||
|
<div class="col"> |
||||||
|
<button class="btn btn-info tracker-action-button" |
||||||
|
@click="showCustomBookingForTracker(tracker)" |
||||||
|
title="Manuelle Buchung"> |
||||||
|
<i class="fas fa-user-edit"></i> |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
<div class="col"> |
||||||
|
<button class="btn btn-warning tracker-action-button" data-bs-dismiss="modal" |
||||||
|
@click="archiveTracker(trackerIndex)" title="Archivieren"> |
||||||
|
<i class="fas fa-archive"></i> |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
<div class="col" v-if="tracker.history.length > 0"> |
||||||
|
<button class="btn btn-info tracker-action-button" data-bs-dismiss="modal" |
||||||
|
@click="showHistoryForTracker(tracker)" title="History"> |
||||||
|
<i class="fas fa-history"></i> |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
<div class="col"> |
||||||
|
<button class="btn btn-danger tracker-action-button" |
||||||
|
@click="deleteTracker(trackerIndex)" title="Löschen"> |
||||||
|
<i class="fas fa-trash"></i> |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
<div class="col" v-if="trackerSystemUrl"> |
||||||
|
<a v-if="isTrackerNumber(tracker.number)" :href="trackerSystemUrl + tracker.number.replace('#', '')" |
||||||
|
target="_blank" class="btn btn-dark tracker-action-button" title="Tracker"> |
||||||
|
<i class="fas fa-external-link-square-alt"></i> |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<br/> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</template> |
||||||
|
|
||||||
|
<script> |
||||||
|
export default { |
||||||
|
name: "TrackersDetail" |
||||||
|
} |
||||||
|
</script> |
||||||
|
|
||||||
|
<style scoped> |
||||||
|
|
||||||
|
</style> |
Loading…
Reference in new issue