addRestartAction #12
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -32,7 +36,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 +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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -4,29 +4,33 @@ 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.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 +43,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 +57,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 +112,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 +126,7 @@ public class MainView extends VerticalLayout {
|
|||||||
default -> timeEntryService.getEntriesForUser(currentUser);
|
default -> timeEntryService.getEntriesForUser(currentUser);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
renderKeyFigures(entries);
|
||||||
renderCharts(entries);
|
renderCharts(entries);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,16 +145,13 @@ 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);
|
Component overtimeChart = createOvertimeLineChart(allEntries, scopedEntries);
|
||||||
|
|
||||||
categoryChart.getStyle().set("minHeight", "45vh");
|
|
||||||
overtimeChart.getStyle().set("minHeight", "50vh");
|
|
||||||
|
|
||||||
chartLayout.add(categoryChart, overtimeChart);
|
chartLayout.add(categoryChart, overtimeChart);
|
||||||
chartLayout.setFlexGrow(1, categoryChart);
|
chartLayout.setFlexGrow(1, categoryChart);
|
||||||
@@ -157,6 +160,42 @@ 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 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) {
|
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 +230,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)"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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