diff --git a/README.md b/README.md index 16208adb9b6..4a8a9ac2e4e 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,23 @@ -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) - +[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/AY2425S2-CS2103T-W13-1/tp/actions) +# ScoopBook ![Ui](docs/images/Ui.png) -* This is **a sample project for Software Engineering (SE) students**.
- Example usages: - * as a starting point of a course project (as opposed to writing everything from scratch) - * as a case study -* The project simulates an ongoing software project for a desktop application (called _AddressBook_) used for managing contact details. - * It is **written in OOP fashion**. It provides a **reasonably well-written** code base **bigger** (around 6 KLoC) than what students usually write in beginner-level SE modules, without being overwhelmingly big. - * It comes with a **reasonable level of user and developer documentation**. -* It is named `AddressBook Level 3` (`AB3` for short) because it was initially created as a part of a series of `AddressBook` projects (`Level 1`, `Level 2`, `Level 3` ...). -* For the detailed documentation of this project, see the **[Address Book Product Website](https://se-education.org/addressbook-level3)**. -* This project is a **part of the se-education.org** initiative. If you would like to contribute code to this project, see [se-education.org](https://se-education.org/#contributing-to-se-edu) for more info. +# Overview +ScoopBook is a lightweight **desktop application** to manage your contacts. Specialised and catered to journalists seeking a faster and offline solution to efficiently manage their contacts. + +## Features +ScoopBook provides journalists with a simple and efficient way to manage their contacts. It allows users to: +* Add and manage contacts +* Tag contacts for better organisation +* Add notes for each contact to keep track of important information +* Import and export contacts for easy transfer + +For more information, check out the [ScoopBook Project Website](https://ay2425s2-cs2103t-w13-1.github.io/tp/). + +## Authors +Find out more about us on our [About Us](https://ay2425s2-cs2103t-w13-1.github.io/tp/AboutUs.html) page! + +## Acknowledgements +This project is based on the AddressBook-Level3 project created by the [SE-EDU initiative](https://se-education.org).
+We would also like to thank all professors, TAs, coursemates and friends who have helped and supported us throughout the project. + diff --git a/build.gradle b/build.gradle index 0db3743584e..948a0008b32 100644 --- a/build.gradle +++ b/build.gradle @@ -66,7 +66,11 @@ dependencies { } shadowJar { - archiveFileName = 'addressbook.jar' + archiveFileName = 'ScoopBook.jar' +} + +run { + enableAssertions = true } defaultTasks 'clean', 'test' diff --git a/docs/AboutUs.md b/docs/AboutUs.md index ff3f04abd02..6910a7ce787 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -5,55 +5,52 @@ title: About Us We are a team based in the [School of Computing, National University of Singapore](https://www.comp.nus.edu.sg). -You can reach us at the email `seer[at]comp.nus.edu.sg` +You can reach us at the email `shawn.goh@u.nus.edu` ## Project team -### John Doe +### Shawn Goh - + -[[homepage](http://www.comp.nus.edu.sg/~damithch)] -[[github](https://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[homepage](https://shawnnygoh.github.io/)] +[[github](https://github.com/shawnnygoh)] -* Role: Project Advisor +* Role: Deliverables and Deadlines -### Jane Doe +### Freddie Ong - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/souledfigurine)] -* Role: Team Lead -* Responsibilities: UI +* Role: Testing +* Responsibilities: unconfirmed -### Johnny Doe +### Chua Wen Ting - + -[[github](http://github.com/johndoe)] [[portfolio](team/johndoe.md)] +[[github](http://github.com/wentingchua)] * Role: Developer -* Responsibilities: Data +* Responsibilities: Scheduling and tracking: In charge of defining, assigning, and tracking project tasks. -### Jean Doe +### Soh Tze Jen - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/Meatsushi64)] -* Role: Developer -* Responsibilities: Dev Ops + Threading -### James Doe +* Role: Developer, Integration +* Responsibilities: In addition to developing, also in charge of versioning of the code, maintaining the code repository, integrating various parts of the software to create a whole. + +### Timothy Tay (tim0tay) - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/tim0tay)] * Role: Developer -* Responsibilities: UI +* Responsibilities: Code Quality diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 743c65a49d2..9d215647702 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -9,7 +9,9 @@ title: Developer Guide ## **Acknowledgements** -* {list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +* Libraries used: [_JavaFX_](https://openjfx.io/), [_JUnit5_](https://github.com/junit-team/junit5) +* This project is based on the AddressBook-Level3 project created by the [SE-EDU initiative](https://se-education.org). +* We would also like to thank all professors, TAs, PE Dry Run Testers, coursemates and friends who have helped and supported us throughout the project. -------------------------------------------------------------------------------------------------------------------- @@ -127,6 +129,9 @@ The `Model` component, * stores a `UserPref` object that represents the user’s preferences. This is exposed to the outside as a `ReadOnlyUserPref` objects. * does not depend on any of the other three components (as the `Model` represents data entities of the domain, they should make sense on their own without depending on other components) +The `Person`, +* requires a name, and at least one of the following fields: phone number, email, address. +
:information_source: **Note:** An alternative (arguably, a more OOP) model is given below. It has a `Tag` list in the `AddressBook`, which `Person` references. This allows `AddressBook` to only require one `Tag` object per unique tag, instead of each `Person` needing their own `Tag` objects.
@@ -155,6 +160,59 @@ Classes used by multiple components are in the `seedu.address.commons` package. This section describes some noteworthy details on how certain features are implemented. +### Notes feature + +The **Notes feature** allows users to write and save plain text notes for each contact. These notes can be opened in a separate window, edited at any time, and are saved to a file on the user's computer. This allows users to keep important information linked to specific people in their contact list. + +#### Implementation +The Notes system spans across the UI, Logic and Storage layers of the application. + +#### UI Layer +When the user executes the command `note INDEX`, the `MainWindow` receives the `CommandResult` from Logic containing the flag `isShowNote == true` and the `targetPerson`. It then calls: `handleNote(commandResult.getTargetPerson())` + +This method delegates to `NoteWindowHandler`, which manages the opening and closing of note windows. + +Each `NoteWindow` is uniquely associated with a `Person`. Only one note window can be opened per person at a time, tracked by a `Map`. + +When the window is initialized, `NoteWindow` calls `logic.readNote(person)` to load the existing note (if any), and displays it in a `TextArea`. When the window is closed, notes are automatically saved by calling `logic.saveNote(person, content)`. + +#### Logic Layer + +The `LogicManager` connects the UI and Storage layers for the Notes system. It exposes three key methods: +* `readNote(Person person)` +* `saveNote(Person person, String content)` +* `deleteNote(Person person)` + +These are called directly by the UI when a `NoteWindow` is opened, closed, or deleted. + +Additionally, `LogicManager` performs note cleanup when specific commands are executed: +* `DeleteCommand` → deletes the note for the removed person +* `ClearCommand` and `ImportCommand` → delete all notes + +These cleanup operations are handled in `handleNoteOperations(Command command)`, which runs after each command execution. + +Below are the sequence diagrams for `NoteCommand` and `DeleteNoteCommand` to illustrate the flow of events + +1. Sequence diagram for **NoteCommand**: + + +2. Sequence diagram for **DeleteNoteCommand**: + +#### Storage Layer + +The Notes system uses a dedicated `NotesStorage` interface, implemented by `FileNotesStorage`, to handle note persistence. + +Each note is saved as a `.txt` file in a designated notes directory, with filenames based on the `Person`'s unique ID (e.g., `12.txt`). This ensures that notes remain uniquely tied to each contact even if their name changes. + +The `FileNotesStorage` class provides three main methods: +* `readNote(Person person)` — Returns the note content as a string if the file exists, or an empty string otherwise. +* `saveNote(Person person, String content)` — Saves the note content to a file. If the file does not exist, it is created automatically. +* `deleteNote(Person person)` — Deletes the note file associated with the given person. + +For bulk operations, `deleteAllNotes()` removes all note files from the directory. +* triggered by `DeleteCommand`& `ClearCommand`. + + ### \[Proposed\] Undo/redo feature #### Proposed Implementation @@ -239,10 +297,6 @@ The following activity diagram summarizes what happens when a user executes a ne _{more aspects and alternatives to be added}_ -### \[Proposed\] Data archiving - -_{Explain here how the data archiving feature will be implemented}_ - -------------------------------------------------------------------------------------------------------------------- @@ -260,73 +314,537 @@ _{Explain here how the data archiving feature will be implemented}_ ### Product scope -**Target user profile**: +**Target user profile**: +* has a need to manage a significant number of contacts +* prefers desktop apps over other types +* can type fast +* prefers typing to mouse interactions +* is reasonably comfortable using CLI apps +* requires a way to categorise and group contacts easily +* requires a high level of privacy -* has a need to manage a significant number of contacts -* prefer desktop apps over other types -* can type fast -* prefers typing to mouse interactions -* is reasonably comfortable using CLI apps +**Value proposition**: manage contacts faster than a typical mouse/GUI-driven app, keep contacts safe -**Value proposition**: manage contacts faster than a typical mouse/GUI driven app - -### User stories +### User Stories Priorities: High (must have) - `* * *`, Medium (nice to have) - `* *`, Low (unlikely to have) - `*` -| Priority | As a …​ | I want to …​ | So that I can…​ | -| -------- | ------------------------------------------ | ------------------------------ | ---------------------------------------------------------------------- | -| `* * *` | new user | see usage instructions | refer to instructions when I forget how to use the App | -| `* * *` | user | add a new person | | -| `* * *` | user | delete a person | remove entries that I no longer need | -| `* * *` | user | find a person by name | locate details of persons without having to go through the entire list | -| `* *` | user | hide private contact details | minimize chance of someone else seeing them by accident | -| `*` | user with many persons in the address book | sort persons by name | locate a person easily | +| Priority | As a … | I want to … | So that I can… | +|----------|--------|------------|----------------| +| `* * *` | new user | see usage instructions | refer to instructions when I forget how to use the App | +| `* * *` | user | add a new contact | | +| `* * *` | user | delete a contact | remove them if they are no longer relevant to my investigations | +| `* * *` | user | find a person by name | locate details of persons without having to go through the entire list | +| `* * *` | user | tag contacts by credibility (e.g. verified, unverified, anonymous) | assess reliability quickly | +| `* * *` | user | search for a contact by tags (e.g. topic, organization) | quickly find relevant contacts | +| `* * *` | user | access previously saved contacts | contact people that I have saved the contacts of | +| `* * *` | user | keep my contacts locally on my device | maintain the privacy of my contacts | +| `* * *` | user | save the home address of a contact | know where to find them if required | +| `* *` | user | edit previously saved contacts | change contacts if they have any different information | +| `* *` | user | hide private contact details | minimize chance of someone else seeing them by accident | +| `* *` | user | export my saved contacts to a different device and have them read easily by the same program | transfer my files to different devices easily | +| `*` | user | log conversation notes with each contact | keep track of all my notes and critical information in one place | +| `*` | user | view a conversation note | refer back to previously recorded conversation note | +| `*` | user | delete conversation notes for each contact | delete conversation notes that are no longer relevant to prevent clutter | +| `*` | user | export my notes to a .txt / .pdf file | share information with my team | +| `*` | user | create keyboard shortcuts for frequently used actions | work faster | +| `*` | user | view my existing keyboard shortcuts | remind myself what keyboard shortcuts I currently have | +| `*` | user | delete an existing keyboard shortcut | update my keyboard shortcuts if I have a change of preference | +| `*` | user | copy important pieces of information quickly | call or email my contacts | +| `*` | user | see when a contact was saved | keep track of when I met the contact | +| `*` | user | set follow-up reminders | do not miss out on getting updates from my contacts | +| `*` | user | create investigations (groups) | group related contacts together | +| `*` | user | add a profile photo for contacts | remember their faces too | + + + +### Use Cases + +**Use case: Ask for help - UC1** + +**MSS** + +1. User asks for help regarding instructions. +2. ScoopBook directs User to a website with detailed instructions for each functionality. + + Use case ends. + +**Use case: Add a contact - UC2** + +**MSS** + +1. User requests to add a contact with information into the list. +2. ScoopBook adds contact into the list. + + Use case ends. + +**Extensions** -*{More to be added}* +* 2a. The input given by User is invalid. + * 2a1. ScoopBook shows an error message. -### Use cases + Use case resumes at step 1. -(For all use cases below, the **System** is the `AddressBook` and the **Actor** is the `user`, unless specified otherwise) -**Use case: Delete a person** +**Use case: View contacts - UC3** **MSS** -1. User requests to list persons -2. AddressBook shows a list of persons -3. User requests to delete a specific person in the list -4. AddressBook deletes the person +1. User requests to view list of contacts. +2. ScoopBook shows a list of contacts. - Use case ends. + Use case ends. **Extensions** -* 2a. The list is empty. +* 2a. Contact list is empty. Use case ends. -* 3a. The given index is invalid. - * 3a1. AddressBook shows an error message. + +**Use case: Delete a contact - UC4** + +**MSS** + +1. User requests to view contacts (UC3). +2. User requests to delete a specific contact in the list. +3. ScoopBook deletes the contact. + + Use case ends. + +**Extensions** + +* 2a. The given index is invalid, or the input is not a number. + * 2a1. ScoopBook shows an error message. + + Use case resumes at step 1. + + + +**Use case: Edit a contact - UC5** + +**MSS** + +1. User requests to view contacts (UC3). +2. User requests to edit a specific contact in the list. +3. ScoopBook edits that contact. + + Use case ends. + +**Extensions** + +* 2a. The given input is invalid. + * 2a1. ScoopBook shows an error message. + + Use case resumes at step 1. + + + +**Use case: Add tag(s) to a contact - UC6** + +**MSS** + +1. User requests to view contacts (UC3). +2. User requests to add tags to a specific contact in the list. +3. ScoopBook adds tags to that contact. + + Use case ends. + +**Extensions** + +* 2a. User provides an invalid input. + * 2a1. ScoopBook shows an error message. + + Use case resumes at step 1. + + + +**Use case: Remove tag(s) to a contact - UC7** + +**MSS** + +1. User requests to view contacts (UC3). +2. User requests to remove tag(s) from a specific contact in the list. +3. ScoopBook removes the specified tags from the specified contact. + + Use case ends. + +**Extensions** + +* 2a. The given index is invalid, or the input tag does not exist. + * 2a1. ScoopBook shows an error message. + + Use case resumes at step 1. + + + +**Use case: Search contact by name - UC8** + +**MSS** + +1. User requests to search for contacts based on keywords. +2. ScoopBook displays a list of contacts whose name matches the keywords. + + Use case ends. + +**Extensions** + +* 2a. No contact matches with the keywords. + * 2a1. ScoopBook shows an error message. + + Use case resumes at step 1. + + + +**Use case: Search contact by tag - UC9** + +**MSS** + +1. User requests to search for contacts based on tags. +2. ScoopBook displays a list of contacts that match the tags. + + Use case ends. + +**Extensions** + +* 2a. No contact matches with the tags. + * 2a1. ScoopBook shows an error message. + + Use case resumes at step 1. + + + +**Use case: Export contacts - UC10** + +**MSS** + +1. User requests to export contacts to a specified file location. +2. ScoopBook saves the relevant contacts in the export file. + + Use case ends. + +**Extensions** + +* 1a. User provided invalid file location. + * 1a1. Scoopbook raises an error + + Use case ends. + + + +**Use case: Import contacts - UC11** + +**MSS** + +1. User requests to import contacts from a file location. +2. ScoopBook imports the contacts from the import file. + + Use case ends. + +**Extensions** + +* 1a. User provided invalid file location. + * 1a1. Scoopbook raises an error + + Use case ends. + +* 1b. User provided file with invalid format + * 1b1. Scoopbook raises an error + + Use case ends. + + + +**Use case: Create investigation - UC12 (To be implemented)** + +**MSS** + +1. User requests to create an investigation. +2. ScoopBook creates the investigation. + + Use case ends. + +**Extensions** + +* 1a. The name of the investigation already exists. + * 1a1. ScoopBook informs User that there is already an investigation with the same name. + + Use case ends. + + + +**Use case: Add contact to investigation - UC13 (To be implemented)** + +**MSS** + +1. User requests to add a contact to an existing investigation. +2. ScoopBook adds the contact to the specified investigation. + + Use case ends. + +**Extensions** + +* 1a. The investigation specified by User does not exist. + * 1a1. ScoopBook informs User that no such investigation exists. + + Use case ends. + +* 1b. The contact specified by User does not exist. + * 1b1. ScoopBook informs User that no such contact exists. + + Use case ends. + +* 1c. Multiple contacts contain the keywords specified by User. + * 1c1. ScoopBook informs User that there are duplicates, and that the operation cannot be performed. + + Use case ends. + + + +**Use case: Remove contact from investigation - UC14 (To be implemented)** + +**MSS** + +1. User requests to remove a contact from an existing investigation. +2. ScoopBook removes the contact from the specified investigation. + + Use case ends. + +**Extensions** + +* 1a. The investigation specified by User does not exist. + * 1a1. ScoopBook informs User that no such investigation exists. + + Use case ends. + +* 1b. The contact specified by User does not exist in the investigation. + * 1b1. ScoopBook informs User that no such contact exists. + + Use case ends. + +* 1c. Multiple contacts or investigations contain the keywords specified by User. + * 1c1. ScoopBook informs User that there are duplicates, and that the operation cannot be performed. + + Use case ends. + + + +**Use case: View and edit specific conversation note - UC15** + +**MSS** + +1. User requests to view contacts (UC3). +2. User requests to view a specific conversation note from a specific contact. +3. ScoopBook displays the specified conversation note to edit. + + Use case ends. + +**Extensions** + +* 2a. The given conversation note index or contact index is invalid. + * 2a1. ScoopBook shows an error message. + + Use case resumes at step 1. + + + +**Use case: Delete conversation note - UC16** + +**MSS** + +1. User requests to view contacts (UC3). +2. User requests to delete a specific conversation note by index. +3. ScoopBook deletes the conversation note. + + Use case ends. + +**Extensions** + +* 2a. User provides an invalid input. + * 2a1. ScoopBook shows an error message. + + Use case resumes at step 1. + + + +**Use case: Export note - UC17 (To be implemented)** + +**MSS** + +1. User requests to view contact (UC3). +2. User requests to export notes by index or export all for a specific contact. +3. ScoopBook prompts the location to save the export file of notes in. +4. User confirms the save location. +5. ScoopBook saves the relevant notes in the export file. + + Use case ends. + +**Extensions** + +* 2a. The given contact index or notes indexes are invalid. + * 2a1. ScoopBook shows an error message. + + Use case resumes at step 1. + +* 3a. User decides to cancel the operation. + + Use case resumes at step 1. + + + +**Use case: View keyboard shortcuts - UC18 (To be implemented)** + +**MSS** + +1. User requests to view keyboard shortcuts. +2. ScoopBook displays all actions with keyboard shortcuts with their respective shortcuts. + + Use case ends. + +**Extensions** + +* 2a. Keyboard shortcut list is empty. + * 2a1. ScoopBook displays an empty list. + + Use case ends. + + + +**Use case: Create keyboard shortcuts - UC19 (To be implemented)** + +**MSS** + +1. User requests to view keyboard shortcuts (UC18). +2. User requests to create keyboard shortcuts. +3. ScoopBook displays available actions that can be assigned to a shortcut. +4. User selects a specific action and specifies a key combination. +5. ScoopBook assigns the shortcut. + + Use case ends. + +**Extensions** + +* 4a. User provides an invalid action index. + * 4a1. ScoopBook shows an error message. + + Use case resumes at step 3. + +* 4b. User provides an invalid or existing shortcut combination. + * 4b1. ScoopBook shows an error message. + + Use case resumes at step 3. + + + +**Use case: Delete keyboard shortcuts - UC20 (To be implemented)** + +**MSS** + +1. User requests to view keyboard shortcuts (UC18). +2. User requests to delete a specific keyboard shortcut in the list. +3. ScoopBook deletes the specific keyboard shortcut. + + Use case ends. + +**Extensions** + +* 2a. User provides an invalid index. + * 2a1. ScoopBook shows an error message. + + Use case resumes at step 1. + + + +**Use case: Set follow-up reminder - UC21 (To be implemented)** + +**MSS** + +1. User requests to view contacts (UC3). +2. User requests to set a follow-up reminder to specific contact. +3. ScoopBook sets a follow-up reminder. + + Use case ends. + +**Extensions** + +* 2a. User provides an invalid input. + * 2a1. ScoopBook shows an error message. + + Use case resumes at step 1. + + + +**Use case: Delete follow-up reminder - UC22 (To be implemented)** + +**MSS** + +1. User requests to view contacts (UC3). +2. User requests to delete a specific follow-up reminder to a specific contact. +3. ScoopBook deletes the follow-up reminder. + + Use case ends. + +**Extensions** + +* 2a. User provides an invalid input. + * 2a1. ScoopBook shows an error message. + + Use case resumes at step 1. + + + +**Use case: Add profile photo - UC23 (To be implemented)** + +**MSS** + +1. User requests to view contacts (UC3). +2. User requests to add a profile photo for a specific contact. +3. ScoopBook prompts the location to access the profile photo file. +4. User selects the profile photo. +5. ScoopBook saves the profile photo. + + Use case ends. + +**Extensions** + +* 2a. User provides an invalid input. + * 2a1. ScoopBook shows an error message. + + Use case resumes at step 1. + +* 4a. User selects an invalid file. + * 4a1. ScoopBook shows an error message. Use case resumes at step 2. -*{More to be added}* + ### Non-Functional Requirements -1. Should work on any _mainstream OS_ as long as it has Java `17` or above installed. -2. Should be able to hold up to 1000 persons without a noticeable sluggishness in performance for typical usage. -3. A user with above average typing speed for regular English text (i.e. not code, not system admin commands) should be able to accomplish most of the tasks faster using commands than using the mouse. - -*{More to be added}* +* Should work on any mainstream OS as long as it has Java 17 installed. +* Should be able to store up to 1000 persons with no more than 1s of response time between each command entered. +* A user with above-average typing speed (50 WPM) for regular English text (i.e., not code, not system admin commands) should be able to accomplish most basic tasks like adding contacts faster using commands than using the mouse. +* Should not require a login, since ScoopBook is on a user’s own device. +* All user data must be stored locally and should not require an internet connection for core functionality. +* The application must be developed using modular, well-documented code to support future feature additions and maintenance. ### Glossary -* **Mainstream OS**: Windows, Linux, Unix, MacOS -* **Private contact detail**: A contact detail that is not meant to be shared with others +**OS**: Operating System. A program that manages both hardware and software on a device. + +**CLI**: Command Line Interface. A mechanism through which users interact with their operating system. + +**CLI (-based) app**: An app primarily based on the key feature of the CLI, which is text-based typing. + +**GUI**: Graphical User Interface. A form of user interface that allows users to interact with electronic devices through graphical icons and visual indicators. + +**User**: The person using ScoopBook. + +**Attributes**: Relevant information that a contact may have. E.g., “high priority,” “whistleblower,” or “government official.” + +**Investigation**: A group of contacts that share some commonality. Users can create investigations and choose who to add to or remove from them. -------------------------------------------------------------------------------------------------------------------- @@ -345,7 +863,9 @@ testers are expected to do more *exploratory* testing. 1. Download the jar file and copy into an empty folder - 1. Double-click the jar file Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum. + 2. Using your computer's terminal, `cd` into the folder in the previous step. + + 3. Use `java -jar ScoopBook.jar` to open the application 1. Saving window preferences @@ -354,29 +874,210 @@ testers are expected to do more *exploratory* testing. 1. Re-launch the app by double-clicking the jar file.
Expected: The most recent window size and location is retained. -1. _{ more test cases …​ }_ +--- + +### Viewing Help + +**Test case:** `help` +**Expected:** Opens the help window with instructions on how to use the commands. + +**Test case:** `help abc` +**Expected:** Still opens the help window. Extraneous parameter is ignored. + +--- + +### Adding a person + +**Prerequisite:** App is launched. + +**Test case:** `add n/John Doe p/98765432 e/johnd@example.com a/123 John Street` +**Expected:** Adds John Doe to the contact list. Details shown in result display. + +**Test case:** `add n/Betsy Crowe t/friend e/betsycrowe@example.com a/Newgate Prison p/1234567 t/criminal t/silent` +**Expected:** Adds Betsy Crowe with multiple tags. + +**Test case:** `add n/Johnny Appleseed` +**Expected:** Error shown. Missing phone number, email, or address. + +**Test case:** `add n/John! Doe p/1234567` +**Expected:** Error shown. Name contains characters that are not accepted. Acceptable characters: alphanumeric characters, whitespaces, `,`, `(`, `)`, `@`, `.`, `-`, `'`. + +--- + +### Listing all persons + +**Test case:** `list` +**Expected:** All persons currently in the address book are displayed. + +**Test case:** `list 123` +**Expected:** All persons currently in the address book are displayed. Extraneous parameters ignored. + +--- + +### Editing a person + +**Prerequisite:** At least 2 persons in the list. + +**Test case:** `edit 1 p/91234567 e/johndoe@example.com` +**Expected:** 1st person’s phone number and email are updated. + +**Test case:** `edit 2 n/Betsy Crower t/` +**Expected:** Updates name, clears all tags. + +**Test case:** `edit 2` +**Expected:** Error. No field provided. + +**Test case:** `edit 0 p/12345678` +**Expected:** Error. Index must be a positive integer. + +--- + +### Finding persons by name + +**Prerequisite:** Contact list currently contains people with the following names: `John Mary`, `John Doe`, `Alex Yeoh`, `David Lim`, `Hans Solo`. + +**Test case:** `find John` +**Expected:** Displays persons with names containing “John”. + +**Test case:** `find Alex David` +**Expected:** Displays any persons with name containing “Alex” or “David”. + +**Test case:** `find Han` +**Expected:** No match for "Hans". Partial matches not allowed. + +--- ### Deleting a person -1. Deleting a person while all persons are being shown +**Prerequisites:** Should have at least 1 contact in the menu. + +**Test case:** `delete 1` (after `list`) +**Expected:** Deletes first contact. + +**Test case:** `delete 0` +**Expected:** Error. Invalid index. + +**Test case:** `delete` +**Expected:** Error. Missing index. + +--- + +### Adding tags + +**Prerequisite:** Should have at least 1 contact in the menu. + +**Test case:** `addtag 1 t/friend t/neighbour` +**Expected:** Adds both tags to person at index 1. + +**Test case:** Following the previous test case, `addtag 1 t/friend!` +**Expected:** Error. Tag contains invalid character. + +--- + +### Removing tags + +**Prerequisite:** Should have at least 1 contact in the menu. The first contact should have `friend` as a tag. + +**Test case:** `removetag 1 t/friend` +**Expected:** Removes "friend" tag. + +**Test case:** Following the previous test case, `removetag 1 t/Friend` +**Expected:** No tag removed. Case mismatch. + +--- + +### Finding by tag + +**Prerequisite:** Should have at least 5 contacts in the menu. At least two contacts should have `friends` as a tag. At least one of the `friends` contacts should have a `neighbours` tag. - 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. +**Test case:** `findtag t/friends` +**Expected:** Displays persons with tag "friends", "Friends", etc. + +**Test case:** `findtag t/friends t/neighbours` +**Expected:** Only persons with both tags are displayed. + +--- + +### Notes + +**Prerequisite:** Should have at least 1 contact in the menu. + +**Test case:** `note 1` +**Expected:** Opens a note window for person at index 1. + +**Test case:** `note 0` +**Expected:** Error. Invalid index. + +--- + +### Deleting a note + +**Prerequisites:** Should have at least 1 contact in the menu. +**Test case:** `deletenote 1` +**Expected:** Deletes note from person at index 1. + +**Test case:** `deletenote` +**Expected:** Error. Missing index. + +--- - 1. Test case: `delete 1`
- Expected: First contact is deleted from the list. Details of the deleted contact shown in the status message. Timestamp in the status bar is updated. +### Clearing all entries - 1. Test case: `delete 0`
- Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. +**Test case:** `clear` +**Expected:** Delete all contacts and notes from the address book. - 1. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
- Expected: Similar to previous. +**Test case:** `clear abc` +**Expected:** Still clears all contacts and notes. Extraneous parameter ignored. -1. _{ more test cases …​ }_ +--- + +### Export + +**Test case:** `export Contacts.json` +**Expected:** Export the json file as Contacts.json in the root folder of where the .jar is located at. + +**Test case:** `export /invalid/path/Contacts.json` +**Expected:** Error shown. Invalid path. + +--- + +### Import + +**Test case:** `import Contacts.json` (valid exported file) +**Expected:** Replaces all contacts with imported data. Notes are deleted. + +**Test case:** `import corrupted.json` +**Expected:** Returns error message. + +--- + +### Saving + +**Prerequisite:** Should have at least 1 contact in the menu. + +**Test case:** `note 1`. Then, add random text. Then, close the note window. Enter `note 1` again. +**Expected:** Note should show the same random text shown earlier. + +**Test case:** `list`. Then, `addtag 1 t/colleague`. Use `exit` to exit the application. Launch the application again. +**Expected:** Upon opening the application, the first contact should have the tag `colleague`. + +--- + +### Exit + +**Test case:** `exit` +**Expected:** Application closes. + + +-------------------------------------------------------------------------------------------------------------------- -### Saving data +## **Appendix: Planned Enhancements** -1. Dealing with missing/corrupted data files +Team size: 5 - 1. _{explain how to simulate a missing/corrupted file, and the expected behavior}_ +1. **Removing placeholder values for Person:** Currently Person uses a placeholder value for optional fields in the contact. I.e., if Alex Yeoh has a phone number only, the email and address fields will be replaced with a placeholder value instead of being blank. We plan to wrap each field in an Optional class so that it is able to handle empty values rather than leaving it with a placeholder value. +2. **Person cards to be more visible for longer texts:** Currently, Person fields truncates long text fields (i.e., Name, Phone Number, Email, Address). For example, if the name exceeds a certain length, it will be truncated with a "...". It shall be updated in future iterations to be able to display the whole text field by using a scrolling mechanic to see the whole text field for all fields (Name, Phone Number, Email, Address, Tags). +3. **Standardise error messages for commands that use Index referencing:** Currently, commands that use index referencing do not return the same error message for different cases of index errors. Invalid cases include: out of bounds for addressbook, non-positive integers, non-integers. We plan to standardise the index error messages to the different cases of invalid index accordingly. +4. **Improve search commands**: `find` command currently uses an OR operator to search. (i.e. `find John Mary` will return contacts with names `John`, `Mary`, and `John Mary`, but `John Mary` does not appear as the first contact). `find` shall be updated to return results where the first contact is the most relevant contact (i.e Names with `John Mary`) followed by other contacts. +5. **Allow user to copy from display**: Currently, the display does not allow users to copy text from the display. This may cause inconvenience or introduce delays for journalists who may want to copy text from the display. We plan to introduce support for copying for ease of use. -1. _{ more test cases …​ }_ diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 27c2d1cf16c..c97c5238f5c 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -3,8 +3,29 @@ layout: page title: User Guide --- -AddressBook Level 3 (AB3) 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. +## :trophy: Our Goal +**ScoopBook** is built to help journalists efficiently manage the contacts of their sources, witnesses, and other key individuals they interact with on the job. + +## :bust_in_silhouette: Target Audience + +ScoopBook is designed for **journalists** who need a fast, efficient and offline way to manage sources and contacts. We assume that these journalists are comfortable with the keyboard, but **no technical background is required**. + +## :dart: Problems We’re Solving + +- Traditional contact apps (like those on mobile phones) often have clunky interfaces that make it difficult for journalists to handle large number of contacts, sources and witnesses. +- Journalists frequently juggle multiple tools just to do simple tasks (like saving a contact or jotting down notes) wasting valuable time when the next big scoop is on the line. + +## :mag: How does ScoopBook work? + +**ScoopBook** is a **desktop app** designed with journalists in mind. It combines the speed and precision of a **Command Line Interface (CLI)** with the ease of a **Graphical User Interface (GUI)**—so if you can type fast, you can work fast. + +With ScoopBook, you get: +- **Blazing-fast** contact entry: Offering a Typing-based interface. +- **Smart categorization** of contacts (e.g. sources, leads, officials) +- **Powerful, instant search** to find the right sources, right contacts, fast. + +## :memo: Table of Contents * Table of Contents {:toc} @@ -15,28 +36,28 @@ AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized fo 1. Ensure you have Java `17` or above installed in your Computer.
**Mac users:** Ensure you have the precise JDK version prescribed [here](https://se-education.org/guides/tutorials/javaInstallationMac.html). -1. Download the latest `.jar` file from [here](https://github.com/se-edu/addressbook-level3/releases). +2. Download the latest `.jar` file from [here](https://github.com/AY2425S2-CS2103T-W13-1/tp/releases). -1. Copy the file to the folder you want to use as the _home folder_ for your AddressBook. +3. Copy the file to the folder you want to use as the _home folder_ for your ScoopBook. -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.
+4. Open a command terminal, `cd` into the folder you put the jar file in, and use the `java -jar ScoopBook.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) -1. Type the command in the command box and press Enter to execute it. e.g. typing **`help`** and pressing Enter will open the help window.
- Some example commands you can try: +5. Type the command into the command box and press `Enter` to execute it. e.g. typing `help` and pressing `Enter` will open the help window.
+ Here are some example commands you can try: - * `list` : Lists all contacts. + * `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` : Adds a contact named `John Doe` to the address book. - * `delete 3` : Deletes the 3rd contact shown in the current list. + * `delete 3` : Deletes the 3rd contact shown in the current list. - * `clear` : Deletes all contacts. + * `clear` : Deletes all contacts. - * `exit` : Exits the app. + * `exit` : Exits the app. -1. Refer to the [Features](#features) below for details of each command. +6. Refer to the [Features](#features) below for details of each command. -------------------------------------------------------------------------------------------------------------------- @@ -46,6 +67,9 @@ AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized fo **:information_source: Notes about the command format:**
+* Commands are case-sensitive.
+ e.g. `LIST`command will not work. + * Words in `UPPER_CASE` are the parameters to be supplied by the user.
e.g. in `add n/NAME`, `NAME` is a parameter which can be used as `add n/John Doe`. @@ -66,117 +90,385 @@ AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized fo ### Viewing help : `help` -Shows a message explaning how to access the help page. - -![help message](images/helpMessage.png) - -Format: `help` - +Shows the user guide, containing instructions on how to use the command. +``` +help +``` + ### Adding a person: `add` Adds a person to the address book. -Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​` +``` +add n/NAME [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG] [t/MORE_TAGS] +``` -
:bulb: **Tip:** -A person can have any number of tags (including 0) + + +
+ +**:information_source: Note:**
+* The add command **must** have a name, and one of the following fields: phone number, email, address. + i.e. `add n/Johnny Appleseed` does not work because there is no phone number, email or address. +* A person can have any number of tags (including 0). +* A person's name can only contain alphanumeric characters (numbers or letters only), whitespaces, and the following special characters: `,`, `(`, `)`, `@`, `.`, `-`, `'`. +* A person's tags can only contain alphanumeric characters (numbers or letters only, no special characters). +* If a contact is added with the following values, they will not be displayed in the contact list, as they are used as internal placeholders: + - Phone Number: `000` + - Email: `unknown@example.com` + - Address: `Unknown address` + This ensures that every contact has a placeholder value for these fields if left empty.
-Examples: +:paperclip: Examples: * `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` -* `add n/Betsy Crowe t/friend e/betsycrowe@example.com a/Newgate Prison p/1234567 t/criminal` +* `add n/Betsy Crowe t/Witness e/betsycrowe@example.com a/Newgate Prison p/1234567 t/Criminal` ### Listing all persons : `list` Shows a list of all persons in the address book. -Format: `list` +``` +list +``` + ### Editing a person : `edit` -Edits an existing person in the address book. +Edits an existing person in the address book at specified index. -Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` +``` +edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG] [t/MORE_TAGS] +``` + +
+ +**:information_source: Note:**
* 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, …​ * 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 - specifying any tags after it. +* Similar to the `add` command, the aforementioned placeholder values will not be displayed in the contact list. +
+ +
-Examples: +:bulb: TIP: You can remove all the person’s tags by typing `edit INDEX t/` without specifying any tags after it. +
+ +:paperclip: 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 1 t/friends t/colleagues` Removes all existing tags of the 1st person, and sets the 1st person's tag to `friends` and `colleagues` only. * `edit 2 n/Betsy Crower t/` Edits the name of the 2nd person to be `Betsy Crower` and clears all existing tags. ### Locating persons by name: `find` Finds persons whose names contain any of the given keywords. -Format: `find KEYWORD [MORE_KEYWORDS]` +``` +find KEYWORD [MORE_KEYWORDS] +``` + + +
+**:information_source: Note:**
* The search is case-insensitive. e.g `hans` will match `Hans` * 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). e.g. `Hans Bo` will return `Hans Gruber`, `Bo Yang` +
-Examples: +:paperclip: Examples: * `find John` returns `john` and `John Doe` * `find alex david` returns `Alex Yeoh`, `David Li`
- ![result for 'find alex david'](images/findAlexDavidResult.png) ### Deleting a person : `delete` Deletes the specified person from the address book. -Format: `delete INDEX` +``` +delete INDEX +``` + + +
+**:information_source: Note:**
* Deletes 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, …​ +
-Examples: +:paperclip: 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. +### Adding tags to a contact: `addtag` + +Adds the tag(s) typed in to the specified person. + +``` +addtag INDEX t/TAG1 [t/MORE_TAGS] +``` + +
+ +**:information_source: Note:**
+* Adds the specified tags to 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, …​ +* Multiple tags in a single `addtag` command is supported. + i.e. `addtag 1 t/Witness t/HomeAffairs` will tag the 1st person with both "Witness" and "HomeAffairs". +* Tags can only contain alphanumeric characters (numbers or letters only, no special characters or spaces). +* Tags are case-sensitive. + i.e. `addtag 1 t/Witness` will add the tag "Witness" while `addtag 1 t/witness` will add the tag "witness". +
+ +:paperclip: Examples: +* `list` followed by `addtag 2 t/Education` tags the 2nd person with "Education" in the address book. +* `find Betsy` followed by `addtag 1 t/Victim` tags the 1st person in the results of the `find` command with "Victim". + +### Removing tag from a contact: `removetag` + +Removes the specified tag(s) from the specified person. + +``` +removetag INDEX t/TAG1 [t/MORE_TAGS] +``` + + +
+ +**:information_source: Note:**
+* Removes the specified tags from 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, …​ +* Multiple tags in a single `removetag` command is supported. i.e. `removetag 1 t/Witness t/Local` will remove both the "Witness" and "Local" tag for the 1st person. +* Tags are case-sensitive. The typed tag must match the tag on the person exactly. + i.e. `removetag 1 t/witness` will not remove the tag "Witness". +
+ +
+:bulb: TIP: Use `edit INDEX t/` instead to remove all tags for specified contact. +
+ +:paperclip: Examples: +* `list` followed by `removetag 2 t/friend` removes the "friend" tag from the 2nd person in the address book. +* `find Betsy` followed by `removetag 1 t/friend` removes the "friend" tag from the 1st person in the results of the `find` command. + +### Finding people with tags: `findtag` + +Find persons who have all the specified tags. + +``` +findtag t/TAG1 [t/MORE_TAGS] +``` + +
+ +**:information_source: Note:**
+* The searching of tags is case-insensitive. e.g `friends` will match `Friends` +* The order of the tags does not matter. i.e. As long as the person has the listed tags, they will be shown. +* Only the tags are searched. +* Only full words will be matched e.g. `Friend` will not match `Friends` +* Only persons matching all the tags will be returned (i.e. `AND` search). +
+ +:paperclip: Examples: +* `findtag t/witness` returns people with tag `witness`, `Witness`, `WitNeSs` (due to case insensitivity). +* `findtag t/Witness t/HomeAffairs` returns people with tag `Witness` **and** `HomeAffairs` only. + +### Opening Note for Person: `note` + +Open a window for the user to add notes to. +If the person at the specified `INDEX` already has a note, the note will be displayed and the user can edit it in the window. + +If no note exists for the person, a new note will be created and displayed in the window for editing.
+ +``` +note INDEX +``` + + +
+ +**:information_source: Note:**
+* Opens a window for the user to add notes to the person at the specified `INDEX`. + * :exclamation: Please use only this opened window to edit the note (see [#Known issues](#known-issues) section below) +* The index refers to the index number shown in the displayed person list. +* The index must be a positive integer 1, 2, 3, … +* The note will be saved when the window is closed. +
+ +:paperclip: Examples: +* `list` followed by `note 2` opens a note window for the 2nd person in the address book. +* `find Betsy` followed by `note 1` opens a note window for the 1st person in the results of the `find` command. + +### Deleting Note from Person: `deletenote` + +Deletes the note from the person. + +``` +deletenote INDEX +``` + + +
+ +**:information_source: Note:**
+* Deletes note for 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, …​ +
+ +:paperclip: Examples: +* `list` followed by `deletenote 2` deletes the note for the 2nd person in the address book, if the note exists. +* `find Betsy` followed by `deletenote 1` deletes the note for the 1st person in the results of the `find` command, if the note exists. + ### Clearing all entries : `clear` Clears all entries from the address book. -Format: `clear` +``` +clear +``` +
:exclamation: **Caution:** +This clears all contacts, notes & `.txt` files from the address book. +
+ +### Exporting your contacts: `export` + +Exports the contacts in a .json file to the target path. + +``` +export TARGET_PATH +``` + + +
+ +**:information_source: Note:**
+- The `export` command only exports your contacts. It does not export the notes tagged to them. +- Before executing the `export` command, add at least 1 contact using the `add` command. +- `export` command is case-insensitive. If `sAmPle.json` already exists (in the folder the `ScoopBook.jar` is located at), `export sample.json` will overwrite `sAmPle.json`. +- Ensure that there are no special characters (E.g. `*!<>`) or spaces in the `TARGET_PATH`. +
+ +
+:bulb: TIP: If you are running into issues with TARGET_PATH, use `export sample.json` to export it directly to the root folder with of the ScoopBook.jar file. Then, move the .json file to wherever you want it to be. +
+ +:paperclip: Examples: +* For Windows: `export C:/Users/username/Desktop/MyContacts.json` + * saves the json file as `MyContacts.json` in the `Users/username/Desktop` folder.
+ + +* For macOS: `export /Users/username/Desktop/MyContacts.json` + * saves the json file as `MyContacts.json` in the `Users/username/Desktop` folder.
+ + +* For Linux: `export /home/user/desktop/MyContacts.json` + * saves the json file as `MyContacts.json` in the `home/user/desktop` folder.
+ + +* For all OS: `export Contacts.json` + * saves the json file as `Contacts.json` in the root folder of where `ScoopBook.jar` is located at. + +### Importing your contacts: `import` + +Imports contacts from the external .json file located at the specified path into the application. + +``` +import TARGET_PATH +``` + + +
:exclamation: **Caution:** +This command overwrites existing contacts and remove all notes. +
+ +
+ +**:information_source: Note:**
+- Only import .json files exported using the `export` command. +- Ensure that there are no special characters (E.g. `*!<>`) or spaces in the `TARGET_PATH`. +
+ +:paperclip: Examples: +* For Windows: `import C:/Users/username/Desktop/MyContacts.json` imports the json file from `MyContacts.json` in the `Users/username/Desktop` folder. +* For macOS: `import /Users/username/Desktop/MyContacts.json` imports the json file from `MyContacts.json` in the `Users/username/Desktop` folder. +* For Linux: `import /home/user/desktop/MyContacts.json` imports the json file from `MyContacts.json` in the `home/user/desktop` folder. +* `import Contacts.json` imports the json file named `Contacts.json` from the root folder of where ScoopBook.jar is located at. ### Exiting the program : `exit` Exits the program. -Format: `exit` +``` +exit +``` ### Saving the data -AddressBook data are saved in the hard disk automatically after any command that changes the data. There is no need to save manually. +ScoopBook data are saved in the hard disk automatically after any command that changes the data. There is no need to save manually. ### Editing the data file -AddressBook data are saved automatically as a JSON file `[JAR file location]/data/addressbook.json`. Advanced users are welcome to update data directly by editing that data file. +ScoopBook data are saved automatically as a JSON file `[JAR file location]/data/addressbook.json`. Advanced users are welcome to update data directly by editing that data file. -
:exclamation: **Caution:** -If your changes to the data file makes its format invalid, AddressBook will discard all data and start with an empty data file at the next run. Hence, it is recommended to take a backup of the file before editing it.
-Furthermore, certain edits can cause the AddressBook to behave in unexpected ways (e.g., if a value entered is outside of the acceptable range). Therefore, edit the data file only if you are confident that you can update it correctly. -
+**Unsure where to find the JSON file? No worries! Follow these instructions:** -### Archiving data files `[coming in v2.0]` +1. In ScoopBook, type the following command: `export temp.json` +2. `temp.json` will be saved in your JAR file location. Open it in an editor of your choice. +3. Edit the fields while adhering to the format of the file. Save the JSON file. +4. In ScoopBook, type the following command: `import temp.json` +5. Done! -_Details coming soon ..._ +
:exclamation: **Caution:** +If your changes to the data file makes its format invalid, ScoopBook will discard all data and start with an empty data file at the next run. Hence, it is recommended to take a backup of the file before editing it.
+Furthermore, certain edits can cause the ScoopBook to behave in unexpected ways (e.g., if a value entered is outside of the acceptable range). Therefore, edit the data file only if you are confident that you can update it correctly. +
-------------------------------------------------------------------------------------------------------------------- ## FAQ -**Q**: How do I transfer my data to another Computer?
-**A**: Install the app in the other computer and overwrite the empty data file it creates with the file that contains the data of your previous AddressBook home folder. +:question: **Q**: **How do I transfer my data to another Computer?**
+**A**: Install the app in the other computer and overwrite the empty data file it creates with the file that contains the data of your previous ScoopBook home folder. + +:question: **Q**: **What are considered duplicate contacts?**
+**A**: Duplicate contacts are contacts with names that match exactly. We do not allow the addition of duplicate contacts in our app. For example, `John Doe` and `John Doe` are considered duplicate contacts, and we will not allow the addition of the second contact if the first has already been added. + +Names that differ in lower and upper case letters are not considered as duplicate contacts even if the same exact letters are used. For example, `John Doe` and `john doe` are not considered duplicate contacts. + +Additionally, names with different amount of spaces between them are also not considered duplicate contacts. For example, `John Doe` and John  Doe are not considered duplicate contacts. + +This way, we leave room for flexibility in deciding contact names, with the bare minimum of preventing the addition of duplicates as specified. + +:question: **Q**: **Why didn't my `add` command work?**
+**A**: Ensure that you entered at least a `name` and **one of the following**: phone number, email, or address. +- Does your name contain special characters? Only whitespace, `,`, `(`, `)`, `@`, `.`, `-`, `'` are allowed. + +:question: **Q**: **Why didn't my `edit`, `addtag`, `removetag` `delete` `note` or `deletenote` command work?**
+**A**: Check your INDEX! Is your index within range? + +:question: **Q**: **My `import` command did not work. What went wrong?**
+**A**: Make sure that the file was originally exported from ScoopBook. Do not import unrelated `.json` files or edit them outside of ScoopBook! +- Did you also check your filepath? + +
+ +:bulb: TIP: If you are unsure, test with a sample `export` first: +``` +export temp.json +import temp.json +``` +
-------------------------------------------------------------------------------------------------------------------- @@ -184,6 +476,12 @@ _Details coming soon ..._ 1. **When using multiple screens**, if you move the application to a secondary screen, and later switch to using only the primary screen, the GUI will open off-screen. The remedy is to delete the `preferences.json` file created by the application before running the application again. 2. **If you minimize the Help Window** and then run the `help` command (or use the `Help` menu, or the keyboard shortcut `F1`) again, the original Help Window will remain minimized, and no new Help Window will appear. The remedy is to manually restore the minimized Help Window. +3. **If you minimize the Note Window**, and then run the `note` command again, the original Note Window will remain minimized, and no new Note Window will appear. The remedy is to manually restore the minimized Note Window. +4. **If you use any other means apart from the note window that ScoopBook opens to edit a note**, (eg. notepad) we cannot guarantee that your edits will be saved. This may be because of an encoding incompatibility between your text editor and ScoopBook's. Please use the note window that ScoopBook opens to edit the note. +5. **Text fields in the GUI**: Currently, text fields that are too long may be cut off in the GUI. We will introduce scrolling as a feature to enable viewing these fields in full in future releases. +6. **Adding a contact with placeholder values**: Currently, we do not prevent the user from adding a contact with placeholder values. This is because we want to allow the user to add a contact with only a name and one other field, and we chose these placeholder values as unlikely values that would be used for a contact. + 1. Regardless, we acknowledge that this may lead to confusion as these contact fields deliberately added with placeholder values will not be displayed in the contact list. We will fix this in future releases. +7. **Finding a contact**: Currently, the `find` command performs an `OR` search. While all contacts matching at least one keyword will be returned, they are not sorted according to the highest similarity or match. We will improve this in future releases. -------------------------------------------------------------------------------------------------------------------- @@ -191,10 +489,17 @@ _Details coming soon ..._ 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` +**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/Witness t/Local` **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` +**Add Tag** | `addtag INDEX t/TAG1 [t/MORE_TAGS]…​`
e.g., `addtag 2 t/Witness` +**Find Tag** | `findtag t/TAG1 [t/MORE_TAGS]…​`
e.g., `findtag t/Witness` +**Remove Tag** | `removetag INDEX t/TAG1 [t/MORE_TAGS]…​`
e.g., `removetag 2 t/Witness` +**Note** | `note INDEX`
e.g., `note 2` +**Delete Note** | `deletenote INDEX`
e.g., `deletenote 3` +**Export Contacts** | `export TARGET_PATH`
e.g., `export backup.json` +**Import Contacts** | `import TARGET_PATH`
e.g., `import previousVer.json` **List** | `list` **Help** | `help` diff --git a/docs/_config.yml b/docs/_config.yml index 6bd245d8f4e..89f78868dd6 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,4 +1,4 @@ -title: "AB-3" +title: "ScoopBook" theme: minima header_pages: @@ -8,7 +8,7 @@ header_pages: markdown: kramdown -repository: "se-edu/addressbook-level3" +repository: "AY2425S2-CS2103T-W13-1/tp" github_icon: "images/github-icon.png" plugins: diff --git a/docs/_sass/minima/_base.scss b/docs/_sass/minima/_base.scss index 0d3f6e80ced..f4bf6e2e1df 100644 --- a/docs/_sass/minima/_base.scss +++ b/docs/_sass/minima/_base.scss @@ -288,7 +288,7 @@ table { text-align: center; } .site-header:before { - content: "AB-3"; + content: "ScoopBook"; font-size: 32px; } } diff --git a/docs/diagrams/BetterModelClassDiagram.puml b/docs/diagrams/BetterModelClassDiagram.puml index 598474a5c82..b914e1c14cc 100644 --- a/docs/diagrams/BetterModelClassDiagram.puml +++ b/docs/diagrams/BetterModelClassDiagram.puml @@ -4,8 +4,8 @@ skinparam arrowThickness 1.1 skinparam arrowColor MODEL_COLOR skinparam classBackgroundColor MODEL_COLOR -AddressBook *-right-> "1" UniquePersonList -AddressBook *-right-> "1" UniqueTagList +AddressBook -right-> "1" UniquePersonList +AddressBook -right-> "1" UniqueTagList UniqueTagList -[hidden]down- UniquePersonList UniqueTagList -[hidden]down- UniquePersonList @@ -14,8 +14,9 @@ UniquePersonList -right-> Person Person -up-> "*" Tag -Person *--> Name -Person *--> Phone -Person *--> Email -Person *--> Address +Person --> "1" Name +Person --> "0..1" Phone +Person --> "1" PersonId +Person --> "0..1" Email +Person --> "0..1" Address @enduml diff --git a/docs/diagrams/DeleteNoteSequenceDiagram.puml b/docs/diagrams/DeleteNoteSequenceDiagram.puml new file mode 100644 index 00000000000..b88c663c780 --- /dev/null +++ b/docs/diagrams/DeleteNoteSequenceDiagram.puml @@ -0,0 +1,76 @@ +@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 ":DeleteNoteCommandParser" as DeleteNoteCommandParser LOGIC_COLOR +participant "d:DeleteNoteCommand" as DeleteNoteCommand LOGIC_COLOR +participant "r:CommandResult" as CommandResult LOGIC_COLOR +end box + +Box Storage STORAGE_COLOR_T1 +participant "s:Storage" as Storage STORAGE_COLOR +end box + + +[-> LogicManager : execute("deletenote 1") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("deletenote 1") +activate AddressBookParser + +create DeleteNoteCommandParser +AddressBookParser -> DeleteNoteCommandParser +activate DeleteNoteCommandParser + +DeleteNoteCommandParser --> AddressBookParser +deactivate DeleteNoteCommandParser + +AddressBookParser -> DeleteNoteCommandParser : parse("1") +activate DeleteNoteCommandParser + +create DeleteNoteCommand +DeleteNoteCommandParser -> DeleteNoteCommand +activate DeleteNoteCommand + +DeleteNoteCommand --> DeleteNoteCommandParser : +deactivate DeleteNoteCommand + +DeleteNoteCommandParser --> AddressBookParser : d +deactivate DeleteNoteCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +DeleteNoteCommandParser -[hidden]-> AddressBookParser +destroy DeleteNoteCommandParser + +AddressBookParser --> LogicManager : d +deactivate AddressBookParser + +LogicManager -> DeleteNoteCommand: setStorage(s) +activate DeleteNoteCommand +DeleteNoteCommand --> LogicManager +deactivate DeleteNoteCommand + +LogicManager -> DeleteNoteCommand : execute(m) +activate DeleteNoteCommand + +DeleteNoteCommand -> Storage : deleteNote(targetPerson) +activate Storage + +Storage --> DeleteNoteCommand +deactivate Storage + +create CommandResult +DeleteNoteCommand -> CommandResult +activate CommandResult + +CommandResult --> DeleteNoteCommand +deactivate CommandResult + +DeleteNoteCommand --> LogicManager : r +deactivate DeleteNoteCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/DeleteSequenceDiagram.puml b/docs/diagrams/DeleteSequenceDiagram.puml index 5241e79d7da..854e098b5c9 100644 --- a/docs/diagrams/DeleteSequenceDiagram.puml +++ b/docs/diagrams/DeleteSequenceDiagram.puml @@ -14,6 +14,11 @@ box Model MODEL_COLOR_T1 participant "m:Model" as Model MODEL_COLOR end box +Box Storage STORAGE_COLOR_T1 +participant "s.Storage" as Storage STORAGE_COLOR +end box + + [-> LogicManager : execute("delete 1") activate LogicManager @@ -65,6 +70,14 @@ deactivate CommandResult DeleteCommand --> LogicManager : r deactivate DeleteCommand +LogicManager -> LogicManager : handleNoteOperation(d) + +LogicManager -> Storage : deleteNote(personDeleted) +activate Storage + +Storage --> LogicManager +deactivate Storage + [<--LogicManager deactivate LogicManager @enduml diff --git a/docs/diagrams/ModelClassDiagram.puml b/docs/diagrams/ModelClassDiagram.puml index 0de5673070d..a191479445a 100644 --- a/docs/diagrams/ModelClassDiagram.puml +++ b/docs/diagrams/ModelClassDiagram.puml @@ -18,6 +18,7 @@ Class Address Class Email Class Name Class Phone +Class PersonId Class Tag Class I #FFFFFF @@ -35,18 +36,19 @@ ModelManager -left-> "1" AddressBook ModelManager -right-> "1" UserPrefs UserPrefs .up.|> ReadOnlyUserPrefs -AddressBook *--> "1" UniquePersonList +AddressBook --> "1" UniquePersonList UniquePersonList --> "~* all" Person -Person *--> Name -Person *--> Phone -Person *--> Email -Person *--> Address -Person *--> "*" Tag +Person --> "1" Name +Person --> "0..1" Phone +Person --> "1" PersonId +Person --> "0..1" Email +Person --> "0..1" Address +Person --> "*" Tag Person -[hidden]up--> I UniquePersonList -[hidden]right-> I -Name -[hidden]right-> Phone +Name -[hidden]right-> PersonId Phone -[hidden]right-> Address Address -[hidden]right-> Email diff --git a/docs/diagrams/NoteSequenceDiagram.puml b/docs/diagrams/NoteSequenceDiagram.puml new file mode 100644 index 00000000000..1bd4065c61f --- /dev/null +++ b/docs/diagrams/NoteSequenceDiagram.puml @@ -0,0 +1,83 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Ui UI_COLOR_T1 +participant "u:Ui" as Ui UI_COLOR +end box + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":NoteCommandParser" as NoteCommandParser LOGIC_COLOR +participant "n:NoteCommand" as NoteCommand LOGIC_COLOR +participant "r:CommandResult" as CommandResult LOGIC_COLOR +end box + +Box Storage STORAGE_COLOR_T1 +participant "s.Storage" as Storage STORAGE_COLOR +end box + +[-> Ui : note 1 +activate Ui + +Ui -> LogicManager : execute("note 1") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("note 1") +activate AddressBookParser + +create NoteCommandParser +AddressBookParser -> NoteCommandParser +activate NoteCommandParser + +NoteCommandParser --> AddressBookParser +deactivate NoteCommandParser + +AddressBookParser -> NoteCommandParser : parse("1") +activate NoteCommandParser + +create NoteCommand +NoteCommandParser -> NoteCommand +activate NoteCommand + +NoteCommand --> NoteCommandParser : +deactivate NoteCommand + +NoteCommandParser --> AddressBookParser : n +deactivate NoteCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +NoteCommandParser -[hidden]-> AddressBookParser +destroy NoteCommandParser + +AddressBookParser --> LogicManager : n +deactivate AddressBookParser + +LogicManager -> NoteCommand : execute(m) +activate NoteCommand + +create CommandResult +NoteCommand -> CommandResult +activate CommandResult + +CommandResult --> NoteCommand +deactivate CommandResult + +NoteCommand --> LogicManager : r +deactivate NoteCommand + +Ui <--LogicManager +deactivate LogicManager + +Ui -> LogicManager : readNote(person) +activate LogicManager + +LogicManager -> Storage : readNote(person) +activate Storage + +Storage --> LogicManager : note content +deactivate Storage + +LogicManager --> Ui : note content +deactivate LogicManager +@enduml diff --git a/docs/diagrams/StorageClassDiagram.puml b/docs/diagrams/StorageClassDiagram.puml index a821e06458c..f73e17dbe29 100644 --- a/docs/diagrams/StorageClassDiagram.puml +++ b/docs/diagrams/StorageClassDiagram.puml @@ -22,6 +22,11 @@ Class JsonAdaptedPerson Class JsonAdaptedTag } +package "Notes Storage" #F4F6F6{ +Class "<>\nNotesStorage" as NotesStorage +Class FilesNotesStorage +} + } Class HiddenOutside #FFFFFF @@ -30,12 +35,15 @@ HiddenOutside ..> Storage StorageManager .up.|> Storage StorageManager -up-> "1" UserPrefsStorage StorageManager -up-> "1" AddressBookStorage +StorageManager -up-> "1" NotesStorage Storage -left-|> UserPrefsStorage Storage -right-|> AddressBookStorage +Storage -down-|> NotesStorage JsonUserPrefsStorage .up.|> UserPrefsStorage JsonAddressBookStorage .up.|> AddressBookStorage +FilesNotesStorage .up.|> NotesStorage JsonAddressBookStorage ..> JsonSerializableAddressBook JsonSerializableAddressBook --> "*" JsonAdaptedPerson JsonAdaptedPerson --> "*" JsonAdaptedTag diff --git a/docs/diagrams/UiClassDiagram.puml b/docs/diagrams/UiClassDiagram.puml index 95473d5aa19..1fa57f4e648 100644 --- a/docs/diagrams/UiClassDiagram.puml +++ b/docs/diagrams/UiClassDiagram.puml @@ -10,7 +10,10 @@ Class "{abstract}\nUiPart" as UiPart Class UiManager Class MainWindow Class HelpWindow +Class NoteWindow +Class NoteWindowHandler Class ResultDisplay +Class DialogBox Class PersonListPanel Class PersonCard Class StatusBarFooter @@ -30,22 +33,26 @@ HiddenOutside ..> Ui UiManager .left.|> Ui UiManager -down-> "1" MainWindow -MainWindow *-down-> "1" CommandBox -MainWindow *-down-> "1" ResultDisplay -MainWindow *-down-> "1" PersonListPanel -MainWindow *-down-> "1" StatusBarFooter +MainWindow -down-> "1" CommandBox +MainWindow -down-> "1" ResultDisplay +MainWindow -down-> "1" StatusBarFooter MainWindow --> "0..1" HelpWindow - -PersonListPanel -down-> "*" PersonCard - +MainWindow -down-> "1" NoteWindowHandler +NoteWindowHandler -down-> "*" NoteWindow +ResultDisplay -down-> "*" DialogBox MainWindow -left-|> UiPart +MainWindow -down-> "1" PersonListPanel +PersonListPanel --> "*" PersonCard + ResultDisplay --|> UiPart CommandBox --|> UiPart PersonListPanel --|> UiPart PersonCard --|> UiPart StatusBarFooter --|> UiPart HelpWindow --|> UiPart +DialogBox --|> UiPart +NoteWindow --|> UiPart PersonCard ..> Model UiManager -right-> Logic diff --git a/docs/images/AddCommand.png b/docs/images/AddCommand.png new file mode 100644 index 00000000000..c9cc85022fc Binary files /dev/null and b/docs/images/AddCommand.png differ diff --git a/docs/images/AddTagCommand.png b/docs/images/AddTagCommand.png new file mode 100644 index 00000000000..c6283e39bc0 Binary files /dev/null and b/docs/images/AddTagCommand.png differ diff --git a/docs/images/BetterModelClassDiagram.png b/docs/images/BetterModelClassDiagram.png index 02a42e35e76..3fb40f252c7 100644 Binary files a/docs/images/BetterModelClassDiagram.png and b/docs/images/BetterModelClassDiagram.png differ diff --git a/docs/images/DeleteCommand.png b/docs/images/DeleteCommand.png new file mode 100644 index 00000000000..771c20bdcce Binary files /dev/null and b/docs/images/DeleteCommand.png differ diff --git a/docs/images/DeleteNoteCommand.png b/docs/images/DeleteNoteCommand.png new file mode 100644 index 00000000000..55af4a9ef26 Binary files /dev/null and b/docs/images/DeleteNoteCommand.png differ diff --git a/docs/images/DeleteNoteSequenceDiagram.png b/docs/images/DeleteNoteSequenceDiagram.png new file mode 100644 index 00000000000..9a4e55d0c60 Binary files /dev/null and b/docs/images/DeleteNoteSequenceDiagram.png differ diff --git a/docs/images/DeleteSequenceDiagram.png b/docs/images/DeleteSequenceDiagram.png index ac2ae217c51..651b94f3997 100644 Binary files a/docs/images/DeleteSequenceDiagram.png and b/docs/images/DeleteSequenceDiagram.png differ diff --git a/docs/images/EditCommand.png b/docs/images/EditCommand.png new file mode 100644 index 00000000000..ac0a1b7798d Binary files /dev/null and b/docs/images/EditCommand.png differ diff --git a/docs/images/ExportCommand.png b/docs/images/ExportCommand.png new file mode 100644 index 00000000000..0f76559a152 Binary files /dev/null and b/docs/images/ExportCommand.png differ diff --git a/docs/images/FindTagCommand.png b/docs/images/FindTagCommand.png new file mode 100644 index 00000000000..0507aebca59 Binary files /dev/null and b/docs/images/FindTagCommand.png differ diff --git a/docs/images/HelpCommand.png b/docs/images/HelpCommand.png new file mode 100644 index 00000000000..6d95fc6ca5d Binary files /dev/null and b/docs/images/HelpCommand.png differ diff --git a/docs/images/ImportCommand.png b/docs/images/ImportCommand.png new file mode 100644 index 00000000000..140825dbeee Binary files /dev/null and b/docs/images/ImportCommand.png differ diff --git a/docs/images/ListCommand.png b/docs/images/ListCommand.png new file mode 100644 index 00000000000..41eb25adc17 Binary files /dev/null and b/docs/images/ListCommand.png differ diff --git a/docs/images/ModelClassDiagram.png b/docs/images/ModelClassDiagram.png index a19fb1b4ac8..3cad6067cec 100644 Binary files a/docs/images/ModelClassDiagram.png and b/docs/images/ModelClassDiagram.png differ diff --git a/docs/images/NoteCommand.png b/docs/images/NoteCommand.png new file mode 100644 index 00000000000..a67ca07c8b1 Binary files /dev/null and b/docs/images/NoteCommand.png differ diff --git a/docs/images/NoteSequenceDiagram.png b/docs/images/NoteSequenceDiagram.png new file mode 100644 index 00000000000..bc64ab8c0e3 Binary files /dev/null and b/docs/images/NoteSequenceDiagram.png differ diff --git a/docs/images/RemoveTagCommand.png b/docs/images/RemoveTagCommand.png new file mode 100644 index 00000000000..def62b69b96 Binary files /dev/null and b/docs/images/RemoveTagCommand.png differ diff --git a/docs/images/StorageClassDiagram.png b/docs/images/StorageClassDiagram.png index 18fa4d0d51f..9ce736f197a 100644 Binary files a/docs/images/StorageClassDiagram.png and b/docs/images/StorageClassDiagram.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 5bd77847aa2..ecca22369dc 100644 Binary files a/docs/images/Ui.png and b/docs/images/Ui.png differ diff --git a/docs/images/UiClassDiagram.png b/docs/images/UiClassDiagram.png index 11f06d68671..9803c8886f4 100644 Binary files a/docs/images/UiClassDiagram.png and b/docs/images/UiClassDiagram.png differ diff --git a/docs/images/findAlexDavidResult.png b/docs/images/findAlexDavidResult.png index 235da1c273e..e2273375e4c 100644 Binary files a/docs/images/findAlexDavidResult.png and b/docs/images/findAlexDavidResult.png differ diff --git a/docs/images/meatsushi64.png b/docs/images/meatsushi64.png new file mode 100644 index 00000000000..c9b085dc726 Binary files /dev/null and b/docs/images/meatsushi64.png differ diff --git a/docs/images/shawnnygoh.png b/docs/images/shawnnygoh.png new file mode 100644 index 00000000000..5ebe3027ece Binary files /dev/null and b/docs/images/shawnnygoh.png differ diff --git a/docs/images/souledfigurine.png b/docs/images/souledfigurine.png new file mode 100644 index 00000000000..a7dadab9d79 Binary files /dev/null and b/docs/images/souledfigurine.png differ diff --git a/docs/images/tim0tay.png b/docs/images/tim0tay.png new file mode 100644 index 00000000000..2d16765712a Binary files /dev/null and b/docs/images/tim0tay.png differ diff --git a/docs/images/wentingchua.png b/docs/images/wentingchua.png new file mode 100644 index 00000000000..b6aed0d27dd Binary files /dev/null and b/docs/images/wentingchua.png differ diff --git a/docs/img.png b/docs/img.png new file mode 100644 index 00000000000..b96c36a90fa Binary files /dev/null and b/docs/img.png differ diff --git a/docs/index.md b/docs/index.md index 7601dbaad0d..b84eea3313f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,6 @@ --- layout: page -title: AddressBook Level-3 +title: ScoopBook --- [![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) @@ -8,10 +8,10 @@ title: AddressBook Level-3 ![Ui](images/Ui.png) -**AddressBook is a desktop application for managing your contact details.** While it has a GUI, most of the user interactions happen using a CLI (Command Line Interface). +**ScoopBook is a desktop application for managing your contact details.** While it has a GUI, most of the user interactions happen using a CLI (Command Line Interface). -* If you are interested in using AddressBook, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). -* If you are interested about developing AddressBook, the [**Developer Guide**](DeveloperGuide.html) is a good place to start. +* If you are interested in using ScoopBook, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). +* If you are interested about developing ScoopBook, the [**Developer Guide**](DeveloperGuide.html) is a good place to start. **Acknowledgements** diff --git a/src/main/java/seedu/address/Main.java b/src/main/java/seedu/address/Main.java index 9461d6da769..76fd4d9262a 100644 --- a/src/main/java/seedu/address/Main.java +++ b/src/main/java/seedu/address/Main.java @@ -34,7 +34,6 @@ public static void main(String[] args) { // The warning however, can be safely ignored. Thus, the following log informs // the user (if looking at the log output) that the said warning appearing in the log // can be ignored. - logger.warning("The warning about Unsupported JavaFX configuration below (if any) can be ignored."); Application.launch(MainApp.class, args); } diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java index 678ddc8c218..89587b7eecc 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/seedu/address/MainApp.java @@ -36,7 +36,7 @@ */ public class MainApp extends Application { - public static final Version VERSION = new Version(0, 2, 2, true); + public static final Version VERSION = new Version(1, 5, 1, true); private static final Logger logger = LogsCenter.getLogger(MainApp.class); @@ -77,14 +77,21 @@ private Model initModelManager(Storage storage, ReadOnlyUserPrefs userPrefs) { Optional addressBookOptional; ReadOnlyAddressBook initialData; + boolean isSampleDataUsed = false; try { addressBookOptional = storage.readAddressBook(); if (!addressBookOptional.isPresent()) { logger.info("Creating a new data file " + storage.getAddressBookFilePath() + " populated with a sample AddressBook."); + isSampleDataUsed = true; } initialData = addressBookOptional.orElseGet(SampleDataUtil::getSampleAddressBook); - } catch (DataLoadingException e) { + + if (isSampleDataUsed) { + storage.saveAddressBook(initialData); + logger.info("Sample AddressBook saved to disk."); + } + } catch (DataLoadingException | IOException e) { logger.warning("Data file at " + storage.getAddressBookFilePath() + " could not be loaded." + " Will be starting with an empty AddressBook."); initialData = new AddressBook(); diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java index 92cd8fa605a..78cc706d1d2 100644 --- a/src/main/java/seedu/address/logic/Logic.java +++ b/src/main/java/seedu/address/logic/Logic.java @@ -1,5 +1,6 @@ package seedu.address.logic; +import java.io.IOException; import java.nio.file.Path; import javafx.collections.ObservableList; @@ -9,6 +10,7 @@ import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.person.Person; +import seedu.address.storage.Storage; /** * API of the Logic component @@ -47,4 +49,24 @@ public interface Logic { * Set the user prefs' GUI settings. */ void setGuiSettings(GuiSettings guiSettings); + + /** + * Returns the storage. + */ + Storage getStorage(); + + /** + * Reads the note for a person. + */ + String readNote(Person person) throws IOException; + + /** + * Saves a note for a person. + */ + void saveNote(Person person, String content) throws IOException; + + /** + * Deletes a note for a person. + */ + void deleteNote(Person person) throws IOException; } diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java index 5aa3b91c7d0..368bb43ff6f 100644 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ b/src/main/java/seedu/address/logic/LogicManager.java @@ -8,8 +8,12 @@ import javafx.collections.ObservableList; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; +import seedu.address.logic.commands.ClearCommand; import seedu.address.logic.commands.Command; import seedu.address.logic.commands.CommandResult; +import seedu.address.logic.commands.DeleteCommand; +import seedu.address.logic.commands.DeleteNoteCommand; +import seedu.address.logic.commands.ImportCommand; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.AddressBookParser; import seedu.address.logic.parser.exceptions.ParseException; @@ -48,10 +52,15 @@ public CommandResult execute(String commandText) throws CommandException, ParseE CommandResult commandResult; Command command = addressBookParser.parseCommand(commandText); - commandResult = command.execute(model); + // Inject storage manually if it's DeleteNoteCommand + if (command instanceof DeleteNoteCommand castCommand) { + castCommand.setStorage(storage); + } + commandResult = command.execute(model); try { storage.saveAddressBook(model.getAddressBook()); + handleNoteOperations(command); } catch (AccessDeniedException e) { throw new CommandException(String.format(FILE_OPS_PERMISSION_ERROR_FORMAT, e.getMessage()), e); } catch (IOException ioe) { @@ -85,4 +94,47 @@ public GuiSettings getGuiSettings() { public void setGuiSettings(GuiSettings guiSettings) { model.setGuiSettings(guiSettings); } + + @Override + public Storage getStorage() { + return storage; + } + + @Override + public String readNote(Person person) throws IOException { + return storage.readNote(person); + } + + @Override + public void saveNote(Person person, String content) throws IOException { + storage.saveNote(person, content); + } + + @Override + public void deleteNote(Person person) throws IOException { + storage.deleteNote(person); + } + + /** + * Handles note operations based on the command type. + * + * @param command The command that was executed + * @throws IOException If there is an issue with file operations + */ + private void handleNoteOperations(Command command) throws IOException { + if (command instanceof DeleteCommand) { + DeleteCommand deleteCommand = (DeleteCommand) command; + Person personDeleted = deleteCommand.getTargetPerson(); + if (personDeleted != null) { + storage.deleteNote(personDeleted); + } + } else if (command instanceof ClearCommand) { + ClearCommand clearCommand = (ClearCommand) command; + if (clearCommand.hasCleared()) { + storage.deleteAllNotes(); + } + } else if (command instanceof ImportCommand) { + storage.deleteAllNotes(); + } + } } diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddCommand.java index 5d7185a9680..64ccdbdbb8e 100644 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ b/src/main/java/seedu/address/logic/commands/AddCommand.java @@ -22,7 +22,8 @@ public class AddCommand extends Command { public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a person to the address book. " + "Parameters: " - + PREFIX_NAME + "NAME " + + PREFIX_NAME + "NAME (alphanumeric characters, spaces and , ( ) . @ - ' only) " + + "and at least one of the following: " + PREFIX_PHONE + "PHONE " + PREFIX_EMAIL + "EMAIL " + PREFIX_ADDRESS + "ADDRESS " diff --git a/src/main/java/seedu/address/logic/commands/AddTagCommand.java b/src/main/java/seedu/address/logic/commands/AddTagCommand.java new file mode 100644 index 00000000000..c8a541e8b62 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AddTagCommand.java @@ -0,0 +1,118 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Address; +import seedu.address.model.person.Email; +import seedu.address.model.person.Name; +import seedu.address.model.person.Person; +import seedu.address.model.person.PersonId; +import seedu.address.model.person.Phone; +import seedu.address.model.tag.Tag; + +/** + * Adds one or more tags to a person in the address book. + */ +public class AddTagCommand extends Command { + public static final String COMMAND_WORD = "addtag"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a tag to a person in the address book. " + + "Parameters: " + + "INDEX (must be a positive integer) " + + PREFIX_TAG + "TAG (need not be a single word) " + + "[" + PREFIX_TAG + "TAG]...\n" + + "Example: " + COMMAND_WORD + " 1 " + PREFIX_TAG + "friends " + + PREFIX_TAG + "owesMoney"; + + public static final String MESSAGE_SUCCESS = "Tag added to person: %1$s"; + + public static final String MESSAGE_EMPTY_TAG = "Tags cannot be empty"; + + private final Index index; + private final PersonDescriptor personDescriptor; + + /** + * @param targetIndex of the person in the filtered person list to edit + * @param personDescriptor details to edit the person with + */ + public AddTagCommand(Index targetIndex, PersonDescriptor personDescriptor) { + this.index = targetIndex; + this.personDescriptor = personDescriptor; + } + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredPersonList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Person personToAddTag = lastShownList.get(index.getZeroBased()); + Person personWithTags = createPersonWithAddedTags(personToAddTag, personDescriptor); + + model.setPerson(personToAddTag, personWithTags); + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + return new CommandResult(String.format(MESSAGE_SUCCESS, Messages.format(personWithTags))); + } + + + /** + * Creates and returns a {@code Person} with the details of {@code personToAddTag} + * edited with {@code personDescriptor}. + */ + private static Person createPersonWithAddedTags(Person personToAddTag, + PersonDescriptor personDescriptor) { + assert personToAddTag != null; + + Name updatedName = personToAddTag.getName(); + Phone updatedPhone = personToAddTag.getPhone(); + Email updatedEmail = personToAddTag.getEmail(); + Address updatedAddress = personToAddTag.getAddress(); + // gets current tags for personToAddTag + Set updatedTags = new HashSet<>(personToAddTag.getTags()); + // add new tags from personDescriptor + updatedTags.addAll(personDescriptor.getTags().orElse(Collections.emptySet())); + PersonId personId = personToAddTag.getId(); + + return new Person( + updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags, personId); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof AddTagCommand)) { + return false; + } + + AddTagCommand otherAddTagCommand = (AddTagCommand) other; + return index.equals(otherAddTagCommand.index) + && personDescriptor.equals(otherAddTagCommand.personDescriptor); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("index", index) + .add("personDescriptor", personDescriptor) + .toString(); + } +} + diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/address/logic/commands/ClearCommand.java index 9c86b1fa6e4..c1f30ff9fe2 100644 --- a/src/main/java/seedu/address/logic/commands/ClearCommand.java +++ b/src/main/java/seedu/address/logic/commands/ClearCommand.java @@ -4,6 +4,7 @@ import seedu.address.model.AddressBook; import seedu.address.model.Model; +import seedu.address.model.person.PersonId; /** * Clears the address book. @@ -12,12 +13,23 @@ public class ClearCommand extends Command { public static final String COMMAND_WORD = "clear"; public static final String MESSAGE_SUCCESS = "Address book has been cleared!"; + private boolean hasCleared = false; @Override public CommandResult execute(Model model) { requireNonNull(model); model.setAddressBook(new AddressBook()); - return new CommandResult(MESSAGE_SUCCESS); + hasCleared = true; + PersonId.reset(); + return new CommandResult(MESSAGE_SUCCESS, false, false, + false, NoteCloseInstruction.CLOSE_ALL); + } + + /** + * Returns whether this command has cleared the address book. + */ + public boolean hasCleared() { + return hasCleared; } } diff --git a/src/main/java/seedu/address/logic/commands/CommandResult.java b/src/main/java/seedu/address/logic/commands/CommandResult.java index 249b6072d0d..ff9b5d9a898 100644 --- a/src/main/java/seedu/address/logic/commands/CommandResult.java +++ b/src/main/java/seedu/address/logic/commands/CommandResult.java @@ -1,10 +1,12 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; +import static seedu.address.logic.commands.NoteCloseInstruction.CLOSE_NONE; import java.util.Objects; import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.person.Person; /** * Represents the result of a command execution. @@ -17,15 +19,27 @@ public class CommandResult { private final boolean showHelp; /** The application should exit. */ - private final boolean exit; + private final boolean shouldExit; + + /** Note should be shown to the user. */ + private final boolean showNote; + /** Note should be deleted. */ + private final NoteCloseInstruction shouldDeleteNote; + + /** The person to show the note for. */ + private final Person targetPerson; /** * Constructs a {@code CommandResult} with the specified fields. */ - public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) { + public CommandResult(String feedbackToUser, boolean showHelp, boolean shouldExit, + boolean showNote, NoteCloseInstruction shouldDeleteNote, Person targetPerson) { this.feedbackToUser = requireNonNull(feedbackToUser); this.showHelp = showHelp; - this.exit = exit; + this.shouldExit = shouldExit; + this.showNote = showNote; + this.shouldDeleteNote = shouldDeleteNote; + this.targetPerson = targetPerson; } /** @@ -33,7 +47,20 @@ 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, CLOSE_NONE, null); + } + + /** + * Constructs a {@code CommandResult} with the specified {@code feedbackToUser}, {@code showHelp}, {@code exit}, + * and {@code showNote}, and other fields set to their default value. + */ + public CommandResult(String feedbackToUser, boolean showHelp, boolean shouldExit, + boolean showNote, NoteCloseInstruction shouldDeleteNote) { + this(feedbackToUser, showHelp, shouldExit, showNote, shouldDeleteNote, null); + } + + public Person getTargetPerson() { + return targetPerson; } public String getFeedbackToUser() { @@ -45,7 +72,14 @@ public boolean isShowHelp() { } public boolean isExit() { - return exit; + return shouldExit; + } + + public boolean isShowNote() { + return showNote; + } + public NoteCloseInstruction shouldDeleteNote() { + return shouldDeleteNote; } @Override @@ -62,12 +96,16 @@ public boolean equals(Object other) { CommandResult otherCommandResult = (CommandResult) other; return feedbackToUser.equals(otherCommandResult.feedbackToUser) && showHelp == otherCommandResult.showHelp - && exit == otherCommandResult.exit; + && shouldExit == otherCommandResult.shouldExit + && showNote == otherCommandResult.showNote + && shouldDeleteNote == otherCommandResult.shouldDeleteNote + && Objects.equals(targetPerson, otherCommandResult.targetPerson); } @Override public int hashCode() { - return Objects.hash(feedbackToUser, showHelp, exit); + return Objects.hash(feedbackToUser, showHelp, shouldExit, + showNote, shouldDeleteNote, targetPerson); } @Override @@ -75,7 +113,9 @@ public String toString() { return new ToStringBuilder(this) .add("feedbackToUser", feedbackToUser) .add("showHelp", showHelp) - .add("exit", exit) + .add("exit", shouldExit) + .add("showNote", showNote) + .add("shouldDeleteNote", shouldDeleteNote) .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..1c26a90a004 100644 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ b/src/main/java/seedu/address/logic/commands/DeleteCommand.java @@ -26,6 +26,7 @@ public class DeleteCommand extends Command { public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted Person: %1$s"; private final Index targetIndex; + private Person targetPerson; public DeleteCommand(Index targetIndex) { this.targetIndex = targetIndex; @@ -41,8 +42,11 @@ public CommandResult execute(Model model) throws CommandException { } Person personToDelete = lastShownList.get(targetIndex.getZeroBased()); + this.targetPerson = personToDelete; model.deletePerson(personToDelete); - return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, Messages.format(personToDelete))); + + return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, Messages.format(personToDelete)), + false, false, false, NoteCloseInstruction.CLOSE_ONE, personToDelete); } @Override @@ -66,4 +70,11 @@ public String toString() { .add("targetIndex", targetIndex) .toString(); } + + /** + * Returns the target person to be deleted. + */ + public Person getTargetPerson() { + return targetPerson; + } } diff --git a/src/main/java/seedu/address/logic/commands/DeleteNoteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteNoteCommand.java new file mode 100644 index 00000000000..40daf150d71 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/DeleteNoteCommand.java @@ -0,0 +1,112 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.io.IOException; +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Person; +import seedu.address.storage.Storage; + +/** + * Deletes the note tagged to the contact identified using it's displayed index from the address book. + */ +public class DeleteNoteCommand extends Command { + + public static final String COMMAND_WORD = "deletenote"; + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Deletes the note of the person identified by the index number used in the displayed person list.\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + public static final String MESSAGE_DELETENOTE_SUCCESS = "Note deleted for: %1$s"; + public static final String MESSAGE_NO_NOTE = "No note found for: %1$s"; + private final Index targetIndex; + private Storage storage; + private Person targetPerson; + + /** + * Constructs a {@code DeleteNoteCommand} that targets a person by their index in the filtered person list. + * + * @param targetIndex The index of the person in the filtered list. + */ + public DeleteNoteCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + /** + * Executes the delete note command by attempting to remove a note from the target person. + * + * @param model The model containing the current state of the address book. + * @return A {@code CommandResult} indicating success or failure, along with instructions to close the note view. + * @throws CommandException If the target index is invalid or an I/O error occurs during deletion. + */ + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredPersonList(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + Person personToDeleteNote = lastShownList.get(targetIndex.getZeroBased()); + this.targetPerson = personToDeleteNote; + + boolean deleted; + try { + deleted = storage.deleteNote(targetPerson); + } catch (IOException e) { + throw new CommandException("Failed to delete note: " + e.getMessage(), e); + } + + String message = deleted + ? String.format(MESSAGE_DELETENOTE_SUCCESS, Messages.format(personToDeleteNote)) + : String.format(MESSAGE_NO_NOTE, Messages.format(personToDeleteNote)); + + return new CommandResult(message, false, false, false, + NoteCloseInstruction.CLOSE_ONE, personToDeleteNote); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + //instanceof handles nulls + if (!(other instanceof DeleteNoteCommand)) { + return false; + } + + DeleteNoteCommand otherDeleteNoteCommand = (DeleteNoteCommand) other; + return targetIndex.equals(otherDeleteNoteCommand.targetIndex); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("targetIndex", targetIndex) + .toString(); + } + + /** + * Returns the person associated with this command at execution time. + * + * @return The target {@code Person} whose note was attempted to be deleted. + */ + public Person getTargetPerson() { + return targetPerson; + } + /** + * Sets the {@code Storage} instance required to perform note deletion. + * + * @param storage The storage implementation to use. + */ + public void setStorage(Storage storage) { + this.storage = storage; + } +} diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java index 4b581c7331e..a6a1a7fbe9b 100644 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ b/src/main/java/seedu/address/logic/commands/EditCommand.java @@ -8,15 +8,10 @@ import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; -import java.util.Collections; -import java.util.HashSet; import java.util.List; -import java.util.Objects; -import java.util.Optional; import java.util.Set; import seedu.address.commons.core.index.Index; -import seedu.address.commons.util.CollectionUtil; import seedu.address.commons.util.ToStringBuilder; import seedu.address.logic.Messages; import seedu.address.logic.commands.exceptions.CommandException; @@ -25,6 +20,7 @@ import seedu.address.model.person.Email; import seedu.address.model.person.Name; import seedu.address.model.person.Person; +import seedu.address.model.person.PersonId; import seedu.address.model.person.Phone; import seedu.address.model.tag.Tag; @@ -53,18 +49,18 @@ public class EditCommand extends Command { public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book."; private final Index index; - private final EditPersonDescriptor editPersonDescriptor; + private final PersonDescriptor personDescriptor; /** * @param index of the person in the filtered person list to edit - * @param editPersonDescriptor details to edit the person with + * @param personDescriptor details to edit the person with */ - public EditCommand(Index index, EditPersonDescriptor editPersonDescriptor) { + public EditCommand(Index index, PersonDescriptor personDescriptor) { requireNonNull(index); - requireNonNull(editPersonDescriptor); + requireNonNull(personDescriptor); this.index = index; - this.editPersonDescriptor = new EditPersonDescriptor(editPersonDescriptor); + this.personDescriptor = new PersonDescriptor(personDescriptor); } @Override @@ -77,7 +73,7 @@ public CommandResult execute(Model model) throws CommandException { } Person personToEdit = lastShownList.get(index.getZeroBased()); - Person editedPerson = createEditedPerson(personToEdit, editPersonDescriptor); + Person editedPerson = createEditedPerson(personToEdit, personDescriptor); if (!personToEdit.isSamePerson(editedPerson) && model.hasPerson(editedPerson)) { throw new CommandException(MESSAGE_DUPLICATE_PERSON); @@ -90,18 +86,19 @@ public CommandResult execute(Model model) throws CommandException { /** * Creates and returns a {@code Person} with the details of {@code personToEdit} - * edited with {@code editPersonDescriptor}. + * edited with {@code personDescriptor}. */ - private static Person createEditedPerson(Person personToEdit, EditPersonDescriptor editPersonDescriptor) { + private static Person createEditedPerson(Person personToEdit, PersonDescriptor personDescriptor) { assert personToEdit != null; - Name updatedName = editPersonDescriptor.getName().orElse(personToEdit.getName()); - Phone updatedPhone = editPersonDescriptor.getPhone().orElse(personToEdit.getPhone()); - Email updatedEmail = editPersonDescriptor.getEmail().orElse(personToEdit.getEmail()); - Address updatedAddress = editPersonDescriptor.getAddress().orElse(personToEdit.getAddress()); - Set updatedTags = editPersonDescriptor.getTags().orElse(personToEdit.getTags()); + Name updatedName = personDescriptor.getName().orElse(personToEdit.getName()); + Phone updatedPhone = personDescriptor.getPhone().orElse(personToEdit.getPhone()); + Email updatedEmail = personDescriptor.getEmail().orElse(personToEdit.getEmail()); + Address updatedAddress = personDescriptor.getAddress().orElse(personToEdit.getAddress()); + Set updatedTags = personDescriptor.getTags().orElse(personToEdit.getTags()); + PersonId personId = personToEdit.getId(); - return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags); + return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags, personId); } @Override @@ -117,126 +114,14 @@ public boolean equals(Object other) { EditCommand otherEditCommand = (EditCommand) other; return index.equals(otherEditCommand.index) - && editPersonDescriptor.equals(otherEditCommand.editPersonDescriptor); + && personDescriptor.equals(otherEditCommand.personDescriptor); } @Override public String toString() { return new ToStringBuilder(this) .add("index", index) - .add("editPersonDescriptor", editPersonDescriptor) + .add("personDescriptor", personDescriptor) .toString(); } - - /** - * Stores the details to edit the person with. Each non-empty field value will replace the - * corresponding field value of the person. - */ - public static class EditPersonDescriptor { - private Name name; - private Phone phone; - private Email email; - private Address address; - private Set tags; - - public EditPersonDescriptor() {} - - /** - * Copy constructor. - * A defensive copy of {@code tags} is used internally. - */ - public EditPersonDescriptor(EditPersonDescriptor toCopy) { - setName(toCopy.name); - setPhone(toCopy.phone); - setEmail(toCopy.email); - setAddress(toCopy.address); - setTags(toCopy.tags); - } - - /** - * Returns true if at least one field is edited. - */ - public boolean isAnyFieldEdited() { - return CollectionUtil.isAnyNonNull(name, phone, email, address, tags); - } - - public void setName(Name name) { - this.name = name; - } - - public Optional getName() { - return Optional.ofNullable(name); - } - - public void setPhone(Phone phone) { - this.phone = phone; - } - - public Optional getPhone() { - return Optional.ofNullable(phone); - } - - public void setEmail(Email email) { - this.email = email; - } - - public Optional getEmail() { - return Optional.ofNullable(email); - } - - public void setAddress(Address address) { - this.address = address; - } - - public Optional
getAddress() { - return Optional.ofNullable(address); - } - - /** - * Sets {@code tags} to this object's {@code tags}. - * A defensive copy of {@code tags} is used internally. - */ - public void setTags(Set tags) { - this.tags = (tags != null) ? new HashSet<>(tags) : null; - } - - /** - * Returns an unmodifiable tag set, which throws {@code UnsupportedOperationException} - * if modification is attempted. - * Returns {@code Optional#empty()} if {@code tags} is null. - */ - public Optional> getTags() { - return (tags != null) ? Optional.of(Collections.unmodifiableSet(tags)) : Optional.empty(); - } - - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof EditPersonDescriptor)) { - return false; - } - - EditPersonDescriptor otherEditPersonDescriptor = (EditPersonDescriptor) other; - return Objects.equals(name, otherEditPersonDescriptor.name) - && Objects.equals(phone, otherEditPersonDescriptor.phone) - && Objects.equals(email, otherEditPersonDescriptor.email) - && Objects.equals(address, otherEditPersonDescriptor.address) - && Objects.equals(tags, otherEditPersonDescriptor.tags); - } - - @Override - public String toString() { - return new ToStringBuilder(this) - .add("name", name) - .add("phone", phone) - .add("email", email) - .add("address", address) - .add("tags", tags) - .toString(); - } - } } diff --git a/src/main/java/seedu/address/logic/commands/ExitCommand.java b/src/main/java/seedu/address/logic/commands/ExitCommand.java index 3dd85a8ba90..deac69450f1 100644 --- a/src/main/java/seedu/address/logic/commands/ExitCommand.java +++ b/src/main/java/seedu/address/logic/commands/ExitCommand.java @@ -13,7 +13,8 @@ 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, NoteCloseInstruction.CLOSE_NONE); } } diff --git a/src/main/java/seedu/address/logic/commands/ExportCommand.java b/src/main/java/seedu/address/logic/commands/ExportCommand.java new file mode 100644 index 00000000000..98e6160b2b4 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ExportCommand.java @@ -0,0 +1,89 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + +/** + * Exports contacts into a json file. + */ +public class ExportCommand extends Command { + + public static final String COMMAND_WORD = "export"; + public static final String MESSAGE_SOURCE_FILE_NOT_FOUND = "Address book not found." + + " Start adding contacts to form your address book."; + public static final String MESSAGE_INVALID_FILE_FORMAT = "Export failed. Target file must have a .json extension."; + public static final String MESSAGE_EXPORT_SUCCESS = "Exported Address Book to %1$s as requested ..."; + public static final String MESSAGE_EXPORT_FAILURE = "Failed to export json file."; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Exports the address book data at the specified path.\n" + + "Parameters: FILEPATH (must be a valid file path)\n" + + "Example (Windows): " + COMMAND_WORD + " C:/Users/username/Desktop/exported_data.json\n" + + "Example (Mac): " + COMMAND_WORD + " /Users/username/Desktop/exported_data.json\n" + + "Example (Linux): " + COMMAND_WORD + " /home/user/desktop/exported_data.json"; + private static final Path SOURCE_JSON_PATH = Paths.get("data/addressbook.json"); + private final Path targetPath; + private boolean sourcePathValid = true; + + /** + * Creates an ExportCommand to export json file to specified target path + * @param targetPath + */ + public ExportCommand(Path targetPath) { + this.targetPath = targetPath; + } + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(targetPath); + + if (!targetPath.toString().toLowerCase().endsWith(".json")) { + throw new CommandException(MESSAGE_INVALID_FILE_FORMAT); + } + if (!sourcePathValid) { + throw new CommandException(MESSAGE_SOURCE_FILE_NOT_FOUND); + } + try { + // Ensure parent directory exists + if (targetPath.getParent() != null) { + Files.createDirectories(targetPath.getParent()); + } + + // Perform the export + Files.copy(SOURCE_JSON_PATH, targetPath, StandardCopyOption.REPLACE_EXISTING); + return new CommandResult(String.format(MESSAGE_EXPORT_SUCCESS, targetPath.toAbsolutePath())); + + } catch (IOException e) { + throw new CommandException(MESSAGE_EXPORT_FAILURE); + } + } + + /** + * Method to check if the json source file exists and + * store boolean in variable + */ + public void sourceFileExists(String s) { + if (s.equals("simulate invalid source file")) { + sourcePathValid = false; + } else { + sourcePathValid = Files.exists(SOURCE_JSON_PATH); + } + } + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ExportCommand that = (ExportCommand) obj; + return targetPath.equals(that.targetPath); // Compare the file path + } +} diff --git a/src/main/java/seedu/address/logic/commands/FindTagCommand.java b/src/main/java/seedu/address/logic/commands/FindTagCommand.java new file mode 100644 index 00000000000..a02be6fa514 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/FindTagCommand.java @@ -0,0 +1,59 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.model.Model; +import seedu.address.model.person.TagNamesContainsTagsPredicate; + +/** + * Finds and lists all contacts in ScoopBook whose tag(s) match(es) all of the argument tag(s). + * Tag matching is case insensitive. + */ +public class FindTagCommand extends Command { + + public static final String COMMAND_WORD = "findtag"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all contacts whose tag(s) match(es) all of " + + "the specified tags (case-insensitive) and displays them as a list with index numbers.\n" + + "Parameters: t/TAG [t/MORE_TAGS]...\n" + + "Example: " + COMMAND_WORD + " t/friend t/reporter t/government"; + + public static final String MESSAGE_EMPTY_TAG = "Tags cannot be empty."; + + private final TagNamesContainsTagsPredicate predicate; + + public FindTagCommand(TagNamesContainsTagsPredicate predicate) { + this.predicate = predicate; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + model.updateFilteredPersonList(predicate); + return new CommandResult( + String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size())); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof FindTagCommand)) { + return false; + } + + FindTagCommand otherFindTagCommand = (FindTagCommand) other; + return predicate.equals(otherFindTagCommand.predicate); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("predicate", predicate) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/HelpCommand.java b/src/main/java/seedu/address/logic/commands/HelpCommand.java index bf824f91bd0..a6ce5df38e4 100644 --- a/src/main/java/seedu/address/logic/commands/HelpCommand.java +++ b/src/main/java/seedu/address/logic/commands/HelpCommand.java @@ -16,6 +16,7 @@ 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, NoteCloseInstruction.CLOSE_NONE); } } diff --git a/src/main/java/seedu/address/logic/commands/ImportCommand.java b/src/main/java/seedu/address/logic/commands/ImportCommand.java new file mode 100644 index 00000000000..d229e1ecb94 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ImportCommand.java @@ -0,0 +1,124 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; + +import seedu.address.commons.exceptions.DataLoadingException; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.person.Person; +import seedu.address.storage.JsonAddressBookStorage; + +/** + * Imports contacts from a specified JSON file. + */ +public class ImportCommand extends Command { + + public static final String COMMAND_WORD = "import"; + public static final String MESSAGE_INVALID_FILE_FORMAT = "Import failed. Target file must have a .json extension."; + public static final String MESSAGE_IMPORT_SUCCESS = "Imported contacts from %1$s as requested ..."; + public static final String MESSAGE_IMPORT_FAILURE = "Failed to import json file."; + public static final String MESSAGE_INVALID_JSON = "JSON file does not follow required format."; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Imports contacts from the specified path.\n" + + "Parameters: FILEPATH (must be a valid file path)\n" + + "Example (Windows): " + COMMAND_WORD + " C:/Users/username/Desktop/exported_data.json\n" + + "Example (Mac): " + COMMAND_WORD + " /Users/username/Desktop/exported_data.json\n" + + "Example (Linux): " + COMMAND_WORD + " /home/user/desktop/exported_data.json"; + private int previousIdNum = 0; + private final Path targetPath; + + /** + * Imports contacts from specified .json file from {@param targetPath} + */ + public ImportCommand(Path targetPath) { + this.targetPath = targetPath; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + requireNonNull(targetPath); + + if (!targetPath.toString().toLowerCase().endsWith(".json")) { + throw new CommandException(MESSAGE_INVALID_FILE_FORMAT); + } + + JsonAddressBookStorage jsonStorage = new JsonAddressBookStorage(targetPath); + + try { + ReadOnlyAddressBook importedAddressBook = jsonStorage.readAddressBook() + .orElseThrow(() -> new CommandException(MESSAGE_IMPORT_FAILURE)); + + validateImportedContacts(importedAddressBook); + model.setAddressBook(importedAddressBook); + + Path savePath = Paths.get("data", "addressbook.json"); + new JsonAddressBookStorage(savePath).saveAddressBook(model.getAddressBook()); + + return new CommandResult(String.format(MESSAGE_IMPORT_SUCCESS, targetPath)); + + } catch (DataLoadingException | IOException e) { + throw new CommandException("Invalid JSON file: " + shortenErrorMessage(e)); + } + } + + /** + * Method to shorten error message + * @param e + * @return shortened error message + */ + private String shortenErrorMessage(Exception e) { + if (e.getCause() instanceof IllegalValueException) { + IllegalValueException ive = (IllegalValueException) e.getCause(); + return ive.getMessage(); + } + return MESSAGE_INVALID_JSON; + } + + /** + * Checks that at least one of "phone", "email", or "address" is different from placeholder values. + * Checks that personId of the first person is 1 and increment sequentially. + */ + private void validateImportedContacts(ReadOnlyAddressBook addressBook) throws CommandException { + for (Person person : addressBook.getPersonList()) { + if (isAllPlaceholderValues(person)) { + throw new CommandException("Invalid JSON file: Person '" + person.getName() + + "' does not have any added phone, email, or address."); + } + int personId = person.getId().getIntId(); + if (personId > previousIdNum) { + previousIdNum = personId; + } else { + throw new CommandException("Invalid JSON file: Person '" + person.getName() + + "' has either out-of-order OR duplicate person ID."); + } + } + } + + /** + * Returns true if all three fields (phone, email, address) are set to placeholder values. + */ + private boolean isAllPlaceholderValues(Person person) { + return "000".equals(person.getPhone().value) + && "unknown@example.com".equals(person.getEmail().value) + && "Unknown address".equals(person.getAddress().value); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + ImportCommand that = (ImportCommand) obj; + return targetPath.equals(that.targetPath); // Compare the file path + } +} diff --git a/src/main/java/seedu/address/logic/commands/NoteCloseInstruction.java b/src/main/java/seedu/address/logic/commands/NoteCloseInstruction.java new file mode 100644 index 00000000000..3b1c170ea57 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/NoteCloseInstruction.java @@ -0,0 +1,7 @@ +package seedu.address.logic.commands; +/** + * Represents the instruction for deletion of notes. + */ +public enum NoteCloseInstruction { + CLOSE_ONE, CLOSE_ALL, CLOSE_NONE +} diff --git a/src/main/java/seedu/address/logic/commands/NoteCommand.java b/src/main/java/seedu/address/logic/commands/NoteCommand.java new file mode 100644 index 00000000000..b82da674d41 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/NoteCommand.java @@ -0,0 +1,57 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Person; + +/** + * Creates a .txt file tagged to contact identified using it's displayed index from the address book. + */ +public class NoteCommand extends Command { + + public static final String COMMAND_WORD = "note"; + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Adds a note to the person identified by the index number used in the displayed person list.\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + public static final String MESSAGE_NOTE_PERSON_SUCCESS = "Viewing note for: %1$s"; + + private final Index targetIndex; + public NoteCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredPersonList(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + Person personToAddNote = lastShownList.get(targetIndex.getZeroBased()); + return new CommandResult(String.format(MESSAGE_NOTE_PERSON_SUCCESS, Messages.format(personToAddNote)), + false, false, true, NoteCloseInstruction.CLOSE_NONE, personToAddNote); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof NoteCommand)) { + return false; + } + + NoteCommand otherNoteCommand = (NoteCommand) other; + return targetIndex.equals(otherNoteCommand.targetIndex); + } +} diff --git a/src/main/java/seedu/address/logic/commands/PersonDescriptor.java b/src/main/java/seedu/address/logic/commands/PersonDescriptor.java new file mode 100644 index 00000000000..c0338e40d40 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/PersonDescriptor.java @@ -0,0 +1,129 @@ +package seedu.address.logic.commands; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import seedu.address.commons.util.CollectionUtil; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.person.Address; +import seedu.address.model.person.Email; +import seedu.address.model.person.Name; +import seedu.address.model.person.Phone; +import seedu.address.model.tag.Tag; + +/** + * Stores the details to tag the person with. Each non-empty field value will replace the + * corresponding field value of the person. + */ +public class PersonDescriptor { + private Name name; + private Phone phone; + private Email email; + private Address address; + private Set tags; + + public PersonDescriptor() { + } + + /** + * Copy constructor. + * A defensive copy of {@code tags} is used internally. + */ + public PersonDescriptor(PersonDescriptor toCopy) { + setName(toCopy.name); + setPhone(toCopy.phone); + setEmail(toCopy.email); + setAddress(toCopy.address); + setTags(toCopy.tags); + } + + /** + * Returns true if at least one field is edited. + */ + public boolean isAnyFieldEdited() { + return CollectionUtil.isAnyNonNull(name, phone, email, address, tags); + } + + public void setName(Name name) { + this.name = name; + } + + public Optional getName() { + return Optional.ofNullable(name); + } + + public void setPhone(Phone phone) { + this.phone = phone; + } + + public Optional getPhone() { + return Optional.ofNullable(phone); + } + + public void setEmail(Email email) { + this.email = email; + } + + public Optional getEmail() { + return Optional.ofNullable(email); + } + + public void setAddress(Address address) { + this.address = address; + } + + public Optional
getAddress() { + return Optional.ofNullable(address); + } + + /** + * Sets {@code tags} to this object's {@code tags}. + * A defensive copy of {@code tags} is used internally. + */ + public void setTags(Set tags) { + this.tags = (tags != null) ? new HashSet<>(tags) : null; + } + + /** + * Returns an unmodifiable tag set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + * Returns {@code Optional#empty()} if {@code tags} is null. + */ + public Optional> getTags() { + return (tags != null) ? Optional.of(Collections.unmodifiableSet(tags)) : Optional.empty(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof PersonDescriptor)) { + return false; + } + + PersonDescriptor otherPersonDescriptor = + (PersonDescriptor) other; + return Objects.equals(name, otherPersonDescriptor.name) + && Objects.equals(phone, otherPersonDescriptor.phone) + && Objects.equals(email, otherPersonDescriptor.email) + && Objects.equals(address, otherPersonDescriptor.address) + && Objects.equals(tags, otherPersonDescriptor.tags); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("name", name) + .add("phone", phone) + .add("email", email) + .add("address", address) + .add("tags", tags) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/RemoveTagCommand.java b/src/main/java/seedu/address/logic/commands/RemoveTagCommand.java new file mode 100644 index 00000000000..e82477362f6 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/RemoveTagCommand.java @@ -0,0 +1,195 @@ +package seedu.address.logic.commands; +import static java.util.Objects.requireNonNull; + +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Address; +import seedu.address.model.person.Email; +import seedu.address.model.person.Name; +import seedu.address.model.person.Person; +import seedu.address.model.person.PersonId; +import seedu.address.model.person.Phone; +import seedu.address.model.tag.Tag; + +/** + * Removes specified tags from a person in the address book. + */ +public class RemoveTagCommand extends Command { + + public static final String COMMAND_WORD = "removetag"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Removes the specified tag(s) from the person in the address book.\n" + + "Parameters: INDEX (must be a positive integer) t/TAG [t/TAG]...\n" + + "Example: " + COMMAND_WORD + " 1 t/Friend t/Colleague"; + public static final String MESSAGE_REMOVE_TAG_SUCCESS = "Tag(s) %2$s removed for person: %1$s"; + public static final String MESSAGE_INVALID_TAGS = "Tag(s) %2$s do not exist for this person."; + public static final String MESSAGE_TAG_NOT_FOUND = "Tag(s) do not exist for this person."; + public static final String MESSAGE_EMPTY_TAG = "Tags cannot be empty"; + private final Index targetIndex; + private final PersonDescriptor personDescriptor; + + /** + * @param targetIndex of the person in the filtered person list to edit + * @param personDescriptor details to edit the person with + */ + public RemoveTagCommand(Index targetIndex, PersonDescriptor personDescriptor) { + this.targetIndex = targetIndex; + this.personDescriptor = new PersonDescriptor(personDescriptor); + } + + /** + * Executes the RemoveTagCommand by removing specified tags from the selected person. + * Displays which tags were removed and which were invalid (not found). + * + * @param model The current model containing the address book. + * @return CommandResult containing feedback to the user. + * @throws CommandException if the index is invalid or no valid tags were found. + */ + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredPersonList(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Person personToRemoveTag = lastShownList.get(targetIndex.getZeroBased()); + TagRemovalResult result = createTagRemovalResult(personToRemoveTag, personDescriptor); + Person removedTagPerson = result.updatedPerson; + + if (result.validTags.isEmpty()) { + throw new CommandException(MESSAGE_TAG_NOT_FOUND); + } + + String message = messageBuilder(result); + + model.setPerson(personToRemoveTag, removedTagPerson); + return new CommandResult(message); + } + + /** + * Creates and returns a {@code Person}, removing from {@code personToRemoveTag} + * only the tags specified in {@code personDescriptor}. + */ + private static TagRemovalResult createTagRemovalResult(Person personToRemoveTag, + PersonDescriptor personDescriptor) { + assert personToRemoveTag != null; + + Name name = personToRemoveTag.getName(); + Phone phone = personToRemoveTag.getPhone(); + Email email = personToRemoveTag.getEmail(); + Address address = personToRemoveTag.getAddress(); + + // Start with the existing set of Tags + Set existingTags = new HashSet<>(personToRemoveTag.getTags()); + Set validTags = new HashSet<>(); + Set invalidTags = new HashSet<>(); + Optional> tagsToRemove = personDescriptor.getTags(); + PersonId personId = personToRemoveTag.getId(); + + //Sort and remove valid tags + for (Tag tag : tagsToRemove.orElse(Collections.emptySet())) { + if (existingTags.contains(tag)) { + existingTags.remove(tag); + validTags.add(tag); + } else { + invalidTags.add(tag); + } + } + + Person removedTagPerson = new Person(name, phone, email, address, existingTags, personId); + return new TagRemovalResult(removedTagPerson, validTags, invalidTags); + } + + /** + * Constructs a user-friendly message summarizing which tags were removed + * and which tags were not found on the person. + * + * @param result The result of the tag removal operation. + * @return A formatted message to be displayed to the user. + */ + private static String messageBuilder(TagRemovalResult result) { + Person removedTagPerson = result.updatedPerson; + + String validTagString = formatTagSet(result.validTags); + String invalidTagString = formatTagSet(result.invalidTags); + + StringBuilder sb = new StringBuilder(); + sb.append(String.format(MESSAGE_REMOVE_TAG_SUCCESS, Messages.format(removedTagPerson), validTagString)); + if (!result.invalidTags.isEmpty()) { + sb.append("\n\n").append(String.format(MESSAGE_INVALID_TAGS, + Messages.format(removedTagPerson), invalidTagString)); + } + return sb.toString(); + } + + /** + * Formats a set of tags into a comma-separated string. + * + * @param tags Set of tags to format. + * @return A string representation of the tag set, or "none" if empty. + */ + private static String formatTagSet(Set tags) { + return tags.stream() + .map(Tag::toString) + .sorted() + .reduce((a, b) -> a + ", " + b) + .orElse("none"); + } + + + /** + * A container for the result of a removeTag operation. + * Stores the updated {@code Person} object after attempting to remove tags, + * along with sets of tags that were successfully removed and tags that were not found. + */ + private static class TagRemovalResult { + private final Person updatedPerson; + private final Set validTags; + private final Set invalidTags; + + /** + * Constructs a {@code TagRemovalResult}. + * + * @param updatedPerson The updated person after tag removal. + * @param validTags Tags that were successfully removed. + * @param invalidTags Tags that were not found on the person. + */ + public TagRemovalResult(Person updatedPerson, Set validTags, Set invalidTags) { + this.updatedPerson = updatedPerson; + this.validTags = validTags; + this.invalidTags = invalidTags; + } + } + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (!(other instanceof RemoveTagCommand otherCommand)) { + return false; + } + + return targetIndex.equals(otherCommand.targetIndex) + && personDescriptor.equals(otherCommand.personDescriptor); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("targetIndex", targetIndex) + .add("personDescriptor", personDescriptor) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddCommandParser.java index 4ff1a97ed77..80fc59400f6 100644 --- a/src/main/java/seedu/address/logic/parser/AddCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/AddCommandParser.java @@ -33,16 +33,19 @@ public AddCommand parse(String args) throws ParseException { ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); - if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ADDRESS, PREFIX_PHONE, PREFIX_EMAIL) + if ((!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ADDRESS) + && !arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_PHONE) + && !arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_EMAIL)) || !argMultimap.getPreamble().isEmpty()) { throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); } argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS); Name name = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()); - Phone phone = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get()); - Email email = ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get()); - Address address = ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get()); + Phone phone = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).orElseGet(() -> Phone.EMPTY_PHONE)); + Email email = ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).orElseGet(() -> Email.EMPTY_EMAIL)); + Address address = + ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).orElseGet(() -> Address.EMPTY_ADDRESS)); Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); Person person = new Person(name, phone, email, address, tagList); diff --git a/src/main/java/seedu/address/logic/parser/AddTagCommandParser.java b/src/main/java/seedu/address/logic/parser/AddTagCommandParser.java new file mode 100644 index 00000000000..b5d856bfbc6 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AddTagCommandParser.java @@ -0,0 +1,67 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.AddTagCommand; +import seedu.address.logic.commands.PersonDescriptor; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.tag.Tag; + +/** + * Parses input arguments and creates a new AddTagCommand object + */ +public class AddTagCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddTagCommand + * and returns an AddTagCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + + public AddTagCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_TAG); + + Index index; + + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddTagCommand.MESSAGE_USAGE), pe); + } + if (!arePrefixesPresent(argMultimap, PREFIX_TAG)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddTagCommand.MESSAGE_USAGE)); + } + PersonDescriptor personDescriptor = new PersonDescriptor(); + // If no tags are provided, throw an exception + parseTags(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(personDescriptor::setTags); + return new AddTagCommand(index, personDescriptor); + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } + + private Optional> parseTags(Collection tags) throws ParseException { + assert tags != null; + + if (tags.isEmpty() || tags.contains("")) { + throw new ParseException(AddTagCommand.MESSAGE_EMPTY_TAG); + } + Collection tagSet = tags.size() == 1 && tags.contains("") ? Collections.emptySet() : tags; + return Optional.of(ParserUtil.parseTags(tagSet)); + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java index 3149ee07e0b..1621b0ce9e5 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -9,14 +9,21 @@ import seedu.address.commons.core.LogsCenter; import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.AddTagCommand; import seedu.address.logic.commands.ClearCommand; import seedu.address.logic.commands.Command; import seedu.address.logic.commands.DeleteCommand; +import seedu.address.logic.commands.DeleteNoteCommand; import seedu.address.logic.commands.EditCommand; import seedu.address.logic.commands.ExitCommand; +import seedu.address.logic.commands.ExportCommand; import seedu.address.logic.commands.FindCommand; +import seedu.address.logic.commands.FindTagCommand; import seedu.address.logic.commands.HelpCommand; +import seedu.address.logic.commands.ImportCommand; import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.NoteCommand; +import seedu.address.logic.commands.RemoveTagCommand; import seedu.address.logic.parser.exceptions.ParseException; /** @@ -68,6 +75,9 @@ public Command parseCommand(String userInput) throws ParseException { case FindCommand.COMMAND_WORD: return new FindCommandParser().parse(arguments); + case FindTagCommand.COMMAND_WORD: + return new FindTagCommandParser().parse(arguments); + case ListCommand.COMMAND_WORD: return new ListCommand(); @@ -77,6 +87,24 @@ public Command parseCommand(String userInput) throws ParseException { case HelpCommand.COMMAND_WORD: return new HelpCommand(); + case RemoveTagCommand.COMMAND_WORD: + return new RemoveTagCommandParser().parse(arguments); + + case AddTagCommand.COMMAND_WORD: + return new AddTagCommandParser().parse(arguments); + + case ExportCommand.COMMAND_WORD: + return new ExportCommandParser().parse(arguments); + + case NoteCommand.COMMAND_WORD: + return new NoteCommandParser().parse(arguments); + + case DeleteNoteCommand.COMMAND_WORD: + return new DeleteNoteCommandParser().parse(arguments); + + case ImportCommand.COMMAND_WORD: + return new ImportCommandParser().parse(arguments); + default: logger.finer("This user input caused a ParseException: " + userInput); throw new ParseException(MESSAGE_UNKNOWN_COMMAND); diff --git a/src/main/java/seedu/address/logic/parser/DeleteNoteCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteNoteCommandParser.java new file mode 100644 index 00000000000..753534e79ae --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/DeleteNoteCommandParser.java @@ -0,0 +1,28 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.DeleteNoteCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new DeleteNoteCommand object + */ +public class DeleteNoteCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the DeleteNoteCommand + * and returns a DeleteNoteCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public DeleteNoteCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new DeleteNoteCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteNoteCommand.MESSAGE_USAGE), pe); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/seedu/address/logic/parser/EditCommandParser.java index 46b3309a78b..c97037adbac 100644 --- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/EditCommandParser.java @@ -15,7 +15,7 @@ import seedu.address.commons.core.index.Index; import seedu.address.logic.commands.EditCommand; -import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; +import seedu.address.logic.commands.PersonDescriptor; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.tag.Tag; @@ -44,27 +44,27 @@ public EditCommand parse(String args) throws ParseException { argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS); - EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor(); + PersonDescriptor personDescriptor = new PersonDescriptor(); if (argMultimap.getValue(PREFIX_NAME).isPresent()) { - editPersonDescriptor.setName(ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get())); + personDescriptor.setName(ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get())); } if (argMultimap.getValue(PREFIX_PHONE).isPresent()) { - editPersonDescriptor.setPhone(ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get())); + personDescriptor.setPhone(ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get())); } if (argMultimap.getValue(PREFIX_EMAIL).isPresent()) { - editPersonDescriptor.setEmail(ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get())); + personDescriptor.setEmail(ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get())); } if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { - editPersonDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); + personDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); } - parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); + parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(personDescriptor::setTags); - if (!editPersonDescriptor.isAnyFieldEdited()) { + if (!personDescriptor.isAnyFieldEdited()) { throw new ParseException(EditCommand.MESSAGE_NOT_EDITED); } - return new EditCommand(index, editPersonDescriptor); + return new EditCommand(index, personDescriptor); } /** diff --git a/src/main/java/seedu/address/logic/parser/ExportCommandParser.java b/src/main/java/seedu/address/logic/parser/ExportCommandParser.java new file mode 100644 index 00000000000..df3d2b55638 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ExportCommandParser.java @@ -0,0 +1,39 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; + +import seedu.address.logic.commands.ExportCommand; +import seedu.address.logic.parser.exceptions.ParseException; +/** + * Parses input arguments and creates a new ExportCommand object + */ +public class ExportCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the ExportCommand + * and returns an ExportCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public ExportCommand parse(String args) throws ParseException { + requireNonNull(args); + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ExportCommand.MESSAGE_USAGE)); + } + String[] parts = trimmedArgs.split("\\s+", 2); + if (parts.length > 1) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ExportCommand.MESSAGE_USAGE)); + } + try { + Path targetPath = Paths.get(trimmedArgs); + return new ExportCommand(targetPath); + } catch (InvalidPathException e) { + throw new ParseException(ExportCommand.MESSAGE_EXPORT_FAILURE + " Invalid path: " + + e.getMessage()); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/FindTagCommandParser.java b/src/main/java/seedu/address/logic/parser/FindTagCommandParser.java new file mode 100644 index 00000000000..4450b1f2e6d --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/FindTagCommandParser.java @@ -0,0 +1,45 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import java.util.List; + +import seedu.address.logic.commands.FindTagCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.TagNamesContainsTagsPredicate; + +/** + * Parses input arguments and creates a new FindTagCommand object + */ +public class FindTagCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the FindTagCommand + * and returns a FindTagCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public FindTagCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_TAG); + + List tagNames = argMultimap.getAllValues(PREFIX_TAG); + + if (tagNames.isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindTagCommand.MESSAGE_USAGE)); + } + + if (tagNames.contains("")) { + throw new ParseException(FindTagCommand.MESSAGE_EMPTY_TAG); + } + + try { + ParserUtil.parseTags(tagNames); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindTagCommand.MESSAGE_USAGE), pe); + } + + return new FindTagCommand(new TagNamesContainsTagsPredicate(tagNames)); + } +} diff --git a/src/main/java/seedu/address/logic/parser/ImportCommandParser.java b/src/main/java/seedu/address/logic/parser/ImportCommandParser.java new file mode 100644 index 00000000000..96f19c445ba --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ImportCommandParser.java @@ -0,0 +1,40 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; + +import seedu.address.logic.commands.ImportCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new ImportCommand object + */ +public class ImportCommandParser { + /** + * Parses the given {@code String} of arguments in the context of the ImportCommand + * and returns an ImportCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public ImportCommand parse(String args) throws ParseException { + requireNonNull(args); + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ImportCommand.MESSAGE_USAGE)); + } + String[] parts = trimmedArgs.split("\\s+", 2); + if (parts.length > 1) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ImportCommand.MESSAGE_USAGE)); + } + try { + Path targetPath = Paths.get(trimmedArgs); + return new ImportCommand(targetPath); + } catch (InvalidPathException e) { + throw new ParseException(ImportCommand.MESSAGE_IMPORT_FAILURE + " Invalid path: " + + e.getMessage()); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/NoteCommandParser.java b/src/main/java/seedu/address/logic/parser/NoteCommandParser.java new file mode 100644 index 00000000000..69e362ccba4 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/NoteCommandParser.java @@ -0,0 +1,28 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.NoteCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new NoteCommand object + */ +public class NoteCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the NoteCommand + * and returns a NoteCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public NoteCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new NoteCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, NoteCommand.MESSAGE_USAGE), pe); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/RemoveTagCommandParser.java b/src/main/java/seedu/address/logic/parser/RemoveTagCommandParser.java new file mode 100644 index 00000000000..826b0652725 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/RemoveTagCommandParser.java @@ -0,0 +1,62 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; + +import java.util.Collection; +import java.util.Collections; +import java.util.Optional; +import java.util.Set; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.PersonDescriptor; +import seedu.address.logic.commands.RemoveTagCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.tag.Tag; +/** + * Parses input arguments and creates a new RemoveTagCommand object + */ +public class RemoveTagCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the RemoveTagCommand + * and returns a RemoveTagCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public RemoveTagCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_TAG); + + Index index; + + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, RemoveTagCommand.MESSAGE_USAGE), pe); + } + + if (!argMultimap.getValue(PREFIX_TAG).isPresent()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, RemoveTagCommand.MESSAGE_USAGE)); + } + + PersonDescriptor removeTagPersonDescriptor = new PersonDescriptor(); + + parseTags(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(removeTagPersonDescriptor::setTags); + return new RemoveTagCommand(index, removeTagPersonDescriptor); + } + + /** + * Parses {@code Collection tags} into a {@code Set} if {@code tags} is non-empty. + * If {@code tags} contain only one element which is an empty string, it will be parsed into a + * {@code Set} containing zero tags. + */ + private Optional> parseTags(Collection tags) throws ParseException { + assert tags != null; + + if (tags.isEmpty() || tags.contains("")) { + throw new ParseException(RemoveTagCommand.MESSAGE_EMPTY_TAG); + } + + Collection tagSet = tags.size() == 1 && tags.contains("") ? Collections.emptySet() : tags; + return Optional.of(ParserUtil.parseTags(tagSet)); + } +} diff --git a/src/main/java/seedu/address/model/person/Address.java b/src/main/java/seedu/address/model/person/Address.java index 469a2cc9a1e..f82cfbb7827 100644 --- a/src/main/java/seedu/address/model/person/Address.java +++ b/src/main/java/seedu/address/model/person/Address.java @@ -16,6 +16,7 @@ public class Address { * otherwise " " (a blank string) becomes a valid input. */ public static final String VALIDATION_REGEX = "[^\\s].*"; + public static final String EMPTY_ADDRESS = "Unknown address"; public final String value; @@ -39,7 +40,7 @@ public static boolean isValidAddress(String test) { @Override public String toString() { - return value; + return value.equals(EMPTY_ADDRESS) ? "" : value; } @Override diff --git a/src/main/java/seedu/address/model/person/Email.java b/src/main/java/seedu/address/model/person/Email.java index c62e512bc29..85b513cb379 100644 --- a/src/main/java/seedu/address/model/person/Email.java +++ b/src/main/java/seedu/address/model/person/Email.java @@ -9,6 +9,7 @@ */ public class Email { + public static final String EMPTY_EMAIL = "unknown@example.com"; private static final String SPECIAL_CHARACTERS = "+_.-"; public static final String MESSAGE_CONSTRAINTS = "Emails should be of the format local-part@domain " + "and adhere to the following constraints:\n" @@ -53,7 +54,7 @@ public static boolean isValidEmail(String test) { @Override public String toString() { - return value; + return value.equals(EMPTY_EMAIL) ? "" : value; } @Override diff --git a/src/main/java/seedu/address/model/person/Name.java b/src/main/java/seedu/address/model/person/Name.java index 173f15b9b00..1483dc603c2 100644 --- a/src/main/java/seedu/address/model/person/Name.java +++ b/src/main/java/seedu/address/model/person/Name.java @@ -10,13 +10,14 @@ public class Name { public static final String MESSAGE_CONSTRAINTS = - "Names should only contain alphanumeric characters and spaces, and it should not be blank"; + "Names should only contain alphanumeric characters, spaces, and the following special characters " + + ", ( ) . @ - ' \nIt should also not be blank."; /* * The first character of the address must not be a whitespace, * otherwise " " (a blank string) becomes a valid input. */ - public static final String VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} ]*"; + public static final String VALIDATION_REGEX = "[\\p{Alnum},.'()@-][\\p{Alnum} ,.'()@-]*"; public final String fullName; diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java index abe8c46b535..4c667431282 100644 --- a/src/main/java/seedu/address/model/person/Person.java +++ b/src/main/java/seedu/address/model/person/Person.java @@ -25,8 +25,12 @@ public class Person { private final Address address; private final Set tags = new HashSet<>(); + // For internal usage; ID field + private final PersonId id; + /** * Every field must be present and not null. + * This constructor is used when creating a new person. */ public Person(Name name, Phone phone, Email email, Address address, Set tags) { requireAllNonNull(name, phone, email, address, tags); @@ -35,6 +39,21 @@ public Person(Name name, Phone phone, Email email, Address address, Set tag this.email = email; this.address = address; this.tags.addAll(tags); + this.id = new PersonId(); + } + + /** + * Every field must be present and not null. + * This constructor is used when loading data from storage. + */ + public Person(Name name, Phone phone, Email email, Address address, Set tags, PersonId id) { + requireAllNonNull(name, phone, email, address, tags, id); + this.name = name; + this.phone = phone; + this.email = email; + this.address = address; + this.tags.addAll(tags); + this.id = id; } public Name getName() { @@ -53,6 +72,10 @@ public Address getAddress() { return address; } + public PersonId getId() { + return id; + } + /** * Returns an immutable tag set, which throws {@code UnsupportedOperationException} * if modification is attempted. @@ -100,7 +123,7 @@ public boolean equals(Object other) { @Override public int hashCode() { // use this method for custom fields hashing instead of implementing your own - return Objects.hash(name, phone, email, address, tags); + return Objects.hash(name, phone, email, address, tags, id); } @Override @@ -111,6 +134,7 @@ public String toString() { .add("email", email) .add("address", address) .add("tags", tags) + .add("id", id) .toString(); } diff --git a/src/main/java/seedu/address/model/person/PersonId.java b/src/main/java/seedu/address/model/person/PersonId.java new file mode 100644 index 00000000000..b4f58bc07bc --- /dev/null +++ b/src/main/java/seedu/address/model/person/PersonId.java @@ -0,0 +1,91 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Person's ID in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidId(String)} + * For internal use and reference only. + */ +public class PersonId { + + public static final String MESSAGE_CONSTRAINTS = + "PersonId should be an integer"; // The person id must be a valid integer + public static final String VALIDATION_REGEX = "^0|[1-9]\\d*$"; + + private static int counter = 0; + public final String value; + + /** + * Constructs a {@code PersonId}. + * When a new {@code Person} is created, the {@code Person} is assigned a unique ID. + */ + public PersonId() { + PersonId.counter++; + this.value = String.valueOf(PersonId.counter); + } + + /** + * Constructs a {@code PersonId} with a specified ID. + * Used when loading data from storage. + */ + public PersonId(String id) { + requireNonNull(id); + checkArgument(isValidId(id), MESSAGE_CONSTRAINTS); + value = id; + } + + /** + * Returns true if a given string is a valid person id. + */ + public static boolean isValidId(String test) { + return test.matches(VALIDATION_REGEX); + } + + /** + * Resets the counter to 0. + * Called in conjunction with {@code ClearCommand#execute()}. + */ + public static int reset() { + PersonId.counter = 0; + return PersonId.counter; + } + + /** + * Sets the counter to the specified value. + * Called in conjunction with {@code Storage#readAddressBook()}. + */ + public static int setCounter(int maxId) { + checkArgument(isValidId(String.valueOf(maxId)), MESSAGE_CONSTRAINTS); + PersonId.counter = maxId; + return PersonId.counter; + } + + /** + * Getter to obtain person ID + * @return the person ID in integer + */ + public int getIntId() { + return Integer.parseInt(this.value); + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof PersonId)) { + return false; + } + + PersonId otherId = (PersonId) other; + return value.equals(otherId.value); + } +} diff --git a/src/main/java/seedu/address/model/person/Phone.java b/src/main/java/seedu/address/model/person/Phone.java index d733f63d739..5b4b09fde2e 100644 --- a/src/main/java/seedu/address/model/person/Phone.java +++ b/src/main/java/seedu/address/model/person/Phone.java @@ -13,6 +13,7 @@ public class Phone { public static final String MESSAGE_CONSTRAINTS = "Phone numbers should only contain numbers, and it should be at least 3 digits long"; public static final String VALIDATION_REGEX = "\\d{3,}"; + public static final String EMPTY_PHONE = "000"; public final String value; /** @@ -33,9 +34,17 @@ public static boolean isValidPhone(String test) { return test.matches(VALIDATION_REGEX); } + /** + * Returns a string representation of the phone number. + * If the phone number is not present, it returns "Unknown number". + */ + public String getPhoneNumber() { + return value.equals(EMPTY_PHONE) ? "Unknown number" : value; + } + @Override public String toString() { - return value; + return value.equals(EMPTY_PHONE) ? "" : value; } @Override diff --git a/src/main/java/seedu/address/model/person/TagNamesContainsTagsPredicate.java b/src/main/java/seedu/address/model/person/TagNamesContainsTagsPredicate.java new file mode 100644 index 00000000000..dff15899f33 --- /dev/null +++ b/src/main/java/seedu/address/model/person/TagNamesContainsTagsPredicate.java @@ -0,0 +1,45 @@ +package seedu.address.model.person; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.ToStringBuilder; + +/** + * Tests that a {@code Person}'s tags contain all the tag(s) given. + * Tag matching is case-insensitive. + */ +public class TagNamesContainsTagsPredicate implements Predicate { + private final List tags; + + public TagNamesContainsTagsPredicate(List tags) { + this.tags = tags; + } + + @Override + public boolean test(Person person) { + return tags.stream() + .allMatch(tagName -> person.getTags().stream() + .anyMatch(tag -> tag.tagName.equalsIgnoreCase(tagName))); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof TagNamesContainsTagsPredicate)) { + return false; + } + + TagNamesContainsTagsPredicate otherTagNamesContainsTagsPredicate = (TagNamesContainsTagsPredicate) other; + return tags.equals(otherTagNamesContainsTagsPredicate.tags); + } + + @Override + public String toString() { + return new ToStringBuilder(this).add("tags", tags).toString(); + } +} diff --git a/src/main/java/seedu/address/storage/FileNotesStorage.java b/src/main/java/seedu/address/storage/FileNotesStorage.java new file mode 100644 index 00000000000..a0e252551d8 --- /dev/null +++ b/src/main/java/seedu/address/storage/FileNotesStorage.java @@ -0,0 +1,77 @@ +package seedu.address.storage; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.model.person.Person; + +/** + * A class to manage notes storage in the file system. + */ +public class FileNotesStorage implements NotesStorage { + private static final Logger logger = LogsCenter.getLogger(FileNotesStorage.class); + private final Path notesDir; + + /** + * Creates a new FileNotesStorage. + * + * @param notesDir The directory to store notes in + */ + public FileNotesStorage(Path notesDir) { + this.notesDir = notesDir; + createNotesDirectory(); + } + + private void createNotesDirectory() { + try { + Files.createDirectories(notesDir); + } catch (IOException e) { + logger.warning("Failed to create notes directory: " + e.getMessage()); + } + } + + private Path getNoteFilePath(Person person) { + return notesDir.resolve(person.getId().value + ".txt"); + } + + @Override + public String readNote(Person person) throws IOException { + Path notePath = getNoteFilePath(person); + if (Files.exists(notePath)) { + return Files.readString(notePath); + } + return ""; + } + + @Override + public void saveNote(Person person, String content) throws IOException { + Files.createDirectories(notesDir); + Files.writeString(getNoteFilePath(person), content); + } + + @Override + public boolean deleteNote(Person person) throws IOException { + Path notePath = getNoteFilePath(person); + if (Files.exists(notePath)) { + Files.delete(notePath); + return true; + } + return false; + } + + @Override + public void deleteAllNotes() throws IOException { + if (Files.exists(notesDir)) { + Files.list(notesDir).filter(path -> path.toString().endsWith(".txt")).forEach(path -> { + try { + Files.delete(path); + } catch (IOException e) { + logger.warning("Failed to delete note: " + e.getMessage()); + } + }); + } + } +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java index bd1ca0f56c8..9a0253e4228 100644 --- a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java +++ b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java @@ -14,6 +14,7 @@ import seedu.address.model.person.Email; import seedu.address.model.person.Name; import seedu.address.model.person.Person; +import seedu.address.model.person.PersonId; import seedu.address.model.person.Phone; import seedu.address.model.tag.Tag; @@ -29,6 +30,7 @@ class JsonAdaptedPerson { private final String email; private final String address; private final List tags = new ArrayList<>(); + private final String personId; /** * Constructs a {@code JsonAdaptedPerson} with the given person details. @@ -36,7 +38,7 @@ class JsonAdaptedPerson { @JsonCreator public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone") String phone, @JsonProperty("email") String email, @JsonProperty("address") String address, - @JsonProperty("tags") List tags) { + @JsonProperty("tags") List tags, @JsonProperty("personId") String personId) { this.name = name; this.phone = phone; this.email = email; @@ -44,6 +46,7 @@ public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone if (tags != null) { this.tags.addAll(tags); } + this.personId = personId; } /** @@ -57,6 +60,7 @@ public JsonAdaptedPerson(Person source) { tags.addAll(source.getTags().stream() .map(JsonAdaptedTag::new) .collect(Collectors.toList())); + personId = source.getId().value; } /** @@ -101,9 +105,24 @@ public Person toModelType() throws IllegalValueException { throw new IllegalValueException(Address.MESSAGE_CONSTRAINTS); } final Address modelAddress = new Address(address); + if (personId == null) { + throw new IllegalValueException( + String.format(MISSING_FIELD_MESSAGE_FORMAT, PersonId.class.getSimpleName())); + } + if (!PersonId.isValidId(personId)) { + throw new IllegalValueException(PersonId.MESSAGE_CONSTRAINTS); + } + final PersonId modelPersonId = new PersonId(personId); final Set modelTags = new HashSet<>(personTags); - return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags); + return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags, modelPersonId); + } + + /** + * Returns the personId of the person. + */ + public String getPersonId() { + return personId; } } diff --git a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java index 5efd834091d..bbeff4bf99e 100644 --- a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java +++ b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java @@ -12,6 +12,7 @@ import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.person.Person; +import seedu.address.model.person.PersonId; /** * An Immutable AddressBook that is serializable to JSON format. @@ -54,6 +55,13 @@ public AddressBook toModelType() throws IllegalValueException { } addressBook.addPerson(person); } + + // Set the id counter to the maximum id in the address book + int maxId = persons.stream() + .mapToInt(jsonAdaptedPerson -> Integer.parseInt(jsonAdaptedPerson.getPersonId())) + .max() + .orElse(0); + PersonId.setCounter(maxId); return addressBook; } diff --git a/src/main/java/seedu/address/storage/NotesStorage.java b/src/main/java/seedu/address/storage/NotesStorage.java new file mode 100644 index 00000000000..8346c642418 --- /dev/null +++ b/src/main/java/seedu/address/storage/NotesStorage.java @@ -0,0 +1,41 @@ +package seedu.address.storage; + +import java.io.IOException; + +import seedu.address.model.person.Person; + +/** + * Represents a storage for Notes. + */ +public interface NotesStorage { + + /** + * Reads the note for a person. + * @param person The person to read the note for. + * @return The content of the note. + * @throws IOException If an error occurs during reading. + */ + String readNote(Person person) throws IOException; + + /** + * Saves a note for a person. + * @param person The person to save the note for. + * @param content The content of the note. + * @throws IOException If an error occurs during saving. + */ + void saveNote(Person person, String content) throws IOException; + + /** + * Deletes the note for a person if it exists. + * @param person The person to delete the note for. + * @return true if the note was found and successfully deleted, false if no note was found. + * @throws IOException If an error occurs during deletion. + */ + boolean deleteNote(Person person) throws IOException; + + /** + * Deletes all notes. + * @throws IOException If an error occurs during deletion. + */ + void deleteAllNotes() throws IOException; +} diff --git a/src/main/java/seedu/address/storage/Storage.java b/src/main/java/seedu/address/storage/Storage.java index 9fba0c7a1d6..f7f6eba5b85 100644 --- a/src/main/java/seedu/address/storage/Storage.java +++ b/src/main/java/seedu/address/storage/Storage.java @@ -8,11 +8,12 @@ import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.ReadOnlyUserPrefs; import seedu.address.model.UserPrefs; +import seedu.address.model.person.Person; /** * API of the Storage component */ -public interface Storage extends AddressBookStorage, UserPrefsStorage { +public interface Storage extends AddressBookStorage, UserPrefsStorage, NotesStorage { @Override Optional readUserPrefs() throws DataLoadingException; @@ -29,4 +30,15 @@ public interface Storage extends AddressBookStorage, UserPrefsStorage { @Override void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException; + @Override + String readNote(Person person) throws IOException; + + @Override + void saveNote(Person person, String content) throws IOException; + + @Override + boolean deleteNote(Person person) throws IOException; + + @Override + void deleteAllNotes() throws IOException; } diff --git a/src/main/java/seedu/address/storage/StorageManager.java b/src/main/java/seedu/address/storage/StorageManager.java index 8b84a9024d5..266b22d5eb4 100644 --- a/src/main/java/seedu/address/storage/StorageManager.java +++ b/src/main/java/seedu/address/storage/StorageManager.java @@ -2,6 +2,7 @@ import java.io.IOException; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Optional; import java.util.logging.Logger; @@ -10,6 +11,7 @@ import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.ReadOnlyUserPrefs; import seedu.address.model.UserPrefs; +import seedu.address.model.person.Person; /** * Manages storage of AddressBook data in local storage. @@ -19,13 +21,26 @@ public class StorageManager implements Storage { private static final Logger logger = LogsCenter.getLogger(StorageManager.class); private AddressBookStorage addressBookStorage; private UserPrefsStorage userPrefsStorage; + private final NotesStorage notesStorage; /** * Creates a {@code StorageManager} with the given {@code AddressBookStorage} and {@code UserPrefStorage}. */ public StorageManager(AddressBookStorage addressBookStorage, UserPrefsStorage userPrefsStorage) { + this(addressBookStorage, userPrefsStorage, + new FileNotesStorage(Paths.get("data/notes"))); + } + + /** + * Creates a {@code StorageManager} with the given {@code AddressBookStorage}, {@code UserPrefStorage} and + * {@code NotesStorage}. + */ + public StorageManager(AddressBookStorage addressBookStorage, + UserPrefsStorage userPrefsStorage, + NotesStorage notesStorage) { this.addressBookStorage = addressBookStorage; this.userPrefsStorage = userPrefsStorage; + this.notesStorage = notesStorage; } // ================ UserPrefs methods ============================== @@ -75,4 +90,25 @@ public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) thro addressBookStorage.saveAddressBook(addressBook, filePath); } + // ================ Notes methods ============================== + + @Override + public String readNote(Person person) throws IOException { + return notesStorage.readNote(person); + } + + @Override + public void saveNote(Person person, String content) throws IOException { + notesStorage.saveNote(person, content); + } + + @Override + public boolean deleteNote(Person person) throws IOException { + return notesStorage.deleteNote(person); + } + + @Override + public void deleteAllNotes() throws IOException { + notesStorage.deleteAllNotes(); + } } diff --git a/src/main/java/seedu/address/ui/DialogBox.java b/src/main/java/seedu/address/ui/DialogBox.java new file mode 100644 index 00000000000..1cf80e37f0e --- /dev/null +++ b/src/main/java/seedu/address/ui/DialogBox.java @@ -0,0 +1,47 @@ +package seedu.address.ui; + +import static java.util.Objects.requireNonNull; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.Region; + +/** + * A custom control using FXML that represents a dialog box. + */ +public class DialogBox extends UiPart { + private static final String FXML = "DialogBox.fxml"; + @FXML + private Label text; + + /** + * Constructor for DialogBox. + * @param s + */ + public DialogBox(String s) { + super(FXML); + text.setText(s); + } + + public void setDialogBox(String textToSet) { + requireNonNull(textToSet); + text.setText(textToSet); + } + + + + public static DialogBox getUserDialog(String text) { + return new DialogBox(text); + } + + public static DialogBox getScoopBookDialog(String text) { + var db = new DialogBox(text); + setScoopBookDialogStyle(db); + return db; + } + private static void setScoopBookDialogStyle(DialogBox db) { + db.getRoot().setScaleX(-1); + db.text.setScaleX(-1); + db.text.styleProperty().set("-fx-background-color: #9c9c9c"); + } +} diff --git a/src/main/java/seedu/address/ui/HelpWindow.java b/src/main/java/seedu/address/ui/HelpWindow.java index 3f16b2fcf26..07eb84eaf77 100644 --- a/src/main/java/seedu/address/ui/HelpWindow.java +++ b/src/main/java/seedu/address/ui/HelpWindow.java @@ -1,10 +1,15 @@ package seedu.address.ui; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.util.logging.Logger; import javafx.fxml.FXML; import javafx.scene.control.Button; import javafx.scene.control.Label; +import javafx.scene.control.TextArea; import javafx.scene.input.Clipboard; import javafx.scene.input.ClipboardContent; import javafx.stage.Stage; @@ -15,7 +20,7 @@ */ public class HelpWindow extends UiPart { - public static final String USERGUIDE_URL = "https://se-education.org/addressbook-level3/UserGuide.html"; + public static final String USERGUIDE_URL = "https://ay2425s2-cs2103t-w13-1.github.io/tp/UserGuide.html"; public static final String HELP_MESSAGE = "Refer to the user guide: " + USERGUIDE_URL; private static final Logger logger = LogsCenter.getLogger(HelpWindow.class); @@ -27,6 +32,9 @@ public class HelpWindow extends UiPart { @FXML private Label helpMessage; + @FXML + private TextArea localGuideTextArea; + /** * Creates a new HelpWindow. * @@ -35,6 +43,7 @@ public class HelpWindow extends UiPart { public HelpWindow(Stage root) { super(FXML, root); helpMessage.setText(HELP_MESSAGE); + loadLocalGuide(); } /** @@ -99,4 +108,37 @@ private void copyUrl() { url.putString(USERGUIDE_URL); clipboard.setContent(url); } + + /** + * Loads the local user guide from a text file + */ + private void loadLocalGuide() { + String content = loadResourceFile("help/local_userguide.txt"); + if (content != null) { + localGuideTextArea.setText(content); + } else { + localGuideTextArea.setText("Local user guide not found."); + } + } + + /** + * Loads a resource file from the classpath. + * + * @param filename The filename of the resource. + * @return The content of the file, or null if an error occurs. + */ + public String loadResourceFile(String filename) { + StringBuilder content = new StringBuilder(); + try (InputStream inputStream = getClass().getResourceAsStream("/" + filename); + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream))) { + String line; + while ((line = reader.readLine()) != null) { + content.append(line).append("\n"); + } + return content.toString(); + } catch (IOException | NullPointerException e) { + logger.warning("Error loading resource file: " + filename); + return null; + } + } } diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java index 79e74ef37c0..99c6842256a 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/address/ui/MainWindow.java @@ -5,10 +5,12 @@ import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.MenuItem; +import javafx.scene.control.ScrollPane; import javafx.scene.control.TextInputControl; import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyEvent; import javafx.scene.layout.StackPane; +import javafx.scene.layout.VBox; import javafx.stage.Stage; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; @@ -16,6 +18,7 @@ import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Person; /** * The Main Window. Provides the basic application layout containing @@ -34,9 +37,11 @@ public class MainWindow extends UiPart { private PersonListPanel personListPanel; private ResultDisplay resultDisplay; private HelpWindow helpWindow; - + private NoteWindowHandler noteWindowHandler; @FXML private StackPane commandBoxPlaceholder; + @FXML + private ScrollPane scrollPane; @FXML private MenuItem helpMenuItem; @@ -45,11 +50,14 @@ public class MainWindow extends UiPart { private StackPane personListPanelPlaceholder; @FXML - private StackPane resultDisplayPlaceholder; + private VBox resultDisplayPlaceholder; @FXML private StackPane statusbarPlaceholder; + @FXML + private DialogBox dialogBox; + /** * Creates a {@code MainWindow} with the given {@code Stage} and {@code Logic}. */ @@ -66,6 +74,7 @@ public MainWindow(Stage primaryStage, Logic logic) { setAccelerators(); helpWindow = new HelpWindow(); + noteWindowHandler = new NoteWindowHandler(logic); } public Stage getPrimaryStage() { @@ -109,12 +118,14 @@ private void setAccelerator(MenuItem menuItem, KeyCombination keyCombination) { /** * Fills up all the placeholders of this window. */ + @SuppressWarnings("checkstyle:Regexp") void fillInnerParts() { personListPanel = new PersonListPanel(logic.getFilteredPersonList()); personListPanelPlaceholder.getChildren().add(personListPanel.getRoot()); - resultDisplay = new ResultDisplay(); - resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot()); + + resultDisplayPlaceholder.heightProperty().addListener((observable) -> scrollPane.setVvalue(1.0)); + scrollPane.setContent(resultDisplayPlaceholder); StatusBarFooter statusBarFooter = new StatusBarFooter(logic.getAddressBookFilePath()); statusbarPlaceholder.getChildren().add(statusBarFooter.getRoot()); @@ -147,6 +158,14 @@ public void handleHelp() { } } + /** + * Opens the note window for a specific person. + */ + @FXML + public void handleNote(Person person) { + noteWindowHandler.openNoteWindow(person); + } + void show() { primaryStage.show(); } @@ -160,6 +179,7 @@ private void handleExit() { (int) primaryStage.getX(), (int) primaryStage.getY()); logic.setGuiSettings(guiSettings); helpWindow.hide(); + noteWindowHandler.closeAllNoteWindows(true); primaryStage.hide(); } @@ -176,8 +196,14 @@ private CommandResult executeCommand(String commandText) throws CommandException try { CommandResult commandResult = logic.execute(commandText); logger.info("Result: " + commandResult.getFeedbackToUser()); + // prints the user's input + resultDisplayPlaceholder.getChildren().add(DialogBox.getUserDialog(commandText).getRoot()); + // print the result of the command + resultDisplayPlaceholder.getChildren().add( + DialogBox.getScoopBookDialog(commandResult.getFeedbackToUser()).getRoot()); resultDisplay.setFeedbackToUser(commandResult.getFeedbackToUser()); + if (commandResult.isShowHelp()) { handleHelp(); } @@ -186,10 +212,30 @@ private CommandResult executeCommand(String commandText) throws CommandException handleExit(); } + if (commandResult.isShowNote()) { + handleNote(commandResult.getTargetPerson()); + } + + switch (commandResult.shouldDeleteNote()) { + case CLOSE_ONE: + noteWindowHandler.closeNoteWindowWithoutSaving(commandResult.getTargetPerson()); + break; + case CLOSE_ALL: + // For clear command, close all note windows without saving + noteWindowHandler.closeAllNoteWindows(false); + break; + default: + break; + } return commandResult; } catch (CommandException | ParseException e) { logger.info("An error occurred while executing command: " + commandText); resultDisplay.setFeedbackToUser(e.getMessage()); + // prints the user's input + resultDisplayPlaceholder.getChildren().add(DialogBox.getUserDialog(commandText).getRoot()); + // print the result of the command + resultDisplayPlaceholder.getChildren().add( + DialogBox.getScoopBookDialog(e.getMessage()).getRoot()); throw e; } } diff --git a/src/main/java/seedu/address/ui/NoteWindow.java b/src/main/java/seedu/address/ui/NoteWindow.java new file mode 100644 index 00000000000..2c4ac422eca --- /dev/null +++ b/src/main/java/seedu/address/ui/NoteWindow.java @@ -0,0 +1,153 @@ +package seedu.address.ui; + +import java.io.IOException; +import java.util.logging.Logger; + +import javafx.fxml.FXML; +import javafx.scene.control.TextArea; +import javafx.stage.Stage; +import seedu.address.commons.core.LogsCenter; +import seedu.address.logic.Logic; +import seedu.address.model.person.Person; + +/** + * Controller for a note page + */ +public class NoteWindow extends UiPart { + + private static final Logger logger = LogsCenter.getLogger(NoteWindow.class); + private static final String FXML = "NoteWindow.fxml"; + + @FXML + private TextArea noteTextArea; + + private Person person; + private Logic logic; + + /** + * Creates a new NoteWindow. + * + * @param root Stage to use as the root of the NoteWindow. + */ + public NoteWindow(Stage root) { + super(FXML, root); + } + + /** + * Creates a new NoteWindow with a new Stage. + */ + public NoteWindow() { + this(new Stage()); + } + + /** + * Creates a new NoteWindow for a specific person. + * + * @param person The person to create notes for + */ + public NoteWindow(Person person, Logic logic) { + this(); + this.person = person; + this.logic = logic; + if (person != null) { + getRoot().setTitle("Notes for " + person.getName().fullName); + loadExistingNotes(); + } + } + + /** + * Sets up the close and hide handler for the Note window. + */ + public void setupCloseAndHideHandler(Runnable handler) { + getRoot().setOnCloseRequest(event -> { + saveNotes(); + handler.run(); + }); + getRoot().setOnHidden(event -> { + saveNotes(); + handler.run(); + }); + } + /** + * Closes the notes for a specific person if available. + */ + public void closeWithoutSaving() { + getRoot().setOnCloseRequest(event -> {}); + getRoot().setOnHidden(event -> {}); + getRoot().close(); + } + + /** + * Loads existing notes for a specific person if available. + */ + private void loadExistingNotes() { + try { + String content = logic.readNote(person); + noteTextArea.setText(content); + logger.info("Loaded notes for person: " + person.getName().fullName); + } catch (IOException e) { + logger.warning("Failed to load notes for person: " + person.getName().fullName); + } + } + + /** + * Saves the notes for a specific person to a file. + */ + private void saveNotes() { + try { + logic.saveNote(person, noteTextArea.getText()); + logger.info("Saved notes for person: " + person.getName().fullName); + } catch (IOException e) { + logger.warning("Failed to save notes: " + e.getMessage()); + } + } + + /** + * Delete the notes for a specific person if available. + */ + private void deleteNotes() { + try { + logic.deleteNote(person); + logger.info("Deleted notes for person: " + person.getName().fullName); + } catch (IOException e) { + logger.warning("Failed to delete notes: " + e.getMessage()); + } + } + + /** + * Shows the Note window. + */ + public void show() { + logger.fine("Showing Note window."); + getRoot().show(); + getRoot().centerOnScreen(); + } + + /** + * Hides the Note window. + */ + public void hide() { + getRoot().hide(); + } + + /** + * Focuses on the Note window. + */ + public void focus() { + getRoot().requestFocus(); + } + + /** + * Sets the text in the note area. + */ + public void setText(String text) { + noteTextArea.setText(text); + } + + /** + * Gets the text from the note area. + */ + public String getText() { + return noteTextArea.getText(); + } +} diff --git a/src/main/java/seedu/address/ui/NoteWindowHandler.java b/src/main/java/seedu/address/ui/NoteWindowHandler.java new file mode 100644 index 00000000000..fc02b70f393 --- /dev/null +++ b/src/main/java/seedu/address/ui/NoteWindowHandler.java @@ -0,0 +1,72 @@ +package seedu.address.ui; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import seedu.address.logic.Logic; +import seedu.address.model.person.Person; +/** + * Handles the opening and closing of NoteWindows. + */ +public class NoteWindowHandler { + private final Map openNoteWindows = new HashMap<>(); + private final Logic logic; + public NoteWindowHandler(Logic logic) { + this.logic = logic; + } + + /** + * Opens a NoteWindow for the specified person. If a NoteWindow is already open for the person, it will be focused. + * @param person + */ + public void openNoteWindow(Person person) { + if (openNoteWindows.containsKey(person)) { + NoteWindow existingNoteWindow = openNoteWindows.get(person); + existingNoteWindow.focus(); + } else { + NoteWindow newNoteWindow = new NoteWindow(person, logic); + newNoteWindow.setupCloseAndHideHandler(() -> closeNoteWindowWithSaving(person)); + openNoteWindows.put(person, newNoteWindow); + newNoteWindow.show(); + } + } + + /** + * Closes the NoteWindow for the specified person, saving the notes. + * @param person + */ + public void closeNoteWindowWithSaving(Person person) { + // Since the notes are set to save on close/hide by default, + // we only need to close/hide the window to save the notes. + NoteWindow noteWindow = openNoteWindows.get(person); + if (noteWindow != null) { + noteWindow.hide(); + openNoteWindows.remove(person); + } + } + /** + * Closes the NoteWindow for the specified person without saving the notes. + * @param person + */ + public void closeNoteWindowWithoutSaving(Person person) { + // Since the notes are set to save on close/hide by default, + // we need to reset the handler, then close/hide the window to save the notes. + NoteWindow noteWindow = openNoteWindows.get(person); + if (noteWindow != null) { + noteWindow.closeWithoutSaving(); + openNoteWindows.remove(person); + } + } + + /** + * Closes all open NoteWindows. + */ + public void closeAllNoteWindows(boolean isToSave) { + if (isToSave) { + new ArrayList<>(openNoteWindows.keySet()).forEach(this::closeNoteWindowWithSaving); + } else { + new ArrayList<>(openNoteWindows.keySet()).forEach(this::closeNoteWindowWithoutSaving); + } + } +} diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java index 094c42cda82..483569a8498 100644 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ b/src/main/java/seedu/address/ui/PersonCard.java @@ -1,5 +1,9 @@ package seedu.address.ui; +import static seedu.address.model.person.Address.EMPTY_ADDRESS; +import static seedu.address.model.person.Email.EMPTY_EMAIL; +import static seedu.address.model.person.Phone.EMPTY_PHONE; + import java.util.Comparator; import javafx.fxml.FXML; @@ -52,8 +56,23 @@ public PersonCard(Person person, int displayedIndex) { phone.setText(person.getPhone().value); address.setText(person.getAddress().value); email.setText(person.getEmail().value); + + // Apply visibility settings for optional fields + updateVisibility(phone, phone.getText(), EMPTY_PHONE); + updateVisibility(address, address.getText(), EMPTY_ADDRESS); + updateVisibility(email, email.getText(), EMPTY_EMAIL); + person.getTags().stream() .sorted(Comparator.comparing(tag -> tag.tagName)) .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); } + + /** + * Updates the visibility of a label based on whether the field is empty. + */ + public void updateVisibility(Label label, String text, String emptyText) { + boolean isEmpty = text.equals(emptyText); + label.setVisible(!isEmpty); + label.setManaged(!isEmpty); + } } diff --git a/src/main/java/seedu/address/ui/UiManager.java b/src/main/java/seedu/address/ui/UiManager.java index fdf024138bc..d638530f59e 100644 --- a/src/main/java/seedu/address/ui/UiManager.java +++ b/src/main/java/seedu/address/ui/UiManager.java @@ -20,7 +20,7 @@ public class UiManager implements Ui { public static final String ALERT_DIALOG_PANE_FIELD_ID = "alertDialogPane"; private static final Logger logger = LogsCenter.getLogger(UiManager.class); - private static final String ICON_APPLICATION = "/images/address_book_32.png"; + private static final String ICON_APPLICATION = "/images/ScoopBook.png"; private Logic logic; private MainWindow mainWindow; diff --git a/src/main/resources/help/local_userguide.txt b/src/main/resources/help/local_userguide.txt new file mode 100644 index 00000000000..ad9866fe8da --- /dev/null +++ b/src/main/resources/help/local_userguide.txt @@ -0,0 +1,224 @@ +How to use ScoopBook? + +1. Adding a contact: add + +Adds a contact to the address book. + +Format: add n/NAME [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG] + +Examples: +- add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01 +- add n/Betsy Crowe e/betsycrowe@example.com a/Newgate Prison p/1234567 t/Criminal + +Note: +- The add command must have a name, and one of the following fields: phone number, email, address. + i.e. 'add n/Johnny Appleseed' does not work because there is no phone number, email or address. +- A person can have any number of tags (including 0). +- A person's name can only contain alphanumeric characters (numbers or letters only), whitespaces, and the following + special characters , ( ) @ . - ' +- A person's tags can only contain alphanumeric characters (numbers or letters only, no special characters). +- If a contact is added with the following values, they will not be displayed in the contact list, + as they are used as internal placeholders: + - Phone Number: `000` + - Email: `unknown@example.com` + - Address: `Unknown address` + This ensures that every contact has a placeholder value for these fields if left empty. + +2. Listing all contacts: list + +Shows a list of all contacts in the address book. + +Format: list + +3. Editing a contact: edit + +Edits an existing person in the address book at specified index. + +Format: edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG]... + +Note: +- Edits the contact at the specified INDEX. The index refers to the index number shown in the displayed contact 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 contact’s tags by typing t/ without specifying any tags after it. +- Similar to the add command, the aforementioned placeholder values will not be displayed in the contact list. + + +Examples: +- 'edit 1 p/91234567 e/johndoe@example.com' edits the phone number and email address of the 1st person to "91234567" and + "johndoe@example.com". +- 'edit 2 n/Betsy Crower t/' edits the name of the 2nd person to "Betsy Crower" and clears all existing tags. + +4. Locating contacts by name: find + +Finds persons whose names contain any of the given keywords. + +Format: find KEYWORD [MORE_KEYWORDS] + +Note: +- The search is case-insensitive. For example, "hans" will match "Hans". +- The order of the keywords does not matter. For example, "Hans Bo" will match "Bo Hans". +- Only the name is searched. +- Only full words will be matched. For example, "Han" will not match "Hans". +- Persons matching at least one keyword will be returned (OR search). For example, "Hans Bo" will return "Hans Gruber" + and "Bo Yang". + +5. Deleting a person: delete + +Deletes the specified person from the address book. + +Format: delete INDEX + +Note: +- Deletes 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, ...). + +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. + +6. Adding tags to a contact: addtag + +Adds the tag(s) typed in to the specified person. + +Format: addtag INDEX t/TAG1 [t/MORETAGS] + +Note: +- Adds the specified tags to 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, ... +- Multiple tags in a single addtag command is supported. i.e. 'addtag 1 t/Witness t/HomeAffairs' will tag the 1st person + with both "Witness" and "HomeAffairs". +- Tags can only contain alphanumeric characters (numbers or letters only, no special characters or spaces). +- Tags are case-sensitive. i.e. 'addtag 1 t/witness' will add the tag "witness" while 'addtag 1 t/Witness' + will add the tag "Witness". + +Examples: +- 'list' followed by 'addtag 2 t/Education' tags the 2nd person with "Education" in the address book. +- 'find Betsy' followed by 'addtag 1 t/Victim' tags the 1st person in the results of the find command with "Victim". + +7. Removing tag from a contact: removetag + +Removes the specified tag(s) from the specified person. + +Format: removetag INDEX t/TAG1 [t/MORE_TAGS] + +Note: +- Removes the specified tag(s) from 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, ... +- Multiple tags in a single removetag command is supported. i.e. 'removetag 1 t/Witness t/Local' will remove + both the "Witness" and "Local" tag for the 1st person. +- Tags are case-sensitive. The typed tag must match the tag on the person exactly. i.e. 'removetag 1 t/witness' will + not remove the tag "Witness". + +Examples: +- 'list' followed by 'removetag 2 t/Witness' removes the "Witness" tag from the 2nd person in the address book. +- 'find Betsy' followed by 'removetag 1 t/Victim' removes the "Victim" tag from the 1st person in the results of + the find command. + +8. Finding people with tags: findtag + +Find persons who have all the specified tag(s). + +Format: findtag t/TAG1 [t/MORE_TAGS] + +Note: +- The searching of tags is case-insensitive. e.g "friends" will match "Friends" +- The order of the tags does not matter. i.e. As long as the person has the listed tags, they will be shown. +- Only the tags are searched. +- Only full words will be matched e.g. "Friend" will not match "Friends" +- Only persons matching all the tags will be returned (i.e. AND search). + +Examples: +- 'findtag t/witness' returns people with tag "witness", "Witness", "witNeSs" (due to case insensitivity). +- 'findtag t/witness t/HomeAffairs' returns people with tag "Witness" and "HomeAffairs" only. + +9. Opening Note for Person: note + +Opens a window for the user to add notes to the person at the specified INDEX. + +Format: note INDEX + +Note: +- Opens a window for the user to add notes to the person at the specified INDEX. +- Please use only this opened window to edit the note. +- The index refers to the index number shown in the displayed person list. +- The index must be a positive integer 1, 2, 3, ... +- The note will be saved when the window is closed. + +Examples: +* 'list' followed by 'note 2' opens a note window for the 2nd person in the address book. +* 'find Betsy' followed by 'note 1' opens a note window for the 1st person in the results of the find command. + +10. Deleting Note from Person: deletenote + +Deletes the note from the person. + +Format: deletenote INDEX + +Note: +- Deletes note for 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, ... + +11. Exporting your contacts: export + +Exports the contacts in a .json file to the target path. + +Format: export TARGET_PATH + +Note: +- The export command only exports your contacts. It does not export the notes tagged to them. +- Before executing the export command, add at least 1 contact using the add command. +- Export command is case-insensitive. If 'sAmPle.json' already exists (in the folder the 'ScoopBook.jar' + is located at), export sample.json` will overwrite 'sAmPle.json'. +- Ensure that there are no special characters (E.g. `*!<>`) or spaces in the TARGET_PATH. +- TIP: If you are running into issues with TARGET_PATH, use export sample.json to export it directly to the root + folder with of the ScoopBook.jar file. Then, move the .json file to wherever you want it to be. + +Examples: +- For Windows: 'export C:/Users/username/Desktop/MyContacts.json' saves the json file as MyContacts.json in the + Users/username/Desktop folder. +- For macOS: 'export /Users/username/Desktop/MyContacts.json' saves the json file as MyContacts.json in the + Users/username/Desktop folder. +- For Linux: 'export /home/user/desktop/MyContacts.json' saves the json file as MyContacts.json in the + home/user/desktop folder. +- For all OS: 'export Contacts.json' saves the json file as Contacts.json in the root folder of where + ScoopBook.jar is located at. + +12. Importing your contacts: import + +Imports the external .json file from target path into the application. + +Format: import TARGET_PATH + +Note: +- WARNING: This command overwrites existing contacts and remove all notes. +- Only import .json files exported using the export command. +- Ensure that there are no special characters (E.g. *!<>) or spaces in the TARGET_PATH. + +Examples: +- For Windows: 'import C:/Users/username/Desktop/MyContacts.json' imports the json file from MyContacts.json in the + Users/username/Desktop folder. +- For macOS: 'import /Users/username/Desktop/MyContacts.json' imports the json file from MyContacts.json in the + Users/username/Desktop folder. +- For Linux: 'import /home/user/desktop/MyContacts.json' imports the json file from MyContacts.json in the + home/user/desktop folder. +- For all OS: 'import Contacts.json' imports the json file named Contacts.json from the root folder of where + ScoopBook.jar is located at. + +13. Clearing all entries: clear + +Warning: this clears all contacts, notes & .txt files from the address book. + +Format: clear + +14. Exiting the program: exit + +Exits the program. + +Format: exit diff --git a/src/main/resources/images/ScoopBook.png b/src/main/resources/images/ScoopBook.png new file mode 100644 index 00000000000..4401557641a Binary files /dev/null and b/src/main/resources/images/ScoopBook.png differ diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index 36e6b001cd8..e19f5fe8328 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -100,11 +100,11 @@ } .list-cell:filled:even { - -fx-background-color: #3c3e3f; + -fx-background-color: #3f3c3f; } .list-cell:filled:odd { - -fx-background-color: #515658; + -fx-background-color: #585156; } .list-cell:filled:selected { @@ -344,7 +344,7 @@ #tags .label { -fx-text-fill: white; - -fx-background-color: #3e7b91; + -fx-background-color: #eb73dd; -fx-padding: 1 3 1 3; -fx-border-radius: 2; -fx-background-radius: 2; diff --git a/src/main/resources/view/DialogBox.fxml b/src/main/resources/view/DialogBox.fxml new file mode 100644 index 00000000000..c8fe5db94c1 --- /dev/null +++ b/src/main/resources/view/DialogBox.fxml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + diff --git a/src/main/resources/view/HelpWindow.css b/src/main/resources/view/HelpWindow.css index 17e8a8722cd..09e61156a28 100644 --- a/src/main/resources/view/HelpWindow.css +++ b/src/main/resources/view/HelpWindow.css @@ -1,4 +1,4 @@ -#copyButton, #helpMessage { +#copyButton, #helpMessage, #noInternetMessage { -fx-text-fill: white; } diff --git a/src/main/resources/view/HelpWindow.fxml b/src/main/resources/view/HelpWindow.fxml index e01f330de33..d9b23cec784 100644 --- a/src/main/resources/view/HelpWindow.fxml +++ b/src/main/resources/view/HelpWindow.fxml @@ -1,15 +1,15 @@ - - - - - - - - + + + + + + + - + @@ -19,26 +19,32 @@ - + - - + + + + + + +