feat: Added teachers

This commit is contained in:
2026-05-30 14:40:45 +02:00
parent 991c023ed9
commit 4448d98ccf
13 changed files with 767 additions and 194 deletions
+11
View File
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="com.mituuz.fuzzier.FuzzierSettings">
<option name="modules">
<map>
<entry key="jecnaclient" value="$PROJECT_DIR$" />
</map>
</option>
<option name="recentlySearchedFiles" value="rO0ABXNyABxqYXZheC5zd2luZy5EZWZhdWx0TGlzdE1vZGVsBgfGCGLvV2ICAAFMAAhkZWxlZ2F0ZXQAEkxqYXZhL3V0aWwvVmVjdG9yO3hyAB1qYXZheC5zd2luZy5BYnN0cmFjdExpc3RNb2RlbFrW+oYSs63tAgABTAAMbGlzdGVuZXJMaXN0dAAlTGphdmF4L3N3aW5nL2V2ZW50L0V2ZW50TGlzdGVuZXJMaXN0O3hwc3IAI2phdmF4LnN3aW5nLmV2ZW50LkV2ZW50TGlzdGVuZXJMaXN0kUjMLXPfDt4DAAB4cHB4c3IAEGphdmEudXRpbC5WZWN0b3LZl31bgDuvAQMAA0kAEWNhcGFjaXR5SW5jcmVtZW50SQAMZWxlbWVudENvdW50WwALZWxlbWVudERhdGF0ABNbTGphdmEvbGFuZy9PYmplY3Q7eHAAAAAAAAAAAXVyABNbTGphdmEubGFuZy5PYmplY3Q7kM5YnxBzKWwCAAB4cAAAAApzcgBIY29tLm1pdHV1ei5mdXp6aWVyLmVudGl0aWVzLkZ1enp5TWF0Y2hDb250YWluZXIkU2VyaWFsaXplZE1hdGNoQ29udGFpbmVy2S8sP5uny74CAARMAAhmaWxlUGF0aHQAEkxqYXZhL2xhbmcvU3RyaW5nO0wACGZpbGVuYW1lcQB+AA1MAA5tb2R1bGVCYXNlUGF0aHEAfgANTAAFc2NvcmV0ADxMY29tL21pdHV1ei9mdXp6aWVyL2VudGl0aWVzL0Z1enp5TWF0Y2hDb250YWluZXIkRnV6enlTY29yZTt4cHQAQy9zcmMvbWFpbi9qYXZhL2N6L2p6aXRuaWsvY29udHJvbGxlcnMvVGVhY2hlclByb2ZpbGVDb250cm9sbGVyLmphdmF0AB1UZWFjaGVyUHJvZmlsZUNvbnRyb2xsZXIuamF2YXQAHS9ob21lL2t1YmEvQ29kaW5nL2plY25hY2xpZW50c3IAOmNvbS5taXR1dXouZnV6emllci5lbnRpdGllcy5GdXp6eU1hdGNoQ29udGFpbmVyJEZ1enp5U2NvcmUvP9P07ROG2QIABUkADWZpbGVuYW1lU2NvcmVJAA9tdWx0aU1hdGNoU2NvcmVJABBwYXJ0aWFsUGF0aFNjb3JlSQALc3RyZWFrU2NvcmVMABNoaWdobGlnaHRDaGFyYWN0ZXJzdAAPTGphdmEvdXRpbC9TZXQ7eHAAAAAVAAAAAAAAAAAAAAATc3IAEWphdmEudXRpbC5IYXNoU2V0ukSFlZa4tzQDAAB4cHcMAAAAID9AAAAAAAAVc3IAEWphdmEubGFuZy5JbnRlZ2VyEuKgpPeBhzgCAAFJAAV2YWx1ZXhyABBqYXZhLmxhbmcuTnVtYmVyhqyVHQuU4IsCAAB4cAAAAABzcQB+ABgAAAABc3EAfgAYAAAAAnNxAH4AGAAAAANzcQB+ABgAAAAEc3EAfgAYAAAABXNxAH4AGAAAAAZzcQB+ABgAAAAHc3EAfgAYAAAACHNxAH4AGAAAAAlzcQB+ABgAAAAKc3EAfgAYAAAAC3NxAH4AGAAAAAxzcQB+ABgAAAANc3EAfgAYAAAADnNxAH4AGAAAAA9zcQB+ABgAAAAQc3EAfgAYAAAAEXNxAH4AGAAAABJzcQB+ABgAAAATc3EAfgAYAAAAFHhwcHBwcHBwcHB4" />
</component>
</project>
@@ -0,0 +1,152 @@
package cz.jzitnik.controllers;
import cz.jzitnik.router.Route;
import cz.jzitnik.router.Router;
import cz.jzitnik.query.QueryOptions;
import cz.jzitnik.util.ImageFetcher;
import io.github.tomhula.jecnaapi.data.schoolStaff.Teacher;
import javafx.application.Platform;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.*;
import java.util.Map;
@Route(path = "/teacher_profile", fxml = "/teacher_profile.fxml")
public class TeacherProfileController extends DashboardBaseController {
@FXML
private ProgressIndicator loadingIndicator;
@FXML
private ScrollPane scrollPane;
@FXML
private Label headerNameLabel;
@FXML
private Label fullNameLabel;
@FXML
private Label tagLabel;
@FXML
private GridPane detailsGrid;
@FXML
private GridPane timetableGrid;
@FXML
private ImageView avatarView;
private String teacherTag;
@Override
public void onNavigate(Map<String, Object> props) {
super.onNavigate(props);
teacherTag = (String) props.get("tag");
if (teacherTag == null || teacherTag.isEmpty()) {
onBack();
return;
}
loadTeacher();
}
private void loadTeacher() {
loadingIndicator.setVisible(true);
scrollPane.setVisible(false);
detailsGrid.getChildren().clear();
appState.getQueryClient().fetch("teacher:" + teacherTag, () -> {
try {
return appState.getClient().getTeacher(teacherTag).join();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}, QueryOptions.defaultOptions()).thenAccept(result -> {
Platform.runLater(() -> {
loadingIndicator.setVisible(false);
if (result.isSuccess() && result.getData().isPresent()) {
renderTeacher(result.getData().get());
scrollPane.setVisible(true);
} else {
headerNameLabel.setText("Chyba");
detailsGrid.add(new Label("Failed to load teacher details."), 0, 0);
scrollPane.setVisible(true);
}
});
});
}
private void renderTeacher(Teacher teacher) {
headerNameLabel.setText(teacher.getFullName());
fullNameLabel.setText(teacher.getFullName());
tagLabel.setText(teacher.getTag());
if (teacher.getProfilePicturePath() != null && !teacher.getProfilePicturePath().isEmpty()) {
new Thread(() -> {
Image image = ImageFetcher.fetchImage(teacher.getProfilePicturePath());
Platform.runLater(() -> {
avatarView.setImage(image);
});
}).start();
} else {
avatarView.setImage(null);
}
int row = 0;
row = addDetailRow("Kabinet", teacher.getCabinet(), row);
row = addDetailRow("Školní email", teacher.getSchoolMail(), row);
row = addDetailRow("Soukromý email", teacher.getPrivateMail(), row);
if (!teacher.getPhoneNumbers().isEmpty()) {
row = addDetailRow("Telefon", String.join(", ", teacher.getPhoneNumbers()), row);
}
row = addDetailRow("Pevná linka", teacher.getLandline(), row);
row = addDetailRow("Soukromý telefon", teacher.getPrivatePhoneNumber(), row);
row = addDetailRow("Třídní", teacher.getTutorOfClass(), row);
row = addDetailRow("Konzultační hodiny", teacher.getConsultationHours(), row);
if (timetableGrid != null) {
if (teacher.getTimetable() != null) {
cz.jzitnik.util.TimetableRenderer.renderTimetable(timetableGrid, teacher.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("/teachers");
}
}
@@ -0,0 +1,119 @@
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.schoolStaff.TeacherReference;
import io.github.tomhula.jecnaapi.data.schoolStaff.TeachersPage;
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;
@Route(path = "/teachers", fxml = "/teachers.fxml")
public class TeachersController extends DashboardBaseController {
@FXML
private ProgressIndicator loadingIndicator;
@FXML
private ScrollPane scrollPane;
@FXML
private FlowPane teachersFlow;
@FXML
private TextField searchField;
private List<TeacherReference> allTeachers = new ArrayList<>();
@Override
public void onNavigate(Map<String, Object> props) {
super.onNavigate(props);
searchField.textProperty().addListener((obs, oldVal, newVal) -> {
filterTeachers(newVal);
});
loadTeachers();
}
private void loadTeachers() {
loadingIndicator.setVisible(true);
scrollPane.setVisible(false);
teachersFlow.getChildren().clear();
appState.getQueryClient().fetch("teachers:page", () -> {
try {
return appState.getClient().getTeachersPage().join();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}, QueryOptions.defaultOptions()).thenAccept(result -> {
Platform.runLater(() -> {
loadingIndicator.setVisible(false);
if (result.isSuccess() && result.getData().isPresent()) {
TeachersPage page = result.getData().get();
allTeachers = new ArrayList<>(page.getTeachersReferences());
allTeachers.sort((t1, t2) -> t1.getFullName().compareToIgnoreCase(t2.getFullName()));
filterTeachers(searchField.getText());
scrollPane.setVisible(true);
} else {
Label errorLabel = new Label("Failed to load teachers.");
errorLabel.getStyleClass().addAll("text-danger", "title-3");
teachersFlow.getChildren().add(errorLabel);
scrollPane.setVisible(true);
}
});
});
}
private void filterTeachers(String query) {
teachersFlow.getChildren().clear();
String lowerQuery = query == null ? "" : query.toLowerCase();
List<TeacherReference> filtered = allTeachers.stream()
.filter(t -> t.getFullName().toLowerCase().contains(lowerQuery) || t.getTag().toLowerCase().contains(lowerQuery))
.toList();
if (filtered.isEmpty()) {
Label empty = new Label("Nenalezen žádný učitel.");
empty.getStyleClass().addAll("text-muted", "title-3");
teachersFlow.getChildren().add(empty);
return;
}
for (TeacherReference teacher : filtered) {
VBox card = new VBox(5);
card.setAlignment(Pos.CENTER_LEFT);
card.getStyleClass().addAll("card", "teacher-card");
Label nameLbl = new Label(teacher.getFullName());
nameLbl.getStyleClass().addAll("title-4", "text-light");
Label tagLbl = new Label(teacher.getTag());
tagLbl.getStyleClass().addAll("teacher-tag");
card.getChildren().addAll(nameLbl, tagLbl);
card.setOnMouseClicked(e -> {
Router.getInstance().navigate("/teacher_profile", Map.of("tag", teacher.getTag()));
});
teachersFlow.getChildren().add(card);
}
}
@FXML
protected void onBackToDashboard() {
Router.getInstance().navigate("/dashboard");
}
}
@@ -4,6 +4,7 @@ import cz.jzitnik.router.Route;
import cz.jzitnik.router.Router; import cz.jzitnik.router.Router;
import cz.jzitnik.query.QueryOptions; import cz.jzitnik.query.QueryOptions;
import cz.jzitnik.query.QueryResult; import cz.jzitnik.query.QueryResult;
import cz.jzitnik.util.TimetableRenderer;
import io.github.tomhula.jecnaapi.data.timetable.Lesson; import io.github.tomhula.jecnaapi.data.timetable.Lesson;
import io.github.tomhula.jecnaapi.data.timetable.LessonPeriod; import io.github.tomhula.jecnaapi.data.timetable.LessonPeriod;
import io.github.tomhula.jecnaapi.data.timetable.LessonSpot; import io.github.tomhula.jecnaapi.data.timetable.LessonSpot;
@@ -80,183 +81,7 @@ public class TimetableController extends DashboardBaseController {
timetableGrid.setAlignment(Pos.CENTER); timetableGrid.setAlignment(Pos.CENTER);
Timetable timetable = currentPage.getTimetable(); Timetable timetable = currentPage.getTimetable();
List<LessonPeriod> periods = timetable.getLessonPeriods(); TimetableRenderer.renderTimetable(timetableGrid, timetable);
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 @FXML
@@ -0,0 +1,58 @@
package cz.jzitnik.util;
import javafx.scene.image.Image;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.zip.GZIPInputStream;
import java.util.zip.InflaterInputStream;
public class ImageFetcher {
public static Image fetchImage(String imagePath) {
if (imagePath == null || imagePath.isEmpty()) {
return null;
}
try {
if (imagePath.startsWith("/")) {
imagePath = "https://www.spsejecna.cz" + imagePath;
}
URL url = new URL(imagePath);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36");
conn.setRequestProperty("Accept", "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8");
conn.setRequestProperty("Accept-Language", "cs,en;q=0.9,en-US;q=0.8");
conn.setRequestProperty("Accept-Encoding", "gzip, deflate");
conn.setRequestProperty("Referer", "https://www.spsejecna.cz/");
conn.setRequestProperty("Connection", "keep-alive");
conn.setConnectTimeout(5000);
conn.setReadTimeout(5000);
int responseCode = conn.getResponseCode();
if (responseCode >= 200 && responseCode < 300) {
InputStream in = conn.getInputStream();
String encoding = conn.getContentEncoding();
if ("gzip".equalsIgnoreCase(encoding)) {
in = new GZIPInputStream(in);
} else if ("deflate".equalsIgnoreCase(encoding)) {
in = new InflaterInputStream(in);
}
Image img = new Image(in);
in.close();
return img;
} else {
System.err.println("Failed to fetch image: HTTP " + responseCode + " for " + imagePath);
}
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
@@ -0,0 +1,217 @@
package cz.jzitnik.util;
import cz.jzitnik.router.Router;
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 javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.ButtonType;
import javafx.scene.control.Dialog;
import javafx.scene.control.Hyperlink;
import javafx.scene.control.Label;
import javafx.scene.control.Separator;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import kotlinx.datetime.DayOfWeek;
import java.util.List;
import java.util.Map;
public class TimetableRenderer {
public static void renderTimetable(GridPane timetableGrid, Timetable timetable) {
timetableGrid.getChildren().clear();
timetableGrid.getColumnConstraints().clear();
timetableGrid.getRowConstraints().clear();
timetableGrid.setAlignment(Pos.CENTER);
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 static 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 static 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 static 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) {
Hyperlink teacherLbl = new Hyperlink(lesson.getTeacherName().getFull());
teacherLbl.getStyleClass().addAll("timetable-teacher", "teacher-link");
teacherLbl.setOnAction(e -> {
dialog.close();
String tag = lesson.getTeacherName().getShort() != null ? lesson.getTeacherName().getShort() : lesson.getTeacherName().getFull();
Router.getInstance().navigate("/teacher_profile", Map.of("tag", tag));
});
grid.add(new Label("Učitel:"), 0, row);
grid.add(teacherLbl, 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 static 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";
};
}
}
+9 -9
View File
@@ -31,18 +31,18 @@
<ProgressIndicator fx:id="loadingIndicator" maxWidth="50" maxHeight="50" /> <ProgressIndicator fx:id="loadingIndicator" maxWidth="50" maxHeight="50" />
<ScrollPane fx:id="scrollPane" fitToWidth="true" style="-fx-background: transparent; -fx-background-color: transparent;" visible="false"> <ScrollPane fx:id="scrollPane" fitToWidth="true" style="-fx-background: transparent; -fx-background-color: transparent;" visible="false">
<padding> <StackPane alignment="TOP_CENTER">
<Insets top="20" right="30" bottom="30" left="30"/> <padding>
</padding> <Insets top="20" right="30" bottom="30" left="30"/>
<VBox spacing="25" alignment="TOP_CENTER" maxWidth="900" StackPane.alignment="TOP_CENTER"> </padding>
<VBox spacing="25" alignment="TOP_CENTER" maxWidth="900">
<!-- Summary Box --> <HBox fx:id="summaryBox" spacing="20" alignment="CENTER" />
<HBox fx:id="summaryBox" spacing="20" alignment="CENTER" />
<!-- Detail Grid --> <FlowPane fx:id="detailFlow" hgap="15" vgap="15" alignment="CENTER" />
<FlowPane fx:id="detailFlow" hgap="15" vgap="15" alignment="CENTER" />
</VBox> </VBox>
</StackPane>
</ScrollPane> </ScrollPane>
</StackPane> </StackPane>
</center> </center>
+1 -1
View File
@@ -39,7 +39,7 @@
<!-- Row 0 --> <!-- Row 0 -->
<Button text="Známky" GridPane.rowIndex="0" GridPane.columnIndex="0" onAction="#onNavigateToGrades" styleClass="card"/> <Button text="Známky" GridPane.rowIndex="0" GridPane.columnIndex="0" onAction="#onNavigateToGrades" styleClass="card"/>
<Button text="Rozvrh" GridPane.rowIndex="0" GridPane.columnIndex="1" onAction="#onNavigateToTimetable" 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"/> <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="#onDoNothing" styleClass="card"/>
+6 -4
View File
@@ -29,10 +29,12 @@
<ProgressIndicator fx:id="loadingIndicator" maxWidth="50" maxHeight="50" /> <ProgressIndicator fx:id="loadingIndicator" maxWidth="50" maxHeight="50" />
<ScrollPane fx:id="scrollPane" fitToWidth="true" style="-fx-background: transparent; -fx-background-color: transparent;" visible="false"> <ScrollPane fx:id="scrollPane" fitToWidth="true" style="-fx-background: transparent; -fx-background-color: transparent;" visible="false">
<padding> <StackPane alignment="TOP_CENTER">
<Insets top="20" right="30" bottom="30" left="30"/> <padding>
</padding> <Insets top="20" right="30" bottom="30" left="30"/>
<VBox fx:id="contentBox" spacing="15" alignment="TOP_CENTER" maxWidth="900" /> </padding>
<VBox fx:id="contentBox" spacing="15" alignment="TOP_CENTER" maxWidth="900" />
</StackPane>
</ScrollPane> </ScrollPane>
</StackPane> </StackPane>
</center> </center>
+60
View File
@@ -0,0 +1,60 @@
.search-field {
-fx-min-width: 250px;
-fx-background-color: rgba(0, 0, 0, 0.2);
-fx-border-color: rgba(255, 255, 255, 0.1);
-fx-border-radius: 4px;
-fx-background-radius: 4px;
-fx-text-fill: white;
}
.search-field:focused {
-fx-border-color: #58a6ff;
}
.teacher-card {
-fx-min-width: 220px;
-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;
}
.teacher-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);
}
.teacher-tag {
-fx-text-fill: #58a6ff;
-fx-font-size: 12px;
-fx-font-weight: bold;
-fx-padding: 2px 6px;
-fx-background-color: rgba(88, 166, 255, 0.1);
-fx-background-radius: 4px;
}
.teacher-tag.large {
-fx-font-size: 16px;
-fx-padding: 4px 10px;
}
.teacher-profile-header {
-fx-background-color: rgba(255, 255, 255, 0.03);
-fx-padding: 20px;
-fx-background-radius: 12px;
-fx-border-color: rgba(255, 255, 255, 0.05);
-fx-border-radius: 12px;
}
.teacher-details-grid {
-fx-background-color: rgba(0, 0, 0, 0.15);
-fx-padding: 25px;
-fx-background-radius: 12px;
}
.text-bold {
-fx-font-weight: bold;
}
+9
View File
@@ -95,3 +95,12 @@
-fx-border-color: rgba(255, 255, 255, 0.1); -fx-border-color: rgba(255, 255, 255, 0.1);
-fx-border-radius: 8px; -fx-border-radius: 8px;
} }
.teacher-link {
-fx-border-color: transparent;
-fx-padding: 0;
-fx-text-fill: #58a6ff;
}
.teacher-link:hover {
-fx-underline: true;
}
+75
View File
@@ -0,0 +1,75 @@
<?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.ColumnConstraints?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.shape.Rectangle?>
<?import javafx.scene.image.ImageView?>
<BorderPane xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="cz.jzitnik.controllers.TeacherProfileController"
style="-fx-background-color: linear-gradient(#0f1724, #071023);"
stylesheets="@styles/teachers.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="Profil" 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">
<HBox spacing="20" alignment="CENTER_LEFT" styleClass="teacher-profile-header" maxWidth="800">
<StackPane>
<ImageView fx:id="avatarView" fitWidth="150" fitHeight="193" preserveRatio="false">
<clip>
<Rectangle width="150" height="193" arcWidth="20" arcHeight="20" />
</clip>
</ImageView>
</StackPane>
<VBox spacing="5" alignment="CENTER_LEFT">
<Label fx:id="fullNameLabel" styleClass="title-1, text-light" />
<Label fx:id="tagLabel" styleClass="teacher-tag, large" />
</VBox>
</HBox>
<GridPane fx:id="detailsGrid" hgap="20" vgap="15" styleClass="teacher-details-grid" maxWidth="800">
<columnConstraints>
<ColumnConstraints minWidth="150" halignment="RIGHT" />
<ColumnConstraints hgrow="ALWAYS" />
</columnConstraints>
</GridPane>
<VBox spacing="10" alignment="TOP_CENTER" styleClass="teacher-timetable-container">
<Label text="Rozvrh učitele" styleClass="title-2, text-light" />
<ScrollPane vbarPolicy="NEVER" 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>
+45
View File
@@ -0,0 +1,45 @@
<?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?>
<?import javafx.scene.layout.VBox?>
<BorderPane xmlns="http://javafx.com/javafx/21" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="cz.jzitnik.controllers.TeachersController"
style="-fx-background-color: linear-gradient(#0f1724, #071023);"
stylesheets="@styles/teachers.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čitelé" styleClass="title-2, text-light" />
<Region HBox.hgrow="ALWAYS" />
<TextField fx:id="searchField" promptText="Hledat učitele..." 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">
<padding>
<Insets top="20" right="30" bottom="30" left="30"/>
</padding>
<VBox alignment="TOP_CENTER">
<FlowPane fx:id="teachersFlow" hgap="15" vgap="15" alignment="CENTER" maxWidth="900" />
</VBox>
</ScrollPane>
</StackPane>
</center>
</BorderPane>