commit
741716877b
26 changed files with 29964 additions and 0 deletions
@ -0,0 +1,23 @@
@@ -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 @@
@@ -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 @@
@@ -0,0 +1,5 @@
|
||||
module.exports = { |
||||
presets: [ |
||||
'@vue/cli-plugin-babel/preset' |
||||
] |
||||
} |
@ -0,0 +1,52 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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