Compare commits
35 Commits
switch-to-
...
master
Author | SHA1 | Date | |
---|---|---|---|
d123c773a0 | |||
e225fb0c43 | |||
3b2959aff2 | |||
c35ebe2735 | |||
|
8156081e04 | ||
|
d7492ad5a1 | ||
|
564e4feb86 | ||
bd9a23a0b0 | |||
50f8e36800 | |||
6a00a258a2 | |||
0d6df49493 | |||
4188c15b4b | |||
ee874ac51e | |||
9c145c0d9c | |||
|
76530bc09f | ||
4974ae0e10 | |||
1d9740c9bc | |||
|
2152fa61f7 | ||
6feed2ec2a | |||
|
605dcb7f51 | ||
e0cab7f5b3 | |||
|
6defc2c353 | ||
6f42a97b9e | |||
b79a997bf8 | |||
56bfd038e4 | |||
9382624d3f | |||
8de701c19f | |||
1f7d044ccc | |||
fa82e80f13 | |||
f525316400 | |||
ad833f3831 | |||
d46411c4e4 | |||
fffde13440 | |||
7b4887ecac | |||
ff1afd7c32 |
2
.gitattributes
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*.pptx filter=lfs diff=lfs merge=lfs -text
|
||||
*.apk filter=lfs diff=lfs merge=lfs -text
|
@ -1 +1,2 @@
|
||||
# UBC - Timetracking with Geofences
|
||||
# UBC SS2020 - Geo Timetracking - Team TacocaT
|
||||
![Geo Timetracking](other-artifacts/Product-Flyer.png)
|
@ -22,7 +22,6 @@ dependencies {
|
||||
compileOnly 'org.projectlombok:lombok'
|
||||
annotationProcessor 'org.projectlombok:lombok'
|
||||
implementation 'org.mariadb.jdbc:mariadb-java-client'
|
||||
implementation 'org.postgresql:postgresql'
|
||||
|
||||
implementation "org.springframework.boot:spring-boot-starter-security"
|
||||
implementation "com.auth0:java-jwt:3.10.2"
|
||||
|
@ -45,7 +45,11 @@ public interface RecordRepository extends PagingAndSortingRepository<TimeRecord,
|
||||
Pageable pageable
|
||||
);
|
||||
|
||||
@Query("SELECT record from TimeRecord record where record.account.user.username = :#{principal} AND record.enddate > current_date")
|
||||
@Query("SELECT record from TimeRecord record " +
|
||||
"where record.account.user.username = :#{principal} " +
|
||||
"AND record.enddate > current_date " +
|
||||
"AND record.enddate < current_date+1"
|
||||
)
|
||||
Page<TimeRecord> today(Pageable pageable);
|
||||
|
||||
@RestResource(rel = "openEntries", path = "openEntries")
|
||||
|
@ -1,5 +1,5 @@
|
||||
spring.jpa.hibernate.ddl-auto=update
|
||||
spring.datasource.url=jdbc:postgresql://db:5432/geotime
|
||||
spring.datasource.url=jdbc:mariadb://db:3306/geotime
|
||||
spring.datasource.username=root
|
||||
spring.datasource.password=supersecure
|
||||
spring.datasource.driver-class-name=org.postgresql.Driver
|
||||
spring.datasource.driver-class-name=org.mariadb.jdbc.Driver
|
@ -34,6 +34,8 @@ INSERT INTO time_record (id, enddate, startdate, `type`, account_id) VALUES
|
||||
(6, '2020-05-30 23:00:00', '2020-05-30 22:00:00', 1, 1),
|
||||
(7, '2020-05-31 01:00:00', '2020-05-31 00:00:00', 1, 1),
|
||||
(8, '2020-05-31 04:00:00', '2020-05-31 02:00:00', 1, 1),
|
||||
(9, '2020-05-31 16:00:00', '2020-05-31 12:00:00', 1, 1);
|
||||
(9, '2020-05-31 16:00:00', '2020-05-31 12:00:00', 1, 1),
|
||||
(10, '2020-06-14 16:00:00', '2020-06-14 12:00:00', 1, 1),
|
||||
(11, '2020-06-15 16:00:00', '2020-06-15 12:00:00', 1, 1);
|
||||
|
||||
SET FOREIGN_KEY_CHECKS=1;
|
@ -26,11 +26,10 @@ services:
|
||||
build:
|
||||
context: ./sql
|
||||
volumes:
|
||||
- "db-data:/var/lib/postgresql/data"
|
||||
- "qbc-db-data:/var/lib/mysql"
|
||||
environment:
|
||||
POSTGRES_DB: geotime
|
||||
POSTGRES_PASSWORD: supersecure
|
||||
POSTGRES_USER: root
|
||||
MYSQL_DATABASE: geotime
|
||||
MYSQL_ROOT_PASSWORD: supersecure
|
||||
|
||||
volumes:
|
||||
db-data:
|
||||
qbc-db-data:
|
@ -124,6 +124,20 @@
|
||||
{[}{{{\color{delim}{[}}}}{1}
|
||||
{]}{{{\color{delim}{]}}}}{1},
|
||||
}
|
||||
\lstdefinelanguage{JavaScript}{
|
||||
keywords={typeof, new, true, false, catch, function, return, null, catch, switch, var, if, in, while, do, else, case, break},
|
||||
keywordstyle=\color{blue}\bfseries,
|
||||
ndkeywords={class, export, boolean, throw, implements, import, this},
|
||||
ndkeywordstyle=\color{darkgray}\bfseries,
|
||||
identifierstyle=\color{black},
|
||||
sensitive=false,
|
||||
comment=[l]{//},
|
||||
morecomment=[s]{/*}{*/},
|
||||
commentstyle=\color{purple}\ttfamily,
|
||||
stringstyle=\color{red}\ttfamily,
|
||||
morestring=[b]',
|
||||
morestring=[b]"
|
||||
}
|
||||
|
||||
\usepackage{setspace}
|
||||
\setstretch{1.2} %Zeilenabstand
|
||||
@ -142,7 +156,7 @@
|
||||
%\missingfigure[figwidth=6cm]{Testing a long text string}
|
||||
|
||||
%Tiefe des Inhaltsverzeichnisses
|
||||
\setcounter{tocdepth}{2}
|
||||
\setcounter{tocdepth}{1}
|
||||
|
||||
% Metadaten
|
||||
\author{Simon Kellner, Marcel Schwarz, Tobias Wieck, Tim Zieger}
|
||||
@ -152,27 +166,6 @@
|
||||
|
||||
\include{parts/titlepage}
|
||||
|
||||
\begin{abstract}
|
||||
{\huge Checkliste Lieferobjekte}
|
||||
|
||||
\begin{itemize}
|
||||
\item Ein Journal (z.B. Excel in Dropbox, Office 365, Google), in dem jedes Projektmitglied Tages- und Themenspezifisch seine geleisteten Arbeiten mit Aufwandsangabe listet. Z.b. "Hans 12.4 3 Std Backend Coding"- Das Journal muss zeitnah geführt werden und verfügbar sein. Ein unvollständiges Zeitjournal führt zur Abwertung in der Benotung.
|
||||
\item Projektdokumentation (*: ca. 15-25 Seiten, je nach Bilderdichte)
|
||||
\begin{itemize}
|
||||
\item Source Code und alle erstellten Dokumente
|
||||
\item Erläuterung des Designs (*)
|
||||
\item Einrichtung der Entwicklungsumgebung (*) \\
|
||||
(falls anwendbar, Standarddinge wie Eclipse, Android SDK müssen nicht detailliert erwähnt werden):
|
||||
\begin{itemize}
|
||||
\item Welche Frameworks wurden für die Entwicklung benutzt und was gab es bei der Konfiguration / Installation dieser Tools zu beachten?
|
||||
\item Wie wurden die Zielanwendungen erzeugt und z.B. auf ein Zielgerät oder Server übertragen und dort aktiviert?
|
||||
\end{itemize}
|
||||
\item Projektbericht: Wie sind Sie vorgegangen, was wollten Sie erreichen, was haben Sie gelernt, was gab es für Probleme, was haben Sie erreicht, wer hat was gemacht?
|
||||
\end{itemize}
|
||||
\item Datei mit der Präsentation
|
||||
\end{itemize}
|
||||
\end{abstract}
|
||||
|
||||
\tableofcontents
|
||||
|
||||
\listoffigures
|
||||
@ -192,19 +185,27 @@
|
||||
\include{parts/android}
|
||||
|
||||
\chapter{Vollständiger Application Stack}
|
||||
\begin{figure}[H]
|
||||
\begin{figure}[H]
|
||||
\centering
|
||||
\includegraphics[width=\linewidth]{img/ApplicationStack}
|
||||
\includegraphics[width=0.9\linewidth]{img/ApplicationStack}
|
||||
\caption{Application Stack}
|
||||
\end{figure}
|
||||
Das Deployment von Geotime ist in drei große Schichten aufgeteilt. Zunächst wäre hier die Backend Schicht, die Schicht der Datenhaltung und der API. Dieser Teil der Anwendung braucht am meisten Schutz, da er der wichtigste ist und dort alle Daten gespeichert werden. Der Zugriff auf die Datenbank ist nur auf das Backend beschränkt. Um nun die Applikation zu Nutzen gibt es zwei Möglichkeiten: Eine Android App oder ein Webbrowser.
|
||||
\end{figure}
|
||||
Das Deployment der Geo Timetracking Application ist in drei große Schichten aufgeteilt. Zunächst wäre hier die Backend Schicht, die Schicht der Datenhaltung und der API. Dieser Teil der Anwendung braucht am meisten Schutz, da er der wichtigste ist und dort alle Daten gespeichert werden. Der Zugriff auf die Datenbank ist nur auf das Backend beschränkt. Um nun die Applikation zu Nutzen gibt es zwei Möglichkeiten: Eine Android App oder ein Webbrowser.\\
|
||||
|
||||
Die Android App implementiert die View Schicht selbst und fragt nur für Daten den Backend-Dienst an. Diese Anfragen gehen zunächst an den Server, der die App hostet und werden dann von dem darauf laufenden Docker Deamon an den entsprechenden Container weitergeleitet.
|
||||
Die Android App implementiert die View Schicht selbst und fragt nur für Daten den Backend-Dienst an. Diese Anfragen gehen zunächst an den Server, der die App hostet und werden dann von dem darauf laufenden Docker Deamon an den entsprechenden Container weitergeleitet.\\
|
||||
|
||||
Beim Zugriff über den Webbrowser funktioniert die Kommunikation geringfügig anders. Zunächst wird vom Client der nginx Container nach dem statischen Teil der Website gefragt, dieser lädt dann über ähnliche Anfragen wie in der Android App die Daten vom Backend. Das global gesprochene Protokoll ist hierbei immer HTTP.
|
||||
Beim Zugriff über den Webbrowser funktioniert die Kommunikation geringfügig anders. Zunächst wird vom Client der nginx Container nach dem statischen Teil der Website gefragt, dieser lädt dann über ähnliche Anfragen wie in der Android App die Daten vom Backend. Das global gesprochene Protokoll ist hierbei immer HTTP.
|
||||
|
||||
\chapter{Projektjournal}
|
||||
\begin{figure}[H]
|
||||
\centering
|
||||
\includegraphics[width=\linewidth]{img/Clockify_Summary-Overview.pdf}
|
||||
\end{figure}
|
||||
Dieser Report zeigt eine Übersicht der geleisteten Arbeit jedes Gruppenmitglieds. Der vollständige Report ist separat angehängt. Dort kann jede Aktivität auf Issue Ebene genau nachvollzogen werden. Da lediglich die Issuenummern angegeben wurden, können die eigentlich dahinter liegenden Aufgaben auf GitLab\footnote{\url{https://gitlab.com/marcel.schwarz/2020ss-qbc-geofence-timetracking/-/issues?scope=all\&utf8=\%E2\%9C\%93\&state=all}} eingesehen werden.
|
||||
|
||||
\chapter{Projektfazit und Ausblick}
|
||||
Bei dem Projekt im Rahmen von Ubiquitous/Pervasive Computing konnten wir Bekanntes anwenden und Neues lernen. Wir alle konnten uns gut einbringen und zusammen auf unser gemeinsames Ziel hinarbeiten. Im Rückblick auf die vergangenen fünf Sprints lässt sich sagen, dass diese erfolgreich verlaufen sind. Die Verteilung der Aufgaben war gleichmäßig und funktionierte reibungslos. Die Idee des Projekts konnte vollständig umgesetzt werden, zudem konnten anfangs nicht geplante Features umgesetzt werden. Hierzu zählen z.B. tagesübergreifende Time Records. Wir alle sind mit dem Ergebnis unserer Arbeit zufrieden und können das Projekt als erfolgreich bezeichnen.\\
|
||||
|
||||
Ebenso sehen wir ein großes Potential in der Weiterentwicklung unseres Endprodukts. Hier haben wir Ideen wie: Zuordnung der Benutzer in Gruppen, Benutzerprofile mit Daten über den Benutzer und dessen Tätigkeit oder auch Zuweisung von Kernarbeitszeit und Zeitrahmen, um Timetracking nur in einem festgelegten Zeitfenster zu erlauben. Mit ein paar Verbesserungen könnte unser Produkt von kleinen Unternehmen verwendet werden, die ein auf Vertrauen basiertes Zeitmeldesystem suchen.
|
||||
|
||||
\end{document}
|
||||
|
BIN
documentation/img/Clockify_Summary-Overview.pdf
Normal file
BIN
documentation/img/frontend/about.PNG
Normal file
After Width: | Height: | Size: 43 KiB |
BIN
documentation/img/frontend/accounts.PNG
Normal file
After Width: | Height: | Size: 24 KiB |
BIN
documentation/img/frontend/admin.PNG
Normal file
After Width: | Height: | Size: 71 KiB |
BIN
documentation/img/frontend/color_sceme.png
Normal file
After Width: | Height: | Size: 2.7 KiB |
BIN
documentation/img/frontend/home.PNG
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
documentation/img/frontend/homeLoggedOut.PNG
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
documentation/img/frontend/logo_dark_gt.png
Normal file
After Width: | Height: | Size: 56 KiB |
BIN
documentation/img/frontend/statistics_column.PNG
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
documentation/img/frontend/statistics_pie.PNG
Normal file
After Width: | Height: | Size: 74 KiB |
BIN
documentation/img/frontend/timerecords.PNG
Normal file
After Width: | Height: | Size: 77 KiB |
BIN
documentation/img/frontend/verwaltung.PNG
Normal file
After Width: | Height: | Size: 5.3 KiB |
@ -1,9 +1,9 @@
|
||||
\chapter{Android-App}
|
||||
\section{Technologiebeschreibung}
|
||||
\subsection{Android SDK}
|
||||
Die Android-Entwicklung wurde, aufgrund der Ausgereiftheit und den Emulatoren, mit Android-Studio realisiert. Android-Studio verwaltet auch das SDK und unterstützt beim aktuell halten der Bibliotheken.
|
||||
Die Android-Entwicklung wurde, aufgrund der Ausgereiftheit und den Emulatoren, mit Android-Studio realisiert. Android-Studio verwaltet auch das SDK und unterstützt beim aktuell Halten der Bibliotheken.\\
|
||||
|
||||
Das minimale API-Level, welches das Endgerät haben darf, wurde auf 23 "Marshmallow" festgelegt. Dadurch werden ca. 85\% der Geräte unterstützt und ist aktuell genug um gewisse Features, wie das neue Berechtigungssystem, zu unterstützen. Die Zielversion ist das aktuelle Android 10 mit API-Level 29. In dieser Version wurden erneut Berechtigungen geändert, wodurch im Code einige Anpassungen gemacht werden mussten (siehe: \ref{sec:Probleme})
|
||||
Das minimale API-Level, welches das Endgerät haben darf, wurde auf 23 "Marshmallow" festgelegt. Dadurch werden ca. 85\% der Geräte unterstützt und ist aktuell genug um gewisse Features, wie das neue Berechtigungssystem, zu unterstützen. Die Zielversion ist das aktuelle Android 10 mit API-Level 29. In dieser Version wurden erneut Berechtigungen geändert, wodurch im Code einige Anpassungen gemacht werden mussten (siehe: \ref{sec:Probleme}).
|
||||
|
||||
\subsection{Kotlin}
|
||||
Die Entscheidung fiel auf Kotlin als Programmiersprache, da die Sprache von Google für die Entwicklung von Android-Apps bevorzugt wird. Außerdem bietet dies die Gelegenheit, eine neue Programmiersprache zu erlernen. Dadurch musste jedoch viel Zeit investiert werden um zum einen die Sprache und zum anderen die Entwicklungsumgebung, sowie den Aufbau einer Android-App zu lernen. Dafür wurden zwei von insgesamt fünf Sprints eingeplant, weswegen die App nur die Grundfunktionen besitzt.
|
||||
@ -23,7 +23,7 @@
|
||||
|
||||
\section{Umsetzung}
|
||||
\subsection{Design der Activities}
|
||||
Insgesamt besitzt die App die vier Activities: Login, MainActivity, Register und Settings. Wobei die Register- und die Settings-Activity aus zeitlichen Gründen ohne Funktion sind. Sie haben auch noch die alten unschönen Eingabefelder, sind aber für die Funktion der gesamten Anwendung nicht sonderlich relevant, weshalb entschieden wurde, diese zu vernachlässigen und den Fokus auf die Funktionalität zu legen.
|
||||
Insgesamt besitzt die App die vier Activities: Login, MainActivity, Register und Settings. Wobei die Register- und die Settings-Activity aus zeitlichen Gründen ohne Funktion sind. Sie haben auch noch die alten unschönen Eingabefelder, sind aber für die Funktion der gesamten Anwendung nicht sonderlich relevant, weshalb entschieden wurde, diese zu vernachlässigen und den Fokus auf die Funktionalität zu legen.\\
|
||||
|
||||
Jeder Bildschirm hat eine Top-Bar auf der, je nachdem auf welchem Bildschirm man sich befindet, unterschiedliche Inhalte angezeigt werden. Beim Einloggen und Account erstellen wird außer dem Logo und dem Namen der App nichts angezeigt. In den Einstellungen erscheint anstatt des Logos ein Zurück-Button und auf dem Hauptbildschirm gibt es ein Menü zum Ausloggen und um zu den Einstellungen zu gelangen.
|
||||
\begin{figure}[H]
|
||||
@ -54,12 +54,12 @@
|
||||
\label{Abb:settings}
|
||||
\end{minipage}
|
||||
\end{figure}
|
||||
Wie zu erkennen ist lag, der Fokus der Implementierung deutlich auf der Main Activtiy, da sie auch das Wichtigste der App beinhaltet. Prominent ist dabei der 'START'-Knopf an der Unterseite, mit dem die Aufzeichnung gestartet werden kann (genaueres im Kapitel \ref{subsec:main}).
|
||||
Wie zu erkennen ist lag, der Fokus der Implementierung deutlich auf der Main Activtiy, da sie auch das Wichtigste der App beinhaltet. Prominent ist dabei der "START"-Knopf an der Unterseite, mit dem die Aufzeichnung gestartet werden kann (genaueres im Kapitel \ref{subsec:main}).
|
||||
|
||||
\subsection{Authentifizierung}
|
||||
Zur Authentifizierung benutzen wir JWT, welches bei jeder Anfrage ans Backend mit geschickt werden muss. Das Token erhält man beim Einloggen mit den richtigen Daten und muss persistiert werden, bis sich der Benutzer ausloggt. Dazu wird das Token im privaten Speicher der App gespeichert. In allen weiteren Activities kann dann auf den Speicher zugegriffen werden und das Token beim Erstellen des \verb|AuthenticationInterceptor|s mitgegeben werden. Beim Ausloggen wird einfach die Datei mit dem Token aus dem Speicher gelöscht.
|
||||
Zur Authentifizierung benutzen wir JWT, welches bei jeder Anfrage ans Backend mit geschickt werden muss. Das Token erhält man beim Einloggen mit den richtigen Daten und muss persistiert werden, bis sich der Benutzer ausloggt. Dazu wird das Token im privaten Speicher der App gespeichert. In allen weiteren Activities kann dann auf den Speicher zugegriffen werden und das Token beim Erstellen des "AuthenticationInterceptor"s mitgegeben werden. Beim Ausloggen wird einfach die Datei mit dem Token aus dem Speicher gelöscht.
|
||||
|
||||
Der \verb|AuthenticationInterceptor| ist Kind von der \verb|Interceptor|-Klasse aus der \verb|okhttp3|-Bibliothek, welche in Retrofit eingebunden ist. Mithilfe des Interceptors können REST-Aufrufen Header-Daten mitgegeben werden. In unserem Fall ist das das \verb|Authorization|-Feld mit dem Token.
|
||||
Der "AuthenticationInterceptor" ist Kind von der "Interceptor"-Klasse aus der "okhttp3"-Bibliothek, welche in Retrofit eingebunden ist. Mithilfe des Interceptors können REST-Aufrufen Header-Daten mitgegeben werden. In unserem Fall ist das das "Authorization"-Feld mit dem Token.
|
||||
\begin{lstlisting}[language=Kotlin,caption=AuthenticationInterceptor]
|
||||
class AuthenticationInterceptor(pToken: String) : Interceptor {
|
||||
private val token = pToken
|
||||
@ -89,7 +89,7 @@ val builder = Retrofit.Builder()
|
||||
val retrofit = builder.build()
|
||||
service = retrofit.create(GeofenceService::class.java)
|
||||
\end{lstlisting}
|
||||
Die Klasse \verb|GeofenceService| dient, wie oben beschrieben, zur Definition der Endpunkte in Form von Methodenaufrufen. Dort wird definiert, ob es ein \verb|POST|- oder \verb|GET|-Entpunkt ist, wie der Pfad lautet und was für Parameter mitgegeben werden.
|
||||
Die Klasse "GeofenceService" dient, wie oben beschrieben, zur Definition der Endpunkte in Form von Methodenaufrufen. Dort wird definiert, ob es ein "POST"- oder "GET"-Entpunkt ist, wie der Pfad lautet und was für Parameter mitgegeben werden.
|
||||
\begin{lstlisting}[language=Kotlin,caption=GeofenceService]
|
||||
@POST("/login")
|
||||
fun login(@Body login_data: ValuesUserLogin): Call<Void>
|
||||
@ -100,14 +100,14 @@ fun getUser(): Call<ValuesUser>
|
||||
@GET("accounts/search/findByUsername")
|
||||
fun getAccounts(@Query("username") username : String): Call<EmbeddedAccounts>
|
||||
\end{lstlisting}
|
||||
Der Rückgabewert der Methoden ist immer vom Typ \verb|Call|. Wenn aus dem Body Werte gelesen werden sollen, muss eine Art Skelett-Klasse angelegt werden mit den relevanten Feldern. Die Klasse \verb|ValuesUser| stellt Werte der Antwort bereit, wie z. B. den Vornamen.
|
||||
Der Rückgabewert der Methoden ist immer vom Typ "Call". Wenn aus dem Body Werte gelesen werden sollen, muss eine Art Skelett-Klasse angelegt werden mit den, für die Anwendung relevanten, Feldern. Die Klasse "ValuesUser" stellt Werte der Antwort bereit, wie z. B. den Vornamen.
|
||||
\begin{lstlisting}[language=Kotlin,caption=ValuesUser]
|
||||
class ValuesUser(firstname: String) {
|
||||
@SerializedName("firstname")
|
||||
var firstname = firstname
|
||||
}
|
||||
\end{lstlisting}
|
||||
Der Aufruf der Methode erfolgt asynchron. Deshalb darf sich nicht auf das Ergebnis des Aufrufs direkt danach verlassen werden, sonst bekommt man eine Null-Pointer-Excetion. Die Methode \verb|enqueue| besitzt ein Callback-Objekt als Parameter, welches \verb|onResponse| und \verb|onFailure| überschreibt. Dort wird entsprechend definiert, was in den jeweiligen Fällen ausgeführt werden soll.
|
||||
Der Aufruf der Methode erfolgt asynchron. Deshalb darf sich nicht auf das Ergebnis des Aufrufs direkt danach verlassen werden, sonst bekommt man eine Null-Pointer-Excetion. Die Methode "enqueue" besitzt ein Callback-Objekt als Parameter, welches "onResponse" und "onFailure" überschreibt. Dort wird entsprechend definiert, was in den jeweiligen Fällen ausgeführt werden soll.
|
||||
\begin{lstlisting}[language=Kotlin,caption=Callback der "getUser" Funktion]
|
||||
val call = service.getUser()
|
||||
call.enqueue(object : Callback<ValuesUser> {
|
||||
@ -136,11 +136,11 @@ call.enqueue(object : Callback<ValuesUser> {
|
||||
\end{itemize}
|
||||
|
||||
\subsection{Geofencing}\label{subsec:geofence}
|
||||
Die Geofencing-Funktion ist die zentrale Funktion für die App und auch für das gesamte Projekt. Deshalb war es wichtig, dass sie frühzeitig funktioniert.
|
||||
Die Geofencing-Funktion ist die zentrale Funktion für die App und auch für das gesamte Projekt. Deshalb war es wichtig, dass sie frühzeitig funktioniert.\\
|
||||
|
||||
Um die Position eines Gerätes zu bestimmen, bedarf es einer Berechtigung, die vom Benutzer bestätigt werden muss. Für Geräte mit API-Level 28 und niedriger muss dafür die \verb|ACCESS_FINE_LOCATION|-Berechtigung gesetzt werden und für API-Level 29 und höher \verb|ACCESS_BACKGROUND_LOCATION|.
|
||||
Um die Position eines Gerätes zu bestimmen, bedarf es einer Berechtigung, die vom Benutzer bestätigt werden muss. Für Geräte mit API-Level 28 und niedriger muss dafür die \linebreak "ACCESS\_FINE\_LOCATION"-Berechtigung gesetzt werden und für API-Level 29 und höher "ACCESS\_BACKGROUND\_LOCATION".\\
|
||||
|
||||
Der Geofence wird initialisiert, wenn für den Benutzer Geo-Daten gespeichert sind. Ist dies der Fall, so wird ein \verb|GeofencingClient| angelegt, dem dann der Geofence hinzugefügt wird. Der Geofence wird erzeugt mit den Parametern: Breitengrad, Längengrad, Radius, der Lebenszeit des Fence und den Übergangstypen. Die Typen sind in unserem Fall \verb|GEOFENCE_TRANSITION_ENTER| und \verb|GEOFENCE_TRANSITION_EXIT|, da wir immer reagieren wollen, wenn der Nutzer den Bereich verlässt oder betritt.
|
||||
Der Geofence wird initialisiert, wenn für den Benutzer Geo-Daten gespeichert sind. Ist dies der Fall, so wird ein "GeofencingClient" angelegt, dem dann der Geofence hinzugefügt wird. Der Geofence wird erzeugt mit den Parametern: Breitengrad, Längengrad, Radius, der Lebenszeit des Fence und den Übergangstypen. Die Typen sind in unserem Fall "GEOFENCE\_TRANSITION\_ENTER" und "GEOFENCE\_TRANSITION\_EXIT", da wir immer reagieren wollen, wenn der Nutzer den Bereich verlässt oder betritt.
|
||||
\begin{lstlisting}[language=Kotlin,caption=Anlegen des Geofencing Clients]
|
||||
geofencingClient = LocationServices.getGeofencingClient(this)
|
||||
geofence = Geofence.Builder().setRequestId("Geofence")
|
||||
@ -149,14 +149,14 @@ geofence = Geofence.Builder().setRequestId("Geofence")
|
||||
.setTransitionTypes(Geofence.GEOFENCE_TRANSITION_ENTER or Geofence.GEOFENCE_TRANSITION_EXIT)
|
||||
.build()
|
||||
\end{lstlisting}
|
||||
Um den Geofence-Client zu starten wird auf das Objekt die \verb|addGeofences|-Methode ausgeführt mit einem \verb|GeofencingRequest|-Objekt und einem \verb|PendingIntent|-Objekt als Parameter.
|
||||
Um den Geofence-Client zu starten wird auf das Objekt die "addGeofences"-Methode ausgeführt mit einem "GeofencingRequest"-Objekt und einem "PendingIntent"-Objekt als Parameter.
|
||||
\begin{lstlisting}[language=Kotlin,caption="addGeofences" Methode]
|
||||
geofencingClient.addGeofences(getGeofencingRequest(), geofencePendingIntent)?.run {
|
||||
addOnSuccessListener { ... }
|
||||
addOnFailureListener { ... }
|
||||
}
|
||||
\end{lstlisting}
|
||||
In der \verb|getGeofencingRequest|-Methode wird festgelegt, auf welches initiale Event reagiert werden soll und der oben erstellte Geofence wird hinzugefügt. Als initiales Event haben wir \verb|INITIAL_TRIGGER_ENTER| gewählt, da es ausgelöst wird, wenn man sich bereits im Bereich befindet und die App startet. Denn erst mit dem Eintrittsevent wird der Button zum Starten der Aufzeichnung freigeschaltet. Das \verb|geofencePendingIntent| definiert die BroadcastReceiver-Klasse, welche bei jedem Event aufgerufen wird.
|
||||
In der "getGeofencingRequest"-Methode wird festgelegt, auf welches initiale Event reagiert werden soll und der oben erstellte Geofence wird hinzugefügt. Als initiales Event haben wir "INITIAL\_TRIGGER\_ENTER" gewählt, da es ausgelöst wird, wenn man sich bereits im Bereich befindet und die App startet. Denn erst mit dem Eintrittsevent wird der Button zum Starten der Aufzeichnung freigeschaltet. Das "geofencePendingIntent" definiert die BroadcastReceiver-Klasse, welche bei jedem Event aufgerufen wird.
|
||||
\begin{lstlisting}[language=Kotlin,caption=Setzen der Geofence Trigger]
|
||||
private fun getGeofencingRequest(): GeofencingRequest {
|
||||
return GeofencingRequest.Builder().apply {
|
||||
@ -169,21 +169,21 @@ private val geofencePendingIntent: PendingIntent by lazy {
|
||||
PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
\end{lstlisting}
|
||||
Die \verb|GeofenceBroadcastReceiver|-Klasse definiert, was bei den jeweiligen Events ausgeführt werden soll. In unserem Fall ist dies das Verändern einer boolean Shared-Prefrences-Variable, je nachdem ob der Bereich betreten oder verlassen wurde. Warum diese Art und Weise gewählt wurde lesen Sie in Kapitel \ref{sec:Probleme}. Das Code-Beispiel zeigt die Aktion beim Betreten des Bereichs.
|
||||
Die "GeofenceBroadcastReceiver"-Klasse definiert, was bei den jeweiligen Events ausgeführt werden soll. In unserem Fall ist dies das Verändern einer boolean Shared-Prefrences-Variable, je nachdem ob der Bereich betreten oder verlassen wurde. Warum diese Art und Weise gewählt wurde lesen Sie in Kapitel \ref{sec:Probleme}. Das Code-Beispiel zeigt die Aktion beim Betreten des Bereichs.
|
||||
\begin{lstlisting}[language=Kotlin,caption=Ändern der Shared-Preferences]
|
||||
context!!.getSharedPreferences("LOCATION", Context.MODE_PRIVATE)
|
||||
?.edit()
|
||||
?.putBoolean("ENABLED", true)
|
||||
?.apply()
|
||||
\end{lstlisting}
|
||||
In der \verb|MainActivity| wird ein Listener für diese Shared-Prefrences-Variable definiert. Je nachdem, zu welchem Wert sich die Variable ändert, wird der Start/Stopp-Button freigeschaltet oder gesperrt. Wenn der Benutzer den Bereich verlässt, aber noch aufzeichnet, wird dadurch die Aufzeichnung automatisch gestoppt und gespeichert.
|
||||
In der "MainActivity" wird ein Listener für diese Shared-Prefrences-Variable definiert. Je nachdem, zu welchem Wert sich die Variable ändert, wird der Start/Stopp-Button freigeschaltet oder gesperrt. Wenn der Benutzer den Bereich verlässt, aber noch aufzeichnet, wird dadurch die Aufzeichnung automatisch gestoppt und gespeichert.
|
||||
|
||||
\section{Funktionen der App}
|
||||
Wie oben beschrieben, besteht die Android-App aus vier Activities. Die Register- und Settings-Activity sind aus zeitlichen Gründen ohne Funktion und layouttechnisch nicht überarbeitet. Der Fokus lag stark auf der Main-Activity, die das Kernstück der App darstellt. Im Folgenden die Funktionalitäten der Activities Login und Main.
|
||||
\subsection{Login Screen}
|
||||
In der Abbildung \ref{Abb:login} ist der Login Screen zu sehen. Er besteht aus der Top-Action-Bar mit Logo und App-Name, den Eingabefeldern und zwei Buttons. Alle Komponenten sind aus der Material-Design-Bibliothek.
|
||||
|
||||
Zum Einloggen werden die Daten in die jeweiligen Felder eingegeben. Wenn ein Feld markiert ist, wird das ausgewählte Feld blau umrandet und der Hinweis wird auf die obere Linie verschoben. Das Passwortfeld zeigt nur kurz den eingegebenen Buchstaben an und wird dann zu einem '*', sodass das Passwort nicht offen lesbar ist.
|
||||
Zum Einloggen werden die Daten in die jeweiligen Felder eingegeben. Wenn ein Feld markiert ist, wird das ausgewählte Feld blau umrandet und der Hinweis wird auf die obere Linie verschoben. Das Passwortfeld zeigt nur kurz den eingegebenen Buchstaben an und wird dann zu einem "*", sodass das Passwort nicht offen lesbar ist.
|
||||
|
||||
Der Login-Button sendet die Daten an das Backend und prüft, ob die Daten korrekt sind. Wenn dies der Fall ist, enthält die Antwort das Token, welches in den privaten Speicher abgelegt wird, und die App wechselt zum Hauptbildschirm. War der Login nicht erfolgreich, wird dem Benutzer eine Pop-Up-Meldung angezeigt und nichts weiter unternommen. Mit dem Betätigen des Registrieren-Knopfes wird man auf die Register-Activity weitergeleitet.
|
||||
|
||||
@ -197,18 +197,17 @@ context!!.getSharedPreferences("LOCATION", Context.MODE_PRIVATE)
|
||||
\end{figure}
|
||||
Direkt unter der Top-Action-Bar wird der Benutzer mit dem Vornamen begrüßt (Abb.: \ref{Abb:main}).
|
||||
|
||||
In der Bedienfläche kann der Benutzer den Timetrack-Account auswählen und dessen Details ansehen, seine heutigen Arbeitszeiten ansehen und die Aufzeichnung starten oder stoppen.
|
||||
In der Bedienfläche kann der Benutzer den Timetrack-Account auswählen und dessen Details ansehen, seine heutigen Arbeitszeiten ansehen und die Aufzeichnung starten oder stoppen.\\
|
||||
|
||||
Die Auswahl des Accounts erfolgt über ein Dropdown-Menü. Bei Auswahl wird sofort die zugehörige Beschreibung und die Vergütung angezeigt. Wenn die Aufzeichnung am Laufen ist, wird das Dropdown-Menü ausgeblendet. Das verhindert, dass der Benutzer eine Aktivität für einen anderen Account stoppen kann, als für den, auf dem er sie gestartet hat. Ist für den Benutzer noch kein Account vorhanden, wird "None" im Menü angezeigt und die beiden Felder für Beschreibung und Vergütung werden ausgeblendet.
|
||||
Die Auswahl des Accounts erfolgt über ein Dropdown-Menü. Bei Auswahl wird sofort die zugehörige Beschreibung und die Vergütung angezeigt. Wenn die Aufzeichnung am Laufen ist, wird das Dropdown-Menü ausgeblendet. Das verhindert, dass der Benutzer eine Aktivität für einen anderen Account stoppen kann, als für den, auf dem er sie gestartet hat. Ist für den Benutzer noch kein Account vorhanden, wird "None" im Menü angezeigt und die beiden Felder für Beschreibung und Vergütung werden ausgeblendet.\\
|
||||
|
||||
Für die Anzeige der heutigen Arbeitszeiten haben wir eine RecyclerView verwendet. Das Layout dazu wird in einer extra XML-Datei definiert und mit Daten in einer Adapter-Klasse befüllt. Durch eine Backendabfrage bekommen wir die nötigen Daten dafür. Bei aktiver Aufzeichnung wird ein Element angezeigt mit der Startzeit und der Info, dass das Ende offen ist.
|
||||
\begin{figure}[H]
|
||||
\centering
|
||||
\includegraphics[width=0.4\linewidth]{img/android/main_recording}
|
||||
\caption{Laufende Aufzeichnung}
|
||||
\label{Abb:menu}
|
||||
\end{figure}
|
||||
Der Start-Stop-Button schaltet die Aufzeichnung um, in dem ein Backend-Endpunkt angesprochen wird. In der App haben wir eine boolean-Variable \verb|running| definiert, welche speichert, ob die Aufzeichnung aktiv ist. Anhand ihr wird entschieden, wie der Start-Stop-Button aussieht und ob beim Verlassen des Geofence noch gestoppt werden muss. Der Button ist nicht auswählbar, wenn sich der Nutzer außerhalb seines Arbeitsplatzes befindet und zeigt dies auch an (Abb.: \ref{Abb:outside}). Ist der Nutzer dann im Bereich, wird "Start" angezeigt und der Button ist freigeschaltet. Während der Aufzeichnung trägt der Button die Schrift "Stop". Hat der Nutzer noch keine Geo-Daten für seinen Arbeitsplatz definiert, wird auch das auf dem Button angezeigt.
|
||||
Der Start-Stop-Button schaltet die Aufzeichnung um, in dem ein Backend-Endpunkt angesprochen wird. In der App haben wir eine boolean-Variable "running" definiert, welche speichert, ob die Aufzeichnung aktiv ist. Anhand ihr wird entschieden, wie der Start-Stop-Button aussieht und ob beim Verlassen des Geofence noch gestoppt werden muss. Der Button ist nicht auswählbar, wenn sich der Nutzer außerhalb seines Arbeitsplatzes befindet und zeigt dies auch an (Abb.: \ref{Abb:outside}). Ist der Nutzer dann im Bereich, wird "Start" angezeigt und der Button ist freigeschaltet. Während der Aufzeichnung trägt der Button die Schrift "Stop". Hat der Nutzer noch keine Geo-Daten für seinen Arbeitsplatz definiert, wird auch das auf dem Button angezeigt.
|
||||
\begin{figure}[H]
|
||||
\centering
|
||||
\includegraphics[width=0.4\linewidth]{img/android/btn_outside}
|
||||
@ -228,22 +227,21 @@ context!!.getSharedPreferences("LOCATION", Context.MODE_PRIVATE)
|
||||
\begin{figure}[H]
|
||||
\centering
|
||||
\includegraphics[width=0.4\linewidth]{img/android/btn_no_fence}
|
||||
\caption{Nutzer hat noch keinen Geo-Daten}
|
||||
\caption{Nutzer hat noch keine Geo-Daten}
|
||||
\end{figure}
|
||||
Um versehentliches Stoppen der Aufzeichnung zu verhindern, muss der Nutzer in einem Pop-Up-Dialog seine Aktion bestätigen.
|
||||
\begin{figure}[H]
|
||||
\centering
|
||||
\includegraphics[width=0.4\linewidth]{img/android/confirm_stop}
|
||||
\caption{Bestätigungs Dialog zum stoppen}
|
||||
\label{Abb:menu}
|
||||
\caption{Bestätigungsdialog zum stoppen}
|
||||
\end{figure}
|
||||
|
||||
\section{Probleme und Lösungen}\label{sec:Probleme}
|
||||
Damit die App auch die aktuellste Android Version unterstützt, mussten einige zusätzliche Punkte berücksichtigt werden. Neben der Berechtigung aus Kapitel \ref{subsec:geofence} mussten in der \verb|build.gradle|-Datei Kompilierungsoptionen gesetzt werden.\\
|
||||
Damit die App auch die aktuellste Android Version unterstützt, mussten einige zusätzliche Punkte berücksichtigt werden. Neben der Berechtigung aus Kapitel \ref{subsec:geofence} mussten in der "build.gradle"-Datei Kompilierungsoptionen gesetzt werden.\\
|
||||
|
||||
Zu Beginn wollten wir alle Activities mit Fragments realisieren, sodass es nur eine Activity gibt und alles weitere Fragments sind. Allerdings war es schwieriger, zwischen den Fragments zu wechseln, als in den Tutorials beschrieben. Deshalb sind wir auf reine Activities umgestiegen. Zwischen diesen ist das Hin- und Herschalten deutlich einfacher, es besteht jedoch kein Zugriff auf Elemente der anderen Activities.\\
|
||||
|
||||
Das wurde aber erst dann problematisch, als wir aus der Klasse \verb|GeofenceBroadcastReceiver| eine Methode der \verb|MainActivity| zur Änderung der Oberfläche aufrufen wollten. Das hat den Grund, dass Android nicht sicher sagen kann, dass diese Activity gerade auch aktiv ist. Deshalb haben wir den Weg über die Shared-Preferences gewählt mit einem Listener in der \verb|MainActivity|.\\
|
||||
Das wurde aber erst dann problematisch, als wir aus der Klasse "GeofenceBroadcastReceiver" eine Methode der "MainActivity" zur Änderung der Oberfläche aufrufen wollten. Das hat den Grund, dass Android nicht sicher sagen kann, dass diese Activity gerade auch aktiv ist. Deshalb haben wir den Weg über die Shared-Preferences gewählt mit einem Listener in der "MainActivity".\\
|
||||
|
||||
Initial wollten wir das Token in einer Datenklasse abspeichern, welche beim Einloggen befüllt wird. Dazu müsste allerdings das Objekt oder die Referenz zu jeder anderen Activity übergeben werden. Eine andere Möglichkeit stellen erneut die Shared Preferences dar. Das wäre auch eine gute Lösung gewesen, welche wir aber zu spät entdeckt haben. Deshalb haben wir das Problem mit dem privaten Speicher gelöst. Er ist durch andere Apps und den Benutzer nicht einsehbar, bildet deshalb also kein Sicherheitsrisiko.\\
|
||||
|
||||
|
@ -7,7 +7,7 @@
|
||||
Es wurden im Projektverlauf auch noch diverse Erweiterungen des Frameworks genutzt.
|
||||
\begin{itemize}
|
||||
\item \textbf{org.springframework.boot:spring-boot-starter-web} bringt einen integrierten Tomcat Application Server mit und ermöglicht das Verarbeiten von Webanfragen.
|
||||
\item \textbf{org.springframework.boot:spring-boot-starter-actuator} wird genutzt, um Diagnose-Endpoints freizuschalten.
|
||||
\item \textbf{org.springframework.boot:spring-boot-starter-actuator} wird genutzt, um Endpoints für Diagnose freizuschalten.
|
||||
\item \textbf{org.springframework.boot:spring-boot-starter-data-jpa} bringt alle nötigen Abhängigkeiten, um mit der Java Persistence API Daten in einer Datenbank abzulegen.
|
||||
\item \textbf{org.springframework.boot:spring-boot-starter-data-rest} bietet Möglichkeiten, sehr leicht Datenbank Entitäten als HTTP REST Ressourcen bereitzustellen.
|
||||
\item \textbf{org.springframework.boot:spring-boot-starter-security} wird später zusammen mit der Authentifizierung über JWT genutzt.
|
||||
@ -47,7 +47,7 @@
|
||||
|
||||
Die komplette Klasse ist durch die Lombok Integration sehr klein gehalten. Alles weitere wird durch Annotationen geregelt, einige Beispiele sind hier:
|
||||
\begin{itemize}
|
||||
\item[] \textbf{@Entity} markiert die Klasse als Speicherbar in der Datenbank.
|
||||
\item[] \textbf{@Entity} markiert die Klasse als speicherbar in der Datenbank.
|
||||
\item[] \textbf{@ManyToOne} markiert das Attribut als Fremdschlüssenrelation aus einer anderen Tabelle.
|
||||
\item[] \textbf{@Id} zeichnet den Primärschlüssel der Tabelle aus.
|
||||
\item[] \textbf{@Column} setzt spezielle Attribute für die Spalte in der Datenbank.
|
||||
@ -58,7 +58,7 @@
|
||||
\lstinputlisting[language=Java,caption=TimetrackAccount,firstline=11]{../backend/src/main/java/de/hft/geotime/entities/TimetrackAccount.java}
|
||||
|
||||
\subsection{Sicherheit durch JWT}
|
||||
Da wird die Web App im Laufe des Projekts auch öffentlich in Internet stellen mussten, war eine Art Authentifizierung so gut wie unumgänglich. Damit wir keine Probleme mit Session-Affinity haben, entschieden wir uns für eine Token-Based Authentifizierung. Bei der genauen Implementation handelt es sich hier um das Json Web Token, kurz JWT.
|
||||
Da wird die Web App im Laufe des Projekts auch öffentlich in Internet stellen mussten, war eine Art Authentifizierung so gut wie unumgänglich. Damit wir keine Probleme mit Session-Affinity haben, entschieden wir uns für eine Token-Based Authentifizierung. Bei der genauen Implementation handelt es sich hier um das JSON Web Token, kurz JWT.
|
||||
\begin{figure}[H]
|
||||
\centering
|
||||
\includegraphics[width=\linewidth]{img/backend/jwt.io.png}
|
||||
@ -104,19 +104,19 @@
|
||||
|
||||
Werden nun noch weitere Funktionalitäten in den Repositories benötigt, können diese entweder selbst implementiert werden, oder durch gut ausgewählte Funktionsdefinitionen im Interface der Ressource deklariert werden. Spring kann die Implementierung dann aus dem Namen und den Parametern der Funktion ableiten. Als unser Maximalbeispiel dient hier das RecordRepository.
|
||||
|
||||
\lstinputlisting[language=Java,linerange={14-27,38-41,47-54},caption=RecordRepository,label=code:record-repository]{../backend/src/main/java/de/hft/geotime/repositories/RecordRepository.java}
|
||||
\lstinputlisting[language=Java,linerange={14-27,38-41,47-58},caption=RecordRepository,label=code:record-repository]{../backend/src/main/java/de/hft/geotime/repositories/RecordRepository.java}
|
||||
|
||||
In diesem Repository befinden sich diverse verschiedene Methoden, wie Datenoperationen definiert werden können, ohne dass sie aktiv implementiert werden müssen. Es beginnt mit der Funktion "findAllByStartdateBetween". Dieser Name kann direkt als Java Hibernate Statement interpretiert werden und nimmt als Parameter zwei Datumsangaben entgegen und eine Page. Die zwei Datumsangaben werden aus dem Schlüsselwort "Between" abgeleitet. Damit es sich aber um echt vergleichbare Daten handelt, müssen diese nach einem bestimmten Schema geparsed werden. Dieses Schema ist in der "DateTimeFormat" Annotation angegeben. Als Rückgabe liefert diese Funktion dann eine Menge aller Einträge zwischen diesen Daten.
|
||||
In diesem Repository befinden sich diverse verschiedene Methoden, wie Datenoperationen, die definiert werden können, ohne das sie aktiv implementiert werden müssen. Es beginnt mit der Funktion "findAllByStartdateBetween". Dieser Name kann direkt als Java Hibernate Statement interpretiert werden und nimmt als Parameter zwei Datumsangaben entgegen und eine Page. Die zwei Datumsangaben werden aus dem Schlüsselwort "Between" abgeleitet. Damit es sich aber um echt vergleichbare Daten handelt, müssen diese nach einem bestimmten Schema geparsed werden. Dieses Schema ist in der "DateTimeFormat" Annotation angegeben. Als Rückgabe liefert diese Funktion dann eine Menge aller Einträge zwischen diesen Daten.\\
|
||||
|
||||
Die nächste Funktion funktioniert nun ähnlich, nur dass dort über Eigenschaften mehrerer verlinkter Objekte gegangen werden kann. "findAllBy" ist wieder das selbe wie oben und zeigt an, dass eine Liste von Ergebnissen zurückgeliefert wird, aber "Account\_User\_Username" bedeutet nun folgendes: "Gehe zum Account des Records, dann zum User dieses Accounts und von diesem User dann den Username". Der gefundene Username wird dann mit dem Parameter der Funktion verglichen und die Ergebnisse entsprechend gefiltert. Weiter zeigt das "And" eine Verkettung eines weiteren Ausdrucks an. So können auch relativ komplexe Abfragen automatisch implementiert werden.
|
||||
Die nächste Funktion funktioniert nun ähnlich, nur dass dort über Eigenschaften mehrerer verlinkter Objekte gegangen werden kann. "findAllBy" ist wieder das selbe wie oben und zeigt an, dass eine Liste von Ergebnissen zurückgeliefert wird, aber "Account\_User\_Username" bedeutet nun folgendes: "Gehe zum Account des Records, dann zum User dieses Accounts und von diesem User dann den Username". Der gefundene Username wird dann mit dem Parameter der Funktion verglichen und die Ergebnisse entsprechend gefiltert. Weiter zeigt das "And" eine Verkettung eines weiteren Ausdrucks an. So können auch relativ komplexe Abfragen automatisch implementiert werden.\\
|
||||
|
||||
Reicht allerdings die obige Syntax nicht mehr aus, kann auch direkt eine Hibernate Abfrage über die "@Query" Annotation angegeben werden. Der Name der Funktion ist dann nicht mehr relevant für die Implementation, sondern nur noch für den Pfad, unter dem die Funktion später zu erreichen ist. Die Query an der "today" Funktion bietet nun die Möglichkeit, alle Einträge in der Records Tabelle für den aktuell anfragenden User zu bestimmen. Zusätzlich wird der Zeitraum noch auf den aktuellen Tag eingeschränkt, daher ergab sich auch der passende Name "today" für die Funktion. Der Nutzer wird automatisch über die "principal" Variable in der Abfrage eingefügt. Der Pricipal wird gesetzt, sobald der Authentication Filter den User erfolgreich eingeloggt hat. Weiter wird der aktuelle Tag über die Datenbankvariable "current\_date" abgefragt.
|
||||
Reicht allerdings die obige Syntax nicht mehr aus, kann auch direkt eine Hibernate Abfrage über die "@Query" Annotation angegeben werden. Der Name der Funktion ist dann nicht mehr relevant für die Implementation, sondern nur noch für den Pfad, unter dem die Funktion später zu erreichen ist. Die Query an der "today" Funktion bietet nun die Möglichkeit, alle Einträge in der Records Tabelle für den aktuell anfragenden User zu bestimmen. Zusätzlich wird der Zeitraum noch auf den aktuellen Tag eingeschränkt, daher ergab sich auch der passende Name "today" für die Funktion. Der Nutzer wird automatisch über die "principal" Variable in der Abfrage eingefügt. Der Pricipal wird gesetzt, sobald der Authentication Filter den User erfolgreich eingeloggt hat. Weiter wird der aktuelle Tag über die Datenbankvariable "current\_date" abgefragt.\\
|
||||
|
||||
Zuletzt kann auch nach Standard SQL Werten wie "null" oder "not null" gefragt werden. Zu sehen ist dies in der zuletzt dargestellten Funktion.
|
||||
Zuletzt kann auch nach Standard SQL Werten wie "null" oder "not null" gefragt werden. Zu sehen ist dies in der zuletzt dargestellten Funktion.\\
|
||||
|
||||
Die Datei ist nicht vollständig abgedruckt, sondern nur ausschnittsweise, um die Grundkonzepte zu erläutern.
|
||||
\subsection{Projections}
|
||||
Projections bieten nun noch weitere Möglichkeiten, Daten vor der Rückgabe noch zu transformieren und gegebenenfalls mit Zusatzdaten anzureichern. Eine Projektion ist ebenfalls durch ein Interface definiert und bring vor allem dann Vorteile, wenn mehrere Ressourcen gebündelt angefragt werden müssen, um beispielsweise eine Übersicht zu erstellen.
|
||||
Projections bieten nun noch weitere Möglichkeiten, Daten vor der Rückgabe noch zu transformieren und gegebenenfalls mit Zusatzdaten anzureichern. Eine Projektion ist ebenfalls durch ein Interface definiert und bringt vor allem dann Vorteile, wenn mehrere Ressourcen gebündelt angefragt werden müssen, um beispielsweise eine Übersicht zu erstellen.
|
||||
|
||||
\lstinputlisting[language=Java,firstline=9,caption=RecordOverviewProjection,label=code:record-overview-projection]{../backend/src/main/java/de/hft/geotime/entities/projections/RecordOverviewProjection.java}
|
||||
|
||||
@ -300,16 +300,16 @@
|
||||
\item \textbf{"\_links"} zeigt weiterführende Links zu Ressourcen oder Informationen zum Paging an.
|
||||
\item \textbf{"\_embedded"} enthält die Nutzdaten zur entsprechenden Ressource, aber auch weitere Einbettungen zu Sub-Ressourcen.
|
||||
\end{itemize}
|
||||
Zusätzlich dazu nutzt Spring bei der Generation der Repositories auch Teile der "Hypermedia as the Engine of Application State", kurz HATEOAS, Spezifikation. Das Listing \ref{code:main-route-api} zeigt hierfür den zusätzlichen Endpoint "profile". Unter diesem sind viele Spezifikationen zu finden, wie alle anderen Routen auf bestimmte Daten reagieren und auch antworten.
|
||||
Zusätzlich dazu nutzt Spring bei der Generierung der Repositories auch Teile der "Hypermedia as the Engine of Application State", kurz HATEOAS, Spezifikation. Das Listing \ref{code:main-route-api} zeigt hierfür den zusätzlichen Endpoint "profile". Unter diesem sind viele Spezifikationen zu finden, wie alle anderen Routen auf bestimmte Daten reagieren und auch antworten.
|
||||
|
||||
Der "profile" Endpoint zeigt zusätzlich noch alle Projektionen an, die auf eine bestimmte Ressource angewendet werden können. Der Name der Projektion wird dann durch den URL-Parameter "projection=" angehängt.
|
||||
|
||||
Zuletzt gilt es noch zu erwähnen, dass alle Ressourcen Paging und Sorting unterstützen. Paging ist besonders bei Web APIs wichtig, da die Geschwindigkeit sehr stark von der Menge der übertragenen Daten abhängt. Wenn eine Ressource immer alle Daten zurückliefern würde, würde dies bei mehreren hundert Einträgen sicher noch funktionieren. Aber sobald die Zahl der Einträge deutlich höher wird, muss abgeschnitten und aufgeteilt werden. Unsere Standard Seitengröße ist auf 20 Einträge gesetzt. Weiter enthält die Antwort des Servers durch die HAL Integration immer Links zur aktuellen, nächsten und vorherigen Seite als Link. Sorting wird ebenfalls unterstützt. Es kann nach jedem Feld im zurückgegebenen JSON sortiert werden, auch die Richtung ist spezifizierbar.
|
||||
\section{Probleme und Lösungen}
|
||||
\subsection{Einlesen in Spring}
|
||||
Spring ist ein sehr komplexes Framework, weshalb es manchmal wirklich sehr schwierig war, Fehler zu verstehen, und die Gründe dahinter zu verstehen. Solange man sich aber an viele der Best-Practicies von Spring hält, ist es überhaupt nicht schwer, in relativ kurzer Zeit auch sehr komplexe APIs zu bauen. Durch die enorme Menge an Dokumentation und auch Hilfe aus der Community sowie Techtalks können viele Probleme leicht gelöst werden.
|
||||
Spring ist ein sehr komplexes Framework, weshalb es manchmal wirklich sehr schwierig war, Fehler zu verstehen, und die Gründe dahinter zu verstehen. Solange man sich aber an viele der Best-Practices von Spring hält, ist es überhaupt nicht schwer, in relativ kurzer Zeit auch sehr komplexe APIs zu bauen. Durch die enorme Menge an Dokumentation und auch Hilfe aus der Community sowie Techtalks können viele Probleme leicht gelöst werden.
|
||||
\subsection{Änderungen an den Endpoints}
|
||||
Es mussten anfangs viele Endpoints immer wieder umdefiniert werden, da sie nicht Best-Practicies entsprochen haben oder nicht performant funktioniert haben. Dies wurde später aber immer einfacher, wenn man sich an die Denkweise einer REST-API gewöhnt hat. Auch zwei Wege Links zwischen Ressourcen waren bei uns nicht möglich, da sie zu Endlosrekursionen führten. Später wurde aber auch klar, das dies überhaupt nicht gewünscht ist.
|
||||
Es mussten anfangs viele Endpoints immer wieder umdefiniert werden, da sie nicht Best-Practices entsprochen haben oder nicht performant funktioniert haben. Dies wurde später aber immer einfacher, wenn man sich an die Denkweise einer REST-API gewöhnt hat. Auch zwei Wege Links zwischen Ressourcen waren bei uns nicht möglich, da sie zu Endlosrekursionen führten. Später wurde aber auch klar, das dies überhaupt nicht gewünscht ist.
|
||||
\subsection{Probleme mit MariaDB}
|
||||
Zu Beginn haben wir für das Docker-Image der Datenbank den "latest"-Tag benutzt. Dies war möglich, da wir keinerlei eigene Konfiguration der Datenbank und deren Tabellen vorgenommen haben. Allerdings wurde Mitte April die neue LTS-Version von Ubuntu veröffentlicht und damit auch das Basisimage von MariaDB angepasst. Durch Änderungen in Ubuntu 20.04 funktionierten nun gewisse Datenbankfunktionen nicht mehr ordnungsgemäß. Als Lösung kam dann nur ein Downgrade auf eine ältere Version in Frage.
|
||||
\section{Deployment}
|
||||
|
@ -1,8 +1,8 @@
|
||||
\chapter{Einleitung}
|
||||
Im Rahmen der Vorlesung Ubiquitous Computing ist eine Projektarbeit mit dem Thema der Allgegenwärtigkeit von Computern vorgesehen.
|
||||
Im Rahmen der Vorlesung Ubiquitous Computing ist eine Projektarbeit mit dem Thema der Allgegenwärtigkeit von Computern vorgesehen.\\
|
||||
|
||||
Dazu fanden wir uns als Gruppe zusammen und sammelten unsere Ideen für Projektthemen. Letztendlich setzte sich der Timetracker mit Geofence gegen eine Wetterstation und eine Augmented Reality App durch.
|
||||
Dazu fanden wir uns als Gruppe zusammen und sammelten unsere Ideen für Projektthemen. Letztendlich setzte sich der Timetracker mit Geofence gegen eine Wetterstation und eine Augmented Reality App durch.\\
|
||||
|
||||
Die Idee der Timetrack-Anwendung ist, dass ein Arbeitnehmer erst dann mit der Aufzeichnung seiner Arbeitszeit beginnen kann, wenn er sich am Arbeitsort befindet. Wenn er diesen verlässt, wird seine Aufzeichnung beendet. Dabei hat er mehrere Zeitkonten zur Auswahl, auf die er seine Arbeitszeit verbuchen kann.
|
||||
Die Idee der Timetrack-Anwendung ist, dass ein Arbeitnehmer erst dann mit der Aufzeichnung seiner Arbeitszeit beginnen kann, wenn er sich am Arbeitsort befindet. Wenn er diesen verlässt, wird seine Aufzeichnung beendet. Dabei hat er mehrere Zeitkonten zur Auswahl, auf die er seine Arbeitszeit verbuchen kann.\\
|
||||
|
||||
Das Projekt ist in drei Teile unterteilt: Backend, Web-Frontend und Android-App. Entsprechend dem Aufwand arbeiteten am Web-Frontend zwei Studenten, am Backend und der Android-App jeweils ein Student. Die Android-App soll lediglich die Basisfunktionalitäten bieten, wie Starten und Stoppen der Aufzeichnung für einen gewählten Account, wenn man sich am Arbeitsort befindet, sowie log in und log out. Im Web-Frontend sollen verschiedenste Statistiken angezeigt werden, sowie Adminfunktionaltäten, um Accounts zu editieren, neue Einträge hinzufügen, Geodaten für den Arbeitnehmer setzen und Benutzer löschen. Das Backend kommuniziert mit der Datenbank, sichert die Authentifizierung der Benutzer und stellt den Oberflächen Endpoints zur Verfügung.
|
@ -1,17 +1,17 @@
|
||||
\chapter{Entwicklungsumgebung}
|
||||
Da wir uns für eine Full-Stack Application entschieden haben, war es wichtig, gleich zu Beginn die Entwicklungsumgebung so robust wie möglich zu gestalten. Weiter sollte das ganze Setup einfach unter Versionskontrolle gestellt werden können, um überall reproduzierbar zu sein.
|
||||
\section{Versionsverwaltung}
|
||||
Für die Versionsverwaltung haben wir das aktuell am weitesten verbreitete Tool Git benutzt. Dies war notwendig, damit wir unabhängig voneinander arbeiten können. Das Repository ist öffentlich auf der Plattform GitLab einsehbar. Die Adresse lautet \url{https://gitlab.com/marcel.schwarz/2020ss-qbc-geofence-timetracking}
|
||||
Für die Versionsverwaltung haben wir das aktuell am weitesten verbreitete Tool Git benutzt. Dies war notwendig, damit wir unabhängig voneinander arbeiten können. Das Repository ist öffentlich auf der Plattform GitLab einsehbar. Die Adresse lautet \url{https://gitlab.com/marcel.schwarz/2020ss-qbc-geofence-timetracking}.
|
||||
\subsection{GitLab}
|
||||
Die Entscheidung für GitLab fiel aber nicht ohne Grund. GitLab bietet auch sehr ausgeprägte Projektplanungsmöglichkeiten, die die Kollaboration sehr vereinfachen. Dazu zählen:
|
||||
\begin{itemize}
|
||||
\item Issues. In den Issues werden alle Aufgaben für das Projekt abgelegt, die noch erledigt werden müssen oder eine weitere Betrachtung benötigen. Auch Bugfixes werden dort angelegt.
|
||||
\item Issues werden in Merge Requests bearbeitet. Diese Merge Requests können genutzt werden, um über Code zu diskutieren und gegebenenfalls zu verbessern.
|
||||
\item Code Ownership. Da wir die Teile der Anwendung nach Personen aufgeteilt haben, gibt es für jeden Teil der Anwendung mindestens ein Teammitglied, welches sich besonders gut mit diesen Themen auskennt. Diese Teammitglieder haben dadurch auch die Codeownership für diesen Teil des Codes.
|
||||
\item Merge Request Approval. Wenn ein Issue mehrere Teile der Anwendung ändert, muss der jeweilige Codeowner dieses Teils dem Merge Request ebenfalls zustimmen. Ein Beispiel wäre hier die Implementation: "ein Datum im Frontend ändern", was aber zusätzlich die Anpassung des Datumsformates im Backend erfordert. Bei diesem Merge Request muss dann sowohl ein Codeowner des Frontends als auch des Backends zustimmen. Diese zusätzliche Sicherheitsschicht dient der Stabilität des Master Branches und der allgemeinen Codequalität.
|
||||
\item Code-Ownership. Da wir die Teile der Anwendung nach Personen aufgeteilt haben, gibt es für jeden Teil der Anwendung mindestens ein Teammitglied, welches sich besonders gut mit diesen Themen auskennt. Diese Teammitglieder haben dadurch auch die Code-Ownership für diesen Teil des Codes.
|
||||
\item Merge Request Approval. Wenn ein Issue mehrere Teile der Anwendung ändert, muss der jeweilige Codeowner dieses Teils dem Merge Request ebenfalls zustimmen. Ein Beispiel wäre hier die Implementation: "ein Datum im Frontend ändern", was aber zusätzlich die Anpassung des Datumsformates im Backend erfordert. Bei diesem Merge Request muss dann sowohl ein Codeowner des Frontends als auch des Backends zustimmen. Diese zusätzliche Sicherheitsschicht dient der Stabilität des Master-Branches und der allgemeinen Codequalität.
|
||||
\end{itemize}
|
||||
\subsection{Umgang mit Issues}
|
||||
Die Issues sind die komplette Dokumentation der erledigten Aufgaben während des Projekts. In ihnen können alle Informationen abgelegt werden, die relevant sind. Beispiele sind hier Bugfixes, Features aber auch Definitionen von Design und Farbschema, die die Zustimmung mehrerer Gruppenmitglieder benötigen. Auch API Definitionen und Alignment-Meetings gehören bei uns dazu.
|
||||
Die Issues sind die komplette Dokumentation der erledigten Aufgaben während des Projekts. In ihnen können alle Informationen abgelegt werden, die relevant sind. Beispiele sind hier Bugfixes, Features aber auch Definitionen von Design und Farbschema, die die Zustimmung mehrerer Gruppenmitglieder benötigen. Auch API-Definitionen und Alignment-Meetings gehören bei uns dazu.
|
||||
|
||||
Der Lebenszyklus eines Issues sieht wie folgt aus:
|
||||
\begin{enumerate}
|
||||
@ -32,7 +32,7 @@
|
||||
|
||||
\lstinputlisting[language=docker, caption=Dockerfile Backend]{../backend/Dockerfile}
|
||||
\lstinputlisting[language=docker, caption=Dockerfile Frontend]{../frontend/Dockerfile}
|
||||
\lstinputlisting[language=docker, caption=Dockerfile Android]{../android/Dockerfile}
|
||||
\lstinputlisting[language=docker, caption=Dockerfile Android, basicstyle={\footnotesize\ttfamily}]{../android/Dockerfile}
|
||||
|
||||
Zu erwähnen sind noch einige Besonderheiten. Da wir ein Programm entwickeln, was sich mit Zeiterfassung beschäftigt, ist es wichtig im Backend die Zeitzone des Images zu setzen, da sonst immer die UTC Zeitzone als Standard angenommen wird. Selbiges gilt auch für die SQL Datenbank, welche mit MariaDB umgesetzt ist.
|
||||
\section{Docker-Compose}
|
||||
@ -42,6 +42,6 @@
|
||||
\begin{lstlisting}[language=bash]
|
||||
docker-compose up --build -d
|
||||
\end{lstlisting}
|
||||
im Rootverzeichnis der Anwendung ausgeführt werden. Es sei noch zu erwähnen, dass beim allerersten Start die Datenbankinitialisierung etwas länger brauchen kann und deshalb das backend mehrere Versuche braucht, bis die Verbindung aufgebaut werden kann.
|
||||
im Root-Verzeichnis der Anwendung ausgeführt werden. Es sei noch zu erwähnen, dass beim allerersten Start die Datenbankinitialisierung etwas länger brauchen kann und deshalb das backend mehrere Versuche braucht, bis die Verbindung aufgebaut werden kann.
|
||||
\section{Infrastruktur}
|
||||
Die Infrastruktur, auf dem die Anwendung zur Zeit bereitgestellt ist, ist ein kleiner Linux Server bei Strato. Dieser Server hat ebenfalls eine Docker-Compose Installation und läuft auf Ubuntu 20.04 LTS. Gestartet wird es dann exakt gleich, wie im obigen Abschnitt erklärt. Natürlich ist Docker-Compose kein Deployment, welches in Produktion verwendet werden sollte, aber es reicht aktuell für unsere Zwecke aus. Nichts desto trotz ist die Anwendung aber auf eine sehr viel größere Skalierung bestens vorbereitet. Durch die Containerisierung ist unsere Anwendung komplett entkoppelt und könnte somit unabhängig skalieren. Einzig die SQL Datenbank müsste als Container entfernt werden und in ein eigenes Deployment verschoben werden.
|
@ -1,19 +1,256 @@
|
||||
\chapter{Web-Frontend}
|
||||
\section{Technologiebeschreibung}
|
||||
\subsection{Vuejs}
|
||||
\subsection{Vue.js}
|
||||
Vue.js\footnote{\url{https://vuejs.org/}} ist ein Javascript Framework, welches den Aufbau von Frontend-Anwendungen erleichtert. Ein Hauptmerkmal hierbei ist die Kapselung der einzelnen Elemente in Komponenten, welche ihren eigenen HTML, Javascript und CSS Code enthalten. Eine Komponente kann mehrere andere Komponenten einbinden, sowie diesen Daten mitgeben. Eingebundene Komponenten können an die übergeordnete Komponente Daten senden.
|
||||
\subsection{Vuetify}
|
||||
Vuetify\footnote{\url{https://vuetifyjs.com/de-DE/}} ist ein Designframework für Vue.js, das viele Elemente wie Menüleisten, Buttons und Dialogfenster bereitstellt. Ein bekanntes äquivalentes Framework ist Bootstrap. Das Designschema von Vuetify ist an Googles Material Design angelehnt. Nach Installation können die Elemente sehr einfach eingebunden und verwendet werden.
|
||||
\section{Farbschema und Designsprache}
|
||||
Wir haben uns für die, von Google entwickelte, Designsprache "Material Design" entschieden. Diese zeichnet sich durch ihre kartenartigen Flächen und dem Gestaltungsstil Flat Design aus. Verwendet werden auch viele Schatten, um die materialistisch-physikalische Darstellung zu erzeugen.
|
||||
|
||||
In unserem eigens entwickelten Farbschema haben wir uns für ein dunkles Thema mit Blau als Hauptfarbe entschieden.
|
||||
\begin{figure}[H]
|
||||
\centering
|
||||
\includegraphics[width=\linewidth/2]{img/frontend/color_sceme.png}
|
||||
\caption{Farbschema}
|
||||
\end{figure}
|
||||
Unser Logo wurde im abgestimmten Farbschema umgesetzt und stellt die Kombination zwischen einer Stoppuhr und einem Kompass dar. So verbindet das Logo Zeit und Ort, welche bei der Verwendung unseres Produkts eine wichtige Rolle spielen.
|
||||
\begin{figure}[H]
|
||||
\centering
|
||||
\includegraphics[width=\linewidth/3]{img/frontend/logo_dark_gt.png}
|
||||
\caption{Logo unserer Anwendung}
|
||||
\end{figure}
|
||||
|
||||
\section{Umsetzung}
|
||||
\subsection{Einarbeitung}
|
||||
Zur Einarbeitung haben wir den Vue JS Crash Course\footnote{\url{https://www.youtube.com/watch?v=Wy9q22isx3U}} von Traversy Media genutzt. Dieser ist kostenlos auf YouTube zu finden.
|
||||
\subsection{Arbeit mit Dummy-Daten}
|
||||
Zur Erstellung der Listen und Diagramme haben wir häufig Dummy-Daten verwendet, um die Funktionalität im Frontend unabhängig vom Backend zu entwickeln. Die Dummy-Daten haben wir im jeweiligen Vue Component wie folgt angelegt:
|
||||
\begin{lstlisting}[language=JavaScript,caption=Dummy-Daten]
|
||||
<script>
|
||||
...
|
||||
export default {
|
||||
...
|
||||
data() {
|
||||
return {
|
||||
timeRecords: [
|
||||
{
|
||||
id: 1,
|
||||
start: "25.04.2020 / 8:00",
|
||||
end: "25.04.2020 / 13:00",
|
||||
time: "5:00",
|
||||
type: "Paid"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
start: "25.04.2020 / 13:00",
|
||||
end: "25.04.2020 / 14:00",
|
||||
time: "1:00",
|
||||
type: "Lunch"
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
start: "25.04.2020 / 14:00 ",
|
||||
end: "25.04.2020 / 16:30",
|
||||
time: "2:30",
|
||||
type: "Paid"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
...
|
||||
}
|
||||
</script>
|
||||
\end{lstlisting}
|
||||
Durch Verwendung der Dummy-Daten war es ebenso möglich, Funktionsaufrufe zum Löschen oder Bearbeiten von Daten zu testen, ohne persistente Veränderungen an den Daten auszulösen. Durch neu laden der Seite sind die Dummy-Daten wiederhergestellt. Bei der Erstellung der Diagramme waren die Dummy-Daten ebenfalls wichtig, so konnten Formatierungsfunktionen für die Zeitanzeige getestet werden. Ebenfalls konnte so die optimale Größe und Anordnung der Diagramme bestimmt werden.\\
|
||||
|
||||
Durch die Verwendung von Dummy-Daten war der Umstieg auf die Livedaten nicht allzu schwer. Die Dummy-Daten konnten bei Anbindung an die Datenbank reibungslos durch Live-Daten aus der Datenbank ersetzt werden.
|
||||
|
||||
\subsection{Authentifizierung}
|
||||
Wie schon im Backend beschrieben wurde, haben wir zur Authenfizierung JSON Web Token benutzt. Beim Login wurde das Token abgeholt und in den Sessionstorage geschrieben. Wir haben uns für den Sessionstorage entschieden, weil dieser beim Schließen des Browsertabs automatisch gelöscht wird. Der Logout Button entfernt ebenso das Token aus dem Storage.
|
||||
\subsection{Abrufen der Daten in Listen}
|
||||
Zum Abrufen der Daten nutzen wir "XMLHttpRequests". Diese geben vom Backend ein JSON Objekt zurück. Dies ermöglicht es uns, die JSON Funktionen von Java Script zu nutzen.
|
||||
\begin{lstlisting}[language=JavaScript,caption=Get Request]
|
||||
var xhttp = new XMLHttpRequest();
|
||||
var today;
|
||||
xhttp.onreadystatechange = function() {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
today = JSON.parse(xhttp.responseText);
|
||||
today = today._embedded.records;
|
||||
}
|
||||
};
|
||||
xhttp.open("GET", baseUri + "/records/search/today", false);
|
||||
xhttp.setRequestHeader("Authorization", sessionStorage.getItem("jwt"));
|
||||
xhttp.send(null);
|
||||
\end{lstlisting}
|
||||
\subsection{Ändern und Hinzufügen von Daten}
|
||||
Zum Ändern und Hinzufügen von Daten haben wir ebenfalls "XMLHttpRequests" genutzt. Zum Hinzufügen wurden Post Requests gesendet, zum Ändern Patch Requests.
|
||||
\begin{lstlisting}[language=JavaScript, caption=Post Request]
|
||||
xhttp.open("Post", baseUri + path, false);
|
||||
|
||||
xhttp.setRequestHeader("Authorization", sessionStorage.getItem("jwt"));
|
||||
|
||||
xhttp.send(JSONData);
|
||||
\end{lstlisting}
|
||||
\begin{lstlisting}[language=JavaScript, caption=Patch Request]
|
||||
xhttp.open("PATCH", baseUri + path, false);
|
||||
|
||||
xhttp.setRequestHeader("Authorization", sessionStorage.getItem("jwt"));
|
||||
|
||||
xhttp.send(JSONData);
|
||||
\end{lstlisting}
|
||||
\subsection{Auswertung in Diagrammen}
|
||||
\subsection{Administrator Funktionalitäten}
|
||||
Jeder Benutzer kann seine Daten in einer Übersicht zusammengefasst betrachten, hier verwenden wir folgende Diagramme:
|
||||
\begin{itemize}
|
||||
\item Kreisdiagramme
|
||||
\begin{itemize}
|
||||
\item Verhältnis von Arbeitszeit zu Pausenzeit von allen Accounts des Benutzers.
|
||||
\item Verhältnis der Arbeitszeit je Timetrack Account des Benutzers mit Angabe des Gesamtverdienstes.
|
||||
\item Verhältnis des Verdienstes je Timetrack Account des Benutzers.
|
||||
\end{itemize}
|
||||
\item Säulendiagramme
|
||||
\begin{itemize}
|
||||
\item Übersicht über die Letzten 7 Tage mit Arbeits- und Pausenzeit.
|
||||
\item Übersicht über die Letzten 30 Tage mit Arbeits- und Pausenzeit.
|
||||
\end{itemize}
|
||||
\end{itemize}
|
||||
Um Diagramme verwenden zu können, haben wir das Framework Apexcharts\footnote{\url{https://apexcharts.com/}} eingebunden, welches es ermöglicht, konfigurierbare Diagramme einzufügen.
|
||||
Die Konfiguration des Säulendiagramms für die Ansicht der letzten 30 Tage ist nachfolgend dargestellt.
|
||||
\begin{lstlisting}[language=JavaScript, caption=Konfiguration Säulendiagramm]
|
||||
<script>
|
||||
...
|
||||
export default {
|
||||
...
|
||||
data() {
|
||||
return {
|
||||
series: [
|
||||
{
|
||||
name: "Working Hours",
|
||||
data: []
|
||||
},
|
||||
{
|
||||
name: "Pause Hours",
|
||||
data: []
|
||||
}
|
||||
],
|
||||
chartOptions: {
|
||||
chart: {
|
||||
type: "bar",
|
||||
stacked: true,
|
||||
background: "#202020",
|
||||
toolbar: {
|
||||
...
|
||||
}
|
||||
},
|
||||
colors: ["#0096ff", "#e21d1f", "#546E7A", "#E91E63", "#FF9800"],
|
||||
...
|
||||
plotOptions: {
|
||||
bar: {
|
||||
horizontal: false,
|
||||
columnWidth: "50%"
|
||||
}
|
||||
},
|
||||
xaxis: {
|
||||
type: "datetime",
|
||||
categories: []
|
||||
},
|
||||
yaxis: {
|
||||
labels: formatter: function(value) {
|
||||
...
|
||||
return hours + ":" + minutes + ":" + seconds;
|
||||
}
|
||||
},
|
||||
...
|
||||
}
|
||||
};
|
||||
},
|
||||
\end{lstlisting}
|
||||
|
||||
Für die Kreisdiagramme war es notwendig, alle Zeiteinträge abzuholen, die Zeiten zu addieren und in die Datenfelder des Diagramms zu schreiben. Bei dem Kreisdiagramm, das die Accounts darstellt, war es notwendig, die Timetrack-Accounts des Benutzers abzufragen und für jeden ein Feld der addierten Zeit anzulegen und den Lohn in einer Variablen abzulegen.
|
||||
\begin{lstlisting}[language=JavaScript, caption=Zuordnung der Zeit zu den Timetrack Accounts]
|
||||
for (let index = 0; index < records.length; index++){
|
||||
var record = records[index];
|
||||
for (let indexAccs = 0; indexAccs < this.chartOptions.labels.length; indexAccs++) {
|
||||
if (record.account == this.chartOptions.labels[indexAccs] && record.type == "PAID") {
|
||||
this.series[indexAccs] += record.duration;
|
||||
}
|
||||
}
|
||||
}
|
||||
\end{lstlisting}
|
||||
|
||||
Ebenfalls war es notwendig, eine Funktion zu erstellen, die den gesamten Lohn des jeweiligen Accounts nach den ermittelten Stunden berechnet.\\
|
||||
|
||||
Bei den Säulendiagrammen müssen lediglich die nötigen Zeiteinträge beim zuständigen Endpoint angefragt werden. Dieser Endpoint liefert alle Einträge zwischen einem Startdatum und einem Enddatum. Hier wird immer das aktuelle Datum verwendet und die Zeitspanne entsprechend zurückgerechnet.\\
|
||||
|
||||
Um riesige Anfragen zu verhindern wird Paging verwendet, das heißt, es werden so oft 50 Einträge angefragt, bis die letzte Seite erreicht ist.
|
||||
|
||||
\subsection{Administrator Funktionalitäten}\label{subsection:frontend:admin}
|
||||
Ein Administrator hat die Möglichkeit zur vollen Nutzerverwaltung. Er kann Nutzer löschen, und bearbeiten. Als Bearbeitungsmöglichkeiten hat er die Accountverwaltung von Nutzern, das Setzen der Arbeitslocation für einen Nutzer und das Ändern des Namens.
|
||||
\section{Funktionen der Website}
|
||||
\subsection{Home}
|
||||
Die Home Seite hat zwei Ansichten. Wenn kein User angemeldet ist, sieht man lediglich einen Wilkommensgruß und hat die Möglichkeit, sich anzumelden. Wenn man angemeldet ist, sieht man seine persönlichen Informationen, die einem zugeordnete Abeitslocation, die Tagesarbeitszeit und die eigenen Accounts.
|
||||
\begin{figure}[H]
|
||||
\centering
|
||||
\includegraphics[width=\linewidth]{img/frontend/home.PNG}
|
||||
\caption{Home eingeloggt}
|
||||
\end{figure}
|
||||
\begin{figure}[H]
|
||||
\centering
|
||||
\includegraphics[width=\linewidth]{img/frontend/HomeLoggedOut.PNG}
|
||||
\caption{Home ausgeloggt}
|
||||
\end{figure}
|
||||
|
||||
\subsection{Time Records}
|
||||
\subsection{...}
|
||||
\subsection{Probleme und Lösungen}
|
||||
\section{Deployment}
|
||||
Auf der Time Records Seite kann man die eigenen Arbeitszeiten einsehen. Außerdem hat man die Möglichkeit, fehlerhafte Einträge zu verbessern oder zu löschen, indem man über den Stift hovert. Neue Einträge können erstellt werden, indem man den "+"-Button am Ende der Seite anklickt.
|
||||
\begin{figure}[H]
|
||||
\centering
|
||||
\includegraphics[width=\linewidth]{img/frontend/timerecords.PNG}
|
||||
\caption{Time Records}
|
||||
\end{figure}
|
||||
|
||||
\subsection{Statistics}
|
||||
Auf der Statistics Seite sind die Daten der Time Records übersichtlich ausgewertet. Hier werden zwei verschiedene Diagrammtypen eingesetzt, um dem Benutzer eine bestmögliche Auswertung seiner Zeiteinträge zu bieten.
|
||||
\begin{figure}[H]
|
||||
\centering
|
||||
\includegraphics[width=\linewidth]{img/frontend/statistics_pie.PNG}
|
||||
\caption{Kreisdiagramme}
|
||||
\end{figure}
|
||||
\begin{figure}[H]
|
||||
\centering
|
||||
\includegraphics[width=\linewidth]{img/frontend/statistics_column.PNG}
|
||||
\caption{Säulendiagramme}
|
||||
\end{figure}
|
||||
\subsection{Accounts}
|
||||
Die Accouts Seite bietet Möglichkeiten, um eigene Accounts einzusehen und zu verwalten. Es ist möglich, neue Accounts hinzuzufügen und bestehende Accounts zu löschen oder anzupassen.
|
||||
|
||||
\begin{figure}[H]
|
||||
\centering
|
||||
\includegraphics[width=\linewidth]{img/frontend/accounts.PNG}
|
||||
\caption{Accounts}
|
||||
\end{figure}
|
||||
|
||||
\subsection{Admin}
|
||||
Die Admin Seite bietet einem die Möglichkeiten, welche in \ref{subsection:frontend:admin} beschrieben werden. Um die einzelnen Verwaltungsmöglichkeiten zu sehen, reicht es über den Stift zu hovern. Das linke Zeichen (rote Mülltonne) löscht den jeweiligen Nutzer, das mittlere (grünes Papier mit Stift) ist zum Ändern der Nutzerinformationen und der Position der Arbeitsstelle. Das rechte Zeichen (blaue Person mit drei Strichen) führt zur Accountverwaltung für den jeweiligen Nutzer.
|
||||
\begin{figure}[H]
|
||||
\centering
|
||||
\includegraphics[width=\linewidth]{img/frontend/admin.PNG}
|
||||
\caption{Admin}
|
||||
\end{figure}
|
||||
\begin{figure}[H]
|
||||
\centering
|
||||
\includegraphics[width=\linewidth/2]{img/frontend/verwaltung.PNG}
|
||||
\caption{Nutzerverwaltung}
|
||||
\end{figure}
|
||||
\section{Probleme und Lösungen}
|
||||
\subsection{Diagramme}
|
||||
Beim Erstellen der Säulendiagramme sind wir auf den Fehler gestoßen, dass der erste Eintrag von links nicht richtig angezeigt wird. Dieser Fehler ist den Entwicklern von Apexcharts bekannt, aber noch nicht behoben. Wir haben das Problem behoben, indem wir die Daten an der ersten Stelle entfernen. Dies führt zu einem kleinen Abstand, jedoch wird das Diagramm so optimal ohne fehlende Beschriftungen dargestellt.
|
||||
\subsection{Custom Headers Chrome}
|
||||
Ein weiteres unserer Probleme war, dass Chrome sich geweigert hat, auf den selbst erstellten Header zuzugreifen. Dieses Problem konnten wir im Backend lösen, indem wir den Header zu den "Access-Control-Expose-Headers" hinzugefügt haben.
|
||||
\begin{lstlisting}[language=Java]
|
||||
res.setHeader("Access-Control-Expose-Headers", "Authorization");
|
||||
\end{lstlisting}
|
||||
\subsection{Kein Patch möglich}
|
||||
Beim Erstellen eines Patch Requests hatten wir das Problem, dass dieser verweigert wurde. Dies lag daran, dass in den Standard Cors-Konfigurationen nur Get, Head und Post erlaubt sind. Da wir noch Delete und Patch brauchen, haben wir alle Methoden erlaubt.
|
||||
\begin{lstlisting}[language=Java]
|
||||
final CorsConfiguration configuration = new CorsConfiguration().applyPermitDefaultValues();
|
||||
configuration.addAllowedMethod("*");
|
||||
\end{lstlisting}
|
||||
|
@ -1,21 +1,21 @@
|
||||
\chapter{Projektplanung}
|
||||
\section{Ziel des Projekts}
|
||||
Es sollte ein Projekt gebaut werden, das es ermöglicht, die Arbeitszeit über eine App zu tracken. Dies sollte aber nur möglich sein, wenn man sich in einem definierten Umkreis zu seinem Arbeitsplatz befindet. Des weiteren sollte es eine Website zur Verwaltung geben.
|
||||
Es sollte ein Projekt gebaut werden, das es ermöglicht, die Arbeitszeit über eine App zu tracken. Dies sollte aber nur möglich sein, wenn man sich in einem definierten Umkreis zu seinem Arbeitsplatz befindet. Des Weiteren sollte es eine Website zur Verwaltung geben.
|
||||
\section{Definition des Workflows}
|
||||
\subsection{Kommunikation}
|
||||
Zur Kommunikation haben wir für kurze Fragen, sowie das Vereinbaren von Treffen, WhatsApp genutzt. Für Besprechungen haben wir TeamSpeak verwendet.
|
||||
\subsection{Sprints}
|
||||
Wir haben die Projektzeit in 5 zweiwöchige Arbeitssprints und einen einwöchigen Vorbereitungssprint aufgeteilt. Die Enddaten der Sprints waren dabei an die Treffen mit Professor Knauth angepasst.
|
||||
Wir haben die Projektzeit in fünf zweiwöchige Arbeitssprints und einen einwöchigen Vorbereitungssprint aufgeteilt. Die Enddaten der Sprints waren dabei an die Treffen mit Professor Knauth angepasst.
|
||||
\subsection{Code-Owners}
|
||||
In unserem Git-Reposetory haben wir mit Code Ownership gearbeitet. Dazu haben wir 3 Ownerships eingeführt. Marcel Schwarz war Code-Owner für das Backend, Tobias Wieck für die Android App und Simon Kellner, sowie Tim Zieger für das Frontend. Wenn eine Änderung im jeweiligen Gebiet gemacht wurde, musste immer mindestens ein Code-Owner diese genehmigen.
|
||||
In unserem Git-Repository haben wir mit Code-Ownership gearbeitet. Dazu haben wir drei Ownerships eingeführt. Marcel Schwarz war Code-Owner für das Backend, Tobias Wieck für die Android App und Simon Kellner, sowie Tim Zieger für das Frontend. Wenn eine Änderung im jeweiligen Gebiet gemacht wurde, musste immer mindestens ein Code-Owner diese genehmigen.
|
||||
\section{Sprintziele}
|
||||
\subsection{Iteration 1}
|
||||
Das Ziel des ersten Sprints war die Erlernung der notwendigen Technologien und die Schnittstellendefinition.
|
||||
\subsection{Iteration 2}
|
||||
Im zweiten Sprint sollten die Designgrundlagen und Feature Scopes besprochen werden. Des weiteren sollte im Backend die Verbindung zwischen dem Backend und der Datenbank hergestellt werden. Im Frontend war geplant, weiter am Grundgerüst der Seite zu arbeiten und bei der App wurde die Einarbeitung weitergeführt.
|
||||
Im zweiten Sprint sollten die Designgrundlagen und Feature Scopes besprochen werden. Des Weiteren sollte im Backend die Verbindung zwischen dem Backend und der Datenbank hergestellt werden. Im Frontend war geplant, weiter am Grundgerüst der Seite zu arbeiten und bei der App wurde die Einarbeitung weitergeführt.
|
||||
\subsection{Iteration 3}
|
||||
Im Frontend sollte im dritten Sprint ein neues Designframework eingeführt werden und die
|
||||
Kommunikation mit dem Backend getestet werden. Der Plan fürs Backend war die Erstellung der Restcontroller. Bei der Android App sollte die Loginfunktionalität, sowie das Einlesen der Geo-Informationen realisiert werden.
|
||||
Kommunikation mit dem Backend getestet werden. Der Plan für das Backend war die Erstellung der REST-Controller. Bei der Android App sollte die Login-Funktionalität, sowie das Einlesen der Geo-Informationen realisiert werden.
|
||||
\subsection{Iteration 4}
|
||||
Für den vierten Sprint war im Backend geplant, die letzten Controller und Endpoints zu entwickeln. Im Frontend sollten die restlichen Seiten mitsamt Datenabholung aus dem Backend entwickelt werden. Für die App sollte der Geofence entwickelt und die Kommunikation mit dem Backend aufgebaut werden.
|
||||
\subsection{Iteration 5}
|
||||
|
@ -7,7 +7,7 @@
|
||||
\vspace{1cm}\\
|
||||
{\scshape\LARGE Studiengang Informatik \\ Ubiquitous Computing \\ Sommersemester 2020 \par}
|
||||
\vspace{2cm}
|
||||
{\Huge\bfseries GeoTime \par}
|
||||
{\Huge\bfseries Geo Timetracking \par}
|
||||
{\LARGE Timetracking auf Standortbasis \par}
|
||||
\vfill
|
||||
{\Large \textsc{von}}
|
||||
|
550
frontend/package-lock.json
generated
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "geotime",
|
||||
"name": "Geo_Timetracking",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
@ -5167,554 +5167,14 @@
|
||||
"integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
|
||||
},
|
||||
"fsevents": {
|
||||
"version": "1.2.12",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.12.tgz",
|
||||
"integrity": "sha512-Ggd/Ktt7E7I8pxZRbGIs7vwqAPscSESMrCSkx2FtWeqmheJgCo2R74fTsZFCifr0VTPwqRpPv17+6b8Zp7th0Q==",
|
||||
"version": "1.2.13",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz",
|
||||
"integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"bindings": "^1.5.0",
|
||||
"nan": "^2.12.1",
|
||||
"node-pre-gyp": "*"
|
||||
},
|
||||
"dependencies": {
|
||||
"abbrev": {
|
||||
"version": "1.1.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"ansi-regex": {
|
||||
"version": "2.1.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"aproba": {
|
||||
"version": "1.2.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"are-we-there-yet": {
|
||||
"version": "1.1.5",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"delegates": "^1.0.0",
|
||||
"readable-stream": "^2.0.6"
|
||||
}
|
||||
},
|
||||
"balanced-match": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"chownr": {
|
||||
"version": "1.1.4",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"code-point-at": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"concat-map": {
|
||||
"version": "0.0.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"debug": {
|
||||
"version": "3.2.6",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ms": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"deep-extend": {
|
||||
"version": "0.6.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"delegates": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"detect-libc": {
|
||||
"version": "1.0.3",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"fs-minipass": {
|
||||
"version": "1.2.7",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minipass": "^2.6.0"
|
||||
}
|
||||
},
|
||||
"fs.realpath": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"gauge": {
|
||||
"version": "2.7.4",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"aproba": "^1.0.3",
|
||||
"console-control-strings": "^1.0.0",
|
||||
"has-unicode": "^2.0.0",
|
||||
"object-assign": "^4.1.0",
|
||||
"signal-exit": "^3.0.0",
|
||||
"string-width": "^1.0.1",
|
||||
"strip-ansi": "^3.0.1",
|
||||
"wide-align": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"glob": {
|
||||
"version": "7.1.6",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"fs.realpath": "^1.0.0",
|
||||
"inflight": "^1.0.4",
|
||||
"inherits": "2",
|
||||
"minimatch": "^3.0.4",
|
||||
"once": "^1.3.0",
|
||||
"path-is-absolute": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"has-unicode": {
|
||||
"version": "2.0.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"iconv-lite": {
|
||||
"version": "0.4.24",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
}
|
||||
},
|
||||
"ignore-walk": {
|
||||
"version": "3.0.3",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimatch": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"inflight": {
|
||||
"version": "1.0.6",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"once": "^1.3.0",
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"inherits": {
|
||||
"version": "2.0.4",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"ini": {
|
||||
"version": "1.3.5",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"number-is-nan": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"isarray": {
|
||||
"version": "1.0.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.0.4",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
}
|
||||
},
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"minipass": {
|
||||
"version": "2.9.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"minizlib": {
|
||||
"version": "1.3.3",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minipass": "^2.9.0"
|
||||
}
|
||||
},
|
||||
"mkdirp": {
|
||||
"version": "0.5.3",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"minimist": "^1.2.5"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"needle": {
|
||||
"version": "2.3.3",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"debug": "^3.2.6",
|
||||
"iconv-lite": "^0.4.4",
|
||||
"sax": "^1.2.4"
|
||||
}
|
||||
},
|
||||
"node-pre-gyp": {
|
||||
"version": "0.14.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"detect-libc": "^1.0.2",
|
||||
"mkdirp": "^0.5.1",
|
||||
"needle": "^2.2.1",
|
||||
"nopt": "^4.0.1",
|
||||
"npm-packlist": "^1.1.6",
|
||||
"npmlog": "^4.0.2",
|
||||
"rc": "^1.2.7",
|
||||
"rimraf": "^2.6.1",
|
||||
"semver": "^5.3.0",
|
||||
"tar": "^4.4.2"
|
||||
}
|
||||
},
|
||||
"nopt": {
|
||||
"version": "4.0.3",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"abbrev": "1",
|
||||
"osenv": "^0.1.4"
|
||||
}
|
||||
},
|
||||
"npm-bundled": {
|
||||
"version": "1.1.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"npm-normalize-package-bin": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"npm-normalize-package-bin": {
|
||||
"version": "1.0.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"npm-packlist": {
|
||||
"version": "1.4.8",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ignore-walk": "^3.0.1",
|
||||
"npm-bundled": "^1.0.1",
|
||||
"npm-normalize-package-bin": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"npmlog": {
|
||||
"version": "4.1.2",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"are-we-there-yet": "~1.1.2",
|
||||
"console-control-strings": "~1.1.0",
|
||||
"gauge": "~2.7.3",
|
||||
"set-blocking": "~2.0.0"
|
||||
}
|
||||
},
|
||||
"number-is-nan": {
|
||||
"version": "1.0.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"object-assign": {
|
||||
"version": "4.1.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"once": {
|
||||
"version": "1.4.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"os-homedir": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"os-tmpdir": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"osenv": {
|
||||
"version": "0.1.5",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"os-homedir": "^1.0.0",
|
||||
"os-tmpdir": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"path-is-absolute": {
|
||||
"version": "1.0.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"process-nextick-args": {
|
||||
"version": "2.0.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"rc": {
|
||||
"version": "1.2.8",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"deep-extend": "^0.6.0",
|
||||
"ini": "~1.3.0",
|
||||
"minimist": "^1.2.0",
|
||||
"strip-json-comments": "~2.0.1"
|
||||
}
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "2.3.7",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"core-util-is": "~1.0.0",
|
||||
"inherits": "~2.0.3",
|
||||
"isarray": "~1.0.0",
|
||||
"process-nextick-args": "~2.0.0",
|
||||
"safe-buffer": "~5.1.1",
|
||||
"string_decoder": "~1.1.1",
|
||||
"util-deprecate": "~1.0.1"
|
||||
}
|
||||
},
|
||||
"rimraf": {
|
||||
"version": "2.7.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"glob": "^7.1.3"
|
||||
}
|
||||
},
|
||||
"safe-buffer": {
|
||||
"version": "5.1.2",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"sax": {
|
||||
"version": "1.2.4",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"set-blocking": {
|
||||
"version": "2.0.0",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"signal-exit": {
|
||||
"version": "3.0.2",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"string-width": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"code-point-at": "^1.0.0",
|
||||
"is-fullwidth-code-point": "^1.0.0",
|
||||
"strip-ansi": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
}
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "3.0.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"strip-json-comments": {
|
||||
"version": "2.0.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"tar": {
|
||||
"version": "4.4.13",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"chownr": "^1.1.1",
|
||||
"fs-minipass": "^1.2.5",
|
||||
"minipass": "^2.8.6",
|
||||
"minizlib": "^1.2.1",
|
||||
"mkdirp": "^0.5.0",
|
||||
"safe-buffer": "^5.1.2",
|
||||
"yallist": "^3.0.3"
|
||||
}
|
||||
},
|
||||
"util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"wide-align": {
|
||||
"version": "1.1.3",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"requires": {
|
||||
"string-width": "^1.0.2 || 2"
|
||||
}
|
||||
},
|
||||
"wrappy": {
|
||||
"version": "1.0.2",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"yallist": {
|
||||
"version": "3.1.1",
|
||||
"bundled": true,
|
||||
"dev": true,
|
||||
"optional": true
|
||||
}
|
||||
"nan": "^2.12.1"
|
||||
}
|
||||
},
|
||||
"fstream": {
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "geotime",
|
||||
"name": "Geo_Timetracking",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 1.1 KiB |
@ -18,58 +18,91 @@ const routes = [
|
||||
{
|
||||
path: "/",
|
||||
name: "Home",
|
||||
component: Home
|
||||
component: Home,
|
||||
meta: {
|
||||
title: "Geo Timetracking - Home",
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/timerecords",
|
||||
name: "TimeRecords",
|
||||
component: TimeRecords
|
||||
component: TimeRecords,
|
||||
meta: {
|
||||
title: 'Geo Timetracking - Time Records',
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
path: "/about",
|
||||
name: "About",
|
||||
component: About
|
||||
component: About,
|
||||
meta: {
|
||||
title: 'Geo Timetracking - About',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/statistics",
|
||||
name: "Statistics",
|
||||
component: StatisticOverview
|
||||
component: StatisticOverview,
|
||||
meta: {
|
||||
title: 'Geo Timetracking - Statistics',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/users",
|
||||
name: "Users",
|
||||
component: Users
|
||||
component: Users,
|
||||
meta: {
|
||||
title: 'Geo Timetracking - Users',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/edituser",
|
||||
name: "EditUser",
|
||||
component: EditUser
|
||||
component: EditUser,
|
||||
meta: {
|
||||
title: 'Geo Timetracking - Edit User',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/timetrackaccounts",
|
||||
name: "TimeTrack Accounts",
|
||||
component: TimeTrackAccounts
|
||||
component: TimeTrackAccounts,
|
||||
meta: {
|
||||
title: 'Geo Timetracking - TimeTrack Accounts',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/edittimetrackaccount",
|
||||
name: "Edit TimeTrack Account",
|
||||
component: EditTimeTrackAccount
|
||||
component: EditTimeTrackAccount,
|
||||
meta: {
|
||||
title: 'Geo Timetracking - Edit TimeTrack Account',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/createtimetrackaccount",
|
||||
name: "Create TimeTrack Account",
|
||||
component: CreateTimeTrackAccount
|
||||
component: CreateTimeTrackAccount,
|
||||
meta: {
|
||||
title: 'Geo Timetracking - Create TimeTrack Accounts',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/edittimerecord",
|
||||
name: "EditTimerecord",
|
||||
component: EditTimerecord
|
||||
component: EditTimerecord,
|
||||
meta: {
|
||||
title: 'Geo Timetracking - Edit Time Record',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: "/createtimerecord",
|
||||
name: "CreateTimerecord",
|
||||
component: CreateTimerecord
|
||||
component: CreateTimerecord,
|
||||
meta: {
|
||||
title: 'Geo Timetracking - Create Time Record',
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
|
@ -1,26 +1,47 @@
|
||||
<template>
|
||||
<v-container fluid>
|
||||
<v-row class="ma-5">
|
||||
<v-col cols="3" ></v-col>
|
||||
<v-col cols="6">
|
||||
<v-card>
|
||||
<p class="text-center logowhite--text" style="font-size:20pt">This is a ubiquitos computing project.</p>
|
||||
<p class="text-center logowhite--text" style="font-size:20pt">It was created by the team TacocaT.</p>
|
||||
<p
|
||||
class="text-center logowhite--text"
|
||||
style="font-size:20pt"
|
||||
>This is a ubiquitous computing project.</p>
|
||||
<p
|
||||
class="text-center logowhite--text"
|
||||
style="font-size:20pt"
|
||||
>It was created by the team TacocaT.</p>
|
||||
</v-card>
|
||||
</v-col>
|
||||
<v-col cols="3"></v-col>
|
||||
<v-col cols="3"></v-col>
|
||||
<v-col cols="6">
|
||||
<v-card>
|
||||
<p class="text-center logowhite--text" style="font-size:30pt">Team TacocaT</p>
|
||||
<br>
|
||||
<p class="text-center logowhite--text" style="font-size:20pt">Backend Developer: Marcel Schwarz</p>
|
||||
<p class="text-center logowhite--text" style="font-size:20pt">Android Developer: Tobias Wieck</p>
|
||||
<p class="text-center logowhite--text" style="font-size:20pt">Frontend Developer: Simon Kellner, Tim Zieger</p>
|
||||
|
||||
<br />
|
||||
<p
|
||||
class="text-center logowhite--text"
|
||||
style="font-size:20pt"
|
||||
>Backend Developer: Marcel Schwarz</p>
|
||||
<p
|
||||
class="text-center logowhite--text"
|
||||
style="font-size:20pt"
|
||||
>Android Developer: Tobias Wieck</p>
|
||||
<p
|
||||
class="text-center logowhite--text"
|
||||
style="font-size:20pt"
|
||||
>Frontend Developer: Simon Kellner, Tim Zieger</p>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
<script>
|
||||
// @ is an alias to /src
|
||||
|
||||
|
||||
export default {
|
||||
name: "About",
|
||||
|
||||
name: "About"
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -66,7 +66,7 @@
|
||||
|
||||
</v-row>
|
||||
<v-card v-if="loggedIn == 'false'" class="pa-3">
|
||||
<p style="font-size:20pt">Welcome to geotime</p>
|
||||
<p style="font-size:20pt">Welcome to Geo Timetracking</p>
|
||||
</v-card>
|
||||
</v-container>
|
||||
</template>
|
||||
|
@ -1,22 +1,26 @@
|
||||
<template>
|
||||
<v-row class="ma-5">
|
||||
<v-col cols="3">
|
||||
<v-col cols="4">
|
||||
<WorkPausePie />
|
||||
</v-col>
|
||||
<v-col cols="9">
|
||||
<WeekSummary />
|
||||
<v-col cols="4">
|
||||
<account-pie />
|
||||
</v-col>
|
||||
<v-col cols="9">
|
||||
<v-col cols="4">
|
||||
<revenue-pie/>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<MonthSummary />
|
||||
</v-col>
|
||||
<v-col cols="3">
|
||||
<account-pie/>
|
||||
<v-col cols="12">
|
||||
<WeekSummary />
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import RevenuePie from "./charts/RevenuePie.vue"
|
||||
import WeekSummary from "./charts/WeekSummary.vue";
|
||||
import MonthSummary from "./charts/MonthSummary.vue";
|
||||
import WorkPausePie from "./charts/WorkPausePie.vue";
|
||||
@ -25,6 +29,7 @@ import AccountPie from "./charts/AccountPie.vue";
|
||||
export default {
|
||||
name: "StatisticOverview",
|
||||
components: {
|
||||
RevenuePie,
|
||||
WeekSummary,
|
||||
MonthSummary,
|
||||
WorkPausePie,
|
||||
|
@ -20,7 +20,8 @@ export default {
|
||||
},
|
||||
computed: {
|
||||
chartOptions: function() {
|
||||
return{labels: ["Working hours", "Pause hours"],
|
||||
return {
|
||||
labels: ["Working hours", "Pause hours"],
|
||||
chart: {
|
||||
type: "donut",
|
||||
background: "#202020",
|
||||
@ -49,14 +50,14 @@ export default {
|
||||
show: false
|
||||
},
|
||||
y: {
|
||||
formatter: (value) => {
|
||||
formatter: value => {
|
||||
var revenueTMP = 0;
|
||||
for (let index = 0; index < this.series.length; index++) {
|
||||
if (value == this.series[index]) {
|
||||
revenueTMP = this.revenue[index];
|
||||
}
|
||||
}
|
||||
revenueTMP = revenueTMP / 60 * value * 1.00;
|
||||
revenueTMP = (revenueTMP / 60) * value * 1.0;
|
||||
revenueTMP = Math.round(revenueTMP * 100) / 100;
|
||||
var decimalTimeString = value;
|
||||
var decimalTime = parseFloat(decimalTimeString);
|
||||
@ -78,11 +79,27 @@ export default {
|
||||
if (seconds < 10) {
|
||||
seconds = "0" + seconds;
|
||||
}
|
||||
return hours + ":" + minutes + ":" + seconds + " with " + revenueTMP + "$ revenue.";
|
||||
return (
|
||||
hours +
|
||||
":" +
|
||||
minutes +
|
||||
":" +
|
||||
seconds +
|
||||
" with " +
|
||||
revenueTMP +
|
||||
"$ revenue."
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
colors: ["#0096ff", "#e21d1f", "#03ac13", "#800080", "#f9d71c", "ff1694"],
|
||||
colors: [
|
||||
"#0096ff",
|
||||
"#e21d1f",
|
||||
"#03ac13",
|
||||
"#800080",
|
||||
"#f9d71c",
|
||||
"ff1694"
|
||||
],
|
||||
theme: {
|
||||
mode: "dark"
|
||||
},
|
||||
@ -105,11 +122,12 @@ export default {
|
||||
}
|
||||
}
|
||||
]
|
||||
}}
|
||||
};
|
||||
}
|
||||
},
|
||||
data: () => ({
|
||||
series: [0, 0],
|
||||
revenue: [0, 0],
|
||||
revenue: [0, 0]
|
||||
}),
|
||||
created() {
|
||||
// Get all Timetrack accounts for the current user
|
||||
@ -153,12 +171,24 @@ export default {
|
||||
|
||||
// Get and sort all Records to the accounts
|
||||
var xhttp = new XMLHttpRequest();
|
||||
var records;
|
||||
var records = new Array(0);
|
||||
var hasNext = true;
|
||||
var page = 0;
|
||||
|
||||
while (hasNext) {
|
||||
xhttp.onreadystatechange = function() {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
var recordsDB = JSON.parse(xhttp.responseText);
|
||||
|
||||
records = recordsDB._embedded.records;
|
||||
recordsDB._embedded.records.forEach(tmpRecord => {
|
||||
records.push(tmpRecord);
|
||||
});
|
||||
page++;
|
||||
if (recordsDB.page.number == recordsDB.page.totalPages - 1) {
|
||||
hasNext = false;
|
||||
}
|
||||
}
|
||||
if (this.status == 403) {
|
||||
hasNext = false;
|
||||
}
|
||||
};
|
||||
xhttp.open(
|
||||
@ -167,18 +197,28 @@ export default {
|
||||
"/records/search/allForUser?username=" +
|
||||
sessionStorage.getItem("username") +
|
||||
"&projection=overview" +
|
||||
"&size=200",
|
||||
"&page=" +
|
||||
page +
|
||||
"&size=50",
|
||||
false
|
||||
);
|
||||
|
||||
xhttp.setRequestHeader("Authorization", sessionStorage.getItem("jwt"));
|
||||
|
||||
xhttp.send(null);
|
||||
}
|
||||
|
||||
for (let index = 0; index < records.length; index++) {
|
||||
var record = records[index];
|
||||
for (let indexAccs = 0; indexAccs < this.chartOptions.labels.length; indexAccs++) {
|
||||
if (record.account == this.chartOptions.labels[indexAccs] && record.type == "PAID") {
|
||||
for (
|
||||
let indexAccs = 0;
|
||||
indexAccs < this.chartOptions.labels.length;
|
||||
indexAccs++
|
||||
) {
|
||||
if (
|
||||
record.account == this.chartOptions.labels[indexAccs] &&
|
||||
record.type == "PAID"
|
||||
) {
|
||||
this.series[indexAccs] += record.duration;
|
||||
}
|
||||
}
|
||||
|
@ -112,6 +112,30 @@ export default {
|
||||
type: "datetime",
|
||||
categories: []
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
formatter: function(value) {
|
||||
var decimalTimeString = value;
|
||||
var decimalTime = parseFloat(decimalTimeString);
|
||||
decimalTime = decimalTime * 60;
|
||||
var hours = Math.floor(decimalTime / (60 * 60));
|
||||
decimalTime = decimalTime - hours * 60 * 60;
|
||||
var minutes = Math.floor(decimalTime / 60);
|
||||
decimalTime = decimalTime - minutes * 60;
|
||||
var seconds = Math.round(decimalTime);
|
||||
if (hours < 10) {
|
||||
hours = "0" + hours;
|
||||
}
|
||||
if (minutes < 10) {
|
||||
minutes = "0" + minutes;
|
||||
}
|
||||
if (seconds < 10) {
|
||||
seconds = "0" + seconds;
|
||||
}
|
||||
return hours + ":" + minutes + ":" + seconds;
|
||||
}
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
position: "left",
|
||||
@ -126,7 +150,10 @@ export default {
|
||||
created() {
|
||||
var daysToDisplay = 31;
|
||||
var xhttp = new XMLHttpRequest();
|
||||
var records;
|
||||
var records = new Array(0);
|
||||
var hasNext = true;
|
||||
var page = 0;
|
||||
|
||||
this.chartOptions.xaxis.categories = new Array(daysToDisplay);
|
||||
this.series[0].data = new Array(daysToDisplay);
|
||||
this.series[1].data = new Array(daysToDisplay);
|
||||
@ -150,13 +177,23 @@ export default {
|
||||
}
|
||||
today = yyyy + "-" + mm + "-" + dd;
|
||||
|
||||
while (hasNext) {
|
||||
xhttp.onreadystatechange = function() {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
var recordsDB = JSON.parse(xhttp.responseText);
|
||||
|
||||
records = recordsDB._embedded.records;
|
||||
recordsDB._embedded.records.forEach(tmpRecord => {
|
||||
records.push(tmpRecord);
|
||||
});
|
||||
page++;
|
||||
if (recordsDB.page.number == recordsDB.page.totalPages - 1) {
|
||||
hasNext = false;
|
||||
}
|
||||
}
|
||||
if (this.status == 403) {
|
||||
hasNext = false;
|
||||
}
|
||||
};
|
||||
|
||||
xhttp.open(
|
||||
"GET",
|
||||
baseUri +
|
||||
@ -165,22 +202,28 @@ export default {
|
||||
"T00:00:01" +
|
||||
"&end=" +
|
||||
today +
|
||||
"T00:00:01" +
|
||||
"T23:59:59" +
|
||||
"&username=" +
|
||||
sessionStorage.getItem("username") +
|
||||
"&size=150",
|
||||
"&page=" +
|
||||
page +
|
||||
"&size=50",
|
||||
false
|
||||
);
|
||||
|
||||
xhttp.setRequestHeader("Authorization", sessionStorage.getItem("jwt"));
|
||||
|
||||
xhttp.send(null);
|
||||
}
|
||||
|
||||
for (let index = 0; index < records.length; index++) {
|
||||
var record = records[index];
|
||||
|
||||
for (let indexA = 0; indexA < daysToDisplay; indexA++) {
|
||||
if (record.startdate.split("T")[0] == this.chartOptions.xaxis.categories[indexA]) {
|
||||
if (
|
||||
record.startdate.split("T")[0] ==
|
||||
this.chartOptions.xaxis.categories[indexA]
|
||||
) {
|
||||
if (record.type == "PAID") {
|
||||
this.series[0].data[indexA] += record.duration;
|
||||
} else {
|
||||
|
202
frontend/src/views/charts/RevenuePie.vue
Normal file
@ -0,0 +1,202 @@
|
||||
<template>
|
||||
<v-card color="main_accent" outlined>
|
||||
<div>
|
||||
<p></p>
|
||||
<h3 align="center">Revenue by TimeTrack Account</h3>
|
||||
<div id="chart">
|
||||
<apexchart class="ma-5" type="donut" width="100%" :options="chartOptions" :series="series"></apexchart>
|
||||
</div>
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import VueApexCharts from "vue-apexcharts";
|
||||
import { baseUri } from "../../variables.js";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
apexchart: VueApexCharts
|
||||
},
|
||||
computed: {
|
||||
chartOptions: function() {
|
||||
return {
|
||||
labels: ["Working hours", "Pause hours"],
|
||||
chart: {
|
||||
type: "donut",
|
||||
background: "#202020",
|
||||
toolbar: {
|
||||
show: true,
|
||||
offsetX: 0,
|
||||
offsetY: 0,
|
||||
tools: {
|
||||
download: true,
|
||||
selection: true,
|
||||
zoom: false,
|
||||
zoomin: false,
|
||||
zoomout: false,
|
||||
pan: false,
|
||||
reset: false,
|
||||
customIcons: []
|
||||
},
|
||||
autoSelected: "zoom"
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
followCursor: true,
|
||||
fillSeriesColor: false,
|
||||
x: {
|
||||
show: false
|
||||
},
|
||||
y: {
|
||||
formatter: value => {
|
||||
return Math.round(value * 100) / 100 + "$"
|
||||
}
|
||||
}
|
||||
},
|
||||
colors: [
|
||||
"#0096ff",
|
||||
"#e21d1f",
|
||||
"#03ac13",
|
||||
"#800080",
|
||||
"#f9d71c",
|
||||
"ff1694"
|
||||
],
|
||||
theme: {
|
||||
mode: "dark"
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
position: "left",
|
||||
offsetY: 40
|
||||
},
|
||||
|
||||
responsive: [
|
||||
{
|
||||
breakpoint: 480,
|
||||
options: {
|
||||
chart: {
|
||||
width: 300
|
||||
},
|
||||
legend: {
|
||||
position: "bottom"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
},
|
||||
data: () => ({
|
||||
series: [0, 0],
|
||||
revenue: [0, 0]
|
||||
}),
|
||||
created() {
|
||||
// Get all Timetrack accounts for the current user
|
||||
var accounts;
|
||||
var timeTrackAccountsTMP;
|
||||
var accountxhttp = new XMLHttpRequest();
|
||||
accountxhttp.onreadystatechange = function() {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
accounts = JSON.parse(accountxhttp.responseText);
|
||||
timeTrackAccountsTMP = accounts._embedded.accounts;
|
||||
}
|
||||
};
|
||||
accountxhttp.open(
|
||||
"GET",
|
||||
baseUri +
|
||||
"/accounts/search/findByUsername?username=" +
|
||||
sessionStorage.getItem("username"),
|
||||
false
|
||||
);
|
||||
|
||||
accountxhttp.setRequestHeader(
|
||||
"Authorization",
|
||||
sessionStorage.getItem("jwt")
|
||||
);
|
||||
|
||||
accountxhttp.send(null);
|
||||
|
||||
this.chartOptions.labels = new Array(timeTrackAccountsTMP.length);
|
||||
|
||||
this.series = new Array(timeTrackAccountsTMP.length);
|
||||
this.revenue = new Array(timeTrackAccountsTMP.length);
|
||||
for (let index = 0; index < this.series.length; index++) {
|
||||
this.series[index] = 0;
|
||||
this.revenue[index] = 0;
|
||||
}
|
||||
|
||||
for (let index = 0; index < timeTrackAccountsTMP.length; index++) {
|
||||
this.chartOptions.labels[index] = timeTrackAccountsTMP[index].name;
|
||||
this.revenue[index] = timeTrackAccountsTMP[index].revenue;
|
||||
}
|
||||
|
||||
// Get and sort all Records to the accounts
|
||||
var xhttp = new XMLHttpRequest();
|
||||
var records = new Array(0);
|
||||
var hasNext = true;
|
||||
var page = 0;
|
||||
|
||||
while (hasNext) {
|
||||
xhttp.onreadystatechange = function() {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
var recordsDB = JSON.parse(xhttp.responseText);
|
||||
recordsDB._embedded.records.forEach(tmpRecord => {
|
||||
records.push(tmpRecord);
|
||||
});
|
||||
page++;
|
||||
if (recordsDB.page.number == recordsDB.page.totalPages - 1) {
|
||||
hasNext = false;
|
||||
}
|
||||
}
|
||||
if (this.status == 403) {
|
||||
hasNext = false;
|
||||
}
|
||||
};
|
||||
xhttp.open(
|
||||
"GET",
|
||||
baseUri +
|
||||
"/records/search/allForUser?username=" +
|
||||
sessionStorage.getItem("username") +
|
||||
"&projection=overview" +
|
||||
"&page=" +
|
||||
page +
|
||||
"&size=50",
|
||||
false
|
||||
);
|
||||
|
||||
xhttp.setRequestHeader("Authorization", sessionStorage.getItem("jwt"));
|
||||
|
||||
xhttp.send(null);
|
||||
}
|
||||
|
||||
for (let index = 0; index < records.length; index++) {
|
||||
var record = records[index];
|
||||
for (
|
||||
let indexAccs = 0;
|
||||
indexAccs < this.chartOptions.labels.length;
|
||||
indexAccs++
|
||||
) {
|
||||
if (
|
||||
record.account == this.chartOptions.labels[indexAccs] &&
|
||||
record.type == "PAID"
|
||||
) {
|
||||
this.series[indexAccs] += record.duration;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let index = 0; index < this.series.length; index++) {
|
||||
this.series[index] = this.series[index] * this.revenue[index] /60;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.v-card {
|
||||
border-radius: 20px !important;
|
||||
}
|
||||
</style>
|
@ -112,6 +112,30 @@ export default {
|
||||
type: "datetime",
|
||||
categories: []
|
||||
},
|
||||
yaxis: {
|
||||
labels: {
|
||||
formatter: function(value) {
|
||||
var decimalTimeString = value;
|
||||
var decimalTime = parseFloat(decimalTimeString);
|
||||
decimalTime = decimalTime * 60;
|
||||
var hours = Math.floor(decimalTime / (60 * 60));
|
||||
decimalTime = decimalTime - hours * 60 * 60;
|
||||
var minutes = Math.floor(decimalTime / 60);
|
||||
decimalTime = decimalTime - minutes * 60;
|
||||
var seconds = Math.round(decimalTime);
|
||||
if (hours < 10) {
|
||||
hours = "0" + hours;
|
||||
}
|
||||
if (minutes < 10) {
|
||||
minutes = "0" + minutes;
|
||||
}
|
||||
if (seconds < 10) {
|
||||
seconds = "0" + seconds;
|
||||
}
|
||||
return hours + ":" + minutes + ":" + seconds;
|
||||
}
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
position: "left",
|
||||
@ -126,7 +150,10 @@ export default {
|
||||
created() {
|
||||
var daysToDisplay = 8;
|
||||
var xhttp = new XMLHttpRequest();
|
||||
var records;
|
||||
var records = new Array(0);
|
||||
var hasNext = true;
|
||||
var page = 0;
|
||||
|
||||
this.chartOptions.xaxis.categories = new Array(daysToDisplay);
|
||||
this.series[0].data = new Array(daysToDisplay);
|
||||
this.series[1].data = new Array(daysToDisplay);
|
||||
@ -150,11 +177,21 @@ export default {
|
||||
}
|
||||
today = yyyy + "-" + mm + "-" + dd;
|
||||
|
||||
|
||||
while (hasNext) {
|
||||
xhttp.onreadystatechange = function() {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
var recordsDB = JSON.parse(xhttp.responseText);
|
||||
|
||||
records = recordsDB._embedded.records;
|
||||
recordsDB._embedded.records.forEach(tmpRecord => {
|
||||
records.push(tmpRecord);
|
||||
});
|
||||
page++;
|
||||
if (recordsDB.page.number == recordsDB.page.totalPages - 1) {
|
||||
hasNext = false;
|
||||
}
|
||||
}
|
||||
if (this.status == 403) {
|
||||
hasNext = false;
|
||||
}
|
||||
};
|
||||
xhttp.open(
|
||||
@ -165,9 +202,11 @@ export default {
|
||||
"T00:00:01" +
|
||||
"&end=" +
|
||||
today +
|
||||
"T00:00:01" +
|
||||
"T23:59:59" +
|
||||
"&username=" +
|
||||
sessionStorage.getItem("username") +
|
||||
"&page=" +
|
||||
page +
|
||||
"&size=50",
|
||||
false
|
||||
);
|
||||
@ -175,6 +214,8 @@ export default {
|
||||
xhttp.setRequestHeader("Authorization", sessionStorage.getItem("jwt"));
|
||||
|
||||
xhttp.send(null);
|
||||
}
|
||||
|
||||
|
||||
for (let index = 0; index < records.length; index++) {
|
||||
var record = records[index];
|
||||
|
@ -102,12 +102,24 @@ export default {
|
||||
}),
|
||||
created() {
|
||||
var xhttp = new XMLHttpRequest();
|
||||
var records;
|
||||
var records = new Array(0);
|
||||
var hasNext = true;
|
||||
var page = 0;
|
||||
|
||||
while (hasNext) {
|
||||
xhttp.onreadystatechange = function() {
|
||||
if (this.readyState == 4 && this.status == 200) {
|
||||
var recordsDB = JSON.parse(xhttp.responseText);
|
||||
|
||||
records = recordsDB._embedded.records;
|
||||
recordsDB._embedded.records.forEach(tmpRecord => {
|
||||
records.push(tmpRecord)
|
||||
});
|
||||
page++;
|
||||
if (recordsDB.page.number == recordsDB.page.totalPages - 1) {
|
||||
hasNext = false;
|
||||
}
|
||||
}
|
||||
if (this.status == 403) {
|
||||
hasNext = false;
|
||||
}
|
||||
};
|
||||
xhttp.open(
|
||||
@ -115,13 +127,17 @@ export default {
|
||||
baseUri +
|
||||
"/records/search/allForUser?username=" +
|
||||
sessionStorage.getItem("username") +
|
||||
"&projection=overview",
|
||||
"&page=" +
|
||||
page +
|
||||
"&projection=overview" +
|
||||
"&size=50",
|
||||
false
|
||||
);
|
||||
|
||||
xhttp.setRequestHeader("Authorization", sessionStorage.getItem("jwt"));
|
||||
|
||||
xhttp.send(null);
|
||||
}
|
||||
|
||||
var paidTime = 0;
|
||||
var breakTime = 0;
|
||||
@ -130,7 +146,6 @@ export default {
|
||||
var record = records[index];
|
||||
var type = record.type + "";
|
||||
|
||||
|
||||
if (type == "PAID") {
|
||||
paidTime += record.duration;
|
||||
} else {
|
||||
|
BIN
other-artifacts/Praesentation.pptx
(Stored with Git LFS)
Normal file
BIN
other-artifacts/Product-Flyer.png
Normal file
After Width: | Height: | Size: 553 KiB |
BIN
other-artifacts/app-release.apk
(Stored with Git LFS)
Normal file
BIN
other-artifacts/documentation-2023-07-24.pdf
Normal file
@ -1,2 +1,2 @@
|
||||
FROM postgres
|
||||
FROM mariadb
|
||||
ENV TZ=Europe/Berlin
|