nearly done, still needs a bit of work

This commit is contained in:
Ksan 2026-04-09 15:38:39 +02:00
parent a659e1217c
commit be2d918620
9 changed files with 1111 additions and 51 deletions

View File

@ -165,6 +165,7 @@ public class UserRepository {
} }
private static <T extends User> List<T> findAll( private static <T extends User> List<T> findAll(
Path dir, Class<T> type, String lockPrefix) throws Exception { Path dir, Class<T> type, String lockPrefix) throws Exception {

View File

@ -10,7 +10,6 @@ import javafx.application.Platform;
import javafx.concurrent.Task; import javafx.concurrent.Task;
import javafx.fxml.FXML; import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader; import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene; import javafx.scene.Scene;
import javafx.scene.control.*; import javafx.scene.control.*;
import javafx.scene.input.Dragboard; import javafx.scene.input.Dragboard;
@ -261,27 +260,42 @@ public class LoginController {
private void navigateToUserUI(User user) { private void navigateToUserUI(User user) {
try { try {
if (user instanceof Organizer organizer) {
if (user instanceof Organizer) {
FXMLLoader loader = new FXMLLoader( FXMLLoader loader = new FXMLLoader(
getClass().getResource("/fxml/OrganizerView.fxml")); getClass().getResource("/fxml/OrganizerView.fxml"));
Parent root = loader.load(); Scene oldScene = stage.getScene();
Scene scene = new Scene(loader.load(),
oldScene.getWidth(),
oldScene.getHeight()
);
OrganizerController controller = loader.getController(); OrganizerController controller = loader.getController();
controller.setOrganizer(organizer); controller.setOrganizer((Organizer) user);
controller.setStage(stage); controller.setStage(stage);
stage.setScene(new Scene(root));
} else if (user instanceof Voter voter) { stage.setScene(scene);
stage.setTitle(((Organizer) user).getName());
} else if (user instanceof Voter) {
FXMLLoader loader = new FXMLLoader( FXMLLoader loader = new FXMLLoader(
getClass().getResource("/fxml/VoterView.fxml")); getClass().getResource("/fxml/VoterView.fxml"));
Parent root = loader.load(); Scene oldScene = stage.getScene();
Scene scene = new Scene(loader.load(),
oldScene.getWidth(),
oldScene.getHeight()
);
VoterController controller = loader.getController(); VoterController controller = loader.getController();
controller.setVoter(voter); controller.setVoter((Voter) user);
controller.setStage(stage); controller.setStage(stage);
stage.setScene(new Scene(root));
stage.setScene(scene);
stage.setTitle(((Voter) user).getName());
} }
} catch (Exception e) { } catch (Exception e) {
showError("Failed to load UI: " + e.getMessage()); showError("Failed to load UI: " + e.getMessage());
System.out.println(e);
loginButton.setDisable(false); loginButton.setDisable(false);
} }
} }

View File

@ -1,18 +1,432 @@
package dev.ksan.ui; package dev.ksan.ui;
import dev.ksan.CASystem.KeystoreService;
import dev.ksan.Users.Organizer; import dev.ksan.Users.Organizer;
import dev.ksan.voteSystem.*;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.stage.Stage; import javafx.stage.Stage;
import java.security.PrivateKey;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class OrganizerController { public class OrganizerController {
private Organizer organizer; private Organizer organizer;
@FXML
private Label usernameLabel;
@FXML private Label errorLabel;
@FXML private VBox panesBox;
@FXML
private TitledPane activeList;
@FXML
private TitledPane inactiveList;
@FXML
private ListView<Poll> activePollsListView;
@FXML
private ListView<Poll> inactivePollsListView;
@FXML
private Button newButton;
@FXML
private StackPane centerPane;
@FXML
private TextField descriptionField;
@FXML
private TextField titleField;
@FXML
private DatePicker startDatePicker;
@FXML
private DatePicker endDatePicker;
@FXML
private Button confirmButton;
@FXML
private Button newOptionButton;
@FXML
private TextField startTimeField;
@FXML
private TextField endTimeField;
@FXML
private VBox optionsContainer;
private Stage stage; private Stage stage;
private final List<TextField> optionFields = new ArrayList<>();
public void setOrganizer(Organizer organizer) { public void setOrganizer(Organizer organizer) {
this.organizer = organizer; this.organizer = organizer;
usernameLabel.setText(organizer.getName());
loadPolls();
} }
public void setStage(Stage stage) { public void setStage(Stage stage) {
this.stage = stage; this.stage = stage;
} }
public void initialize() {
errorLabel.setVisible(false);
confirmButton.setOnAction(e -> createNewPoll());
newOptionButton.setOnAction(e -> addOptionField());
newButton.setOnAction(e -> showCreatePollForm());
}
private void addOptionField() {
if (optionFields.size() >= 5) return; // max 5 options
HBox optionBox = new HBox(10);
Text label = new Text("option " + (optionFields.size() + 1) + ":");
TextField textField = new TextField();
optionBox.getChildren().addAll(label, textField);
optionsContainer.getChildren().add(optionBox);
optionFields.add(textField);
}
private void createNewPoll() {
clearError();
if (titleField.getText() == null || titleField.getText().isBlank()) {
showError("Title is required."); return;
}
if (descriptionField.getText() == null || descriptionField.getText().isBlank()) {
showError("Description is required."); return;
}
if (startDatePicker.getValue() == null) {
showError("Start date is required."); return;
}
if (endDatePicker.getValue() == null) {
showError("End date is required."); return;
}
if (startTimeField.getText() == null || startTimeField.getText().isBlank()) {
showError("Start time is required."); return;
}
if (endTimeField.getText() == null || endTimeField.getText().isBlank()) {
showError("End time is required."); return;
}
LocalTime startTime = parseTime(startTimeField.getText());
LocalTime endTime = parseTime(endTimeField.getText());
if (startTime == null) {
showError("Invalid start time. Use HH:MM format."); return;
}
if (endTime == null) {
showError("Invalid end time. Use HH:MM format."); return;
}
LocalDateTime startDateTime = LocalDateTime.of(startDatePicker.getValue(), startTime);
LocalDateTime endDateTime = LocalDateTime.of(endDatePicker.getValue(), endTime);
if (startDateTime.isBefore(LocalDateTime.now())) {
showError("Start time cannot be in the past."); return;
}
if (!endDateTime.isAfter(startDateTime)) {
showError("End time must be after start time."); return;
}
List<String> optionTexts = optionFields.stream()
.map(tf -> tf.getText().trim())
.filter(t -> !t.isEmpty())
.collect(Collectors.toList());
if (optionTexts.size() < 2) {
showError("At least 2 options are required."); return;
}
long distinctCount = optionTexts.stream().distinct().count();
if (distinctCount != optionTexts.size()) {
showError("Options must be unique."); return;
}
confirmButton.setDisable(true);
Task<Poll> task = new Task<>() {
@Override
protected Poll call() throws Exception {
Poll poll = new Poll(
organizer.getId(),
titleField.getText().trim(),
descriptionField.getText().trim(),
startDateTime,
endDateTime
);
for (String text : optionTexts) {
poll.addOption(new PollOption(text));
}
PollRepository.save(poll);
return poll;
}
};
task.setOnSucceeded(e -> {
Poll created = task.getValue();
System.out.println("Poll created: " + created.getTitle());
confirmButton.setDisable(false);
clearForm();
loadPolls(); // refresh the poll lists
});
task.setOnFailed(e -> {
showError("Failed to create poll: " + task.getException().getMessage());
confirmButton.setDisable(false);
});
new Thread(task).start();
}
private void clearForm() {
titleField.clear();
descriptionField.clear();
startDatePicker.setValue(null);
endDatePicker.setValue(null);
startTimeField.clear();
endTimeField.clear();
optionsContainer.getChildren().clear();
optionFields.clear();
clearError();
}
private void loadPolls() {
Task<List<Poll>> task = new Task<>() {
@Override
protected List<Poll> call() throws Exception {
return PollRepository.findByOrganizer(organizer.getId());
}
};
task.setOnSucceeded(e -> {
List<Poll> polls = task.getValue();
List<Poll> active = polls.stream()
.filter(p -> p.getStatus() == PollStatus.ACTIVE)
.collect(Collectors.toList());
List<Poll> inactive = polls.stream()
.filter(p -> p.getStatus() != PollStatus.ACTIVE)
.collect(Collectors.toList());
activePollsListView.setItems(FXCollections.observableArrayList(active));
inactivePollsListView.setItems(FXCollections.observableArrayList(inactive));
activePollsListView.setCellFactory(lv -> new PollListCell());
inactivePollsListView.setCellFactory(lv -> new PollListCell());
});
task.setOnFailed(e ->
showError("Failed to load polls: " + task.getException().getMessage()));
new Thread(task).start();
}
private class PollListCell extends ListCell<Poll> {
@Override
protected void updateItem(Poll poll, boolean empty) {
super.updateItem(poll, empty);
if (empty || poll == null) {
setGraphic(null);
return;
}
VBox card = new VBox(4);
card.setPadding(new Insets(8));
Label title = new Label(poll.getTitle());
title.setStyle("-fx-font-weight: bold; -fx-font-size: 13;");
Label period = new Label(
poll.getStartTime().format(DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"))
+ "" +
poll.getEndTime().format(DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"))
);
period.setStyle("-fx-text-fill: #666; -fx-font-size: 11;");
Label status = new Label(formatStatus(poll));
status.setStyle("-fx-font-size: 11; -fx-text-fill: " + statusColor(poll) + ";");
card.getChildren().addAll(title, period, status);
HBox buttons = new HBox(8);
if (poll.isReadyForTally()) {
Button tallyBtn = new Button("Count Votes");
tallyBtn.setStyle("-fx-background-color: #4A90D9; -fx-text-fill: white;");
tallyBtn.setOnAction(e -> handleTally(poll));
buttons.getChildren().add(tallyBtn);
}
if (poll.isTallied()) {
Button resultsBtn = new Button("View Results");
resultsBtn.setOnAction(e -> handleViewResults(poll));
buttons.getChildren().add(resultsBtn);
}
if (!buttons.getChildren().isEmpty()) {
card.getChildren().add(buttons);
}
setGraphic(card);
}
private void handleViewResults(Poll poll) {
Task<TallyResult> task = new Task<>() {
@Override
protected TallyResult call() throws Exception {
return TallyResultRepository.findByPoll(poll.getPollId());
}
};
task.setOnSucceeded(e -> {
TallyResult result = task.getValue();
if (result == null) { showError("Results not found."); return; }
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle("Results — " + poll.getTitle());
alert.setHeaderText(null);
StringBuilder sb = new StringBuilder();
result.getResultsByOption().forEach((option, count) ->
sb.append(option).append(": ").append(count).append(" votes\n"));
sb.append("\nValid: ").append(result.getValidVotes());
sb.append("\nInvalid: ").append(result.getInvalidVotes());
alert.setContentText(sb.toString());
alert.showAndWait();
});
task.setOnFailed(e ->
showError("Could not load results: " + task.getException().getMessage()));
new Thread(task).start();
}
private String formatStatus(Poll poll) {
return switch (poll.getStatus()) {
case PENDING -> "⏳ Pending";
case ACTIVE -> "✅ Active";
case FINISHED -> poll.isTallied() ? "📊 Tallied" : "🔒 Finished";
};
}
private String statusColor(Poll poll) {
return switch (poll.getStatus()) {
case PENDING -> "#F5A623";
case ACTIVE -> "#7ED321";
case FINISHED -> "#9B9B9B";
};
}
}
private void handleTally(Poll poll) {
TextInputDialog dialog = new TextInputDialog();
dialog.setTitle("Tally Votes");
dialog.setHeaderText("Enter your password to decrypt and count votes.");
dialog.setContentText("Password:");
PasswordField pf = new PasswordField();
dialog.getDialogPane().setContent(pf);
dialog.showAndWait().ifPresent(ignored -> {
String password = pf.getText();
if (password.isBlank()) return;
Task<TallyResult> task = new Task<>() {
@Override
protected TallyResult call() throws Exception {
char[] ksPassword = KeystoreService.deriveKeystorePassword(
password, organizer.getId());
PrivateKey privateKey = KeystoreService.getPrivateKey(
organizer.getId(), ksPassword);
return TallyService.tallyPoll(poll, organizer, privateKey);
}
};
task.setOnSucceeded(e -> {
TallyResult result = task.getValue();
result.printReport();
loadPolls();
});
task.setOnFailed(e ->
showError("Tally failed: " + task.getException().getMessage()));
new Thread(task).start();
});
}
public static LocalTime parseTime(String input) {
if (input == null || input.trim().isEmpty()) return null;
input = input.trim().replaceAll("\\s+", ":"); // "7 30" to "7:30"
String[] parts = input.split(":");
try {
int hours = Integer.parseInt(parts[0]);
int minutes = (parts.length > 1) ? Integer.parseInt(parts[1]) : 0;
if (hours < 0 || hours > 23 || minutes < 0 || minutes > 59) {
return null;
}
return LocalTime.of(hours, minutes);
} catch (Exception e) {
return null;
}
}
private void showError(String msg) {
Platform.runLater(() -> {
errorLabel.setText(msg);
errorLabel.setVisible(true);
});
}
private void clearError() {
errorLabel.setText("");
errorLabel.setVisible(false);
}
private void showCreatePollForm() {
centerPane.setVisible(true);
clearForm();
}
} }

View File

@ -0,0 +1,5 @@
package dev.ksan.ui;
public class PollListCell {
}

View File

@ -1,17 +1,440 @@
package dev.ksan.ui; package dev.ksan.ui;
import dev.ksan.CASystem.KeystoreService;
import dev.ksan.Users.Organizer;
import dev.ksan.Users.UserRepository;
import dev.ksan.Users.Voter; import dev.ksan.Users.Voter;
import dev.ksan.voteSystem.*;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.layout.HBox;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.stage.Stage; import javafx.stage.Stage;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
public class VoterController { public class VoterController {
private Voter voter; private Voter voter;
private Stage stage; private Stage stage;
private Poll selectedPoll;
@FXML
private Label usernameLabel;
@FXML
private Label alreadyVotedLabel;
@FXML
private TitledPane activeList;
@FXML
private TitledPane inactiveList;
@FXML
private StackPane centerPane;
@FXML
private Text titleText;
@FXML
private Button refreshButton;
@FXML
private Text descriptionText;
@FXML
private Text startDateText;
@FXML
private Text endDateText;
@FXML
private Button confirmButton;
@FXML
private Label errorLabel;
@FXML
private VBox optionsContainer;
private final ToggleGroup optionToggleGroup = new ToggleGroup();
@FXML private VBox panesBox;
@FXML
private ListView<Poll> activePollsListView;
@FXML
private ListView<Poll> inactivePollsListView;
private final List<TextField> optionFields = new ArrayList<>();
public void setVoter(Voter voter) { public void setVoter(Voter voter) {
this.voter = voter; this.voter = voter;
loadPolls();
}
public void refreshPolls() {
loadPolls();
} }
public void setStage(Stage stage) { public void setStage(Stage stage) {
this.stage = stage; this.stage = stage;
usernameLabel.setText(voter.getName());
confirmButton.setOnAction(e -> castVote());
activePollsListView.getSelectionModel().selectedItemProperty()
.addListener((obs, oldVal, newVal) -> {
if (newVal != null) showPollDetail(newVal);
});
// Inactive polls are view-only just show detail with no voting
inactivePollsListView.getSelectionModel().selectedItemProperty()
.addListener((obs, oldVal, newVal) -> {
if (newVal != null) showPollDetail(newVal);
});
refreshButton.setOnAction(e -> refreshPolls());
}
public void initialize() {
errorLabel.setVisible(false);
centerPane.setVisible(false);
alreadyVotedLabel.setVisible(false);
}
private void castVote() {
clearError();
if (selectedPoll == null) return;
Toggle selected = optionToggleGroup.getSelectedToggle();
if (selected == null) {
showError("Please select an option before confirming.");
return;
}
String selectedOptionId = (String) selected.getUserData();
Dialog<String> passwordDialog = buildPasswordDialog();
passwordDialog.showAndWait().ifPresent(password -> {
if (password.isBlank()) {
showError("Password is required to sign your vote.");
return;
}
confirmButton.setDisable(true);
Task<Vote> task = new Task<>() {
@Override
protected Vote call() throws Exception {
char[] ksPassword = KeystoreService.deriveKeystorePassword(
password, voter.getUsername());
PrivateKey voterPrivateKey = KeystoreService.getPrivateKey(
voter.getUsername(), ksPassword);
Organizer organizer = (Organizer) UserRepository
.findOrganizer(selectedPoll.getOrganizerId());
if (organizer == null)
throw new IllegalStateException("Organizer not found.");
PublicKey organizerPublicKey = organizer.getCertificate().getPublicKey();
return VotingService.castVote(
selectedOptionId,
selectedPoll,
voter,
voterPrivateKey,
organizerPublicKey
);
}
};
task.setOnSucceeded(e -> {
showSuccess("");
loadPolls(); // refresh list
});
task.setOnFailed(e -> {
task.getException().printStackTrace();
showError("Vote failed: " + task.getException().getMessage());
confirmButton.setDisable(false);
});
new Thread(task).start();
});
}
private void loadPolls() {
Task<List<Poll>> task = new Task<>() {
@Override
protected List<Poll> call() throws Exception {
return PollRepository.findAll();
}
};
task.setOnSucceeded(e -> {
List<Poll> polls = task.getValue();
List<Poll> active = polls.stream()
.filter(p -> p.getStatus() == PollStatus.ACTIVE)
.collect(Collectors.toList());
List<Poll> inactive = polls.stream()
.filter(p -> p.getStatus() != PollStatus.ACTIVE)
.collect(Collectors.toList());
activePollsListView.setItems(FXCollections.observableArrayList(active));
inactivePollsListView.setItems(FXCollections.observableArrayList(inactive));
activePollsListView.setCellFactory(lv -> new PollListCell());
inactivePollsListView.setCellFactory(lv -> new PollListCell());
});
task.setOnFailed(e -> {
task.getException().printStackTrace();
showError("Failed to load polls: " + task.getException().getMessage());
});
new Thread(task).start();
}
private Dialog<String> buildPasswordDialog() {
Dialog<String> dialog = new Dialog<>();
dialog.setTitle("Confirm Vote");
dialog.setHeaderText("Enter your password to sign and submit your vote.");
PasswordField pf = new PasswordField();
pf.setPromptText("Password");
VBox content = new VBox(8);
content.getChildren().addAll(new Label("Password:"), pf);
dialog.getDialogPane().setContent(content);
ButtonType confirmType = new ButtonType("Submit Vote", ButtonBar.ButtonData.OK_DONE);
dialog.getDialogPane().getButtonTypes().addAll(confirmType, ButtonType.CANCEL);
dialog.setResultConverter(btn ->
btn == confirmType ? pf.getText() : null);
return dialog;
}
public void showPoll(Poll poll) {
centerPane.getChildren().clear();
VBox mainBox = new VBox(10);
mainBox.setPadding(new Insets(20));
mainBox.setAlignment(Pos.TOP_LEFT);
Label titleLabel = new Label("Title: " + poll.getTitle());
titleLabel.setStyle("-fx-font-size: 18px; -fx-font-weight: bold;");
Label descLabel = new Label("Description: " + poll.getDescription());
descLabel.setWrapText(true);
Label timeLabel = new Label(
"Start: " + poll.getStartTime() + "\nEnd: " + poll.getEndTime()
);
ToggleGroup toggleGroup = new ToggleGroup();
VBox optionsBox = new VBox(5);
for (PollOption option : poll.getOptions()) {
RadioButton rb = new RadioButton(option.getText());
rb.setToggleGroup(toggleGroup);
optionsBox.getChildren().add(rb);
}
Button confirmBtn = new Button("Confirm");
confirmBtn.setOnAction(e -> {
RadioButton selected = (RadioButton) toggleGroup.getSelectedToggle();
if (selected != null) {
System.out.println("Voted for: " + selected.getText());
// TODO vote logic
} else {
Alert alert = new Alert(Alert.AlertType.WARNING, "Please select an option.");
alert.showAndWait();
}
});
HBox confirmBox = new HBox(confirmBtn);
confirmBox.setAlignment(Pos.BOTTOM_RIGHT);
mainBox.getChildren().addAll(titleLabel, descLabel, timeLabel, optionsBox, confirmBox);
centerPane.getChildren().add(mainBox);
}
private void showPollDetail(Poll poll) {
selectedPoll = poll;
clearError();
titleText.setText(poll.getTitle());
descriptionText.setText(poll.getDescription());
startDateText.setText(poll.getStartTime().toString());
endDateText.setText(poll.getEndTime().toString());
optionsContainer.getChildren().clear();
optionToggleGroup.getToggles().clear();
for (PollOption option : poll.getOptions()) {
RadioButton rb = new RadioButton(option.getText());
rb.setToggleGroup(optionToggleGroup);
rb.setUserData(option.getOptionId());
optionsContainer.getChildren().add(rb);
}
boolean hasVoted = VotingService.hasVoted(voter.getUsername(), poll.getPollId());
confirmButton.setVisible(poll.getStatus() == PollStatus.ACTIVE && !hasVoted);
confirmButton.setDisable(hasVoted);
alreadyVotedLabel.setVisible(hasVoted);
// If already voted show verify button instead
if (hasVoted) {
showVerifyButton(poll);
}
centerPane.setVisible(true);
}
private void showVerifyButton(Poll poll) {
// Remove old verify button if present
optionsContainer.getChildren().removeIf(
node -> node instanceof Button &&
((Button) node).getText().equals("Verify My Vote"));
Button verifyBtn = new Button("Verify My Vote");
verifyBtn.setStyle("-fx-background-color: #7ED321; -fx-text-fill: white;");
verifyBtn.setOnAction(e -> verifyVote(poll));
optionsContainer.getChildren().add(verifyBtn);
}
private void verifyVote(Poll poll) {
Task<Boolean> task = new Task<>() {
@Override
protected Boolean call() throws Exception {
return VotingService.verifyVote(voter.getUsername(), poll.getPollId());
}
};
task.setOnSucceeded(e -> {
boolean verified = task.getValue();
Alert alert = new Alert(
verified ? Alert.AlertType.INFORMATION : Alert.AlertType.WARNING);
alert.setTitle("Vote Verification");
alert.setHeaderText(null);
alert.setContentText(verified
? "✅ Your vote is recorded and its integrity is verified."
: "⚠️ Could not verify your vote. Please contact the organizer.");
alert.showAndWait();
});
task.setOnFailed(e -> showError("Verification failed: "
+ task.getException().getMessage()));
new Thread(task).start();
}
private class PollListCell extends ListCell<Poll> {
@Override
protected void updateItem(Poll poll, boolean empty) {
super.updateItem(poll, empty);
if (empty || poll == null) {
setGraphic(null);
return;
}
VBox card = new VBox(4);
card.setPadding(new Insets(8));
Label title = new Label(poll.getTitle());
title.setStyle("-fx-font-weight: bold; -fx-font-size: 13;");
Label period = new Label(
poll.getStartTime().format(DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"))
+ "" +
poll.getEndTime().format(DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"))
);
period.setStyle("-fx-text-fill: #666; -fx-font-size: 11;");
Label status = new Label(formatStatus(poll));
status.setStyle("-fx-text-fill: " + statusColor(poll) + "; -fx-font-size: 11;");
boolean hasVoted = VotingService.hasVoted(voter.getUsername(), poll.getPollId());
if (hasVoted) {
Label votedBadge = new Label("✅ Voted");
votedBadge.setStyle("-fx-text-fill: #7ED321; -fx-font-size: 10;");
card.getChildren().addAll(title, period, status, votedBadge);
} else {
card.getChildren().addAll(title, period, status);
}
setGraphic(card);
}
private String formatStatus(Poll poll) {
return switch (poll.getStatus()) {
case PENDING -> "⏳ Pending";
case ACTIVE -> "✅ Active";
case FINISHED -> "🔒 Finished";
};
}
private String statusColor(Poll poll) {
return switch (poll.getStatus()) {
case PENDING -> "#F5A623";
case ACTIVE -> "#7ED321";
case FINISHED -> "#9B9B9B";
};
}
}
private void showError(String msg) {
Platform.runLater(() -> {
errorLabel.setText("⚠️ " + msg);
errorLabel.setStyle("-fx-text-fill: red;");
errorLabel.setVisible(true);
});
}
private void showSuccess(String msg) {
Platform.runLater(() -> {
errorLabel.setText(msg);
errorLabel.setStyle("-fx-text-fill: green;");
errorLabel.setVisible(true);
});
}
private void clearError() {
errorLabel.setText("");
errorLabel.setVisible(false);
} }
} }

View File

@ -4,6 +4,7 @@ import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.ObjectInputStream; import java.io.ObjectInputStream;
import java.io.ObjectOutputStream; import java.io.ObjectOutputStream;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock; import java.nio.channels.FileLock;
import java.nio.file.DirectoryStream; import java.nio.file.DirectoryStream;
import java.nio.file.Files; import java.nio.file.Files;
@ -32,6 +33,8 @@ public class PollRepository {
writeLock.lock(); writeLock.lock();
try { try {
writeToFile(poll, resolvePath(poll.getPollId())); writeToFile(poll, resolvePath(poll.getPollId()));
} catch (Exception e) {
e.printStackTrace();
} finally { } finally {
writeLock.unlock(); writeLock.unlock();
} }
@ -63,6 +66,8 @@ public class PollRepository {
readLock.unlock(); readLock.unlock();
} }
} }
} catch (Exception e) {
e.printStackTrace();
} }
return results; return results;
} }
@ -101,23 +106,28 @@ public class PollRepository {
private static void writeToFile(Object obj, Path targetPath) throws Exception { private static void writeToFile(Object obj, Path targetPath) throws Exception {
Path tmpPath = targetPath.resolveSibling(targetPath.getFileName() + ".tmp"); Path tmpPath = targetPath.resolveSibling(targetPath.getFileName() + ".tmp");
try (FileOutputStream fos = new FileOutputStream(tmpPath.toFile()); try (FileOutputStream fos = new FileOutputStream(tmpPath.toFile());
FileLock fileLock = fos.getChannel().lock(); FileChannel channel = fos.getChannel()) {
ObjectOutputStream oos = new ObjectOutputStream(fos)) {
oos.writeObject(obj); try (FileLock fileLock = channel.lock()) {
oos.flush(); ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(obj);
oos.flush();
channel.force(true);
}
} }
Files.move(tmpPath, targetPath, Files.move(tmpPath, targetPath,
StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); StandardCopyOption.ATOMIC_MOVE,
StandardCopyOption.REPLACE_EXISTING);
} }
private static Object deserialize(Path path) throws Exception { private static Object deserialize(Path path) throws Exception {
if (!Files.exists(path)) return null; if (!Files.exists(path)) return null;
try (FileInputStream fis = new FileInputStream(path.toFile()); try (FileInputStream fis = new FileInputStream(path.toFile());
FileLock fileLock = fis.getChannel().tryLock(0, Long.MAX_VALUE, true);
ObjectInputStream ois = new ObjectInputStream(fis)) { ObjectInputStream ois = new ObjectInputStream(fis)) {
if (fileLock == null)
throw new IllegalStateException("Could not acquire read lock on: " + path);
return ois.readObject(); return ois.readObject();
} }
} }

View File

@ -93,23 +93,29 @@ public class VoteRepository {
private static void writeToFile(Object obj, Path targetPath) throws Exception { private static void writeToFile(Object obj, Path targetPath) throws Exception {
Path tmpPath = targetPath.resolveSibling(targetPath.getFileName() + ".tmp"); Path tmpPath = targetPath.resolveSibling(targetPath.getFileName() + ".tmp");
try (FileOutputStream fos = new FileOutputStream(tmpPath.toFile()); try (FileOutputStream fos = new FileOutputStream(tmpPath.toFile());
FileLock fileLock = fos.getChannel().lock();
ObjectOutputStream oos = new ObjectOutputStream(fos)) { ObjectOutputStream oos = new ObjectOutputStream(fos)) {
oos.writeObject(obj);
oos.flush(); FileLock lock = fos.getChannel().lock();
try {
oos.writeObject(obj);
oos.flush();
} finally {
lock.release();
}
} }
Files.move(tmpPath, targetPath, Files.move(tmpPath, targetPath,
StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); StandardCopyOption.ATOMIC_MOVE,
StandardCopyOption.REPLACE_EXISTING);
} }
private static Object deserialize(Path path) throws Exception { private static Object deserialize(Path path) throws Exception {
if (!Files.exists(path)) return null; if (!Files.exists(path)) return null;
try (FileInputStream fis = new FileInputStream(path.toFile()); try (FileInputStream fis = new FileInputStream(path.toFile());
FileLock fileLock = fis.getChannel().tryLock(0, Long.MAX_VALUE, true);
ObjectInputStream ois = new ObjectInputStream(fis)) { ObjectInputStream ois = new ObjectInputStream(fis)) {
if (fileLock == null)
throw new IllegalStateException("Could not acquire read lock on: " + path);
return ois.readObject(); return ois.readObject();
} }
} }

View File

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.DatePicker?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.control.TextField?>
<?import javafx.scene.control.TitledPane?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Text?>
<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="dev.ksan.ui.OrganizerController">
<left>
<VBox prefHeight="200.0" prefWidth="250.0" BorderPane.alignment="CENTER">
<children>
<Button fx:id="newButton" mnemonicParsing="false" text="new" textAlignment="CENTER">
<VBox.margin>
<Insets bottom="5.0" left="30.0" top="5.0" />
</VBox.margin>
</Button>
<ScrollPane prefHeight="200.0" prefWidth="200.0" VBox.vgrow="ALWAYS">
<content>
<VBox fx:id="panesBox">
<children>
<TitledPane fx:id="activeList" animated="false" text="untitled" VBox.vgrow="ALWAYS">
<content>
<ListView fx:id="activePollsListView" prefHeight="200.0" prefWidth="200.0" />
</content>
<VBox.margin>
<Insets bottom="5.0" />
</VBox.margin>
</TitledPane>
<TitledPane fx:id="inactiveList" animated="false" text="untitled" VBox.vgrow="ALWAYS">
<content>
<ListView fx:id="inactivePollsListView" />
</content>
</TitledPane>
</children>
</VBox>
</content>
</ScrollPane>
</children>
<BorderPane.margin>
<Insets />
</BorderPane.margin>
</VBox>
</left>
<top>
<HBox prefHeight="27.0" prefWidth="600.0" BorderPane.alignment="CENTER">
<children>
<Label fx:id="usernameLabel" text="teemp" textAlignment="CENTER">
<HBox.margin>
<Insets left="20.0" top="5.0" />
</HBox.margin>
</Label>
</children>
</HBox>
</top>
<center>
<StackPane fx:id="centerPane" prefHeight="150.0" prefWidth="200.0" BorderPane.alignment="CENTER">
<children>
<VBox alignment="CENTER" prefHeight="200.0" prefWidth="100.0">
<children>
<HBox prefHeight="100.0" prefWidth="200.0">
<children>
<Text strokeType="OUTSIDE" strokeWidth="0.0" text="Title:" />
<TextField fx:id="titleField" />
</children>
</HBox>
<HBox prefHeight="100.0" prefWidth="200.0">
<children>
<Text strokeType="OUTSIDE" strokeWidth="0.0" text="Description:" />
<TextField fx:id="descriptionField" />
</children>
</HBox>
<VBox prefHeight="200.0" prefWidth="100.0">
<children>
<HBox prefHeight="100.0" prefWidth="200.0">
<children>
<DatePicker fx:id="startDatePicker" />
<Text strokeType="OUTSIDE" strokeWidth="0.0" text="&lt;---&gt;">
<HBox.margin>
<Insets top="3.0" />
</HBox.margin>
</Text>
<DatePicker fx:id="endDatePicker" />
</children>
</HBox>
<HBox prefHeight="100.0" prefWidth="200.0">
<children>
<TextField fx:id="startTimeField" />
<Text strokeType="OUTSIDE" strokeWidth="0.0" text="&lt;---&gt;" />
<TextField fx:id="endTimeField" />
</children>
</HBox>
</children>
</VBox>
<ScrollPane VBox.vgrow="ALWAYS">
<content>
<HBox>
<children>
<Button fx:id="newOptionButton" mnemonicParsing="false" text="new" />
<VBox fx:id="optionsContainer" alignment="CENTER" />
</children>
</HBox>
</content>
</ScrollPane>
<Button fx:id="confirmButton" alignment="BOTTOM_RIGHT" mnemonicParsing="false" text="create">
<graphic>
<Label fx:id="errorLabel" text="Label" />
</graphic>
</Button>
</children>
</VBox>
</children>
</StackPane>
</center>
</BorderPane>

View File

@ -1,43 +1,107 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<?import com.gluonhq.charm.glisten.control.Avatar?>
<?import com.gluonhq.charm.glisten.control.DropdownButton?>
<?import com.gluonhq.charm.glisten.control.ExpansionPanel?>
<?import javafx.geometry.Insets?> <?import javafx.geometry.Insets?>
<?import javafx.scene.control.MenuItem?> <?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ListView?>
<?import javafx.scene.control.ScrollPane?>
<?import javafx.scene.control.TitledPane?>
<?import javafx.scene.layout.BorderPane?> <?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.HBox?> <?import javafx.scene.layout.HBox?>
<?import javafx.scene.layout.StackPane?>
<?import javafx.scene.layout.VBox?> <?import javafx.scene.layout.VBox?>
<?import javafx.scene.text.Text?> <?import javafx.scene.text.Text?>
<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="dev.ksan.ui.VoterController">
<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/23.0.1" xmlns:fx="http://javafx.com/fxml/1">
<left> <left>
<VBox prefHeight="200.0" prefWidth="100.0" BorderPane.alignment="CENTER"> <VBox prefHeight="200.0" prefWidth="250.0" BorderPane.alignment="CENTER">
<children>
<ScrollPane prefHeight="200.0" prefWidth="200.0" VBox.vgrow="ALWAYS">
<content>
<VBox fx:id="panesBox">
<children>
<Button fx:id="refreshButton" mnemonicParsing="false" text="Button" />
<TitledPane fx:id="activeList" animated="false" text="untitled" VBox.vgrow="ALWAYS">
<content>
<ListView fx:id="activePollsListView" prefHeight="200.0" prefWidth="200.0" />
</content>
<VBox.margin>
<Insets bottom="5.0" />
</VBox.margin>
</TitledPane>
<TitledPane fx:id="inactiveList" animated="false" text="untitled" VBox.vgrow="ALWAYS">
<content>
<ListView fx:id="inactivePollsListView" />
</content>
</TitledPane>
</children>
</VBox>
</content>
</ScrollPane>
</children>
<BorderPane.margin>
<Insets />
</BorderPane.margin>
</VBox>
</left>
<top>
<HBox prefHeight="27.0" prefWidth="600.0" BorderPane.alignment="CENTER">
<children>
<Label fx:id="usernameLabel" text="teemp" textAlignment="CENTER">
<HBox.margin>
<Insets left="20.0" top="5.0" />
</HBox.margin>
</Label>
</children>
</HBox>
</top>
<center>
<StackPane fx:id="centerPane" prefHeight="150.0" prefWidth="200.0" BorderPane.alignment="CENTER">
<children> <children>
<HBox alignment="CENTER" prefHeight="100.0" prefWidth="200.0">
<children>
<Avatar>
<HBox.margin>
<Insets left="5.0" right="5.0" />
</HBox.margin>
</Avatar>
<Text strokeType="OUTSIDE" strokeWidth="0.0" text="Text" />
</children>
</HBox>
<VBox alignment="CENTER" prefHeight="200.0" prefWidth="100.0"> <VBox alignment="CENTER" prefHeight="200.0" prefWidth="100.0">
<children> <children>
<DropdownButton> <HBox prefHeight="100.0" prefWidth="200.0">
<items> <children>
<MenuItem text="Choice 1" /> <Text strokeType="OUTSIDE" strokeWidth="0.0" text="Title:" />
<MenuItem text="Choice 2" /> <Text fx:id="titleText" strokeType="OUTSIDE" strokeWidth="0.0" text="Text" />
<MenuItem text="Choice 3" /> </children>
</items> </HBox>
</DropdownButton> <HBox prefHeight="100.0" prefWidth="200.0">
<ExpansionPanel /> <children>
<Text strokeType="OUTSIDE" strokeWidth="0.0" text="Description:" />
<Text fx:id="descriptionText" strokeType="OUTSIDE" strokeWidth="0.0" text="Text" />
</children>
</HBox>
<VBox prefHeight="200.0" prefWidth="100.0">
<children>
<HBox prefHeight="100.0" prefWidth="200.0">
<children>
<Text fx:id="startDateText" strokeType="OUTSIDE" strokeWidth="0.0" text="Text" />
<Text strokeType="OUTSIDE" strokeWidth="0.0" text="&lt;---&gt;">
<HBox.margin>
<Insets top="3.0" />
</HBox.margin>
</Text>
<Text fx:id="endDateText" strokeType="OUTSIDE" strokeWidth="0.0" text="Text" />
</children>
</HBox>
</children>
</VBox>
<ScrollPane VBox.vgrow="ALWAYS">
<content>
<HBox>
<children>
<VBox fx:id="optionsContainer" alignment="CENTER" />
</children>
</HBox>
</content>
</ScrollPane>
<Label fx:id="alreadyVotedLabel" text="Label" />
<Label fx:id="errorLabel" text="Label" />
<Button fx:id="confirmButton" alignment="BOTTOM_RIGHT" mnemonicParsing="false" text="create" />
</children> </children>
</VBox> </VBox>
</children> </children>
</VBox> </StackPane>
</left> </center>
</BorderPane> </BorderPane>