diff --git a/src/main/java/dev/ksan/Users/UserRepository.java b/src/main/java/dev/ksan/Users/UserRepository.java index ac517ea..8d41392 100644 --- a/src/main/java/dev/ksan/Users/UserRepository.java +++ b/src/main/java/dev/ksan/Users/UserRepository.java @@ -165,6 +165,7 @@ public class UserRepository { } + private static List findAll( Path dir, Class type, String lockPrefix) throws Exception { diff --git a/src/main/java/dev/ksan/ui/LoginController.java b/src/main/java/dev/ksan/ui/LoginController.java index 575efaf..f0d9a08 100644 --- a/src/main/java/dev/ksan/ui/LoginController.java +++ b/src/main/java/dev/ksan/ui/LoginController.java @@ -10,7 +10,6 @@ import javafx.application.Platform; import javafx.concurrent.Task; import javafx.fxml.FXML; import javafx.fxml.FXMLLoader; -import javafx.scene.Parent; import javafx.scene.Scene; import javafx.scene.control.*; import javafx.scene.input.Dragboard; @@ -261,27 +260,42 @@ public class LoginController { private void navigateToUserUI(User user) { try { - if (user instanceof Organizer organizer) { + + if (user instanceof Organizer) { FXMLLoader loader = new FXMLLoader( 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(); - controller.setOrganizer(organizer); + controller.setOrganizer((Organizer) user); 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( 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(); - controller.setVoter(voter); + controller.setVoter((Voter) user); controller.setStage(stage); - stage.setScene(new Scene(root)); + + stage.setScene(scene); + stage.setTitle(((Voter) user).getName()); } } catch (Exception e) { showError("Failed to load UI: " + e.getMessage()); + System.out.println(e); loginButton.setDisable(false); } } diff --git a/src/main/java/dev/ksan/ui/OrganizerController.java b/src/main/java/dev/ksan/ui/OrganizerController.java index 53b94f8..2f9f5d1 100644 --- a/src/main/java/dev/ksan/ui/OrganizerController.java +++ b/src/main/java/dev/ksan/ui/OrganizerController.java @@ -1,18 +1,432 @@ package dev.ksan.ui; +import dev.ksan.CASystem.KeystoreService; 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 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 { 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 activePollsListView; + @FXML + private ListView 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 final List optionFields = new ArrayList<>(); + public void setOrganizer(Organizer organizer) { this.organizer = organizer; + usernameLabel.setText(organizer.getName()); + + loadPolls(); } public void setStage(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 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 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> task = new Task<>() { + @Override + protected List call() throws Exception { + return PollRepository.findByOrganizer(organizer.getId()); + } + }; + + task.setOnSucceeded(e -> { + + List polls = task.getValue(); + + List active = polls.stream() + .filter(p -> p.getStatus() == PollStatus.ACTIVE) + .collect(Collectors.toList()); + + List 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 { + + @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 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 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(); + } + } diff --git a/src/main/java/dev/ksan/ui/PollListCell.java b/src/main/java/dev/ksan/ui/PollListCell.java new file mode 100644 index 0000000..18cb532 --- /dev/null +++ b/src/main/java/dev/ksan/ui/PollListCell.java @@ -0,0 +1,5 @@ +package dev.ksan.ui; + + +public class PollListCell { +} diff --git a/src/main/java/dev/ksan/ui/VoterController.java b/src/main/java/dev/ksan/ui/VoterController.java index 726b9e0..57e70da 100644 --- a/src/main/java/dev/ksan/ui/VoterController.java +++ b/src/main/java/dev/ksan/ui/VoterController.java @@ -1,17 +1,440 @@ 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.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 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 { private Voter voter; 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 activePollsListView; + @FXML + private ListView inactivePollsListView; + + private final List optionFields = new ArrayList<>(); + public void setVoter(Voter voter) { this.voter = voter; + loadPolls(); + } + + public void refreshPolls() { + loadPolls(); } public void setStage(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 passwordDialog = buildPasswordDialog(); + passwordDialog.showAndWait().ifPresent(password -> { + if (password.isBlank()) { + showError("Password is required to sign your vote."); + return; + } + + confirmButton.setDisable(true); + + Task 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> task = new Task<>() { + @Override + protected List call() throws Exception { + return PollRepository.findAll(); + } + }; + + task.setOnSucceeded(e -> { + List polls = task.getValue(); + + List active = polls.stream() + .filter(p -> p.getStatus() == PollStatus.ACTIVE) + .collect(Collectors.toList()); + + List 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 buildPasswordDialog() { + Dialog 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 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 { + + @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); } } diff --git a/src/main/java/dev/ksan/voteSystem/PollRepository.java b/src/main/java/dev/ksan/voteSystem/PollRepository.java index cd20c64..7cfd078 100644 --- a/src/main/java/dev/ksan/voteSystem/PollRepository.java +++ b/src/main/java/dev/ksan/voteSystem/PollRepository.java @@ -4,6 +4,7 @@ import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; +import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.nio.file.DirectoryStream; import java.nio.file.Files; @@ -32,6 +33,8 @@ public class PollRepository { writeLock.lock(); try { writeToFile(poll, resolvePath(poll.getPollId())); + } catch (Exception e) { + e.printStackTrace(); } finally { writeLock.unlock(); } @@ -63,6 +66,8 @@ public class PollRepository { readLock.unlock(); } } + } catch (Exception e) { + e.printStackTrace(); } return results; } @@ -101,23 +106,28 @@ public class PollRepository { private static void writeToFile(Object obj, Path targetPath) throws Exception { Path tmpPath = targetPath.resolveSibling(targetPath.getFileName() + ".tmp"); + try (FileOutputStream fos = new FileOutputStream(tmpPath.toFile()); - FileLock fileLock = fos.getChannel().lock(); - ObjectOutputStream oos = new ObjectOutputStream(fos)) { - oos.writeObject(obj); - oos.flush(); + FileChannel channel = fos.getChannel()) { + + try (FileLock fileLock = channel.lock()) { + ObjectOutputStream oos = new ObjectOutputStream(fos); + oos.writeObject(obj); + oos.flush(); + channel.force(true); + } } + Files.move(tmpPath, targetPath, - StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + StandardCopyOption.ATOMIC_MOVE, + StandardCopyOption.REPLACE_EXISTING); } private static Object deserialize(Path path) throws Exception { if (!Files.exists(path)) return null; + try (FileInputStream fis = new FileInputStream(path.toFile()); - FileLock fileLock = fis.getChannel().tryLock(0, Long.MAX_VALUE, true); ObjectInputStream ois = new ObjectInputStream(fis)) { - if (fileLock == null) - throw new IllegalStateException("Could not acquire read lock on: " + path); return ois.readObject(); } } diff --git a/src/main/java/dev/ksan/voteSystem/VoteRepository.java b/src/main/java/dev/ksan/voteSystem/VoteRepository.java index 719f94e..b1de806 100644 --- a/src/main/java/dev/ksan/voteSystem/VoteRepository.java +++ b/src/main/java/dev/ksan/voteSystem/VoteRepository.java @@ -93,23 +93,29 @@ public class VoteRepository { private static void writeToFile(Object obj, Path targetPath) throws Exception { Path tmpPath = targetPath.resolveSibling(targetPath.getFileName() + ".tmp"); + try (FileOutputStream fos = new FileOutputStream(tmpPath.toFile()); - FileLock fileLock = fos.getChannel().lock(); 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, - StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + StandardCopyOption.ATOMIC_MOVE, + StandardCopyOption.REPLACE_EXISTING); } private static Object deserialize(Path path) throws Exception { if (!Files.exists(path)) return null; + try (FileInputStream fis = new FileInputStream(path.toFile()); - FileLock fileLock = fis.getChannel().tryLock(0, Long.MAX_VALUE, true); ObjectInputStream ois = new ObjectInputStream(fis)) { - if (fileLock == null) - throw new IllegalStateException("Could not acquire read lock on: " + path); return ois.readObject(); } } diff --git a/src/main/resources/fxml/OrganizerView.fxml b/src/main/resources/fxml/OrganizerView.fxml index e69de29..7caec1f 100644 --- a/src/main/resources/fxml/OrganizerView.fxml +++ b/src/main/resources/fxml/OrganizerView.fxml @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
diff --git a/src/main/resources/fxml/VoterView.fxml b/src/main/resources/fxml/VoterView.fxml index 6f7f04d..fd1a42c 100644 --- a/src/main/resources/fxml/VoterView.fxml +++ b/src/main/resources/fxml/VoterView.fxml @@ -1,43 +1,107 @@ - - - - + + + + + + - - + - + + + + + + +