Compare commits
4 Commits
6b72da2d1e
...
feature/2_
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d7329a811 | ||
|
|
f09fee5a55 | ||
|
|
ea9ceaa57e | ||
|
|
68ab7f49f1 |
@@ -10,7 +10,7 @@ version = '0.0.1-SNAPSHOT'
|
|||||||
|
|
||||||
java {
|
java {
|
||||||
toolchain {
|
toolchain {
|
||||||
languageVersion = JavaLanguageVersion.of(21)
|
languageVersion = JavaLanguageVersion.of(17)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,7 +22,6 @@ configurations {
|
|||||||
|
|
||||||
repositories {
|
repositories {
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven { url 'https://maven.vaadin.com/vaadin-addons' }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ext {
|
ext {
|
||||||
@@ -33,7 +32,6 @@ dependencies {
|
|||||||
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-security'
|
implementation 'org.springframework.boot:spring-boot-starter-security'
|
||||||
implementation 'com.vaadin:vaadin-spring-boot-starter'
|
implementation 'com.vaadin:vaadin-spring-boot-starter'
|
||||||
implementation("org.vaadin.crudui:crudui:7.2.0")
|
|
||||||
|
|
||||||
runtimeOnly 'com.mysql:mysql-connector-j:8.3.0'
|
runtimeOnly 'com.mysql:mysql-connector-j:8.3.0'
|
||||||
|
|
||||||
|
|||||||
@@ -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 org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:223)
|
||||||
at jdk.proxy4/jdk.proxy4.$Proxy164.save(Unknown Source)
|
at jdk.proxy4/jdk.proxy4.$Proxy164.save(Unknown Source)
|
||||||
at de.nilzbu.mytimetracker.service.TimeEntryService.save(TimeEntryService.java:31)
|
at de.nilzbu.mytimetracker.service.TimeEntryService.save(TimeEntryService.java:31)
|
||||||
at de.nilzbu.mytimetracker.ui.view.timeentry.TimeEntryView.lambda$configureButtons$9b1b5227$1(TimeEntryView.java:141)
|
at de.nilzbu.mytimetracker.ui.view.TimeEntryView.lambda$configureButtons$9b1b5227$1(TimeEntryView.java:141)
|
||||||
at com.vaadin.flow.component.ComponentEventBus.fireEventForListener(ComponentEventBus.java:244)
|
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.handleDomEvent(ComponentEventBus.java:501)
|
||||||
at com.vaadin.flow.component.ComponentEventBus.lambda$addDomTrigger$dd1b7957$1(ComponentEventBus.java:303)
|
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 org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:223)
|
||||||
at jdk.proxy4/jdk.proxy4.$Proxy164.save(Unknown Source)
|
at jdk.proxy4/jdk.proxy4.$Proxy164.save(Unknown Source)
|
||||||
at de.nilzbu.mytimetracker.service.TimeEntryService.save(TimeEntryService.java:31)
|
at de.nilzbu.mytimetracker.service.TimeEntryService.save(TimeEntryService.java:31)
|
||||||
at de.nilzbu.mytimetracker.ui.view.timeentry.TimeEntryView.lambda$configureButtons$9b1b5227$1(TimeEntryView.java:141)
|
at de.nilzbu.mytimetracker.ui.view.TimeEntryView.lambda$configureButtons$9b1b5227$1(TimeEntryView.java:141)
|
||||||
at com.vaadin.flow.component.ComponentEventBus.fireEventForListener(ComponentEventBus.java:244)
|
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.handleDomEvent(ComponentEventBus.java:501)
|
||||||
at com.vaadin.flow.component.ComponentEventBus.lambda$addDomTrigger$dd1b7957$1(ComponentEventBus.java:303)
|
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 org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:223)
|
||||||
at jdk.proxy4/jdk.proxy4.$Proxy164.save(Unknown Source)
|
at jdk.proxy4/jdk.proxy4.$Proxy164.save(Unknown Source)
|
||||||
at de.nilzbu.mytimetracker.service.TimeEntryService.save(TimeEntryService.java:31)
|
at de.nilzbu.mytimetracker.service.TimeEntryService.save(TimeEntryService.java:31)
|
||||||
at de.nilzbu.mytimetracker.ui.view.timeentry.TimeEntryView.lambda$configureButtons$9b1b5227$1(TimeEntryView.java:141)
|
at de.nilzbu.mytimetracker.ui.view.TimeEntryView.lambda$configureButtons$9b1b5227$1(TimeEntryView.java:141)
|
||||||
at com.vaadin.flow.component.ComponentEventBus.fireEventForListener(ComponentEventBus.java:244)
|
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.handleDomEvent(ComponentEventBus.java:501)
|
||||||
at com.vaadin.flow.component.ComponentEventBus.lambda$addDomTrigger$dd1b7957$1(ComponentEventBus.java:303)
|
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 org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:223)
|
||||||
at jdk.proxy4/jdk.proxy4.$Proxy164.save(Unknown Source)
|
at jdk.proxy4/jdk.proxy4.$Proxy164.save(Unknown Source)
|
||||||
at de.nilzbu.mytimetracker.service.TimeEntryService.save(TimeEntryService.java:31)
|
at de.nilzbu.mytimetracker.service.TimeEntryService.save(TimeEntryService.java:31)
|
||||||
at de.nilzbu.mytimetracker.ui.view.timeentry.TimeEntryView.lambda$configureButtons$9b1b5227$1(TimeEntryView.java:141)
|
at de.nilzbu.mytimetracker.ui.view.TimeEntryView.lambda$configureButtons$9b1b5227$1(TimeEntryView.java:141)
|
||||||
at com.vaadin.flow.component.ComponentEventBus.fireEventForListener(ComponentEventBus.java:244)
|
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.handleDomEvent(ComponentEventBus.java:501)
|
||||||
at com.vaadin.flow.component.ComponentEventBus.lambda$addDomTrigger$dd1b7957$1(ComponentEventBus.java:303)
|
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 org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:223)
|
||||||
at jdk.proxy4/jdk.proxy4.$Proxy164.save(Unknown Source)
|
at jdk.proxy4/jdk.proxy4.$Proxy164.save(Unknown Source)
|
||||||
at de.nilzbu.mytimetracker.service.TimeEntryService.save(TimeEntryService.java:31)
|
at de.nilzbu.mytimetracker.service.TimeEntryService.save(TimeEntryService.java:31)
|
||||||
at de.nilzbu.mytimetracker.ui.view.timeentry.TimeEntryView.lambda$configureButtons$9b1b5227$1(TimeEntryView.java:141)
|
at de.nilzbu.mytimetracker.ui.view.TimeEntryView.lambda$configureButtons$9b1b5227$1(TimeEntryView.java:141)
|
||||||
at com.vaadin.flow.component.ComponentEventBus.fireEventForListener(ComponentEventBus.java:244)
|
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.handleDomEvent(ComponentEventBus.java:501)
|
||||||
at com.vaadin.flow.component.ComponentEventBus.lambda$addDomTrigger$dd1b7957$1(ComponentEventBus.java:303)
|
at com.vaadin.flow.component.ComponentEventBus.lambda$addDomTrigger$dd1b7957$1(ComponentEventBus.java:303)
|
||||||
|
|||||||
@@ -1,69 +1,32 @@
|
|||||||
:root {
|
|
||||||
--lumo-font-size: 1rem;
|
|
||||||
--lumo-font-size-xxxl: 3rem;
|
|
||||||
--lumo-font-size-xxl: 2.25rem;
|
|
||||||
--lumo-font-size-xl: 1.75rem;
|
|
||||||
--lumo-font-size-l: 1.375rem;
|
|
||||||
--lumo-font-size-m: 1.125rem;
|
|
||||||
--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);
|
:root {
|
||||||
--lumo-primary-color-50pct: rgba(9, 134, 24, 0.5);
|
--lumo-font-size: 1rem;
|
||||||
--lumo-primary-color-10pct: rgba(9, 134, 24, 0.1);
|
--lumo-font-size-xxxl: 3rem;
|
||||||
--lumo-primary-color: hsl(127, 87%, 28%);
|
--lumo-font-size-xxl: 2.25rem;
|
||||||
|
--lumo-font-size-xl: 1.75rem;
|
||||||
--lumo-base-color: #dadcc7;
|
--lumo-font-size-l: 1.375rem;
|
||||||
|
--lumo-font-size-m: 1.125rem;
|
||||||
--lumo-tint-5pct: rgba(101, 105, 63, 0.05);
|
--lumo-font-size-s: 1rem;
|
||||||
--lumo-tint-10pct: rgba(101, 105, 63, 0.1);
|
--lumo-font-size-xs: 0.875rem;
|
||||||
--lumo-tint-20pct: rgba(101, 105, 63, 0.2);
|
--lumo-font-size-xxs: 0.8125rem;
|
||||||
--lumo-tint-30pct: rgba(101, 105, 63, 0.3);
|
--lumo-primary-text-color: rgb(9, 134, 24);
|
||||||
--lumo-tint-40pct: rgba(101, 105, 63, 0.4);
|
--lumo-primary-color-50pct: rgba(9, 134, 24, 0.5);
|
||||||
--lumo-tint-50pct: rgba(101, 105, 63, 0.5);
|
--lumo-primary-color-10pct: rgba(9, 134, 24, 0.1);
|
||||||
--lumo-tint-60pct: rgba(101, 105, 63, 0.6);
|
--lumo-primary-color: hsl(127, 87%, 28%);
|
||||||
--lumo-tint-70pct: rgba(101, 105, 63, 0.7);
|
--lumo-base-color: #dadcc79e;
|
||||||
--lumo-tint-80pct: rgba(101, 105, 63, 0.8);
|
--lumo-tint-5pct: rgba(101, 105, 63, 0.05);
|
||||||
--lumo-tint-90pct: rgba(101, 105, 63, 0.9);
|
--lumo-tint-10pct: rgba(101, 105, 63, 0.1);
|
||||||
--lumo-tint: #65693f;
|
--lumo-tint-20pct: rgba(101, 105, 63, 0.2);
|
||||||
|
--lumo-tint-30pct: rgba(101, 105, 63, 0.3);
|
||||||
--lumo-success-text-color: rgb(56, 204, 36);
|
--lumo-tint-40pct: rgba(101, 105, 63, 0.4);
|
||||||
--lumo-success-color-50pct: rgba(56, 204, 36, 0.5);
|
--lumo-tint-50pct: rgba(101, 105, 63, 0.5);
|
||||||
--lumo-success-color-10pct: rgba(56, 204, 36, 0.1);
|
--lumo-tint-60pct: rgba(101, 105, 63, 0.6);
|
||||||
--lumo-success-color: hsl(113, 70%, 47%);
|
--lumo-tint-70pct: rgba(101, 105, 63, 0.7);
|
||||||
|
--lumo-tint-80pct: rgba(101, 105, 63, 0.8);
|
||||||
/* Lumo-Tokens, die IntelliJ anmeckert – mit sinnvollen Defaults */
|
--lumo-tint-90pct: rgba(101, 105, 63, 0.9);
|
||||||
--lumo-space-xs: 0.25rem;
|
--lumo-tint: #65693fed;
|
||||||
--lumo-border-radius-s: 0.25rem;
|
--lumo-success-text-color: rgb(56, 204, 36);
|
||||||
--lumo-contrast-10pct: rgba(0, 0, 0, 0.06);
|
--lumo-success-color-50pct: rgba(56, 204, 36, 0.5);
|
||||||
--lumo-size-s: 2rem;
|
--lumo-success-color-10pct: rgba(56, 204, 36, 0.1);
|
||||||
}
|
--lumo-success-color: hsl(113, 70%, 47%);
|
||||||
|
}
|
||||||
/* 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%;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ public class TimeEntry {
|
|||||||
*/
|
*/
|
||||||
@Enumerated(EnumType.STRING)
|
@Enumerated(EnumType.STRING)
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
@Builder.Default
|
|
||||||
private DayStatus status = DayStatus.REMOTE;
|
private DayStatus status = DayStatus.REMOTE;
|
||||||
|
|
||||||
private String comment;
|
private String comment;
|
||||||
|
|||||||
@@ -34,11 +34,9 @@ public class User {
|
|||||||
private Set<String> roles; // z.B. ROLE_USER, ROLE_ADMIN
|
private Set<String> roles; // z.B. ROLE_USER, ROLE_ADMIN
|
||||||
|
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
@Builder.Default
|
|
||||||
private boolean enabled = true;
|
private boolean enabled = true;
|
||||||
|
|
||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
@Builder.Default
|
|
||||||
private boolean locked = false;
|
private boolean locked = false;
|
||||||
|
|
||||||
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
|
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ 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.DashboardOverView;
|
import de.nilzbu.mytimetracker.ui.view.DashboardOverView;
|
||||||
import de.nilzbu.mytimetracker.ui.view.timeentry.TimeEntryView;
|
import de.nilzbu.mytimetracker.ui.view.TimeEntryView;
|
||||||
import de.nilzbu.mytimetracker.ui.view.UserManagementView;
|
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;
|
||||||
|
|||||||
239
src/main/java/de/nilzbu/mytimetracker/ui/view/TimeEntryView.java
Normal file
239
src/main/java/de/nilzbu/mytimetracker/ui/view/TimeEntryView.java
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
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;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
setSizeFull();
|
||||||
|
|
||||||
|
initializeUser();
|
||||||
|
configureFormFields();
|
||||||
|
configureButtons();
|
||||||
|
configureGrid();
|
||||||
|
|
||||||
|
datePicker.addValueChangeListener(e -> toggleSaveButton());
|
||||||
|
|
||||||
|
add(buildFormLayout(), buildButtonLayout(), buildDateRangeFilterLayout(), 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 HorizontalLayout buildDateRangeFilterLayout() {
|
||||||
|
applyFilterBtn.addClickListener(e -> refreshGrid());
|
||||||
|
|
||||||
|
HorizontalLayout filterLayout = new HorizontalLayout(fromDate, toDate, applyFilterBtn);
|
||||||
|
filterLayout.setAlignItems(Alignment.END);
|
||||||
|
return filterLayout;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.setSizeFull();
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
entryGrid.getColumns().forEach(col -> col.setAutoWidth(true));
|
||||||
|
|
||||||
|
entryGrid.asSingleSelect().addValueChangeListener(event -> populateForm(event.getValue()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshGrid() {
|
||||||
|
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() {
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
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.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user