Browse Source

first commit

modals-to-spa
Nero Ignis 4 years ago
commit
138f82f4bb
  1. 130
      css/app.css
  2. BIN
      img/favicon.ico
  3. BIN
      img/google.webp
  4. BIN
      img/logo.png
  5. BIN
      img/redmine.png
  6. BIN
      img/search.png
  7. BIN
      img/stackoverflow.png
  8. 372
      index.html
  9. 284
      js/app.js
  10. 11965
      js/vue.js

130
css/app.css

@ -0,0 +1,130 @@ @@ -0,0 +1,130 @@
body {
background-color: #e7e7e7;
overflow-y: auto;
}
.container {
margin: 20px auto 20px auto;
}
.add-button {
padding: 1px 5px 1px 5px;
}
.ticket-action-button {
margin-top: 10px;
padding: 1px 5px 1px 5px;
width: 100%;
}
.delete-ticket-button {
margin-top: 10px;
margin-bottom: 15px;
padding: 1px 5px 1px 5px;
width: 100%;
}
.side-button-right {
position: absolute;
right: 30px;
height: 60px;
width: 60px;
font-size: 1.7em;
}
.add-tracker-button {
position: absolute;
left: 30px;
bottom: 30px;
height: 60px;
width: 60px;
font-size: 1.7em;
}
.first-button {
bottom: 30px;
}
.second-button {
bottom: 110px;
padding-left: 16px;
}
.third-button {
bottom: 190px;
padding-left: 16px;
}
.fourth-button {
bottom: 270px;
padding-left: 16px;
}
.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;
}
#showTrackedTicketsModal .showTrackedTicketsModalDialog,
#showArchivedTicketsModal .showArchivedTicketsModalDialog{
max-width: 900px;
}
.btn {
text-transform: initial !important;
}
.ticket-time-info {
margin-top: 1em;
clear: both;
}
.brand-title {
margin-left: 2em;
margin-top: 0.5em;
}
#settingsModal .form-group {
margin-bottom: 0.5em;
}

BIN
img/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
img/google.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

BIN
img/logo.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
img/redmine.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
img/search.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

BIN
img/stackoverflow.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

372
index.html

@ -0,0 +1,372 @@ @@ -0,0 +1,372 @@
<!doctype html>
<html lang="en" xmlns:v-on="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>vDash</title>
<link rel="shortcut icon" type="image/x-icon" href="img/favicon.ico">
</head>
<body oncontextmenu="return false;">
<div class="container" id="root">
<link rel="stylesheet" :href="'https://bootswatch.com/5/' + theme + '/bootstrap.min.css'">
<link rel="stylesheet" href="css/app.css">
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand">
<img :src="dashboardLogo" alt="logo" class="logo-nav float-end" v-if="dashboardLogo"/>
<h5 class="brand-title">Timetrack</h5>
</a>
</nav>
<div class="row">
<div class="col-md-12">
<div class="row">
<div class=" col-lg-4 col-md-6"
v-for="(ticket, ticketIndex) in trackedTickets"
v-if="!ticket.archived">
<div class="card bg-gradient-secondary">
<div class="card-body">
<div class="card-text">
<input type="text"
v-model="ticket.number"
class="form-control trackingNameField"
@keydown="updateStorage()"/>
<div class="ticket-time-info">
<div v-if="ticket.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="ticket.tracking === true" class="ticket-time-info">
<span class="float-end">{{ getTrackingStartTime(ticket) }}</span>
<span v-if="ticket.tracking === true">Gestartet: </span>
<br/>
<span class="float-end">{{ currentTrackingRunningFor(ticket) }}</span>
<span v-if="ticket.tracking === true">Läuft seit: </span>
</div>
<div class="ticket-time-info">
<span class="float-end">{{ getTotalTime(ticket) }}</span>
<span class="current-ticket-info">Gesamt: </span>
</div>
<span class="float-end">{{ getTotalTimeToday(ticket) }}</span>
<span class="">Heute: </span>
<div class="row">
<div class="col-md-12" v-if="ticket.tracking == false">
<button type="button" class="btn btn-info ticket-action-button"
@click="startTracking(ticket)">
<i class="far fa-play-circle"></i>
</button>
</div>
<div class="col-md-12" v-else>
<button type="button" class="btn btn-danger ticket-action-button"
@click="stopTracking(ticket)">
<i class="far fa-stop-circle"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<a type="button" class="btn btn-success add-tracker-button text-light" @click="addTrackedTicket()">
<i class="fas fa-plus"></i>
</a>
<a type="button" :class="'btn btn-primary side-button-right text-light ' + (archivedTrackers > 0 ? 'third-button' : 'second-button')" data-toggle="modal"
data-target="#showTrackedTicketsModal" v-if="activeTrackers > 0">
<i class="fas fa-user-clock"></i>
</a>
<a type="button" :class="'btn btn-secondary side-button-right text-dark second-button'" data-toggle="modal"
data-target="#showArchivedTicketsModal" v-if="archivedTrackers > 0">
<i class="fas fa-archive"></i>
</a>
<a type="button" class="btn btn-dark text-light side-button-right first-button" data-toggle="modal"
data-target="#settingsModal">
<i class="fas fa-sliders-h"></i>
</a>
<div class="modal modal-fullscreen fade" id="showTrackedTicketsModal" tabindex="-1" role="dialog"
aria-labelledby="showTrackedTicketsModalLabel"
aria-hidden="true">
<div class="modal-dialog showTrackedTicketsModalDialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-user-clock"></i> Tracker</h5>
<button type="button" class="btn-close" data-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-6" v-for="(ticket, ticketIndex) in trackedTickets" v-if="!ticket.archived">
<h6><span v-if="isTicketNumber(ticket.number)"></span>{{ ticket.number }}</h6>
<div class="form-group">
<input type="text" class="form-control" v-model="ticket.description" @keydown="updateStorage()" placeholder="Beschreibung">
</div>
<span>Gesamtzeit: {{ getTotalTime(ticket) }}</span>
<br>
<div class="row">
<div class="col-md-3">
<button class="btn btn-success ticket-action-button"
@click="bookTimeManually(ticket, 30)">
+ ½h
</button>
</div>
<div class="col-md-3">
<button class="btn btn-success ticket-action-button"
@click="bookTimeManually(ticket, 60)">
+ 1h
</button>
</div>
<div class="col-md-3">
<button class="btn btn-warning ticket-action-button"
@click="bookTimeManually(ticket, -30)">
- ½h
</button>
</div>
<div class="col-md-3">
<button class="btn btn-warning ticket-action-button"
@click="bookTimeManually(ticket, -60)">
- 1h
</button>
</div>
<div class="col">
<button class="btn btn-secondary ticket-action-button" data-bs-dismiss="modal"
@click="archiveTicket(ticket)" title="Archivieren">
<i class="fas fa-archive"></i>
</button>
</div>
<div class="col" v-if="ticket.history.length > 0">
<button class="btn btn-info ticket-action-button" data-bs-dismiss="modal"
@click="showHistoryForTicket(ticket)" title="History">
<i class="fas fa-history"></i>
</button>
</div>
<div class="col">
<button class="btn btn-danger ticket-action-button"
@click="deleteTicket(ticket)" title="Löschen">
<i class="fas fa-trash"></i>
</button>
</div>
<div class="col" v-if="ticketSystemUrl">
<a v-if="isTicketNumber(ticket.number)" :href="ticketSystemUrl + ticket.number"
target="_blank" class="btn btn-dark ticket-action-button" title="Ticket">
<i class="fas fa-external-link-square-alt"></i>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal modal-fullscreen fade" id="showArchivedTicketsModal" tabindex="-1" role="dialog"
aria-labelledby="showArchivedTicketsModalLabel"
aria-hidden="true">
<div class="modal-dialog showArchivedTicketsModalDialog" 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-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-6" v-for="(ticket, ticketIndex) in trackedTickets" v-if="ticket.archived">
<h6><span v-if="isTicketNumber(ticket.number)"></span>{{ ticket.number }}</h6>
<div v-if="ticket.description">
<p class="blockquote">{{ ticket.description }}</p>
</div>
<span>Gesamtzeit: {{ getTotalTime(ticket) }}</span>
<br>
<div class="col-md-12 row">
<div class="col">
<button class="btn btn-success ticket-action-button" data-bs-dismiss="modal"
@click="reactivateTicket(ticket)" title="Reaktivieren">
<i class="fas fa-power-off"></i>
</button>
</div>
<div class="col" v-if="ticket.history.length > 0">
<button class="btn btn-info ticket-action-button" data-bs-dismiss="modal"
@click="showHistoryForTicket(ticket)" title="History">
<i class="fas fa-history"></i>
</button>
</div>
<div class="col">
<button class="btn btn-danger ticket-action-button"
@click="deleteTicket(ticket)" title="Löschen">
<i class="fas fa-trash"></i>
</button>
</div>
<div class="col" v-if="ticketSystemUrl">
<a v-if="isTicketNumber(ticket.number)" :href="ticketSystemUrl + ticket.number"
target="_blank" class="btn btn-dark ticket-action-button" title="Ticket">
<i class="fas fa-external-link-square-alt"></i>
</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal fade" id="settingsModal" tabindex="-1" role="dialog" aria-labelledby="settingsModalLabel"
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-sliders-h"></i> Einstellungen</h5>
<button type="button" class="btn-close" data-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<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>Ticket-Link <small>Link zu einem Ticket ohne Ticketnummer</small></label>
<input type="text" v-model="ticketSystemUrl" 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>Design</label>
<select v-model="theme" class="form-control">
<option v-for="availableTheme in themes" :value="availableTheme.name.toLowerCase()">{{ availableTheme.name }}</option>
</select>
</div>
<br/>
<h5>Zurücksetzen & Löschen</h5>
<div class="row">
<div class="col-md-6">
<button class="btn btn-outline-warning btn-full-width" @click="resetToDefault()">
Zurücksetzen
</button>
</div>
<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">{{ 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="inputs.importJson"></textarea>
<button class="btn btn-success btn-full-width" @click="importData">Import</button>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-primary" v-on:click="updateStorage()" data-dismiss="modal">Speichern</button>
<button type="button" class="btn btn-secondary" data-dismiss="modal">Schließen</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="historyModal" tabindex="-1" role="dialog" aria-labelledby="historyModalLabel"
aria-hidden="true" v-if="selectedTicket">
<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 {{ selectedTicket.number }}</h5>
<button type="button" class="btn-close" data-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<ul class="list-group ticket-history">
<li class="list-group-item" v-for="(tracker, historyIndex) in selectedTicket.history" 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>
<li class="list-group-item bg-light" v-for="(tracker, historyIndex) in selectedTicket.history"
v-if="tracker.manually && tracker.manually == true">
<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(ticketIndex, historyIndex)" class="float-end">
<i class="fas fa-trash"></i>
</a>
<br/>
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<script src="https://kit.fontawesome.com/b54a4cceff.js" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment-with-locales.min.js"></script>
<script src="js/vue.js"></script>
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.21.1/axios.min.js"></script>
<script src="js/app.js"></script>
</body>
</html>

284
js/app.js

@ -0,0 +1,284 @@ @@ -0,0 +1,284 @@
let tab = new Vue({
el: '#root',
data: {
theme: 'quartz',
themes: null,
dashboardLogo: 'img/logo.png',
ticketSystemUrl: '',
showPT: true,
linkTarget: '_blank',
inputs: {
importJson: ''
},
trackedTickets: [],
selectedTicket: null
},
mounted() {
let vue = this;
this.loadStorage();
this.fetchThemes();
moment.locale('de');
setInterval(() => {
vue.$forceUpdate();
}, 1000 * 60)
},
methods: {
loadStorage() {
let storedTrackedTickets = JSON.parse(localStorage.getItem('trackedTickets'));
this.trackedTickets = storedTrackedTickets == null ? [] : storedTrackedTickets;
let storedticketSystemUrl = localStorage.getItem('ticketSystemUrl');
this.ticketSystemUrl = storedticketSystemUrl == null ? '' : storedticketSystemUrl;
let storedShowPT = localStorage.getItem('showPT');
this.showPT = storedShowPT == null ? true : storedShowPT;
let storedTheme = localStorage.getItem('theme');
this.theme = storedTheme == null ? 'materia' : storedTheme;
},
updateStorage() {
localStorage.setItem('trackedTickets', JSON.stringify(this.trackedTickets));
localStorage.setItem('ticketSystemUrl', this.ticketSystemUrl);
localStorage.setItem('showPT', this.showPT);
localStorage.setItem('theme', this.theme);
},
resetToDefault() {
this.updateStorage();
},
addTrackedTicket() {
let newTicket = {
archived: false,
tracking: false,
number: '#',
trackingStarted: null,
trackingStopped: null,
history: []
};
this.trackedTickets.push(newTicket);
this.updateStorage();
},
startTracking(ticket) {
this.stopTrackingTicket();
ticket.trackingStarted = moment();
ticket.tracking = true;
this.$forceUpdate();
this.updateStorage();
},
stopTracking(ticket) {
ticket.trackingStopped = moment();
ticket.tracking = false;
let minutesSpent = moment.duration(
ticket.trackingStopped.diff(ticket.trackingStarted)
).as('minutes');
ticket.history.push({
trackingStarted: ticket.trackingStarted,
trackingStopped: ticket.trackingStopped,
manually: false,
minutes: Math.round(minutesSpent)
});
ticket.trackingStarted = null;
ticket.trackingStopped = null;
this.$forceUpdate();
this.updateStorage();
},
formattedDate(date) {
return moment(date).format('llll');
},
exactTimestamp(date) {
return moment(date).format('LTS');
},
currentTrackingRunningFor(ticket) {
return this.timeWithPostFix(Math.round(moment.duration(moment().diff(ticket.trackingStarted)).as('minutes')));
},
getTotalTime(ticket) {
let totalTime = 0;
if (ticket.history.length > 0) {
ticket.history.forEach(function (historyEntry) {
totalTime += Math.round(historyEntry.minutes);
});
}
if (ticket.tracking) {
totalTime += Math.round(moment.duration(moment().diff(ticket.trackingStarted)).as('minutes'));
}
return this.timeWithPostFix(totalTime);
},
getTotalTimeToday(ticket) {
let totalTime = 0;
if (ticket.history.length > 0) {
ticket.history.forEach(function (historyEntry) {
if (moment(historyEntry.trackingStarted).format("MMM Do YY") === moment().format("MMM Do YY")) {
totalTime += Math.round(historyEntry.minutes);
}
});
}
if (ticket.tracking) {
totalTime += Math.round(moment.duration(moment().diff(ticket.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) {
postFix = ' Stunde';
time = (time / 60).toFixed(2);
}
let plural = '';
if ((time > 1 || time <= 0) && postFix !== ' PT') {
plural = 'n'
}
return time + postFix + plural;
},
stopTrackingTicket() {
let vue = this;
vue.trackedTickets.forEach(function (ticket) {
if (ticket.tracking === true) {
vue.stopTracking(ticket);
}
})
},
getTrackingStartTime(ticket) {
return moment(ticket.trackingStarted).format('LT');
},
isTicketNumber(number) {
return number.indexOf('#') >= 0;
},
deleteTicket(index) {
this.trackedTickets.splice(index, 1);
this.updateStorage();
},
archiveTicket(ticket) {
ticket.archived = true;
if (ticket.tracking) {
this.stopTrackingTicket();
}
this.updateStorage();
this.$forceUpdate();
},
reactivateTicket(ticket) {
ticket.archived = false;
this.updateStorage();
},
deleteHistoryEntry(ticketIndex, historyIndex) {
if (ticketIndex) {
this.trackedTickets[ticketIndex].history.splice(historyIndex, 1);
} else {
this.selectedTicket.history.splice(historyIndex, 1);
}
this.updateStorage();
},
bookTimeManually(ticket, minutes) {
ticket.history.push({
trackingStarted: moment(),
trackingStopped: moment(),
manually: true,
minutes: Math.round(minutes)
});
this.updateStorage();
},
importData() {
let json = JSON.parse(this.inputs.importJson);
this.trackedTickets = json.trackedTickets;
this.ticketSystemUrl = json.ticketSystemUrl;
this.showPT = json.showPT;
this.theme = json.theme;
this.updateStorage();
location.reload();
},
copy2Clipboard() {
let copyText = document.getElementById("exportJsonInput");
copyText.select();
copyText.setSelectionRange(0, 99999);
document.execCommand("copy");
alert('Text kopiert!');
},
fetchThemes() {
let vue = this;
axios.get(
'https://bootswatch.com/api/5.json'
).then((response) => {
vue.themes = response.data.themes;
}).catch((error) => {
console.log(error);
});
},
showHistoryForTicket(ticket) {
this.selectedTicket = ticket;
this.$forceUpdate();
setTimeout(() => {
let historyModal = new bootstrap.Modal(document.getElementById('historyModal'));
historyModal.toggle();
}, 50)
}
},
watch: {
showPT() {
this.updateStorage();
this.$forceUpdate();
},
theme() {
this.updateStorage();
this.$forceUpdate();
}
},
computed: {
exportJson() {
return JSON.stringify({
trackedTickets: this.trackedTickets,
ticketSystemUrl: this.ticketSystemUrl,
showPT: this.showPT,
theme: this.theme
});
},
archivedTrackers() {
let vue = this;
let count = 0;
this.trackedTickets.forEach((ticket) => {
count += (ticket.archived ? 1 : 0);
})
return count;
},
activeTrackers() {
let vue = this;
let count = 0;
this.trackedTickets.forEach((ticket) => {
count += (ticket.archived ? 0 : 1);
})
return count;
}
},
created() {
}
});

11965
js/vue.js

File diff suppressed because it is too large Load Diff