Browse Source

Beginning of time

poc/redesign-trackers
Nero Ignis 4 years ago
commit
741716877b
  1. 23
      .gitignore
  2. 24
      README.md
  3. 5
      babel.config.js
  4. 27768
      package-lock.json
  5. 52
      package.json
  6. 174
      public/app.css
  7. BIN
      public/favicon.ico
  8. 20
      public/index.html
  9. 436
      src/App.vue
  10. BIN
      src/assets/logo.png
  11. 18
      src/helpers.js
  12. 6
      src/main.js
  13. 27
      src/router/index.js
  14. 96
      src/store/index.js
  15. 162
      src/views/Archive.vue
  16. 149
      src/views/Boardview.vue
  17. 37
      src/views/CustomBookForTracker.vue
  18. 41
      src/views/History.vue
  19. 74
      src/views/HistoryForTracker.vue
  20. 112
      src/views/Menu.vue
  21. 157
      src/views/Settings.vue
  22. 59
      src/views/Switcher.vue
  23. 75
      src/views/TasksForTracker.vue
  24. 374
      src/views/Trackers.vue
  25. 74
      src/views/TrackersDetail.vue
  26. 1
      timetrack_legacy

23
.gitignore vendored

@ -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?

24
README.md

@ -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/).

5
babel.config.js

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}

27768
package-lock.json generated

File diff suppressed because it is too large Load Diff

52
package.json

@ -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"
]
}

174
public/app.css

@ -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%;
}

BIN
public/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

20
public/index.html

@ -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>

436
src/App.vue

@ -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>

BIN
src/assets/logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

18
src/helpers.js

@ -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;
}

6
src/main.js

@ -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')

27
src/router/index.js

@ -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

96
src/store/index.js

@ -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;
}
}
})

162
src/views/Archive.vue

@ -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>

149
src/views/Boardview.vue

@ -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>

37
src/views/CustomBookForTracker.vue

@ -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>

41
src/views/History.vue

@ -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>

74
src/views/HistoryForTracker.vue

@ -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>

112
src/views/Menu.vue

@ -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>

157
src/views/Settings.vue

@ -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>

59
src/views/Switcher.vue

@ -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>

75
src/views/TasksForTracker.vue

@ -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>

374
src/views/Trackers.vue

@ -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>

74
src/views/TrackersDetail.vue

@ -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>

1
timetrack_legacy

@ -0,0 +1 @@ @@ -0,0 +1 @@
Subproject commit 77cf11aed719b970208e6b43e5a8940a3d35c47d
Loading…
Cancel
Save