No description
  • Java 27.1%
  • CSS 25%
  • JavaScript 20.4%
  • TypeScript 13.6%
  • Riot 11.6%
  • Other 2.3%
Find a file
2026-07-10 15:15:42 +02:00
webapp Merge remote-tracking branch 'git.app.tivano.net/main' 2026-07-10 15:15:42 +02:00
.gitignore .gitignore: /webapp/src/main/webapp/public 2026-03-10 11:02:34 +01:00
pom.xml basic webapp setup: project dependencies, RestApiServlet 2026-03-02 12:48:49 +01:00
README.md README.md 2026-04-02 16:45:46 +02:00

TivAppX


Tivano Showcase App: Zeiterfassung

Table of Contents

  1. General Info
  2. Technologien
  3. Installation
  4. Konfiguration
  5. Anwender-Dokumentation
  6. Entwickler-Dokumentation

General Info


Zeiterfassungs-App als Showcase Projekt.

Technologien


A list of technologies used within the project:

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
  • de.tivano.app.webapp
    • RestApiServlet: das CXFNonSpringJaxrsServlet, welches die REST Services zur Verfügung stellt
    • LoginFilter: ein HttpFilter, der sicherstellt, dass nur angemeldete Benutzer auf die Applikation Zugriff haben, und die Anmeldung per OpenID Connect implementiert
    • OpenIDConnectConfiguration: 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

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-Applikation
  • login.html: Login-Seite für Anmeldung per OpenID Connect
  • logout.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.ts
  • c-snackbar
    Die Snackbar ist in die Hauptseite eingebunden und blendet Info- und Fehlermeldungen ein. Sie kann von allen Modul-Komponenten getriggert werden.
    Steuerung: MessageController.ts
  • c-times
    Das Modul "Zeiterfassung".
    Steuerung: TimeRecordController.ts
    Repräsentation eines Eintrags: TimeRecordModel.ts
  • c-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-contracts
  • c-contracts
    Verträge für einen Mitarbeiter
    Steuerung: ContractController.ts
    Repräsentation eines Eintrags: ContractModel.ts
    Eingebundene Komponenten: c-contracts
  • c-users
    Das Modul "User"
    Steuerung: UserController.ts
    Repräsentation eines Eintrags: UserModel.ts
    Eingebundene Komponenten: c-checkbox, c-select
  • c-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);
                }
            });
    }