diff --git a/src/main/java/ch/zhaw/gartenverwaltung/CropDetailController.java b/src/main/java/ch/zhaw/gartenverwaltung/CropDetailController.java index 7ec8652..bbe7cea 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/CropDetailController.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/CropDetailController.java @@ -104,16 +104,16 @@ public class CropDetailController { * open dialog to set area */ @FXML - void setArea() { - + void setArea() throws IOException { + openTextFieldDialog("set Text Area", "Text Area", area_label.getText(), false); } /** * open dialog to set location */ @FXML - void setLocation() { - + void setLocation() throws IOException { + openTextFieldDialog("set Location", "Location", location_label.getText(), true); } /** @@ -218,6 +218,7 @@ public class CropDetailController { setIconToButton(edit, "editIcon.png"); setIconToButton(delete, "deleteIcon.png"); edit.setOnAction(getEditTaskEvent(task)); + delete.setOnAction(deleteTask(task)); hBox.getChildren().addAll(taskName, taskDescription, edit, delete); return hBox; @@ -260,6 +261,12 @@ public class CropDetailController { }; } + private EventHandler deleteTask(Task task) { + return (event) -> { + showDeleteTask(task); + }; + } + private void createTaskDialog(boolean newTask, Task givenTask) throws IOException { Dialog dialog = new Dialog<>(); dialog.setTitle("Set Task"); @@ -278,6 +285,7 @@ public class CropDetailController { if (appLoader.loadPaneToDialog("TaskFormular.fxml", dialogPane) instanceof TaskFormularController controller) { controller.setCorp(this.crop); + controller.initSaveButton((Button) dialogPane.lookupButton(saveTask)); if (!newTask) { controller.setTaskValue(givenTask); } @@ -300,4 +308,56 @@ public class CropDetailController { } } + + private void openTextFieldDialog(String title, String labelDescription, String value, boolean isLocation) throws IOException { + Dialog dialog = new Dialog<>(); + dialog.setTitle(title); + dialog.setHeaderText(title); + dialog.setResizable(false); + + DialogPane dialogPane = dialog.getDialogPane(); + + ButtonType save = new ButtonType("Save", ButtonBar.ButtonData.OK_DONE); + dialogPane.getButtonTypes().addAll(save, ButtonType.CANCEL); + + if (appLoader.loadPaneToDialog("TextFieldFormular.fxml", dialogPane) instanceof TextFieldFormularController controller) { + controller.setDescription_label(labelDescription); + controller.setValueTextArea(value); + controller.initSaveButton((Button) dialogPane.lookupButton(save)); + + dialog.setResultConverter(button -> button.equals(save) ? controller.getValue() : null); + + dialog.showAndWait() + .ifPresent(string -> { + if (isLocation) { + System.out.println(string); + //ToDo method to set location + location_label.setText(string); + } else { + System.out.println(string); + //ToDo method to set area of crop in garden + area_label.setText(string); + } + }); + } + } + + private void showDeleteTask(Task task) { + Alert alert = new Alert(Alert.AlertType.CONFIRMATION); + alert.setTitle("Delete " + task.getName()); + alert.setHeaderText("Are you sure want to delete this Task?"); + + alert.showAndWait() + .ifPresent(buttonType -> { + if (buttonType == ButtonType.OK) { + try { + gardenSchedule.removeTask(task); + setTaskListProperty(this.crop); + } catch (IOException e) { + // TODO: Show error alert + LOG.log(Level.SEVERE, "Could not remove crop.", e); + } + } + }); + } } diff --git a/src/main/java/ch/zhaw/gartenverwaltung/MainFXMLController.java b/src/main/java/ch/zhaw/gartenverwaltung/MainFXMLController.java index 2cc05d3..33a67c5 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/MainFXMLController.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/MainFXMLController.java @@ -11,6 +11,9 @@ import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.layout.AnchorPane; import javafx.scene.layout.Pane; +import javafx.stage.Modality; +import javafx.stage.Stage; +import javafx.stage.WindowEvent; import java.io.IOException; import java.util.logging.Level; @@ -40,6 +43,9 @@ public class MainFXMLController { @FXML private Button tutorial_button; + private final Stage tutorialModal = new Stage(); + + /** * go to home pane */ @@ -95,11 +101,23 @@ public class MainFXMLController { } /** - * go to Tutorial pane + * Show the tutorial window */ - public void goToTutorial() { - showPaneAsMainView("Tutorial.fxml"); - styleChangeButton(tutorial_button); + public void showTutorial() { + if (!tutorialModal.isShowing()) { + if (tutorialModal.getScene() == null) { + try { + appLoader.loadSceneToStage("Tutorial.fxml", tutorialModal); + tutorialModal.initModality(Modality.NONE); + tutorialModal.setResizable(false); + tutorialModal.sizeToScene(); + } catch (IOException e) { + LOG.log(Level.SEVERE, "Could not load Tutorial"); + } + } + tutorialModal.show(); + } + tutorialModal.requestFocus(); } /** @@ -132,7 +150,6 @@ public class MainFXMLController { appLoader.loadAndCacheFxml("MyGarden.fxml"); appLoader.loadAndCacheFxml("MySchedule.fxml"); appLoader.loadAndCacheFxml("Plants.fxml"); - appLoader.loadAndCacheFxml("Tutorial.fxml"); } private void styleChangeButton(Button button) { @@ -153,12 +170,14 @@ public class MainFXMLController { } catch (IOException e) { LOG.log(Level.SEVERE, "Failed to load FXML-Pane!", e); } + mainPane.getScene().getWindow().setOnCloseRequest(this::closeWindowHandler); setIconToButton(home_button, "homeIcon.png"); setIconToButton(settings_button, "settingsIcon.png"); - Settings.getInstance().getShowTutorialProperty().addListener((observable, oldValue, newValue) -> { - tutorial_button.setVisible(newValue); - }); - tutorial_button.setVisible(Settings.getInstance().getShowTutorial()); + tutorial_button.visibleProperty().bind(Settings.getInstance().getShowTutorialProperty()); + } + + private void closeWindowHandler(WindowEvent windowEvent) { + tutorialModal.close(); } /** @@ -173,7 +192,4 @@ public class MainFXMLController { imageView.setPreserveRatio(true); button.setGraphic(imageView); } - - } - diff --git a/src/main/java/ch/zhaw/gartenverwaltung/TaskFormularController.java b/src/main/java/ch/zhaw/gartenverwaltung/TaskFormularController.java index 938eea9..936bf09 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/TaskFormularController.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/TaskFormularController.java @@ -2,12 +2,14 @@ package ch.zhaw.gartenverwaltung; import ch.zhaw.gartenverwaltung.types.Crop; import ch.zhaw.gartenverwaltung.types.Task; +import javafx.beans.binding.Binding; +import javafx.beans.binding.Bindings; +import javafx.beans.binding.BooleanBinding; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; import javafx.fxml.FXML; import javafx.fxml.Initializable; -import javafx.scene.control.DateCell; -import javafx.scene.control.DatePicker; -import javafx.scene.control.TextArea; -import javafx.scene.control.TextField; +import javafx.scene.control.*; import javafx.util.Callback; import java.net.URL; @@ -56,7 +58,7 @@ public class TaskFormularController implements Initializable { } } - private Callback getDayCellFactory() { + private Callback getDayCellFactoryStartDate() { return (datePicker) -> new DateCell() { private final LocalDate today = LocalDate.now(); @@ -71,16 +73,61 @@ public class TaskFormularController implements Initializable { setDisable(false); setStyle("-fx-background-color: #32CD32;"); } + + if (end_datePicker.getValue() != null && item.compareTo(end_datePicker.getValue()) > 0) { + setDisable(true); + setStyle("-fx-background-color: #ffc0cb;"); + } } }; } + private Callback getDayCellFactoryEndDate() { + + return (datePicker) -> new DateCell() { + private final LocalDate today = LocalDate.now(); + + @Override + public void updateItem(LocalDate item, boolean empty) { + super.updateItem(item, empty); + setDisable(true); + setStyle("-fx-background-color: #ffc0cb;"); + + if (item.compareTo(today) > 0 && item.compareTo(crop.getStartDate()) > 0) { + setDisable(false); + setStyle("-fx-background-color: #32CD32;"); + } + + if (start_datePicker.getValue() != null && item.compareTo(start_datePicker.getValue()) < 0) { + setDisable(true); + setStyle("-fx-background-color: #ffc0cb;"); + } + } + }; + } + + public void initSaveButton(Button button) { + interval_field.textProperty().addListener((observable, oldValue, newValue) -> { + if (!newValue.matches("\\d*")) { + interval_field.setText(newValue.replaceAll("[^\\d]", "")); + } + }); + + button.disableProperty().bind(start_datePicker.valueProperty().isNull() + .or(end_datePicker.valueProperty().isNull()) + .or(taskName_field.textProperty().isEmpty()) + .or(description_area.textProperty().isEmpty()) + .or(interval_field.textProperty().isEmpty())); + + + } + @Override public void initialize(URL location, ResourceBundle resources) { - start_datePicker.setDayCellFactory(getDayCellFactory()); + start_datePicker.setDayCellFactory(getDayCellFactoryStartDate()); start_datePicker.setEditable(false); - end_datePicker.setDayCellFactory(getDayCellFactory()); + end_datePicker.setDayCellFactory(getDayCellFactoryEndDate()); end_datePicker.setEditable(false); } } diff --git a/src/main/java/ch/zhaw/gartenverwaltung/TextFieldFormularController.java b/src/main/java/ch/zhaw/gartenverwaltung/TextFieldFormularController.java new file mode 100644 index 0000000..a53c240 --- /dev/null +++ b/src/main/java/ch/zhaw/gartenverwaltung/TextFieldFormularController.java @@ -0,0 +1,32 @@ +package ch.zhaw.gartenverwaltung; + +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; + +public class TextFieldFormularController { + + @FXML + private Label description_label; + + @FXML + private TextField text_area; + + + public void setDescription_label(String string) { + description_label.setText(string); + } + + public void setValueTextArea(String string) { + text_area.setText(string); + } + + public String getValue() { + return text_area.getText(); + } + + public void initSaveButton(Button button) { + button.disableProperty().bind(text_area.textProperty().isEmpty()); + } +} diff --git a/src/main/java/ch/zhaw/gartenverwaltung/TutorialController.java b/src/main/java/ch/zhaw/gartenverwaltung/TutorialController.java index aa02930..a1de10b 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/TutorialController.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/TutorialController.java @@ -1,6 +1,61 @@ package ch.zhaw.gartenverwaltung; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; +import javafx.scene.layout.StackPane; +import javafx.stage.Stage; + public class TutorialController { + @FXML + public Button previousPageButton; + @FXML + public Button nextPageButton; + @FXML + public StackPane tourPages; + public ImageView imgAddNewPlant; + public ImageView imgTaskList; + public ImageView imgSelectDate; + + private int page = 0; + + @FXML + public void initialize() { + switchViews(); + setButtonAbilities(); + + Image placeholder = new Image(String.valueOf(PlantsController.class.getResource("placeholder.png"))); + imgAddNewPlant.setImage(placeholder); + imgSelectDate.setImage(placeholder); + imgTaskList.setImage(placeholder); + } + + public void viewNextPage() { + page++; + switchViews(); + setButtonAbilities(); + } + public void viewPreviousPage() { + page--; + switchViews(); + setButtonAbilities(); + } + + private void setButtonAbilities() { + previousPageButton.setDisable(page <= 0); + nextPageButton.setDisable(page >= tourPages.getChildren().size() - 1); + } + + private void switchViews() { + tourPages.getChildren().forEach(node -> node.setOpacity(0)); + tourPages.getChildren().get(page).setOpacity(1); + } + + public void closeTutorial() { + Stage root = (Stage) tourPages.getScene().getWindow(); + root.close(); + } } diff --git a/src/main/java/ch/zhaw/gartenverwaltung/bootstrap/AppLoader.java b/src/main/java/ch/zhaw/gartenverwaltung/bootstrap/AppLoader.java index 59a4f7d..9c14010 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/bootstrap/AppLoader.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/bootstrap/AppLoader.java @@ -73,7 +73,10 @@ public class AppLoader { public Object loadSceneToStage(String fxmlFile, Stage appendee) throws IOException { FXMLLoader loader = new FXMLLoader(Objects.requireNonNull(HelloApplication.class.getResource(fxmlFile))); Pane root = loader.load(); - appendee.setScene(new Scene(root)); + Scene scene = new Scene(root); + String css = Objects.requireNonNull(this.getClass().getResource("styles.css")).toExternalForm(); + appendee.setScene(scene); + scene.getStylesheets().add(css); Object controller = loader.getController(); annotationInject(controller); return controller; diff --git a/src/main/java/ch/zhaw/gartenverwaltung/io/JsonTaskList.java b/src/main/java/ch/zhaw/gartenverwaltung/io/JsonTaskList.java index 6b18f72..7f6d36b 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/io/JsonTaskList.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/io/JsonTaskList.java @@ -13,6 +13,7 @@ import java.net.URISyntaxException; import java.net.URL; import java.time.LocalDate; import java.time.format.DateTimeFormatter; +import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -29,6 +30,7 @@ public class JsonTaskList implements TaskList { private final static String INVALID_DATASOURCE_MSG = "Invalid datasource specified!"; private Map taskMap = Collections.emptyMap(); + private final List subscribers = new ArrayList<>(); /** * Creating constant objects required to deserialize the {@link LocalDate} classes @@ -87,6 +89,7 @@ public class JsonTaskList implements TaskList { loadTaskListFromFile(); } taskMap.values().removeIf(task -> task.getCropId() == cropId); + notifySubscribers(); } /** @@ -107,6 +110,7 @@ public class JsonTaskList implements TaskList { taskMap.put(id, task.withId(id)); writeTaskListToFile(); + notifySubscribers(); } /** @@ -126,6 +130,25 @@ public class JsonTaskList implements TaskList { taskMap.remove(taskId); writeTaskListToFile(); } + notifySubscribers(); + } + + /** + * {@inheritDoc} + * @param observer The change handler + */ + @Override + public void subscribe(TaskListObserver observer) { + subscribers.add(observer); + } + + /** + * Calls the change handler method on all registered observers. + */ + private void notifySubscribers() { + for (TaskListObserver subscriber : subscribers) { + subscriber.onChange(taskMap.values().stream().toList()); + } } /** diff --git a/src/main/java/ch/zhaw/gartenverwaltung/io/TaskList.java b/src/main/java/ch/zhaw/gartenverwaltung/io/TaskList.java index 97ef3a7..95f5b07 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/io/TaskList.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/io/TaskList.java @@ -51,4 +51,22 @@ public interface TaskList { * @throws IOException If the database cannot be accessed */ void removeTask(Task task) throws IOException; + + /** + * Registers an observer to be notified of Changes in the TaskList + * @param observer The change handler + */ + void subscribe(TaskListObserver observer); + + /** + * Specifies an observer for a TaskList + */ + @FunctionalInterface + interface TaskListObserver { + /** + * Method which will be called when changes occur. + * @param newTaskList The new values + */ + void onChange(List newTaskList); + } } diff --git a/src/main/java/ch/zhaw/gartenverwaltung/models/GardenSchedule.java b/src/main/java/ch/zhaw/gartenverwaltung/models/GardenSchedule.java index 9195d51..c3292f6 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/models/GardenSchedule.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/models/GardenSchedule.java @@ -9,6 +9,7 @@ import java.time.LocalDate; import java.util.ArrayList; import java.util.Comparator; import java.util.List; +import java.util.function.Consumer; import java.util.stream.Collectors; public class GardenSchedule { @@ -19,6 +20,7 @@ public class GardenSchedule { * Comparators to create sorted Task List */ static final Comparator sortByStartDate = Comparator.comparing(Task::getStartDate); + static final Comparator sortByNextExecution = Comparator.comparing(Task::getNextExecution); /** * Constructor to create Database Objects. @@ -46,7 +48,8 @@ public class GardenSchedule { */ public void planTasksForCrop(Crop crop) throws PlantNotFoundException, HardinessZoneNotSetException, IOException { Plant plant = plantList.getPlantById(Settings.getInstance().getCurrentHardinessZone(), crop.getPlantId()).orElseThrow(PlantNotFoundException::new); - for (GrowthPhase growthPhase : plant.lifecycle()) { + int growPhaseGroup = plant.getGrowphaseGroupForDate(crop.getStartDate()); + for (GrowthPhase growthPhase : plant.lifecycleForGroup(growPhaseGroup)) { for (TaskTemplate taskTemplate : growthPhase.taskTemplates()) { addTask(taskTemplate.generateTask(crop.getStartDate(), crop.getCropId().orElse(0L))); } @@ -135,10 +138,22 @@ public class GardenSchedule { * @throws IOException If the database cannot be accessed */ public List> getTasksUpcomingWeek() throws IOException { + final int listLength = 7; + List weekTasks = taskList.getTaskList(LocalDate.now(), LocalDate.now().plusDays(listLength - 1)); List> dayTaskList = new ArrayList<>(); - for(int i = 0; i < 7; i++) { + for(int i = 0; i < listLength; i++) { LocalDate date = LocalDate.now().plusDays(i); - dayTaskList.add(taskList.getTaskList(date, date)); + dayTaskList.add(new ArrayList<>()); + final int finalI = i; + weekTasks.forEach(task -> { + LocalDate checkDate = task.getNextExecution(); + do { + if (date.equals(checkDate) && !date.isAfter(task.getEndDate().orElse(LocalDate.MIN))) { + dayTaskList.get(finalI).add(task); + } + checkDate = checkDate.plusDays(task.getInterval().orElse(0)); + } while (task.getInterval().isPresent() && checkDate.isBefore(LocalDate.now().plusDays(listLength))); + }); } return dayTaskList; } @@ -149,11 +164,8 @@ public class GardenSchedule { * @throws IOException If the database cannot be accessed */ public List> getTasksUpcomingWeekForCrop(Long cropId) throws IOException { - List> dayTaskList = new ArrayList<>(); - for(int i = 0; i < 7; i++) { - LocalDate date = LocalDate.now().plusDays(i); - dayTaskList.add(filterListByCrop(taskList.getTaskList(date, date), cropId)); - } + List> dayTaskList = getTasksUpcomingWeek(); + dayTaskList.forEach(taskList -> taskList.removeIf(task -> task.getCropId() != cropId)); return dayTaskList; } @@ -165,7 +177,7 @@ public class GardenSchedule { * @throws IOException If the database cannot be accessed */ public List getFilteredTaskList(LocalDate start, LocalDate end) throws IOException { - return getSortedTaskList(taskList.getTaskList(start, end), sortByStartDate); + return getSortedTaskList(taskList.getTaskList(start, end), sortByNextExecution); } /** diff --git a/src/main/java/ch/zhaw/gartenverwaltung/types/Plant.java b/src/main/java/ch/zhaw/gartenverwaltung/types/Plant.java index 1cf6466..b77540c 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/types/Plant.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/types/Plant.java @@ -39,6 +39,16 @@ public record Plant( .collect(Collectors.toList()); } + public int getGrowphaseGroupForDate(LocalDate date) { + for(GrowthPhase growthPhase : lifecycle){ + MonthDay plantingDate = MonthDay.of(date.getMonth().getValue(), date.getDayOfMonth()); + if(plantingDate.isAfter(growthPhase.startDate()) && plantingDate.isBefore(growthPhase.endDate())){ + return growthPhase.group(); + } + } + return 0; // TODO implement + } + /** * get sow date from given harvest day from lifecycle group * @param harvestDate date of the harvest diff --git a/src/main/java/ch/zhaw/gartenverwaltung/types/Task.java b/src/main/java/ch/zhaw/gartenverwaltung/types/Task.java index a2bc6a0..16fd155 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/types/Task.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/types/Task.java @@ -15,6 +15,8 @@ public class Task { private final LocalDate startDate; private Integer interval; private LocalDate endDate; + private LocalDate nextExecution; + private LocalDate nextNotification; private long cropId; /** @@ -25,12 +27,16 @@ public class Task { name= ""; description= ""; startDate = LocalDate.now(); + endDate = startDate; + nextExecution = startDate; } public Task(String name, String description, LocalDate startDate, long cropId) { this.name = name; this.description = description; this.startDate = startDate; + this.endDate = startDate; + nextExecution = startDate; this.cropId = cropId; } /** @@ -46,6 +52,7 @@ public class Task { this.name = name; this.description = description; this.startDate = startDate; + nextExecution = startDate; this.endDate = endDate; this.interval = interval; this.cropId = cropId; @@ -66,10 +73,32 @@ public class Task { } public boolean isInTimePeriode(LocalDate searchStartDate, LocalDate searchEndDate){ - return startDate.isAfter(searchStartDate) && startDate.isBefore(searchEndDate); + return endDate.isAfter(searchStartDate) && startDate.isBefore(searchEndDate); + } + + public void done(){ + if(interval != null && !nextExecution.plusDays(interval).isAfter(endDate)){ + nextExecution = nextExecution.plusDays(interval); + } else { + nextExecution = null; + } + } + + public boolean isDone(){ + return nextExecution == null; + } + + public void setNextExecution(LocalDate nextExecution) { + this.nextExecution = nextExecution; + } + + public void setNextNotification(LocalDate nextNotification) { + this.nextNotification = nextNotification; } // Getters + public LocalDate getNextNotification() { return nextNotification; } + public LocalDate getNextExecution() { return nextExecution; } public Optional getId() { return Optional.ofNullable(id); } public String getName() { return name; } public String getDescription() { return description; } diff --git a/src/main/java/ch/zhaw/gartenverwaltung/types/TaskTemplate.java b/src/main/java/ch/zhaw/gartenverwaltung/types/TaskTemplate.java index 2e471cb..b2cb06f 100644 --- a/src/main/java/ch/zhaw/gartenverwaltung/types/TaskTemplate.java +++ b/src/main/java/ch/zhaw/gartenverwaltung/types/TaskTemplate.java @@ -16,10 +16,6 @@ public class TaskTemplate { @JsonProperty private Integer interval; - // TODO: reconsider if we need this - @JsonProperty - private boolean isOptional = false; - /** * Default constructor * (Used by deserializer) @@ -48,6 +44,10 @@ public class TaskTemplate { public Task generateTask(LocalDate realStartDate, long cropId) { LocalDate endDate = relativeEndDate != null ? realStartDate.plusDays(relativeEndDate) : null; + if (interval == null) { + this.interval = 0; + } + return new Task(name, description, realStartDate.plusDays(relativeStartDate), cropId) .withInterval(interval) .withEndDate(endDate); diff --git a/src/main/resources/ch/zhaw/gartenverwaltung/MainFXML.fxml b/src/main/resources/ch/zhaw/gartenverwaltung/MainFXML.fxml index ddca3e3..74a6e34 100644 --- a/src/main/resources/ch/zhaw/gartenverwaltung/MainFXML.fxml +++ b/src/main/resources/ch/zhaw/gartenverwaltung/MainFXML.fxml @@ -12,7 +12,7 @@