diff --git a/build.gradle b/build.gradle index 47fbca250ec..c3b51dae38d 100644 --- a/build.gradle +++ b/build.gradle @@ -57,6 +57,8 @@ dependencies { implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'mac' implementation group: 'org.openjfx', name: 'javafx-graphics', version: javaFxVersion, classifier: 'linux' + implementation 'org.apache.commons:commons-lang3:3.12.0' + implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: '2.7.0' implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: '2.7.4' diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 5d4d8535457..81b32d1cb00 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -72,7 +72,7 @@ The **API** of this component is specified in [`Ui.java`](https://github.com/se- ![Structure of the UI Component](images/UiClassDiagram.png) -The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `PersonListPanel`, `StatusBarFooter` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class which captures the commonalities between classes that represent parts of the visible GUI. +The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `PersonListPanel`, `StatusBarFooter` `HelpWindow` `ViewWindow` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class which captures the commonalities between classes that represent parts of the visible GUI. The `UI` component uses the JavaFx UI framework. The layout of these UI parts are defined in matching `.fxml` files that are in the `src/main/resources/view` folder. For example, the layout of the [`MainWindow`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/ui/MainWindow.java) is specified in [`MainWindow.fxml`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/resources/view/MainWindow.fxml) @@ -155,6 +155,62 @@ Classes used by multiple components are in the `seedu.addressbook.commons` packa This section describes some noteworthy details on how certain features are implemented. +### Filter Command + +#### Implementation + +The Address book filter is made with `GradeSubjectFilterPredicate`. It extends from `Predicate` functional interface and is used to set as a condition to check that for each student, the `Grade` and `Subject` specified is met. Predicates created can be updated using the method `Model#updateFilteredPersonList(predicate)`. + +The `Grade` and `Subject` classes are set as additional data fields in `Person`. + +* Both classes have exposed methods `isEmpty()`, to check if the grade and subject parameters are specified. +* When unspecified: + * `Grade()` equals to `Grade("-")` (i.e. "-" will be shown when there is no grade for the student.) + * `Subject()` equals to `Subject("No subject")` (i.e. "No subject" will be shown when no subject is assigned for the student.) + +Given below is an example usage scenario and what the predicate is at each step. + +Step 1. The user launches the application for the first time. The student's contacts in a form of `FilteredList` will be shown, where the predicate states that condition is true for all. + +
:information_source: **Note:** By default, `FilteredList` predicate is set to `FilteredList.ALWAYS_TRUE`. But since both are equivalent, `Model.PREDICATE_SHOW_ALL_PERSONS` is used. + +
+ +![FilterState0](images/FilterState0-FilteredList.png) + +Step 2. The user executes `filter g/A` command to get all students in the `FilteredList` who has an "A" grade. The `filter` command creates `GradeSubjectFilterPredicate`, and calls `Model#updateFilteredPersonList(predicate)`, updating the list to show students that has an "A" grade. + +![FilterState1](images/FilterState1-FilteredList.png) + +Calling `list` command will revert the predicate back to `Model.PREDICATE_SHOW_ALL_PERSONS`. + +
+ +The following sequence diagram shows how a filter operation goes through the `Logic` component: + +![FilterSequenceDiagram-Logic](images/FilterSequenceDiagram-Logic.png) + +
:information_source: **Note:** The lifeline for `FilterCommand` and `GradeSubjectFilterPredicate` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. + +
+ +Similarly, how a filter operation goes through the `Model` component is shown below: + +![FilterSequenceDiagram-Model](images/FilterSequenceDiagram-Model.png) + +The following activity diagram summarizes what happens when a tutor executes a filter command. + +![FilterActivityDiagram](images/FilterActivityDiagram.png) + +#### Design considerations + +* Alternative 1: Filtered address book result can be saved, since in practice, there will only be a few combinations of filters. + * Pros: Operation will be fast as the number of students increases. + * Cons: More memory usage. +* Alternative 2: Introduce command history to avoid typing long commands. + * Pros: Useful for the entire application, and would use less memory (e.g. storing the first 10 commands). + * Cons: Harder to implement. + ### \[Proposed\] Undo/redo feature #### Proposed Implementation diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 192d87a0683..835d86c5acd 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -3,7 +3,7 @@ layout: page title: User Guide --- -TutorsGo is a **desktop app for managing contacts, optimized for use via a Command Line Interface** (CLI) while still having the benefits of a Graphical User Interface (GUI). If you can type fast, AB3 can get your contact management tasks done faster than traditional GUI apps. +TutorsGo is a **desktop app for managing contacts, optimized for use via a Command Line Interface** (CLI) while still having the benefits of a Graphical User Interface (GUI). If you can type fast, TutorsGo can get your contact management tasks done faster than traditional GUI apps. * Table of Contents {:toc} @@ -14,11 +14,11 @@ TutorsGo is a **desktop app for managing contacts, optimized for use via a Comma 1. Ensure you have Java `11` or above installed in your Computer. -1. Download the latest `addressbook.jar` from [here](https://github.com/se-edu/addressbook-level3/releases). +1. Download the latest `.jar` from [here](https://github.com/AY2324S2-CS2103-F15-2/tp/releases). 1. Copy the file to the folder you want to use as the _home folder_ for your AddressBook. -1. Open a command terminal, `cd` into the folder you put the jar file in, and use the `java -jar addressbook.jar` command to run the application.
+1. Open a command terminal, `cd` into the folder you put the jar file in, and use the `java -jar .jar` command to run the application.
A GUI similar to the below should appear in a few seconds. Note how the app contains some sample data.
![Ui](images/Ui.png) @@ -27,7 +27,7 @@ TutorsGo is a **desktop app for managing contacts, optimized for use via a Comma * `list` : Lists all contacts. - * `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` : Adds a contact named `John Doe` to the Address Book. + * `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01 d/2024-02-03 1800 at/present pa/paid` : Adds a contact named `John Doe` to the Address Book. * `delete 3` : Deletes the 3rd contact shown in the current list. @@ -79,41 +79,41 @@ Adds a student to the address book. Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [g/GRADE] [s/SUBJECT] [d/DATETIME] [t/TAG]…​`
:bulb: **Tip:** -A person can have any number of tags (including 0) +A student can have any number of tags (including 0)
-* Grade should only contain a single letter from A to D, with plus(+), minus(-) or neither. +* Grade follows NUS grading system. (i.e. [A+, A, A-, B+, B, B-, C+, C, D+, D, F, -]) * DateTime should be in yyyy-mm-dd hhmm Examples: * `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01 g/B+ s/Mathematics d/2024-02-03 1800` -* `add n/Betsy Crowe t/friend e/betsycrowe@example.com a/Newgate Prison p/1234567 t/criminal` +* `add n/Betsy Crowe t/friend e/betsycrowe@example.com a/Newgate Prison p/1234567` -### Listing all persons : `list` +### Listing all students : `list` -Shows a list of all persons in the address book. +Shows a list of all students in the address book. Format: `list` -### Editing a person : `edit` +### Editing a student : `edit` -Edits an existing person in the address book. +Edits an existing student in the address book. Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` -* Edits the person at the specified `INDEX`. The index refers to the index number shown in the displayed person list. The index **must be a positive integer** 1, 2, 3, …​ +* Edits the student at the specified `INDEX`. The index refers to the index number shown in the displayed student list. The index **must be a positive integer** 1, 2, 3, …​ * At least one of the optional fields must be provided. * Existing values will be updated to the input values. -* When editing tags, the existing tags of the person will be removed i.e adding of tags is not cumulative. -* You can remove all the person’s tags by typing `t/` without +* When editing tags, the existing tags of the student will be removed i.e adding of tags is not cumulative. +* You can remove all the student’s tags by typing `t/` without specifying any tags after it. Examples: -* `edit 1 p/91234567 e/johndoe@example.com` Edits the phone number and email address of the 1st person to be `91234567` and `johndoe@example.com` respectively. -* `edit 2 n/Betsy Crower t/` Edits the name of the 2nd person to be `Betsy Crower` and clears all existing tags. +* `edit 1 p/91234567 e/johndoe@example.com` Edits the phone number and email address of the 1st student to be `91234567` and `johndoe@example.com` respectively. +* `edit 2 n/Betsy Crower t/` Edits the name of the 2nd student to be `Betsy Crower` and clears all existing tags. -### Locating persons by name: `find` +### Locating students by name: `find` -Finds persons whose names contain any of the given keywords. +Finds students whose names contain any of the given keywords. Format: `find KEYWORD [MORE_KEYWORDS]` @@ -121,7 +121,7 @@ Format: `find KEYWORD [MORE_KEYWORDS]` * The order of the keywords does not matter. e.g. `Hans Bo` will match `Bo Hans` * Only the name is searched. * Only full words will be matched e.g. `Han` will not match `Hans` -* Persons matching at least one keyword will be returned (i.e. `OR` search). +* Students matching at least one keyword will be returned (i.e. `OR` search). e.g. `Hans Bo` will return `Hans Gruber`, `Bo Yang` Examples: @@ -129,19 +129,35 @@ Examples: * `find alex david` returns `Alex Yeoh`, `David Li`
![result for 'find alex david'](images/findAlexDavidResult.png) -### Deleting a person : `delete` +### Filter student by grade / subject: `filter` -Deletes the specified person from the address book. +Filters student who has the specified grade and/or subject. + +Format: `filter [g/GRADE] [s/SUBJECT]` + +* Search is case-sensitive. +* The order of keywords does not matter. +* At least one of the optional fields must be provided. + +### View Schedule: `view` + +Displays a calendar to view scheduled classes. + +Format: `view` + +### Deleting a student : `delete` + +Deletes the specified student from the address book. Format: `delete INDEX` -* Deletes the person at the specified `INDEX`. -* The index refers to the index number shown in the displayed person list. +* Deletes the student at the specified `INDEX`. +* The index refers to the index number shown in the displayed student list. * The index **must be a positive integer** 1, 2, 3, …​ Examples: -* `list` followed by `delete 2` deletes the 2nd person in the address book. -* `find Betsy` followed by `delete 1` deletes the 1st person in the results of the `find` command. +* `list` followed by `delete 2` deletes the 2nd student in the address book. +* `find Betsy` followed by `delete 1` deletes the 1st student in the results of the `find` command. ### Clearing all entries : `clear` @@ -189,14 +205,14 @@ _Details coming soon ..._ ## Command summary -Action | Format, Examples ---------|------------------ -**Add** | `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​`
e.g., `add n/James Ho p/22224444 e/jamesho@example.com a/123, Clementi Rd, 1234665 t/friend t/colleague` -**Clear** | `clear` -**Delete** | `delete INDEX`
e.g., `delete 3` -**Edit** | `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG]…​`
e.g.,`edit 2 n/James Lee e/jameslee@example.com` -**Find** | `find KEYWORD [MORE_KEYWORDS]`
e.g., `find James Jake` -**List** | `list` -**Help** | `help` -**Mark payment**|`mark_payment INDEX`
e.g. `mark_payment 2` -**payment_list**|`payment_list` +Action | Format, Examples +--------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------- +**Add** | `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​`
e.g., `add n/James Ho p/22224444 e/jamesho@example.com a/123, Clementi Rd, 1234665 t/friend t/colleague` +**Clear** | `clear` +**Delete** | `delete INDEX`
e.g., `delete 3` +**Edit** | `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG]…​`
e.g.,`edit 2 n/James Lee e/jameslee@example.com` +**Find** | `find KEYWORD [MORE_KEYWORDS]`
e.g., `find James Jake` +**Filter**| `filter [g/GRADE] [s/SUBJECT]` +**View Schedule**|`view` +**List** | `list` +**Help** | `help` diff --git a/docs/diagrams/BetterModelClassDiagram.puml b/docs/diagrams/BetterModelClassDiagram.puml index 196d0d2caac..53d33fcd39d 100644 --- a/docs/diagrams/BetterModelClassDiagram.puml +++ b/docs/diagrams/BetterModelClassDiagram.puml @@ -4,13 +4,13 @@ skinparam arrowThickness 1.1 skinparam arrowColor MODEL_COLOR skinparam classBackgroundColor MODEL_COLOR -AddressBook *-right-> "1" UniquePersonList +AddressBook *-right-> "1" UniquePersonAndDateTimeList AddressBook *-right-> "1" UniqueTagList -UniqueTagList -[hidden]down- UniquePersonList -UniqueTagList -[hidden]down- UniquePersonList +UniqueTagList -[hidden]down- UniquePersonAndDateTimeList +UniqueTagList -[hidden]down- UniquePersonAndDateTimeList UniqueTagList -right-> "*" Tag -UniquePersonList -right-> Person +UniquePersonAndDateTimeList -right-> Person Person -up-> "*" Tag diff --git a/docs/diagrams/FilterActivityDiagram.puml b/docs/diagrams/FilterActivityDiagram.puml new file mode 100644 index 00000000000..e5dd5461703 --- /dev/null +++ b/docs/diagrams/FilterActivityDiagram.puml @@ -0,0 +1,14 @@ +@startuml +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 +start +:User executes filter command; + +'Since the beta syntax does not support placing the condition outside the +'diamond we place it as the true branch instead. + +:System updates filter predicate; +:Returns filtered student list; +stop +@enduml diff --git a/docs/diagrams/FilterSequenceDiagram-Logic.puml b/docs/diagrams/FilterSequenceDiagram-Logic.puml new file mode 100644 index 00000000000..7ebbe87b080 --- /dev/null +++ b/docs/diagrams/FilterSequenceDiagram-Logic.puml @@ -0,0 +1,55 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":FilterCommand" as FilterCommand LOGIC_COLOR +participant ":GradeSubjectFilterPredicate" as GradeSubjectFilterPredicate LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box +[-> LogicManager : execute(filter g/...) +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand() +activate AddressBookParser + +create GradeSubjectFilterPredicate +AddressBookParser -> GradeSubjectFilterPredicate +activate GradeSubjectFilterPredicate + +GradeSubjectFilterPredicate --> AddressBookParser +deactivate GradeSubjectFilterPredicate + +create FilterCommand +AddressBookParser -> FilterCommand +activate FilterCommand + +FilterCommand --> AddressBookParser +deactivate FilterCommand + +AddressBookParser --> LogicManager +deactivate AddressBookParser + +LogicManager -> FilterCommand : execute() +activate FilterCommand + +FilterCommand -> Model : updateFilteredPersonList(predicate) +activate Model + +Model --> FilterCommand +deactivate Model + +FilterCommand --> LogicManager : commandResult +deactivate FilterCommand +FilterCommand -[hidden]-> LogicManager : commandResult +destroy FilterCommand +destroy GradeSubjectFilterPredicate + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/FilterSequenceDiagram-Model.puml b/docs/diagrams/FilterSequenceDiagram-Model.puml new file mode 100644 index 00000000000..b7e69c2e09f --- /dev/null +++ b/docs/diagrams/FilterSequenceDiagram-Model.puml @@ -0,0 +1,17 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> Model : updateFilteredPersonList(predicate) +activate Model + +Model -> Model :setPredicate(predicate) + +[<-- Model +deactivate Model + +@enduml diff --git a/docs/diagrams/FilterState0.puml b/docs/diagrams/FilterState0.puml new file mode 100644 index 00000000000..6efbd2e7b17 --- /dev/null +++ b/docs/diagrams/FilterState0.puml @@ -0,0 +1,18 @@ +@startuml +!include style.puml +skinparam ClassFontColor #000000 +skinparam ClassBorderColor #000000 +skinparam ClassBackgroundColor #FFFFAA + +title FilteredList + +package Predicates { + class State1 as "Model.PREDICATE_SHOW_ALL_PERSONS" + class State2 as ":GradeSubjectFilterPredicate" +} +State1 -[hidden]right-> State2 +hide State2 + +class Pointer as "Current State" #FFFFFF +Pointer -up-> State1 +@end diff --git a/docs/diagrams/FilterState1.puml b/docs/diagrams/FilterState1.puml new file mode 100644 index 00000000000..ed638134a5e --- /dev/null +++ b/docs/diagrams/FilterState1.puml @@ -0,0 +1,17 @@ +@startuml +!include style.puml +skinparam ClassFontColor #000000 +skinparam ClassBorderColor #000000 +skinparam ClassBackgroundColor #FFFFAA + +title FilteredList + +package Predicates { + class State1 as "Model.PREDICATE_SHOW_ALL_PERSONS" + class State2 as ":GradeSubjectFilterPredicate" +} +State1 -[hidden]right-> State2 + +class Pointer as "Current State" #FFFFFF +Pointer -up-> State2 +@end diff --git a/docs/diagrams/ModelClassDiagram.puml b/docs/diagrams/ModelClassDiagram.puml index 6b1ec31b1c0..47e6b3032fc 100644 --- a/docs/diagrams/ModelClassDiagram.puml +++ b/docs/diagrams/ModelClassDiagram.puml @@ -12,7 +12,7 @@ Class AddressBook Class ModelManager Class UserPrefs -Class UniquePersonList +Class UniquePersonAndDateTimeList Class Person Class Address Class Email @@ -40,8 +40,8 @@ ModelManager -left-> "1" AddressBook ModelManager -right-> "1" UserPrefs UserPrefs .up.|> ReadOnlyUserPrefs -AddressBook *--> "1" UniquePersonList -UniquePersonList --> "~* all" Person +AddressBook *--> "1" UniquePersonAndDateTimeList +UniquePersonAndDateTimeList --> "~* all" Person Person *--> Name Person *--> Phone Person *--> Email diff --git a/docs/diagrams/StorageClassDiagram.puml b/docs/diagrams/StorageClassDiagram.puml index a821e06458c..3878fd7fe0c 100644 --- a/docs/diagrams/StorageClassDiagram.puml +++ b/docs/diagrams/StorageClassDiagram.puml @@ -19,6 +19,7 @@ Class "<>\nAddressBookStorage" as AddressBookStorage Class JsonAddressBookStorage Class JsonSerializableAddressBook Class JsonAdaptedPerson +Class JsonAdaptedDateTime Class JsonAdaptedTag } @@ -38,6 +39,7 @@ JsonUserPrefsStorage .up.|> UserPrefsStorage JsonAddressBookStorage .up.|> AddressBookStorage JsonAddressBookStorage ..> JsonSerializableAddressBook JsonSerializableAddressBook --> "*" JsonAdaptedPerson +JsonAdaptedPerson --> "*" JsonAdaptedDateTime JsonAdaptedPerson --> "*" JsonAdaptedTag @enduml diff --git a/docs/diagrams/UiClassDiagram.puml b/docs/diagrams/UiClassDiagram.puml index 95473d5aa19..c1e4a97fde2 100644 --- a/docs/diagrams/UiClassDiagram.puml +++ b/docs/diagrams/UiClassDiagram.puml @@ -10,6 +10,7 @@ Class "{abstract}\nUiPart" as UiPart Class UiManager Class MainWindow Class HelpWindow +Class ViewWindow Class ResultDisplay Class PersonListPanel Class PersonCard @@ -35,6 +36,7 @@ MainWindow *-down-> "1" ResultDisplay MainWindow *-down-> "1" PersonListPanel MainWindow *-down-> "1" StatusBarFooter MainWindow --> "0..1" HelpWindow +MainWindow --> "0..1" ViewWindow PersonListPanel -down-> "*" PersonCard @@ -46,6 +48,7 @@ PersonListPanel --|> UiPart PersonCard --|> UiPart StatusBarFooter --|> UiPart HelpWindow --|> UiPart +ViewWindow --|> UiPart PersonCard ..> Model UiManager -right-> Logic @@ -53,6 +56,7 @@ MainWindow -left-> Logic PersonListPanel -[hidden]left- HelpWindow HelpWindow -[hidden]left- CommandBox +ViewWindow -[hidden]left- CommandBox CommandBox -[hidden]left- ResultDisplay ResultDisplay -[hidden]left- StatusBarFooter diff --git a/docs/images/FilterActivityDiagram.png b/docs/images/FilterActivityDiagram.png new file mode 100644 index 00000000000..db76f5c722b Binary files /dev/null and b/docs/images/FilterActivityDiagram.png differ diff --git a/docs/images/FilterSequenceDiagram-Logic.png b/docs/images/FilterSequenceDiagram-Logic.png new file mode 100644 index 00000000000..41fa75e4121 Binary files /dev/null and b/docs/images/FilterSequenceDiagram-Logic.png differ diff --git a/docs/images/FilterSequenceDiagram-Model.png b/docs/images/FilterSequenceDiagram-Model.png new file mode 100644 index 00000000000..f96766f4408 Binary files /dev/null and b/docs/images/FilterSequenceDiagram-Model.png differ diff --git a/docs/images/FilterState0-FilteredList.png b/docs/images/FilterState0-FilteredList.png new file mode 100644 index 00000000000..8540f3acb4d Binary files /dev/null and b/docs/images/FilterState0-FilteredList.png differ diff --git a/docs/images/FilterState1-FilteredList.png b/docs/images/FilterState1-FilteredList.png new file mode 100644 index 00000000000..e645897e941 Binary files /dev/null and b/docs/images/FilterState1-FilteredList.png differ diff --git a/docs/images/StorageClassDiagram.png b/docs/images/StorageClassDiagram.png index 18fa4d0d51f..6ae5620b2e7 100644 Binary files a/docs/images/StorageClassDiagram.png and b/docs/images/StorageClassDiagram.png differ diff --git a/docs/images/UiClassDiagram.png b/docs/images/UiClassDiagram.png index 11f06d68671..e4eeec1733a 100644 Binary files a/docs/images/UiClassDiagram.png and b/docs/images/UiClassDiagram.png differ diff --git a/docs/images/helpMessage.png b/docs/images/helpMessage.png index b1f70470137..1942f2d7e38 100644 Binary files a/docs/images/helpMessage.png and b/docs/images/helpMessage.png differ diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddCommand.java index b2d2f1b8792..1bb0618bd0d 100644 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ b/src/main/java/seedu/address/logic/commands/AddCommand.java @@ -50,6 +50,7 @@ public class AddCommand extends Command { public static final String MESSAGE_SUCCESS = "New Student added: %1$s"; public static final String MESSAGE_DUPLICATE_PERSON = "This student already exists in the address book"; + public static final String MESSAGE_DUPLICATE_DATETIME = "This datetime already exists in the address book"; private final Person toAdd; @@ -69,6 +70,10 @@ public CommandResult execute(Model model) throws CommandException { throw new CommandException(MESSAGE_DUPLICATE_PERSON); } + if (model.hasDateTime(toAdd)) { + throw new CommandException(MESSAGE_DUPLICATE_DATETIME); + } + model.addPerson(toAdd); return new CommandResult(String.format(MESSAGE_SUCCESS, Messages.format(toAdd))); } diff --git a/src/main/java/seedu/address/logic/commands/CommandResult.java b/src/main/java/seedu/address/logic/commands/CommandResult.java index 249b6072d0d..0690cd79d66 100644 --- a/src/main/java/seedu/address/logic/commands/CommandResult.java +++ b/src/main/java/seedu/address/logic/commands/CommandResult.java @@ -19,13 +19,17 @@ public class CommandResult { /** The application should exit. */ private final boolean exit; + /** The application show the schedule to the user. */ + private final boolean showView; + /** * Constructs a {@code CommandResult} with the specified fields. */ - public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) { + public CommandResult(String feedbackToUser, boolean showHelp, boolean exit, boolean showView) { this.feedbackToUser = requireNonNull(feedbackToUser); this.showHelp = showHelp; this.exit = exit; + this.showView = showView; } /** @@ -33,7 +37,7 @@ public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) { * and other fields set to their default value. */ public CommandResult(String feedbackToUser) { - this(feedbackToUser, false, false); + this(feedbackToUser, false, false, false); } public String getFeedbackToUser() { @@ -48,6 +52,10 @@ public boolean isExit() { return exit; } + public boolean isView() { + return showView; + } + @Override public boolean equals(Object other) { if (other == this) { @@ -62,12 +70,13 @@ public boolean equals(Object other) { CommandResult otherCommandResult = (CommandResult) other; return feedbackToUser.equals(otherCommandResult.feedbackToUser) && showHelp == otherCommandResult.showHelp - && exit == otherCommandResult.exit; + && exit == otherCommandResult.exit + && showView == otherCommandResult.showView; } @Override public int hashCode() { - return Objects.hash(feedbackToUser, showHelp, exit); + return Objects.hash(feedbackToUser, showHelp, exit, showView); } @Override @@ -76,6 +85,7 @@ public String toString() { .add("feedbackToUser", feedbackToUser) .add("showHelp", showHelp) .add("exit", exit) + .add("showView", showView) .toString(); } diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java index 1135ac19b74..5fa51087c6c 100644 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ b/src/main/java/seedu/address/logic/commands/DeleteCommand.java @@ -23,7 +23,7 @@ public class DeleteCommand extends Command { + "Parameters: INDEX (must be a positive integer)\n" + "Example: " + COMMAND_WORD + " 1"; - public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted Person: %1$s"; + public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted Student: %1$s"; private final Index targetIndex; diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java index 01296a56ece..93840a23e04 100644 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ b/src/main/java/seedu/address/logic/commands/EditCommand.java @@ -66,6 +66,7 @@ public class EditCommand extends Command { public static final String MESSAGE_EDIT_PERSON_SUCCESS = "Edited Person: %1$s"; public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book."; + public static final String MESSAGE_DUPLICATE_DATETIME = "This datetime already exists in the address book"; private final Index index; private final EditPersonDescriptor editPersonDescriptor; @@ -98,6 +99,10 @@ public CommandResult execute(Model model) throws CommandException { throw new CommandException(MESSAGE_DUPLICATE_PERSON); } + if (!personToEdit.isSameDateTime(editedPerson) && model.hasDateTime(editedPerson)) { + throw new CommandException(MESSAGE_DUPLICATE_DATETIME); + } + model.setPerson(personToEdit, editedPerson); model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(editedPerson))); diff --git a/src/main/java/seedu/address/logic/commands/ExitCommand.java b/src/main/java/seedu/address/logic/commands/ExitCommand.java index 3dd85a8ba90..acac9a21374 100644 --- a/src/main/java/seedu/address/logic/commands/ExitCommand.java +++ b/src/main/java/seedu/address/logic/commands/ExitCommand.java @@ -13,7 +13,7 @@ public class ExitCommand extends Command { @Override public CommandResult execute(Model model) { - return new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, false, true); + return new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, false, true, false); } } diff --git a/src/main/java/seedu/address/logic/commands/FilterCommand.java b/src/main/java/seedu/address/logic/commands/FilterCommand.java index df3f69e64c4..b5935795a17 100644 --- a/src/main/java/seedu/address/logic/commands/FilterCommand.java +++ b/src/main/java/seedu/address/logic/commands/FilterCommand.java @@ -17,7 +17,7 @@ public class FilterCommand extends Command { public static final String MESSAGE_USAGE = COMMAND_WORD + ": Filters the address book based on the Grade or Subject.\n" - + "Parameters: \n" + + "Parameters: [g/GRADE] [s/SUBJECT]\n" + "Example: " + COMMAND_WORD + " g/A s/Maths"; public static final String MESSAGE_FILTER_ADDRESS_BOOK_SUCCESS = "Filtered address book by %2$s!\n"; @@ -34,7 +34,7 @@ public CommandResult execute(Model model) throws CommandException { model.updateFilteredPersonList(predicate); return new CommandResult( String.format(MESSAGE_FILTER_ADDRESS_BOOK_SUCCESS + Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, - model.getFilteredPersonList().size(), predicate)); + model.getFilteredPersonList().size(), predicate.filterResult())); } @Override diff --git a/src/main/java/seedu/address/logic/commands/HelpCommand.java b/src/main/java/seedu/address/logic/commands/HelpCommand.java index bf824f91bd0..07d26e2a23c 100644 --- a/src/main/java/seedu/address/logic/commands/HelpCommand.java +++ b/src/main/java/seedu/address/logic/commands/HelpCommand.java @@ -16,6 +16,6 @@ public class HelpCommand extends Command { @Override public CommandResult execute(Model model) { - return new CommandResult(SHOWING_HELP_MESSAGE, true, false); + return new CommandResult(SHOWING_HELP_MESSAGE, true, false, false); } } diff --git a/src/main/java/seedu/address/logic/commands/ViewCommand.java b/src/main/java/seedu/address/logic/commands/ViewCommand.java new file mode 100644 index 00000000000..dacd330aa83 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ViewCommand.java @@ -0,0 +1,17 @@ +package seedu.address.logic.commands; + +import seedu.address.model.Model; + +/** + * Format full help instructions for every command for display. + */ +public class ViewCommand extends Command { + + public static final String COMMAND_WORD = "view"; + public static final String SHOWING_SCHEDULE_MESSAGE = "Opened schedule window."; + + @Override + public CommandResult execute(Model model) { + return new CommandResult(SHOWING_SCHEDULE_MESSAGE, false, false, true); + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java index 3d1b98f57dc..c0b5f0523f0 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -18,6 +18,7 @@ import seedu.address.logic.commands.FindCommand; import seedu.address.logic.commands.HelpCommand; import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.ViewCommand; import seedu.address.logic.parser.exceptions.ParseException; /** @@ -75,12 +76,15 @@ public Command parseCommand(String userInput) throws ParseException { case FilterCommand.COMMAND_WORD: return new FilterCommandParser().parse(arguments); - case ExitCommand.COMMAND_WORD: - return new ExitCommand(); - case HelpCommand.COMMAND_WORD: return new HelpCommand(); + case ViewCommand.COMMAND_WORD: + return new ViewCommand(); + + case ExitCommand.COMMAND_WORD: + return new ExitCommand(); + default: logger.finer("This user input caused a ParseException: " + userInput); throw new ParseException(MESSAGE_UNKNOWN_COMMAND); diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java index 73397161e84..7a428852d6e 100644 --- a/src/main/java/seedu/address/model/AddressBook.java +++ b/src/main/java/seedu/address/model/AddressBook.java @@ -7,7 +7,7 @@ import javafx.collections.ObservableList; import seedu.address.commons.util.ToStringBuilder; import seedu.address.model.person.Person; -import seedu.address.model.person.UniquePersonList; +import seedu.address.model.person.UniquePersonAndDateTimeList; /** * Wraps all data at the address-book level @@ -15,7 +15,7 @@ */ public class AddressBook implements ReadOnlyAddressBook { - private final UniquePersonList persons; + private final UniquePersonAndDateTimeList persons; /* * The 'unusual' code block below is a non-static initialization block, sometimes used to avoid duplication @@ -25,7 +25,7 @@ public class AddressBook implements ReadOnlyAddressBook { * among constructors. */ { - persons = new UniquePersonList(); + persons = new UniquePersonAndDateTimeList(); } public AddressBook() {} @@ -67,6 +67,14 @@ public boolean hasPerson(Person person) { return persons.contains(person); } + /** + * Returns true if a person with the same datetime as {@code person} exists in the address book. + */ + public boolean hasDateTime(Person person) { + requireNonNull(person); + return persons.containsDateTime(person); + } + /** * Adds a person to the address book. * The person must not already exist in the address book. diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index d54df471c1f..3c69596ccf2 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -57,6 +57,11 @@ public interface Model { */ boolean hasPerson(Person person); + /** + * Returns true if a person with the same datetime as {@code person} exists in the address book. + */ + boolean hasDateTime(Person person); + /** * Deletes the given person. * The person must exist in the address book. diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index 57bc563fde6..ac8c2e741e9 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/seedu/address/model/ModelManager.java @@ -93,6 +93,12 @@ public boolean hasPerson(Person person) { return addressBook.hasPerson(person); } + @Override + public boolean hasDateTime(Person person) { + requireNonNull(person); + return addressBook.hasDateTime(person); + } + @Override public void deletePerson(Person target) { addressBook.removePerson(target); diff --git a/src/main/java/seedu/address/model/person/DateTime.java b/src/main/java/seedu/address/model/person/DateTime.java index fef24b789ba..acdf3fc2360 100644 --- a/src/main/java/seedu/address/model/person/DateTime.java +++ b/src/main/java/seedu/address/model/person/DateTime.java @@ -12,7 +12,8 @@ * Guarantees: immutable; dateTime is valid as declared in {@link #isValidDateTime(String)} */ public class DateTime { - public static final String MESSAGE_CONSTRAINTS = "DateTime should be in the format yyyy-mm-dd hhmm"; + public static final String MESSAGE_CONSTRAINTS = "DateTime should be in the format yyyy-mm-dd hhmm and" + + " a valid date and time"; public static final String VALIDATION_REGEX = "\\d{4}-\\d{2}-\\d{2} \\d{4}"; public static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("uuuu-MM-dd HHmm") .withResolverStyle(ResolverStyle.STRICT); diff --git a/src/main/java/seedu/address/model/person/GradeSubjectFilterPredicate.java b/src/main/java/seedu/address/model/person/GradeSubjectFilterPredicate.java index bd974a49ebb..7e50cadc3a5 100644 --- a/src/main/java/seedu/address/model/person/GradeSubjectFilterPredicate.java +++ b/src/main/java/seedu/address/model/person/GradeSubjectFilterPredicate.java @@ -36,6 +36,16 @@ public boolean test(Person person) { return isGradeFiltered && isSubjectFiltered; } + /** + * Returns a String representation of the filter result for GUI display. + * + * @return Filter result. + */ + public String filterResult() { + return "Grade: " + filteredGrade.toString() + "," + + "Subject: " + filteredSubject.toString(); + } + @Override public boolean equals(Object other) { if (other == this) { diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java index 9e29e584ab7..7a6fa0fb011 100644 --- a/src/main/java/seedu/address/model/person/Person.java +++ b/src/main/java/seedu/address/model/person/Person.java @@ -103,6 +103,18 @@ public boolean isSamePerson(Person otherPerson) { && otherPerson.getName().equals(getName()); } + /** + * Returns true if both persons have the same set of datetime. + */ + public boolean isSameDateTime(Person otherPerson) { + if (otherPerson == this) { + return true; + } + + return otherPerson != null + && otherPerson.getDateTimes().equals(getDateTimes()); + } + /** * Returns true if both persons have the same identity and data fields. * This defines a stronger notion of equality between two persons. diff --git a/src/main/java/seedu/address/model/person/UniquePersonList.java b/src/main/java/seedu/address/model/person/UniquePersonAndDateTimeList.java similarity index 76% rename from src/main/java/seedu/address/model/person/UniquePersonList.java rename to src/main/java/seedu/address/model/person/UniquePersonAndDateTimeList.java index cc0a68d79f9..c4b3852fbf9 100644 --- a/src/main/java/seedu/address/model/person/UniquePersonList.java +++ b/src/main/java/seedu/address/model/person/UniquePersonAndDateTimeList.java @@ -5,9 +5,11 @@ import java.util.Iterator; import java.util.List; +import java.util.Set; import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import seedu.address.model.person.exceptions.DuplicateDateTimeException; import seedu.address.model.person.exceptions.DuplicatePersonException; import seedu.address.model.person.exceptions.PersonNotFoundException; @@ -15,14 +17,14 @@ * A list of persons that enforces uniqueness between its elements and does not allow nulls. * A person is considered unique by comparing using {@code Person#isSamePerson(Person)}. As such, adding and updating of * persons uses Person#isSamePerson(Person) for equality so as to ensure that the person being added or updated is - * unique in terms of identity in the UniquePersonList. However, the removal of a person uses Person#equals(Object) so - * as to ensure that the person with exactly the same fields will be removed. + * unique in terms of identity in the UniquePersonAndDateTimeList. However, the removal of a person uses + * Person#equals(Object) so as to ensure that the person with exactly the same fields will be removed. * * Supports a minimal set of list operations. * * @see Person#isSamePerson(Person) */ -public class UniquePersonList implements Iterable { +public class UniquePersonAndDateTimeList implements Iterable { private final ObservableList internalList = FXCollections.observableArrayList(); private final ObservableList internalUnmodifiableList = @@ -36,15 +38,32 @@ public boolean contains(Person toCheck) { return internalList.stream().anyMatch(toCheck::isSamePerson); } + /** + * Returns true if the list contains an equivalent DateTime as the given argument. + */ + public boolean containsDateTime(Person toCheck) { + requireNonNull(toCheck); + Set toCheckDateTime = toCheck.getDateTimes(); + + return internalList.stream() + .flatMap(person -> person.getDateTimes().stream()) + .anyMatch(toCheckDateTime::contains); + } + /** * Adds a person to the list. * The person must not already exist in the list. + * The person must not already have an existing datetime in the list. */ public void add(Person toAdd) { requireNonNull(toAdd); if (contains(toAdd)) { throw new DuplicatePersonException(); } + + if (containsDateTime(toAdd)) { + throw new DuplicateDateTimeException(); + } internalList.add(toAdd); } @@ -79,7 +98,7 @@ public void remove(Person toRemove) { } } - public void setPersons(UniquePersonList replacement) { + public void setPersons(UniquePersonAndDateTimeList replacement) { requireNonNull(replacement); internalList.setAll(replacement.internalList); } @@ -116,12 +135,12 @@ public boolean equals(Object other) { } // instanceof handles nulls - if (!(other instanceof UniquePersonList)) { + if (!(other instanceof UniquePersonAndDateTimeList)) { return false; } - UniquePersonList otherUniquePersonList = (UniquePersonList) other; - return internalList.equals(otherUniquePersonList.internalList); + UniquePersonAndDateTimeList otherUniquePersonAndDateTimeList = (UniquePersonAndDateTimeList) other; + return internalList.equals(otherUniquePersonAndDateTimeList.internalList); } @Override diff --git a/src/main/java/seedu/address/model/person/exceptions/DuplicateDateTimeException.java b/src/main/java/seedu/address/model/person/exceptions/DuplicateDateTimeException.java new file mode 100644 index 00000000000..a63433eeff0 --- /dev/null +++ b/src/main/java/seedu/address/model/person/exceptions/DuplicateDateTimeException.java @@ -0,0 +1,11 @@ +package seedu.address.model.person.exceptions; + +/** + * Signals that the operation will result in duplicate DateTime. + */ +public class DuplicateDateTimeException extends RuntimeException { + public DuplicateDateTimeException() { + super("Operation would result in 2 student having the same tutoring slot"); + } +} + diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java index 35992a394d2..89763c1d8ba 100644 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ b/src/main/java/seedu/address/model/util/SampleDataUtil.java @@ -35,23 +35,23 @@ public static Person[] getSamplePersons() { new Person(new Name("Bernice Yu"), new Phone("99272758"), new Email("berniceyu@example.com"), new Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), EMPTY_GRADE, EMPTY_SUBJECT, new Attendance("Present"), new Payment("Paid"), - getDateTimeSet("2024-03-02 1800", "2024-03-04 2000"), getTagSet("colleagues", "friends")), + getDateTimeSet("2024-03-02 1900", "2024-03-04 2000"), getTagSet("colleagues", "friends")), new Person(new Name("Charlotte Oliveiro"), new Phone("93210283"), new Email("charlotte@example.com"), new Address("Blk 11 Ang Mo Kio Street 74, #11-04"), EMPTY_GRADE, EMPTY_SUBJECT, new Attendance("Present"), new Payment("Paid"), - getDateTimeSet("2024-03-02 1800", "2024-03-21 1600"), getTagSet("neighbours")), + getDateTimeSet("2024-03-02 2100", "2024-03-21 2200"), getTagSet("neighbours")), new Person(new Name("David Li"), new Phone("91031282"), new Email("lidavid@example.com"), new Address("Blk 436 Serangoon Gardens Street 26, #16-43"), EMPTY_GRADE, EMPTY_SUBJECT, new Attendance("Present"), new Payment("Paid"), - getDateTimeSet("2024-03-14 2100"), getTagSet("family")), + getDateTimeSet("2024-03-14 2300"), getTagSet("family")), new Person(new Name("Irfan Ibrahim"), new Phone("92492021"), new Email("irfan@example.com"), new Address("Blk 47 Tampines Street 20, #17-35"), EMPTY_GRADE, EMPTY_SUBJECT, new Attendance("Present"), new Payment("Paid"), - getDateTimeSet("2024-03-18 2200"), getTagSet("classmates")), + getDateTimeSet("2024-03-18 0100"), getTagSet("classmates")), new Person(new Name("Roy Balakrishnan"), new Phone("92624417"), new Email("royb@example.com"), new Address("Blk 45 Aljunied Street 85, #11-31"), EMPTY_GRADE, EMPTY_SUBJECT, new Attendance("Present"), new Payment("Paid"), - getDateTimeSet("2024-03-28 2300"), getTagSet("colleagues")) + getDateTimeSet("2024-03-28 0200"), getTagSet("colleagues")) }; } diff --git a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java index 5efd834091d..c8b479089a2 100644 --- a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java +++ b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java @@ -21,6 +21,8 @@ class JsonSerializableAddressBook { public static final String MESSAGE_DUPLICATE_PERSON = "Persons list contains duplicate person(s)."; + public static final String MESSAGE_DUPLICATE_DATETIME = "Persons list contains duplicate datetime(s)."; + private final List persons = new ArrayList<>(); /** @@ -52,6 +54,9 @@ public AddressBook toModelType() throws IllegalValueException { if (addressBook.hasPerson(person)) { throw new IllegalValueException(MESSAGE_DUPLICATE_PERSON); } + if (addressBook.hasDateTime(person)) { + throw new IllegalValueException(MESSAGE_DUPLICATE_DATETIME); + } addressBook.addPerson(person); } return addressBook; diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java index 79e74ef37c0..bb06dd70856 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/address/ui/MainWindow.java @@ -8,6 +8,7 @@ import javafx.scene.control.TextInputControl; import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyEvent; +import javafx.scene.layout.FlowPane; import javafx.scene.layout.StackPane; import javafx.stage.Stage; import seedu.address.commons.core.GuiSettings; @@ -34,7 +35,10 @@ public class MainWindow extends UiPart { private PersonListPanel personListPanel; private ResultDisplay resultDisplay; private HelpWindow helpWindow; + private ViewWindow viewWindow; + @FXML + private FlowPane calendar; @FXML private StackPane commandBoxPlaceholder; @@ -66,6 +70,7 @@ public MainWindow(Stage primaryStage, Logic logic) { setAccelerators(); helpWindow = new HelpWindow(); + viewWindow = new ViewWindow(logic); } public Stage getPrimaryStage() { @@ -147,7 +152,20 @@ public void handleHelp() { } } + /** + * Opens the view schedule window or focuses on it if it's already opened. + */ + @FXML + public void handleView() { + if (!viewWindow.isShowing()) { + viewWindow.show(); + } else { + viewWindow.focus(); + } + } + void show() { + primaryStage.setMaxWidth(400); primaryStage.show(); } @@ -159,6 +177,7 @@ private void handleExit() { GuiSettings guiSettings = new GuiSettings(primaryStage.getWidth(), primaryStage.getHeight(), (int) primaryStage.getX(), (int) primaryStage.getY()); logic.setGuiSettings(guiSettings); + viewWindow.hide(); helpWindow.hide(); primaryStage.hide(); } @@ -182,6 +201,10 @@ private CommandResult executeCommand(String commandText) throws CommandException handleHelp(); } + if (commandResult.isView()) { + handleView(); + } + if (commandResult.isExit()) { handleExit(); } diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java index 2df9ee24c17..2741f423e12 100644 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ b/src/main/java/seedu/address/ui/PersonCard.java @@ -8,6 +8,7 @@ import javafx.scene.control.Label; import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; import javafx.scene.layout.Region; import seedu.address.model.person.Person; @@ -15,8 +16,15 @@ * An UI component that displays information of a {@code Person}. */ public class PersonCard extends UiPart { - private static final String FXML = "PersonListCard.fxml"; + private static final String PHONE_DESCRIPTION = "Phone: "; + private static final String ADDRESS_DESCRIPTION = "Address: "; + private static final String EMAIL_DESCRIPTION = "Email: "; + private static final String SUBJECT_DESCRIPTION = "Subject: "; + private static final String GRADE_DESCRIPTION = "Grade: "; + private static final String ATTENDANCE_DESCRIPTION = "Last class attendance: "; + private static final String PAYMENT_DESCRIPTION = "Current monthly fees status: "; + private static final String CELL_SMALL_LABEL_CLASS = "cell_small_label"; /** * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. @@ -35,19 +43,23 @@ public class PersonCard extends UiPart { @FXML private Label id; @FXML - private Label phone; + private HBox phone; + @FXML + private HBox address; @FXML - private Label address; + private HBox email; @FXML - private Label email; + private HBox subject; @FXML - private FlowPane subjectWithGrade; + private HBox grade; @FXML - private Label attendance; + private HBox attendance; @FXML - private Label payment; + private HBox payment; @FXML - private FlowPane dateTimes; + private HBox dateTimes; + @FXML + private Label dateTimeDescription; @FXML private FlowPane tags; @@ -59,22 +71,41 @@ public PersonCard(Person person, int displayedIndex) { this.person = person; id.setText(displayedIndex + ". "); name.setText(person.getName().fullName); - phone.setText(person.getPhone().value); - address.setText(person.getAddress().value); - email.setText(person.getEmail().value); - subjectWithGrade.getChildren().add(new Label(person.getSubject().value)); - subjectWithGrade.getChildren().add(new Label(person.getGrade().value)); - attendance.setText(person.getAttendance().value); - payment.setText(person.getPayment().value); - dateTimes.setHgap(5); - person.getDateTimes().stream() - .sorted(Comparator.comparing(dateTime -> dateTime.value)) - .forEach(dateTime -> dateTimes.getChildren() - .add(new Label(LocalDateTime.parse(dateTime.value, - DateTimeFormatter.ofPattern("uuuu-MM-dd HHmm")) - .format(DateTimeFormatter.ofPattern("MMM d uuuu h:mma"))))); person.getTags().stream() .sorted(Comparator.comparing(tag -> tag.tagName)) .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); + setField(phone, PHONE_DESCRIPTION, person.getPhone().value); + setField(address, ADDRESS_DESCRIPTION, person.getAddress().value); + setField(email, EMAIL_DESCRIPTION, person.getEmail().value); + setField(attendance, ATTENDANCE_DESCRIPTION, person.getAttendance().value); + setField(payment, PAYMENT_DESCRIPTION, person.getPayment().value); + setField(subject, SUBJECT_DESCRIPTION, person.getSubject().value); + setField(grade, GRADE_DESCRIPTION, person.getGrade().value); + person.getDateTimes().stream() + .sorted(Comparator.comparing(dateTime -> dateTime.value)) + .forEach(dateTime -> { + Label dateTimeLabel = new Label(LocalDateTime.parse(dateTime.value, + DateTimeFormatter.ofPattern("uuuu-MM-dd HHmm")) + .format(DateTimeFormatter.ofPattern("MMM d uuuu h:mma"))); + dateTimeLabel.getStyleClass().add(CELL_SMALL_LABEL_CLASS); + dateTimes.getChildren() + .add(dateTimeLabel); + }); + + } + + public void setField(HBox hbox, String description, String value) { + Label descriptionLabel = new Label(description); + descriptionLabel.getStyleClass().add(CELL_SMALL_LABEL_CLASS); + HBox.setHgrow(descriptionLabel, Priority.NEVER); + + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + + + Label valueLabel = new Label(value); + valueLabel.getStyleClass().add(CELL_SMALL_LABEL_CLASS); + HBox.setHgrow(valueLabel, Priority.NEVER); + hbox.getChildren().addAll(descriptionLabel, spacer, valueLabel); } } diff --git a/src/main/java/seedu/address/ui/ViewWindow.java b/src/main/java/seedu/address/ui/ViewWindow.java new file mode 100644 index 00000000000..711580496bd --- /dev/null +++ b/src/main/java/seedu/address/ui/ViewWindow.java @@ -0,0 +1,256 @@ +package seedu.address.ui; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.logging.Logger; + +import org.apache.commons.lang3.tuple.Pair; + +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.geometry.Pos; +import javafx.scene.control.ScrollPane; +import javafx.scene.layout.FlowPane; +import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Color; +import javafx.scene.shape.Rectangle; +import javafx.scene.text.Font; +import javafx.scene.text.FontWeight; +import javafx.scene.text.Text; +import javafx.stage.Stage; +import seedu.address.commons.core.LogsCenter; +import seedu.address.logic.Logic; +import seedu.address.model.person.DateTime; +import seedu.address.model.person.Person; + +/** + * Controller for a view schedule page + */ +public class ViewWindow extends UiPart { + private static final Logger logger = LogsCenter.getLogger(ViewWindow.class); + private static final String FXML = "Calendar.fxml"; + private final Logic logic; + private LocalDateTime dateFocus; + private LocalDateTime today; + + @FXML + private Text year; + + @FXML + private Text month; + + @FXML + private FlowPane calendar; + + /** + * Creates a {@code viewWindow} with the given {@code Stage} and {@code Logic}. + */ + public ViewWindow(Stage root, Logic logic) { + super(FXML, root); + this.logic = logic; + dateFocus = LocalDateTime.now(); + today = LocalDateTime.now(); + drawCalendar(); + } + + public ViewWindow(Logic logic) { + this(new Stage(), logic); + } + + /** + * Shows the view schedule window. + * @throws IllegalStateException + *
    + *
  • + * if this method is called on a thread other than the JavaFX Application Thread. + *
  • + *
  • + * if this method is called during animation or layout processing. + *
  • + *
  • + * if this method is called on the primary stage. + *
  • + *
  • + * if {@code dialogStage} is already showing. + *
  • + *
+ */ + public void show() { + logger.fine("Showing schedule."); + getRoot().show(); + getRoot().centerOnScreen(); + } + + public boolean isShowing() { + return getRoot().isShowing(); + } + + public void hide() { + getRoot().hide(); + } + + public void focus() { + getRoot().requestFocus(); + } + + @FXML + void backOneMonth(ActionEvent event) { + dateFocus = dateFocus.minusMonths(1); + calendar.getChildren().clear(); + drawCalendar(); + } + + @FXML + void forwardOneMonth(ActionEvent event) { + dateFocus = dateFocus.plusMonths(1); + calendar.getChildren().clear(); + drawCalendar(); + } + + private void drawCalendar() { + year.setText(String.valueOf(dateFocus.getYear())); + month.setText(String.valueOf(dateFocus.getMonth())); + + double calendarWidth = calendar.getPrefWidth(); + double calendarHeight = calendar.getPrefHeight(); + double strokeWidth = 1; + double spacingH = calendar.getHgap(); + double spacingV = calendar.getVgap(); + + Map>>> mapDaysToSessions = createSessionMap(dateFocus); + + int monthMaxDate = getMonthMaxDate(); + int dateOffset = LocalDateTime.of(dateFocus.getYear(), dateFocus.getMonthValue(), 1, 0, 0, 0, 0) + .getDayOfWeek().getValue(); + + for (int i = 0; i < 6; i++) { + for (int j = 0; j < 7; j++) { + StackPane stackPane = new StackPane(); + Rectangle rectangle = new Rectangle(); + rectangle.setFill(Color.TRANSPARENT); + rectangle.setStroke(Color.WHITE); + rectangle.setStrokeWidth(strokeWidth); + double rectangleWidth = (calendarWidth / 7) - strokeWidth - spacingH; + rectangle.setWidth(rectangleWidth); + double rectangleHeight = (calendarHeight / 6) - strokeWidth - spacingV; + rectangle.setHeight(rectangleHeight); + stackPane.getChildren().add(rectangle); + + int calculatedDate = (j + 1) + (7 * i); + if (calculatedDate > dateOffset) { + int currentDate = calculatedDate - dateOffset; + if (currentDate <= monthMaxDate) { + Text date = new Text(String.valueOf(currentDate)); + date.setFill(Color.WHITE); + double textTranslationY = -(rectangleHeight / 2) * 0.75; + date.setTranslateY(textTranslationY); + stackPane.getChildren().add(date); + + TreeSet>> tutorSessions = mapDaysToSessions.get(currentDate); + if (tutorSessions != null) { + createSessionsOnDay(tutorSessions, rectangleHeight, rectangleWidth, stackPane); + } + } + if (today.getYear() == dateFocus.getYear() && today.getMonth() == dateFocus.getMonth() + && today.getDayOfMonth() == currentDate) { + rectangle.setStroke(Color.BLUE); + } + } + calendar.getChildren().add(stackPane); + } + } + } + + private void createSessionsOnDay(TreeSet>> tutorSessions, double rectangleHeight, + double rectangleWidth, StackPane stackPane) { + VBox sessionsBox = new VBox(); + for (Pair> personAndTime : tutorSessions) { + Person person = personAndTime.getLeft(); + List hourAndMinute = personAndTime.getRight(); + String min = hourAndMinute.get(1) < 10 ? "0" + hourAndMinute.get(1) : "" + hourAndMinute.get(1); + Text text = new Text("Name: " + person.getName() + "\n" + "Address: " + person.getAddress() + + "\n" + "Subject: " + person.getSubject() + "\n" + "Time: " + + hourAndMinute.get(0) + ":" + min + "\n"); + text.setFont(Font.font("Segoe UI Light", FontWeight.THIN, 12)); + text.setStroke(Color.WHITE); + text.setStrokeWidth(1); + text.setFill(Color.WHITE); + text.setWrappingWidth(rectangleWidth * 0.9); + sessionsBox.getChildren().add(text); + } + sessionsBox.setStyle("-fx-background-color: #515658; -fx-background-radius: 5;"); + sessionsBox.setMaxWidth(rectangleWidth); + sessionsBox.setAlignment(Pos.TOP_LEFT); + + ScrollPane scrollPane = new ScrollPane(); + scrollPane.setContent(sessionsBox); + scrollPane.setFitToWidth(true); + scrollPane.setPrefSize(rectangleWidth, rectangleHeight); + scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER); + + stackPane.getChildren().add(scrollPane); + + } + + private Map>>> createSessionMap(LocalDateTime dateFocus) { + List persons = logic.getFilteredPersonList(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HHmm"); + + Map>>> mapDaysToSessions = new HashMap<>(); + + for (Person person: persons) { + Set dateTimes = person.getDateTimes(); + + for (DateTime dateTimeStr : dateTimes) { + LocalDateTime dateTime = LocalDateTime.parse(dateTimeStr.value, formatter); + int year = dateTime.getYear(); + int month = dateTime.getMonthValue(); + int day = dateTime.getDayOfMonth(); + int hour = dateTime.getHour(); + int minute = dateTime.getMinute(); + + if (year > dateFocus.getYear() || (year == dateFocus.getYear() && month > dateFocus.getMonthValue())) { + break; + } + + if (year == dateFocus.getYear() && month == dateFocus.getMonthValue()) { + TreeSet>> sessionSet = mapDaysToSessions.computeIfAbsent(day, + k -> new TreeSet<>(new HourMinuteComparator())); + Pair> personAndTime = Pair.of(person, List.of(hour, minute)); + sessionSet.add(personAndTime); + mapDaysToSessions.put(day, sessionSet); + } + } + } + return mapDaysToSessions; + } + + private int getMonthMaxDate() { + int monthMaxDate = dateFocus.getMonth().maxLength(); + if (dateFocus.getYear() % 4 != 0 && monthMaxDate == 29) { + monthMaxDate = 28; + } + return monthMaxDate; + } + + private static class HourMinuteComparator implements Comparator>> { + @Override + public int compare(Pair> pair1, Pair> pair2) { + int hour1 = pair1.getRight().get(0); + int minute1 = pair1.getRight().get(1); + int hour2 = pair2.getRight().get(0); + int minute2 = pair2.getRight().get(1); + if (hour1 != hour2) { + return Integer.compare(hour1, hour2); + } + return Integer.compare(minute1, minute2); + } + } +} diff --git a/src/main/resources/view/Calendar.fxml b/src/main/resources/view/Calendar.fxml new file mode 100644 index 00000000000..ed60f97f3dd --- /dev/null +++ b/src/main/resources/view/Calendar.fxml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + +