Compare commits
17 Commits
171f71209b
...
feature/2_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d7329a811 | ||
|
|
f09fee5a55 | ||
| ac3993545e | |||
| c62127e24d | |||
| f36fd63a04 | |||
| 75ddbecc79 | |||
|
|
ea9ceaa57e | ||
|
|
68ab7f49f1 | ||
| 7de22a723e | |||
| a64d56bbd9 | |||
| 7446839a7b | |||
|
|
2c7dd8c8f2 | ||
|
|
44672876be | ||
|
|
e828e83991 | ||
|
|
6b57a0c5e5 | ||
|
|
0f005ca130 | ||
|
|
acb96dc1c6 |
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
|
*.bak
|
||||||
*.tmpdata/*.mv.db
|
*.tmpdata/*.mv.db
|
||||||
src/main/frontend/generated/
|
src/main/frontend/generated/
|
||||||
|
/data/*.mv.db
|
||||||
|
|||||||
Binary file not shown.
@@ -1,5 +1,8 @@
|
|||||||
package de.nilzbu.mytimetracker.model;
|
package de.nilzbu.mytimetracker.model;
|
||||||
|
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
public enum DayStatus {
|
public enum DayStatus {
|
||||||
OFFICE("In office"),
|
OFFICE("In office"),
|
||||||
REMOTE("Remote work"),
|
REMOTE("Remote work"),
|
||||||
@@ -12,7 +15,4 @@ public enum DayStatus {
|
|||||||
this.displayName = displayName;
|
this.displayName = displayName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getDisplayName() {
|
|
||||||
return displayName;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,10 +12,6 @@ public interface TimeEntryRepository extends JpaRepository<TimeEntry, Long> {
|
|||||||
|
|
||||||
List<TimeEntry> findAllByUserOrderByDateDesc(User user);
|
List<TimeEntry> findAllByUserOrderByDateDesc(User user);
|
||||||
|
|
||||||
Optional<TimeEntry> findTopByUserOrderByDateDesc(User user);
|
|
||||||
|
|
||||||
boolean existsByUserAndDate(User user, LocalDate date);
|
|
||||||
|
|
||||||
Optional<TimeEntry> findByUserAndDate(User user, LocalDate date);
|
Optional<TimeEntry> findByUserAndDate(User user, LocalDate date);
|
||||||
|
|
||||||
List<TimeEntry> findByUserAndDateBetween(User user, LocalDate start, LocalDate end);
|
List<TimeEntry> findByUserAndDateBetween(User user, LocalDate start, LocalDate end);
|
||||||
|
|||||||
@@ -12,10 +12,7 @@ public class SecurityConfig extends VaadinWebSecurity {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void configure(HttpSecurity http) throws Exception {
|
protected void configure(HttpSecurity http) throws Exception {
|
||||||
// Standard-Konfiguration von Vaadin-Security erweitern
|
|
||||||
super.configure(http);
|
super.configure(http);
|
||||||
|
|
||||||
// Login-View setzen
|
|
||||||
setLoginView(http, LoginView.class);
|
setLoginView(http, LoginView.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ public class TimeEntryService {
|
|||||||
return repository.findAllByUserOrderByDateDesc(user);
|
return repository.findAllByUserOrderByDateDesc(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getNumberOfEntriesForUser(User user) {
|
||||||
|
return getEntriesForUser(user).size();
|
||||||
|
}
|
||||||
|
|
||||||
public TimeEntry save(TimeEntry entry) {
|
public TimeEntry save(TimeEntry entry) {
|
||||||
return repository.save(entry);
|
return repository.save(entry);
|
||||||
}
|
}
|
||||||
@@ -49,6 +53,10 @@ public class TimeEntryService {
|
|||||||
return repository.findByUserAndDateBetween(user, start, end);
|
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) {
|
public List<TimeEntry> getEntriesForQuarter(User user, int year, int quarter) {
|
||||||
int startMonth = (quarter - 1) * 3 + 1;
|
int startMonth = (quarter - 1) * 3 + 1;
|
||||||
LocalDate start = LocalDate.of(year, startMonth, 1);
|
LocalDate start = LocalDate.of(year, startMonth, 1);
|
||||||
@@ -56,12 +64,20 @@ public class TimeEntryService {
|
|||||||
return repository.findByUserAndDateBetween(user, start, end);
|
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) {
|
public List<TimeEntry> getEntriesForYear(User user, int year) {
|
||||||
LocalDate start = LocalDate.of(year, 1, 1);
|
LocalDate start = LocalDate.of(year, 1, 1);
|
||||||
LocalDate end = LocalDate.of(year, 12, 31);
|
LocalDate end = LocalDate.of(year, 12, 31);
|
||||||
return repository.findByUserAndDateBetween(user, start, end);
|
return repository.findByUserAndDateBetween(user, start, end);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getNumberOfEntriesForYear(User user, int year) {
|
||||||
|
return getEntriesForYear(user, year).size();
|
||||||
|
}
|
||||||
|
|
||||||
public Optional<LocalDate> getEarliestEntryDate(User user) {
|
public Optional<LocalDate> getEarliestEntryDate(User user) {
|
||||||
return repository.findFirstByUserOrderByDateAsc(user)
|
return repository.findFirstByUserOrderByDateAsc(user)
|
||||||
.map(TimeEntry::getDate);
|
.map(TimeEntry::getDate);
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
package de.nilzbu.mytimetracker.ui.component;
|
package de.nilzbu.mytimetracker.ui.component;
|
||||||
|
|
||||||
import com.vaadin.flow.component.HasSize;
|
import com.vaadin.flow.component.AttachEvent;
|
||||||
import com.vaadin.flow.component.html.Div;
|
import com.vaadin.flow.component.Tag;
|
||||||
import com.vaadin.flow.component.dependency.JsModule;
|
import com.vaadin.flow.component.dependency.JsModule;
|
||||||
import com.vaadin.flow.component.dependency.NpmPackage;
|
import com.vaadin.flow.component.dependency.NpmPackage;
|
||||||
import com.vaadin.flow.component.Tag;
|
import com.vaadin.flow.component.html.Div;
|
||||||
import com.vaadin.flow.component.AttachEvent;
|
|
||||||
import elemental.json.Json;
|
import elemental.json.Json;
|
||||||
import elemental.json.JsonArray;
|
import elemental.json.JsonArray;
|
||||||
import elemental.json.JsonObject;
|
import elemental.json.JsonObject;
|
||||||
@@ -31,9 +30,15 @@ public class ChartJsComponent extends Div {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onAttach(AttachEvent attachEvent) {
|
protected void onAttach(AttachEvent attachEvent) {
|
||||||
|
renderChart();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderChart() {
|
||||||
getElement().executeJs(
|
getElement().executeJs(
|
||||||
"""
|
"""
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.style.width = "100%";
|
||||||
|
canvas.style.height = "100%";
|
||||||
this.appendChild(canvas);
|
this.appendChild(canvas);
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
new Chart(ctx, {
|
new Chart(ctx, {
|
||||||
@@ -54,47 +59,54 @@ public class ChartJsComponent extends Div {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
""",
|
""",
|
||||||
getChartTypeFromData(chartData),
|
detectChartType(chartData),
|
||||||
chartData,
|
chartData,
|
||||||
chartTitle
|
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 {
|
try {
|
||||||
JsonArray datasets = data.getArray("datasets");
|
JsonArray datasets = data.getArray("datasets");
|
||||||
if (datasets.length() > 0) {
|
if (datasets.length() > 0) {
|
||||||
JsonObject first = datasets.getObject(0);
|
JsonObject firstDataset = datasets.getObject(0);
|
||||||
if (first.hasKey("fill") && !first.getBoolean("fill")) {
|
if (firstDataset.hasKey("fill") && !firstDataset.getBoolean("fill")) {
|
||||||
return "line";
|
return "line";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception ignored) {}
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
return "bar";
|
return "bar";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Balkendiagramm für Kategorien
|
/**
|
||||||
|
* Erstellt ein Balkendiagramm für Kategoriezählungen.
|
||||||
|
*/
|
||||||
public static JsonObject generateBarChartData(Map<String, Long> categoryCounts) {
|
public static JsonObject generateBarChartData(Map<String, Long> categoryCounts) {
|
||||||
JsonObject data = Json.createObject();
|
JsonObject data = Json.createObject();
|
||||||
JsonArray labels = Json.createArray();
|
JsonArray labels = Json.createArray();
|
||||||
JsonArray values = 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()) {
|
for (Map.Entry<String, Long> entry : categoryCounts.entrySet()) {
|
||||||
labels.set(i, Json.create(entry.getKey()));
|
labels.set(index, Json.create(entry.getKey()));
|
||||||
values.set(i, Json.create(entry.getValue()));
|
values.set(index, Json.create(entry.getValue()));
|
||||||
backgroundColors.set(i, Json.create(defaultColors[i % defaultColors.length]));
|
colors.set(index, Json.create(defaultColors[index % defaultColors.length]));
|
||||||
i++;
|
index++;
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonObject dataset = Json.createObject();
|
JsonObject dataset = Json.createObject();
|
||||||
dataset.put("label", "Einträge nach Kategorie");
|
dataset.put("label", "Entries by Category");
|
||||||
dataset.put("data", values);
|
dataset.put("data", values);
|
||||||
dataset.put("backgroundColor", backgroundColors);
|
dataset.put("backgroundColor", colors);
|
||||||
|
|
||||||
JsonArray datasets = Json.createArray();
|
JsonArray datasets = Json.createArray();
|
||||||
datasets.set(0, dataset);
|
datasets.set(0, dataset);
|
||||||
@@ -105,19 +117,21 @@ public class ChartJsComponent extends Div {
|
|||||||
return data;
|
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();
|
JsonObject data = Json.createObject();
|
||||||
JsonArray labels = Json.createArray();
|
JsonArray labels = Json.createArray();
|
||||||
JsonArray values = Json.createArray();
|
JsonArray values = Json.createArray();
|
||||||
|
|
||||||
for (int i = 0; i < dates.size(); i++) {
|
for (int i = 0; i < dates.size(); i++) {
|
||||||
labels.set(i, Json.create(dates.get(i).toString()));
|
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();
|
JsonObject dataset = Json.createObject();
|
||||||
dataset.put("label", "Überstunden-Saldo (h)");
|
dataset.put("label", "Overtime Balance (hours)");
|
||||||
dataset.put("data", values);
|
dataset.put("data", values);
|
||||||
dataset.put("borderColor", "rgb(54, 162, 235)");
|
dataset.put("borderColor", "rgb(54, 162, 235)");
|
||||||
dataset.put("tension", 0.1);
|
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.router.RouterLink;
|
||||||
import com.vaadin.flow.server.VaadinServletRequest;
|
import com.vaadin.flow.server.VaadinServletRequest;
|
||||||
import com.vaadin.flow.server.VaadinServletResponse;
|
import com.vaadin.flow.server.VaadinServletResponse;
|
||||||
import de.nilzbu.mytimetracker.ui.view.MainView;
|
import de.nilzbu.mytimetracker.ui.view.DashboardOverView;
|
||||||
import de.nilzbu.mytimetracker.ui.view.TimeEntryView;
|
import de.nilzbu.mytimetracker.ui.view.TimeEntryView;
|
||||||
import de.nilzbu.mytimetracker.ui.view.UserAdminView;
|
import de.nilzbu.mytimetracker.ui.view.UserManagementView;
|
||||||
import jakarta.annotation.security.PermitAll;
|
import jakarta.annotation.security.PermitAll;
|
||||||
import jakarta.servlet.http.HttpServletRequest;
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
@@ -19,44 +19,46 @@ import org.springframework.security.core.context.SecurityContextHolder;
|
|||||||
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
|
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
|
||||||
|
|
||||||
@PermitAll
|
@PermitAll
|
||||||
|
|
||||||
public class MainLayout extends AppLayout {
|
public class MainLayout extends AppLayout {
|
||||||
|
|
||||||
public MainLayout() {
|
public MainLayout() {
|
||||||
createHeader();
|
buildHeader();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void createHeader() {
|
private void buildHeader() {
|
||||||
H1 logo = new H1("Time Tracker");
|
H1 title = new H1("Time Tracker");
|
||||||
logo.getStyle()
|
title.getStyle()
|
||||||
.set("font-size", "var(--lumo-font-size-l)")
|
.set("font-size", "var(--lumo-font-size-l)")
|
||||||
.set("margin", "0");
|
.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 bookingsLink = new RouterLink("Bookings", TimeEntryView.class);
|
||||||
RouterLink userAdminView = new RouterLink("Admin", UserAdminView.class);
|
RouterLink adminLink = new RouterLink("Admin", UserManagementView.class);
|
||||||
homeLink.getStyle().set("margin-left", "2em");
|
|
||||||
|
dashboardLink.getStyle().set("margin-left", "2em");
|
||||||
bookingsLink.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);
|
HorizontalLayout headerLayout = new HorizontalLayout(
|
||||||
header.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER);
|
title, dashboardLink, bookingsLink, adminLink, logoutButton
|
||||||
header.setWidthFull();
|
);
|
||||||
header.setPadding(true);
|
headerLayout.setDefaultVerticalComponentAlignment(FlexComponent.Alignment.CENTER);
|
||||||
header.setSpacing(true);
|
headerLayout.setWidthFull();
|
||||||
|
headerLayout.setPadding(true);
|
||||||
|
headerLayout.setSpacing(true);
|
||||||
|
|
||||||
addToNavbar(header);
|
addToNavbar(headerLayout);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleLogout() {
|
private void performLogout() {
|
||||||
HttpServletRequest request = VaadinServletRequest.getCurrent().getHttpServletRequest();
|
HttpServletRequest request = VaadinServletRequest.getCurrent().getHttpServletRequest();
|
||||||
HttpServletResponse response = VaadinServletResponse.getCurrent().getHttpServletResponse();
|
HttpServletResponse response = VaadinServletResponse.getCurrent().getHttpServletResponse();
|
||||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
|
||||||
if (auth != null) {
|
if (authentication != null) {
|
||||||
new SecurityContextLogoutHandler().logout(request, response, auth);
|
new SecurityContextLogoutHandler().logout(request, response, authentication);
|
||||||
}
|
}
|
||||||
|
|
||||||
getUI().ifPresent(ui -> ui.getPage().setLocation("/login"));
|
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.combobox.ComboBox;
|
||||||
import com.vaadin.flow.component.html.Div;
|
import com.vaadin.flow.component.html.Div;
|
||||||
import com.vaadin.flow.component.html.H2;
|
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.orderedlayout.VerticalLayout;
|
||||||
import com.vaadin.flow.component.tabs.Tab;
|
import com.vaadin.flow.component.tabs.Tab;
|
||||||
import com.vaadin.flow.component.tabs.Tabs;
|
import com.vaadin.flow.component.tabs.Tabs;
|
||||||
import com.vaadin.flow.router.PageTitle;
|
import com.vaadin.flow.router.PageTitle;
|
||||||
import com.vaadin.flow.router.Route;
|
import com.vaadin.flow.router.Route;
|
||||||
|
import de.nilzbu.mytimetracker.model.DayStatus;
|
||||||
import de.nilzbu.mytimetracker.model.TimeEntry;
|
import de.nilzbu.mytimetracker.model.TimeEntry;
|
||||||
import de.nilzbu.mytimetracker.model.User;
|
import de.nilzbu.mytimetracker.model.User;
|
||||||
import de.nilzbu.mytimetracker.repository.UserRepository;
|
import de.nilzbu.mytimetracker.repository.UserRepository;
|
||||||
import de.nilzbu.mytimetracker.service.TimeEntryService;
|
import de.nilzbu.mytimetracker.service.TimeEntryService;
|
||||||
import de.nilzbu.mytimetracker.ui.component.ChartJsComponent;
|
import de.nilzbu.mytimetracker.ui.component.ChartJsComponent;
|
||||||
import de.nilzbu.mytimetracker.ui.layout.MainLayout;
|
import de.nilzbu.mytimetracker.ui.layout.MainLayout;
|
||||||
|
import de.nilzbu.mytimetracker.ui.widget.KeyFigureWidget;
|
||||||
import jakarta.annotation.security.PermitAll;
|
import jakarta.annotation.security.PermitAll;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.function.BiFunction;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.IntStream;
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
@PermitAll
|
@PermitAll
|
||||||
@PageTitle("Dashboard")
|
@PageTitle("Dashboard")
|
||||||
@Route(value = "", layout = MainLayout.class)
|
@Route(value = "", layout = MainLayout.class)
|
||||||
public class MainView extends VerticalLayout {
|
public class DashboardOverView extends VerticalLayout {
|
||||||
|
|
||||||
private final TimeEntryService timeEntryService;
|
private final TimeEntryService timeEntryService;
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
@@ -39,7 +44,10 @@ public class MainView extends VerticalLayout {
|
|||||||
private final Div filterContainer = new Div();
|
private final Div filterContainer = new Div();
|
||||||
private final Div contentContainer = 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.timeEntryService = timeEntryService;
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
|
|
||||||
@@ -50,13 +58,11 @@ public class MainView extends VerticalLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void configureLayout() {
|
private void configureLayout() {
|
||||||
setSizeFull();
|
|
||||||
setPadding(false);
|
setPadding(false);
|
||||||
setSpacing(false);
|
setSpacing(false);
|
||||||
setMargin(false);
|
setMargin(false);
|
||||||
|
|
||||||
contentContainer.setSizeFull();
|
contentContainer.setSizeFull();
|
||||||
contentContainer.getStyle().set("overflow", "auto");
|
|
||||||
setFlexGrow(1, contentContainer);
|
setFlexGrow(1, contentContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,6 +127,7 @@ public class MainView extends VerticalLayout {
|
|||||||
default -> timeEntryService.getEntriesForUser(currentUser);
|
default -> timeEntryService.getEntriesForUser(currentUser);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
renderKeyFigures(entries);
|
||||||
renderCharts(entries);
|
renderCharts(entries);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,16 +146,19 @@ public class MainView extends VerticalLayout {
|
|||||||
private void renderCharts(List<TimeEntry> scopedEntries) {
|
private void renderCharts(List<TimeEntry> scopedEntries) {
|
||||||
List<TimeEntry> allEntries = timeEntryService.getEntriesForUser(currentUser);
|
List<TimeEntry> allEntries = timeEntryService.getEntriesForUser(currentUser);
|
||||||
|
|
||||||
VerticalLayout chartLayout = new VerticalLayout();
|
HorizontalLayout chartLayout = new HorizontalLayout();
|
||||||
chartLayout.setSizeFull();
|
|
||||||
chartLayout.setPadding(false);
|
chartLayout.setPadding(false);
|
||||||
chartLayout.setSpacing(true);
|
chartLayout.setSpacing(true);
|
||||||
|
|
||||||
Component categoryChart = createCategoryBarChart(scopedEntries);
|
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");
|
Component overtimeChart = createOvertimeLineChart(allEntries, scopedEntries);
|
||||||
overtimeChart.getStyle().set("minHeight", "50vh");
|
overtimeChart.getStyle().setBackgroundColor("lightgray");
|
||||||
|
overtimeChart.getStyle().setBorder("1px solid black" );
|
||||||
|
overtimeChart.getStyle().setMargin("10px");
|
||||||
|
|
||||||
chartLayout.add(categoryChart, overtimeChart);
|
chartLayout.add(categoryChart, overtimeChart);
|
||||||
chartLayout.setFlexGrow(1, categoryChart);
|
chartLayout.setFlexGrow(1, categoryChart);
|
||||||
@@ -157,6 +167,54 @@ public class MainView extends VerticalLayout {
|
|||||||
contentContainer.add(chartLayout);
|
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) {
|
private Component createCategoryBarChart(List<TimeEntry> entries) {
|
||||||
Map<String, Long> statusCount = entries.stream()
|
Map<String, Long> statusCount = entries.stream()
|
||||||
.collect(Collectors.groupingBy(e -> e.getStatus().name(), Collectors.counting()));
|
.collect(Collectors.groupingBy(e -> e.getStatus().name(), Collectors.counting()));
|
||||||
@@ -191,6 +249,7 @@ public class MainView extends VerticalLayout {
|
|||||||
|
|
||||||
return new ChartJsComponent(
|
return new ChartJsComponent(
|
||||||
ChartJsComponent.generateLineChartData(scopedDates, saldoValues),
|
ChartJsComponent.generateLineChartData(scopedDates, saldoValues),
|
||||||
"Overtime Balance Over Time (in hours)");
|
"Overtime Balance Over Time (in hours)"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -22,6 +22,7 @@ import jakarta.annotation.security.PermitAll;
|
|||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Route(value = "time-entry", layout = MainLayout.class)
|
@Route(value = "time-entry", layout = MainLayout.class)
|
||||||
@PageTitle("Time Entry")
|
@PageTitle("Time Entry")
|
||||||
@@ -30,75 +31,85 @@ public class TimeEntryView extends VerticalLayout {
|
|||||||
|
|
||||||
private final TimeEntryService timeEntryService;
|
private final TimeEntryService timeEntryService;
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
|
private User currentUser;
|
||||||
|
|
||||||
private final DatePicker datePicker = new DatePicker("Date");
|
private final DatePicker datePicker = new DatePicker("Date");
|
||||||
private final TimePicker startTimePicker = new TimePicker("Start Time");
|
private final TimePicker startTimePicker = new TimePicker("Start Time");
|
||||||
private final TimePicker endTimePicker = new TimePicker("End Time");
|
private final TimePicker endTimePicker = new TimePicker("End Time");
|
||||||
private final NumberField breakMinutesField = new NumberField("Break (minutes)");
|
private final NumberField breakField = new NumberField("Break (min)");
|
||||||
private final NumberField targetMinutesField = new NumberField("Target Time (minutes)");
|
private final NumberField targetField = new NumberField("Target Time (min)");
|
||||||
private final TextArea commentField = new TextArea("Comment");
|
private final TextArea commentArea = new TextArea("Comment");
|
||||||
private final ComboBox<DayStatus> statusComboBox = new ComboBox<>("Status");
|
private final ComboBox<DayStatus> statusCombo = new ComboBox<>("Status");
|
||||||
|
|
||||||
private final Button saveButton = new Button("Save");
|
private final Button saveBtn = new Button("Save");
|
||||||
private final Button updateButton = new Button("Update");
|
private final Button updateBtn = new Button("Update");
|
||||||
private final Button deleteButton = new Button("Delete");
|
private final Button deleteBtn = new Button("Delete");
|
||||||
|
|
||||||
private final Grid<TimeEntry> entryGrid = new Grid<>(TimeEntry.class, false);
|
private final Grid<TimeEntry> entryGrid = new Grid<>(TimeEntry.class, false);
|
||||||
|
|
||||||
private TimeEntry selectedEntry = null;
|
private TimeEntry selectedEntry = null;
|
||||||
private User currentUser;
|
|
||||||
|
private final DatePicker fromDate = new DatePicker("From");
|
||||||
|
private final DatePicker toDate = new DatePicker("To");
|
||||||
|
private final Button applyFilterBtn = new Button("Apply Filter");
|
||||||
|
|
||||||
public TimeEntryView(TimeEntryService timeEntryService, UserRepository userRepository) {
|
public TimeEntryView(TimeEntryService timeEntryService, UserRepository userRepository) {
|
||||||
this.timeEntryService = timeEntryService;
|
this.timeEntryService = timeEntryService;
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
|
|
||||||
initializeCurrentUser();
|
setSizeFull();
|
||||||
configureFields();
|
|
||||||
|
initializeUser();
|
||||||
|
configureFormFields();
|
||||||
configureButtons();
|
configureButtons();
|
||||||
configureGrid();
|
configureGrid();
|
||||||
|
|
||||||
datePicker.addValueChangeListener(e -> checkIfDateAlreadyExists());
|
datePicker.addValueChangeListener(e -> toggleSaveButton());
|
||||||
|
|
||||||
add(createFormLayout(), createButtonLayout(), entryGrid);
|
add(buildFormLayout(), buildButtonLayout(), buildDateRangeFilterLayout(), entryGrid);
|
||||||
refreshGrid();
|
refreshGrid();
|
||||||
checkIfDateAlreadyExists(); // Initiale Prüfung
|
toggleSaveButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeCurrentUser() {
|
private void initializeUser() {
|
||||||
String username = SecurityContextHolder.getContext().getAuthentication().getName();
|
String username = SecurityContextHolder.getContext().getAuthentication().getName();
|
||||||
this.currentUser = userRepository.findByUsername(username)
|
this.currentUser = userRepository.findByUsername(username)
|
||||||
.orElseThrow(() -> new RuntimeException("User not found"));
|
.orElseThrow(() -> new RuntimeException("User not found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void configureFields() {
|
private void configureFormFields() {
|
||||||
datePicker.setValue(LocalDate.now());
|
datePicker.setValue(LocalDate.now());
|
||||||
breakMinutesField.setValue(30.0);
|
breakField.setValue(30.0);
|
||||||
targetMinutesField.setValue(480.0);
|
targetField.setValue(480.0);
|
||||||
|
statusCombo.setItems(DayStatus.values());
|
||||||
statusComboBox.setItems(DayStatus.values());
|
statusCombo.setValue(DayStatus.REMOTE);
|
||||||
statusComboBox.setValue(DayStatus.REMOTE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private HorizontalLayout createFormLayout() {
|
private HorizontalLayout buildFormLayout() {
|
||||||
return new HorizontalLayout(
|
return new HorizontalLayout(datePicker, startTimePicker, endTimePicker,
|
||||||
datePicker,
|
breakField, targetField, commentArea, statusCombo);
|
||||||
startTimePicker,
|
|
||||||
endTimePicker,
|
|
||||||
breakMinutesField,
|
|
||||||
targetMinutesField,
|
|
||||||
commentField,
|
|
||||||
statusComboBox
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private HorizontalLayout createButtonLayout() {
|
private HorizontalLayout buildButtonLayout() {
|
||||||
updateButton.setEnabled(false);
|
updateBtn.setEnabled(false);
|
||||||
deleteButton.setEnabled(false);
|
deleteBtn.setEnabled(false);
|
||||||
return new HorizontalLayout(saveButton, updateButton, deleteButton);
|
return new HorizontalLayout(saveBtn, updateBtn, deleteBtn);
|
||||||
|
}
|
||||||
|
|
||||||
|
private HorizontalLayout buildDateRangeFilterLayout() {
|
||||||
|
applyFilterBtn.addClickListener(e -> refreshGrid());
|
||||||
|
|
||||||
|
HorizontalLayout filterLayout = new HorizontalLayout(fromDate, toDate, applyFilterBtn);
|
||||||
|
filterLayout.setAlignItems(Alignment.END);
|
||||||
|
return filterLayout;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void configureButtons() {
|
private void configureButtons() {
|
||||||
saveButton.addClickListener(e -> {
|
saveBtn.addClickListener(e -> saveEntry());
|
||||||
|
updateBtn.addClickListener(e -> updateEntry());
|
||||||
|
deleteBtn.addClickListener(e -> deleteEntry());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveEntry() {
|
||||||
if (!isDateAvailable(datePicker.getValue())) {
|
if (!isDateAvailable(datePicker.getValue())) {
|
||||||
Notification.show("An entry already exists for this date.");
|
Notification.show("An entry already exists for this date.");
|
||||||
return;
|
return;
|
||||||
@@ -109,80 +120,98 @@ public class TimeEntryView extends VerticalLayout {
|
|||||||
.date(datePicker.getValue())
|
.date(datePicker.getValue())
|
||||||
.startTime(startTimePicker.getValue())
|
.startTime(startTimePicker.getValue())
|
||||||
.endTime(endTimePicker.getValue())
|
.endTime(endTimePicker.getValue())
|
||||||
.pauseMinutes(breakMinutesField.getValue().intValue())
|
.pauseMinutes(breakField.getValue().intValue())
|
||||||
.targetMinutes(targetMinutesField.getValue().intValue())
|
.targetMinutes(targetField.getValue().intValue())
|
||||||
.status(statusComboBox.getValue())
|
.status(statusCombo.getValue())
|
||||||
.comment(commentField.getValue())
|
.comment(commentArea.getValue())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
timeEntryService.save(entry);
|
timeEntryService.save(entry);
|
||||||
clearForm();
|
clearForm();
|
||||||
refreshGrid();
|
refreshGrid();
|
||||||
Notification.show("Entry saved");
|
Notification.show("Entry saved");
|
||||||
});
|
}
|
||||||
|
|
||||||
updateButton.addClickListener(e -> {
|
private void updateEntry() {
|
||||||
if (selectedEntry == null) return;
|
if (selectedEntry == null) return;
|
||||||
|
|
||||||
selectedEntry.setDate(datePicker.getValue());
|
selectedEntry.setDate(datePicker.getValue());
|
||||||
selectedEntry.setStartTime(startTimePicker.getValue());
|
selectedEntry.setStartTime(startTimePicker.getValue());
|
||||||
selectedEntry.setEndTime(endTimePicker.getValue());
|
selectedEntry.setEndTime(endTimePicker.getValue());
|
||||||
selectedEntry.setPauseMinutes(breakMinutesField.getValue().intValue());
|
selectedEntry.setPauseMinutes(breakField.getValue().intValue());
|
||||||
selectedEntry.setTargetMinutes(targetMinutesField.getValue().intValue());
|
selectedEntry.setTargetMinutes(targetField.getValue().intValue());
|
||||||
selectedEntry.setStatus(statusComboBox.getValue());
|
selectedEntry.setStatus(statusCombo.getValue());
|
||||||
selectedEntry.setComment(commentField.getValue());
|
selectedEntry.setComment(commentArea.getValue());
|
||||||
|
|
||||||
timeEntryService.save(selectedEntry);
|
timeEntryService.save(selectedEntry);
|
||||||
clearForm();
|
clearForm();
|
||||||
refreshGrid();
|
refreshGrid();
|
||||||
Notification.show("Entry updated");
|
Notification.show("Entry updated");
|
||||||
});
|
}
|
||||||
|
|
||||||
deleteButton.addClickListener(e -> {
|
private void deleteEntry() {
|
||||||
if (selectedEntry == null) return;
|
if (selectedEntry == null) return;
|
||||||
|
|
||||||
timeEntryService.delete(selectedEntry);
|
timeEntryService.delete(selectedEntry);
|
||||||
clearForm();
|
clearForm();
|
||||||
refreshGrid();
|
refreshGrid();
|
||||||
Notification.show("Entry deleted");
|
Notification.show("Entry deleted");
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void configureGrid() {
|
private void configureGrid() {
|
||||||
entryGrid.addColumn(TimeEntry::getDate).setHeader("Date");
|
entryGrid.setSizeFull();
|
||||||
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 -> {
|
entryGrid.addColumn(TimeEntry::getDate).setHeader("Date").setSortable(true);
|
||||||
selectedEntry = event.getValue();
|
entryGrid.addColumn(TimeEntry::getStartTime).setHeader("Start").setSortable(true);
|
||||||
if (selectedEntry != null) {
|
entryGrid.addColumn(TimeEntry::getEndTime).setHeader("End").setSortable(true);
|
||||||
datePicker.setValue(selectedEntry.getDate());
|
entryGrid.addColumn(TimeEntry::getPauseMinutes).setHeader("Break (min)").setSortable(true);
|
||||||
startTimePicker.setValue(selectedEntry.getStartTime());
|
entryGrid.addColumn(TimeEntry::getTargetMinutes).setHeader("Target (min)").setSortable(true);
|
||||||
endTimePicker.setValue(selectedEntry.getEndTime());
|
entryGrid.addColumn(timeEntryService::calculateNetWorkMinutes).setHeader("Actual (min)").setSortable(true);
|
||||||
breakMinutesField.setValue((double) selectedEntry.getPauseMinutes());
|
entryGrid.addColumn(TimeEntry::getStatus).setHeader("Status").setSortable(true);
|
||||||
targetMinutesField.setValue((double) selectedEntry.getTargetMinutes());
|
entryGrid.addColumn(TimeEntry::getComment).setHeader("Comment").setSortable(true);
|
||||||
statusComboBox.setValue(selectedEntry.getStatus());
|
entryGrid.addColumn(timeEntryService::calculateDeviation).setHeader("Deviation (min)").setSortable(true);
|
||||||
commentField.setValue(selectedEntry.getComment() != null ? selectedEntry.getComment() : "");
|
|
||||||
|
|
||||||
updateButton.setEnabled(true);
|
entryGrid.getColumns().forEach(col -> col.setAutoWidth(true));
|
||||||
deleteButton.setEnabled(true);
|
|
||||||
saveButton.setEnabled(false);
|
entryGrid.asSingleSelect().addValueChangeListener(event -> populateForm(event.getValue()));
|
||||||
} else {
|
|
||||||
updateButton.setEnabled(false);
|
|
||||||
deleteButton.setEnabled(false);
|
|
||||||
checkIfDateAlreadyExists();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void refreshGrid() {
|
private void refreshGrid() {
|
||||||
entryGrid.setItems(timeEntryService.getEntriesForUser(currentUser));
|
List<TimeEntry> entries = timeEntryService.getEntriesForUser(currentUser);
|
||||||
|
|
||||||
|
if (fromDate.getValue() != null) {
|
||||||
|
entries = entries.stream()
|
||||||
|
.filter(entry -> !entry.getDate().isBefore(fromDate.getValue()))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toDate.getValue() != null) {
|
||||||
|
entries = entries.stream()
|
||||||
|
.filter(entry -> !entry.getDate().isAfter(toDate.getValue()))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
entryGrid.setItems(entries);
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
private void clearForm() {
|
||||||
@@ -190,22 +219,17 @@ public class TimeEntryView extends VerticalLayout {
|
|||||||
datePicker.setValue(LocalDate.now());
|
datePicker.setValue(LocalDate.now());
|
||||||
startTimePicker.clear();
|
startTimePicker.clear();
|
||||||
endTimePicker.clear();
|
endTimePicker.clear();
|
||||||
breakMinutesField.setValue(30.0);
|
breakField.setValue(30.0);
|
||||||
targetMinutesField.setValue(480.0);
|
targetField.setValue(480.0);
|
||||||
statusComboBox.setValue(DayStatus.REMOTE);
|
statusCombo.setValue(DayStatus.REMOTE);
|
||||||
commentField.clear();
|
commentArea.clear();
|
||||||
updateButton.setEnabled(false);
|
updateBtn.setEnabled(false);
|
||||||
deleteButton.setEnabled(false);
|
deleteBtn.setEnabled(false);
|
||||||
checkIfDateAlreadyExists();
|
toggleSaveButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkIfDateAlreadyExists() {
|
private void toggleSaveButton() {
|
||||||
LocalDate date = datePicker.getValue();
|
saveBtn.setEnabled(isDateAvailable(datePicker.getValue()));
|
||||||
if (!isDateAvailable(date)) {
|
|
||||||
saveButton.setEnabled(false);
|
|
||||||
} else {
|
|
||||||
saveButton.setEnabled(true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isDateAvailable(LocalDate date) {
|
private boolean isDateAvailable(LocalDate date) {
|
||||||
|
|||||||
@@ -18,37 +18,37 @@ import org.springframework.security.crypto.password.PasswordEncoder;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Route(value = "admin/users", layout = MainLayout.class)
|
@Route(value = "admin/user-management", layout = MainLayout.class)
|
||||||
@PageTitle("Benutzerverwaltung")
|
@PageTitle("User Management")
|
||||||
@RolesAllowed("ROLE_ADMIN")
|
@RolesAllowed("ROLE_ADMIN")
|
||||||
public class UserAdminView extends VerticalLayout {
|
public class UserManagementView extends VerticalLayout {
|
||||||
|
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
private final PasswordEncoder passwordEncoder;
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
private final Grid<User> userGrid = new Grid<>(User.class, false);
|
private final Grid<User> userGrid = new Grid<>(User.class, false);
|
||||||
private final TextField usernameField = new TextField("Benutzername");
|
private final TextField usernameField = new TextField("Username");
|
||||||
private final PasswordField passwordField = new PasswordField("Passwort");
|
private final PasswordField passwordField = new PasswordField("Password");
|
||||||
private final MultiSelectComboBox<String> rolesField = new MultiSelectComboBox<>("Rollen");
|
private final MultiSelectComboBox<String> rolesField = new MultiSelectComboBox<>("Roles");
|
||||||
|
|
||||||
private final Button saveButton = new Button("Speichern");
|
private final Button saveButton = new Button("Save");
|
||||||
private final Button deleteButton = new Button("Löschen");
|
private final Button deleteButton = new Button("Delete");
|
||||||
|
|
||||||
private User selectedUser;
|
private User selectedUser;
|
||||||
|
|
||||||
public UserAdminView(UserRepository userRepository, PasswordEncoder passwordEncoder) {
|
public UserManagementView(UserRepository userRepository, PasswordEncoder passwordEncoder) {
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
this.passwordEncoder = passwordEncoder;
|
this.passwordEncoder = passwordEncoder;
|
||||||
|
|
||||||
add(new H2("Benutzerverwaltung"));
|
add(new H2("User Management"));
|
||||||
configureGrid();
|
setupUserGrid();
|
||||||
configureForm();
|
setupFormLayout();
|
||||||
refreshGrid();
|
loadUsersToGrid();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void configureGrid() {
|
private void setupUserGrid() {
|
||||||
userGrid.addColumn(User::getUsername).setHeader("Benutzername");
|
userGrid.addColumn(User::getUsername).setHeader("Username");
|
||||||
userGrid.addColumn(user -> String.join(", ", user.getRoles())).setHeader("Rollen");
|
userGrid.addColumn(user -> String.join(", ", user.getRoles())).setHeader("Roles");
|
||||||
userGrid.setSelectionMode(Grid.SelectionMode.SINGLE);
|
userGrid.setSelectionMode(Grid.SelectionMode.SINGLE);
|
||||||
userGrid.setWidthFull();
|
userGrid.setWidthFull();
|
||||||
|
|
||||||
@@ -64,10 +64,18 @@ public class UserAdminView extends VerticalLayout {
|
|||||||
add(userGrid);
|
add(userGrid);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void configureForm() {
|
private void setupFormLayout() {
|
||||||
rolesField.setItems("ROLE_USER", "ROLE_ADMIN");
|
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) {
|
if (selectedUser != null) {
|
||||||
selectedUser.setUsername(usernameField.getValue());
|
selectedUser.setUsername(usernameField.getValue());
|
||||||
selectedUser.setRoles(rolesField.getValue());
|
selectedUser.setRoles(rolesField.getValue());
|
||||||
@@ -85,29 +93,25 @@ public class UserAdminView extends VerticalLayout {
|
|||||||
.build();
|
.build();
|
||||||
userRepository.save(newUser);
|
userRepository.save(newUser);
|
||||||
}
|
}
|
||||||
clearForm();
|
|
||||||
refreshGrid();
|
|
||||||
});
|
|
||||||
|
|
||||||
deleteButton.addClickListener(e -> {
|
resetFormFields();
|
||||||
|
loadUsersToGrid();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteUser() {
|
||||||
if (selectedUser != null) {
|
if (selectedUser != null) {
|
||||||
userRepository.delete(selectedUser);
|
userRepository.delete(selectedUser);
|
||||||
clearForm();
|
resetFormFields();
|
||||||
refreshGrid();
|
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();
|
List<User> users = userRepository.findAll();
|
||||||
userGrid.setItems(users);
|
userGrid.setItems(users);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void clearForm() {
|
private void resetFormFields() {
|
||||||
selectedUser = null;
|
selectedUser = null;
|
||||||
usernameField.clear();
|
usernameField.clear();
|
||||||
passwordField.clear();
|
passwordField.clear();
|
||||||
@@ -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