13 Commits

Author SHA1 Message Date
Nils Burghardt
0cd0f5f4df change runner 2025-07-21 14:18:03 +02:00
Nils Burghardt
e2e832cfcf add restart action 2025-07-21 14:09:13 +02:00
Nils Burghardt
ea9ceaa57e remove unused method 2025-07-21 12:51:38 +02:00
Nils Burghardt
68ab7f49f1 add key figures 2025-07-21 12:32:24 +02:00
7de22a723e fix main view charts (#8)
All checks were successful
Build-und-Deploy / build (push) Successful in 21s
Co-authored-by: Nils Burghardt <nilzbu@gmail.com>
Reviewed-on: #8
2025-07-14 09:49:20 +00:00
a64d56bbd9 Feature 5 enhance table with sort and filter functionality (#6)
All checks were successful
Build-und-Deploy / build (push) Successful in 11s
Co-authored-by: Nils Burghardt <nilzbu@gmail.com>
Reviewed-on: #6
2025-07-13 17:14:00 +00:00
7446839a7b Merge pull request 'Clean code update 1' (#1) from cq_1 into master
All checks were successful
Build-und-Deploy / build (push) Successful in 12s
Reviewed-on: #1
2025-07-13 16:42:27 +00:00
Nils Burghardt
2c7dd8c8f2 upgrade clean code DayStatus SecurityConfig 2025-07-13 18:41:53 +02:00
Nils Burghardt
44672876be upgrade clean code ChartJsComponent 2025-07-13 18:35:03 +02:00
Nils Burghardt
e828e83991 upgrade clean code MainLayout 2025-07-13 18:34:31 +02:00
Nils Burghardt
6b57a0c5e5 upgrade clean code UserManagementView 2025-07-13 18:30:20 +02:00
Nils Burghardt
0f005ca130 Remove DB file from Git and add to .gitignore 2025-07-13 18:28:10 +02:00
Nils Burghardt
acb96dc1c6 upgrade clean code TimeEntryView 2025-07-13 18:27:21 +02:00
13 changed files with 353 additions and 234 deletions

View 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 /pfad/zum/docker-verzeichnis
docker compose down
docker compose up -d

1
.gitignore vendored
View File

@@ -93,3 +93,4 @@ coverage/
*.bak
*.tmpdata/*.mv.db
src/main/frontend/generated/
/data/*.mv.db

Binary file not shown.

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -49,6 +49,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 +60,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);

View File

@@ -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);

View File

@@ -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.DashboardOverView;
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.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"));

View File

@@ -4,29 +4,33 @@ 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.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 +43,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 +57,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 +126,7 @@ public class MainView extends VerticalLayout {
default -> timeEntryService.getEntriesForUser(currentUser);
};
renderKeyFigures(entries);
renderCharts(entries);
}
@@ -139,16 +145,13 @@ 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().set("minHeight", "45vh");
overtimeChart.getStyle().set("minHeight", "50vh");
chartLayout.add(categoryChart, overtimeChart);
chartLayout.setFlexGrow(1, categoryChart);
@@ -157,6 +160,42 @@ 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 deviation = new KeyFigureWidget(
"Deviation",
"" + scopedEntries.stream()
.map(entry -> timeEntryService.calculateNetWorkMinutes(entry) - entry.getTargetMinutes())
.reduce(0, Integer::sum)
);
keyFigureLayout.add(deviation, workingDays, remoteDays, officeDays, vacationDays, sickDays);
contentContainer.add(keyFigureLayout);
}
private Component createCategoryBarChart(List<TimeEntry> entries) {
Map<String, Long> statusCount = entries.stream()
.collect(Collectors.groupingBy(e -> e.getStatus().name(), Collectors.counting()));
@@ -191,6 +230,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)"
);
}
}

View File

@@ -22,6 +22,7 @@ import jakarta.annotation.security.PermitAll;
import org.springframework.security.core.context.SecurityContextHolder;
import java.time.LocalDate;
import java.util.List;
@Route(value = "time-entry", layout = MainLayout.class)
@PageTitle("Time Entry")
@@ -30,75 +31,85 @@ 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 breakMinutesField = new NumberField("Break (minutes)");
private final NumberField targetMinutesField = new NumberField("Target Time (minutes)");
private final TextArea commentField = new TextArea("Comment");
private final ComboBox<DayStatus> statusComboBox = new ComboBox<>("Status");
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 saveButton = new Button("Save");
private final Button updateButton = new Button("Update");
private final Button deleteButton = new Button("Delete");
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;
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) {
this.timeEntryService = timeEntryService;
this.userRepository = userRepository;
initializeCurrentUser();
configureFields();
setSizeFull();
initializeUser();
configureFormFields();
configureButtons();
configureGrid();
datePicker.addValueChangeListener(e -> checkIfDateAlreadyExists());
datePicker.addValueChangeListener(e -> toggleSaveButton());
add(createFormLayout(), createButtonLayout(), entryGrid);
add(buildFormLayout(), buildButtonLayout(), buildDateRangeFilterLayout(), entryGrid);
refreshGrid();
checkIfDateAlreadyExists(); // Initiale Prüfung
toggleSaveButton();
}
private void initializeCurrentUser() {
private void initializeUser() {
String username = SecurityContextHolder.getContext().getAuthentication().getName();
this.currentUser = userRepository.findByUsername(username)
.orElseThrow(() -> new RuntimeException("User not found"));
}
private void configureFields() {
private void configureFormFields() {
datePicker.setValue(LocalDate.now());
breakMinutesField.setValue(30.0);
targetMinutesField.setValue(480.0);
statusComboBox.setItems(DayStatus.values());
statusComboBox.setValue(DayStatus.REMOTE);
breakField.setValue(30.0);
targetField.setValue(480.0);
statusCombo.setItems(DayStatus.values());
statusCombo.setValue(DayStatus.REMOTE);
}
private HorizontalLayout createFormLayout() {
return new HorizontalLayout(
datePicker,
startTimePicker,
endTimePicker,
breakMinutesField,
targetMinutesField,
commentField,
statusComboBox
);
private HorizontalLayout buildFormLayout() {
return new HorizontalLayout(datePicker, startTimePicker, endTimePicker,
breakField, targetField, commentArea, statusCombo);
}
private HorizontalLayout createButtonLayout() {
updateButton.setEnabled(false);
deleteButton.setEnabled(false);
return new HorizontalLayout(saveButton, updateButton, deleteButton);
private HorizontalLayout buildButtonLayout() {
updateBtn.setEnabled(false);
deleteBtn.setEnabled(false);
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() {
saveButton.addClickListener(e -> {
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;
@@ -109,80 +120,98 @@ public class TimeEntryView extends VerticalLayout {
.date(datePicker.getValue())
.startTime(startTimePicker.getValue())
.endTime(endTimePicker.getValue())
.pauseMinutes(breakMinutesField.getValue().intValue())
.targetMinutes(targetMinutesField.getValue().intValue())
.status(statusComboBox.getValue())
.comment(commentField.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");
});
}
updateButton.addClickListener(e -> {
private void updateEntry() {
if (selectedEntry == null) return;
selectedEntry.setDate(datePicker.getValue());
selectedEntry.setStartTime(startTimePicker.getValue());
selectedEntry.setEndTime(endTimePicker.getValue());
selectedEntry.setPauseMinutes(breakMinutesField.getValue().intValue());
selectedEntry.setTargetMinutes(targetMinutesField.getValue().intValue());
selectedEntry.setStatus(statusComboBox.getValue());
selectedEntry.setComment(commentField.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");
});
}
deleteButton.addClickListener(e -> {
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.setSizeFull();
entryGrid.asSingleSelect().addValueChangeListener(event -> {
selectedEntry = event.getValue();
if (selectedEntry != null) {
datePicker.setValue(selectedEntry.getDate());
startTimePicker.setValue(selectedEntry.getStartTime());
endTimePicker.setValue(selectedEntry.getEndTime());
breakMinutesField.setValue((double) selectedEntry.getPauseMinutes());
targetMinutesField.setValue((double) selectedEntry.getTargetMinutes());
statusComboBox.setValue(selectedEntry.getStatus());
commentField.setValue(selectedEntry.getComment() != null ? selectedEntry.getComment() : "");
entryGrid.addColumn(TimeEntry::getDate).setHeader("Date").setSortable(true);
entryGrid.addColumn(TimeEntry::getStartTime).setHeader("Start").setSortable(true);
entryGrid.addColumn(TimeEntry::getEndTime).setHeader("End").setSortable(true);
entryGrid.addColumn(TimeEntry::getPauseMinutes).setHeader("Break (min)").setSortable(true);
entryGrid.addColumn(TimeEntry::getTargetMinutes).setHeader("Target (min)").setSortable(true);
entryGrid.addColumn(timeEntryService::calculateNetWorkMinutes).setHeader("Actual (min)").setSortable(true);
entryGrid.addColumn(TimeEntry::getStatus).setHeader("Status").setSortable(true);
entryGrid.addColumn(TimeEntry::getComment).setHeader("Comment").setSortable(true);
entryGrid.addColumn(timeEntryService::calculateDeviation).setHeader("Deviation (min)").setSortable(true);
updateButton.setEnabled(true);
deleteButton.setEnabled(true);
saveButton.setEnabled(false);
} else {
updateButton.setEnabled(false);
deleteButton.setEnabled(false);
checkIfDateAlreadyExists();
}
});
entryGrid.getColumns().forEach(col -> col.setAutoWidth(true));
entryGrid.asSingleSelect().addValueChangeListener(event -> populateForm(event.getValue()));
}
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() {
@@ -190,22 +219,17 @@ public class TimeEntryView extends VerticalLayout {
datePicker.setValue(LocalDate.now());
startTimePicker.clear();
endTimePicker.clear();
breakMinutesField.setValue(30.0);
targetMinutesField.setValue(480.0);
statusComboBox.setValue(DayStatus.REMOTE);
commentField.clear();
updateButton.setEnabled(false);
deleteButton.setEnabled(false);
checkIfDateAlreadyExists();
breakField.setValue(30.0);
targetField.setValue(480.0);
statusCombo.setValue(DayStatus.REMOTE);
commentArea.clear();
updateBtn.setEnabled(false);
deleteBtn.setEnabled(false);
toggleSaveButton();
}
private void checkIfDateAlreadyExists() {
LocalDate date = datePicker.getValue();
if (!isDateAvailable(date)) {
saveButton.setEnabled(false);
} else {
saveButton.setEnabled(true);
}
private void toggleSaveButton() {
saveBtn.setEnabled(isDateAvailable(datePicker.getValue()));
}
private boolean isDateAvailable(LocalDate date) {

View File

@@ -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();

View File

@@ -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);
}
}