Compare commits

..

28 Commits

  1. 3
      .idea/timetrack.iml
  2. BIN
      assets/img/gitlab.png
  3. BIN
      assets/img/jira.png
  4. BIN
      assets/img/redmine.png
  5. 17
      css/app.css
  6. 484
      index.html
  7. 241
      js/app.js

3
.idea/timetrack.iml

@ -5,5 +5,8 @@
<orderEntry type="inheritedJdk" /> <orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="izitoast" level="application" /> <orderEntry type="library" name="izitoast" level="application" />
<orderEntry type="library" name="quill" level="application" />
<orderEntry type="library" name="quill.snow" level="application" />
<orderEntry type="library" name="quill.bubble" level="application" />
</component> </component>
</module> </module>

BIN
assets/img/gitlab.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

BIN
assets/img/jira.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
assets/img/redmine.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

17
css/app.css

@ -1,4 +1,5 @@
body { body {
background-color: #e7e7e7;
overflow-y: auto; overflow-y: auto;
} }
@ -221,3 +222,19 @@ nav, .card {
bottom: 0; bottom: 0;
left: 0; left: 0;
} }
.finished-task {
text-decoration: line-through;
}
#trackerTasksModal a li {
color: black !important;
}
#trackerTasksModal input[type=range] {
width: 100%;
}
.ticket-icon {
max-width: 24px;
}

484
index.html

@ -15,12 +15,28 @@
<body> <body>
<div id="root"> <div id="root">
<!-- Tracker-Display -->
<div class="container" id="content">
<link rel="stylesheet" :href="'https://bootswatch.com/5/' + theme + '/bootstrap.min.css'"> <link rel="stylesheet" :href="'https://bootswatch.com/5/' + theme + '/bootstrap.min.css'">
<link rel="stylesheet" href="css/app.css"> <link rel="stylesheet" href="css/app.css">
<!-- <nav class="navbar navbar-expand-lg navbar-light bg-light">-->
<!-- Content --> <!-- <a class="navbar-brand">-->
<div class="container" id="content"> <!-- <img :src="dashboardLogo" alt="logo" class="logo-nav float-end" v-if="dashboardLogo"/>-->
<div id="trackers" class="row" v-if="view === 'trackers'"> <!-- <div v-if="experimental.trackWorktime">-->
<!-- <template v-if="worktimeTracker.tracking">-->
<!-- <i class="far fa-pause-circle"></i>-->
<!-- <i class="far fa-stop-circle" @click="stopTracking(worktimeTracker)"></i>-->
<!-- </template>-->
<!-- <template v-else>-->
<!-- <a href="javascript:" @click="startTracking(worktimeTracker)">-->
<!-- <i class="far fa-play-circle"></i>-->
<!-- </a>-->
<!-- </template>-->
<!-- </div>-->
<!-- <h5 class="brand-title">Timetrack</h5>-->
<!-- </a>-->
<!-- </nav>-->
<div class="row" v-if="!experimental.boardView || view === 'trackers'">
<div class="col-md-12"> <div class="col-md-12">
<div class="row"> <div class="row">
<template v-for="(ticket, ticketIndex) in tickets"> <template v-for="(ticket, ticketIndex) in tickets">
@ -62,30 +78,38 @@
<span class="">Heute: </span> <span class="">Heute: </span>
<div class="row"> <div class="row">
<div class="col-md-12" v-if="!ticket.tracking"> <div class="col-md-6" v-if="!ticket.tracking">
<button type="button" class="btn btn-info ticket-action-button" <button type="button" class="btn btn-primary ticket-action-button"
@click="startTracking(ticket)"> @click="startTracking(ticket)">
<i class="far fa-play-circle"></i> <i class="far fa-play-circle"></i> <small>Starten</small>
</button> </button>
</div> </div>
<div class="col-md-12" v-else> <div class="col-md-6" v-else>
<button type="button" class="btn btn-danger ticket-action-button" <button type="button" class="btn btn-danger ticket-action-button"
@click="stopTracking(ticket)"> @click="stopTracking(ticket)">
<i class="far fa-stop-circle"></i> <i class="far fa-stop-circle"></i> <small>Stoppen</small>
</button> </button>
</div> </div>
<div class="col-6"> <div class="col-6">
<button class="btn btn-secondary ticket-action-button" data-bs-dismiss="modal" <button class="btn btn-warning ticket-action-button" data-bs-dismiss="modal"
@click="archiveTracker(ticketIndex)" title="Archivieren"> @click="archiveTracker(ticketIndex)" title="Archivieren">
<i class="fas fa-archive"></i> <i class="fas fa-archive"></i> <small>Archivieren</small>
</button> </button>
</div> </div>
<div class="col-md-6">
<button type="button" class="btn btn-info ticket-action-button"
@click="openTasksForTracker(ticket)" title="Tasks">
<i class="fas fa-clipboard-check"></i> <small>Tasks {{ showOpenTasksForTracker(ticket.tasks) }}</small>
</button>
</div>
<div class="col-6"> <div class="col-6">
<button class="btn btn-danger ticket-action-button" <button class="btn btn-danger ticket-action-button"
@click="deleteTracker(ticketIndex)" title="Löschen"> @click="deleteTracker(ticketIndex)" title="Löschen">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i> <small>Löschen</small>
</button> </button>
</div> </div>
</div> </div>
@ -114,12 +138,12 @@
<div class="col-12"> <div class="col-12">
Minuten: Minuten:
</div> </div>
<!-- <div class="col">--> <!-- <div class="col">-->
<!-- <button type="button" class="btn btn-secondary ticket-action-button"--> <!-- <button type="button" class="btn btn-secondary ticket-action-button"-->
<!-- @click="startTimeBox(ticket, 1)">--> <!-- @click="startTimeBox(ticket, 1)">-->
<!-- <i class="far fa-play-circle"></i> 1--> <!-- <i class="far fa-play-circle"></i> 1-->
<!-- </button>--> <!-- </button>-->
<!-- </div>--> <!-- </div>-->
<div class="col"> <div class="col">
<button type="button" class="btn btn-secondary ticket-action-button" <button type="button" class="btn btn-secondary ticket-action-button"
@click="startTimeBox(ticket, 15)"> @click="startTimeBox(ticket, 15)">
@ -153,20 +177,20 @@
<div class="col-md-12" v-if="ticket.tracking"> <div class="col-md-12" v-if="ticket.tracking">
<button type="button" class="btn btn-danger ticket-action-button" <button type="button" class="btn btn-danger ticket-action-button"
@click="stopTracking(ticket)"> @click="stopTracking(ticket)">
<i class="far fa-stop-circle"></i> <i class="far fa-stop-circle"></i> <small>Stoppen</small>
</button> </button>
</div> </div>
<div class="col-md-12" v-if="!ticket.tracking && ticket.timeBoxMinutes && timeBoxTimeLeft(ticket) > 0"> <div class="col-md-12" v-if="!ticket.tracking && ticket.timeBoxMinutes && timeBoxTimeLeft(ticket) > 0">
<button type="button" class="btn btn-info ticket-action-button" <button type="button" class="btn btn-primary ticket-action-button"
@click="startTracking(ticket)"> @click="startTracking(ticket)">
<i class="far fa-play-circle"></i> <i class="far fa-play-circle"></i> <small>Starten</small>
</button> </button>
</div> </div>
<div class="col-md-12"> <div class="col-md-12">
<button class="btn btn-danger ticket-action-button" <button class="btn btn-danger ticket-action-button"
@click="deleteTracker(ticketIndex)" title="Löschen"> @click="deleteTracker(ticketIndex)" title="Löschen">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i> <small>Löschen</small>
</button> </button>
</div> </div>
</div> </div>
@ -179,9 +203,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="row" v-if="experimental.boardView && view === 'board'" id="board-view">
<!-- Board View-->
<div id="board" class="col row" v-if="view === 'board'">
<div class="col-4 col-board-inner"> <div class="col-4 col-board-inner">
<h5>Todo</h5> <h5>Todo</h5>
<template v-for="(ticket, ticketIndex) in tickets"> <template v-for="(ticket, ticketIndex) in tickets">
@ -241,7 +263,7 @@
</div> </div>
<div class="col-md-12"> <div class="col-md-12">
<button class="btn btn-secondary ticket-action-button" data-bs-dismiss="modal" <button class="btn btn-warning ticket-action-button" data-bs-dismiss="modal"
@click="archiveTracker(ticketIndex)" title="Archivieren"> @click="archiveTracker(ticketIndex)" title="Archivieren">
<i class="fas fa-archive"></i> <i class="fas fa-archive"></i>
</button> </button>
@ -306,7 +328,7 @@
</div> </div>
<div class="col-md-12"> <div class="col-md-12">
<button class="btn btn-secondary ticket-action-button" data-bs-dismiss="modal" <button class="btn btn-warning ticket-action-button" data-bs-dismiss="modal"
@click="archiveTracker(ticketIndex)" title="Archivieren"> @click="archiveTracker(ticketIndex)" title="Archivieren">
<i class="fas fa-archive"></i> <i class="fas fa-archive"></i>
</button> </button>
@ -318,12 +340,26 @@
</template> </template>
</div> </div>
</div> </div>
<!-- Trackers-Detail --> </div>
<!-- TODO: Merge to main Trackers-page -->
<div id="trackersDetail" class="col-md-auto row" v-if="view === 'trackersDetail'"> <!-- Tickets Modal -->
<div class="modal modal-fullscreen fade" id="showTicketsModal" tabindex="-1" role="dialog"
aria-labelledby="showTicketsModalLabel"
aria-hidden="true">
<div class="modal-dialog showTicketsModalDialog" 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="(ticket, ticketIndex) in tickets"> <template v-for="(ticket, ticketIndex) in tickets">
<div class="col-md-6"> <div class="col-md-6">
<h6><span v-if="isTicketNumber(ticket.number)"></span>{{ ticket.number }}</h6> <!-- <h6><span v-if="isTicketNumber(ticket.number)"></span>{{ ticket.number }}</h6>-->
<div class="form-group">
<input type="text" class="form-control" v-model="ticket.number" @keydown="updateStorage()" placeholder="Name">
</div>
<div class="form-group"> <div class="form-group">
<input type="text" class="form-control" v-model="ticket.description" @keydown="updateStorage()" placeholder="Beschreibung"> <input type="text" class="form-control" v-model="ticket.description" @keydown="updateStorage()" placeholder="Beschreibung">
@ -340,17 +376,29 @@
</button> </button>
</div> </div>
<div class="col"> <div class="col">
<button class="btn btn-secondary ticket-action-button" data-bs-dismiss="modal" <button class="btn btn-warning ticket-action-button" data-bs-dismiss="modal"
@click="archiveTracker(ticketIndex)" title="Archivieren"> @click="archiveTracker(ticketIndex)" title="Archivieren">
<i class="fas fa-archive"></i> <i class="fas fa-archive"></i>
</button> </button>
</div> </div>
<div class="col" v-if="ticket.tasks && ticket.tasks.length > 0">
<button class="btn btn-info ticket-action-button" data-bs-dismiss="modal"
@click="openTasksForTracker(ticket)" title="Tasks">
<i class="fas fa-clipboard-check"></i>
</button>
</div>
<div class="col" v-if="ticket.history.length > 0"> <div class="col" v-if="ticket.history.length > 0">
<button class="btn btn-info ticket-action-button" data-bs-dismiss="modal" <button class="btn btn-info ticket-action-button" data-bs-dismiss="modal"
@click="showHistoryForTracker(ticket)" title="History"> @click="showHistoryForTracker(ticket)" title="History">
<i class="fas fa-history"></i> <i class="fas fa-history"></i>
</button> </button>
</div> </div>
<div class="col" v-if="ticket.history.length > 0 && this.ticketSystemUrl">
<button class="btn btn-white ticket-action-button"
@click="sendLastBookingToTicketSystem(ticket)" title="Letzte Buchung in Ticketsystem eintragen">
<img :src="ticketSystemIcon" class="ticket-icon"/>
</button>
</div>
<div class="col"> <div class="col">
<button class="btn btn-danger ticket-action-button" <button class="btn btn-danger ticket-action-button"
@click="deleteTracker(ticketIndex)" title="Löschen"> @click="deleteTracker(ticketIndex)" title="Löschen">
@ -368,19 +416,103 @@
</div> </div>
</template> </template>
</div> </div>
<!-- Archive --> </div>
<div id="archive" class="col-md-auto row" v-if="view === 'archive'"> </div>
</div>
</div>
<!-- Tasks Modal -->
<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>
<!-- Archive Modal -->
<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-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"> <div class="col-sm-6">
<i class="fas fa-search"></i> <i class="fas fa-search"></i>
<input @keydown="$forceUpdate()" type="text" class="form-control search-field float-right" v-model="searchQuery" placeholder="Suche.."> <input @keydown="$forceUpdate()" type="text" class="form-control search-field float-right" v-model="searchQuery" placeholder="Suche..">
</div> </div>
<div class="col-sm-6"></div>
<template v-for="(ticket, ticketIndex) in archive"> <template v-for="(ticket, ticketIndex) in archive">
<div class="col-md-6" v-if="searchQuery === '' || ticket.number.search(searchQuery) >= 0 || (ticket.description && ticket.description.search(searchQuery)) >= 0"> <div class="col-md-6" v-if="searchQuery === '' || ticket.number.search(searchQuery) >= 0 || (ticket.description && ticket.description.search(searchQuery)) >= 0">
<h6><span v-if="isTicketNumber(ticket.number)"></span>{{ ticket.number }}</h6> <div class="form-group">
<div v-if="ticket.description"> <input type="text" class="form-control" v-model="ticket.number" @keydown="updateStorage()" placeholder="Name">
<p class="fst-italic">{{ ticket.description }}</p>
</div> </div>
<div class="form-group">
<input type="text" class="form-control" v-model="ticket.description" @keydown="updateStorage()" placeholder="Beschreibung">
</div>
<div class="ticket-time-info"> <div class="ticket-time-info">
<span class="float-end">{{ getTotalTime(ticket) }}</span> <span class="float-end">{{ getTotalTime(ticket) }}</span>
<span class="current-ticket-info">Gesamt: </span> <span class="current-ticket-info">Gesamt: </span>
@ -397,12 +529,24 @@
<i class="fas fa-power-off"></i> <i class="fas fa-power-off"></i>
</button> </button>
</div> </div>
<div class="col" v-if="ticket.tasks && ticket.tasks.length > 0">
<button class="btn btn-info ticket-action-button" data-bs-dismiss="modal"
@click="openTasksForTracker(ticket)" title="Tasks">
<i class="fas fa-clipboard-check"></i>
</button>
</div>
<div class="col" v-if="ticket.history.length > 0"> <div class="col" v-if="ticket.history.length > 0">
<button class="btn btn-info ticket-action-button" data-bs-dismiss="modal" <button class="btn btn-info ticket-action-button" data-bs-dismiss="modal"
@click="showHistoryForTracker(ticket)" title="History"> @click="showHistoryForTracker(ticket)" title="History">
<i class="fas fa-history"></i> <i class="fas fa-history"></i>
</button> </button>
</div> </div>
<div class="col" v-if="ticket.history.length > 0 && this.ticketSystemUrl">
<button class="btn btn-white ticket-action-button"
@click="sendLastBookingToTicketSystem(ticket)" title="Letzte Buchung in Ticketsystem eintragen">
<img :src="ticketSystemIcon" class="ticket-icon"/>
</button>
</div>
<div class="col"> <div class="col">
<button class="btn btn-danger ticket-action-button" <button class="btn btn-danger ticket-action-button"
@click="deleteTracker(ticketIndex, true)" title="Löschen"> @click="deleteTracker(ticketIndex, true)" title="Löschen">
@ -420,8 +564,21 @@
</div> </div>
</template> </template>
</div> </div>
<!-- Settings --> </div>
<div id="settings" class="col-md-auto row" v-if="view === 'settings'"> </div>
</div>
</div>
<!-- Settings Modal -->
<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-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body row">
<div class="col-md-6"> <div class="col-md-6">
<h5>Allgemeine Einstellungen</h5> <h5>Allgemeine Einstellungen</h5>
<div class="form-group"> <div class="form-group">
@ -498,8 +655,88 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Custom Booking--> <div class="modal-footer">
<div id="customBooking" class="col-md-auto row" v-if="view === 'customBooking' && selectedTracker"> <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>
</div>
</div>
</div>
<!-- History Modal -->
<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 ticket-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(ticketIndex, historyIndex)" class="float-end">
<i class="fas fa-trash"></i>
</a>
<br/>
</li>
</template>
</ul>
</div>
</div>
</div>
</div>
<!-- Custom Booking Modal -->
<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"> <div class="form-group">
<label>Zeit in Minuten:</label> <label>Zeit in Minuten:</label>
<input type="text" class="form-control" v-model="customBookingValue"> <input type="text" class="form-control" v-model="customBookingValue">
@ -513,8 +750,21 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Switcher --> </div>
<div id="switcher" class="col-md-auto row" v-if="view === 'switcher'"> </div>
</div>
<!-- Switcher Modal -->
<div class="modal fade" id="switcherModal" tabindex="-1" role="dialog" aria-labelledby="switcherModalLabel"
aria-hidden="true" v-if="experimental.portalSwitcher">
<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="col-12">
<div class="form-group"> <div class="form-group">
<label class="text-muted">Portalname:</label> <label class="text-muted">Portalname:</label>
@ -548,8 +798,22 @@
</div> </div>
</template> </template>
</div> </div>
<!-- Past Days--> </div>
<div id="pastDays" class="col-md-auto row" v-if="view === 'pastDays'"> </div>
</div>
</div>
<!-- Past Days Modal -->
<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="col-12">
<div class="form-group"> <div class="form-group">
<div class="form-group"> <div class="form-group">
@ -565,8 +829,22 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Snippet Space--> </div>
<div id="snippetSpace" class="col-md-auto row" v-if="view === 'snippetSpace'"> </div>
</div>
</div>
<!-- SnippetSpace Modal -->
<div class="modal fade" id="snippetSpaceModal" tabindex="-1" role="dialog" aria-labelledby="snippetSpaceModalLabel"
aria-hidden="true" v-if="experimental.snippetSpace">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title"><i class="fas fa-code"></i> Snippet Space</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="col-12">
<a href="javascript:" class="btn btn-sm btn-success btn-wide float-end" @click="createSnippet()"> <a href="javascript:" class="btn btn-sm btn-success btn-wide float-end" @click="createSnippet()">
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
@ -584,59 +862,6 @@
</div> </div>
</div> </div>
</div> </div>
<div class="col-6 card" v-if="showHistory">
<div class="card-body">
<!-- History -->
<div class="row">
<ul class="list-group ticket-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(ticketIndex, historyIndex)" class="float-end">
<i class="fas fa-trash"></i>
</a>
<br/>
</li>
</template>
</ul>
</div> </div>
</div> </div>
</div> </div>
@ -652,43 +877,27 @@
<i class="fas fa-plus"></i> <i class="fas fa-plus"></i>
</button> </button>
<ul class="dropdown-menu" aria-labelledby="create-dropdown-menu"> <ul class="dropdown-menu" aria-labelledby="create-dropdown-menu">
<li> <li><button class="btn btn-light dropdown-menu-button" type="button" @click="createTracker()">Tracker</button></li>
<button class="btn btn-light dropdown-menu-button" type="button" @click="createTracker()">Tracker</button> <li><button class="btn btn-light dropdown-menu-button" type="button" @click="createTimeBox()">Timebox</button></li>
</li>
<li>
<button class="btn btn-light dropdown-menu-button" type="button" @click="createTimeBox()">Timebox</button>
</li>
</ul> </ul>
</div> </div>
</div> </div>
<div>
<a type="button"
class="btn btn-dark text-light bottom-menu-item"
v-on:click="view = 'trackers'"
title="Alle Tracker">
<i class="fas fa-home"></i>
</a>
</div>
<div>
<a type="button"
class="btn btn-dark text-light bottom-menu-item"
v-on:click="view = 'board'"
title="Alle Tracker">
<i class="fas fa-clipboard-check"></i>
</a>
</div>
<div class="" v-if="tickets.length > 0"> <div class="" v-if="tickets.length > 0">
<a type="button" <a type="button"
class="btn btn-primary text-light bottom-menu-item" class="btn btn-primary text-light bottom-menu-item"
v-on:click="view = 'trackersDetail'" data-bs-toggle="modal"
data-bs-target="#showTicketsModal"
data-bs-placement="top"
title="Alle Tracker"> title="Alle Tracker">
<i class="fas fa-clock"></i> <i class="fas fa-clock"></i>
</a> </a>
</div> </div>
<div class="" v-if="archive.length > 0"> <div class="" v-if="archive.length > 0">
<a type="button" <a type="button"
class="btn btn-secondary text-dark bottom-menu-item" class="btn btn-warning bottom-menu-item"
v-on:click="view = 'archive'" data-bs-toggle="modal"
data-bs-target="#showArchivedTicketsModal"
data-bs-placement="top"
title="Archivierte Tracker"> title="Archivierte Tracker">
<i class="fas fa-archive"></i> <i class="fas fa-archive"></i>
</a> </a>
@ -696,7 +905,9 @@
<div class="" v-if="experimental.portalSwitcher"> <div class="" v-if="experimental.portalSwitcher">
<a type="button" <a type="button"
class="btn btn-warning text-light bottom-menu-item" class="btn btn-warning text-light bottom-menu-item"
v-on:click="view = 'switcher'" data-bs-toggle="modal"
data-bs-target="#switcherModal"
data-bs-placement="top"
title="Portal Switcher"> title="Portal Switcher">
<i class="fas fa-random"></i> <i class="fas fa-random"></i>
</a> </a>
@ -704,7 +915,9 @@
<div class="" v-if="experimental.snippetSpace"> <div class="" v-if="experimental.snippetSpace">
<a type="button" <a type="button"
class="btn btn-info text-light bottom-menu-item" class="btn btn-info text-light bottom-menu-item"
v-on:click="view = 'snippetSpace'" data-bs-toggle="modal"
data-bs-target="#snippetSpaceModal"
data-bs-placement="top"
title="Snippet Space"> title="Snippet Space">
<i class="fas fa-code"></i> <i class="fas fa-code"></i>
</a> </a>
@ -712,7 +925,9 @@
<div class=""> <div class="">
<a type="button" <a type="button"
class="btn btn-info text-light bottom-menu-item" class="btn btn-info text-light bottom-menu-item"
v-on:click="view = 'pastDays'" data-bs-toggle="modal"
data-bs-target="#pastDaysModal"
data-bs-placement="top"
title="Buchungsverlauf"> title="Buchungsverlauf">
<i class="fas fa-history"></i> <i class="fas fa-history"></i>
</a> </a>
@ -720,7 +935,9 @@
<div class=""> <div class="">
<a type="button" <a type="button"
class="btn btn-dark text-light bottom-menu-item" class="btn btn-dark text-light bottom-menu-item"
v-on:click="view = 'settings'" data-bs-toggle="modal"
data-bs-target="#settingsModal"
data-bs-placement="left"
title="Einstellungen"> title="Einstellungen">
<i class="fas fa-sliders-h"></i> <i class="fas fa-sliders-h"></i>
</a> </a>
@ -728,6 +945,13 @@
</div> </div>
</div> </div>
</div> </div>
<template v-if="fun">
<div id="hidden-one" @click="tellJoke('pun', 'de')"></div>
<div id="hidden-two" @click="tellJoke('programming,coding,development')"></div>
<div id="hidden-three" @click="tellJoke('dark')"></div>
<div id="hidden-four" @click="tellJoke('misc')"></div>
</template>
</div> </div>
<script src="js/codemirror.js"></script> <script src="js/codemirror.js"></script>

241
js/app.js

@ -1,3 +1,7 @@
Array.prototype.pushToBeginning = function (toPush) {
return this.unshift.apply(this, [toPush]);
}
const TimeTrack = { const TimeTrack = {
data() { data() {
return { return {
@ -7,8 +11,7 @@ const TimeTrack = {
snippetSpace: false, snippetSpace: false,
portalSwitcher: true portalSwitcher: true
}, },
view: 'trackers', view: 'board',
showHistory: false,
theme: 'materia', theme: 'materia',
themes: null, themes: null,
dashboardLogo: 'assets/img/logo.png', dashboardLogo: 'assets/img/logo.png',
@ -21,6 +24,7 @@ const TimeTrack = {
}, },
tickets: [], tickets: [],
archive: [], archive: [],
trashed: {},
worktimeTracker: { worktimeTracker: {
tracking: false, tracking: false,
number: 'Worktime', number: 'Worktime',
@ -53,7 +57,8 @@ const TimeTrack = {
snippets: [], snippets: [],
codeMirrors: [], codeMirrors: [],
customBookingValue: '', customBookingValue: '',
customDateForPastDays: '' customDateForPastDays: '',
newTaskInput: ''
} }
}, },
mounted() { mounted() {
@ -61,7 +66,9 @@ const TimeTrack = {
this.loadStorage(); this.loadStorage();
this.fetchThemes(); this.fetchThemes();
moment.locale('de'); moment.locale('de');
this.customDateForPastDays = moment().format();
setInterval(() => { setInterval(() => {
vue.$forceUpdate(); vue.$forceUpdate();
@ -153,7 +160,7 @@ const TimeTrack = {
this.updateStorage(); this.updateStorage();
}, },
createTracker() { createTracker() {
this.tickets.push({ this.tickets.pushToBeginning({
tracking: false, tracking: false,
number: '#', number: '#',
trackingStarted: null, trackingStarted: null,
@ -164,7 +171,7 @@ const TimeTrack = {
this.updateStorage(); this.updateStorage();
}, },
createTimeBox() { createTimeBox() {
this.tickets.push({ this.tickets.pushToBeginning({
tracking: false, tracking: false,
number: 'Timebox ', number: 'Timebox ',
trackingStarted: null, trackingStarted: null,
@ -204,6 +211,7 @@ const TimeTrack = {
}, },
stopTracking(ticket) { stopTracking(ticket) {
// console.log(ticket); // console.log(ticket);
let component = this;
ticket.trackingStopped = moment(); ticket.trackingStopped = moment();
ticket.tracking = false; ticket.tracking = false;
@ -218,7 +226,6 @@ const TimeTrack = {
manually: false, manually: false,
minutes: Math.round(minutesSpent) minutes: Math.round(minutesSpent)
}; };
// console.log(historyEntry);
if (this.experimental.trackWorktime) { if (this.experimental.trackWorktime) {
if (ticket.paused) { if (ticket.paused) {
@ -226,7 +233,34 @@ const TimeTrack = {
} }
} }
ticket.history.push(historyEntry); if (this.ticketSystemUrl && this.isTicketNumber(ticket.number)) {
let finishedTasks = ticket.tasks.filter((task) => {
return task.finished > ticket.trackingStarted && task.finished < ticket.trackingStopped;
}).map((task) => {
return task.name;
});
iziToast.show({
message: 'Buchung gespeichert',
color: 'blue',
buttons: [
['<button><img src="'+component.ticketSystemIcon+'" class="ticket-icon"/></button>', function (instance, toast) {
instance.hide({
transitionOut: 'fadeOutUp',
onClosing: function () {
window.open(
component.ticketSystemUrl + ticket.number.replace('#', '').trim()+'/time_entries/new?time_entry[hours]='+(Math.round(minutesSpent)/60)+
'&time_entry[activity_id]=9&time_entry[comments]='+finishedTasks.join(', '),
'_blank'
);
}
}, toast, 'buttonName');
}, true]
]
});
}
ticket.history.pushToBeginning(historyEntry);
} }
ticket.trackingStarted = null; ticket.trackingStarted = null;
@ -242,6 +276,28 @@ const TimeTrack = {
this.stopTracking(ticket); this.stopTracking(ticket);
}, },
sendLastBookingToTicketSystem(ticket) {
let component = this;
let latestHistoryItem = ticket.history[0];
if (this.ticketSystemUrl && this.isTicketNumber(ticket.number)) {
let finishedTasks = [];
if (ticket.tasks && ticket.tasks.length > 0) {
finishedTasks = ticket.tasks.filter((task) => {
return task.finished > latestHistoryItem.trackingStarted && task.finished < latestHistoryItem.trackingStopped;
}).map((task) => {
return task.name;
});
}
window.open(
component.ticketSystemUrl + ticket.number.replace('#', '').trim()+'/time_entries/new?time_entry[hours]='+(Math.round(latestHistoryItem.minutes)/60)+
'&time_entry[activity_id]=9&time_entry[comments]='+finishedTasks.join(', '),
'_blank'
);
}
},
resumeTracking(ticket) { resumeTracking(ticket) {
ticket.trackingStarted = moment(); ticket.trackingStarted = moment();
ticket.tracking = true; ticket.tracking = true;
@ -336,12 +392,16 @@ const TimeTrack = {
return number.indexOf('#') >= 0; return number.indexOf('#') >= 0;
}, },
deleteTracker(index, archive = false) { deleteTracker(index, archive = false) {
let component = this;
let message = ''; let message = '';
if (archive) { if (archive) {
Object.assign(this.trashed, this.archive[index]);
let name = this.archive[index].number; let name = this.archive[index].number;
message = 'Tracker "' + name + '" wurde gelöscht'; message = 'Tracker "' + name + '" wurde gelöscht';
this.archive.splice(index, 1); this.archive.splice(index, 1);
} else { } else {
Object.assign(this.trashed, this.tickets[index]);
let name = this.tickets[index].number; let name = this.tickets[index].number;
message = 'Tracker "' + name + '" wurde gelöscht'; message = 'Tracker "' + name + '" wurde gelöscht';
this.tickets.splice(index, 1); this.tickets.splice(index, 1);
@ -350,29 +410,39 @@ const TimeTrack = {
iziToast.show({ iziToast.show({
message: message, message: message,
color: 'blue', color: 'blue',
position: 'topCenter' buttons: [
['<button><i class="fas fa-undo"></i></button>', function (instance, toast) {
instance.hide({
transitionOut: 'fadeOutUp',
onClosing: function(instance, toast, closedBy){
component.restoreTrashed();
}
}, toast, 'buttonName');
}, true]
]
}); });
this.updateStorage(); this.updateStorage();
}, },
restoreTrashed() {
let restoredTracker = {};
Object.assign(restoredTracker, this.trashed);
this.trashed = {};
this.tickets.pushToBeginning(restoredTracker);
this.updateStorage();
},
archiveTracker(index) { archiveTracker(index) {
if (this.tickets[index].tracking) { if (this.tickets[index].tracking) {
this.stopActiveTracker(); this.stopActiveTracker();
} }
iziToast.show({ this.archive.pushToBeginning(this.tickets[index]);
title: 'Tracker archiviert',
color: 'green',
position: 'topCenter'
});
this.archive.push(this.tickets[index]);
this.tickets.splice(index, 1); this.tickets.splice(index, 1);
this.updateStorage(); this.updateStorage();
}, },
reactivateTicket(index) { reactivateTicket(index) {
this.view = 'trackers'; this.tickets.pushToBeginning(this.archive[index]);
this.tickets.push(this.archive[index]);
this.archive.splice(index, 1); this.archive.splice(index, 1);
this.updateStorage(); this.updateStorage();
}, },
@ -385,7 +455,7 @@ const TimeTrack = {
this.updateStorage(); this.updateStorage();
}, },
bookTimeManually(ticket, minutes) { bookTimeManually(ticket, minutes) {
ticket.history.push({ ticket.history.pushToBeginning({
trackingStarted: moment(), trackingStarted: moment(),
trackingStopped: moment(), trackingStopped: moment(),
manually: true, manually: true,
@ -423,9 +493,9 @@ const TimeTrack = {
ticketCollection.forEach((ticket) => { ticketCollection.forEach((ticket) => {
if (ticket.archived || (ticket.active && ticket.active === false)) { if (ticket.archived || (ticket.active && ticket.active === false)) {
archive.push(ticket); archive.pushToBeginning(ticket);
} else { } else {
tickets.push(ticket) tickets.pushToBeginning(ticket)
} }
}); });
@ -450,15 +520,22 @@ const TimeTrack = {
console.log(error); console.log(error);
}); });
}, },
showHistoryForTracker(ticket) { showHistoryForTracker(tracker) {
this.selectedTracker = ticket; this.selectedTracker = tracker;
this.showHistory = true;
this.$forceUpdate(); this.$forceUpdate();
setTimeout(() => { setTimeout(() => {
let historyModal = new bootstrap.Modal(document.getElementById('historyModal')); let historyModal = new bootstrap.Modal(document.getElementById('historyModal'));
historyModal.toggle(); historyModal.toggle();
}, 50); }, 50);
}, },
openTasksForTracker(tracker) {
this.selectedTracker = tracker;
this.$forceUpdate();
setTimeout(() => {
let tasksModal = new bootstrap.Modal(document.getElementById('trackerTasksModal'));
tasksModal.toggle();
}, 50);
},
showCustomBookingForTracker(ticket) { showCustomBookingForTracker(ticket) {
this.selectedTracker = ticket; this.selectedTracker = ticket;
this.$forceUpdate(); this.$forceUpdate();
@ -497,8 +574,17 @@ const TimeTrack = {
this.tickets.forEach((ticket) => { this.tickets.forEach((ticket) => {
ticket.history.forEach((historyEntry) => { ticket.history.forEach((historyEntry) => {
if (moment(historyEntry.trackingStarted).format("MMM Do YY") === day) { if (moment(historyEntry.trackingStarted).format("MMM Do YY") === day) {
historyEntry.ticket = ticket.number; let newEntry = {};
collection.push(historyEntry); Object.assign(newEntry, historyEntry);
newEntry.ticket = ticket.number;
let existingEntry = this.getCollectionItemWithValue(collection, 'ticket', ticket.number);
if (existingEntry) {
existingEntry.minutes = Number(existingEntry.minutes) + Number(newEntry.minutes);
} else {
collection.pushToBeginning(newEntry);
}
} }
}); });
}); });
@ -506,15 +592,34 @@ const TimeTrack = {
this.archive.forEach((ticket) => { this.archive.forEach((ticket) => {
ticket.history.forEach((historyEntry) => { ticket.history.forEach((historyEntry) => {
if (moment(historyEntry.trackingStarted).format("MMM Do YY") === day) { if (moment(historyEntry.trackingStarted).format("MMM Do YY") === day) {
historyEntry.ticket = ticket.number; let newEntry = {};
historyEntry.archive = true; Object.assign(newEntry, historyEntry);
collection.push(historyEntry); newEntry.ticket = ticket.number;
let existingEntry = this.getCollectionItemWithValue(collection, 'ticket', ticket.number);
if (existingEntry) {
existingEntry.minutes = Number(existingEntry.minutes) + Number(newEntry.minutes);
} else {
collection.pushToBeginning(newEntry);
}
} }
}); });
}); });
return collection; return collection;
}, },
getCollectionItemWithValue(collection, property, value) {
let found = false;
collection.forEach((item) => {
if (item[property] === value) {
found = item;
}
})
return found;
},
sendPortalChangeRequest() { sendPortalChangeRequest() {
let vue = this; let vue = this;
let publicDBParam = this.publicDB ? '&publicDB=1' : ''; let publicDBParam = this.publicDB ? '&publicDB=1' : '';
@ -557,7 +662,7 @@ const TimeTrack = {
id: getRandomID() id: getRandomID()
}; };
this.snippets.push(newSnippet); this.snippets.pushToBeginning(newSnippet);
this.loadSnippet(this.snippets[this.snippets.length-1]); this.loadSnippet(this.snippets[this.snippets.length-1]);
this.updateStorage(); this.updateStorage();
}, },
@ -634,10 +739,71 @@ const TimeTrack = {
tellJoke(category, language = 'en') { tellJoke(category, language = 'en') {
let jokeService = new JokeService(category ?? undefined, language); let jokeService = new JokeService(category ?? undefined, language);
jokeService.tell(); jokeService.tell();
},
addTask() {
if (this.newTaskInput.length <= 0 || this.newTaskInput.trim() === '') {
return false;
}
if (!this.selectedTracker.tasks) {
this.selectedTracker.tasks = [];
}
this.selectedTracker.tasks.pushToBeginning({
name: this.newTaskInput,
done: false,
created: moment(),
percentDone: 0,
finished: null
});
this.newTaskInput = '';
this.updateStorage();
},
deleteTask(taskIndex) {
this.selectedTracker.tasks.splice(taskIndex, 1)
this.updateStorage();
},
toggleTask(task) {
task.done = !task.done;
if (task.done) {
task.finished = moment();
task.percentDone = 100;
} else {
task.finished = null;
task.percentDone = 0;
}
this.updateStorage();
},
checkForCompletionOfTask(task) {
task.done = task.percentDone == 100;
this.$forceUpdate();
this.updateStorage();
},
getOpenTasksForTracker(tasks) {
if (!tasks) {
return 0;
}
let counter = 0;
tasks.forEach((task) => {
if (!task.done) {
counter++;
}
});
return counter;
},
showOpenTasksForTracker(tasks) {
let count = this.getOpenTasksForTracker(tasks);
return count > 0 ? ' ('+count+' offen)' : '';
} }
}, },
beforeDestroy() { beforeDestroy() {
this.stopTrackingTicket(); this.stopActiveTracker();
}, },
watch: { watch: {
publicDB() { publicDB() {
@ -675,6 +841,21 @@ const TimeTrack = {
fun: this.fun, fun: this.fun,
theme: this.theme theme: this.theme
}); });
},
ticketSystemIcon() {
if (this.ticketSystemUrl) {
if (this.ticketSystemUrl.search('redmine')) {
return 'assets/img/redmine.png';
}
if (this.ticketSystemUrl.search('jira')) {
return 'assets/img/jira.png';
}
if (this.ticketSystemUrl.search('gitlab')) {
return 'assets/img/gitlab.png';
}
}
} }
} }
}; };