feat: Added grades

This commit is contained in:
2026-05-29 19:17:40 +02:00
parent c0feda4ba8
commit 0a80f89136
7 changed files with 474 additions and 74 deletions
@@ -0,0 +1,266 @@
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.timetable.Lesson;
import io.github.tomhula.jecnaapi.data.timetable.LessonPeriod;
import io.github.tomhula.jecnaapi.data.timetable.LessonSpot;
import io.github.tomhula.jecnaapi.data.timetable.Timetable;
import io.github.tomhula.jecnaapi.data.timetable.TimetablePage;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import kotlinx.datetime.DayOfWeek;
import java.util.List;
import java.util.Map;
@Route(path = "/timetable", fxml = "/timetable.fxml")
public class TimetableController extends DashboardBaseController {
@FXML
private GridPane timetableGrid;
@FXML
private ProgressIndicator loadingIndicator;
@FXML
private ScrollPane scrollPane;
private TimetablePage currentPage;
@Override
public void onNavigate(Map<String, Object> props) {
super.onNavigate(props);
loadTimetable();
}
private void loadTimetable() {
loadingIndicator.setVisible(true);
scrollPane.setVisible(false);
timetableGrid.getChildren().clear();
appState.getQueryClient().fetch("timetable:page", () -> {
try {
return appState.getClient().getTimetablePage().join();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}, QueryOptions.defaultOptions()).thenAccept(this::handleTimetableResult);
}
private void handleTimetableResult(QueryResult<TimetablePage> result) {
Platform.runLater(() -> {
loadingIndicator.setVisible(false);
if (result.isSuccess() && result.getData().isPresent()) {
scrollPane.setVisible(true);
currentPage = result.getData().get();
renderTimetable();
} else {
Label errorLabel = new Label("Failed to load timetable.");
errorLabel.getStyleClass().addAll("text-danger", "title-3");
timetableGrid.add(errorLabel, 0, 0);
timetableGrid.setAlignment(Pos.CENTER);
scrollPane.setVisible(true);
}
});
}
private void renderTimetable() {
timetableGrid.getChildren().clear();
timetableGrid.getColumnConstraints().clear();
timetableGrid.getRowConstraints().clear();
timetableGrid.setAlignment(Pos.CENTER);
Timetable timetable = currentPage.getTimetable();
List<LessonPeriod> periods = timetable.getLessonPeriods();
List<DayOfWeek> days = timetable.getDaysSorted();
ColumnConstraints dayCol = new ColumnConstraints();
dayCol.setMinWidth(120);
timetableGrid.getColumnConstraints().add(dayCol);
for (int i = 0; i < periods.size(); i++) {
LessonPeriod p = periods.get(i);
VBox header = new VBox(2);
header.setAlignment(Pos.CENTER);
header.getStyleClass().add("timetable-header");
Label indexLbl = new Label(String.valueOf(i));
indexLbl.getStyleClass().add("timetable-header-index");
Label timeLbl = new Label(p.toString());
timeLbl.getStyleClass().add("timetable-header-time");
header.getChildren().addAll(indexLbl, timeLbl);
ColumnConstraints col = new ColumnConstraints();
col.setMinWidth(110);
timetableGrid.getColumnConstraints().add(col);
timetableGrid.add(header, i + 1, 0);
}
int rowIndex = 1;
for (DayOfWeek day : days) {
VBox dayCell = new VBox();
dayCell.setAlignment(Pos.CENTER_RIGHT);
dayCell.getStyleClass().add("timetable-day-cell");
Label dayLabel = new Label(translateDay(day));
dayLabel.getStyleClass().addAll("timetable-day-label", "title-3");
dayCell.getChildren().add(dayLabel);
timetableGrid.add(dayCell, 0, rowIndex);
List<LessonSpot> spots = timetable.get(day);
if (spots != null) {
int colIndex = 1;
for (LessonSpot spot : spots) {
int span = spot.getPeriodSpan();
if (spot.isNotEmpty()) {
VBox cell = createLessonCell(spot);
timetableGrid.add(cell, colIndex, rowIndex, span, 1);
} else {
VBox emptyCell = new VBox();
emptyCell.getStyleClass().add("timetable-cell-empty");
timetableGrid.add(emptyCell, colIndex, rowIndex, span, 1);
}
colIndex += span;
}
}
rowIndex++;
}
}
private VBox createLessonCell(LessonSpot spot) {
VBox cell = new VBox(5);
cell.setAlignment(Pos.CENTER);
cell.getStyleClass().addAll("card", "timetable-cell");
if (spot.getSize() == 1) {
Lesson lesson = spot.getLesson(0);
addLessonInfoToCell(cell, lesson);
} else {
VBox groupContainer = new VBox(5);
groupContainer.setAlignment(Pos.CENTER);
for (int i = 0; i < spot.getSize(); i++) {
Lesson lesson = spot.getLesson(i);
VBox groupCell = new VBox(2);
groupCell.setAlignment(Pos.CENTER);
addLessonInfoToCell(groupCell, lesson);
if (lesson.getGroup() != null) {
Label groupLbl = new Label(lesson.getGroup());
groupLbl.getStyleClass().add("timetable-group-label");
groupCell.getChildren().add(0, groupLbl);
}
groupContainer.getChildren().add(groupCell);
if (i < spot.getSize() - 1) {
Separator sep = new Separator(javafx.geometry.Orientation.HORIZONTAL);
groupContainer.getChildren().add(sep);
}
}
cell.getChildren().add(groupContainer);
}
cell.setOnMouseClicked(e -> showLessonDetails(spot));
return cell;
}
private void addLessonInfoToCell(VBox cell, Lesson lesson) {
Label subjectLbl = new Label(lesson.getSubjectName().getShort() != null ? lesson.getSubjectName().getShort() : lesson.getSubjectName().getFull());
subjectLbl.getStyleClass().add("timetable-subject");
HBox bottomInfo = new HBox(5);
bottomInfo.setAlignment(Pos.CENTER);
if (lesson.getTeacherName() != null) {
Label teacherLbl = new Label(lesson.getTeacherName().getShort() != null ? lesson.getTeacherName().getShort() : lesson.getTeacherName().getFull());
teacherLbl.getStyleClass().add("timetable-teacher");
bottomInfo.getChildren().add(teacherLbl);
}
if (lesson.getClassroom() != null) {
Label roomLbl = new Label(lesson.getClassroom());
roomLbl.getStyleClass().add("timetable-room");
bottomInfo.getChildren().add(roomLbl);
}
cell.getChildren().addAll(subjectLbl, bottomInfo);
}
private void showLessonDetails(LessonSpot spot) {
Dialog<Void> dialog = new Dialog<>();
dialog.setTitle("Detail hodiny");
dialog.setHeaderText(null);
dialog.getDialogPane().getButtonTypes().add(ButtonType.CLOSE);
VBox content = new VBox(15);
content.setPadding(new Insets(20));
for (Lesson lesson : spot) {
VBox lessonInfo = new VBox(5);
lessonInfo.getStyleClass().add("timetable-detail-box");
Label title = new Label(lesson.getSubjectName().getFull());
title.getStyleClass().add("title-3");
lessonInfo.getChildren().add(title);
GridPane grid = new GridPane();
grid.setHgap(10);
grid.setVgap(5);
int row = 0;
if (lesson.getTeacherName() != null) {
grid.add(new Label("Učitel:"), 0, row);
grid.add(new Label(lesson.getTeacherName().getFull()), 1, row++);
}
if (lesson.getClassroom() != null) {
grid.add(new Label("Učebna:"), 0, row);
grid.add(new Label(lesson.getClassroom()), 1, row++);
}
if (lesson.getGroup() != null) {
grid.add(new Label("Skupina:"), 0, row);
grid.add(new Label(lesson.getGroup()), 1, row++);
}
lessonInfo.getChildren().add(grid);
content.getChildren().add(lessonInfo);
}
dialog.getDialogPane().setContent(content);
dialog.show();
}
private String translateDay(DayOfWeek day) {
return switch (day) {
case MONDAY -> "Pondělí";
case TUESDAY -> "Úterý";
case WEDNESDAY -> "Středa";
case THURSDAY -> "Čtvrtek";
case FRIDAY -> "Pátek";
case SATURDAY -> "Sobota";
case SUNDAY -> "Neděle";
};
}
@FXML
protected void onBackToDashboard() {
Router.getInstance().navigate("/dashboard");
}
}
+1 -1
View File
@@ -38,7 +38,7 @@
<!-- Row 0 -->
<Button text="Známky" GridPane.rowIndex="0" GridPane.columnIndex="0" onAction="#onNavigateToGrades" styleClass="card"/>
<Button text="Rozvrh" GridPane.rowIndex="0" GridPane.columnIndex="1" onAction="#onDoNothing" styleClass="card"/>
<Button text="Rozvrh" GridPane.rowIndex="0" GridPane.columnIndex="1" onAction="#onNavigateToTimetable" styleClass="card"/>
<Button text="Učitelé" GridPane.rowIndex="0" GridPane.columnIndex="2" onAction="#onDoNothing" styleClass="card"/>
<!-- Row 1 -->
+2 -2
View File
@@ -7,13 +7,13 @@
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<BorderPane xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="cz.jzitnik.controllers.GradesController"
style="-fx-background-color: linear-gradient(#0f1724, #071023);">
style="-fx-background-color: linear-gradient(#0f1724, #071023);"
stylesheets="@styles/grades.css">
<!-- Header -->
<top>
-71
View File
@@ -51,74 +51,3 @@
-fx-text-fill: transparent;
}
/* Grades Styling */
.subject-card {
-fx-background-color: rgba(30, 40, 56, 0.7);
-fx-border-color: rgba(255, 255, 255, 0.05);
-fx-border-radius: 8px;
-fx-background-radius: 8px;
-fx-padding: 15px 20px;
}
.grade-badge {
-fx-background-radius: 5px;
-fx-min-width: 32px;
-fx-min-height: 32px;
-fx-alignment: center;
-fx-padding: 2px 8px;
}
.grade-small {
-fx-min-height: 20px;
-fx-max-height: 20px;
-fx-min-width: 32px;
-fx-opacity: 0.85;
-fx-padding: 0px 8px;
}
.grade-predicted {
-fx-background-color: transparent !important;
-fx-border-style: dashed;
-fx-border-width: 1px;
-fx-border-radius: 4px;
-fx-opacity: 1.0;
}
.grade-predicted.grade-1 { -fx-border-color: #2ea043; }
.grade-predicted.grade-1 .grade-text { -fx-text-fill: #2ea043; }
.grade-predicted.grade-2 { -fx-border-color: #8957e5; }
.grade-predicted.grade-2 .grade-text { -fx-text-fill: #8957e5; }
.grade-predicted.grade-3 { -fx-border-color: #d29922; }
.grade-predicted.grade-3 .grade-text { -fx-text-fill: #d29922; }
.grade-predicted.grade-4 { -fx-border-color: #f85149; }
.grade-predicted.grade-4 .grade-text { -fx-text-fill: #f85149; }
.grade-predicted.grade-5 { -fx-border-color: #da3633; }
.grade-predicted.grade-5 .grade-text { -fx-text-fill: #da3633; }
.grade-predicted.grade-other { -fx-border-color: #6e7681; }
.grade-predicted.grade-other .grade-text { -fx-text-fill: #6e7681; }
.grade-add-btn {
-fx-background-color: rgba(255, 255, 255, 0.02);
-fx-border-color: rgba(255, 255, 255, 0.2);
-fx-border-style: dashed;
-fx-border-radius: 5px;
-fx-cursor: hand;
}
.grade-add-btn:hover {
-fx-background-color: rgba(255, 255, 255, 0.08);
}
.grade-small .grade-text {
-fx-font-size: 0.85em;
}
.grade-text {
-fx-font-weight: bold;
-fx-text-fill: #ffffff;
}
.grade-1 { -fx-background-color: #2ea043; } /* Primer Green */
.grade-2 { -fx-background-color: #8957e5; } /* Primer Purple */
.grade-3 { -fx-background-color: #d29922; } /* Primer Yellow */
.grade-4 { -fx-background-color: #f85149; } /* Primer Red */
.grade-5 { -fx-background-color: #da3633; -fx-border-color: #ff7b72; -fx-border-width: 1px; -fx-border-radius: 4px; } /* Darker Red */
.grade-other { -fx-background-color: #6e7681; } /* Primer Gray */
+70
View File
@@ -0,0 +1,70 @@
.subject-card {
-fx-background-color: rgba(30, 40, 56, 0.7);
-fx-border-color: rgba(255, 255, 255, 0.05);
-fx-border-radius: 8px;
-fx-background-radius: 8px;
-fx-padding: 15px 20px;
}
.grade-badge {
-fx-background-radius: 5px;
-fx-min-width: 32px;
-fx-min-height: 32px;
-fx-alignment: center;
-fx-padding: 2px 8px;
}
.grade-small {
-fx-min-height: 20px;
-fx-max-height: 20px;
-fx-min-width: 32px;
-fx-opacity: 0.85;
-fx-padding: 0px 8px;
}
.grade-predicted {
-fx-background-color: transparent !important;
-fx-border-style: dashed;
-fx-border-width: 1px;
-fx-border-radius: 4px;
-fx-opacity: 1.0;
}
.grade-predicted.grade-1 { -fx-border-color: #2ea043; }
.grade-predicted.grade-1 .grade-text { -fx-text-fill: #2ea043; }
.grade-predicted.grade-2 { -fx-border-color: #8957e5; }
.grade-predicted.grade-2 .grade-text { -fx-text-fill: #8957e5; }
.grade-predicted.grade-3 { -fx-border-color: #d29922; }
.grade-predicted.grade-3 .grade-text { -fx-text-fill: #d29922; }
.grade-predicted.grade-4 { -fx-border-color: #f85149; }
.grade-predicted.grade-4 .grade-text { -fx-text-fill: #f85149; }
.grade-predicted.grade-5 { -fx-border-color: #da3633; }
.grade-predicted.grade-5 .grade-text { -fx-text-fill: #da3633; }
.grade-predicted.grade-other { -fx-border-color: #6e7681; }
.grade-predicted.grade-other .grade-text { -fx-text-fill: #6e7681; }
.grade-add-btn {
-fx-background-color: rgba(255, 255, 255, 0.02);
-fx-border-color: rgba(255, 255, 255, 0.2);
-fx-border-style: dashed;
-fx-border-radius: 5px;
-fx-cursor: hand;
}
.grade-add-btn:hover {
-fx-background-color: rgba(255, 255, 255, 0.08);
}
.grade-small .grade-text {
-fx-font-size: 0.85em;
}
.grade-text {
-fx-font-weight: bold;
-fx-text-fill: #ffffff;
}
.grade-1 { -fx-background-color: #2ea043; } /* Primer Green */
.grade-2 { -fx-background-color: #8957e5; } /* Primer Purple */
.grade-3 { -fx-background-color: #d29922; } /* Primer Yellow */
.grade-4 { -fx-background-color: #f85149; } /* Primer Red */
.grade-5 { -fx-background-color: #da3633; -fx-border-color: #ff7b72; -fx-border-width: 1px; -fx-border-radius: 4px; } /* Darker Red */
.grade-other { -fx-background-color: #6e7681; } /* Primer Gray */
+97
View File
@@ -0,0 +1,97 @@
.timetable-header {
-fx-padding: 10px;
-fx-background-color: rgba(255, 255, 255, 0.03);
-fx-background-radius: 8px;
-fx-border-color: rgba(255, 255, 255, 0.05);
-fx-border-radius: 8px;
}
.timetable-header-index {
-fx-text-fill: #8b949e;
-fx-font-size: 11px;
-fx-font-weight: bold;
}
.timetable-header-time {
-fx-text-fill: #c9d1d9;
-fx-font-size: 13px;
}
.timetable-day-cell {
-fx-padding: 10px 15px 10px 10px;
-fx-background-color: rgba(255, 255, 255, 0.03);
-fx-background-radius: 8px;
-fx-border-color: rgba(255, 255, 255, 0.05);
-fx-border-radius: 8px;
-fx-alignment: center-right;
-fx-min-height: 80px;
-fx-min-width: USE_PREF_SIZE;
}
.timetable-day-label {
-fx-text-fill: #ffffff;
-fx-font-weight: bold;
-fx-min-width: USE_PREF_SIZE;
}
.timetable-cell {
-fx-background-color: rgba(30, 40, 56, 0.7);
-fx-border-color: rgba(255, 255, 255, 0.05);
-fx-border-radius: 8px;
-fx-background-radius: 8px;
-fx-padding: 10px;
-fx-min-width: 100px;
-fx-min-height: 80px;
-fx-pref-width: -1;
-fx-pref-height: -1;
-fx-alignment: center;
-fx-cursor: hand;
}
.timetable-cell:hover {
-fx-background-color: rgba(45, 55, 71, 0.9);
-fx-border-color: rgba(255, 255, 255, 0.15);
-fx-effect: dropshadow(two-pass-box, rgba(0,0,0,0.3), 8, 0.0, 0, 3);
}
.timetable-cell-empty {
-fx-background-color: transparent;
-fx-border-color: rgba(255, 255, 255, 0.02);
-fx-border-style: dashed;
-fx-border-radius: 8px;
-fx-min-height: 80px;
}
.timetable-subject {
-fx-text-fill: white;
-fx-font-size: 14px;
-fx-font-weight: bold;
}
.timetable-teacher {
-fx-text-fill: #8b949e;
-fx-font-size: 11px;
}
.timetable-room {
-fx-text-fill: #58a6ff;
-fx-font-size: 11px;
-fx-font-weight: bold;
}
.timetable-group-label {
-fx-text-fill: #d29922;
-fx-font-size: 10px;
-fx-font-weight: bold;
-fx-padding: 2px 4px;
-fx-background-color: rgba(210, 153, 34, 0.15);
-fx-background-radius: 4px;
}
.timetable-detail-box {
-fx-background-color: rgba(255, 255, 255, 0.05);
-fx-padding: 15px;
-fx-background-radius: 8px;
-fx-border-color: rgba(255, 255, 255, 0.1);
-fx-border-radius: 8px;
}
+38
View File
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ProgressIndicator?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?>
<BorderPane xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="cz.jzitnik.controllers.TimetableController"
style="-fx-background-color: linear-gradient(#0f1724, #071023);"
stylesheets="@styles/timetable.css">
<top>
<HBox spacing="15" alignment="CENTER_LEFT" style="-fx-background-color: rgba(255, 255, 255, 0.05); -fx-padding: 15 25;">
<Button text="← Zpět" onAction="#onBackToDashboard" styleClass="flat, small" />
<Label text="Rozvrh" styleClass="title-2, text-light" />
</HBox>
</top>
<center>
<StackPane>
<ProgressIndicator fx:id="loadingIndicator" maxWidth="50" maxHeight="50" />
<ScrollPane fx:id="scrollPane" fitToWidth="true" style="-fx-background: transparent; -fx-background-color: transparent;" visible="false">
<padding>
<Insets top="20" right="30" bottom="30" left="30"/>
</padding>
<GridPane fx:id="timetableGrid" hgap="10" vgap="10" alignment="CENTER" />
</ScrollPane>
</StackPane>
</center>
</BorderPane>