- Java 27.1%
- CSS 25%
- JavaScript 20.4%
- TypeScript 13.6%
- Riot 11.6%
- Other 2.3%
| webapp | ||
| .gitignore | ||
| pom.xml | ||
| README.md | ||
TivAppX
Tivano Showcase App: Zeiterfassung
Table of Contents
- General Info
- Technologien
- Installation
- Konfiguration
- Anwender-Dokumentation
- Entwickler-Dokumentation
General Info
Zeiterfassungs-App als Showcase Projekt.
Technologien
A list of technologies used within the project:
- PostgreSQL
- JPA persistence layer
- OpenAPI for TypeScript generation
- TypeScript: Version 5.9.3
- Riot.js (https://riot.js.org/): Version 10.0.1
- Beer CSS (https://www.beercss.com/)
- OpenID Connect
Installation
Installation on developer machines:
$ git clone https://git.app.tivano.net/tivano/zeiterfassung
$ cd TivAppX/webapp
$ npm install
(creates node_modules folder and downloads @riotjs dependencies)
* build in NetBeans IDE and deploy *.war to your tomcat
DB Script, zum Anlegen der DB-Tabellen und initialer Inhalte: TivAppX\webapp\src\main\resources\database\
Konfiguration
Konfigurations-File: TivAppX\webapp\src\main\webapp\META-INF\context.xml
Was wird konfiguriert?
- PostgreSQL DB
- OpenID Connect Anbindung:
Für jede Anmelde-Möglichkeit kann hier ein weiteres Set an Konfiguration-Parametern angelegt werden.
Aktuell unterstützt die GUI zwei Anmeldevarianten:- Anmeldung mit Google (Konfigurations-Parameter-Set openid.1.*)
- Anmeldung mit Tivano OIDC (Konfigurations-Parameter-Set openid.2.*)
- Startdatum für die Berechnung des Zeitkontostands: tivappx.evaluation.startdate (e.g. "2026-03-01")
Nutzung der App
Initial muss es einen User mit der Rolle MANAGER geben. Die Anmeldung erfolgt über OpenID Connect.
Die E-Mail-Adresse, die zur Anmeldung verwendet wird, muss dabei einer im Nutzermanagement eingepflegten E-Mail-Adressen entsprechen.
Ein MANAGER kann:
- Weitere User anlegen und Rollen zuordnen (Menüpunkt "User")
- Mitarbeiter und Vertragsdaten anlegen und die Zuordnung von Mitarbeitern zu Usern vornehmen (Menüpunkt "User")
- Abwesenheiten für Mitarbeiter einpflegen (Menüpunkt "Abwesenheit")
- Seine eigene Arbeitszeit erfassen (Menüpunkt "Zeiterfassung")
Ein USER kann:
- Seine eigene Arbeitszeit erfassen (Menüpunkt "Zeiterfassung")
Zeiterfassung
- Die Zeiterfassung kann mehrmals am Tag gestartet und gestoppt werden.
- Wird die Zeiterfassung nicht gestoppt, wird die Uhr automatisch um Mitternacht angehalten.
- Erfasste Zeiträume lassen sich nachträglich editieren (Start, Stop anpassen, Tätigkeitsbeschreibung hinzufügen) oder löschen
Abwesenheit
- Ein Manager kann für jeden Mitarbeiter Abwesenheiten eintragen (Urlaub, Krankheit, Feiertag)
- Abwesenheiten können nur für Tage, die für den Mitarbeiter als Arbeitstag gelten, eingetragen werden
- Ob ein Tag als Arbeitstag gilt, ergibt sich aus den Vertragsdaten (gültiger Vertragszeitraum + Wochenarbeitstage)
- Je Mitarbeiter und Tag kann es nur einen Abwesenheitseintrag geben
Mitarbeiter
- Ein Manager kann Mitarbeiter anlegen
- Die E-Mail-Adresse muss nicht mit der Anmelde-E-Mail des Benutzerkontos übereinstimmen, sondern soll als Adresse für E-Mail-Benachrichtigungen dienen
- Je Mitarbeiter lassen sich mehrere Verträge anlegen. Es werden nur Vertragsdaten erfasst, die für die Zeiterfassung relevant sind.
- Jeder Vertrag muss ein Startdatum haben, das Enddatum kann offen bleiben.
Für die Entwicklung
Backend (JPA entities, model, REST services)
- de.tivano.app.model
- Immutable interfaces der JPA entities
- de.tivano.app.persistence
- Definition abstrakter Entitätsklassen (
*EntityBase.java), die die model interfaces implementieren und die Felder der Datenbanktabelle beschreiben
- Definition abstrakter Entitätsklassen (
- de.tivano.app.webapp
RestApiServlet: das CXFNonSpringJaxrsServlet, welches die REST Services zur Verfügung stelltLoginFilter: ein HttpFilter, der sicherstellt, dass nur angemeldete Benutzer auf die Applikation Zugriff haben, und die Anmeldung per OpenID Connect implementiertOpenIDConnectConfiguration: repräsentiert eine OpenID Connect Konfiguration (e.g. Google, Tivano)JPAUtil: der Zugriff auf die Persitenzschicht- Viele Methoden sind generisch, einige beziehen sich aber auch auf spezielle Entitäten und Abfragen.
- Für den Zugriff auf die DB werden reine JPA Criteria Queries verwendet.
- Beispiel für eine komplexere Abfrage:
getTimeRecordingsSince(int employeeId, LocalDate since)
- de.tivano.app.webapp.api
- Definition der REST Schnittstellen
- Users /api/users - Nutzer der App
- Employees /api/employees - Mitarbeiter
- Contracts /api/contracts - Mitarbeiterverträge
- Downtimes /api/donwtimes - Abwesenheiten
- Times /api/times - Zeiterfassung
EvaluationManager: Stundenkontoberechnungen
- Definition der REST Schnittstellen
Frontend
TivAppX\webapp\src\main\typescript
- Die generierte TypeScript Repräsentation der REST Schnittstelle
TivAppX\webapp\src\main\webapp:
index.html: Die Hauptseite der Single-Page-Applikationlogin.html: Login-Seite für Anmeldung per OpenID Connectlogout.html: Logout-Seite- \app: Das eigentliche Frontend
TivAppX\webapp\src\main\webapp\app\components
- Ein Unterordner je Riot.js Komponente, darin liegt jeweils
- die c-***.riot component (HTML Schnipsel und RiotComponent Deklaration)
- und die types.ts Datei mit Definition der Props und State Interfaces für die Komponente.
Riot-Komponenten:
c-app
Die Hauptseite, in die alle anderen Komponenten eingebettet werden.
Steuerung: AppController.tsc-snackbar
Die Snackbar ist in die Hauptseite eingebunden und blendet Info- und Fehlermeldungen ein. Sie kann von allen Modul-Komponenten getriggert werden.
Steuerung: MessageController.tsc-times
Das Modul "Zeiterfassung".
Steuerung: TimeRecordController.ts
Repräsentation eines Eintrags: TimeRecordModel.tsc-downtimes
Das Modul "Abwesenheiten".
Steuerung: DowntimeController.ts
Repräsentation eines Eintrags: DowntimeModel.ts
Eingebundene Komponenten: c-select (an zwei Stellen)c-employees
Das Modul "Mitarbeiter"
Steuerung: EmployeeController.ts
Repräsentation eines Eintrags: EmployeeModel.ts
Eingebundene Komponenten: c-contractsc-contracts
Verträge für einen Mitarbeiter
Steuerung: ContractController.ts
Repräsentation eines Eintrags: ContractModel.ts
Eingebundene Komponenten: c-contractsc-users
Das Modul "User"
Steuerung: UserController.ts
Repräsentation eines Eintrags: UserModel.ts
Eingebundene Komponenten: c-checkbox, c-selectc-checkbox
Generische Checkbox-Komponente, die im Status den boolean-value eines items hält (item: UiSwitch)c-select
Generische Select-Komponente, die eine Liste von items (UiSwitch) als Dropdown darstellt und den value des ausgewählten im Status hält
TivAppX\webapp\src\main\webapp\app\controller
- Controller-Logik für die zentralen Riot Komponenten
TivAppX\webapp\src\main\webapp\app\model
- Repräsentation der App-Entitäten im Frontend
TivAppX\webapp\src\main\webapp\app\ui
- Generische UISwitch Klasse zur Verwendung in UI Komponenten c-checkbox und c-select
Guidelines für eine Riot-Komponente am Beispiel c-users
Die Datei c-user.riot enthält die Definition der RiotComponent cUsers. HTML-Code und RiotComponent Deklararion müssen in einer gemeinsamen Datei liegen. Das umschließende Tag definitiert die Komponente.
<c-users>
<!-- html-code -->
<script lang="ts">
// riot component deklaration
</script>
</c-users>
Oben steht der HTML-Code, der beim Einbinden der Komponente in die Seite eingefügt wird. Im HTML kann sowohl auf statische props als auch auf den veränderbaren state zugegriffen werden.
Zugriff auf ein props-Feld:
<h5>{ props.title }</h5>
Zugriff auf ein Feld im State:
<template if="{ state.loading }" >
<div class="overlay blur"></div>
<div class="absolute middle center">
<progress class="circle"></progress>
</div>
</template>
Sowohl der state als auch Funktionen, die von HTML-Komponenten aufgerufen werden, müssen im Interface der RiotComponent weiter unten definiert sein.
Beispiel:
<button class="small-round error" onclick="{ () => deleteUser(user) }">
<i>delete</i>
</button>
...
<script lang="ts">
...
export interface cUsersComponent extends RiotComponent<cUsersProps, cUsersState>, Record<string, any> {
state: cUsersState
addUser(e: any): void
deleteUser(user: UserModel): void
}
Unter dem HTML-Code steht die Deklaration der RiotComponent; diese nutzt die props und state Interfaces aus der zugehörigen types.ts.
<c-users>
<!-- html-code -->
<script lang="ts">
// imports
// export interface cUsersComponent extends RiotComponent<cUsersProps, cUsersState>, Record<string, any> { ... }
// export default withTypes<cUsersComponent> ({ ... })
</script>
</c-users>
Es wird einerseits das Interface der RiotComponent exportiert, andererseits die Implementierung des Interfaces als default withTypes<>.
Beipiel:
<script lang="ts">
import { RiotComponent, component, withTypes } from 'riot'; // import RiotComponent
import { cUsersProps, cUsersState } from './types'; // import props, state der Komponente
import { UserController } from '../../controller/UserController'; // import des Controllers zur Komponente
import { UserModel } from '../../model/UserModel'; // import der Model-Repräsentation eines Eintrags
import { UserRolesEnum } from "../../../../typescript/client/models/User"; // weitere Imports ...
import { EmployeeModel } from "../../model/EmployeeModel";
import cCheckbox from "../checkbox/c-checkbox.riot"; // import einer anderen RiotComponent
import cSelect from "../select/c-select.riot";
// Deklaration des RiotComponent Interfaces
export interface cUsersComponent extends RiotComponent<cUsersProps, cUsersState>, Record<string, any> {
state: cUsersState
addUser(e: any): void
deleteUser(user: UserModel): void
}
// Implementierung des RiotComponent Interfaces
export default withTypes<cUsersComponent> ({
// Deklaration verwendeter RiotComponents
components: {
cCheckbox,
cSelect
},
// Erzeugung des verwendeten Controllers
controller: new UserController(),
// Initialisierung des states
// auch wenn der state später durch den Controller gesetzt und verändert wird,
// muss es hier eine Initialisierung geben, sonst gibt es UI Fehler
state: {
users: new Array<UserModel>(),
employees: new Array<EmployeeModel>(),
loading: true
},
// Guideline: In der onBeforeMount Methode wird der controller initialisiert
// und die RiotComponent selbst wird dem Controller bekannt gemacht.
// Dadurch wird der Controller in die Lage versetzt, ein Update des UIs auszulösen.
// Außerdem wird dem Controller der allgemeine messageHandler übergeben,
// der jeder RiotComponent als Property reingereicht wird.
// Dadurch können im Controller Info- und Fehlermeldungen abgesetzt werden.
async onBeforeMount(props: cUsersProps, state: cUsersState) {
await this.controller.initialize(this, props.messageHandler);
},
// In der onBeforeUpdate Methode können UI Felder ggf. zurückgesetzt/geleert werden
async onBeforeUpdate(props: cUsersProps, state: cUsersState) {
(document.getElementById("email") as HTMLInputElement).value = "";
},
// Guideline: Die Logik, was passieren soll, wenn das Event addUser ausgelöst wird,
// soll im Controller implementiert werden. Die Methode hier dient nur als Vermittler
// zwischen RiotComponent und Controller.
// e.preventDefault verhindert in diversen Fällen/bei geschachtelten Elementen,
// dass ein Event mehrfach behandelt wird.
async addUser(e: any) {
e.preventDefault();
await this.controller.createUser(e.target.email.value);
},
async deleteUser(user: UserModel) {
await this.controller.deleteUser(user);
}
})
</script>
Zusammenspiel mit dem Controller:
Über die initialize Methode wird dem Controller die RiotComponent bekannt gemacht, wodurch der Controller in die Lage versetzt wird, UI Updates auszulösen. Außerdem wird dem Controller der allgemeine messageHandler übergeben, der jeder RiotComponent als Property reingereicht wird, so dass der Controller Info- und Fehlermeldungen absetzen kann.
Typischer Aufbau einer initialize Methode:
async initialize(component: RiotComponent, messageHandler: MessageController): Promise<void> {
let that = this;
// 1.) die RiotComponent in den Attributen speichern
that.component = component;
// 2.) den messageHandler in den Attributen speichern
that.messageHandler = messageHandler;
// 3.) benötigte Daten für die Komponente asynchron laden
that.state.employees = await EmployeeController.getEmployeeListWithoutContracts();
await await that.getUserList()
.then((users: UserModel[]) => {
// sind die Daten geladen, den state aktualisieren,
// loading auf false setzen und ein UI update auslösen
that.state.users = users;
that.state.loading = false;
if (that.component) {
that.component.update(that.state);
}
});
}