3 Commits

Author SHA1 Message Date
Nils Burghardt
f09fee5a55 refactor dashboard 2025-07-21 14:51:25 +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
5 changed files with 100 additions and 18 deletions

View File

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

View File

@@ -32,7 +32,7 @@ public class TimeEntryService {
return 0; return 0;
} }
long total = Duration.between(entry.getStartTime(), entry.getEndTime()).toMinutes(); long total = Duration.between(entry.getStartTime(), entry.getEndTime()).toMinutes();
return (int)(total - entry.getPauseMinutes()); return (int) (total - entry.getPauseMinutes());
} }
public long calculateDeviation(TimeEntry entry) { public long calculateDeviation(TimeEntry entry) {
@@ -49,6 +49,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 +60,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);

View File

@@ -8,7 +8,7 @@ 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.UserManagementView; import de.nilzbu.mytimetracker.ui.view.UserManagementView;
import jakarta.annotation.security.PermitAll; import jakarta.annotation.security.PermitAll;
@@ -31,7 +31,7 @@ public class MainLayout extends AppLayout {
.set("font-size", "var(--lumo-font-size-l)") .set("font-size", "var(--lumo-font-size-l)")
.set("margin", "0"); .set("margin", "0");
RouterLink dashboardLink = 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 adminLink = new RouterLink("Admin", UserManagementView.class); RouterLink adminLink = new RouterLink("Admin", UserManagementView.class);

View File

@@ -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);
} }
@@ -107,7 +113,7 @@ public class MainView extends VerticalLayout {
List<TimeEntry> entries = switch (scope) { List<TimeEntry> entries = switch (scope) {
case "Month" -> { case "Month" -> {
configureFilterScope(yearSelector, monthSelector ); configureFilterScope(yearSelector, monthSelector);
yield timeEntryService.getEntriesForMonth(currentUser, yearSelector.getValue(), monthSelector.getValue()); yield timeEntryService.getEntriesForMonth(currentUser, yearSelector.getValue(), monthSelector.getValue());
} }
case "Quarter" -> { case "Quarter" -> {
@@ -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)"
);
} }
} }

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