Compare commits
49 Commits
acb96dc1c6
...
feature/TT
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29a0336c39 | ||
|
|
78f548eb28 | ||
|
|
01fa55553b | ||
|
|
804bb9dc0a | ||
|
|
0d2334da48 | ||
|
|
f24fef9b16 | ||
|
|
13d8970b7a | ||
|
|
eed815c646 | ||
|
|
5bacd33ea0 | ||
|
|
9bf604719a | ||
|
|
8dcd02c593 | ||
|
|
9efdacea9e | ||
|
|
587e2dbdf2 | ||
|
|
4f3a5d4d66 | ||
|
|
590babea9e | ||
|
|
4e9fd80296 | ||
|
|
eb60e5af16 | ||
|
|
eb02ef4490 | ||
|
|
3c705b6ad8 | ||
|
|
464d644c4f | ||
|
|
c04e4f85fc | ||
|
|
d3c41b3e6c | ||
|
|
b1202577a8 | ||
|
|
39af511bbd | ||
|
|
32dfb0d6b4 | ||
|
|
e70a559f1d | ||
|
|
bbf3e4a20e | ||
|
|
64abe911ee | ||
|
|
d3265d364c | ||
|
|
8be2920952 | ||
|
|
98db0678b4 | ||
|
|
f88274be3d | ||
|
|
6d56458bb9 | ||
|
|
daf43b7ce2 | ||
| 6b72da2d1e | |||
|
|
0361bd7bed | ||
| 8435b976d6 | |||
| ac3993545e | |||
| c62127e24d | |||
| f36fd63a04 | |||
| 75ddbecc79 | |||
| 7de22a723e | |||
| a64d56bbd9 | |||
| 7446839a7b | |||
|
|
2c7dd8c8f2 | ||
|
|
44672876be | ||
|
|
e828e83991 | ||
|
|
6b57a0c5e5 | ||
|
|
0f005ca130 |
@@ -2,37 +2,60 @@ name: Build-und-Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ci-java21-node20:latest
|
||||
|
||||
steps:
|
||||
- name: 📥 Repository auschecken
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: 🧼 IP-Adresse von Gitea zu /etc/hosts hinzufügen
|
||||
run: echo "172.26.0.2 gitea" >> /etc/hosts
|
||||
|
||||
- name: 🛠 Baue das Projekt (ohne Tests)
|
||||
run: ./gradlew clean build -x test
|
||||
|
||||
- name: 📦 JAR ins Shared Volume kopieren und archivieren
|
||||
- name: Map gitea into /etc/hosts (ohne ip)
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gw_hex="$(awk '$2=="00000000" {print $3; exit}' /proc/net/route || true)"
|
||||
if [ -n "${gw_hex:-}" ]; then
|
||||
gw="$(printf "%d.%d.%d.%d" 0x${gw_hex:6:2} 0x${gw_hex:4:2} 0x${gw_hex:2:2} 0x${gw_hex:0:2})"
|
||||
else
|
||||
gw="172.17.0.1"
|
||||
fi
|
||||
echo "$gw gitea" >> /etc/hosts
|
||||
getent hosts gitea || true
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: 🧰 Gradle vorbereiten & Version
|
||||
shell: bash
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
test -f ./gradlew || { echo "gradlew fehlt – bitte lokal 'gradle wrapper' ausführen und committen."; exit 1; }
|
||||
chmod +x ./gradlew
|
||||
./gradlew --version
|
||||
|
||||
- name: 🏗️ Build Backend + Vaadin (Production)
|
||||
shell: bash
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
# Vaadin im Production-Mode bündeln
|
||||
./gradlew clean build -x test --no-daemon -Pvaadin.productionMode
|
||||
ls -al build/libs
|
||||
JAR="$(ls -1 build/libs/*.jar | head -n1)"
|
||||
echo "Gebautes JAR: $JAR"
|
||||
# kurze Sichtprüfung, dass Frontend drin ist
|
||||
jar tf "$JAR" | grep -E 'VAADIN|static/|index\.html' | head || true
|
||||
|
||||
- name: 📦 Nach /shared_jar_data deployen (fester Name + Archiv)
|
||||
shell: bash
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
ts="$(date +'%Y%m%d-%H%M%S')"
|
||||
mkdir -p /shared_jar_data /shared_jar_data/archive
|
||||
|
||||
# Zeitstempel im Format YYYYMMDD-HHMMSS
|
||||
timestamp=$(date +"%Y%m%d-%H%M%S")
|
||||
|
||||
# JAR-Datei kopieren (aktuelle Version)
|
||||
cp build/libs/MyTimeTracker-0.0.1-SNAPSHOT.jar /shared_jar_data/MyTimeTracker-0.0.1-SNAPSHOT.jar
|
||||
|
||||
# JAR-Datei archivieren
|
||||
cp build/libs/MyTimeTracker-0.0.1-SNAPSHOT.jar /shared_jar_data/archive/MyTimeTracker-$timestamp.jar
|
||||
|
||||
- name: 🚦 Neustart der Anwendung triggern
|
||||
run: |
|
||||
mkdir -p /shared_trigger_dir
|
||||
touch /shared_trigger_dir/restart-requested
|
||||
SRC="$(ls -1 build/libs/*.jar | head -n1)"
|
||||
# WICHTIG: Fester Name, den docker-compose startet
|
||||
cp -f "$SRC" "/shared_jar_data/MyTimeTracker-0.0.1-SNAPSHOT.jar"
|
||||
# Zusätzlich archivieren
|
||||
cp -f "$SRC" "/shared_jar_data/archive/MyTimeTracker-${ts}.jar"
|
||||
ls -al /shared_jar_data | tail -n +1
|
||||
|
||||
14
.gitea/workflows/restart.yaml
Normal file
14
.gitea/workflows/restart.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
name: Docker Restart Manuell
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
restart-docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Docker Compose Restart
|
||||
run: |
|
||||
cd /home/docker/apps/mytimetracker
|
||||
docker compose down
|
||||
docker compose up -d
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -93,3 +93,4 @@ coverage/
|
||||
*.bak
|
||||
*.tmpdata/*.mv.db
|
||||
src/main/frontend/generated/
|
||||
/data/*.mv.db
|
||||
|
||||
@@ -10,7 +10,7 @@ version = '0.0.1-SNAPSHOT'
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion = JavaLanguageVersion.of(17)
|
||||
languageVersion = JavaLanguageVersion.of(21)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ configurations {
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
maven { url 'https://maven.vaadin.com/vaadin-addons' }
|
||||
}
|
||||
|
||||
ext {
|
||||
@@ -32,6 +33,7 @@ dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-security'
|
||||
implementation 'com.vaadin:vaadin-spring-boot-starter'
|
||||
implementation("org.vaadin.crudui:crudui:7.2.0")
|
||||
|
||||
runtimeOnly 'com.mysql:mysql-connector-j:8.3.0'
|
||||
|
||||
|
||||
Binary file not shown.
@@ -78,7 +78,7 @@ insert into time_entries (comment,date,end_time,pause_minutes,start_time,status,
|
||||
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:223)
|
||||
at jdk.proxy4/jdk.proxy4.$Proxy164.save(Unknown Source)
|
||||
at de.nilzbu.mytimetracker.service.TimeEntryService.save(TimeEntryService.java:31)
|
||||
at de.nilzbu.mytimetracker.ui.view.TimeEntryView.lambda$configureButtons$9b1b5227$1(TimeEntryView.java:141)
|
||||
at de.nilzbu.mytimetracker.ui.view.timeentry.TimeEntryView.lambda$configureButtons$9b1b5227$1(TimeEntryView.java:141)
|
||||
at com.vaadin.flow.component.ComponentEventBus.fireEventForListener(ComponentEventBus.java:244)
|
||||
at com.vaadin.flow.component.ComponentEventBus.handleDomEvent(ComponentEventBus.java:501)
|
||||
at com.vaadin.flow.component.ComponentEventBus.lambda$addDomTrigger$dd1b7957$1(ComponentEventBus.java:303)
|
||||
@@ -280,7 +280,7 @@ insert into time_entries (comment,date,end_time,pause_minutes,start_time,status,
|
||||
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:223)
|
||||
at jdk.proxy4/jdk.proxy4.$Proxy164.save(Unknown Source)
|
||||
at de.nilzbu.mytimetracker.service.TimeEntryService.save(TimeEntryService.java:31)
|
||||
at de.nilzbu.mytimetracker.ui.view.TimeEntryView.lambda$configureButtons$9b1b5227$1(TimeEntryView.java:141)
|
||||
at de.nilzbu.mytimetracker.ui.view.timeentry.TimeEntryView.lambda$configureButtons$9b1b5227$1(TimeEntryView.java:141)
|
||||
at com.vaadin.flow.component.ComponentEventBus.fireEventForListener(ComponentEventBus.java:244)
|
||||
at com.vaadin.flow.component.ComponentEventBus.handleDomEvent(ComponentEventBus.java:501)
|
||||
at com.vaadin.flow.component.ComponentEventBus.lambda$addDomTrigger$dd1b7957$1(ComponentEventBus.java:303)
|
||||
@@ -482,7 +482,7 @@ insert into time_entries (comment,date,end_time,pause_minutes,start_time,status,
|
||||
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:223)
|
||||
at jdk.proxy4/jdk.proxy4.$Proxy164.save(Unknown Source)
|
||||
at de.nilzbu.mytimetracker.service.TimeEntryService.save(TimeEntryService.java:31)
|
||||
at de.nilzbu.mytimetracker.ui.view.TimeEntryView.lambda$configureButtons$9b1b5227$1(TimeEntryView.java:141)
|
||||
at de.nilzbu.mytimetracker.ui.view.timeentry.TimeEntryView.lambda$configureButtons$9b1b5227$1(TimeEntryView.java:141)
|
||||
at com.vaadin.flow.component.ComponentEventBus.fireEventForListener(ComponentEventBus.java:244)
|
||||
at com.vaadin.flow.component.ComponentEventBus.handleDomEvent(ComponentEventBus.java:501)
|
||||
at com.vaadin.flow.component.ComponentEventBus.lambda$addDomTrigger$dd1b7957$1(ComponentEventBus.java:303)
|
||||
@@ -684,7 +684,7 @@ insert into time_entries (comment,date,end_time,pause_minutes,start_time,status,
|
||||
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:223)
|
||||
at jdk.proxy4/jdk.proxy4.$Proxy164.save(Unknown Source)
|
||||
at de.nilzbu.mytimetracker.service.TimeEntryService.save(TimeEntryService.java:31)
|
||||
at de.nilzbu.mytimetracker.ui.view.TimeEntryView.lambda$configureButtons$9b1b5227$1(TimeEntryView.java:141)
|
||||
at de.nilzbu.mytimetracker.ui.view.timeentry.TimeEntryView.lambda$configureButtons$9b1b5227$1(TimeEntryView.java:141)
|
||||
at com.vaadin.flow.component.ComponentEventBus.fireEventForListener(ComponentEventBus.java:244)
|
||||
at com.vaadin.flow.component.ComponentEventBus.handleDomEvent(ComponentEventBus.java:501)
|
||||
at com.vaadin.flow.component.ComponentEventBus.lambda$addDomTrigger$dd1b7957$1(ComponentEventBus.java:303)
|
||||
@@ -886,7 +886,7 @@ insert into time_entries (comment,date,end_time,pause_minutes,start_time,status,
|
||||
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:223)
|
||||
at jdk.proxy4/jdk.proxy4.$Proxy164.save(Unknown Source)
|
||||
at de.nilzbu.mytimetracker.service.TimeEntryService.save(TimeEntryService.java:31)
|
||||
at de.nilzbu.mytimetracker.ui.view.TimeEntryView.lambda$configureButtons$9b1b5227$1(TimeEntryView.java:141)
|
||||
at de.nilzbu.mytimetracker.ui.view.timeentry.TimeEntryView.lambda$configureButtons$9b1b5227$1(TimeEntryView.java:141)
|
||||
at com.vaadin.flow.component.ComponentEventBus.fireEventForListener(ComponentEventBus.java:244)
|
||||
at com.vaadin.flow.component.ComponentEventBus.handleDomEvent(ComponentEventBus.java:501)
|
||||
at com.vaadin.flow.component.ComponentEventBus.lambda$addDomTrigger$dd1b7957$1(ComponentEventBus.java:303)
|
||||
|
||||
@@ -1 +1,5 @@
|
||||
plugins {
|
||||
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.9.0'
|
||||
}
|
||||
|
||||
rootProject.name = 'MyTimeTracker'
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
:root {
|
||||
--lumo-font-size: 1rem;
|
||||
--lumo-font-size-xxxl: 3rem;
|
||||
@@ -9,11 +8,14 @@
|
||||
--lumo-font-size-s: 1rem;
|
||||
--lumo-font-size-xs: 0.875rem;
|
||||
--lumo-font-size-xxs: 0.8125rem;
|
||||
|
||||
--lumo-primary-text-color: rgb(9, 134, 24);
|
||||
--lumo-primary-color-50pct: rgba(9, 134, 24, 0.5);
|
||||
--lumo-primary-color-10pct: rgba(9, 134, 24, 0.1);
|
||||
--lumo-primary-color: hsl(127, 87%, 28%);
|
||||
--lumo-base-color: #dadcc79e;
|
||||
|
||||
--lumo-base-color: #dadcc7;
|
||||
|
||||
--lumo-tint-5pct: rgba(101, 105, 63, 0.05);
|
||||
--lumo-tint-10pct: rgba(101, 105, 63, 0.1);
|
||||
--lumo-tint-20pct: rgba(101, 105, 63, 0.2);
|
||||
@@ -24,9 +26,44 @@
|
||||
--lumo-tint-70pct: rgba(101, 105, 63, 0.7);
|
||||
--lumo-tint-80pct: rgba(101, 105, 63, 0.8);
|
||||
--lumo-tint-90pct: rgba(101, 105, 63, 0.9);
|
||||
--lumo-tint: #65693fed;
|
||||
--lumo-tint: #65693f;
|
||||
|
||||
--lumo-success-text-color: rgb(56, 204, 36);
|
||||
--lumo-success-color-50pct: rgba(56, 204, 36, 0.5);
|
||||
--lumo-success-color-10pct: rgba(56, 204, 36, 0.1);
|
||||
--lumo-success-color: hsl(113, 70%, 47%);
|
||||
|
||||
/* Lumo-Tokens, die IntelliJ anmeckert – mit sinnvollen Defaults */
|
||||
--lumo-space-xs: 0.25rem;
|
||||
--lumo-border-radius-s: 0.25rem;
|
||||
--lumo-contrast-10pct: rgba(0, 0, 0, 0.06);
|
||||
--lumo-size-s: 2rem;
|
||||
}
|
||||
|
||||
/* Deckender Hintergrund + Rahmen für die Filter-Wrapper im Header */
|
||||
.filter-cell {
|
||||
background: var(--lumo-base-color, #fff);
|
||||
padding: var(--lumo-space-xs, 0.25rem);
|
||||
border-radius: var(--lumo-border-radius-s, 0.25rem);
|
||||
/* zarte Kontur; Fallback, falls IntelliJ die Var. nicht kennt */
|
||||
box-shadow: inset 0 0 0 1px var(--lumo-contrast-10pct, rgba(0,0,0,0.06));
|
||||
display: block;
|
||||
background-clip: padding-box; /* verhindert „Ausbluten“ */
|
||||
}
|
||||
|
||||
vaadin-grid::part(header-row),
|
||||
|
||||
vaadin-grid::part(header-cell) {
|
||||
background: var(--lumo-base-color, #fff);
|
||||
box-shadow: 0 1px 0 0 var(--lumo-contrast-10pct, rgba(0,0,0,0.06));
|
||||
min-height: var(--lumo-size-s, 2rem); /* gleichmäßige Höhe */
|
||||
}
|
||||
|
||||
.filter-cell :is(vaadin-date-picker,
|
||||
vaadin-combo-box,
|
||||
vaadin-text-field,
|
||||
vaadin-checkbox,
|
||||
vaadin-button) {
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package de.nilzbu.mytimetracker.model;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public enum DayStatus {
|
||||
OFFICE("In office"),
|
||||
REMOTE("Remote work"),
|
||||
@@ -12,7 +15,4 @@ public enum DayStatus {
|
||||
this.displayName = displayName;
|
||||
}
|
||||
|
||||
public String getDisplayName() {
|
||||
return displayName;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ public class TimeEntry {
|
||||
*/
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(nullable = false)
|
||||
@Builder.Default
|
||||
private DayStatus status = DayStatus.REMOTE;
|
||||
|
||||
private String comment;
|
||||
|
||||
@@ -34,9 +34,11 @@ public class User {
|
||||
private Set<String> roles; // z.B. ROLE_USER, ROLE_ADMIN
|
||||
|
||||
@Column(nullable = false)
|
||||
@Builder.Default
|
||||
private boolean enabled = true;
|
||||
|
||||
@Column(nullable = false)
|
||||
@Builder.Default
|
||||
private boolean locked = false;
|
||||
|
||||
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||
|
||||
@@ -12,10 +12,6 @@ public interface TimeEntryRepository extends JpaRepository<TimeEntry, Long> {
|
||||
|
||||
List<TimeEntry> findAllByUserOrderByDateDesc(User user);
|
||||
|
||||
Optional<TimeEntry> findTopByUserOrderByDateDesc(User user);
|
||||
|
||||
boolean existsByUserAndDate(User user, LocalDate date);
|
||||
|
||||
Optional<TimeEntry> findByUserAndDate(User user, LocalDate date);
|
||||
|
||||
List<TimeEntry> findByUserAndDateBetween(User user, LocalDate start, LocalDate end);
|
||||
|
||||
@@ -12,10 +12,7 @@ public class SecurityConfig extends VaadinWebSecurity {
|
||||
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
// Standard-Konfiguration von Vaadin-Security erweitern
|
||||
super.configure(http);
|
||||
|
||||
// Login-View setzen
|
||||
setLoginView(http, LoginView.class);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,10 @@ public class TimeEntryService {
|
||||
return repository.findAllByUserOrderByDateDesc(user);
|
||||
}
|
||||
|
||||
public int getNumberOfEntriesForUser(User user) {
|
||||
return getEntriesForUser(user).size();
|
||||
}
|
||||
|
||||
public TimeEntry save(TimeEntry entry) {
|
||||
return repository.save(entry);
|
||||
}
|
||||
@@ -49,6 +53,10 @@ public class TimeEntryService {
|
||||
return repository.findByUserAndDateBetween(user, start, end);
|
||||
}
|
||||
|
||||
public int getNumberOfEntriesForMonth(User user, int year, int month) {
|
||||
return getEntriesForMonth(user, year, month).size();
|
||||
}
|
||||
|
||||
public List<TimeEntry> getEntriesForQuarter(User user, int year, int quarter) {
|
||||
int startMonth = (quarter - 1) * 3 + 1;
|
||||
LocalDate start = LocalDate.of(year, startMonth, 1);
|
||||
@@ -56,12 +64,20 @@ public class TimeEntryService {
|
||||
return repository.findByUserAndDateBetween(user, start, end);
|
||||
}
|
||||
|
||||
public int getNumberOfEntriesForQuarter(User user, int year, int month) {
|
||||
return getEntriesForQuarter(user, year, month).size();
|
||||
}
|
||||
|
||||
public List<TimeEntry> getEntriesForYear(User user, int year) {
|
||||
LocalDate start = LocalDate.of(year, 1, 1);
|
||||
LocalDate end = LocalDate.of(year, 12, 31);
|
||||
return repository.findByUserAndDateBetween(user, start, end);
|
||||
}
|
||||
|
||||
public int getNumberOfEntriesForYear(User user, int year) {
|
||||
return getEntriesForYear(user, year).size();
|
||||
}
|
||||
|
||||
public Optional<LocalDate> getEarliestEntryDate(User user) {
|
||||
return repository.findFirstByUserOrderByDateAsc(user)
|
||||
.map(TimeEntry::getDate);
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
package de.nilzbu.mytimetracker.ui.component;
|
||||
|
||||
import com.vaadin.flow.component.HasSize;
|
||||
import com.vaadin.flow.component.html.Div;
|
||||
import com.vaadin.flow.component.AttachEvent;
|
||||
import com.vaadin.flow.component.Tag;
|
||||
import com.vaadin.flow.component.dependency.JsModule;
|
||||
import com.vaadin.flow.component.dependency.NpmPackage;
|
||||
import com.vaadin.flow.component.Tag;
|
||||
import com.vaadin.flow.component.AttachEvent;
|
||||
import com.vaadin.flow.component.html.Div;
|
||||
import elemental.json.Json;
|
||||
import elemental.json.JsonArray;
|
||||
import elemental.json.JsonObject;
|
||||
@@ -31,9 +30,15 @@ public class ChartJsComponent extends Div {
|
||||
|
||||
@Override
|
||||
protected void onAttach(AttachEvent attachEvent) {
|
||||
renderChart();
|
||||
}
|
||||
|
||||
private void renderChart() {
|
||||
getElement().executeJs(
|
||||
"""
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.style.width = "100%";
|
||||
canvas.style.height = "100%";
|
||||
this.appendChild(canvas);
|
||||
const ctx = canvas.getContext('2d');
|
||||
new Chart(ctx, {
|
||||
@@ -54,47 +59,54 @@ public class ChartJsComponent extends Div {
|
||||
}
|
||||
});
|
||||
""",
|
||||
getChartTypeFromData(chartData),
|
||||
detectChartType(chartData),
|
||||
chartData,
|
||||
chartTitle
|
||||
);
|
||||
}
|
||||
|
||||
private String getChartTypeFromData(JsonObject data) {
|
||||
// Default zu bar wenn keine sinnvolle Bestimmung möglich ist
|
||||
/**
|
||||
* Bestimmt den Diagrammtyp basierend auf dem Dataset.
|
||||
*/
|
||||
private String detectChartType(JsonObject data) {
|
||||
try {
|
||||
JsonArray datasets = data.getArray("datasets");
|
||||
if (datasets.length() > 0) {
|
||||
JsonObject first = datasets.getObject(0);
|
||||
if (first.hasKey("fill") && !first.getBoolean("fill")) {
|
||||
JsonObject firstDataset = datasets.getObject(0);
|
||||
if (firstDataset.hasKey("fill") && !firstDataset.getBoolean("fill")) {
|
||||
return "line";
|
||||
}
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return "bar";
|
||||
}
|
||||
|
||||
// Balkendiagramm für Kategorien
|
||||
/**
|
||||
* Erstellt ein Balkendiagramm für Kategoriezählungen.
|
||||
*/
|
||||
public static JsonObject generateBarChartData(Map<String, Long> categoryCounts) {
|
||||
JsonObject data = Json.createObject();
|
||||
JsonArray labels = Json.createArray();
|
||||
JsonArray values = Json.createArray();
|
||||
JsonArray backgroundColors = Json.createArray();
|
||||
JsonArray colors = Json.createArray();
|
||||
|
||||
String[] defaultColors = {"#1e7e34", "#138496", "#ffc107", "#dc3545", "#6f42c1", "#20c997"};
|
||||
String[] defaultColors = {
|
||||
"#1e7e34", "#138496", "#ffc107", "#dc3545", "#6f42c1", "#20c997"
|
||||
};
|
||||
|
||||
int i = 0;
|
||||
int index = 0;
|
||||
for (Map.Entry<String, Long> entry : categoryCounts.entrySet()) {
|
||||
labels.set(i, Json.create(entry.getKey()));
|
||||
values.set(i, Json.create(entry.getValue()));
|
||||
backgroundColors.set(i, Json.create(defaultColors[i % defaultColors.length]));
|
||||
i++;
|
||||
labels.set(index, Json.create(entry.getKey()));
|
||||
values.set(index, Json.create(entry.getValue()));
|
||||
colors.set(index, Json.create(defaultColors[index % defaultColors.length]));
|
||||
index++;
|
||||
}
|
||||
|
||||
JsonObject dataset = Json.createObject();
|
||||
dataset.put("label", "Einträge nach Kategorie");
|
||||
dataset.put("label", "Entries by Category");
|
||||
dataset.put("data", values);
|
||||
dataset.put("backgroundColor", backgroundColors);
|
||||
dataset.put("backgroundColor", colors);
|
||||
|
||||
JsonArray datasets = Json.createArray();
|
||||
datasets.set(0, dataset);
|
||||
@@ -105,19 +117,21 @@ public class ChartJsComponent extends Div {
|
||||
return data;
|
||||
}
|
||||
|
||||
// Liniendiagramm für Überstunden-Saldo (in Stunden)
|
||||
public static JsonObject generateLineChartData(List<LocalDate> dates, List<Double> saldoValues) {
|
||||
/**
|
||||
* Erstellt ein Liniendiagramm zur Darstellung des Überzeit-Saldos.
|
||||
*/
|
||||
public static JsonObject generateLineChartData(List<LocalDate> dates, List<Double> balanceValues) {
|
||||
JsonObject data = Json.createObject();
|
||||
JsonArray labels = Json.createArray();
|
||||
JsonArray values = Json.createArray();
|
||||
|
||||
for (int i = 0; i < dates.size(); i++) {
|
||||
labels.set(i, Json.create(dates.get(i).toString()));
|
||||
values.set(i, Json.create(saldoValues.get(i)));
|
||||
values.set(i, Json.create(balanceValues.get(i)));
|
||||
}
|
||||
|
||||
JsonObject dataset = Json.createObject();
|
||||
dataset.put("label", "Überstunden-Saldo (h)");
|
||||
dataset.put("label", "Overtime Balance (hours)");
|
||||
dataset.put("data", values);
|
||||
dataset.put("borderColor", "rgb(54, 162, 235)");
|
||||
dataset.put("tension", 0.1);
|
||||
|
||||
@@ -8,9 +8,9 @@ import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.router.RouterLink;
|
||||
import com.vaadin.flow.server.VaadinServletRequest;
|
||||
import com.vaadin.flow.server.VaadinServletResponse;
|
||||
import de.nilzbu.mytimetracker.ui.view.MainView;
|
||||
import de.nilzbu.mytimetracker.ui.view.TimeEntryView;
|
||||
import de.nilzbu.mytimetracker.ui.view.UserAdminView;
|
||||
import de.nilzbu.mytimetracker.ui.view.DashboardOverView;
|
||||
import de.nilzbu.mytimetracker.ui.view.timeentry.TimeEntryView;
|
||||
import de.nilzbu.mytimetracker.ui.view.UserManagementView;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
@@ -19,44 +19,46 @@ import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
|
||||
|
||||
@PermitAll
|
||||
|
||||
public class MainLayout extends AppLayout {
|
||||
|
||||
public MainLayout() {
|
||||
createHeader();
|
||||
buildHeader();
|
||||
}
|
||||
|
||||
private void createHeader() {
|
||||
H1 logo = new H1("Time Tracker");
|
||||
logo.getStyle()
|
||||
private void buildHeader() {
|
||||
H1 title = new H1("Time Tracker");
|
||||
title.getStyle()
|
||||
.set("font-size", "var(--lumo-font-size-l)")
|
||||
.set("margin", "0");
|
||||
|
||||
RouterLink homeLink = new RouterLink("Dashboard", MainView.class);
|
||||
RouterLink dashboardLink = new RouterLink("Dashboard", DashboardOverView.class);
|
||||
RouterLink bookingsLink = new RouterLink("Bookings", TimeEntryView.class);
|
||||
RouterLink userAdminView = new RouterLink("Admin", UserAdminView.class);
|
||||
homeLink.getStyle().set("margin-left", "2em");
|
||||
RouterLink adminLink = new RouterLink("Admin", UserManagementView.class);
|
||||
|
||||
dashboardLink.getStyle().set("margin-left", "2em");
|
||||
bookingsLink.getStyle().set("margin-left", "2em");
|
||||
userAdminView.getStyle().set("margin-left", "2em");
|
||||
adminLink.getStyle().set("margin-left", "2em");
|
||||
|
||||
Button logoutButton = new Button("Logout", event -> handleLogout());
|
||||
Button logoutButton = new Button("Logout", event -> performLogout());
|
||||
|
||||
HorizontalLayout header = new HorizontalLayout(logo, homeLink, bookingsLink, userAdminView, logoutButton);
|
||||
header.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER);
|
||||
header.setWidthFull();
|
||||
header.setPadding(true);
|
||||
header.setSpacing(true);
|
||||
HorizontalLayout headerLayout = new HorizontalLayout(
|
||||
title, dashboardLink, bookingsLink, adminLink, logoutButton
|
||||
);
|
||||
headerLayout.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER);
|
||||
headerLayout.setWidthFull();
|
||||
headerLayout.setPadding(true);
|
||||
headerLayout.setSpacing(true);
|
||||
|
||||
addToNavbar(header);
|
||||
addToNavbar(headerLayout);
|
||||
}
|
||||
|
||||
private void handleLogout() {
|
||||
private void performLogout() {
|
||||
HttpServletRequest request = VaadinServletRequest.getCurrent().getHttpServletRequest();
|
||||
HttpServletResponse response = VaadinServletResponse.getCurrent().getHttpServletResponse();
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
|
||||
if (auth != null) {
|
||||
new SecurityContextLogoutHandler().logout(request, response, auth);
|
||||
if (authentication != null) {
|
||||
new SecurityContextLogoutHandler().logout(request, response, authentication);
|
||||
}
|
||||
|
||||
getUI().ifPresent(ui -> ui.getPage().setLocation("/login"));
|
||||
|
||||
@@ -4,29 +4,34 @@ import com.vaadin.flow.component.Component;
|
||||
import com.vaadin.flow.component.combobox.ComboBox;
|
||||
import com.vaadin.flow.component.html.Div;
|
||||
import com.vaadin.flow.component.html.H2;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.component.tabs.Tab;
|
||||
import com.vaadin.flow.component.tabs.Tabs;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import de.nilzbu.mytimetracker.model.DayStatus;
|
||||
import de.nilzbu.mytimetracker.model.TimeEntry;
|
||||
import de.nilzbu.mytimetracker.model.User;
|
||||
import de.nilzbu.mytimetracker.repository.UserRepository;
|
||||
import de.nilzbu.mytimetracker.service.TimeEntryService;
|
||||
import de.nilzbu.mytimetracker.ui.component.ChartJsComponent;
|
||||
import de.nilzbu.mytimetracker.ui.layout.MainLayout;
|
||||
import de.nilzbu.mytimetracker.ui.widget.KeyFigureWidget;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.*;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
@PermitAll
|
||||
@PageTitle("Dashboard")
|
||||
@Route(value = "", layout = MainLayout.class)
|
||||
public class MainView extends VerticalLayout {
|
||||
public class DashboardOverView extends VerticalLayout {
|
||||
|
||||
private final TimeEntryService timeEntryService;
|
||||
private final UserRepository userRepository;
|
||||
@@ -39,7 +44,10 @@ public class MainView extends VerticalLayout {
|
||||
private final Div filterContainer = new Div();
|
||||
private final Div contentContainer = new Div();
|
||||
|
||||
public MainView(TimeEntryService timeEntryService, UserRepository userRepository) {
|
||||
private final BiFunction<List<TimeEntry>, DayStatus, String> calculateDaysWithDayStatus =
|
||||
(scopedEntries, status) -> "%d".formatted(scopedEntries.stream().filter(entry -> entry.getStatus().equals(status)).count());
|
||||
|
||||
public DashboardOverView(TimeEntryService timeEntryService, UserRepository userRepository) {
|
||||
this.timeEntryService = timeEntryService;
|
||||
this.userRepository = userRepository;
|
||||
|
||||
@@ -50,13 +58,11 @@ public class MainView extends VerticalLayout {
|
||||
}
|
||||
|
||||
private void configureLayout() {
|
||||
setSizeFull();
|
||||
setPadding(false);
|
||||
setSpacing(false);
|
||||
setMargin(false);
|
||||
|
||||
contentContainer.setSizeFull();
|
||||
contentContainer.getStyle().set("overflow", "auto");
|
||||
setFlexGrow(1, contentContainer);
|
||||
}
|
||||
|
||||
@@ -121,6 +127,7 @@ public class MainView extends VerticalLayout {
|
||||
default -> timeEntryService.getEntriesForUser(currentUser);
|
||||
};
|
||||
|
||||
renderKeyFigures(entries);
|
||||
renderCharts(entries);
|
||||
}
|
||||
|
||||
@@ -139,16 +146,19 @@ public class MainView extends VerticalLayout {
|
||||
private void renderCharts(List<TimeEntry> scopedEntries) {
|
||||
List<TimeEntry> allEntries = timeEntryService.getEntriesForUser(currentUser);
|
||||
|
||||
VerticalLayout chartLayout = new VerticalLayout();
|
||||
chartLayout.setSizeFull();
|
||||
HorizontalLayout chartLayout = new HorizontalLayout();
|
||||
chartLayout.setPadding(false);
|
||||
chartLayout.setSpacing(true);
|
||||
|
||||
Component categoryChart = createCategoryBarChart(scopedEntries);
|
||||
Component overtimeChart = createOvertimeLineChart(allEntries, scopedEntries);
|
||||
categoryChart.getStyle().setBackgroundColor("lightgray");
|
||||
categoryChart.getStyle().setBorder("1px solid black" );
|
||||
categoryChart.getStyle().setMargin("10px");
|
||||
|
||||
categoryChart.getStyle().set("minHeight", "45vh");
|
||||
overtimeChart.getStyle().set("minHeight", "50vh");
|
||||
Component overtimeChart = createOvertimeLineChart(allEntries, scopedEntries);
|
||||
overtimeChart.getStyle().setBackgroundColor("lightgray");
|
||||
overtimeChart.getStyle().setBorder("1px solid black" );
|
||||
overtimeChart.getStyle().setMargin("10px");
|
||||
|
||||
chartLayout.add(categoryChart, overtimeChart);
|
||||
chartLayout.setFlexGrow(1, categoryChart);
|
||||
@@ -157,6 +167,54 @@ public class MainView extends VerticalLayout {
|
||||
contentContainer.add(chartLayout);
|
||||
}
|
||||
|
||||
private void renderKeyFigures(List<TimeEntry> scopedEntries) {
|
||||
HorizontalLayout keyFigureLayout = new HorizontalLayout();
|
||||
|
||||
KeyFigureWidget workingDays = new KeyFigureWidget("Working Days", "" + scopedEntries.size());
|
||||
|
||||
KeyFigureWidget remoteDays = new KeyFigureWidget(
|
||||
"Remote Days", calculateDaysWithDayStatus.apply(scopedEntries, DayStatus.REMOTE)
|
||||
);
|
||||
|
||||
KeyFigureWidget officeDays = new KeyFigureWidget(
|
||||
"Office Days",
|
||||
calculateDaysWithDayStatus.apply(scopedEntries, DayStatus.OFFICE)
|
||||
);
|
||||
|
||||
KeyFigureWidget vacationDays = new KeyFigureWidget(
|
||||
"Vacation Days",
|
||||
calculateDaysWithDayStatus.apply(scopedEntries, DayStatus.VACATION)
|
||||
);
|
||||
|
||||
KeyFigureWidget sickDays = new KeyFigureWidget(
|
||||
"Sick Days",
|
||||
calculateDaysWithDayStatus.apply(scopedEntries, DayStatus.SICK)
|
||||
);
|
||||
|
||||
KeyFigureWidget timeBalance = new KeyFigureWidget(
|
||||
"Time Balance",
|
||||
getTimeBalance(scopedEntries)
|
||||
);
|
||||
|
||||
keyFigureLayout.getStyle().setBackgroundColor("lightgray");
|
||||
keyFigureLayout.getStyle().setBorder("1px solid black" );
|
||||
keyFigureLayout.getStyle().setMargin("10px");
|
||||
|
||||
keyFigureLayout.add(timeBalance, workingDays, remoteDays, officeDays, vacationDays, sickDays);
|
||||
|
||||
contentContainer.add(keyFigureLayout);
|
||||
}
|
||||
|
||||
@NotNull
|
||||
private String getTimeBalance(List<TimeEntry> scopedEntries) {
|
||||
Integer minutes = scopedEntries.stream()
|
||||
.map(entry -> timeEntryService.calculateNetWorkMinutes(entry) - entry.getTargetMinutes())
|
||||
.reduce(0, Integer::sum);
|
||||
|
||||
double hours = minutes / 60.0;
|
||||
return "%.2f h".formatted(hours);
|
||||
}
|
||||
|
||||
private Component createCategoryBarChart(List<TimeEntry> entries) {
|
||||
Map<String, Long> statusCount = entries.stream()
|
||||
.collect(Collectors.groupingBy(e -> e.getStatus().name(), Collectors.counting()));
|
||||
@@ -191,6 +249,7 @@ public class MainView extends VerticalLayout {
|
||||
|
||||
return new ChartJsComponent(
|
||||
ChartJsComponent.generateLineChartData(scopedDates, saldoValues),
|
||||
"Overtime Balance Over Time (in hours)");
|
||||
"Overtime Balance Over Time (in hours)"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,206 +0,0 @@
|
||||
package de.nilzbu.mytimetracker.ui.view;
|
||||
|
||||
import com.vaadin.flow.component.button.Button;
|
||||
import com.vaadin.flow.component.combobox.ComboBox;
|
||||
import com.vaadin.flow.component.datepicker.DatePicker;
|
||||
import com.vaadin.flow.component.grid.Grid;
|
||||
import com.vaadin.flow.component.notification.Notification;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.component.textfield.NumberField;
|
||||
import com.vaadin.flow.component.textfield.TextArea;
|
||||
import com.vaadin.flow.component.timepicker.TimePicker;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import de.nilzbu.mytimetracker.model.DayStatus;
|
||||
import de.nilzbu.mytimetracker.model.TimeEntry;
|
||||
import de.nilzbu.mytimetracker.model.User;
|
||||
import de.nilzbu.mytimetracker.repository.UserRepository;
|
||||
import de.nilzbu.mytimetracker.service.TimeEntryService;
|
||||
import de.nilzbu.mytimetracker.ui.layout.MainLayout;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Route(value = "time-entry", layout = MainLayout.class)
|
||||
@PageTitle("Time Entry")
|
||||
@PermitAll
|
||||
public class TimeEntryView extends VerticalLayout {
|
||||
|
||||
private final TimeEntryService timeEntryService;
|
||||
private final UserRepository userRepository;
|
||||
private User currentUser;
|
||||
|
||||
private final DatePicker datePicker = new DatePicker("Date");
|
||||
private final TimePicker startTimePicker = new TimePicker("Start Time");
|
||||
private final TimePicker endTimePicker = new TimePicker("End Time");
|
||||
private final NumberField breakField = new NumberField("Break (min)");
|
||||
private final NumberField targetField = new NumberField("Target Time (min)");
|
||||
private final TextArea commentArea = new TextArea("Comment");
|
||||
private final ComboBox<DayStatus> statusCombo = new ComboBox<>("Status");
|
||||
|
||||
private final Button saveBtn = new Button("Save");
|
||||
private final Button updateBtn = new Button("Update");
|
||||
private final Button deleteBtn = new Button("Delete");
|
||||
|
||||
private final Grid<TimeEntry> entryGrid = new Grid<>(TimeEntry.class, false);
|
||||
private TimeEntry selectedEntry = null;
|
||||
|
||||
public TimeEntryView(TimeEntryService timeEntryService, UserRepository userRepository) {
|
||||
this.timeEntryService = timeEntryService;
|
||||
this.userRepository = userRepository;
|
||||
|
||||
initializeUser();
|
||||
configureFormFields();
|
||||
configureButtons();
|
||||
configureGrid();
|
||||
|
||||
datePicker.addValueChangeListener(e -> toggleSaveButton());
|
||||
|
||||
add(buildFormLayout(), buildButtonLayout(), entryGrid);
|
||||
refreshGrid();
|
||||
toggleSaveButton();
|
||||
}
|
||||
|
||||
private void initializeUser() {
|
||||
String username = SecurityContextHolder.getContext().getAuthentication().getName();
|
||||
this.currentUser = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new RuntimeException("User not found"));
|
||||
}
|
||||
|
||||
private void configureFormFields() {
|
||||
datePicker.setValue(LocalDate.now());
|
||||
breakField.setValue(30.0);
|
||||
targetField.setValue(480.0);
|
||||
statusCombo.setItems(DayStatus.values());
|
||||
statusCombo.setValue(DayStatus.REMOTE);
|
||||
}
|
||||
|
||||
private HorizontalLayout buildFormLayout() {
|
||||
return new HorizontalLayout(datePicker, startTimePicker, endTimePicker,
|
||||
breakField, targetField, commentArea, statusCombo);
|
||||
}
|
||||
|
||||
private HorizontalLayout buildButtonLayout() {
|
||||
updateBtn.setEnabled(false);
|
||||
deleteBtn.setEnabled(false);
|
||||
return new HorizontalLayout(saveBtn, updateBtn, deleteBtn);
|
||||
}
|
||||
|
||||
private void configureButtons() {
|
||||
saveBtn.addClickListener(e -> saveEntry());
|
||||
updateBtn.addClickListener(e -> updateEntry());
|
||||
deleteBtn.addClickListener(e -> deleteEntry());
|
||||
}
|
||||
|
||||
private void saveEntry() {
|
||||
if (!isDateAvailable(datePicker.getValue())) {
|
||||
Notification.show("An entry already exists for this date.");
|
||||
return;
|
||||
}
|
||||
|
||||
TimeEntry entry = TimeEntry.builder()
|
||||
.user(currentUser)
|
||||
.date(datePicker.getValue())
|
||||
.startTime(startTimePicker.getValue())
|
||||
.endTime(endTimePicker.getValue())
|
||||
.pauseMinutes(breakField.getValue().intValue())
|
||||
.targetMinutes(targetField.getValue().intValue())
|
||||
.status(statusCombo.getValue())
|
||||
.comment(commentArea.getValue())
|
||||
.build();
|
||||
|
||||
timeEntryService.save(entry);
|
||||
clearForm();
|
||||
refreshGrid();
|
||||
Notification.show("Entry saved");
|
||||
}
|
||||
|
||||
private void updateEntry() {
|
||||
if (selectedEntry == null) return;
|
||||
|
||||
selectedEntry.setDate(datePicker.getValue());
|
||||
selectedEntry.setStartTime(startTimePicker.getValue());
|
||||
selectedEntry.setEndTime(endTimePicker.getValue());
|
||||
selectedEntry.setPauseMinutes(breakField.getValue().intValue());
|
||||
selectedEntry.setTargetMinutes(targetField.getValue().intValue());
|
||||
selectedEntry.setStatus(statusCombo.getValue());
|
||||
selectedEntry.setComment(commentArea.getValue());
|
||||
|
||||
timeEntryService.save(selectedEntry);
|
||||
clearForm();
|
||||
refreshGrid();
|
||||
Notification.show("Entry updated");
|
||||
}
|
||||
|
||||
private void deleteEntry() {
|
||||
if (selectedEntry == null) return;
|
||||
|
||||
timeEntryService.delete(selectedEntry);
|
||||
clearForm();
|
||||
refreshGrid();
|
||||
Notification.show("Entry deleted");
|
||||
}
|
||||
|
||||
private void configureGrid() {
|
||||
entryGrid.addColumn(TimeEntry::getDate).setHeader("Date");
|
||||
entryGrid.addColumn(TimeEntry::getStartTime).setHeader("Start");
|
||||
entryGrid.addColumn(TimeEntry::getEndTime).setHeader("End");
|
||||
entryGrid.addColumn(TimeEntry::getPauseMinutes).setHeader("Break (min)");
|
||||
entryGrid.addColumn(TimeEntry::getTargetMinutes).setHeader("Target (min)");
|
||||
entryGrid.addColumn(timeEntryService::calculateNetWorkMinutes).setHeader("Actual (min)");
|
||||
entryGrid.addColumn(TimeEntry::getStatus).setHeader("Status");
|
||||
entryGrid.addColumn(TimeEntry::getComment).setHeader("Comment");
|
||||
entryGrid.addColumn(timeEntryService::calculateDeviation).setHeader("Deviation (min)");
|
||||
|
||||
entryGrid.asSingleSelect().addValueChangeListener(event -> populateForm(event.getValue()));
|
||||
}
|
||||
|
||||
private void refreshGrid() {
|
||||
entryGrid.setItems(timeEntryService.getEntriesForUser(currentUser));
|
||||
}
|
||||
|
||||
private void populateForm(TimeEntry entry) {
|
||||
selectedEntry = entry;
|
||||
|
||||
if (entry != null) {
|
||||
datePicker.setValue(entry.getDate());
|
||||
startTimePicker.setValue(entry.getStartTime());
|
||||
endTimePicker.setValue(entry.getEndTime());
|
||||
breakField.setValue((double) entry.getPauseMinutes());
|
||||
targetField.setValue((double) entry.getTargetMinutes());
|
||||
statusCombo.setValue(entry.getStatus());
|
||||
commentArea.setValue(entry.getComment() != null ? entry.getComment() : "");
|
||||
|
||||
updateBtn.setEnabled(true);
|
||||
deleteBtn.setEnabled(true);
|
||||
saveBtn.setEnabled(false);
|
||||
} else {
|
||||
clearForm();
|
||||
}
|
||||
}
|
||||
|
||||
private void clearForm() {
|
||||
selectedEntry = null;
|
||||
datePicker.setValue(LocalDate.now());
|
||||
startTimePicker.clear();
|
||||
endTimePicker.clear();
|
||||
breakField.setValue(30.0);
|
||||
targetField.setValue(480.0);
|
||||
statusCombo.setValue(DayStatus.REMOTE);
|
||||
commentArea.clear();
|
||||
updateBtn.setEnabled(false);
|
||||
deleteBtn.setEnabled(false);
|
||||
toggleSaveButton();
|
||||
}
|
||||
|
||||
private void toggleSaveButton() {
|
||||
saveBtn.setEnabled(isDateAvailable(datePicker.getValue()));
|
||||
}
|
||||
|
||||
private boolean isDateAvailable(LocalDate date) {
|
||||
return timeEntryService.getEntriesForUser(currentUser).stream()
|
||||
.noneMatch(entry -> entry.getDate().equals(date));
|
||||
}
|
||||
}
|
||||
@@ -18,37 +18,37 @@ import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Route(value = "admin/users", layout = MainLayout.class)
|
||||
@PageTitle("Benutzerverwaltung")
|
||||
@Route(value = "admin/user-management", layout = MainLayout.class)
|
||||
@PageTitle("User Management")
|
||||
@RolesAllowed("ROLE_ADMIN")
|
||||
public class UserAdminView extends VerticalLayout {
|
||||
public class UserManagementView extends VerticalLayout {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
private final Grid<User> userGrid = new Grid<>(User.class, false);
|
||||
private final TextField usernameField = new TextField("Benutzername");
|
||||
private final PasswordField passwordField = new PasswordField("Passwort");
|
||||
private final MultiSelectComboBox<String> rolesField = new MultiSelectComboBox<>("Rollen");
|
||||
private final TextField usernameField = new TextField("Username");
|
||||
private final PasswordField passwordField = new PasswordField("Password");
|
||||
private final MultiSelectComboBox<String> rolesField = new MultiSelectComboBox<>("Roles");
|
||||
|
||||
private final Button saveButton = new Button("Speichern");
|
||||
private final Button deleteButton = new Button("Löschen");
|
||||
private final Button saveButton = new Button("Save");
|
||||
private final Button deleteButton = new Button("Delete");
|
||||
|
||||
private User selectedUser;
|
||||
|
||||
public UserAdminView(UserRepository userRepository, PasswordEncoder passwordEncoder) {
|
||||
public UserManagementView(UserRepository userRepository, PasswordEncoder passwordEncoder) {
|
||||
this.userRepository = userRepository;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
|
||||
add(new H2("Benutzerverwaltung"));
|
||||
configureGrid();
|
||||
configureForm();
|
||||
refreshGrid();
|
||||
add(new H2("User Management"));
|
||||
setupUserGrid();
|
||||
setupFormLayout();
|
||||
loadUsersToGrid();
|
||||
}
|
||||
|
||||
private void configureGrid() {
|
||||
userGrid.addColumn(User::getUsername).setHeader("Benutzername");
|
||||
userGrid.addColumn(user -> String.join(", ", user.getRoles())).setHeader("Rollen");
|
||||
private void setupUserGrid() {
|
||||
userGrid.addColumn(User::getUsername).setHeader("Username");
|
||||
userGrid.addColumn(user -> String.join(", ", user.getRoles())).setHeader("Roles");
|
||||
userGrid.setSelectionMode(Grid.SelectionMode.SINGLE);
|
||||
userGrid.setWidthFull();
|
||||
|
||||
@@ -64,10 +64,18 @@ public class UserAdminView extends VerticalLayout {
|
||||
add(userGrid);
|
||||
}
|
||||
|
||||
private void configureForm() {
|
||||
private void setupFormLayout() {
|
||||
rolesField.setItems("ROLE_USER", "ROLE_ADMIN");
|
||||
|
||||
saveButton.addClickListener(e -> {
|
||||
saveButton.addClickListener(e -> saveOrUpdateUser());
|
||||
deleteButton.addClickListener(e -> deleteUser());
|
||||
|
||||
HorizontalLayout formLayout = new HorizontalLayout(usernameField, passwordField, rolesField, saveButton, deleteButton);
|
||||
formLayout.setWidthFull();
|
||||
add(formLayout);
|
||||
}
|
||||
|
||||
private void saveOrUpdateUser() {
|
||||
if (selectedUser != null) {
|
||||
selectedUser.setUsername(usernameField.getValue());
|
||||
selectedUser.setRoles(rolesField.getValue());
|
||||
@@ -85,29 +93,25 @@ public class UserAdminView extends VerticalLayout {
|
||||
.build();
|
||||
userRepository.save(newUser);
|
||||
}
|
||||
clearForm();
|
||||
refreshGrid();
|
||||
});
|
||||
|
||||
deleteButton.addClickListener(e -> {
|
||||
resetFormFields();
|
||||
loadUsersToGrid();
|
||||
}
|
||||
|
||||
private void deleteUser() {
|
||||
if (selectedUser != null) {
|
||||
userRepository.delete(selectedUser);
|
||||
clearForm();
|
||||
refreshGrid();
|
||||
resetFormFields();
|
||||
loadUsersToGrid();
|
||||
}
|
||||
});
|
||||
|
||||
HorizontalLayout formLayout = new HorizontalLayout(usernameField, passwordField, rolesField, saveButton, deleteButton);
|
||||
formLayout.setWidthFull();
|
||||
add(formLayout);
|
||||
}
|
||||
|
||||
private void refreshGrid() {
|
||||
private void loadUsersToGrid() {
|
||||
List<User> users = userRepository.findAll();
|
||||
userGrid.setItems(users);
|
||||
}
|
||||
|
||||
private void clearForm() {
|
||||
private void resetFormFields() {
|
||||
selectedUser = null;
|
||||
usernameField.clear();
|
||||
passwordField.clear();
|
||||
@@ -0,0 +1,103 @@
|
||||
package de.nilzbu.mytimetracker.ui.view.timeentry;
|
||||
|
||||
import com.vaadin.flow.component.combobox.ComboBox;
|
||||
import com.vaadin.flow.component.datepicker.DatePicker;
|
||||
import com.vaadin.flow.component.textfield.IntegerField;
|
||||
import com.vaadin.flow.component.textfield.TextArea;
|
||||
import com.vaadin.flow.component.timepicker.TimePicker;
|
||||
import com.vaadin.flow.function.SerializableSupplier;
|
||||
import de.nilzbu.mytimetracker.model.DayStatus;
|
||||
import de.nilzbu.mytimetracker.model.TimeEntry;
|
||||
import org.vaadin.crudui.crud.CrudOperation;
|
||||
import org.vaadin.crudui.crud.impl.GridCrud;
|
||||
import org.vaadin.crudui.form.CrudFormFactory;
|
||||
import org.vaadin.crudui.form.impl.form.factory.DefaultCrudFormFactory;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalTime;
|
||||
|
||||
final class TimeEntryFormConfigurer {
|
||||
|
||||
private TimeEntryFormConfigurer() {}
|
||||
|
||||
static void configure(GridCrud<TimeEntry> crud, SerializableSupplier<TimeEntry> newInstanceSupplier) {
|
||||
CrudFormFactory<TimeEntry> formFactory = new DefaultCrudFormFactory<>(TimeEntry.class);
|
||||
|
||||
formFactory.setVisibleProperties(
|
||||
CrudOperation.ADD, "date", "startTime", "endTime", "pauseMinutes", "targetMinutes", "status", "comment"
|
||||
);
|
||||
formFactory.setVisibleProperties(
|
||||
CrudOperation.UPDATE, "date", "startTime", "endTime", "pauseMinutes", "targetMinutes", "status", "comment"
|
||||
);
|
||||
|
||||
// Date
|
||||
formFactory.setFieldProvider("date", (TimeEntry bean) -> {
|
||||
DatePicker dp = new DatePicker("Date");
|
||||
dp.setRequiredIndicatorVisible(true);
|
||||
if (bean == null || bean.getDate() == null) {
|
||||
dp.setValue(LocalDate.now());
|
||||
}
|
||||
return dp;
|
||||
});
|
||||
|
||||
// Start/End
|
||||
formFactory.setFieldProvider("startTime", (TimeEntry bean) -> {
|
||||
TimePicker tp = new TimePicker("Start");
|
||||
tp.setRequiredIndicatorVisible(true);
|
||||
tp.setMin(LocalTime.of(5, 0));
|
||||
tp.setMax(LocalTime.of(23, 59));
|
||||
return tp;
|
||||
});
|
||||
formFactory.setFieldProvider("endTime", (TimeEntry bean) -> {
|
||||
TimePicker tp = new TimePicker("End");
|
||||
tp.setRequiredIndicatorVisible(true);
|
||||
tp.setMin(LocalTime.of(5, 0));
|
||||
tp.setMax(LocalTime.of(23, 59));
|
||||
return tp;
|
||||
});
|
||||
|
||||
// Integers (no null-checks on primitives; defaults are provided by newInstanceSupplier)
|
||||
formFactory.setFieldProvider("pauseMinutes", (TimeEntry bean) -> {
|
||||
IntegerField f = new IntegerField("Break (min)");
|
||||
f.setMin(0);
|
||||
f.setStep(1); // allow any integer (e.g., 44)
|
||||
f.setStepButtonsVisible(true);
|
||||
f.setRequiredIndicatorVisible(true);
|
||||
return f;
|
||||
});
|
||||
|
||||
formFactory.setFieldProvider("targetMinutes", (TimeEntry bean) -> {
|
||||
IntegerField f = new IntegerField("Target (min)");
|
||||
f.setMin(0);
|
||||
f.setStep(1);
|
||||
f.setStepButtonsVisible(true);
|
||||
f.setRequiredIndicatorVisible(true);
|
||||
return f;
|
||||
});
|
||||
|
||||
// Status
|
||||
formFactory.setFieldProvider("status", (TimeEntry bean) -> {
|
||||
ComboBox<DayStatus> cb = new ComboBox<>("Status");
|
||||
cb.setItems(DayStatus.values());
|
||||
cb.setItemLabelGenerator(Enum::name);
|
||||
cb.setRequiredIndicatorVisible(true);
|
||||
if (bean == null || bean.getStatus() == null) {
|
||||
cb.setValue(DayStatus.REMOTE);
|
||||
}
|
||||
return cb;
|
||||
});
|
||||
|
||||
// Comment
|
||||
formFactory.setFieldProvider("comment", (TimeEntry bean) -> {
|
||||
TextArea ta = new TextArea("Comment");
|
||||
ta.setMaxLength(500);
|
||||
ta.setWidthFull();
|
||||
return ta;
|
||||
});
|
||||
|
||||
formFactory.setNewInstanceSupplier(newInstanceSupplier);
|
||||
formFactory.setUseBeanValidation(true);
|
||||
|
||||
crud.setCrudFormFactory(formFactory);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package de.nilzbu.mytimetracker.ui.view.timeentry;
|
||||
|
||||
import com.vaadin.flow.component.checkbox.Checkbox;
|
||||
import com.vaadin.flow.component.combobox.ComboBox;
|
||||
import com.vaadin.flow.component.datepicker.DatePicker;
|
||||
import com.vaadin.flow.component.grid.Grid;
|
||||
import com.vaadin.flow.component.grid.HeaderRow;
|
||||
import com.vaadin.flow.component.html.Div;
|
||||
import com.vaadin.flow.component.orderedlayout.HorizontalLayout;
|
||||
import com.vaadin.flow.component.textfield.TextField;
|
||||
import de.nilzbu.mytimetracker.model.DayStatus;
|
||||
import de.nilzbu.mytimetracker.model.TimeEntry;
|
||||
import de.nilzbu.mytimetracker.service.TimeEntryService;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
final class TimeEntryHeaderFilters {
|
||||
|
||||
private final TimeEntryService service; // needed for deviation/actual calculations
|
||||
|
||||
TimeEntryHeaderFilters(TimeEntryService service) {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
// instance-held controls
|
||||
private final DatePicker fromDate = new DatePicker("From");
|
||||
private final DatePicker toDate = new DatePicker("To");
|
||||
private final ComboBox<DayStatus> statusFilter = new ComboBox<>("Status");
|
||||
private final TextField searchField = new TextField("Search");
|
||||
private final Checkbox onlyWithDeviation = new Checkbox("≠ 0");
|
||||
|
||||
void configureGridAndFilters(Grid<TimeEntry> grid, Runnable onFilterChanged) {
|
||||
grid.removeAllColumns();
|
||||
|
||||
var dateCol = grid.addColumn(TimeEntry::getDate).setHeader("Date").setSortable(true).setAutoWidth(true);
|
||||
grid.addColumn(TimeEntry::getStartTime).setHeader("Start").setSortable(true).setAutoWidth(true);
|
||||
grid.addColumn(TimeEntry::getEndTime).setHeader("End").setSortable(true).setAutoWidth(true);
|
||||
grid.addColumn(TimeEntry::getPauseMinutes).setHeader("Break (min)").setSortable(true).setAutoWidth(true);
|
||||
grid.addColumn(TimeEntry::getTargetMinutes).setHeader("Target (min)").setSortable(true).setAutoWidth(true);
|
||||
grid.addColumn(service::calculateNetWorkMinutes).setHeader("Actual (min)").setSortable(true).setAutoWidth(true);
|
||||
var statusCol = grid.addColumn(TimeEntry::getStatus).setHeader("Status").setSortable(true).setAutoWidth(true);
|
||||
var commentCol = grid.addColumn(TimeEntry::getComment).setHeader("Comment").setSortable(true).setAutoWidth(true);
|
||||
var devCol = grid.addColumn(service::calculateDeviation).setHeader("Δ (min)").setSortable(true).setAutoWidth(true);
|
||||
|
||||
grid.setSelectionMode(Grid.SelectionMode.SINGLE);
|
||||
|
||||
HeaderRow filterRow = grid.appendHeaderRow();
|
||||
|
||||
// Date range (From/To)
|
||||
initDatePicker(fromDate);
|
||||
initDatePicker(toDate);
|
||||
HorizontalLayout dateRange = new HorizontalLayout(fromDate, toDate);
|
||||
dateRange.setSpacing(true);
|
||||
dateRange.setPadding(false);
|
||||
dateRange.setAlignItems(HorizontalLayout.Alignment.BASELINE);
|
||||
dateRange.getStyle().set("gap", "0.25rem");
|
||||
filterRow.getCell(dateCol).setComponent(wrap(dateRange));
|
||||
|
||||
// Status
|
||||
statusFilter.setItems(DayStatus.values());
|
||||
statusFilter.setItemLabelGenerator(Enum::name);
|
||||
statusFilter.setPlaceholder("Any");
|
||||
statusFilter.setClearButtonVisible(true);
|
||||
themeSmall(statusFilter);
|
||||
filterRow.getCell(statusCol).setComponent(wrap(statusFilter));
|
||||
|
||||
// Comment search
|
||||
searchField.setPlaceholder("Search…");
|
||||
searchField.setClearButtonVisible(true);
|
||||
themeSmall(searchField);
|
||||
filterRow.getCell(commentCol).setComponent(wrap(searchField));
|
||||
|
||||
// Deviation ≠ 0
|
||||
themeSmall(onlyWithDeviation);
|
||||
filterRow.getCell(devCol).setComponent(wrap(onlyWithDeviation));
|
||||
|
||||
// Auto-apply
|
||||
fromDate.addValueChangeListener(e -> onFilterChanged.run());
|
||||
toDate.addValueChangeListener(e -> onFilterChanged.run());
|
||||
statusFilter.addValueChangeListener(e -> onFilterChanged.run());
|
||||
searchField.addValueChangeListener(e -> onFilterChanged.run());
|
||||
onlyWithDeviation.addValueChangeListener(e -> onFilterChanged.run());
|
||||
|
||||
// Optional widths (valid Column API)
|
||||
dateCol.setFlexGrow(0).setWidth("240px");
|
||||
statusCol.setFlexGrow(0).setWidth("160px");
|
||||
commentCol.setAutoWidth(true); // no setMinWidth in API
|
||||
devCol.setFlexGrow(0).setWidth("120px");
|
||||
}
|
||||
|
||||
List<TimeEntry> applyFilters(List<TimeEntry> source) {
|
||||
LocalDate from = fromDate.getValue();
|
||||
LocalDate to = toDate.getValue();
|
||||
|
||||
boolean invalidRange = from != null && to != null && from.isAfter(to);
|
||||
fromDate.setInvalid(invalidRange);
|
||||
toDate.setInvalid(invalidRange);
|
||||
fromDate.setErrorMessage(invalidRange ? "'From' must not be after 'To'" : null);
|
||||
toDate.setErrorMessage(invalidRange ? "'To' must not be before 'From'" : null);
|
||||
if (invalidRange) return List.of();
|
||||
|
||||
if (from != null) {
|
||||
source = source.stream().filter(e -> !e.getDate().isBefore(from)).toList();
|
||||
}
|
||||
if (to != null) {
|
||||
source = source.stream().filter(e -> !e.getDate().isAfter(to)).toList();
|
||||
}
|
||||
|
||||
DayStatus s = statusFilter.getValue();
|
||||
if (s != null) {
|
||||
source = source.stream().filter(e -> e.getStatus() == s).toList();
|
||||
}
|
||||
|
||||
if (Boolean.TRUE.equals(onlyWithDeviation.getValue())) {
|
||||
source = source.stream()
|
||||
.filter(e -> service.calculateDeviation(e) != 0)
|
||||
.toList();
|
||||
}
|
||||
|
||||
String q = searchField.getValue();
|
||||
if (q != null && !q.isBlank()) {
|
||||
String needle = q.trim().toLowerCase();
|
||||
source = source.stream().filter(e ->
|
||||
(e.getComment() != null && e.getComment().toLowerCase().contains(needle))
|
||||
|| (e.getStatus() != null && e.getStatus().name().toLowerCase().contains(needle))
|
||||
).toList();
|
||||
}
|
||||
|
||||
return source;
|
||||
}
|
||||
|
||||
/* ---- helpers ---- */
|
||||
|
||||
private static void initDatePicker(DatePicker dp) {
|
||||
dp.setClearButtonVisible(true);
|
||||
dp.setPlaceholder(dp.getLabel());
|
||||
themeSmall(dp);
|
||||
}
|
||||
|
||||
private static <T extends com.vaadin.flow.component.Component> T themeSmall(T c) {
|
||||
c.getElement().getThemeList().add("small");
|
||||
return c;
|
||||
}
|
||||
|
||||
private static Div wrap(com.vaadin.flow.component.Component c) {
|
||||
Div w = new Div(c);
|
||||
w.addClassName("filter-cell");
|
||||
w.getStyle().set("width", "100%");
|
||||
return w;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package de.nilzbu.mytimetracker.ui.view.timeentry;
|
||||
|
||||
import de.nilzbu.mytimetracker.model.TimeEntry;
|
||||
import de.nilzbu.mytimetracker.model.User;
|
||||
import de.nilzbu.mytimetracker.service.TimeEntryService;
|
||||
|
||||
import java.time.LocalTime;
|
||||
|
||||
final class TimeEntryValidators {
|
||||
|
||||
private TimeEntryValidators() {}
|
||||
|
||||
static void validate(TimeEntry entry, TimeEntryService service, User currentUser) {
|
||||
requireEndAfterStart(entry);
|
||||
requireUniqueDate(entry, service, currentUser);
|
||||
}
|
||||
|
||||
private static void requireEndAfterStart(TimeEntry entry) {
|
||||
LocalTime s = entry.getStartTime();
|
||||
LocalTime e = entry.getEndTime();
|
||||
if (s != null && e != null && e.isBefore(s)) {
|
||||
throw new IllegalArgumentException("End time must not be before start time.");
|
||||
}
|
||||
}
|
||||
|
||||
private static void requireUniqueDate(TimeEntry candidate, TimeEntryService service, User currentUser) {
|
||||
boolean existsSameDate = service.getEntriesForUser(currentUser).stream()
|
||||
.filter(e -> e.getDate() != null && e.getDate().equals(candidate.getDate()))
|
||||
.anyMatch(e -> candidate.getId() == null || !e.getId().equals(candidate.getId()));
|
||||
if (existsSameDate) {
|
||||
throw new IllegalArgumentException("An entry already exists for this date.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package de.nilzbu.mytimetracker.ui.view.timeentry;
|
||||
|
||||
import com.vaadin.flow.component.html.H2;
|
||||
import com.vaadin.flow.component.orderedlayout.FlexComponent;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
import com.vaadin.flow.router.PageTitle;
|
||||
import com.vaadin.flow.router.Route;
|
||||
import de.nilzbu.mytimetracker.model.TimeEntry;
|
||||
import de.nilzbu.mytimetracker.model.User;
|
||||
import de.nilzbu.mytimetracker.repository.UserRepository;
|
||||
import de.nilzbu.mytimetracker.service.TimeEntryService;
|
||||
import de.nilzbu.mytimetracker.ui.layout.MainLayout;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.vaadin.crudui.crud.impl.GridCrud;
|
||||
import org.vaadin.crudui.layout.impl.VerticalCrudLayout;
|
||||
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Route(value = "time-entry", layout = MainLayout.class)
|
||||
@PageTitle("Time Entries")
|
||||
@PermitAll
|
||||
public class TimeEntryView extends VerticalLayout {
|
||||
|
||||
private final TimeEntryService timeEntryService;
|
||||
private final UserRepository userRepository;
|
||||
private User currentUser;
|
||||
|
||||
private final GridCrud<TimeEntry> crud;
|
||||
|
||||
public TimeEntryView(TimeEntryService timeEntryService, UserRepository userRepository) {
|
||||
this.timeEntryService = timeEntryService;
|
||||
this.userRepository = userRepository;
|
||||
|
||||
setSizeFull();
|
||||
setPadding(true);
|
||||
setSpacing(true);
|
||||
|
||||
initCurrentUser();
|
||||
|
||||
this.crud = new GridCrud<>(TimeEntry.class, new VerticalCrudLayout());
|
||||
crud.setSizeFull();
|
||||
|
||||
// Header filters need the service (for deviation/actual calculations)
|
||||
TimeEntryHeaderFilters headerFilters = new TimeEntryHeaderFilters(timeEntryService);
|
||||
headerFilters.configureGridAndFilters(crud.getGrid(), this::refresh);
|
||||
|
||||
TimeEntryFormConfigurer.configure(crud, this::defaultNewEntry);
|
||||
|
||||
configureOperations(headerFilters);
|
||||
|
||||
add(new H2("Time Entries"), crud);
|
||||
setAlignItems(FlexComponent.Alignment.STRETCH);
|
||||
|
||||
refresh();
|
||||
}
|
||||
|
||||
private void initCurrentUser() {
|
||||
String username = SecurityContextHolder.getContext().getAuthentication().getName();
|
||||
this.currentUser = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new IllegalStateException("User not found"));
|
||||
}
|
||||
|
||||
private TimeEntry defaultNewEntry() {
|
||||
TimeEntry t = new TimeEntry();
|
||||
t.setUser(currentUser);
|
||||
t.setDate(LocalDate.now());
|
||||
// sensible defaults
|
||||
t.setStatus(de.nilzbu.mytimetracker.model.DayStatus.REMOTE);
|
||||
t.setPauseMinutes(30);
|
||||
t.setTargetMinutes(480);
|
||||
return t;
|
||||
}
|
||||
|
||||
private void configureOperations(TimeEntryHeaderFilters headerFilters) {
|
||||
// READ uses header filters
|
||||
crud.setFindAllOperation(() ->
|
||||
headerFilters.applyFilters(
|
||||
timeEntryService.getEntriesForUser(currentUser))
|
||||
);
|
||||
|
||||
// CREATE
|
||||
crud.setAddOperation(entry -> {
|
||||
entry.setUser(currentUser);
|
||||
TimeEntryValidators.validate(entry, timeEntryService, currentUser);
|
||||
return timeEntryService.save(entry);
|
||||
});
|
||||
|
||||
// UPDATE
|
||||
crud.setUpdateOperation(entry -> {
|
||||
entry.setUser(currentUser);
|
||||
TimeEntryValidators.validate(entry, timeEntryService, currentUser);
|
||||
return timeEntryService.save(entry);
|
||||
});
|
||||
|
||||
// DELETE
|
||||
crud.setDeleteOperation(timeEntryService::delete);
|
||||
}
|
||||
|
||||
private void refresh() {
|
||||
crud.refreshGrid();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package de.nilzbu.mytimetracker.ui.widget;
|
||||
|
||||
import com.vaadin.flow.component.html.NativeLabel;
|
||||
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
|
||||
|
||||
public class KeyFigureWidget extends VerticalLayout {
|
||||
|
||||
public KeyFigureWidget(String name, String value) {
|
||||
NativeLabel nameLabel = new NativeLabel(name + ":");
|
||||
NativeLabel valueLabel = new NativeLabel(value);
|
||||
valueLabel.getStyle().set("font-weight", "bold");
|
||||
add(nameLabel, valueLabel);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user