Compare commits
8 Commits
44672876be
...
addRestart
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0cd0f5f4df | ||
|
|
e2e832cfcf | ||
|
|
ea9ceaa57e | ||
|
|
68ab7f49f1 | ||
| 7de22a723e | |||
| a64d56bbd9 | |||
| 7446839a7b | |||
|
|
2c7dd8c8f2 |
14
.gitea/workflows/restart.yaml
Normal file
14
.gitea/workflows/restart.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
name: Docker Restart Manuell
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
restart-docker:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Docker Compose Restart
|
||||
run: |
|
||||
cd /pfad/zum/docker-verzeichnis
|
||||
docker compose down
|
||||
docker compose up -d
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -37,7 +37,9 @@ public class ChartJsComponent extends Div {
|
||||
getElement().executeJs(
|
||||
"""
|
||||
const canvas = document.createElement('canvas');
|
||||
appendChild(canvas);
|
||||
canvas.style.width = "100%";
|
||||
canvas.style.height = "100%";
|
||||
this.appendChild(canvas);
|
||||
const ctx = canvas.getContext('2d');
|
||||
new Chart(ctx, {
|
||||
type: $0,
|
||||
@@ -64,8 +66,7 @@ public class ChartJsComponent extends Div {
|
||||
}
|
||||
|
||||
/**
|
||||
* Detects chart type based on chart data.
|
||||
* If no definitive type can be determined, defaults to "bar".
|
||||
* Bestimmt den Diagrammtyp basierend auf dem Dataset.
|
||||
*/
|
||||
private String detectChartType(JsonObject data) {
|
||||
try {
|
||||
@@ -82,7 +83,7 @@ public class ChartJsComponent extends Div {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates JSON chart data for a bar chart based on category counts.
|
||||
* Erstellt ein Balkendiagramm für Kategoriezählungen.
|
||||
*/
|
||||
public static JsonObject generateBarChartData(Map<String, Long> categoryCounts) {
|
||||
JsonObject data = Json.createObject();
|
||||
@@ -117,7 +118,7 @@ public class ChartJsComponent extends Div {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates JSON chart data for a line chart showing overtime balance.
|
||||
* Erstellt ein Liniendiagramm zur Darstellung des Überzeit-Saldos.
|
||||
*/
|
||||
public static JsonObject generateLineChartData(List<LocalDate> dates, List<Double> balanceValues) {
|
||||
JsonObject data = Json.createObject();
|
||||
|
||||
@@ -8,7 +8,7 @@ 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.UserManagementView;
|
||||
import jakarta.annotation.security.PermitAll;
|
||||
@@ -31,7 +31,7 @@ public class MainLayout extends AppLayout {
|
||||
.set("font-size", "var(--lumo-font-size-l)")
|
||||
.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 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.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)"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
@@ -47,10 +48,16 @@ public class TimeEntryView extends VerticalLayout {
|
||||
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();
|
||||
@@ -58,7 +65,7 @@ public class TimeEntryView extends VerticalLayout {
|
||||
|
||||
datePicker.addValueChangeListener(e -> toggleSaveButton());
|
||||
|
||||
add(buildFormLayout(), buildButtonLayout(), entryGrid);
|
||||
add(buildFormLayout(), buildButtonLayout(), buildDateRangeFilterLayout(), entryGrid);
|
||||
refreshGrid();
|
||||
toggleSaveButton();
|
||||
}
|
||||
@@ -88,6 +95,14 @@ public class TimeEntryView extends VerticalLayout {
|
||||
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());
|
||||
@@ -144,21 +159,39 @@ public class TimeEntryView extends VerticalLayout {
|
||||
}
|
||||
|
||||
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.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() {
|
||||
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) {
|
||||
|
||||
@@ -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