From 991c023ed961a73e6d7ca25f38be75b04e5b8485 Mon Sep 17 00:00:00 2001 From: jzitnik-dev Date: Sat, 30 May 2026 12:24:06 +0200 Subject: [PATCH] feat: Added absences and moodle integration --- TestAvg.java | 16 -- pom.xml | 13 ++ .../controllers/AbsencesController.java | 173 ++++++++++++++++++ .../controllers/DashboardBaseController.java | 3 + .../jzitnik/controllers/MoodleController.java | 167 +++++++++++++++++ src/main/java/cz/jzitnik/state/AppState.java | 7 + src/main/resources/absences.fxml | 50 +++++ src/main/resources/dashboard_modern.fxml | 5 +- src/main/resources/moodle.fxml | 37 ++++ src/main/resources/styles/absences.css | 77 ++++++++ src/main/resources/styles/moodle-dark.css | 8 + src/main/resources/styles/moodle.css | 30 +++ 12 files changed, 569 insertions(+), 17 deletions(-) delete mode 100644 TestAvg.java create mode 100644 src/main/java/cz/jzitnik/controllers/AbsencesController.java create mode 100644 src/main/java/cz/jzitnik/controllers/MoodleController.java create mode 100644 src/main/resources/absences.fxml create mode 100644 src/main/resources/moodle.fxml create mode 100644 src/main/resources/styles/absences.css create mode 100644 src/main/resources/styles/moodle-dark.css create mode 100644 src/main/resources/styles/moodle.css diff --git a/TestAvg.java b/TestAvg.java deleted file mode 100644 index 7d0fbb8..0000000 --- a/TestAvg.java +++ /dev/null @@ -1,16 +0,0 @@ -import io.github.tomhula.jecnaapi.data.grade.*; -import io.github.tomhula.jecnaapi.util.Name; -import kotlinx.datetime.LocalDate; -import java.util.Map; -import java.util.List; - -public class TestAvg { - public static void main(String[] args) { - // Let's see if we can create a Grade - Grade g1 = new Grade(1, false, new Name("Jan", "Novak"), "Test", new LocalDate(2023, 1, 1), 0); - Grade g2 = new Grade(2, true, new Name("Jan", "Novak"), "Test", new LocalDate(2023, 1, 1), 0); - - System.out.println("Grade 1: " + g1.getValue() + " small: " + g1.getSmall()); - System.out.println("Grade 2: " + g2.getValue() + " small: " + g2.getSmall()); - } -} diff --git a/pom.xml b/pom.xml index 675a3ee..7d7c6fb 100644 --- a/pom.xml +++ b/pom.xml @@ -24,6 +24,14 @@ cz.jzitnik.Main + + org.codehaus.mojo + exec-maven-plugin + 3.1.1 + + cz.jzitnik.Main + + @@ -39,6 +47,11 @@ javafx-fxml 21.0.1 + + org.openjfx + javafx-web + 21.0.1 + diff --git a/src/main/java/cz/jzitnik/controllers/AbsencesController.java b/src/main/java/cz/jzitnik/controllers/AbsencesController.java new file mode 100644 index 0000000..50ee2e0 --- /dev/null +++ b/src/main/java/cz/jzitnik/controllers/AbsencesController.java @@ -0,0 +1,173 @@ +package cz.jzitnik.controllers; + +import cz.jzitnik.router.Route; +import cz.jzitnik.router.Router; +import cz.jzitnik.query.QueryOptions; +import cz.jzitnik.query.QueryResult; +import io.github.tomhula.jecnaapi.data.absence.AbsenceInfo; +import io.github.tomhula.jecnaapi.data.absence.AbsencesPage; +import kotlinx.datetime.LocalDate; +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.geometry.Pos; +import javafx.scene.control.*; +import javafx.scene.layout.*; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +@Route(path = "/absences", fxml = "/absences.fxml") +public class AbsencesController extends DashboardBaseController { + + @FXML + private ProgressIndicator loadingIndicator; + + @FXML + private ScrollPane scrollPane; + + @FXML + private HBox summaryBox; + + @FXML + private FlowPane detailFlow; + + private AbsencesPage currentPage; + + @Override + public void onNavigate(Map props) { + super.onNavigate(props); + loadAbsences(); + } + + private void loadAbsences() { + loadingIndicator.setVisible(true); + scrollPane.setVisible(false); + summaryBox.getChildren().clear(); + detailFlow.getChildren().clear(); + + appState.getQueryClient().fetch("absences:page", () -> { + try { + return appState.getClient().getAbsencesPage().join(); + } catch (Exception e) { + e.printStackTrace(); + return null; + } + }, QueryOptions.defaultOptions()).thenAccept(this::handleAbsencesResult); + } + + private void handleAbsencesResult(QueryResult result) { + Platform.runLater(() -> { + loadingIndicator.setVisible(false); + + if (result.isSuccess() && result.getData().isPresent()) { + currentPage = result.getData().get(); + renderAbsences(); + scrollPane.setVisible(true); + } else { + Label errorLabel = new Label("Failed to load absences."); + errorLabel.getStyleClass().addAll("text-danger", "title-3"); + summaryBox.getChildren().add(errorLabel); + scrollPane.setVisible(true); + } + }); + } + + private void renderAbsences() { + summaryBox.getChildren().clear(); + detailFlow.getChildren().clear(); + + if (currentPage.getDays().isEmpty()) { + Label noAbsences = new Label("Žádné absence v tomto roce!"); + noAbsences.getStyleClass().addAll("title-3", "text-success"); + summaryBox.getChildren().add(noAbsences); + return; + } + + int totalAbsent = 0; + int totalUnexcused = 0; + int totalLate = 0; + + List sortedDays = new ArrayList<>(currentPage.getDays()); + sortedDays.sort(Comparator.reverseOrder()); + + for (LocalDate day : sortedDays) { + AbsenceInfo info = currentPage.get(day); + if (info != null) { + totalAbsent += info.getHoursAbsent(); + totalUnexcused += info.getUnexcusedHours(); + totalLate += info.getLateEntryCount(); + + detailFlow.getChildren().add(createAbsenceCard(day, info)); + } + } + + summaryBox.getChildren().addAll( + createSummaryCard("Zameškané hodiny", totalAbsent, "absence-summary-total"), + createSummaryCard("Neomluvené", totalUnexcused, totalUnexcused > 0 ? "absence-summary-danger" : "absence-summary-safe"), + createSummaryCard("Pozdní příchody", totalLate, totalLate > 0 ? "absence-summary-warning" : "absence-summary-safe") + ); + } + + private VBox createSummaryCard(String title, int count, String styleClass) { + VBox card = new VBox(5); + card.setAlignment(Pos.CENTER); + card.getStyleClass().addAll("card", "absence-summary-card", styleClass); + + Label valueLbl = new Label(String.valueOf(count)); + valueLbl.getStyleClass().add("absence-summary-value"); + + Label titleLbl = new Label(title); + titleLbl.getStyleClass().add("text-muted"); + + card.getChildren().addAll(valueLbl, titleLbl); + return card; + } + + private VBox createAbsenceCard(LocalDate day, AbsenceInfo info) { + VBox card = new VBox(8); + card.getStyleClass().addAll("card", "absence-card"); + + if (info.getUnexcusedHours() > 0) { + card.getStyleClass().add("absence-card-danger"); + } else if (info.getLateEntryCount() > 0) { + card.getStyleClass().add("absence-card-warning"); + } + + Label dateLbl = new Label(formatDate(day)); + dateLbl.getStyleClass().addAll("title-4", "absence-card-date"); + + HBox stats = new HBox(15); + stats.setAlignment(Pos.CENTER_LEFT); + + Label hoursLbl = new Label("Hodiny: " + info.getHoursAbsent()); + hoursLbl.getStyleClass().add("text-light"); + stats.getChildren().add(hoursLbl); + + if (info.getUnexcusedHours() > 0) { + Label unexcusedLbl = new Label("Neomluveno: " + info.getUnexcusedHours()); + unexcusedLbl.getStyleClass().addAll("text-danger", "text-bold"); + stats.getChildren().add(unexcusedLbl); + } + + if (info.getLateEntryCount() > 0) { + Label lateLbl = new Label("Pozdě: " + info.getLateEntryCount()); + lateLbl.getStyleClass().addAll("text-warning", "text-bold"); + stats.getChildren().add(lateLbl); + } + + card.getChildren().addAll(dateLbl, stats); + return card; + } + + @SuppressWarnings("deprecation") + private String formatDate(LocalDate date) { + return date.getDayOfMonth() + ". " + date.getMonthNumber() + ". " + date.getYear(); + } + + @FXML + protected void onBackToDashboard() { + Router.getInstance().navigate("/dashboard"); + } +} diff --git a/src/main/java/cz/jzitnik/controllers/DashboardBaseController.java b/src/main/java/cz/jzitnik/controllers/DashboardBaseController.java index 43922f2..211aeb4 100644 --- a/src/main/java/cz/jzitnik/controllers/DashboardBaseController.java +++ b/src/main/java/cz/jzitnik/controllers/DashboardBaseController.java @@ -41,6 +41,9 @@ public class DashboardBaseController implements Routable { @FXML protected void onNavigateToSpecial() { Router.getInstance().navigate("/special"); } + @FXML + protected void onNavigateToMoodle() { Router.getInstance().navigate("/moodle"); } + @FXML protected void onDoNothing() { } diff --git a/src/main/java/cz/jzitnik/controllers/MoodleController.java b/src/main/java/cz/jzitnik/controllers/MoodleController.java new file mode 100644 index 0000000..b02ef70 --- /dev/null +++ b/src/main/java/cz/jzitnik/controllers/MoodleController.java @@ -0,0 +1,167 @@ +package cz.jzitnik.controllers; + +import cz.jzitnik.router.Route; +import cz.jzitnik.router.Router; +import javafx.concurrent.Worker; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ProgressIndicator; +import javafx.scene.web.WebEngine; +import javafx.scene.web.WebHistory; +import javafx.scene.web.WebView; +import java.util.Map; +import java.util.Objects; + +@Route(path = "/moodle", fxml = "/moodle.fxml") +public class MoodleController extends DashboardBaseController { + + @FXML + private WebView webView; + + @FXML + private ProgressIndicator loadingIndicator; + + @FXML + private Label titleLabel; + + @FXML + private Button btnBack; + + @FXML + private Button btnForward; + + private WebEngine webEngine; + + @Override + public void onNavigate(Map props) { + super.onNavigate(props); + + webEngine = webView.getEngine(); + + webEngine.getLoadWorker().stateProperty().addListener((obs, oldState, newState) -> { + if (newState == Worker.State.SUCCEEDED) { + String currentUrl = webEngine.getLocation(); + boolean isLoginPage = currentUrl != null && currentUrl.contains("login/index.php"); + + Boolean isLoggedOut = false; + try { + Object result = webEngine.executeScript("document.body && document.body.classList.contains('notloggedin')"); + if (result instanceof Boolean) { + isLoggedOut = (Boolean) result; + } + } catch (Exception e) {} + + if (isLoginPage) { + String username = appState.getUsername(); + String password = appState.getPassword(); + + if (username != null && password != null) { + String script = String.format( + "var u = document.getElementById('username');" + + "var p = document.getElementById('password');" + + "var b = document.getElementById('loginbtn');" + + "if (u && p && b) {" + + " u.value = '%s';" + + " p.value = '%s';" + + " b.click();" + + "}", + username.replace("'", "\\'"), + password.replace("'", "\\'") + ); + webEngine.executeScript(script); + } else { + loadingIndicator.setVisible(false); + webView.setVisible(true); + } + } else if (isLoggedOut) { + webEngine.executeScript("window.location.href = 'https://moodle.spsejecna.cz/login/index.php';"); + } else { + loadingIndicator.setVisible(false); + webView.setVisible(true); + } + + } else if (newState == Worker.State.FAILED) { + titleLabel.setText("Failed to load"); + loadingIndicator.setVisible(false); + } + }); + + webEngine.titleProperty().addListener((obs, oldTitle, newTitle) -> { + if (newTitle != null && !newTitle.isEmpty()) { + titleLabel.setText(newTitle); + } + }); + + webEngine.getHistory().currentIndexProperty().addListener((obs, oldVal, newVal) -> { + updateNavigationButtons(); + }); + updateNavigationButtons(); + + loadingIndicator.setVisible(true); + webView.setVisible(false); + webEngine.setUserStyleSheetLocation(Objects.requireNonNull(getClass().getResource("/styles/moodle-dark.css")).toExternalForm()); + webEngine.load("https://moodle.spsejecna.cz/"); + } + + private void updateNavigationButtons() { + WebHistory history = webEngine.getHistory(); + int currentIndex = history.getCurrentIndex(); + + int validBackIndex = -1; + for (int i = currentIndex - 1; i >= 0; i--) { + String url = history.getEntries().get(i).getUrl(); + if (url == null || !url.contains("login/index.php")) { + validBackIndex = i; + break; + } + } + btnBack.setDisable(validBackIndex == -1); + + int validForwardIndex = -1; + for (int i = currentIndex + 1; i < history.getEntries().size(); i++) { + String url = history.getEntries().get(i).getUrl(); + if (url == null || !url.contains("login/index.php")) { + validForwardIndex = i; + break; + } + } + btnForward.setDisable(validForwardIndex == -1); + } + + @FXML + protected void onBrowserBack() { + WebHistory history = webEngine.getHistory(); + int currentIndex = history.getCurrentIndex(); + for (int i = currentIndex - 1; i >= 0; i--) { + String url = history.getEntries().get(i).getUrl(); + if (url == null || !url.contains("login/index.php")) { + history.go(i - currentIndex); + break; + } + } + } + + @FXML + protected void onBrowserForward() { + WebHistory history = webEngine.getHistory(); + int currentIndex = history.getCurrentIndex(); + for (int i = currentIndex + 1; i < history.getEntries().size(); i++) { + String url = history.getEntries().get(i).getUrl(); + if (url == null || !url.contains("login/index.php")) { + history.go(i - currentIndex); + break; + } + } + } + + @FXML + protected void onBrowserReload() { + webEngine.reload(); + } + + @FXML + protected void onBackToDashboard() { + Router.getInstance().navigate("/dashboard"); + } +} diff --git a/src/main/java/cz/jzitnik/state/AppState.java b/src/main/java/cz/jzitnik/state/AppState.java index cbe0d7b..69a26d4 100644 --- a/src/main/java/cz/jzitnik/state/AppState.java +++ b/src/main/java/cz/jzitnik/state/AppState.java @@ -8,6 +8,7 @@ import cz.jzitnik.query.QueryClient; public class AppState { private String username; + private String password; private JecnaClientJavaWrapper client; private QueryClient queryClient; @@ -15,6 +16,10 @@ public class AppState { return username; } + public String getPassword() { + return password; + } + public JecnaClientJavaWrapper getClient() { return client; } @@ -38,6 +43,7 @@ public class AppState { boolean ok = client.login(username, password).join(); if (ok) { this.username = username; + this.password = password; try { CredentialStore.saveCredentials(username, password); } catch (Throwable t) { @@ -57,6 +63,7 @@ public class AppState { public void clear() { this.username = null; + this.password = null; if (this.client != null) { try { this.client.logout(); diff --git a/src/main/resources/absences.fxml b/src/main/resources/absences.fxml new file mode 100644 index 0000000..1d010eb --- /dev/null +++ b/src/main/resources/absences.fxml @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + +