feat: Implemented rooms

This commit is contained in:
2026-05-30 14:59:58 +02:00
parent 4448d98ccf
commit f95333b65b
9 changed files with 430 additions and 3 deletions
@@ -0,0 +1,128 @@
package cz.jzitnik.controllers;
import cz.jzitnik.router.Route;
import cz.jzitnik.router.Router;
import cz.jzitnik.query.QueryOptions;
import io.github.tomhula.jecnaapi.data.room.Room;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import java.util.Map;
@Route(path = "/classroom_detail", fxml = "/classroom_detail.fxml")
public class ClassroomDetailController extends DashboardBaseController {
@FXML
private ProgressIndicator loadingIndicator;
@FXML
private ScrollPane scrollPane;
@FXML
private Label headerNameLabel;
@FXML
private GridPane detailsGrid;
@FXML
private GridPane timetableGrid;
private String roomCode;
@Override
public void onNavigate(Map<String, Object> props) {
super.onNavigate(props);
roomCode = (String) props.get("code");
if (roomCode == null || roomCode.isEmpty()) {
onBack();
return;
}
loadRoom();
}
private void loadRoom() {
loadingIndicator.setVisible(true);
scrollPane.setVisible(false);
detailsGrid.getChildren().clear();
appState.getQueryClient().fetch("room:" + roomCode, () -> {
try {
return appState.getClient().getRoom(roomCode).join();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}, QueryOptions.defaultOptions()).thenAccept(result -> {
Platform.runLater(() -> {
loadingIndicator.setVisible(false);
if (result.isSuccess() && result.getData().isPresent()) {
renderRoom(result.getData().get());
scrollPane.setVisible(true);
} else {
headerNameLabel.setText("Chyba");
detailsGrid.add(new Label("Failed to load room details."), 0, 0);
scrollPane.setVisible(true);
}
});
});
}
private void renderRoom(Room room) {
headerNameLabel.setText(room.getName());
int row = 0;
row = addDetailRow("Název", room.getName(), row);
row = addDetailRow("Kód", room.getRoomCode(), row);
row = addDetailRow("Patro", room.getFloor(), row);
row = addDetailRow("Domovská učebna pro", room.getHomeroomOf(), row);
if (room.getManager() != null) {
Hyperlink managerLink = new Hyperlink(room.getManager().getFullName());
managerLink.getStyleClass().add("teacher-link");
managerLink.setOnAction(e -> {
Router.getInstance().navigate("/teacher_profile", Map.of("tag", room.getManager().getTag()));
});
detailsGrid.add(new Label("Správce:"), 0, row);
detailsGrid.add(managerLink, 1, row++);
}
if (timetableGrid != null) {
if (room.getTimetable() != null) {
cz.jzitnik.util.TimetableRenderer.renderTimetable(timetableGrid, room.getTimetable());
} else {
timetableGrid.getChildren().clear();
timetableGrid.getColumnConstraints().clear();
timetableGrid.getRowConstraints().clear();
}
}
}
private int addDetailRow(String labelText, String value, int row) {
if (value == null || value.isBlank()) {
return row;
}
Label lbl = new Label(labelText + ":");
lbl.getStyleClass().add("text-muted");
Label val = new Label(value);
val.getStyleClass().addAll("text-light", "text-bold");
val.setWrapText(true);
detailsGrid.add(lbl, 0, row);
detailsGrid.add(val, 1, row);
return row + 1;
}
@FXML
@Override
protected void onBack() {
Router.getInstance().navigate("/classrooms");
}
}
@@ -0,0 +1,121 @@
package cz.jzitnik.controllers;
import cz.jzitnik.router.Route;
import cz.jzitnik.router.Router;
import cz.jzitnik.query.QueryOptions;
import io.github.tomhula.jecnaapi.data.room.RoomReference;
import io.github.tomhula.jecnaapi.data.room.RoomsPage;
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.List;
import java.util.Map;
import java.util.stream.Collectors;
@Route(path = "/classrooms", fxml = "/classrooms.fxml")
public class ClassroomsController extends DashboardBaseController {
@FXML
private ProgressIndicator loadingIndicator;
@FXML
private ScrollPane scrollPane;
@FXML
private FlowPane classroomsFlow;
@FXML
private TextField searchField;
private List<RoomReference> allRooms = new ArrayList<>();
@Override
public void onNavigate(Map<String, Object> props) {
super.onNavigate(props);
searchField.textProperty().addListener((obs, oldVal, newVal) -> {
filterRooms(newVal);
});
loadRooms();
}
private void loadRooms() {
loadingIndicator.setVisible(true);
scrollPane.setVisible(false);
classroomsFlow.getChildren().clear();
appState.getQueryClient().fetch("rooms:page", () -> {
try {
return appState.getClient().getRoomsPage().join();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}, QueryOptions.defaultOptions()).thenAccept(result -> {
Platform.runLater(() -> {
loadingIndicator.setVisible(false);
if (result.isSuccess() && result.getData().isPresent()) {
RoomsPage page = result.getData().get();
allRooms = new ArrayList<>(page.getRoomReferences());
// Sort by name
allRooms.sort((r1, r2) -> r1.getName().compareToIgnoreCase(r2.getName()));
filterRooms(searchField.getText());
scrollPane.setVisible(true);
} else {
Label errorLabel = new Label("Failed to load rooms.");
errorLabel.getStyleClass().addAll("text-danger", "title-3");
classroomsFlow.getChildren().add(errorLabel);
scrollPane.setVisible(true);
}
});
});
}
private void filterRooms(String query) {
classroomsFlow.getChildren().clear();
String lowerQuery = query == null ? "" : query.toLowerCase();
List<RoomReference> filtered = allRooms.stream()
.filter(r -> r.getName().toLowerCase().contains(lowerQuery) || r.getRoomCode().toLowerCase().contains(lowerQuery))
.toList();
if (filtered.isEmpty()) {
Label empty = new Label("Nenalezena žádná učebna.");
empty.getStyleClass().addAll("text-muted", "title-3");
classroomsFlow.getChildren().add(empty);
return;
}
for (RoomReference room : filtered) {
VBox card = new VBox(5);
card.setAlignment(Pos.CENTER);
card.getStyleClass().addAll("card", "classroom-card");
Label nameLbl = new Label(room.getName());
nameLbl.getStyleClass().addAll("title-4", "text-light");
Label codeLbl = new Label(room.getRoomCode());
codeLbl.getStyleClass().addAll("text-muted");
card.getChildren().addAll(nameLbl, codeLbl);
card.setOnMouseClicked(e -> {
Router.getInstance().navigate("/classroom_detail", Map.of("code", room.getRoomCode()));
});
classroomsFlow.getChildren().add(card);
}
}
@FXML
protected void onBackToDashboard() {
Router.getInstance().navigate("/dashboard");
}
}
@@ -33,7 +33,7 @@ public class DashboardBaseController implements Routable {
protected void onNavigateToTeachers() { Router.getInstance().navigate("/teachers"); } protected void onNavigateToTeachers() { Router.getInstance().navigate("/teachers"); }
@FXML @FXML
protected void onNavigateToRooms() { Router.getInstance().navigate("/rooms"); } protected void onNavigateToClassrooms() { Router.getInstance().navigate("/classrooms"); }
@FXML @FXML
protected void onNavigateToAbsences() { Router.getInstance().navigate("/absences"); } protected void onNavigateToAbsences() { Router.getInstance().navigate("/absences"); }
@@ -187,8 +187,14 @@ public class TimetableRenderer {
grid.add(teacherLbl, 1, row++); grid.add(teacherLbl, 1, row++);
} }
if (lesson.getClassroom() != null) { if (lesson.getClassroom() != null) {
Hyperlink roomLink = new Hyperlink(lesson.getClassroom());
roomLink.getStyleClass().addAll("teacher-link");
roomLink.setOnAction(e -> {
dialog.close();
Router.getInstance().navigate("/classroom_detail", Map.of("code", lesson.getClassroom()));
});
grid.add(new Label("Učebna:"), 0, row); grid.add(new Label("Učebna:"), 0, row);
grid.add(new Label(lesson.getClassroom()), 1, row++); grid.add(roomLink, 1, row++);
} }
if (lesson.getGroup() != null) { if (lesson.getGroup() != null) {
grid.add(new Label("Skupina:"), 0, row); grid.add(new Label("Skupina:"), 0, row);
+59
View File
@@ -0,0 +1,59 @@
<?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.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.HBox?>
<?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.ClassroomDetailController"
style="-fx-background-color: linear-gradient(#0f1724, #071023);"
stylesheets="@styles/classrooms.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="#onBack" styleClass="flat, small" />
<Label fx:id="headerNameLabel" text="Učebna" 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">
<StackPane alignment="TOP_CENTER">
<padding>
<Insets top="30" right="30" bottom="30" left="30"/>
</padding>
<VBox spacing="25" alignment="TOP_CENTER" maxWidth="1000">
<GridPane fx:id="detailsGrid" hgap="20" vgap="15" styleClass="classroom-details-grid" maxWidth="800">
<columnConstraints>
<javafx.scene.layout.ColumnConstraints minWidth="150" halignment="RIGHT" />
<javafx.scene.layout.ColumnConstraints hgrow="ALWAYS" />
</columnConstraints>
</GridPane>
<VBox spacing="10" alignment="TOP_CENTER" styleClass="classroom-timetable-container">
<Label text="Rozvrh učebny" styleClass="title-2, text-light" />
<ScrollPane vbarPolicy="NEVER" hbarPolicy="AS_NEEDED" fitToHeight="true" fitToWidth="true" style="-fx-background: transparent; -fx-background-color: transparent;">
<GridPane fx:id="timetableGrid" hgap="10" vgap="10" alignment="CENTER" />
</ScrollPane>
</VBox>
</VBox>
</StackPane>
</ScrollPane>
</StackPane>
</center>
</BorderPane>
+44
View File
@@ -0,0 +1,44 @@
<?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.control.TextField?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.FlowPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.StackPane?>
<BorderPane xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="cz.jzitnik.controllers.ClassroomsController"
style="-fx-background-color: linear-gradient(#0f1724, #071023);"
stylesheets="@styles/classrooms.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="Učebny" styleClass="title-2, text-light" />
<Region HBox.hgrow="ALWAYS" />
<TextField fx:id="searchField" promptText="Hledat učebnu..." styleClass="search-field" />
</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">
<StackPane alignment="TOP_CENTER">
<padding>
<Insets top="20" right="30" bottom="30" left="30"/>
</padding>
<FlowPane fx:id="classroomsFlow" hgap="15" vgap="15" alignment="CENTER" maxWidth="900" />
</StackPane>
</ScrollPane>
</StackPane>
</center>
</BorderPane>
+1 -1
View File
@@ -42,7 +42,7 @@
<Button text="Učitelé" GridPane.rowIndex="0" GridPane.columnIndex="2" onAction="#onNavigateToTeachers" styleClass="card"/> <Button text="Učitelé" GridPane.rowIndex="0" GridPane.columnIndex="2" onAction="#onNavigateToTeachers" styleClass="card"/>
<!-- Row 1 --> <!-- Row 1 -->
<Button text="Učebny" GridPane.rowIndex="1" GridPane.columnIndex="0" onAction="#onDoNothing" styleClass="card"/> <Button text="Učebny" GridPane.rowIndex="1" GridPane.columnIndex="0" onAction="#onNavigateToClassrooms" styleClass="card"/>
<Button text="Absence" GridPane.rowIndex="1" GridPane.columnIndex="1" onAction="#onNavigateToAbsences" styleClass="card"/> <Button text="Absence" GridPane.rowIndex="1" GridPane.columnIndex="1" onAction="#onNavigateToAbsences" styleClass="card"/>
<Button text="Mimořádný rozvrh" GridPane.rowIndex="1" GridPane.columnIndex="2" onAction="#onDoNothing" styleClass="card"/> <Button text="Mimořádný rozvrh" GridPane.rowIndex="1" GridPane.columnIndex="2" onAction="#onDoNothing" styleClass="card"/>
+44
View File
@@ -0,0 +1,44 @@
<?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.control.TextField?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.FlowPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.Region?>
<?import javafx.scene.layout.StackPane?>
<BorderPane xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="cz.jzitnik.controllers.RoomsController"
style="-fx-background-color: linear-gradient(#0f1724, #071023);"
stylesheets="@styles/rooms.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="Učebny" styleClass="title-2, text-light" />
<Region HBox.hgrow="ALWAYS" />
<TextField fx:id="searchField" promptText="Hledat učebnu..." styleClass="search-field" />
</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">
<StackPane alignment="TOP_CENTER">
<padding>
<Insets top="20" right="30" bottom="30" left="30"/>
</padding>
<FlowPane fx:id="roomsFlow" hgap="15" vgap="15" alignment="CENTER" maxWidth="900" />
</StackPane>
</ScrollPane>
</StackPane>
</center>
</BorderPane>
+25
View File
@@ -0,0 +1,25 @@
.classroom-card {
-fx-min-width: 220px;
-fx-min-height: 100px;
-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;
-fx-cursor: hand;
-fx-alignment: center;
}
.classroom-card: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);
}
.classroom-details-grid {
-fx-background-color: rgba(255, 255, 255, 0.03);
-fx-padding: 25px;
-fx-background-radius: 12px;
-fx-border-color: rgba(255, 255, 255, 0.05);
-fx-border-radius: 12px;
}