diff --git a/1 b/1 new file mode 100644 index 00000000000..e8465397569 --- /dev/null +++ b/1 @@ -0,0 +1,6 @@ +Merge remote-tracking branch 'origin' into branch-search-birthday-appointment +# testease enter a commit message to explain why this merge is necessary, +# especially if it merges an updated upstream into a topic branch. +# +# Lines starting with '#' will be ignored, and an empty message aborts +# the commit. diff --git a/README.md b/README.md index 16208adb9b6..961965d0e0c 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,56 @@ -[![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/AY2425S1-CS2103T-W10-2/tp/workflows/Java%20CI/badge.svg)](https://github.com/AY2425S1-CS2103T-W08-3/tp/actions) + +[![Codecov](https://codecov.io/gh/AY2425S1-CS2103T-W10-2/tp/graph/badge.svg?token=134QRO3UI3)](https://codecov.io/gh/AY2425S1-CS2103T-W10-2/tp) ![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. +# AgentConnect + +## 1. What is AgentConnect? + +**AgentConnect** is a comprehensive contact management tool specifically designed for professionals who need to manage client relationships efficiently, particularly in the insurance and financial sectors. It functions like an address book but offers enhanced features tailored to the needs of agents, such as tracking client policies, managing important dates (e.g., birthdates, payment deadlines, appointments), and organizing clients based on various categories. + +## 2. How AgentConnect Can Help You + +AgentConnect is designed to simplify the often complex task of managing client information and interactions. With this tool, users can: +- Keep client information organized and accessible. +- Never miss important dates such as birthdays, payment deadlines, or scheduled appointments. +- Organize clients based on their policies or other customizable categories. +- Efficiently manage and update client data with an intuitive interface. +- Save time and ensure accuracy by automating reminders and sorting contacts based on key criteria. + +Whether you're an insurance agent, financial advisor, or anyone who needs to manage a large portfolio of clients, AgentConnect helps you stay on top of your client relationships and ensures that you can focus on providing the best service possible. + +## 3. Features + +AgentConnect offers a robust set of features to manage your clients effectively: + +### - Add Contact +Easily add new client details, including name, contact information, policies they hold, birthdate, and more. + +### - Delete Contact +Remove outdated or incorrect client information from the system with a simple delete feature. + +### - Edit Contact +Update client details when necessary, ensuring that all information remains accurate and up-to-date. + +### - Retrieve Birthdates +Quickly access client birthdates to schedule reminders for personal outreach or to send birthday greetings, helping maintain strong relationships. + +### - Next Payment Date +Keep track of important payment deadlines, ensuring you never miss a client's next premium or fee payment. This feature helps in reminding clients to stay up-to-date on their financial commitments. + +### - Next Appointment Date +Set and retrieve the next appointment or meeting dates for each client, ensuring your schedule stays organized and you never miss a client meeting. + +### - Categorize Client According to Policies +Organize your clients into categories based on the policies they have purchased. This makes it easier to filter and sort clients, allowing you to tailor communication and service accordingly. + +### - Sort Contacts +Sort your client list based on different criteria, such as name, policy type, payment date, or appointment date, ensuring that you can quickly access the information you need. + + +## More about it +* For user and developer documentation**, head over to **[AgentConnect Documentation](https://ay2425s1-cs2103t-w10-3.github.io/tp/)** + +* This project is based on the AddressBook-Level3 project created by the [SE-EDU initiative](https://se-education.org) diff --git a/build.gradle b/build.gradle index 0db3743584e..68af84b3ab2 100644 --- a/build.gradle +++ b/build.gradle @@ -23,6 +23,18 @@ checkstyle { test { useJUnitPlatform() finalizedBy jacocoTestReport + jvmArgs '--add-opens', 'javafx.graphics/com.sun.javafx.application=ALL-UNNAMED' + testLogging { + events "passed", "skipped", "failed" + } + options { + // Automatically answer 'yes' to any prompts + systemProperty 'org.gradle.internal.launcher.welcomeMessageEnabled', 'false' + } +} + +task simulateInput(type: Exec) { + commandLine 'sh', '-c', 'echo y | ./gradlew test' } task coverage(type: JacocoReport) { @@ -61,12 +73,21 @@ dependencies { implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: '2.7.4' testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: jUnitVersion + testImplementation 'org.testfx:testfx-junit5:4.0.16-alpha' + testImplementation 'org.testfx:testfx-core:4.0.16-alpha' + testImplementation 'org.mockito:mockito-core:4.0.0' + testImplementation 'org.mockito:mockito-inline:4.0.0' testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: jUnitVersion } shadowJar { - archiveFileName = 'addressbook.jar' + archiveFileName = 'agentconnect.jar' +} + +run { + enableAssertions = true } defaultTasks 'clean', 'test' + diff --git a/docs/AboutUs.md b/docs/AboutUs.md index ff3f04abd02..1ae01ea8f03 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -9,51 +9,65 @@ You can reach us at the email `seer[at]comp.nus.edu.sg` ## Project team -### John Doe - +## Product Name: AgentConnect + +Target User: Insurance agents tracking their clients + +Value proposition: provides quick and efficient access to client details, tailored for insurance agents who need a streamlined interface to manage contacts, track policy updates, and schedule client follow-ups. + + + +### Mant Koh En Wei + + [[homepage](http://www.comp.nus.edu.sg/~damithch)] -[[github](https://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](https://github.com/m-kew)] +[[portfolio](team/m-kew.md)] -* Role: Project Advisor +* Role: Developer +* Responsibilities: Team Lead -### Jane Doe - -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +### Denon Chong -* Role: Team Lead -* Responsibilities: UI + + + +[[github](https://github.com/ssirmentos)] +[[portfolio](team/ssirmentos.md)] + + +* Role: Developer +* Responsibilities: Documentation -### Johnny Doe +### Ong Juan Wen - + -[[github](http://github.com/johndoe)] [[portfolio](team/johndoe.md)] +[[github](http://github.com/the0nlyjuan)] [[portfolio](team/the0nlyjuan.md)] * Role: Developer -* Responsibilities: Data +* Responsibilities: Code Quality -### Jean Doe +### Edwin Wong - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/Edwin1022)] +[[portfolio](team/edwinwong.md)] * Role: Developer * Responsibilities: Dev Ops + Threading -### James Doe +### Aw Ming Yi - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/MingYiAw)] +[[portfolio](team/mingyiaw.md)] * Role: Developer * Responsibilities: UI diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 743c65a49d2..78b171117f6 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -91,9 +91,9 @@ Here's a (partial) class diagram of the `Logic` component: -The sequence diagram below illustrates the interactions within the `Logic` component, taking `execute("delete 1")` API call as an example. +The sequence diagram below illustrates the interactions within the `Logic` component, taking `execute("delete John Doe")` API call as an example. -![Interactions Inside the Logic Component for the `delete 1` Command](images/DeleteSequenceDiagram.png) +![Interactions Inside the Logic Component for the `delete John` Command](images/DeleteSequenceDiagram.png)
:information_source: **Note:** The lifeline for `DeleteCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline continues till the end of diagram.
@@ -114,6 +114,11 @@ How the parsing works: * When called upon to parse a user command, the `AddressBookParser` class creates an `XYZCommandParser` (`XYZ` is a placeholder for the specific command name e.g., `AddCommandParser`) which uses the other classes shown above to parse the user command and create a `XYZCommand` object (e.g., `AddCommand`) which the `AddressBookParser` returns back as a `Command` object. * All `XYZCommandParser` classes (e.g., `AddCommandParser`, `DeleteCommandParser`, ...) inherit from the `Parser` interface so that they can be treated similarly where possible e.g, during testing. +Here is the activity diagram when a user interacts with AgentConnect. + + + + ### Model component **API** : [`Model.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/model/Model.java) @@ -155,11 +160,11 @@ Classes used by multiple components are in the `seedu.address.commons` package. This section describes some noteworthy details on how certain features are implemented. -### \[Proposed\] Undo/redo feature +### Undo/redo feature -#### Proposed Implementation +#### Implementation -The proposed undo/redo mechanism is facilitated by `VersionedAddressBook`. It extends `AddressBook` with an undo/redo history, stored internally as an `addressBookStateList` and `currentStatePointer`. Additionally, it implements the following operations: +The undo/redo mechanism is facilitated by `VersionedAddressBook`. It extends `AddressBook` with an undo/redo history, stored internally as an `addressBookStateList` and `currentStatePointer`. Additionally, it implements the following operations: * `VersionedAddressBook#commit()` — Saves the current address book state in its history. * `VersionedAddressBook#undo()` — Restores the previous address book state from its history. @@ -228,16 +233,16 @@ The following activity diagram summarizes what happens when a user executes a ne **Aspect: How undo & redo executes:** -* **Alternative 1 (current choice):** Saves the entire address book. +* **Current Implementation:** Saves the entire address book. * Pros: Easy to implement. * Cons: May have performance issues in terms of memory usage. -* **Alternative 2:** Individual command knows how to undo/redo by +* **Alternative 1:** Individual command knows how to undo/redo by itself. * Pros: Will use less memory (e.g. for `delete`, just save the person being deleted). * Cons: We must ensure that the implementation of each individual command are correct. -_{more aspects and alternatives to be added}_ + ### \[Proposed\] Data archiving @@ -262,71 +267,291 @@ _{Explain here how the data archiving feature will be implemented}_ **Target user profile**: -* has a need to manage a significant number of contacts -* prefer desktop apps over other types +* Insurance agents tracking their clients +* Has a need to manage and track a significant number of clients with detailed insurance-related information. +* Frequently engages with clients, requiring automated reminders for appointments, renewals, and follow-ups. +* Prefers desktop apps that support fast and efficient data management over mobile or web alternatives. * 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 +**Value proposition**: provides quick and efficient access to client details, tailored for insurance agents who need a streamlined interface to manage contacts, track policy updates, and schedule client follow-ups. + ### 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…​ | +|----------|---------|----------------------------------------------------|-------------------------------------------------------------------------------| +| `* * *` | user | add clients to my existing addressbook | store their contacts and respective information | +| `* * *` | user | know if client has been added successfully | so that I can proceed with the next steps or take corrective action if needed | +| `* * *` | user | mark a client’s insurance payment as paid | keep their due date up to date and know when they’ve fully paid || `* * *` | user | update existing client details | keep their information up to date. | +| `* * *` | user | record client's email address | contact them through email | +| `* * *` | user | sort clients by renewal dates | prioritize my outreach efforts | +| `* * *` | user | remove clients should they change insurance agents | | +| `* * *` | user | know when was my client's last appointment | track when to follow up | +| `* * *` | user | know when is my client's birthday | reach out to build rapport | +| `* * *` | user | know when is my client's next insurance payment | so that I can keep track of client's payment | -*{More to be added}* ### Use cases -(For all use cases below, the **System** is the `AddressBook` and the **Actor** is the `user`, unless specified otherwise) +(For all use cases below, the **System** is the `AgentConnect` and the **Actor** is the `user`, unless specified otherwise) + +**Use case: Add a person** + +**MSS** + +1. User adds a new person by entering the command with name, phone number, email, address, insurance type, and appointment dates. +2. AgentConnect validates the input. +3. AgentConnect adds the new person with all the details provided. +4. AgentConnect shows a success message confirming the person has been added. + + Use case ends. + + +**Extensions** + +* 2a. Some fields are invalid (e.g., name, phone, email). + * 2a1. AgentConnect shows an error message for the invalid fields. + * 2a2. User corrects the fields and resubmits the command. + * Use case resumes from step 2. + +* 2b. Duplicate person detected (same name + address). + * 2b1. AgentConnect shows a warning message about the duplicate entry. + * 2b2. User modifies either the name, address, or both fields, then resubmits the command + * Use case resumes at step 2 if user decides to proceed + **Use case: Delete a person** **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 enters the delete command with the Index or Name of the person to be deleted. +2. AgentConnect validates the input. +3. AgentConnect confirms the deletion request by showing a confirmation dialog with the Name of the person to be deleted. +4. AgentConnect deletes the contact and shows a success message. Use case ends. **Extensions** +* 2a. Contact not found (Invalid Index or Name). + * 2a1. AgentConnect shows an error message indicating Index or Name is invalid. + * 2a2. User can retry with a valid Index or valid Name. + * Use case resumes from step 2. +* 2b. Duplicate Person detected (same name) + * 2b1. AgentConnect updates the list in the GUI with the duplicates and prompts the user to delete by index. + * 2b2. User selects the index to delete the duplicate person. + * 2b3. Use case resumes from step 3. + +**Use case: Delete a policy** + +**MSS** -* 2a. The list is empty. +1. User enters the delete command with the index of the client and the index of the policy to be deleted. +2. AgentConnect validates the input. +3. AgentConnect deletes the policy and shows a success message. - Use case ends. + Use case ends. -* 3a. The given index is invalid. +**Extensions** +* 2a. Client not found (Invalid Index). + * 2a1. AgentConnect shows an error message indicating Index is invalid. + * 2a2. User can retry with a valid Index. + * Use case resumes from step 2. +* 2b. Policy not found (Invalid policy Index). + * 2b1. AgentConnect shows an error message indicating policy Index is invalid. + * 2b2. User can retry with a valid policy Index. + * Use case resumes from step 2. - * 3a1. AddressBook shows an error message. +**Use case: Undo a Command** - Use case resumes at step 2. +**MSS** -*{More to be added}* +1. User enters the undo command. +2. AgentConnect validates the input. +3. AgentConnect restores the previous state of the address book. +4. AgentConnect shows a success message confirming the undo operation. -### Non-Functional Requirements + Use case ends. + +**Extensions** +* 2a. No commands to undo. + * 2a1. AgentConnect shows an error message indicating no commands to undo. + * Use case ends. + +**Use case: Redo a Command** + +**MSS** + +1. User enters the redo command. +2. AgentConnect validates the input. +3. AgentConnect restores the previously undone state of the address book. +4. AgentConnect shows a success message confirming the redo operation. + + Use case ends. + +**Extensions** +* 2a. No commands to redo. + * 2a1. AgentConnect shows an error message indicating no commands to redo. + * Use case ends. + +**Use case: Sort Clients** + +**MSS** + +1. User sort the clients by entering the sort command with a valid parameter and order. +2. AgentConnect validates the input. +3. AgentConnect retrieves the current client list from storage. +4. AgentConnect sorts the clients based on the specified parameter and order. +5. AgentConnect updates the client list in the GUI to reflect the new sorted order. +6. AgentConnect shows a success message confirming the clients have been sorted. + + Use case ends. + + +**Extensions** + +* 2a. Sorting parameter are missing or invalid (e.g., name, insurance type, address, policy renewal date). + * 2a1. AgentConnect shows an error message for the invalid sorting parameter. + * 2a2. User corrects the sorting parameter and resubmits the command. + * Use case resumes from step 2. + +* 2b. Sorting order are missing or invalid (e.g., asc, desc). + * 2a1. AgentConnect shows an error message for the invalid sorting order. + * 2a2. User corrects the sorting order and resubmits the command. + * Use case resumes from step 2. + + +**Use case: Edit Client Details** + +**MSS** + +1. User edits some details for an existing person by entering the edit command with index of the person and new details. +2. AgentConnect validates the input. +3. AgentConnect update the corresponding details of the person with the new details provided. +4. AgentConnect shows a success message confirming the details fo the person have been edited. + +**Extensions** + +* 2a. Some fields are invalid (e.g., name, phone, email). + * 2a1. AgentConnect shows an error message for the invalid fields. + * 2a2. User corrects the fields and resubmits the command. + * Use case resumes from step 2. -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. +* 2b. Person not found (Invalid index). + * 2b1. AgentConnect shows a warning message indicating index is invalid. + * 2b2. User resubmits the command with a valid index. + * Use case resumes from step 2. + +**Use case: Categorise client by policy** + +**MSS** + +1. User requests to list clients. +2. User selects a client to assign a policy. +3. AgentConnect assigns the policy to the client. +4. AgentConnect shows updated client information. +Use case ends. + +**Extensions** + +* 2a. Client does not exist. + * 2a1. AgentConnect shows an error message for the invalid client. + * 2a2. AgentConnect prompts the user to either enter a valid client name or add the client to AgentConnect. + * Use case resumes from step 2. + +* 2b. Policy type is not valid. + * 2b1. AgentConnect shows an error message for the invalid policy type. + * 2b2. AgentConnect prompts the user to enter a valid policy type. + * Use case resumes from step 2. + +**Use case: Retrieve appointment date** + +**MSS** + +1. User requests to list clients. +2. User requests to view a client’s appointment dates. +3. AgentConnect retrieves and displays the requested appointment date. +Use case ends. + +**Extensions** + +* 2a. No appointment data available. + * 2a1. AgentConnect shows a message indicating no appointment date available. + * Use case ends. + +* 2b. Invalid client name. + * 2b1. AgentConnect shows an error message for the invalid client. + * 2b2. AgentConnect prompts the user to either enter a valid client name or add the client to AgentConnect. + * Use case resumes from step 1. + + +**Use case: Retrieve client's birthday** + +**MSS** + +1. User requests to list clients. +2. User requests to view a specific client’s birthday. +3. AgentConnect retrieves and displays the client’s birthday. +Use case ends. + +**Extensions** + +* 2a. No birthday data available. + * 2a1. AgentConnect shows a message indicating no birthday date available. + * Use case ends. + +* 2b. Invalid client name. + * 2b1. AgentConnect shows an error message for the invalid client. + * 2b2. AgentConnect prompts the user to either enter a valid client name or add the client to AgentConnect. + * Use case resumes from step 1. + +**Use case: Retrieve next payment date** + +**MSS** + +1. User requests to list clients. +2. User requests to view a client's payment date. +3. AgentConnect retrieves and display the next payment date. +Use case ends. + +**Extensions** + +* 2a. No payment data available. + * 2a1. AgentConnect shows a message indicating no payment data available. + * Use case ends. + +* 2b. Invalid client name. + * 2b1. AgentConnect shows an error message for the invalid client. + * 2b2. AgentConnect prompts the user to either enter a valid client name or add the client to AgentConnect. + * Use case resumes from step 1. *{More to be added}* +### Non-Functional Requirements + +1. Should be able to hold up to 1000 persons without noticeable lag for typical usage. +2. A user with above-average typing speed for regular English text should be able to add a new contact (including insurance and appointment details) faster using commands than with the mouse. +3. The system should provide real-time validation (e.g., when typing the phone number or email) to reduce error rates and ensure correct input formats. +4. Novice users should be able to complete a typical workflow in under 5 minutes, without external help. +5. The system codebase should allow for the introduction of new features with less than 10% of existing code modification. +6. The system should validate all inputs (e.g., phone number, email, insurance details) according to predefined formats (e.g., email must follow a standard email format) to maintain data consistency and integrity. +7. AgentConnect should detect and handle duplicate entries (based on client name + address) by prompting users to resolve conflicts before adding a new entry. +8. If the system encounters an unexpected error, it should display a user-friendly error message without exposing technical details and allow the user to retry the action. +9. The system should respond to common user actions (e.g., adding or deleting a person, sorting clients) within 1 second, ensuring a smooth and responsive experience. + ### Glossary * **Mainstream OS**: Windows, Linux, Unix, MacOS * **Private contact detail**: A contact detail that is not meant to be shared with others +* **Appointment**: A scheduled meeting or event between the user and a client, managed within AgentConnect, with details like date and purpose. +* **Policy**: An insurance or financial agreement purchased by a client, which can be categorized based on its type (e.g., Life Insurance, Health Insurance, Home Insurance). +* **Client**: A person whose details (e.g., contact information, insurance policies, appointments) are stored and managed within AgentConnect. +* **Duplicate Entry**: When a person with identical details (e.g., same name and address) already exists in the system, the system will flag this as a potential duplicate to avoid redundancy. + -------------------------------------------------------------------------------------------------------------------- @@ -343,40 +568,175 @@ testers are expected to do more *exploratory* testing. 1. Initial launch - 1. Download the jar file and copy into an empty folder + 1. Download the jar file and copy into an empty folder + 2. Open up a terminal, and run the following command: java -jar AgentConnect.jar + - Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum. - 1. Double-click the jar file Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum. - -1. Saving window preferences +2. Saving window preferences 1. Resize the window to an optimum size. Move the window to a different location. Close the window. + 2. Re-launch the app by double-clicking the jar file.
+ - Expected: The most recent window size and location is retained. + +### Adding a Client + +1. Adding a client + 1. Prerequisites: None + 2. Test case: `add n/John Doe p/98765432 e/johnd@example.com addr/311, Clementi Ave 2, #02-25 b/1990-10-10 appt/2024-12-12 12:00` + 3. Expected: A new contact has been added into the list. + - Status Message: "New person added: John Doe; Phone: 98765432; Email: johnd@example.com; Address: 311, Clementi Ave 2, #02-25; Birthday: 1990-10-10; Appointment: 2024-12-12 12:00; Tags:" + 4. The list view should now have the new contact inside. + +### Assigning Policies +1. Creating and Assigning policies to client + 1. Prerequisites: There must be at least one client showing in the list with no Policy Name "PolicyOne". + 2. Test Case: `assign 1 pon/PolicyOne pos/2022-12-12 poe/2023-12-12 paydate/2023-11-01 amt/300.00` + - Expected: "Policy successfully assigned to Alex Yeoh; Phone: 87438807; Email: alexyeoh@example.com; Address: Blk 30 Geylang Street 29, #06-40; Birthday: 1990-05-20; Appointment: 2024-10-15 14:00; Tags: [friends]" + + 3. Test Case: `assign 1 pon/PoicyOne pos/2025-12-12 poe/2023-12-12 paydate/2023-11-01 amt/300.00` + - Expected: "End date cannot be before start date!" + 4. Test Case: `assign 1 pon/PoicyOne pos/2022-12-12 poe/2023-12-12 paydate/2021-11-01 amt/300.00` + - Expected: "Premium due date cannot be before start date!" + 5. Test Case: `assign 1 pon/PoicyOne pos/2022-12-12 poe/2022-12-12 paydate/2022-01-01 amt/300.00` + - Expected: "Start date and end date cannot be the same!" - 1. Re-launch the app by double-clicking the jar file.
- Expected: The most recent window size and location is retained. +### Marking a policy as paid -1. _{ more test cases …​ }_ +1. Marking a policy as paid -### Deleting a person + 1. Prerequisites: There is at least one contact in the list with a policy. + 2. Test case: `paid 1 po/1` + 3. Expected: The first policy of the first person is marked as paid. + - The status bar shows the following: + The policy Life Insurance for `Alex Yeoh` will be fully paid after this payment. + - The list of persons should now have the updated person with the policy marked as paid inside of it. -1. Deleting a person while all persons are being shown - 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. +### Deleting a Client - 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. - 1. Test case: `delete 0`
+1. Deleting a Client while all contacts are being shown + + 1. Prerequisites: List all clients using the `list` command. Multiple persons in the list including a `John Doe`. + + 2. Test case: `delete 1`
+ Expected: First contact is deleted from the list. Details of the deleted contact shown in the status message. + + 3. Test case: `delete Bernice Yu`
+ Expected: Contact with name `Bernice Yu` is deleted from the list. Details of the deleted contact shown in the status message. + + 4. Test case: `delete 0`
Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. - 1. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
+ 5. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
Expected: Similar to previous. -1. _{ more test cases …​ }_ +### Deleting a policy + +1. Deleting a policy from a person + 1. Prerequisites: There is at least one contact in the list with a policy. + 2. Test case: `delete 1 po/1` + 3. Expected: The first policy of the first person is deleted. + - The status bar shows the following: + Deleted Policy 1 from "The first person in the list" + - The list of persons should now have the updated person with the policy deleted inside of it. + +### Editing a person + +1. Editing a person with all details + 1. Prerequisites: There is at least one contact in the list. + 2. Test case: `edit 1 p/9999000` + 3. Expected: The phone number of the first person is updated to 9999000. + - The status bar shows the following: + Edited Person: Alex Yeoh; Phone: 99990000; Email... + - The list of persons should now have the updated person inside of it. + +### Finding a person + +1. Finding a person + 1. Prerequisites: There is at least one contact in the list. + 2. Test case: `find alex` + 3. Expected: The list of persons should now only show persons with the names containing "alex" which is not case-sensitive. + - The status bar shows the following: + `n` persons listed! (where `n` is the number of persons found) + - The list of persons should now only have the found persons inside of it. + +### Sorting the list + +1. Sorting the list by name + 1. Prerequisites: There is at least more than one contact in the list. + 2. Test case: `sort n/ desc` + 3. Expected: The list of persons should now be sorted by name in descending order. + - The status bar shows the following: + Contacts have been sorted by name in desc order. + - The list of persons should now be sorted by name in descending order. + +2. Sorting the list by appointment date + 1. Prerequisites: There is at least more than one contact in the list. + 2. Test case: `sort appt/ asc` + 3. Expected: The list of persons should now be sorted by appointment date in ascending order. + - The status bar shows the following: + Contacts have been sorted by appointment date in asc order. + - The list of persons should now be sorted by appointment date in ascending order. + +3. Sorting the list by birthday + 1. Prerequisites: There is at least more than one contact in the list. + 2. Test case: `sort bday/ desc` + 3. Expected: The list of persons should now be sorted by birthday in descending order. + - The status bar shows the following: + Contacts have been sorted by birthday in desc order. + - The list of persons should now be sorted by birthday in descending order. + +### Searching + +1. Searching for appointments + 1. Prerequisites: There is at least one contact in the list. + 2. Test case: `search b/1990-05-20` + 3. Expected: The list of persons should now only show persons with the birthday on 1990-05-20. + - The status bar shows the following: + Listed all clients with birthdays on 1990-05-20 + - The list of persons should now only have the found persons inside of it. + ### Saving data 1. Dealing with missing/corrupted data files - 1. _{explain how to simulate a missing/corrupted file, and the expected behavior}_ + 1. Test case: missing data file + 1. Locate the `addressbook.json` file in `../data/` and delete it + 2. Relaunch the app + 3. Expected: The app should create a new `addressbook.json` file with default data + + +-------------------------------------------------------------------------------------------------------------------- + +## **Appendix: Planned enhancements** +Team size: 5 + +1. + + +-------------------------------------------------------------------------------------------------------------------- -1. _{ more test cases …​ }_ +## **Appendix: Effort** +#### Difficulty Level +The project was moderately difficult due to the complexity of managing multiple entity types (e.g., `Person`, `Policy`, `Appointment`) and their relationships. The undo/redo feature was also challenging to implement due to the need to manage multiple states of the address book. + +#### Challenges Faced +- Multiple Entity Management: Handling multiple entities such as clients, policies, and appointments required significant effort in designing and implementing the data models and their interactions. +- Undo/Redo Functionality: Implementing a robust undo/redo mechanism that works seamlessly across various commands was challenging and required careful state management. +- User Interface: Ensuring the UI updates correctly in response to changes in the model, especially with the addition of new features like policy management and appointment scheduling, was a significant challenge. +- Testing: Writing comprehensive tests for the new features and ensuring they integrate well with existing functionality required substantial effort. + +#### Effort Required +The project required a considerable ammount of effort in the following areas: +- **Design and Architecture**: Significant time was spent on designing the architecture to support multiple entities and their interactions. +- **Implementation**: Implementing the new features, especially the undo/redo functionality and policy management, required detailed coding and debugging. +- **Testing**: Writing unit tests, integration tests, and ensuring high test coverage was time-consuming but essential for maintaining code quality. +- **Documentation**: Updating the user guide, developer guide, and other documentation to reflect the new features and changes was necessary to ensure clarity and usability. + +#### Achievements +- **Comprehensive Policy Management**: Successfully implemented a feature-rich policy management system that allows users to assign, delete, and mark policies as paid. +- **Robust Undo/Redo Mechanism**: Developed a reliable undo/redo mechanism that enhances user experience by allowing them to revert and reapply changes easily. +- **Enhanced User Interface**: Improved the UI to handle multiple entities and provide a seamless user experience. +- **Extensive Testing**: Achieved high test coverage and ensured the reliability of the application through rigorous testing. diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 84b4ddc4e40..d58e487db4d 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -1,161 +1,669 @@ --- layout: page -title: User Guide +title: AgentConnect --- +![help message](images/agentconnectlogo.png) +Welcome to the **AgentConnect 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. +*AgentConnect* is a simple and powerful desktop application designed specifically for insurance agents like you. Managing your clients' contacts, appointments, and policies has never been easier! +## Why Choose AgentConnect? -* Table of Contents -{:toc} +- **Easy to Use:** AgentConnect combines the speed of typing commands with the ease of a visual interface, making it both powerful and user-friendly. +- **Efficient Management:** Quickly add, update, and organize your client information without the hassle of traditional address books. +- **Stay Organized:** Keep track of appointments and policy details all in one place, so you never miss an important date or update. + +AgentConnect is designed with you in mind, ensuring that even if you're not tech-savvy, you can start managing your clients effectively right away. Let AgentConnect help you save time and stay organized, so you can focus on what matters most—your clients. + +-------------------------------------------------------------------------------------------------------------------- +## Introduction + +[Table of Content](#table-of-contents): View all contents in this guide. +[Quick Start](#quick-start): Get up and running with AgentConnect in no time. +[Getting Started with AgentConnect](#getting-started): Learn the basics of using AgentConnect effectively. +[Client Management](#client-management): Navigate here for more information on how to manage your clients with ease. +[Policy Management](#policy-management): Navigate here for more information on handling policy assignments and payments. +[Search Functions](#search-functions): Navigate here for more information on how to quickly find client's information. +[Advanced Features](#advanced-features): Enhance your workflow with advanced commands. +[Frequently Asked Questions (FAQ)](#frequently-asked-questions-faq): Common questions and answers about using AgentConnect. +[Known Issues](#known-issues): List of current issues and their solutions. + +-------------------------------------------------------------------------------------------------------------------- + +## Table of Contents + +1. [Quick Start](#quick-start) +2. [Getting Started](#getting-started) + - [Command Format](#understanding-command-format) + - [Viewing Help](#viewing-help--help) + - [Saving Your Data](#saving-the-data) + - [Exiting the Program](#exiting-the-program--exit) +3. [Features](#features) + 1. [Client Management](#client-management) + - [Adding a Client](#adding-a-client--add) + - [Deleting a Client](#deleting-a-client--delete) + - [Listing All Clients](#listing-all-clients--list) + - [Editing Client Details](#editing-a-client-details--edit) + - [Sorting Clients](#sorting-clients--sort) + 2. [Search Functions](#search-functions) + - [Locating Clients by Name](#locating-clients-by-name--find) + - [Searching Appointments](#searching-appointments--search-a) + - [Searching Birthdays](#searching-birthdays--search-b) + - [Searching Policies](#searching-policy--search-p) + 3. [Policy Management](#policy-management) + - [Assigning a Policy](#assigning-a-policy--assign) + - [Deleting a Policy](#deleting-a-policy--delete) + - [Mark Policy Payment](#marking-a-policy-payment-installment-as-paid--paid) + 4. [Advanced Features](#advanced-features) + - [Undo a Command](#undo-a-command--undo) + - [Redo a Command](#redo-a-command--redo) + - [Clearing All Entries](#clearing-all-entries--clear) +4. [Frequently Asked Questions (FAQ)](#frequently-asked-questions-faq) +5. [Known Issues](#known-issues) +6. [Command Summary](#command-summary) -------------------------------------------------------------------------------------------------------------------- ## Quick start -1. Ensure you have Java `17` or above installed in your Computer. +1. Ensure that you have **Java 17 or above** installed on your computer: + * If you are unsure, you can check if Java is installed by running `java --version`. For more instruction, you can refer to this link [here](https://www.java.com/en/download/help/version_manual.html). + * If Java is not installed, you can refer to this guide on how to install [here](https://www.java.com/en/download/help/download_options.html). + * You can also download Java directly from [here](https://www.oracle.com/java/technologies/downloads/). -1. Download the latest `.jar` file from [here](https://github.com/se-edu/addressbook-level3/releases). -1. Copy the file to the folder you want to use as the _home folder_ for your AddressBook. +2. Download the latest release version of AgentConnect. The application is a `.jar` file: + * Get the latest version of AgentConnect from [this link](https://github.com/AY2425S1-CS2103T-W10-3/tp/releases). Look for a file named `AgentConnect.jar`. + -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.
- A GUI similar to the below should appear in a few seconds. Note how the app contains some sample data.
- ![Ui](images/Ui.png) +3. Place the File: + * Move the `AgentConnect.jar` file to a home folder where you want to store the application and its data. + * Note that the data folder created by the `.jar` file which stores the client data will be stored in the same home folder -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: - * `list` : Lists all contacts. +4. Run the Application: + - **Option 1: Double-Click:** + Simply double-click the `AgentConnect.jar` file to start the application. + - **Option 2: Use Command Prompt:** + Open the Command Prompt (Windows) or Terminal (Mac/Linux), navigate to the folder containing the `AgentConnect.jar` file, and type: + ``` + java -jar AgentConnect.jar + ``` + For example, if you stored the `AgentConnect.jar` file in the directory `C:/Users/Desktop/Applications`, then run the following command: + ``` + cd C:/Users/Desktop/Applications + java -jar AgentConnect.jar + ``` + To learn more about directory navigation, refer to this [link](https://gomakethings.com/navigating-the-file-system-with-terminal/). - * `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. +5. **Application Interface:** A user interface similar to the below should appear in a few seconds. - * `clear` : Deletes all contacts. +6. ![help message](images/Ui.png) - * `exit` : Exits the app. +6. **Try It Out:** + - **Enter a Command:** + Type a command into the command box and press **Enter**. For example, type `help` to see the help message. + - **Explore:** + Feel free to experiment with different commands. -1. Refer to the [Features](#features) below for details of each command. + +7. Refer to the [Features](#features) below for details of each command. -------------------------------------------------------------------------------------------------------------------- +## Getting Started -## Features +### Understanding Command Format + +Users engage with the AgentConnect application by inputting a series of commands. These commands enable a variety of actions, such as adding, updating, listing, and managing clients and policies, as well as retrieving client information like appointments and birthdays. This section will introduce some pointers to take note of when running commands. -
+* Angle brackets <> are placeholders for information you need to provide. + Example: In `add n/`, replace `NAME` with the client's name. + -**:information_source: Notes about the command format:**
+* Square brackets [ ] indicate optional items. + Example: `n/ [t/TAG]` means the `t/TAG` part is optional. -* 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`. -* Items in square brackets are optional.
- e.g `n/NAME [t/TAG]` can be used as `n/John Doe t/friend` or as `n/John Doe`. +* Ellipsis ... means you can include the item multiple times or not at all. Example: `[t/TAG]...` allows for multiple tags or none. -* Items with `…`​ after them can be used multiple times including zero times.
- e.g. `[t/TAG]…​` can be used as ` ` (i.e. 0 times), `t/friend`, `t/friend t/family` etc. -* Parameters can be in any order.
- e.g. if the command specifies `n/NAME p/PHONE_NUMBER`, `p/PHONE_NUMBER n/NAME` is also acceptable. +* The parameters for the commands does not matter and you can enter the parameters in any order. + Example: `n/John p/123` is the same as `p/123 n/John`. -* Extraneous parameters for commands that do not take in parameters (such as `help`, `list`, `exit` and `clear`) will be ignored.
- e.g. if the command specifies `help 123`, it will be interpreted as `help`. +* Extraneous parameters for commands that do not take in parameters (such as `help`, `list`, `exit` and `clear`) will be ignored. +e.g. if the command specifies `help 123`, it will be interpreted as `help`. * If you are using a PDF version of this document, be careful when copying and pasting commands that span multiple lines as space characters surrounding line-breaks may be omitted when copied over to the application. -
-### Viewing help : `help` -Shows a message explaning how to access the help page. +### Viewing Help : `help` + +Shows a message explaining how to access the help page. ![help message](images/helpMessage.png) Format: `help` -### Adding a person: `add` +### Saving the data + +AgentConnect data are saved in the hard disk automatically after any command that changes the data. There is no need to save manually. + + +### Exiting the program : `exit` + +Exits the program. + +Format: `exit` + +--- +## Features -Adds a person to the address book. +AgentConnect offers a range of features to help you manage your clients effectively. You can navigate to each section below: +* [Client Management](#client-management) +* [Search Functions](#search-functions) +* [Policy Management](#policy-management) +* [Advanced Features](#advanced-features) -Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​` +## Client Management -
:bulb: **Tip:** -A person can have any number of tags (including 0) +The client management section contain commands that allows you to interact with your client's information. Each client contains the following fields: Name, phone number, home address, email address, birthday and next appointment. + +
**Note:** +Policies are not mentioned in this section as there is a dedicated section for managing client's policy. For more information regarding policy management, refer [here](#policy-management). +
+ +| **Action** | **Command Format** | **Command Example** | +|-----------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------| +| [Adding a Client](#adding-a-client--add) | `add n/ p/ e/ addr/
b/ appt/` | `add n/John Doe p/91234567 e/john@example.com addr/123 Street b/1990-01-01 appt/2024-12-12 14:00` | +| [Listing All Clients](#listing-all-clients--list) | `list` | `list` | +| [Deleting a Client](#deleting-a-client--delete) | `delete ` or `delete ` Example: `delete 3` or `delete John Doe` | `delete 3` or `delete John Doe` | +| [Editing Client Details](#editing-a-client-details--edit) | `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [addr/ADDRESS] [b/BIRTHDAY] [appt/APPOINTMENT] [po/POLICY_INDEX pon/POLICY_NAME pos/START_DATE poe/END_DATE paydate/PAYMENT_DUE_DATE amt/AMOUNT_DUE]` | `edit 2 p/98765432` | +| [Sorting Clients](#sorting-clients--sort) | `sort ` | `sort birthday asc` | + + + +### Adding a client : `add` + +Adds a client to the address book. + +Format: +`add n/ p/ e/ addr/ b/ appt/` + +- **Parameters**: + * Birthday should be in `yyyy-mm-dd` + * Appointment should be in `yyyy-mm-dd HH:mm` and accept both past and future dates + * Phone number can only be 3 to 8 digits long + * Clients with the same name and address are considered as duplicates + * The system standardises addresses by removing commas, spaces, and hyphens and making + all characters lowercase. This means that "311, Clementi Ave 5, unit 02-2" and + "311 Clementi ave 5 unit 022" (without commas) will be treated as the same client address. + +Examples: +* The command below adds a new client to the client list with the following information: + * Client name: John Doe + * Client number: 98765432 + * Client email: johnd@example.com + * Home address: John street, block 123, #01-01 + * Client birthday: 2000-12-12 + * Client next appointment: 2024-12-12 12:00 + `add n/John Doe p/98765432 e/johnd@example.com addr/John street, block 123, #01-01 b/2000-12-12 appt/2024-12-12 12:00` + +* The command below adds a new client to the client list with the following information: + * Client name: Betsy Crowe + * Client number: 1234567 + * Client email: betsycrowe@example.com + * Home address: Newgate Prison + * Client birthday: 2001-10-10 + * Client next appointment: 2024-12-01 09:00 + `add n/Betsy Crowe e/betsycrowe@example.com addr/Newgate Prison p/1234567 b/2001-10-10 appt/2024-12-01 09:00` + ![result for 'find alex david'](images/addUI.png) + +--- + +### Deleting a client : `delete` + +Deletes the specified client from the application. There is a confirmation message before the deletion is executed. Type `y` and press enter to confirm deletion. + +Format: `delete ` or `delete ` +- **Parameters**: + * Deletes the client at the specified `INDEX` or the specific `NAME`. + * The index and name refers to the index number or name shown in the displayed client list. + * The index **must be a positive integer** 1, 2, 3, …​ + * Any input after the first index will be ignored. Eg. `delete 1 2` or `delete 1 test` will be interpreted as `delete 1`. + * The name of the client is case-sensitive and must be an exact match. + +
💡**Tip:** +Always verify the client’s index number or exact name before executing the delete command to prevent accidental removals. +Best Practice: Use the list or find command to confirm the exact client you intend to delete.
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` +* `find Denon` followed by `delete 3` deletes the 3rd client in the results of the `find` command. + ![result for 'delete david li'](images/deleteUI.png) -### Listing all persons : `list` +* Delete the client named `Betsy Crowe` from the list of the address book. + ``` + delete Betsy Crowe + ``` -Shows a list of all persons in the address book. +--- + +### Listing all clients : `list` + +Shows a list of all clients in the address book. Format: `list` -### Editing a person : `edit` +--- + +### Editing a client details : `edit` -Edits an existing person in the address book. +Edits an existing client in the address book. -Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` +Format: +`edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG] [b/BIRTHDAY] [appt/APPOINTMENT] [po/POLICY_INDEX pon/POLICY_NAME pos/START_DATE poe/END_DATE paydate/PAY_DATE amt/AMOUNT]…​` -* 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. +- **Parameters**: + + * Edits the client at the specified `INDEX`. The index refers to the index number shown in the displayed client 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 client will be removed i.e adding of tags is not cumulative. + * You can remove all the client’s tags by typing `t/` without + specifying any tags after it. + * When editing birthday and policy, the date format should be `yyyy-mm-dd` + * When editing appointment, the date and time format should be `yyyy-mm-dd hh:mm` in 24-hour notation + * When editing policy, all fields of policy must be included + +
💡 **Tip:** +Although tags are optional, using them can be useful if you want to store additional information about the client. +For example, if a client is a VIP, you can include it in the client's information using the edit command. +
Examples: -* `edit 1 p/91234567 e/johndoe@example.com` Edits the phone number and email address of the 1st person to be `91234567` and `johndoe@example.com` respectively. -* `edit 2 n/Betsy Crower t/` Edits the name of the 2nd person to be `Betsy Crower` and clears all existing tags. +- *Edits the phone number and email address of the 1st client to be `91234567` and `johndoe@example.com` respectively.*: + ``` + edit 1 p/91234567 e/johndoe@example.com + ``` + ![result for 'edit first index client'](images/editUI.png) + +- *Edits the name of the 2nd client to be `Betsy Crower` and clears all existing tags.*: + ``` + edit 2 n/Betsy Crower t/ + ``` + +- *Edits the 1st policy of the 3rd client to be `Health Insurance` which covers from `2024-10-19` to `2025-10-19`. The pay date changes to `2024-11-19` + and premium amount changes to `200`*: + + `edit 3 po/1 pon/Health Insurance pos/2024-10-19 poe/2025-10-19 paydate/2024-11-19 amt/200` + +--- + +### Sorting clients : `sort` -### Locating persons by name: `find` +Organizes your client list based on different criteria for easier management and viewing. -Finds persons whose names contain any of the given keywords. +Format: +`sort ` -Format: `find KEYWORD [MORE_KEYWORDS]` +- **Parameters**: + - `CRITERIA`: The attribute by which you want to sort the clients. Available criteria include: + - `n/`: Sort by client's name (A-Z or Z-A). + - `b/`: Sort by client's birthday (earliest to latest or latest to earliest). + - `appt/`: Sort by upcoming appointment date (soonest to latest or latest to soonest). + - `paydate/`: Sort by policy payment due date (soonest to latest). + - `ORDER`: The direction of sort. Available order includes: + - `asc`: Sort in ascending order + - `desc`: Sort in descending order -* 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` +- **Usage**: + - **Name Sorting**: Orders the client list alphabetically by names. + - **Birthday Sorting**: Orders the client list by birthday dates. + - **Appointment Sorting**: Orders the client list by the dates of upcoming appointments. + - **Payment Due Date Sorting**: Orders the client list by the policy payment due dates. This sort can only be used in ascending order. + +
💡 **Tip:** +Regularly sorting your client list based on different criteria can help you manage your clients more efficiently and keep track of important information. +
Examples: -* `find John` returns `john` and `John Doe` -* `find alex david` returns `Alex Yeoh`, `David Li`
- ![result for 'find alex david'](images/findAlexDavidResult.png) +- **Sort by Name in Ascending Order**: Sorts the client list alphabetically by each client's name. + ``` + sort n/ asc + ``` + + ![Sort by Name](images/sortnameUI.png) + +- **Sort by Birthday in Ascending Order**: Sorts the client list from the earliest to the latest birthday. + ``` + sort b/ asc + ``` + + ![Sort by Birthday](images/sortbirthdayUI.png) + +- **Sort by Appointment Date in Descending Order**: Sorts the client list based on the latest to the earliest upcoming appointment. + ``` + sort appt/ desc + ``` + + ![Sort by Appointment Date](images/sortapptUI.png) + +- **Sort by Policy Payment Due Date in Ascending Order**: Sorts the client list based on the earliest to the latest payment due date of the policy. + ``` + sort paydate/ asc + ``` + + ![Sort by Policy Payment Due Date](images/sortpaydateUI.png) + +--- + +## Search Functions + +The search function section contain commands that allows you to search client-specific information, such as appointments, birthday and policies. -### Deleting a person : `delete` +| **Action** | **Command Format** | **Command Example** | +|---------------------------------------------------------|----------------------------------|------------------------------| +| [Find Clients](#locating-clients-by-name--find) | `find [MORE_KEYWORDS]` | `find Alice` | +| [Search Appointment](#searching-appointments--search-a) | `search a/ ` | `search a/ 2024-10-10 23:00` | +| [Search Birthday](#searching-birthdays--search-b) | `search b/ ` | `search b/ 2000-03-15` | +| [Search Policy](#searching-policy--search-p) | `search p/ ` | `search p/ sample Policy` | -Deletes the specified person from the address book. +### Locating clients by name : `find` -Format: `delete INDEX` +Finds clients whose names contain any of the given keywords. -* 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, …​ +Format: +`find [MORE_KEYWORDS]` + +- **Parameters**: + * 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` + * Clients matching at least one keyword will be returned (i.e. `OR` search). + e.g. `Hans Bo` will return `Hans Gruber`, `Bo Yang` + +
💡 **Tip:** +When using the find command, incorporating multiple keywords can help narrow down search results more effectively. +
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. +- **Search by Specific Keyword**: Finds all client's name that contains the keyword. + ``` + find steven kurt + ``` -### Clearing all entries : `clear` + ![result for 'find alex david'](images/findstevenkurtResult.png) -Clears all entries from the address book. +--- -Format: `clear` +### Searching appointments : `search a/` -### Exiting the program : `exit` +Finds clients with appointments on a specific date or within a date range. -Exits the program. +Format: -Format: `exit` +Single datetime search: +`search a/ ` +Range datetime search: +`search a/ to ` +- **Parameters**: + - `DATETIME`: A specific date and time in `yyyy-mm-dd HH:mm` format. + - `START DATETIME`: A specific start date and time in `yyyy-mm-dd HH:mm` format. + - `END DATETIME`: A specific end date and time in `yyyy-mm-dd HH:mm` format. -### Saving the data +- **Usage**: + - **Single Datetime Search**: Lists all clients with appointments on the specified date and time. + - **Range Datetime Search**: List all clients with appointments that is within the given date and time range. + +Examples: + - **Search by Specific Datetime**: Finds all clients with appointments on May 06, 2024, 2PM. + ``` + search a/ 2024-05-06 14:00 + ``` + + ![Search Appointment by Date](images/searchapptUI.png) + - **Search by Range Datetime**: Finds all clients with appointments from January 01, 2024, 12AM to December 31, 2024, 11.59PM. + ``` + search a/ 2024-01-01 00:00 to 2024-12-31 23:59 + ``` + + ![Search Appointment by Range Date](images/searchapptrangeUI.png) + +--- + +### Searching birthdays : `search b/` + +Finds clients who have birthdays on a specific date or within a date range. + +Format: + +Single date search: +`search b/ ` + +Range date search: +`search b/ to ` + +- **Parameters**: + - `DATE`: A specific date in `yyyy-mm-dd` format. + - `START DATE`: A specific start date in `yyyy-mm-dd` format. + - `END DATE`: A specific end date in `yyyy-mm-dd` format. + - If it is a range date search, the `to` keyword is compulsory. + +- **Usage**: + - **Single Date Search**: Lists all clients whose birthdays fall on the specified date. + - **Range Date Search**: Lists all clients whose birthdays are within the given date range. + +Examples: + - **Search by Specific Date**: Finds all clients with a birthday on October 10, 1990. + ``` + search b/ 1990-10-10 + ``` + ![Search Birthday by Date](images/searchbirthdayUI.png) + + - **Search by Range Date**: Finds all clients with a birthdays between January 1, 1990, to January 1, 2001. + ``` + search b/ 1990-01-01 to 2001-01-01 + ``` + ![Search Birthday by Range Date](images/searchbirthdayrangeUI.png) + + +--- + +### Searching policy : `search p/` + +Find clients who currently owns a certain policy. + +Format: +`search p/ ` + +- **Parameters**: + - `POLICY_NAME`: A specific policy name. + - `POLICY_NAME` is not case-sensitive and whitespaces are ignored. + +
💡 **Tip:** +For quicker search, if the policy name is "Health Advantage Policy 3", +you can type "healthadvantagepolicy3" instead. The search result will be the same. +
+ +Example: + - **Search by Specific Policy**: Finds all clients who own the policy named "PolicyOne". + ``` + search p/ PolicyOne + ``` + ![Search Policy by Policy Name](images/searchpolicyUI.png) + +--- + +## Policy Management + +The policy management section contain commands that allows you to manage policy-related information, such as assigning policy, deleting policy and updating policy payment. + +| **Action** | **Command Format** | **Command Example** | +|----------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------| +| [Assign a Policy](#assigning-a-policy--assign) | `assign INDEX pon/ pos/ poe/ paydate/ amt/` | `assign 1 pon/Health Insurance pos/2022-01-01 poe/2023-01-01 paydate/2022-12-01 amt/300.00` | +| [Delete a Policy](#deleting-a-policy--delete) | `delete po/` | `delete 3 po/2` | +| [Mark Policy as Paid](#marking-a-policy-payment-installment-as-paid--paid) | `paid po/` | `paid 2 po/3` | + +### Assigning a policy : `assign` + +Assign policy to a client. + +Format: +`assign pon/ /pos /poe /paydate /amt ` +- **Parameters**: + * `POLICY START DATE` and `POLICY END DATE` refer to the duration of the policy's coverage. + * `POLICY START DATE` and `POLICY END DATE` must be in the format `yyyy-mm-dd`. + * `POLICY START DATE` and `POLICY END DATE` cannot be the same date. + * `INSURANCE DUE DATE` must be in the format `yyyy-mm-dd`. + * `INSURANCE DUE DATE` cannot be earlier than the `POLICY START DATE`. + * `INSURANCE DUE DATE` cannot be later than the `POLICY START DATE`. + * `AMOUNT DUE` must be a positive number with up to 2 decimal places. + +
**Note:** +Sometimes, after assigning a policy to a client, the user interface may not reflect the changes +immediately. If this occurs, try double-clicking on the particular client profile card. +The user interface will show the assigned policy. +
-AddressBook data are saved in the hard disk automatically after any command that changes the data. There is no need to save manually. +Example: +* Assign a policy to the client listed at index 1 with the following information: + * Policy Name: PolicyOne + * Policy start date: 2022-12-12 + * Policy end date: 2023-12-12 + * Premium due date: 2023-11-01 + * Amount Due: $300.00 + + `assign 1 pon/PolicyOne pos/2022-12-12 poe/2023-12-12 paydate/2023-11-01 amt/300.00` + + ![result for 'assign policy to first client'](images/assignpolicyUI.png) + +--- + +### Deleting a policy : `delete` + +Deletes the specified policy from the specified client using the index of the client and policy. + +Format: +`delete po/` +Deletes the policy at the specified `POLICY_INDEX` of the client at the specified `INDEX`. + + +- **Parameters**: + - `INDEX`: The index of the client in the client list. + - `POLICY_INDEX`: The index of the policy to be deleted. + - The index refers to the index number shown in the displayed client list. The index **must be a positive integer** 1, 2, 3, …​ + - The policy index refers to the index number shown in the displayed client list. The index **must be a positive integer** 1, 2, 3, …​ + +
💡 **Tip:** +Always verify the policy’s index number before executing the delete command to prevent accidental removals. +Best Practice: Use the list command to confirm the exact policy you intend to delete. +
+ +Examples: + +* Delete the 1st policy of the 1st person in the address book. + ``` + delete 1 po/1 + ``` + +* Delete the 1st policy of the 3rd person in the address book. + ``` + delete 3 po/1 + ``` + ![result for 'delete policy from first client'](images/deletepolicyUI.png) + +--- + +### Marking a policy payment installment as paid : `paid` + +Marks a policy payment installment as paid for a client. + +Format: +`paid po/` +- **Parameters**: + - `INDEX`: The index of the client in the client list. + - `POLICY_INDEX`: The index of the policy to be marked as paid. + - The index refers to the index number shown in the displayed client list. The index **must be a positive integer** 1, 2, 3, …​ + - The policy index refers to the index number shown in the displayed client list. The index **must be a positive integer** 1, 2, 3, …​ + +
**Note:**
+1. The payment due date of the policy will be updated to the next scheduled date (ie. one year later). +The amount due will be set to 0 once the final installment of the insurance payment is completed.
+2. Sometimes, after marking a policy payment installment of a client as paid, the user interface may not reflect the changes +immediately. If this occurs, try double-clicking on the particular client profile card. +The user interface will show the updated payment due date. +
+ + + +Examples: +- **Marking a Policy Payment Installment as Paid**: + ``` + paid 1 po/ 1 + ``` + *Marks the policy associated with index 1 as paid for the client at index 1.* + + ![Marking a Policy Payment Installment as Paid](images/paidUI.png) + +--- + +## Advanced Features + +| **Action** | **Command Format** | **Command Example** | +|-----------------------------------------------------|--------------------|---------------------| +| [Undo the Last Deleted Work](#undo-a-command--undo) | `undo` | `undo` | +| [Redo the Last Undo Work](#redo-a-command--redo) | `redo` | `redo` | +| [Clear All Entries](#clearing-all-entries--clear) | `clear` | `clear` | + +### Undo a command : `undo` +Undo the last command. This command works for add, edit, delete client and clear commands. + + +Format: `undo` + +* Undo the last command to restore the address book to the state before the last command. +
**Note:** +The `undo` command does not work for assign and deletion of policies. +
+ +Examples: +* `delete 1` followed by `undo` will restore back the deleted client at index 1. +* `clear` followed by `undo` will restore back all the deleted clients. +![result for 'undo'](images/undoUI.png) + + +--- +### Redo a command : `redo` +Redo the last undo. Works for add, edit, delete client and clear commands. + +Format: `redo` + +* Redo the last undo command to restore the address book to the state before the undo command. +
**Note:** +The `redo` command does not work for assign and deletion of policies. +
+ +Examples: +* `delete 1` followed by `undo` followed by `redo` will delete the client at index 1 again. +* `clear` followed by `undo` followed by `redo` will clear all the entries again. +![redo a deletion](images/redoUI.png) + +--- + +### Clearing all entries : `clear` + +Clears all entries from the address book. + +Format: `clear` + +
:exclamation: **Caution:** +If you accidentally used the `clear` command and want to revert back, do not run any other commands. +Run `undo` immediately to restore the data. +
+ +--- ### Editing the data file @@ -166,34 +674,70 @@ If your changes to the data file makes its format invalid, AddressBook will disc 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.
-### Archiving data files `[coming in v2.0]` +-------------------------------------------------------------------------------------------------------------------- + +## Frequently Asked Questions (FAQ) -_Details coming soon ..._ +**Q:** How do I check if I have Java installed? --------------------------------------------------------------------------------------------------------------------- +**A:** Open the Command Prompt or Terminal and type `java -version`. If Java is installed, it will show the version number. For more information, you can refer [here](#quick-start). + +--- + +**Q:** Can I run AgentConnect on a Mac or Linux computer? + +**A:** Yes! As long as you have Java 17 or above installed, AgentConnect will work on Windows, Mac, and Linux. + +--- + +**Q:** How do I back up my data? + +**A:** Your data is stored in the `addressbook.json` file in the `data` folder. Copy this file to a safe location to back up your data. + +--- + +**Q:** What happens if I enter the wrong command? + +**A:** AgentConnect will show an error message explaining what went wrong. You can then try the command again. + +--- + +**Q:** Can I assign multiple policies to one client? + +**A:** Yes, you can assign multiple policies by using the `assign` command multiple times for the same client. -## 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. -------------------------------------------------------------------------------------------------------------------- -## Known issues +## Known Issues + +1. **Multiple Screens**: If you use multiple monitors and move AgentConnect to a secondary screen, it may open off-screen if that monitor is disconnected. + **Solution**: Delete the `preferences.json` file in the application folder before restarting AgentConnect. -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. +2. **Help Window Minimization**: If you minimize the Help window and try to open another help window (using `help` command, F1 or clicking the help option in the user interface), the original help window will still be minimized. There will be no new help window pop up. + **Solution**: Restore the minimized Help window manually. -------------------------------------------------------------------------------------------------------------------- -## Command summary - -Action | Format, Examples ---------|------------------ -**Add** | `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​`
e.g., `add n/James Ho p/22224444 e/jamesho@example.com a/123, Clementi Rd, 1234665 t/friend t/colleague` -**Clear** | `clear` -**Delete** | `delete INDEX`
e.g., `delete 3` -**Edit** | `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG]…​`
e.g.,`edit 2 n/James Lee e/jameslee@example.com` -**Find** | `find KEYWORD [MORE_KEYWORDS]`
e.g., `find James Jake` -**List** | `list` -**Help** | `help` +## Command Summary + +| Action | Command Format | +|----------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| [Adding a Client](#adding-a-client--add) | `add n/ p/ e/ addr/
b/ appt/`
Example: `add n/John Doe p/91234567 e/john@example.com addr/123 Street b/1990-01-01 appt/2024-12-12 14:00` | +| [Listing All Clients](#listing-all-clients--list) | `list` | +| [Deleting a Client](#deleting-a-client--delete) | `delete ` or `delete `
Example: `delete 3` or `delete John Doe` | +| [Editing Client Details](#editing-a-client-details--edit) | `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [addr/ADDRESS] [b/BIRTHDAY] [appt/APPOINTMENT] [po/POLICY_INDEX pon/POLICY_NAME pos/START_DATE poe/END_DATE paydate/PAYMENT_DUE_DATE amt/AMOUNT_DUE]`
Example: `edit 2 p/98765432` | +| [Sorting Clients](#sorting-clients--sort) | `sort `
Example: `sort birthday asc` | +| [Locating Clients by Name](#locating-clients-by-name--find) | `find [MORE_KEYWORDS]`
Example: `find Alice` | +| [Searching Appointments](#searching-appointments--search-a) | `search a/ `
Example: `search a/ 2024-10-10 23:00` | +| [Searching Birthdays](#searching-birthdays--search-b) | `search b/ `
Example: `search b/ 2000-03-15` | +| [Searching Policies](#searching-policy--search-p) | `search p/ `
Example: `search p/ sample Policy` | +| [Assigning a Policy](#assigning-a-policy--assign) | `assign INDEX pon/ pos/ poe/ paydate/ amt/`
Example: `assign 1 pon/Health Insurance pos/2022-01-01 poe/2023-01-01 paydate/2022-12-01 amt/300.00` | +| [Deleting a Policy](#deleting-a-policy--delete) | `delete po/`
Example: `delete 3 po/2` | +| [Mark Policy Payment](#marking-a-policy-payment-installment-as-paid--paid) | `paid pon/`
Example: `paid 2 pon/3` | +| [Undo a Command](#undo-a-command--undo) | `undo` | +| [Redo a Command](#redo-a-command--redo) | `redo` | +| [Clearing All Entries](#clearing-all-entries--clear) | `clear` | +| [Exiting the Program](#exiting-the-program--exit) | `exit` | +| [Viewing Help](#viewing-help--help) | `help` | diff --git a/docs/_config.yml b/docs/_config.yml index 6bd245d8f4e..5ebdd098301 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,4 +1,4 @@ -title: "AB-3" +title: "AgentConnect" theme: minima header_pages: @@ -8,7 +8,7 @@ header_pages: markdown: kramdown -repository: "se-edu/addressbook-level3" +repository: "AY2425S1-CS2103T-W10-3/tp" github_icon: "images/github-icon.png" plugins: diff --git a/docs/_layouts/page.html b/docs/_layouts/page.html index 01e4b2a93b8..ff334f5e2eb 100644 --- a/docs/_layouts/page.html +++ b/docs/_layouts/page.html @@ -12,3 +12,4 @@

{{ page.title | escape }}

+ diff --git a/docs/_sass/minima/_base.scss b/docs/_sass/minima/_base.scss index 0d3f6e80ced..d0ed9567dc0 100644 --- a/docs/_sass/minima/_base.scss +++ b/docs/_sass/minima/_base.scss @@ -287,9 +287,9 @@ table { .site-header { text-align: center; } - .site-header:before { - content: "AB-3"; - font-size: 32px; - } +// .site-header:before { +// content: "AgentConnect "; +// font-size: 32px; +// } } diff --git a/docs/diagrams/AddSequenceDiagram.puml b/docs/diagrams/AddSequenceDiagram.puml new file mode 100644 index 00000000000..c3a34e7bc3b --- /dev/null +++ b/docs/diagrams/AddSequenceDiagram.puml @@ -0,0 +1,72 @@ +@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 ":AddCommandParser" as AddCommandParser LOGIC_COLOR +participant "a:AddCommand" as AddCommand LOGIC_COLOR +participant "r:CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant "m:Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("add n/John Doe p/...") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("add n/John Doe p/...") +activate AddressBookParser + +create AddCommandParser +AddressBookParser -> AddCommandParser +activate AddCommandParser + +AddCommandParser --> AddressBookParser +deactivate AddCommandParser + +AddressBookParser -> AddCommandParser : parse(John) +activate AddCommandParser + +create AddCommand +AddCommandParser -> AddCommand +activate AddCommand + +AddCommand --> AddCommandParser : +deactivate AddCommand + +AddCommandParser --> AddressBookParser : a +deactivate AddCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +AddCommandParser -[hidden]-> AddressBookParser +destroy AddCommandParser + +AddressBookParser --> LogicManager : a +deactivate AddressBookParser + +LogicManager -> AddCommand : execute(m) +activate AddCommand + +AddCommand -> Model : addPerson(John) +activate Model + +Model --> AddCommand +deactivate Model + +create CommandResult +AddCommand -> CommandResult +activate CommandResult + +CommandResult --> AddCommand +deactivate CommandResult + +AddCommand --> LogicManager : r +deactivate AddCommand + + + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/BetterModelClassDiagram.puml b/docs/diagrams/BetterModelClassDiagram.puml index 598474a5c82..5d612b19b33 100644 --- a/docs/diagrams/BetterModelClassDiagram.puml +++ b/docs/diagrams/BetterModelClassDiagram.puml @@ -13,9 +13,12 @@ UniqueTagList -right-> "*" Tag UniquePersonList -right-> Person Person -up-> "*" Tag - +Person -right-> "*"Policy Person *--> Name Person *--> Phone Person *--> Email Person *--> Address +Person *--> Birthday +Person *--> Appointment + @enduml diff --git a/docs/diagrams/CommandSummaryActivityDiagram.puml b/docs/diagrams/CommandSummaryActivityDiagram.puml new file mode 100644 index 00000000000..33d7bc47cb8 --- /dev/null +++ b/docs/diagrams/CommandSummaryActivityDiagram.puml @@ -0,0 +1,47 @@ +@startuml +!define rect(x) rectangle x +skinparam monochrome true +skinparam defaultFontName Courier +skinparam node { + BorderColor DarkRed + FontSize 14 + FontColor DarkRed + BackgroundColor #FFFFCC + Shadowing true +} + +start + +:User enters a command; +:Parser parses the command; + +if () then ([known command]) + if () then ([Exit]) + :Exit command is created; + :Exit command is executed; + :Application closes; + stop + else + if () then ([List]) + :List command is created; + :List command is executed; + :UI list all available clients; + stop + else ([action commands]) + :Parser processes relevant action command; + if () then ([valid]) + :Command is created; + :Command is executed; + :UI shows respective success message; + stop + else ([invalid]) + :UI displays error message; + stop + endif + endif + endif +else ([unknown command]) + :UI displays invalid command; + stop +endif +@enduml diff --git a/docs/diagrams/DeleteSequenceDiagram.puml b/docs/diagrams/DeleteSequenceDiagram.puml index 5241e79d7da..0c9306f4f24 100644 --- a/docs/diagrams/DeleteSequenceDiagram.puml +++ b/docs/diagrams/DeleteSequenceDiagram.puml @@ -14,10 +14,10 @@ box Model MODEL_COLOR_T1 participant "m:Model" as Model MODEL_COLOR end box -[-> LogicManager : execute("delete 1") +[-> LogicManager : execute("delete John Doe") activate LogicManager -LogicManager -> AddressBookParser : parseCommand("delete 1") +LogicManager -> AddressBookParser : parseCommand("delete John Doe") activate AddressBookParser create DeleteCommandParser @@ -27,14 +27,14 @@ activate DeleteCommandParser DeleteCommandParser --> AddressBookParser deactivate DeleteCommandParser -AddressBookParser -> DeleteCommandParser : parse("1") +AddressBookParser -> DeleteCommandParser : parse("John Doe") activate DeleteCommandParser create DeleteCommand DeleteCommandParser -> DeleteCommand activate DeleteCommand -DeleteCommand --> DeleteCommandParser : +DeleteCommand --> DeleteCommandParser deactivate DeleteCommand DeleteCommandParser --> AddressBookParser : d @@ -49,21 +49,37 @@ deactivate AddressBookParser LogicManager -> DeleteCommand : execute(m) activate DeleteCommand -DeleteCommand -> Model : deletePerson(1) -activate Model +alt Person not found + DeleteCommand -> LogicManager : throw CommandException +else Valid person + DeleteCommand -> Model : getFilteredPersonList() + activate Model -Model --> DeleteCommand -deactivate Model + Model --> DeleteCommand : List + deactivate Model -create CommandResult -DeleteCommand -> CommandResult -activate CommandResult -CommandResult --> DeleteCommand -deactivate CommandResult -DeleteCommand --> LogicManager : r -deactivate DeleteCommand + alt Multiple persons found + DeleteCommand -> LogicManager : handleDuplicateNames() + else Single person found + DeleteCommand -> Model : deletePerson(p) + activate Model + + Model --> DeleteCommand + deactivate Model + + create CommandResult + DeleteCommand -> CommandResult + activate CommandResult + + CommandResult --> DeleteCommand + deactivate CommandResult + + DeleteCommand --> LogicManager : r + deactivate DeleteCommand + end +end [<--LogicManager deactivate LogicManager diff --git a/docs/diagrams/EditNameSequenceDiagram.puml b/docs/diagrams/EditNameSequenceDiagram.puml new file mode 100644 index 00000000000..b2c95cc533e --- /dev/null +++ b/docs/diagrams/EditNameSequenceDiagram.puml @@ -0,0 +1,109 @@ +@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 ":EditCommandParser" as EditCommandParser LOGIC_COLOR +participant "e:EditPersonDescriptor" as EditPersonDescriptor LOGIC_COLOR +participant "c:EditCommand" as EditCommand LOGIC_COLOR +participant "r:CommandResult" as CommandResult LOGIC_COLOR +end box + +box Commons LOGIC_COLOR_T2 +participant "i:Index" as Index LOGIC_COLOR_T3 +end box + +box Model MODEL_COLOR_T1 +participant "m:Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("edit 1 n/John Doe") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("edit 1 n/John Doe") +activate AddressBookParser + +create EditCommandParser +AddressBookParser -> EditCommandParser +activate EditCommandParser + +EditCommandParser --> AddressBookParser +deactivate EditCommandParser + +AddressBookParser -> EditCommandParser : parse(1 n/John Doe) +activate EditCommandParser + +Create Index +EditCommandParser -> Index : parse("1") +activate Index + +Index --> EditCommandParser +deactivate Index + +create EditPersonDescriptor +EditCommandParser -> EditPersonDescriptor +activate EditPersonDescriptor + +EditPersonDescriptor --> EditCommandParser +deactivate EditPersonDescriptor + +EditCommandParser -> EditPersonDescriptor : setName("John Doe") +activate EditPersonDescriptor + +EditPersonDescriptor --> EditCommandParser +deactivate EditPersonDescriptor + +create EditCommand +EditCommandParser -> EditCommand : EditCommand(i, e) +activate EditCommand + +EditCommand --> EditCommandParser : +deactivate EditCommand + +EditCommandParser --> AddressBookParser : c +deactivate EditCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +EditCommandParser -[hidden]-> AddressBookParser +destroy EditCommandParser + +AddressBookParser --> LogicManager : c +deactivate AddressBookParser + +LogicManager -> EditCommand : execute(m) +activate EditCommand + +EditCommand -> Model : setPerson() +activate Model + +Model --> EditCommand +deactivate Model + +EditCommand -> Model : commitAddressBook() +activate Model + +Model --> EditCommand +deactivate Model + +EditCommand -> Model : UpdateFilteredPersonList() +activate Model + +Model --> EditCommand +deactivate Model + +create CommandResult +EditCommand -> CommandResult +activate CommandResult + +CommandResult --> EditCommand +deactivate CommandResult + +EditCommand --> LogicManager : r +deactivate EditCommand + + + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/LogicClassDiagram.puml b/docs/diagrams/LogicClassDiagram.puml index 58b4f602ce6..f54662b687b 100644 --- a/docs/diagrams/LogicClassDiagram.puml +++ b/docs/diagrams/LogicClassDiagram.puml @@ -1,4 +1,5 @@ @startuml + !include style.puml skinparam arrowThickness 1.1 skinparam arrowColor LOGIC_COLOR_T4 diff --git a/docs/diagrams/ModelClassDiagram.puml b/docs/diagrams/ModelClassDiagram.puml index 0de5673070d..2c224676bb8 100644 --- a/docs/diagrams/ModelClassDiagram.puml +++ b/docs/diagrams/ModelClassDiagram.puml @@ -16,9 +16,12 @@ Class UniquePersonList Class Person Class Address Class Email +Class Appointment +Class Birthday Class Name Class Phone Class Tag +Class Policies Class I #FFFFFF } @@ -41,8 +44,10 @@ Person *--> Name Person *--> Phone Person *--> Email Person *--> Address +Person *--> Appointment +Person *--> Birthday Person *--> "*" Tag - +Person *--> "*" Policies Person -[hidden]up--> I UniquePersonList -[hidden]right-> I diff --git a/docs/diagrams/SearchAppointmentSequenceDiagram.puml b/docs/diagrams/SearchAppointmentSequenceDiagram.puml new file mode 100644 index 00000000000..8144c4929d7 --- /dev/null +++ b/docs/diagrams/SearchAppointmentSequenceDiagram.puml @@ -0,0 +1,84 @@ +@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 ":SearchAppointmentCommandParser" as SearchAppointmentCommandParser LOGIC_COLOR +participant "s:SearchAppointmentCommand" as SearchAppointmentCommand LOGIC_COLOR +participant "r:CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant "m:Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("search a/2023-12-31 14:30 to 2024-01-01 16:00") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("search a/2023-12-31 14:30 to 2024-01-01 16:00") +activate AddressBookParser + +create SearchAppointmentCommandParser +AddressBookParser -> SearchAppointmentCommandParser +activate SearchAppointmentCommandParser + +SearchAppointmentCommandParser --> AddressBookParser +deactivate SearchAppointmentCommandParser + +AddressBookParser -> SearchAppointmentCommandParser : parse("2023-12-31 14:30 to 2024-01-01 16:00") +activate SearchAppointmentCommandParser + +create SearchAppointmentCommand +SearchAppointmentCommandParser -> SearchAppointmentCommand +activate SearchAppointmentCommand + +SearchAppointmentCommand --> SearchAppointmentCommandParser +deactivate SearchAppointmentCommand + +SearchAppointmentCommandParser --> AddressBookParser : s +deactivate SearchAppointmentCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +SearchAppointmentCommandParser -[hidden]-> AddressBookParser +destroy SearchAppointmentCommandParser + +AddressBookParser --> LogicManager : s +deactivate AddressBookParser + +LogicManager -> SearchAppointmentCommand : execute(m) +activate SearchAppointmentCommand + +alt Invalid date-time format + SearchAppointmentCommand -> LogicManager : throw CommandException + deactivate SearchAppointmentCommand +else No appointments found + SearchAppointmentCommand -> Model : updateFilteredPersonList(predicate) + activate Model + + Model --> SearchAppointmentCommand + deactivate Model + + SearchAppointmentCommand -> LogicManager : CommandResult("No appointments found.") + deactivate SearchAppointmentCommand +else Appointments found + SearchAppointmentCommand -> Model : updateFilteredPersonList(predicate) + activate Model + + Model --> SearchAppointmentCommand + deactivate Model + + create CommandResult + SearchAppointmentCommand -> CommandResult + activate CommandResult + + CommandResult --> SearchAppointmentCommand + deactivate CommandResult + + SearchAppointmentCommand --> LogicManager : r + deactivate SearchAppointmentCommand +end + +[<-- LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/SearchBirthdaySequenceDiagram.puml b/docs/diagrams/SearchBirthdaySequenceDiagram.puml new file mode 100644 index 00000000000..871bccc73bf --- /dev/null +++ b/docs/diagrams/SearchBirthdaySequenceDiagram.puml @@ -0,0 +1,84 @@ +@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 ":SearchBirthdayCommandParser" as SearchBirthdayCommandParser LOGIC_COLOR +participant "s:SearchBirthdayCommand" as SearchBirthdayCommand LOGIC_COLOR +participant "r:CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant "m:Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("search b/2000-04-25 to 2000-05-25") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("search b/2000-04-25 to 2000-05-25") +activate AddressBookParser + +create SearchBirthdayCommandParser +AddressBookParser -> SearchBirthdayCommandParser +activate SearchBirthdayCommandParser + +SearchBirthdayCommandParser --> AddressBookParser +deactivate SearchBirthdayCommandParser + +AddressBookParser -> SearchBirthdayCommandParser : parse("2000-04-25 to 2000-05-25") +activate SearchBirthdayCommandParser + +create SearchBirthdayCommand +SearchBirthdayCommandParser -> SearchBirthdayCommand +activate SearchBirthdayCommand + +SearchBirthdayCommand --> SearchBirthdayCommandParser +deactivate SearchBirthdayCommand + +SearchBirthdayCommandParser --> AddressBookParser : s +deactivate SearchBirthdayCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +SearchBirthdayCommandParser -[hidden]-> AddressBookParser +destroy SearchBirthdayCommandParser + +AddressBookParser --> LogicManager : s +deactivate AddressBookParser + +LogicManager -> SearchBirthdayCommand : execute(m) +activate SearchBirthdayCommand + +alt Invalid date format + SearchBirthdayCommand -> LogicManager : throw CommandException + deactivate SearchBirthdayCommand +else No birthdays found + SearchBirthdayCommand -> Model : updateFilteredPersonList(predicate) + activate Model + + Model --> SearchBirthdayCommand + deactivate Model + + SearchBirthdayCommand -> LogicManager : CommandResult("No birthdays found.") + deactivate SearchBirthdayCommand +else Birthdays found + SearchBirthdayCommand -> Model : updateFilteredPersonList(predicate) + activate Model + + Model --> SearchBirthdayCommand + deactivate Model + + create CommandResult + SearchBirthdayCommand -> CommandResult + activate CommandResult + + CommandResult --> SearchBirthdayCommand + deactivate CommandResult + + SearchBirthdayCommand --> LogicManager : r + deactivate SearchBirthdayCommand +end + +[<-- LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/SearchPolicySequenceDiagram.puml b/docs/diagrams/SearchPolicySequenceDiagram.puml new file mode 100644 index 00000000000..4811f2d71ab --- /dev/null +++ b/docs/diagrams/SearchPolicySequenceDiagram.puml @@ -0,0 +1,84 @@ +@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 ":SearchPolicyCommandParser" as SearchPolicyCommandParser LOGIC_COLOR +participant "s:SearchPolicyCommand" as SearchPolicyCommand LOGIC_COLOR +participant "r:CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant "m:Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("search p/Health Insurance") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("search p/Health Insurance") +activate AddressBookParser + +create SearchPolicyCommandParser +AddressBookParser -> SearchPolicyCommandParser +activate SearchPolicyCommandParser + +SearchPolicyCommandParser --> AddressBookParser +deactivate SearchPolicyCommandParser + +AddressBookParser -> SearchPolicyCommandParser : parse("Health Insurance") +activate SearchPolicyCommandParser + +create SearchPolicyCommand +SearchPolicyCommandParser -> SearchPolicyCommand +activate SearchPolicyCommand + +SearchPolicyCommand --> SearchPolicyCommandParser +deactivate SearchPolicyCommand + +SearchPolicyCommandParser --> AddressBookParser : s +deactivate SearchPolicyCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +SearchPolicyCommandParser -[hidden]-> AddressBookParser +destroy SearchPolicyCommandParser + +AddressBookParser --> LogicManager : s +deactivate AddressBookParser + +LogicManager -> SearchPolicyCommand : execute(m) +activate SearchPolicyCommand + +alt Invalid policy name + SearchPolicyCommand -> LogicManager : throw CommandException + deactivate SearchPolicyCommand +else No policies found + SearchPolicyCommand -> Model : updateFilteredPersonList(predicate) + activate Model + + Model --> SearchPolicyCommand + deactivate Model + + SearchPolicyCommand -> LogicManager : CommandResult("No policies found.") + deactivate SearchPolicyCommand +else Policies found + SearchPolicyCommand -> Model : updateFilteredPersonList(predicate) + activate Model + + Model --> SearchPolicyCommand + deactivate Model + + create CommandResult + SearchPolicyCommand -> CommandResult + activate CommandResult + + CommandResult --> SearchPolicyCommand + deactivate CommandResult + + SearchPolicyCommand --> LogicManager : r + deactivate SearchPolicyCommand +end + +[<-- LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/SortSequenceDiagram.puml b/docs/diagrams/SortSequenceDiagram.puml new file mode 100644 index 00000000000..e6e8ae6d5c2 --- /dev/null +++ b/docs/diagrams/SortSequenceDiagram.puml @@ -0,0 +1,70 @@ +@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 ":SortCommandParser" as SortCommandParser LOGIC_COLOR +participant "d:SortCommand" as SortCommand LOGIC_COLOR +participant "r:CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant "m:Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("sort n/ asc") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("sort n/ asc") +activate AddressBookParser + +create SortCommandParser +AddressBookParser -> SortCommandParser +activate SortCommandParser + +SortCommandParser --> AddressBookParser +deactivate SortCommandParser + +AddressBookParser -> SortCommandParser : parse("n/ asc") +activate SortCommandParser + +create SortCommand +SortCommandParser -> SortCommand +activate SortCommand + +SortCommand --> SortCommandParser +deactivate SortCommand + +SortCommandParser --> AddressBookParser : s +deactivate SortCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +SortCommandParser -[hidden]-> AddressBookParser +destroy SortCommandParser + +AddressBookParser --> LogicManager : s +deactivate AddressBookParser + +LogicManager -> SortCommand : execute(m) +activate SortCommand + +SortCommand -> Model : sortPersonList(n/ asc) +activate Model + +Model --> SortCommand +deactivate Model + +create CommandResult +SortCommand -> CommandResult +activate CommandResult + +CommandResult --> SortCommand +deactivate CommandResult + +SortCommand --> LogicManager : r +deactivate SortCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/images/BetterModelClassDiagram.png b/docs/images/BetterModelClassDiagram.png index 02a42e35e76..3e82323e8c2 100644 Binary files a/docs/images/BetterModelClassDiagram.png and b/docs/images/BetterModelClassDiagram.png differ diff --git a/docs/images/CommandSummaryActivityDiagram.png b/docs/images/CommandSummaryActivityDiagram.png new file mode 100644 index 00000000000..6c8d70d4b5a Binary files /dev/null and b/docs/images/CommandSummaryActivityDiagram.png differ diff --git a/docs/images/DeletePersonUndoUI.png b/docs/images/DeletePersonUndoUI.png new file mode 100644 index 00000000000..d0a8e780cf2 Binary files /dev/null and b/docs/images/DeletePersonUndoUI.png differ diff --git a/docs/images/DeleteSequenceDiagram.png b/docs/images/DeleteSequenceDiagram.png index ac2ae217c51..069071e455f 100644 Binary files a/docs/images/DeleteSequenceDiagram.png and b/docs/images/DeleteSequenceDiagram.png differ diff --git a/docs/images/ModelClassDiagram.png b/docs/images/ModelClassDiagram.png index a19fb1b4ac8..1db81ac6965 100644 Binary files a/docs/images/ModelClassDiagram.png and b/docs/images/ModelClassDiagram.png differ diff --git a/docs/images/StorageClassDiagram.png b/docs/images/StorageClassDiagram.png index 18fa4d0d51f..3ccd7d80e54 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..e7925c03008 100644 Binary files a/docs/images/Ui.png and b/docs/images/Ui.png differ diff --git a/docs/images/addUI.png b/docs/images/addUI.png new file mode 100644 index 00000000000..fb68efdeef3 Binary files /dev/null and b/docs/images/addUI.png differ diff --git a/docs/images/agentconnectlogo.png b/docs/images/agentconnectlogo.png new file mode 100644 index 00000000000..728070dae2f Binary files /dev/null and b/docs/images/agentconnectlogo.png differ diff --git a/docs/images/assignpolicyUI.png b/docs/images/assignpolicyUI.png new file mode 100644 index 00000000000..da540fd30ee Binary files /dev/null and b/docs/images/assignpolicyUI.png differ diff --git a/docs/images/deleteUI.png b/docs/images/deleteUI.png new file mode 100644 index 00000000000..6c968b0fcb4 Binary files /dev/null and b/docs/images/deleteUI.png differ diff --git a/docs/images/deletepolicyUI.png b/docs/images/deletepolicyUI.png new file mode 100644 index 00000000000..f64034c76b1 Binary files /dev/null and b/docs/images/deletepolicyUI.png differ diff --git a/docs/images/editUI.png b/docs/images/editUI.png new file mode 100644 index 00000000000..1cc8243c837 Binary files /dev/null and b/docs/images/editUI.png differ diff --git a/docs/images/edwin1022.png b/docs/images/edwin1022.png new file mode 100644 index 00000000000..c6470970270 Binary files /dev/null and b/docs/images/edwin1022.png differ diff --git a/docs/images/findstevenkurtResult.png b/docs/images/findstevenkurtResult.png new file mode 100644 index 00000000000..1be6f4d8b78 Binary files /dev/null and b/docs/images/findstevenkurtResult.png differ diff --git a/docs/images/helpMessage.png b/docs/images/helpMessage.png index b1f70470137..3690eca8374 100644 Binary files a/docs/images/helpMessage.png and b/docs/images/helpMessage.png differ diff --git a/docs/images/m-kew.png b/docs/images/m-kew.png new file mode 100644 index 00000000000..95c66bc3f00 Binary files /dev/null and b/docs/images/m-kew.png differ diff --git a/docs/images/mingyiaw.png b/docs/images/mingyiaw.png new file mode 100644 index 00000000000..0251c69478e Binary files /dev/null and b/docs/images/mingyiaw.png differ diff --git a/docs/images/paidUI.png b/docs/images/paidUI.png new file mode 100644 index 00000000000..5ee91691a09 Binary files /dev/null and b/docs/images/paidUI.png differ diff --git a/docs/images/redoUI.png b/docs/images/redoUI.png new file mode 100644 index 00000000000..6864f89f190 Binary files /dev/null and b/docs/images/redoUI.png differ diff --git a/docs/images/searchapptUI.png b/docs/images/searchapptUI.png new file mode 100644 index 00000000000..05c504e91f0 Binary files /dev/null and b/docs/images/searchapptUI.png differ diff --git a/docs/images/searchapptrangeUI.png b/docs/images/searchapptrangeUI.png new file mode 100644 index 00000000000..f91b62a8670 Binary files /dev/null and b/docs/images/searchapptrangeUI.png differ diff --git a/docs/images/searchbirthdayUI.png b/docs/images/searchbirthdayUI.png new file mode 100644 index 00000000000..4b423cc602e Binary files /dev/null and b/docs/images/searchbirthdayUI.png differ diff --git a/docs/images/searchbirthdayrangeUI.png b/docs/images/searchbirthdayrangeUI.png new file mode 100644 index 00000000000..709e69979c6 Binary files /dev/null and b/docs/images/searchbirthdayrangeUI.png differ diff --git a/docs/images/searchpolicyUI.png b/docs/images/searchpolicyUI.png new file mode 100644 index 00000000000..9d846a1baff Binary files /dev/null and b/docs/images/searchpolicyUI.png differ diff --git a/docs/images/sortUI.png b/docs/images/sortUI.png new file mode 100644 index 00000000000..a0d343dc725 Binary files /dev/null and b/docs/images/sortUI.png differ diff --git a/docs/images/sortapptUI.png b/docs/images/sortapptUI.png new file mode 100644 index 00000000000..fa8e1a058a9 Binary files /dev/null and b/docs/images/sortapptUI.png differ diff --git a/docs/images/sortbirthdayUI.png b/docs/images/sortbirthdayUI.png new file mode 100644 index 00000000000..94ac2e44de3 Binary files /dev/null and b/docs/images/sortbirthdayUI.png differ diff --git a/docs/images/sortnameUI.png b/docs/images/sortnameUI.png new file mode 100644 index 00000000000..e0687240772 Binary files /dev/null and b/docs/images/sortnameUI.png differ diff --git a/docs/images/sortpaydateUI.png b/docs/images/sortpaydateUI.png new file mode 100644 index 00000000000..1b78710cb4a Binary files /dev/null and b/docs/images/sortpaydateUI.png differ diff --git a/docs/images/ssirmentos.png b/docs/images/ssirmentos.png new file mode 100644 index 00000000000..80c40024a63 Binary files /dev/null and b/docs/images/ssirmentos.png differ diff --git a/docs/images/the0nlyjuan.png b/docs/images/the0nlyjuan.png new file mode 100644 index 00000000000..2b811154de4 Binary files /dev/null and b/docs/images/the0nlyjuan.png differ diff --git a/docs/images/undoUI.png b/docs/images/undoUI.png new file mode 100644 index 00000000000..aa3dd6347c8 Binary files /dev/null and b/docs/images/undoUI.png differ diff --git a/docs/index.md b/docs/index.md index 7601dbaad0d..009aa67fcd6 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,6 @@ --- layout: page -title: AddressBook Level-3 +title: AgentConnect --- [![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). +**AgentConnect is a desktop application for managing your client details and policies.** 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 AgentConnect, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.md#quick-start). +* If you are interested about developing AgentConnect, the [**Developer Guide**](DeveloperGuide.md) is a good place to start. **Acknowledgements** diff --git a/docs/team/edwinwong.md b/docs/team/edwinwong.md new file mode 100644 index 00000000000..ad3f9ac40b8 --- /dev/null +++ b/docs/team/edwinwong.md @@ -0,0 +1,46 @@ +--- +layout: page +title: Edwin Wong's Project Portfolio Page +--- + +### Project: AddressBook Level 3 + +AddressBook - Level 3 is a desktop address book application used for teaching Software Engineering principles. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. + +Given below are my contributions to the project. + +* **New Feature**: Added the ability to undo/redo previous commands. + * What it does: allows the user to undo all previous commands one at a time. Preceding undo commands can be reversed by using the redo command. + * Justification: This feature improves the product significantly because a user can make mistakes in commands and the app should provide a convenient way to rectify them. + * Highlights: This enhancement affects existing commands and commands to be added in future. It required an in-depth analysis of design alternatives. The implementation too was challenging as it required changes to existing commands. + * Credits: *{mention here if you reused any code/ideas from elsewhere or if a third-party library is heavily used in the feature so that a reader can make a more accurate judgement of how much effort went into the feature}* + +* **New Feature**: Added a history command that allows the user to navigate to previous commands using up/down keys. + +* **Code contributed**: [RepoSense link]() + +* **Project management**: + * Managed releases `v1.3` - `v1.5rc` (3 releases) on GitHub + +* **Enhancements to existing features**: + * Updated the GUI color scheme (Pull requests [\#33](), [\#34]()) + * Wrote additional tests for existing features to increase coverage from 88% to 92% (Pull requests [\#36](), [\#38]()) + +* **Documentation**: + * User Guide: + * Added documentation for the features `delete` and `find` [\#72]() + * Did cosmetic tweaks to existing documentation of features `clear`, `exit`: [\#74]() + * Developer Guide: + * Added implementation details of the `delete` feature. + +* **Community**: + * PRs reviewed (with non-trivial review comments): [\#12](), [\#32](), [\#19](), [\#42]() + * Contributed to forum discussions (examples: [1](), [2](), [3](), [4]()) + * Reported bugs and suggestions for other teams in the class (examples: [1](), [2](), [3]()) + * Some parts of the history feature I added was adopted by several other class mates ([1](), [2]()) + +* **Tools**: + * Integrated a third party library (Natty) to the project ([\#42]()) + * Integrated a new Github plugin (CircleCI) to the team repo + +* _{you can add/remove categories in the list above}_ diff --git a/docs/team/m-kew.md b/docs/team/m-kew.md new file mode 100644 index 00000000000..8ec14f498f9 --- /dev/null +++ b/docs/team/m-kew.md @@ -0,0 +1,46 @@ +--- +layout: page +title: Mant's Project Portfolio Page +--- + +### Project: AddressBook Level 3 + +AddressBook - Level 3 is a desktop address book application used for teaching Software Engineering principles. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. + +Given below are my contributions to the project. + +* **New Feature**: Added the ability to undo/redo previous commands. + * What it does: allows the user to undo all previous commands one at a time. Preceding undo commands can be reversed by using the redo command. + * Justification: This feature improves the product significantly because a user can make mistakes in commands and the app should provide a convenient way to rectify them. + * Highlights: This enhancement affects existing commands and commands to be added in future. It required an in-depth analysis of design alternatives. The implementation too was challenging as it required changes to existing commands. + * Credits: *{mention here if you reused any code/ideas from elsewhere or if a third-party library is heavily used in the feature so that a reader can make a more accurate judgement of how much effort went into the feature}* + +* **New Feature**: Added a history command that allows the user to navigate to previous commands using up/down keys. + +* **Code contributed**: [RepoSense link]() + +* **Project management**: + * Managed releases `v1.3` - `v1.5rc` (3 releases) on GitHub + +* **Enhancements to existing features**: + * Updated the GUI color scheme (Pull requests [\#33](), [\#34]()) + * Wrote additional tests for existing features to increase coverage from 88% to 92% (Pull requests [\#36](), [\#38]()) + +* **Documentation**: + * User Guide: + * Added documentation for the features `delete` and `find` [\#72]() + * Did cosmetic tweaks to existing documentation of features `clear`, `exit`: [\#74]() + * Developer Guide: + * Added implementation details of the `delete` feature. + +* **Community**: + * PRs reviewed (with non-trivial review comments): [\#12](), [\#32](), [\#19](), [\#42]() + * Contributed to forum discussions (examples: [1](), [2](), [3](), [4]()) + * Reported bugs and suggestions for other teams in the class (examples: [1](), [2](), [3]()) + * Some parts of the history feature I added was adopted by several other class mates ([1](), [2]()) + +* **Tools**: + * Integrated a third party library (Natty) to the project ([\#42]()) + * Integrated a new Github plugin (CircleCI) to the team repo + +* _{you can add/remove categories in the list above}_ diff --git a/docs/team/johndoe.md b/docs/team/mingyiaw.md similarity index 100% rename from docs/team/johndoe.md rename to docs/team/mingyiaw.md diff --git a/docs/team/ssirmentos.md b/docs/team/ssirmentos.md new file mode 100644 index 00000000000..b26cf96d719 --- /dev/null +++ b/docs/team/ssirmentos.md @@ -0,0 +1,46 @@ +--- +layout: page +title: Denon's Project Portfolio Page +--- + +### Project: AddressBook Level 3 + +AddressBook - Level 3 is a desktop address book application used for teaching Software Engineering principles. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. + +Given below are my contributions to the project. + +* **New Feature**: Added the ability to undo/redo previous commands. + * What it does: allows the user to undo all previous commands one at a time. Preceding undo commands can be reversed by using the redo command. + * Justification: This feature improves the product significantly because a user can make mistakes in commands and the app should provide a convenient way to rectify them. + * Highlights: This enhancement affects existing commands and commands to be added in future. It required an in-depth analysis of design alternatives. The implementation too was challenging as it required changes to existing commands. + * Credits: *{mention here if you reused any code/ideas from elsewhere or if a third-party library is heavily used in the feature so that a reader can make a more accurate judgement of how much effort went into the feature}* + +* **New Feature**: Added a history command that allows the user to navigate to previous commands using up/down keys. + +* **Code contributed**: [RepoSense link]() + +* **Project management**: + * Managed releases `v1.3` - `v1.5rc` (3 releases) on GitHub + +* **Enhancements to existing features**: + * Updated the GUI color scheme (Pull requests [\#33](), [\#34]()) + * Wrote additional tests for existing features to increase coverage from 88% to 92% (Pull requests [\#36](), [\#38]()) + +* **Documentation**: + * User Guide: + * Added documentation for the features `delete` and `find` [\#72]() + * Did cosmetic tweaks to existing documentation of features `clear`, `exit`: [\#74]() + * Developer Guide: + * Added implementation details of the `delete` feature. + +* **Community**: + * PRs reviewed (with non-trivial review comments): [\#12](), [\#32](), [\#19](), [\#42]() + * Contributed to forum discussions (examples: [1](), [2](), [3](), [4]()) + * Reported bugs and suggestions for other teams in the class (examples: [1](), [2](), [3]()) + * Some parts of the history feature I added was adopted by several other class mates ([1](), [2]()) + +* **Tools**: + * Integrated a third party library (Natty) to the project ([\#42]()) + * Integrated a new Github plugin (CircleCI) to the team repo + +* _{you can add/remove categories in the list above}_ diff --git a/docs/team/the0nlyjuan.md b/docs/team/the0nlyjuan.md new file mode 100644 index 00000000000..0a68b20e961 --- /dev/null +++ b/docs/team/the0nlyjuan.md @@ -0,0 +1,46 @@ +--- +layout: page +title: Juan Wen's Project Portfolio Page +--- + +### Project: AddressBook Level 3 + +AddressBook - Level 3 is a desktop address book application used for teaching Software Engineering principles. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. + +Given below are my contributions to the project. + +* **New Feature**: Added the ability to undo/redo previous commands. + * What it does: allows the user to undo all previous commands one at a time. Preceding undo commands can be reversed by using the redo command. + * Justification: This feature improves the product significantly because a user can make mistakes in commands and the app should provide a convenient way to rectify them. + * Highlights: This enhancement affects existing commands and commands to be added in future. It required an in-depth analysis of design alternatives. The implementation too was challenging as it required changes to existing commands. + * Credits: *{mention here if you reused any code/ideas from elsewhere or if a third-party library is heavily used in the feature so that a reader can make a more accurate judgement of how much effort went into the feature}* + +* **New Feature**: Added a history command that allows the user to navigate to previous commands using up/down keys. + +* **Code contributed**: [RepoSense link]() + +* **Project management**: + * Managed releases `v1.3` - `v1.5rc` (3 releases) on GitHub + +* **Enhancements to existing features**: + * Updated the GUI color scheme (Pull requests [\#33](), [\#34]()) + * Wrote additional tests for existing features to increase coverage from 88% to 92% (Pull requests [\#36](), [\#38]()) + +* **Documentation**: + * User Guide: + * Added documentation for the features `delete` and `find` [\#72]() + * Did cosmetic tweaks to existing documentation of features `clear`, `exit`: [\#74]() + * Developer Guide: + * Added implementation details of the `delete` feature. + +* **Community**: + * PRs reviewed (with non-trivial review comments): [\#12](), [\#32](), [\#19](), [\#42]() + * Contributed to forum discussions (examples: [1](), [2](), [3](), [4]()) + * Reported bugs and suggestions for other teams in the class (examples: [1](), [2](), [3]()) + * Some parts of the history feature I added was adopted by several other class mates ([1](), [2]()) + +* **Tools**: + * Integrated a third party library (Natty) to the project ([\#42]()) + * Integrated a new Github plugin (CircleCI) to the team repo + +* _{you can add/remove categories in the list above}_ diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java index 678ddc8c218..4ee111ee9e4 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, 3, 0, true); private static final Logger logger = LogsCenter.getLogger(MainApp.class); diff --git a/src/main/java/seedu/address/logic/Messages.java b/src/main/java/seedu/address/logic/Messages.java index ecd32c31b53..05df1e1da12 100644 --- a/src/main/java/seedu/address/logic/Messages.java +++ b/src/main/java/seedu/address/logic/Messages.java @@ -15,9 +15,35 @@ public class Messages { public static final String MESSAGE_UNKNOWN_COMMAND = "Unknown command"; public static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format! \n%1$s"; public static final String MESSAGE_INVALID_PERSON_DISPLAYED_INDEX = "The person index provided is invalid"; + public static final String MESSAGE_INVALID_PERSON_DISPLAYED_NAME = "The person name provided is invalid"; public static final String MESSAGE_PERSONS_LISTED_OVERVIEW = "%1$d persons listed!"; public static final String MESSAGE_DUPLICATE_FIELDS = "Multiple values specified for the following single-valued field(s): "; + public static final String MESSAGE_INVALID_POLICY_FORMAT = "The policy name format is invalid."; + public static final String MESSAGE_INVALID_POLICY_DISPLAYED_INDEX = "The policy index provided is invalid"; + public static final String MESSAGE_DUPLICATE_POLICY_NAME = "The policy name provided is duplicated"; + public static final String MESSAGE_DUPLICATE_POLICY_INDEX = "Multiple identical policy index specified to edit"; + public static final String MESSAGE_INVALID_DATE_FORMAT = "The date format is invalid. " + + "Please use yyyy-MM-dd format."; + public static final String MESSAGE_INVALID_DATERANGE_FORMAT = "The date range format is invalid. " + + "Please use yyyy-MM-dd to yyyy-MM-dd format."; + public static final String MESSAGE_START_DATE_AFTER_END_DATE = "Start date cannot be after end date."; + public static final String MESSAGE_SUCCESS_SEARCH_BIRTHDAY = "Listed all clients with birthdays %s"; + public static final String MESSAGE_SUCCESS_SEARCH_APPOINTMENT = "Listed all clients with appointments %s"; + public static final String MESSAGE_INVALID_DATETIME_FORMAT = "The date format is invalid. " + + "Please use yyyy-MM-dd HH:mm format."; + public static final String MESSAGE_INVALID_DATETIMERANGE_FORMAT = "The date-time range format is invalid. " + + "Please use 'yyyy-MM-dd HH:mm to yyyy-MM-dd HH:mm' format."; + public static final String MESSAGE_START_DATETIME_AFTER_END_DATETIME = "Start date-time cannot be " + + "after end date-time."; + public static final String MESSAGE_SUCCESS_SEARCH_POLICY = "Listed all clients with policy: %s"; + public static final String MESSAGE_MISSING_PREFIX_SEARCH = """ + Search command cannot be empty. + The available prefixes are: b/, a/ or /p. + b/ : search for client's birthday given a specified/range date + a/ : search for client's appointment given a specified/range datetime + p/ : search for client's policy given a policy name + """; /** * Returns an error message indicating the duplicate prefixes. @@ -43,6 +69,10 @@ public static String format(Person person) { .append(person.getEmail()) .append("; Address: ") .append(person.getAddress()) + .append("; Birthday: ") + .append(person.getBirthday()) + .append("; Appointment: ") + .append(person.getAppointment()) .append("; Tags: "); person.getTags().forEach(builder::append); return builder.toString(); diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddCommand.java index 5d7185a9680..9823c9904a4 100644 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ b/src/main/java/seedu/address/logic/commands/AddCommand.java @@ -2,10 +2,11 @@ import static java.util.Objects.requireNonNull; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_APPOINTMENT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_BIRTHDAY; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; import seedu.address.commons.util.ToStringBuilder; import seedu.address.logic.Messages; @@ -26,14 +27,15 @@ public class AddCommand extends Command { + PREFIX_PHONE + "PHONE " + PREFIX_EMAIL + "EMAIL " + PREFIX_ADDRESS + "ADDRESS " - + "[" + PREFIX_TAG + "TAG]...\n" - + "Example: " + COMMAND_WORD + " " - + PREFIX_NAME + "John Doe " - + PREFIX_PHONE + "98765432 " - + PREFIX_EMAIL + "johnd@example.com " - + PREFIX_ADDRESS + "311, Clementi Ave 2, #02-25 " - + PREFIX_TAG + "friends " - + PREFIX_TAG + "owesMoney"; + + PREFIX_BIRTHDAY + "BIRTHDAY " + + PREFIX_APPOINTMENT + "APPOINTMENT " + + "\nExample: " + COMMAND_WORD + " " + + PREFIX_NAME + "John Doe" + " " + + PREFIX_PHONE + "98765432" + " " + + PREFIX_EMAIL + "johnd@example.com" + " " + + PREFIX_ADDRESS + "311, Clementi Ave 2, #02-25" + " " + + PREFIX_BIRTHDAY + "1990-10-10" + " " + + PREFIX_APPOINTMENT + "2024-12-12 12:00" + " "; public static final String MESSAGE_SUCCESS = "New person added: %1$s"; public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book"; @@ -57,6 +59,7 @@ public CommandResult execute(Model model) throws CommandException { } model.addPerson(toAdd); + model.commitAddressBook(); return new CommandResult(String.format(MESSAGE_SUCCESS, Messages.format(toAdd))); } diff --git a/src/main/java/seedu/address/logic/commands/AssignPolicyCommand.java b/src/main/java/seedu/address/logic/commands/AssignPolicyCommand.java new file mode 100644 index 00000000000..8d96108f7f1 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AssignPolicyCommand.java @@ -0,0 +1,102 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NEXT_PAYMENT_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PAYMENT_AMOUNT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_POLICY_END_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_POLICY_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_POLICY_START_DATE; + +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; +import seedu.address.model.person.Policy; + + + +/** + * Command that assigns a policy to a person. + */ +public class AssignPolicyCommand extends Command { + public static final String COMMAND_WORD = "assign"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Assigns a policy to a person" + + " by the index number used in the displayed person list. " + + "\nParameters: " + "INDEX " + + PREFIX_POLICY_NAME + "POLICYNAME " + + PREFIX_POLICY_START_DATE + "STARTDATE " + + PREFIX_POLICY_END_DATE + "ENDDATE " + + PREFIX_NEXT_PAYMENT_DATE + "DUEDATE " + + PREFIX_PAYMENT_AMOUNT + "AMOUNT " + + "\nExample: " + COMMAND_WORD + " 1 " + + PREFIX_POLICY_NAME + "PolicyOne " + + PREFIX_POLICY_START_DATE + "2022-12-12 " + + PREFIX_POLICY_END_DATE + "2023-12-12 " + + PREFIX_NEXT_PAYMENT_DATE + "2023-11-01 " + + PREFIX_PAYMENT_AMOUNT + "300.00 "; + + public static final String MESSAGE_SUCCESS = "Policy successfully assigned to %1$s"; + public static final String MESSAGE_DUPLICATE_POLICY = "This policy already exists"; + + private final Index index; + private final Policy policy; + private Person personBeforeEdit; + + /** + * Creates an AssignPolicyCommand to assign the specified {@code policy} to a person + * at the specified {@code index}. + * + * @param index The index of the person to assign the policy to. + * @param policy The policy to be assigned to the person. + * @throws NullPointerException if {@code index} or {@code policy} is null. + */ + public AssignPolicyCommand(Index index, Policy policy) { + requireNonNull(index); + requireNonNull(policy); + this.index = index; + this.policy = policy; + } + + /** + * Executes the command to assign the policy to the person at the specified index. + * + * @param model The model containing the person list to be modified. + * @return The result of the command execution containing the success message. + * @throws CommandException if the index is invalid or if the policy already exists for the person. + * @throws NullPointerException if {@code model} is null. + */ + 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 personToEdit = lastShownList.get(index.getZeroBased()); + boolean editSuccess = personToEdit.assignPolicy(policy); + if (!editSuccess) { + throw new CommandException(MESSAGE_DUPLICATE_POLICY); + } + return new CommandResult(String.format(MESSAGE_SUCCESS, Messages.format(personToEdit))); + } + + @Override + public boolean equals(Object other) { + // Short circuit if the same object + if (other == this) { + return true; + } + // Instance of handles nulls and type check + if (!(other instanceof AssignPolicyCommand)) { + return false; + } + // Cast and compare the policyName attribute + AssignPolicyCommand otherCommand = (AssignPolicyCommand) other; + return this.policy.equals(otherCommand.policy); + } + +} + diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/address/logic/commands/ClearCommand.java index 9c86b1fa6e4..cd3e52dbac0 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.ui.MainWindow; /** * Clears the address book. @@ -12,12 +13,26 @@ public class ClearCommand extends Command { public static final String COMMAND_WORD = "clear"; public static final String MESSAGE_SUCCESS = "Address book has been cleared!"; + public static final String MESSAGE_CONFIRMATION = "Are you sure you want to clear the address book? "; + public static final String MESSAGE_ABORT = "Clear command aborted."; - + /** + * Executes the clear command. + * + * @param model The model in which the command should operate. + * @return The result of the command execution. + */ @Override public CommandResult execute(Model model) { requireNonNull(model); + + boolean isConfirmed = MainWindow.showConfirmationDialog(MESSAGE_CONFIRMATION); + if (!isConfirmed) { + return new CommandResult(MESSAGE_ABORT); + } + model.setAddressBook(new AddressBook()); + model.commitAddressBook(); return new CommandResult(MESSAGE_SUCCESS); } } diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java index 1135ac19b74..7da97742d3a 100644 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ b/src/main/java/seedu/address/logic/commands/DeleteCommand.java @@ -1,48 +1,192 @@ package seedu.address.logic.commands; - import static java.util.Objects.requireNonNull; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; 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.Name; import seedu.address.model.person.Person; +import seedu.address.model.person.Policy; +import seedu.address.ui.MainWindow; /** - * Deletes a person identified using it's displayed index from the address book. + * Deletes a person or policy identified using it's displayed index from the address book. + * Also allows deletion by name. */ public class DeleteCommand extends Command { - public static final String COMMAND_WORD = "delete"; - public static final String MESSAGE_USAGE = COMMAND_WORD - + ": Deletes 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"; + + ": Deletes the specified person or policy from their respective lists.\n" + + "Use the index number shown in the displayed persons and policy list or the person's exact name\n" + + "Parameters: INDEX(positive integers) + po/policyIndex or NAME\n" + + "Example: " + COMMAND_WORD + " 1 or " + COMMAND_WORD + " 1 po/1 or " + COMMAND_WORD + " Alex Yeoh"; public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted Person: %1$s"; + public static final String MESSAGE_DELETE_POLICY_SUCCESS = "Deleted Policy %1$d from %2$s"; + public static final String MESSAGE_DUPLICATE_NAMES = "Multiple persons found with the name '%1$s'." + + " Please specify the index to delete:\n%2$s"; - private final Index targetIndex; + private static final Logger logger = Logger.getLogger(DeleteCommand.class.getName()); + private final Index targetIndex; + private final Name targetName; + private final Index policyIndex; + + /** + * Creates a DeleteCommand to delete the person at the specified {@code Index}. + * + * @param targetIndex Index of the person to delete. + */ public DeleteCommand(Index targetIndex) { this.targetIndex = targetIndex; + this.targetName = null; + this.policyIndex = null; + } + + /** + * Creates a DeleteCommand to delete the person with the specified {@code Name}. + * + * @param targetName Name of the person to delete. + */ + public DeleteCommand(Name targetName) { + this.targetIndex = null; + this.targetName = targetName; + this.policyIndex = null; + } + + /** + * Creates a DeleteCommand to delete the policy at the specified {@code policyIndex} + * from the person at the specified {@code targetIndex}. + * + * @param targetIndex Index of the person. + * @param policyIndex Index of the policy to delete. + */ + public DeleteCommand(Index targetIndex, Index policyIndex) { + this.targetIndex = targetIndex; + this.targetName = null; + this.policyIndex = policyIndex; } + /** + * Executes the delete command. + * + * @param model The model in which the command is executed. + * @return The result of the command execution. + * @throws CommandException If an error occurs during command execution. + */ @Override public CommandResult execute(Model model) throws CommandException { requireNonNull(model); List lastShownList = model.getFilteredPersonList(); + if (targetIndex != null) { + return executeByIndex(model, lastShownList); + } else { + return executeByName(model, lastShownList); + } + } + + /** + * Executes the delete command. + * + * @param model The model in which the command is executed. + * @return The result of the command execution. + * @throws CommandException If an error occurs during command execution. + */ + private CommandResult executeByIndex(Model model, List lastShownList) throws CommandException { if (targetIndex.getZeroBased() >= lastShownList.size()) { + logger.log(Level.WARNING, "Invalid person index: " + targetIndex.getOneBased()); throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); } - Person personToDelete = lastShownList.get(targetIndex.getZeroBased()); + + if (policyIndex != null) { + return deletePolicyFromPerson(personToDelete); + } else { + return deletePerson(model, personToDelete); + } + } + + /** + * Executes the delete command by name. + * + * @param model The model in which the command is executed. + * @param lastShownList The list of persons currently displayed. + * @return The result of the command execution. + * @throws CommandException If an error occurs during command execution. + */ + private CommandResult executeByName(Model model, List lastShownList) throws CommandException { + List personsWithName = lastShownList.stream() + .filter(person -> person.getName().equals(targetName)) + .collect(Collectors.toList()); + + if (personsWithName.isEmpty()) { + logger.log(Level.WARNING, "No person found with name: " + targetName); + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_NAME); + } else if (personsWithName.size() == 1) { + return deletePerson(model, personsWithName.get(0)); + } else { + return handleDuplicateNames(model, personsWithName); + } + } + + /** + * Deletes a policy from a person. + * + * @param personToDelete The person from whom the policy is to be deleted. + * @return The result of the command execution. + * @throws CommandException If an error occurs during command execution. + */ + private CommandResult deletePolicyFromPerson(Person personToDelete) throws CommandException { + if (policyIndex.getZeroBased() >= personToDelete.getPolicies().size()) { + throw new CommandException(Messages.MESSAGE_INVALID_POLICY_DISPLAYED_INDEX); + } + Policy policyToDelete = personToDelete.getPolicies().get(policyIndex.getZeroBased()); + personToDelete.removePolicy(policyToDelete); + return new CommandResult(String.format(MESSAGE_DELETE_POLICY_SUCCESS, policyIndex + .getOneBased(), personToDelete.getName())); + } + + /** + * Deletes a person from the model. + * + * @param model The model in which the command is executed. + * @param personToDelete The person to be deleted. + * @return The result of the command execution. + * @throws CommandException If an error occurs during command execution. + */ + private CommandResult deletePerson(Model model, Person personToDelete) throws CommandException { + boolean isConfirmed = MainWindow.showConfirmationDialog("Are you sure you want to delete " + + personToDelete.getName() + "?"); + if (!isConfirmed) { + return new CommandResult("Deletion cancelled."); + } model.deletePerson(personToDelete); - return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, Messages.format(personToDelete))); + model.commitAddressBook(); + return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, personToDelete)); + } + + /** + * Handles the case where multiple persons with the same name are found. + * + * @param model The model in which the command is executed. + * @param personsWithName The list of persons with the same name. + * @return The result of the command execution. + */ + private CommandResult handleDuplicateNames(Model model, List personsWithName) { + model.updateFilteredPersonList(person -> person.getName().equals(targetName)); + StringBuilder duplicatesList = new StringBuilder(); + for (int i = 0; i < personsWithName.size(); i++) { + duplicatesList.append(i + 1).append(". ").append(personsWithName.get(i).getName()).append("\n"); + } + String message = String.format(MESSAGE_DUPLICATE_NAMES, targetName, duplicatesList.toString()); + return new CommandResult(message); } @Override @@ -50,20 +194,27 @@ public boolean equals(Object other) { if (other == this) { return true; } - - // instanceof handles nulls if (!(other instanceof DeleteCommand)) { return false; } - - DeleteCommand otherDeleteCommand = (DeleteCommand) other; - return targetIndex.equals(otherDeleteCommand.targetIndex); + DeleteCommand otherCommand = (DeleteCommand) other; + return (targetIndex != null ? targetIndex.equals(otherCommand.targetIndex) : otherCommand.targetIndex == null) + && (policyIndex != null ? policyIndex.equals(otherCommand.policyIndex) + : otherCommand.policyIndex == null) + && (targetName != null ? targetName.equals(otherCommand.targetName) : otherCommand.targetName == null); } @Override public String toString() { - return new ToStringBuilder(this) - .add("targetIndex", targetIndex) - .toString(); + if (targetIndex != null) { + if (policyIndex != null) { + return getClass().getCanonicalName() + "{targetIndex=" + + targetIndex.getOneBased() + ", policyIndex=" + policyIndex.getOneBased() + "}"; + } else { + return getClass().getCanonicalName() + "{targetIndex=" + targetIndex.getOneBased() + "}"; + } + } else { + return getClass().getCanonicalName() + "{targetName=" + targetName + "}"; + } } } diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java index 4b581c7331e..7b982b08f32 100644 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ b/src/main/java/seedu/address/logic/commands/EditCommand.java @@ -4,13 +4,22 @@ import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NEXT_PAYMENT_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PAYMENT_AMOUNT; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_POLICY; +import static seedu.address.logic.parser.CliSyntax.PREFIX_POLICY_END_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_POLICY_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_POLICY_START_DATE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; +import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; @@ -22,10 +31,13 @@ import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; import seedu.address.model.person.Address; +import seedu.address.model.person.Appointment; +import seedu.address.model.person.Birthday; import seedu.address.model.person.Email; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.Policy; import seedu.address.model.tag.Tag; /** @@ -35,21 +47,30 @@ public class EditCommand extends Command { public static final String COMMAND_WORD = "edit"; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the details of the person identified " - + "by the index number used in the displayed person list. " - + "Existing values will be overwritten by the input values.\n" - + "Parameters: INDEX (must be a positive integer) " + public static final String MESSAGE_PARAMETERS_EXAMPLE = "Parameters: INDEX (must be a positive integer) " + "[" + PREFIX_NAME + "NAME] " + "[" + PREFIX_PHONE + "PHONE] " + "[" + PREFIX_EMAIL + "EMAIL] " + "[" + PREFIX_ADDRESS + "ADDRESS] " - + "[" + PREFIX_TAG + "TAG]...\n" + + "[" + PREFIX_TAG + "TAG]\n" + + "[" + PREFIX_POLICY + "POLICY_INDEX " + + PREFIX_POLICY_NAME + "POLICY_NAME " + + PREFIX_POLICY_START_DATE + "START_DATE " + + PREFIX_POLICY_END_DATE + "END_DATE " + + PREFIX_NEXT_PAYMENT_DATE + "PAY_DATE " + + PREFIX_PAYMENT_AMOUNT + "AMOUNT]...\n" + "Example: " + COMMAND_WORD + " 1 " + PREFIX_PHONE + "91234567 " + PREFIX_EMAIL + "johndoe@example.com"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the details of the person identified " + + "by the index number used in the displayed person list.\n" + + "Existing values will be overwritten by the input values.\n" + + MESSAGE_PARAMETERS_EXAMPLE; + public static final String MESSAGE_EDIT_PERSON_SUCCESS = "Edited Person: %1$s"; - public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; + public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided.\n" + + MESSAGE_PARAMETERS_EXAMPLE; public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book."; private final Index index; @@ -84,6 +105,7 @@ public CommandResult execute(Model model) throws CommandException { } model.setPerson(personToEdit, editedPerson); + model.commitAddressBook(); model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(editedPerson))); } @@ -92,7 +114,8 @@ public CommandResult execute(Model model) throws CommandException { * Creates and returns a {@code Person} with the details of {@code personToEdit} * edited with {@code editPersonDescriptor}. */ - private static Person createEditedPerson(Person personToEdit, EditPersonDescriptor editPersonDescriptor) { + private static Person createEditedPerson(Person personToEdit, EditPersonDescriptor editPersonDescriptor) + throws CommandException { assert personToEdit != null; Name updatedName = editPersonDescriptor.getName().orElse(personToEdit.getName()); @@ -100,8 +123,81 @@ private static Person createEditedPerson(Person personToEdit, EditPersonDescript Email updatedEmail = editPersonDescriptor.getEmail().orElse(personToEdit.getEmail()); Address updatedAddress = editPersonDescriptor.getAddress().orElse(personToEdit.getAddress()); Set updatedTags = editPersonDescriptor.getTags().orElse(personToEdit.getTags()); + //placeholder + Appointment appointment = editPersonDescriptor.getAppointment().orElse(personToEdit.getAppointment()); + Birthday birthday = editPersonDescriptor.getBirthday().orElse(personToEdit.getBirthday()); + List policies = editPolicies(personToEdit, + editPersonDescriptor.getPolicies().orElse(Collections.emptyMap())); + + Person newPerson = new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags, + appointment, birthday); + + newPerson.setPolicies(policies); + + return newPerson; + } + + /** + * Edits the policies of a given person based on a map of new policies and their corresponding indexes. + * + * @param personToEdit The person whose policies are to be edited. + * @param newPolicies A map where the key is the {@code Index} of the policy to be replaced, and the value is the + * new {@code Policy}. + * @return A list of updated policies after applying the changes specified in the {@code newPolicies} map. + * @throws CommandException If any provided index in the {@code newPolicies} map is out of bounds, indicating that + * the specified policy does not exist. + */ + public static List editPolicies(Person personToEdit, Map newPolicies) + throws CommandException { + List oldPolicies = personToEdit.getPolicies(); + + List updatedPolicies = new ArrayList(oldPolicies); + int policiesSize = updatedPolicies.size(); + + for (Map.Entry entry : newPolicies.entrySet()) { + Index index = entry.getKey(); + Policy policy = entry.getValue(); + int zeroBasedIndex = index.getZeroBased(); + + if (zeroBasedIndex >= policiesSize) { + throw new CommandException(Messages.MESSAGE_INVALID_POLICY_DISPLAYED_INDEX); + } + + int duplicatePolicyIndex = getDuplicatePolicyIndex(policy, updatedPolicies); + boolean isPolicyToEdit = (duplicatePolicyIndex == zeroBasedIndex); + boolean isNotDuplicatePolicy = (duplicatePolicyIndex == -1); + + if (!isNotDuplicatePolicy && !isPolicyToEdit) { + throw new CommandException(Messages.MESSAGE_DUPLICATE_POLICY_NAME); + } + + updatedPolicies.set(zeroBasedIndex, policy); + } + + return updatedPolicies; + } - return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags); + /** + * Checks if a given policy already exists in the provided list of policies. If a policy with the same name is + * found, the method returns the index of the matching policy in the list. If no matching policy is found, it + * returns {@code -1}. + * + * @param policy the {@link Policy} object to check for duplication + * @param policyList the list of {@link Policy} objects to compare against + * @return the index of the duplicate policy in the list, or {@code -1} if + * no duplicate is found + */ + public static Integer getDuplicatePolicyIndex(Policy policy, List policyList) { + String policyName = policy.getPolicyName(); + int size = policyList.size(); + + for (int i = 0; i < size; i++) { + if (policyName.equals(policyList.get(i).getPolicyName())) { + return i; + } + } + + return -1; } @Override @@ -138,6 +234,10 @@ public static class EditPersonDescriptor { private Email email; private Address address; private Set tags; + private Birthday birthday; + private Appointment appointment; + + private Map policies; public EditPersonDescriptor() {} @@ -151,13 +251,16 @@ public EditPersonDescriptor(EditPersonDescriptor toCopy) { setEmail(toCopy.email); setAddress(toCopy.address); setTags(toCopy.tags); + setAppointment(toCopy.appointment); + setBirthday(toCopy.birthday); + setPolicies(toCopy.policies); } /** * Returns true if at least one field is edited. */ public boolean isAnyFieldEdited() { - return CollectionUtil.isAnyNonNull(name, phone, email, address, tags); + return CollectionUtil.isAnyNonNull(name, phone, email, address, tags, birthday, appointment, policies); } public void setName(Name name) { @@ -192,6 +295,21 @@ public Optional
getAddress() { return Optional.ofNullable(address); } + public void setBirthday(Birthday birthday) { + this.birthday = birthday; + } + public Optional getBirthday() { + return Optional.ofNullable(birthday); + } + + public void setAppointment(Appointment appointment) { + this.appointment = appointment; + } + + public Optional getAppointment() { + return Optional.ofNullable(appointment); + } + /** * Sets {@code tags} to this object's {@code tags}. * A defensive copy of {@code tags} is used internally. @@ -209,6 +327,23 @@ public Optional> getTags() { return (tags != null) ? Optional.of(Collections.unmodifiableSet(tags)) : Optional.empty(); } + /** + * Sets {@code policies} to this object's {@code policies}. + * A defensive copy of {@code policies} is used internally. + */ + public void setPolicies(Map policies) { + this.policies = (policies != null) ? new HashMap(policies) : null; + } + + /** + * Returns an unmodifiable policy list, which throws {@code UnsupportedOperationException} + * if modification is attempted. + * Returns {@code Optional#empty()} if {@code policies} is null. + */ + public Optional> getPolicies() { + return (policies != null) ? Optional.of(Collections.unmodifiableMap(policies)) : Optional.empty(); + } + @Override public boolean equals(Object other) { if (other == this) { @@ -225,7 +360,10 @@ public boolean equals(Object other) { && Objects.equals(phone, otherEditPersonDescriptor.phone) && Objects.equals(email, otherEditPersonDescriptor.email) && Objects.equals(address, otherEditPersonDescriptor.address) - && Objects.equals(tags, otherEditPersonDescriptor.tags); + && Objects.equals(tags, otherEditPersonDescriptor.tags) + && Objects.equals(birthday, otherEditPersonDescriptor.birthday) + && Objects.equals(appointment, otherEditPersonDescriptor.appointment) + && Objects.equals(policies, otherEditPersonDescriptor.policies); } @Override @@ -236,6 +374,9 @@ public String toString() { .add("email", email) .add("address", address) .add("tags", tags) + .add("birthday", birthday) + .add("appointment", appointment) + .add("policies", policies) .toString(); } } diff --git a/src/main/java/seedu/address/logic/commands/PaidCommand.java b/src/main/java/seedu/address/logic/commands/PaidCommand.java new file mode 100644 index 00000000000..b45b9a6c632 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/PaidCommand.java @@ -0,0 +1,104 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_POLICY; + +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.model.person.Policy; + +/** + * Marks a policy as paid and updates the next payment date. + */ +public class PaidCommand extends Command { + public static final String COMMAND_WORD = "paid"; + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Marks the policy identified by the policy index as paid and updates the next payment date.\n" + + "Parameters: INDEX (must be a positive integer) " + PREFIX_POLICY + + "POLICY_INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1 " + PREFIX_POLICY + "1"; + + public static final String MESSAGE_SUCCESS = "Policy %1$s for %2$s is marked as paid. Next payment date updated."; + public static final String MESSAGE_FULLY_PAID = "The policy %1$s for %2$s will be fully paid after this payment."; + public static final String MESSAGE_INVALID_POLICY = "The policy associated with index %1$s does not exist for " + + "%2$s."; + public static final String MESSAGE_INVALID_PAYDATE = "The policy %1$s for %2$s is fully paid."; + + private final Index targetIndex; + private final Index policyIndex; + + /** + * Creates a PaidCommand to mark the specified policy as paid. + */ + public PaidCommand(Index targetIndex, Index policyIndex) { + requireNonNull(targetIndex); + requireNonNull(policyIndex); + this.targetIndex = targetIndex; + this.policyIndex = policyIndex; + } + + @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 personToUpdate = lastShownList.get(targetIndex.getZeroBased()); + + if (policyIndex.getZeroBased() >= personToUpdate.getPolicies().size()) { + throw new CommandException(String.format(MESSAGE_INVALID_POLICY, policyIndex.getOneBased(), + personToUpdate.getName())); + } + + Policy policyToUpdate = personToUpdate.getPolicies().get(policyIndex.getZeroBased()); + + if (policyToUpdate.isFullyPaid()) { + throw new CommandException(String.format(MESSAGE_INVALID_PAYDATE, policyToUpdate.getPolicyName(), + personToUpdate.getName())); + } + + if (policyToUpdate.isExpiringSoon()) { + policyToUpdate.updateNextPaymentDate(); + return new CommandResult(String.format(MESSAGE_FULLY_PAID, policyToUpdate.getPolicyName(), + personToUpdate.getName())); + } + + policyToUpdate.updateNextPaymentDate(); + + return new CommandResult(String.format(MESSAGE_SUCCESS, policyToUpdate.getPolicyName(), + personToUpdate.getName())); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof PaidCommand)) { + return false; + } + + PaidCommand otherPaidCommand = (PaidCommand) other; + return targetIndex.equals(otherPaidCommand.targetIndex) + && policyIndex.equals(otherPaidCommand.policyIndex); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("targetIndex", targetIndex) + .add("policyIndex", policyIndex) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/RedoCommand.java b/src/main/java/seedu/address/logic/commands/RedoCommand.java new file mode 100644 index 00000000000..54a377ceb15 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/RedoCommand.java @@ -0,0 +1,31 @@ +package seedu.address.logic.commands; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + +/** + * Redoes the previously undone command. + */ +public class RedoCommand extends Command { + public static final String COMMAND_WORD = "redo"; + public static final String MESSAGE_SUCCESS = "Redo successful!"; + public static final String MESSAGE_FAILURE = "No commands to redo!\n" + + "Redo command works for after an undo command!"; + + /** + * Executes the redo command. + * + * @param model The model in which the command should operate. + * @return The result of the command execution. + * @throws CommandException If there are no commands to redo. + */ + @Override + public CommandResult execute(Model model) throws CommandException { + if (!model.canRedoAddressBook()) { + throw new CommandException(MESSAGE_FAILURE); + } + + model.redoAddressBook(); + return new CommandResult(MESSAGE_SUCCESS); + } +} diff --git a/src/main/java/seedu/address/logic/commands/SearchAppointmentCommand.java b/src/main/java/seedu/address/logic/commands/SearchAppointmentCommand.java new file mode 100644 index 00000000000..7dd5e696167 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/SearchAppointmentCommand.java @@ -0,0 +1,140 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_DATETIMERANGE_FORMAT; +import static seedu.address.logic.Messages.MESSAGE_INVALID_DATETIME_FORMAT; +import static seedu.address.logic.Messages.MESSAGE_START_DATETIME_AFTER_END_DATETIME; +import static seedu.address.logic.Messages.MESSAGE_SUCCESS_SEARCH_APPOINTMENT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SEARCH_APPOINTMENT; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.function.Predicate; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Appointment; +import seedu.address.model.person.Person; + +/** + * Searches for clients who have appointments on the specified date and time. + */ +public class SearchAppointmentCommand extends Command { + public static final String COMMAND_WORD = "search " + PREFIX_SEARCH_APPOINTMENT; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Searches for clients who have appointments on the specified date " + + "and time or within a specified date time range.\n" + + "Parameters: DATE TIME (must be in yyyy-MM-dd HH:mm format)\n" + + "Example: " + COMMAND_WORD + " 2023-12-31 14:30\n" + + "Example: " + COMMAND_WORD + "2023-12-31 14:30 to 2024-01-01 16:00"; + + private final String startDateTime; + private final String endDateTime; + + /** + * Creates a {@code SearchAppointmentCommand} to search for clients with appointments on the specified dateTime. + * + * @param dateTimeInput The date and time in string format used to search for client appointments. + * @throws CommandException if the {@code dateTime} format is invalid. + */ + public SearchAppointmentCommand(String dateTimeInput) throws CommandException { + requireNonNull(dateTimeInput); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + + if (dateTimeInput.contains("to")) { + String[] dateTimes = dateTimeInput.split("to"); + if (dateTimes.length != 2) { + throw new CommandException(MESSAGE_INVALID_DATETIMERANGE_FORMAT); + } + startDateTime = dateTimes[0].trim(); + endDateTime = dateTimes[1].trim(); + + if (!isValidDateTime(startDateTime) || !isValidDateTime(endDateTime)) { + throw new CommandException(MESSAGE_INVALID_DATETIME_FORMAT); + } + + LocalDateTime start = LocalDateTime.parse(startDateTime, formatter); + LocalDateTime end = LocalDateTime.parse(endDateTime, formatter); + + if (start.isAfter(end)) { + throw new CommandException(MESSAGE_START_DATETIME_AFTER_END_DATETIME); + } + + } else { + String dateTimeStr = dateTimeInput.trim(); + if (!isValidDateTime(dateTimeStr)) { + throw new CommandException(MESSAGE_INVALID_DATETIME_FORMAT); + } + startDateTime = dateTimeInput; + endDateTime = startDateTime; + } + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + Predicate predicate = person -> { + Appointment appointment = person.getAppointment(); + + if (appointment == null || appointment.value.isEmpty()) { + return false; + } + String appointmentFormatted = appointment.toString(); + LocalDateTime personAppointment; + try { + personAppointment = LocalDateTime.parse(appointmentFormatted, formatter); + } catch (DateTimeParseException e) { + return false; + } + LocalDateTime start = LocalDateTime.parse(startDateTime, formatter); + LocalDateTime end = LocalDateTime.parse(endDateTime, formatter); + return (!personAppointment.isBefore(start) && !personAppointment.isAfter(end)); + }; + model.updateFilteredPersonList(predicate); + + String dateTimeRangeMessage; + if (startDateTime.equals(endDateTime)) { + dateTimeRangeMessage = "on " + startDateTime; + } else { + dateTimeRangeMessage = "from " + startDateTime + " to " + endDateTime; + } + return new CommandResult(String.format(MESSAGE_SUCCESS_SEARCH_APPOINTMENT, dateTimeRangeMessage)); + } + + /** + * Checks if the given dateTime string is in the yyyy-MM-dd HH:mm format. + * + * @param dateTime The dateTime string to check. + * @return True if the dateTime is in yyyy-MM-dd HH:mm format, false otherwise. + */ + private boolean isValidDateTime(String dateTime) { + try { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + LocalDateTime.parse(dateTime, formatter); + return true; + } catch (DateTimeParseException e) { + return false; + } + } + + @Override + public boolean equals(Object other) { + // Short circuit if the same object + if (other == this) { + return true; + } + + // Instance of handles nulls and type check + if (!(other instanceof SearchAppointmentCommand)) { + return false; + } + + // Cast and compare the dateTime attribute + SearchAppointmentCommand otherCommand = (SearchAppointmentCommand) other; + return this.startDateTime.equals(otherCommand.startDateTime) + && this.endDateTime.equals(otherCommand.endDateTime); + } +} diff --git a/src/main/java/seedu/address/logic/commands/SearchBirthdayCommand.java b/src/main/java/seedu/address/logic/commands/SearchBirthdayCommand.java new file mode 100644 index 00000000000..3a39be8f4d9 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/SearchBirthdayCommand.java @@ -0,0 +1,129 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_DATERANGE_FORMAT; +import static seedu.address.logic.Messages.MESSAGE_INVALID_DATE_FORMAT; +import static seedu.address.logic.Messages.MESSAGE_START_DATE_AFTER_END_DATE; +import static seedu.address.logic.Messages.MESSAGE_SUCCESS_SEARCH_BIRTHDAY; +import static seedu.address.logic.parser.CliSyntax.PREFIX_BIRTHDAY; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.function.Predicate; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Birthday; +import seedu.address.model.person.Person; + +/** + * Searches for clients whose birthdays are on the specified date. + */ +public class SearchBirthdayCommand extends Command { + public static final String COMMAND_WORD = "search " + PREFIX_BIRTHDAY; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Searches for clients whose birthdays are on the specified date or a specified date range.\n" + + "Parameters: DATE (must be in yyyy-MM-dd format)\n" + + "Example: " + COMMAND_WORD + " 2000-04-25\n" + + "Example: " + COMMAND_WORD + " 2000-04-25 to 2000-05-25"; + + private final String startDate; + private final String endDate; + + /** + * Creates a {@code SearchBirthdayCommand} to search for clients with the specified birthday {@code date}. + * + * @param dateInput The date in string format used to search for client birthdays. + * @throws CommandException if the {@code date} format is invalid. + */ + public SearchBirthdayCommand(String dateInput) throws CommandException { + requireNonNull(dateInput); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + // checks if the date input contains to (means it is a range) + if (dateInput.contains("to")) { + String [] dates = dateInput.split("to"); + // ensure dates contains 2 date + if (dates.length != 2) { + throw new CommandException(MESSAGE_INVALID_DATERANGE_FORMAT); + } + startDate = dates[0].trim(); + endDate = dates[1].trim(); + + if (!isValidDate(startDate) || !isValidDate(endDate)) { + throw new CommandException(MESSAGE_INVALID_DATE_FORMAT); + } + + if (LocalDate.parse(startDate, formatter).isAfter(LocalDate.parse(endDate, formatter))) { + throw new CommandException(MESSAGE_START_DATE_AFTER_END_DATE); + } + } else { + String date = dateInput.trim(); + if (!isValidDate(date)) { + throw new CommandException(MESSAGE_INVALID_DATE_FORMAT); + } + startDate = date; + endDate = startDate; + } + + } + + @Override + public CommandResult execute(Model model) throws CommandException { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + requireNonNull(model); + Predicate predicate = person -> { + Birthday birthday = person.getBirthday(); + + if (birthday == null || birthday.value.isEmpty()) { + return false; + } + String birthdayFormatted = birthday.toString(); + LocalDate personBirthday = LocalDate.parse(birthdayFormatted, formatter); + LocalDate start = LocalDate.parse(startDate, formatter); + LocalDate end = LocalDate.parse(endDate, formatter); + return (!personBirthday.isBefore(start) && !personBirthday.isAfter(end)); + }; + + model.updateFilteredPersonList(predicate); + String dateRangeMessage; + if (startDate.equals(endDate)) { + dateRangeMessage = "on " + startDate; + } else { + dateRangeMessage = "from " + startDate + " to " + endDate; + } + + return new CommandResult(String.format(MESSAGE_SUCCESS_SEARCH_BIRTHDAY, dateRangeMessage)); + } + + /** + * Checks if the given date string is in the yyyy-MM-dd format. + * + * @param date The date string to check. + * @return True if the date is in yyyy-MM-dd format, false otherwise. + */ + private boolean isValidDate(String date) { + try { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + LocalDate.parse(date, formatter); + return true; + } catch (DateTimeParseException e) { + return false; + } + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof SearchBirthdayCommand)) { + return false; + } + + SearchBirthdayCommand otherCommand = (SearchBirthdayCommand) other; + return this.startDate.equals(otherCommand.startDate) && this.endDate.equals(otherCommand.endDate); + } +} diff --git a/src/main/java/seedu/address/logic/commands/SearchPolicyCommand.java b/src/main/java/seedu/address/logic/commands/SearchPolicyCommand.java new file mode 100644 index 00000000000..5d0be0bf939 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/SearchPolicyCommand.java @@ -0,0 +1,100 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_POLICY_FORMAT; +import static seedu.address.logic.Messages.MESSAGE_SUCCESS_SEARCH_POLICY; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SEARCH_POLICY; + +import java.util.function.Predicate; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Person; + +/** + * Searches for clients who have a specified policy. + */ +public class SearchPolicyCommand extends Command { + public static final String COMMAND_WORD = "search " + PREFIX_SEARCH_POLICY; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Searches for clients who have the specified policy.\n" + + "Parameters: POLICY_NAME\n" + + "Example: " + COMMAND_WORD + " Health Insurance"; + + private final String policyName; + + /** + * Creates a {@code SearchPolicyCommand} to search for clients with the specified policy name. + * + * @param policyName The policy name used to search for clients. + * @throws CommandException if the {@code policyName} is invalid. + */ + public SearchPolicyCommand(String policyName) throws CommandException { + requireNonNull(policyName); + if (!isValidPolicyName(policyName)) { + throw new CommandException(MESSAGE_INVALID_POLICY_FORMAT); + } + this.policyName = policyName; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + Predicate predicate = person -> personHasPolicy(person, policyName); + model.updateFilteredPersonList(predicate); + return new CommandResult(String.format(MESSAGE_SUCCESS_SEARCH_POLICY, policyName)); + } + + /** + * Normalizes a string by removing all whitespace and converting it to lowercase. + * + * @param input The string to normalize. + * @return The normalized string. + */ + private String normalizeString(String input) { + return input.replaceAll("\\s+", "").toLowerCase(); + } + + /** + * Checks if the given person has the specified policy. + * + * @param person The person to check. + * @param policyName The name of the policy. + * @return True if the person has the policy, false otherwise. + */ + private boolean personHasPolicy(Person person, String policyName) { + String normalizedPolicyName = normalizeString(policyName); + return person.getPolicies().stream() + .anyMatch(policy -> normalizeString(policy.getPolicyName()).equalsIgnoreCase(normalizedPolicyName)); + } + + /** + * Validates the policy name. + * + * @param policyName The policy name to validate. + * @return True if the policy name is valid, false otherwise. + */ + private boolean isValidPolicyName(String policyName) { + if (policyName == null || policyName.trim().isEmpty()) { + return false; + } + + String regex = "^[a-zA-Z0-9 ]+$"; + return policyName.matches(regex); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof SearchPolicyCommand)) { + return false; + } + + SearchPolicyCommand otherCommand = (SearchPolicyCommand) other; + return this.policyName.equalsIgnoreCase(otherCommand.policyName); + } +} diff --git a/src/main/java/seedu/address/logic/commands/SortCommand.java b/src/main/java/seedu/address/logic/commands/SortCommand.java new file mode 100644 index 00000000000..26125e0e32d --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/SortCommand.java @@ -0,0 +1,146 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_APPOINTMENT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_BIRTHDAY; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NEXT_PAYMENT_DATE; + +import java.util.Comparator; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Person; + +/** + * Sorts the contact list based on the specified parameter and order. + */ +public class SortCommand extends Command { + + public static final String COMMAND_WORD = "sort"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Sorts the contact list by the specified parameter in the specified order.\n" + + "Parameters: CRITERIA (" + PREFIX_NAME + ", " + PREFIX_APPOINTMENT + ", " + PREFIX_BIRTHDAY + " or " + + PREFIX_NEXT_PAYMENT_DATE + ") " + "ORDER (asc, desc)\n" + + "Example: " + COMMAND_WORD + " " + PREFIX_NAME + " asc"; + + public static final String MESSAGE_SUCCESS = "Contacts have been sorted by %1$s in %2$s order."; + public static final String MESSAGE_INVALID_PARAMETER = "Invalid parameter. Use one of the following: " + + PREFIX_NAME + ", " + PREFIX_APPOINTMENT + ", " + PREFIX_BIRTHDAY + ", or " + PREFIX_NEXT_PAYMENT_DATE + "."; + public static final String MESSAGE_INVALID_ORDER = "Invalid order. Use `asc` for ascending or " + + "`desc` for descending."; + public static final String MESSAGE_INVALID_SORT_COMMAND = "Cannot sort by next payment date in descending order."; + public static final String MESSAGE_EMPTY_PERSON_LIST = "There are no contacts to sort."; + + private final String parameter; + private final String order; + + /** + * Creates a SortCommand to sort the contact list by the specified parameter and order. + */ + public SortCommand(String parameter, String order) { + requireNonNull(parameter); + requireNonNull(order); + this.parameter = parameter; + this.order = order; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + if (model.getFilteredPersonList().isEmpty()) { + throw new CommandException(MESSAGE_EMPTY_PERSON_LIST); + } + + Comparator comparator; + switch (parameter) { + case "n/": + comparator = Person.getNameComparator(); + break; + case "appt/": + comparator = Person.getAppointmentDateComparator(); + break; + case "b/": + comparator = Person.getBirthdayComparator(); + break; + case "paydate/": + comparator = Person.getPayDateComparator(); + break; + default: + throw new CommandException(MESSAGE_INVALID_PARAMETER); + } + + if (parameter.equals("paydate/") && order.equals("desc")) { + throw new CommandException(MESSAGE_INVALID_SORT_COMMAND); + } + + if (order.equals("desc")) { + comparator = comparator.reversed(); + } else if (!order.equals("asc")) { + throw new CommandException(MESSAGE_INVALID_ORDER); + } + + model.sortPersonList(comparator); + return new CommandResult(String.format(MESSAGE_SUCCESS, getExactParameter(parameter), order)); + } + + /** + * Returns the exact parameter name. + */ + private String getExactParameter(String prefix) { + switch (prefix) { + case "n/": + return "name"; + case "appt/": + return "appointment date"; + case "b/": + return "birthday"; + case "paydate/": + return "next payment date"; + default: + return prefix; + } + } + + /** + * Returns true if the parameter is a valid parameter. + */ + public static boolean isValidParameter(String parameter) { + return parameter.equals("n/") || parameter.equals("appt/") || parameter.equals("b/") + || parameter.equals("paydate/"); + } + + /** + * Returns true if the order is a valid order. + */ + public static boolean isValidOrder(String order) { + return order.equals("asc") || order.equals("desc"); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof SortCommand)) { + return false; + } + + SortCommand otherSortCommand = (SortCommand) other; + return parameter.equals(otherSortCommand.parameter) + && order.equals(otherSortCommand.order); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("parameter", parameter) + .add("order", order) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/UndoCommand.java b/src/main/java/seedu/address/logic/commands/UndoCommand.java new file mode 100644 index 00000000000..7a4a397f257 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/UndoCommand.java @@ -0,0 +1,31 @@ +package seedu.address.logic.commands; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + +/** + * Undoes the previous command. + */ +public class UndoCommand extends Command { + public static final String COMMAND_WORD = "undo"; + public static final String MESSAGE_SUCCESS = "Undo successful!"; + public static final String MESSAGE_FAILURE = "No commands to undo!\n" + + "Undo command works for add, delete(Person only), edit, clear commands"; + + /** + * Executes the undo command. + * + * @param model The model in which the command should operate. + * @return The result of the command execution. + * @throws CommandException If there are no commands to undo. + */ + @Override + public CommandResult execute(Model model) throws CommandException { + if (!model.canUndoAddressBook()) { + throw new CommandException(MESSAGE_FAILURE); + } + + model.undoAddressBook(); + return new CommandResult(MESSAGE_SUCCESS); + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddCommandParser.java index 4ff1a97ed77..8dba415510a 100644 --- a/src/main/java/seedu/address/logic/parser/AddCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/AddCommandParser.java @@ -2,6 +2,8 @@ import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_APPOINTMENT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_BIRTHDAY; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; @@ -13,12 +15,15 @@ import seedu.address.logic.commands.AddCommand; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.person.Address; +import seedu.address.model.person.Appointment; +import seedu.address.model.person.Birthday; import seedu.address.model.person.Email; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; import seedu.address.model.tag.Tag; + /** * Parses input arguments and creates a new AddCommand object */ @@ -31,21 +36,25 @@ public class AddCommandParser implements Parser { */ public AddCommand parse(String args) throws ParseException { ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, + PREFIX_TAG, PREFIX_BIRTHDAY, PREFIX_APPOINTMENT); - if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ADDRESS, PREFIX_PHONE, PREFIX_EMAIL) + if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ADDRESS, PREFIX_PHONE, PREFIX_EMAIL, + PREFIX_BIRTHDAY, PREFIX_APPOINTMENT) || !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); + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, + PREFIX_ADDRESS, PREFIX_BIRTHDAY, PREFIX_APPOINTMENT); 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()); + Birthday birthday = ParserUtil.parseBirthday(argMultimap.getValue(PREFIX_BIRTHDAY).get()); + Appointment appointment = ParserUtil.parseAppointment(argMultimap.getValue(PREFIX_APPOINTMENT).get()); Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); - - Person person = new Person(name, phone, email, address, tagList); + Person person = new Person(name, phone, email, address, tagList, appointment, birthday); return new AddCommand(person); } diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java index 3149ee07e0b..592920452e1 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -9,6 +9,7 @@ import seedu.address.commons.core.LogsCenter; import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.AssignPolicyCommand; import seedu.address.logic.commands.ClearCommand; import seedu.address.logic.commands.Command; import seedu.address.logic.commands.DeleteCommand; @@ -17,6 +18,10 @@ import seedu.address.logic.commands.FindCommand; import seedu.address.logic.commands.HelpCommand; import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.PaidCommand; +import seedu.address.logic.commands.RedoCommand; +import seedu.address.logic.commands.SortCommand; +import seedu.address.logic.commands.UndoCommand; import seedu.address.logic.parser.exceptions.ParseException; /** @@ -52,7 +57,8 @@ public Command parseCommand(String userInput) throws ParseException { logger.fine("Command word: " + commandWord + "; Arguments: " + arguments); switch (commandWord) { - + case "search": + return new SearchCommandParser().parse(arguments); case AddCommand.COMMAND_WORD: return new AddCommandParser().parse(arguments); @@ -77,6 +83,21 @@ public Command parseCommand(String userInput) throws ParseException { case HelpCommand.COMMAND_WORD: return new HelpCommand(); + case SortCommand.COMMAND_WORD: + return new SortCommandParser().parse(arguments); + + case AssignPolicyCommand.COMMAND_WORD: + return new AssignPolicyCommandParser().parse(arguments); + + case UndoCommand.COMMAND_WORD: + return new UndoCommand(); + + case PaidCommand.COMMAND_WORD: + return new PaidCommandParser().parse(arguments); + + case RedoCommand.COMMAND_WORD: + return new RedoCommand(); + 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/AssignPolicyCommandParser.java b/src/main/java/seedu/address/logic/parser/AssignPolicyCommandParser.java new file mode 100644 index 00000000000..0d5ac6187a4 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AssignPolicyCommandParser.java @@ -0,0 +1,88 @@ +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_NEXT_PAYMENT_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PAYMENT_AMOUNT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_POLICY_END_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_POLICY_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_POLICY_START_DATE; + +import java.util.stream.Stream; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.AssignPolicyCommand; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Payment; +import seedu.address.model.person.Policy; + + +/** + * Parses user input for assigning a policy to a client. + */ +public class AssignPolicyCommandParser implements Parser { + + /** + * Parses the given {@code args} and returns an {@code AssignPolicyCommand} object. + * + * @param args The arguments provided by the user for the command. + * @return An {@code AssignPolicyCommand} object containing the parsed index and policy. + * @throws ParseException If the user input is not in the expected format, or if there are + * duplicate prefixes or issues with parsing the index or policy. + */ + @Override + public AssignPolicyCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_POLICY_NAME, PREFIX_POLICY_START_DATE, + PREFIX_POLICY_END_DATE, PREFIX_NEXT_PAYMENT_DATE, + PREFIX_PAYMENT_AMOUNT); + if (!arePrefixesPresent(argMultimap, PREFIX_POLICY_NAME, PREFIX_POLICY_START_DATE, + PREFIX_POLICY_END_DATE, PREFIX_NEXT_PAYMENT_DATE, PREFIX_PAYMENT_AMOUNT)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + AssignPolicyCommand.MESSAGE_USAGE)); + } + Index index; + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + AssignPolicyCommand.MESSAGE_USAGE)); + } + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_POLICY_NAME, PREFIX_POLICY_START_DATE, + PREFIX_POLICY_END_DATE, PREFIX_NEXT_PAYMENT_DATE, + PREFIX_PAYMENT_AMOUNT); + + String nameString = ParserUtil.parseName(argMultimap.getValue(PREFIX_POLICY_NAME).get()).toString(); + String startDateString = argMultimap.getValue(PREFIX_POLICY_START_DATE).get(); + String endDateString = argMultimap.getValue(PREFIX_POLICY_END_DATE).get(); + String paydateString = argMultimap.getValue(PREFIX_NEXT_PAYMENT_DATE).get(); + String paymentAmount = argMultimap.getValue(PREFIX_PAYMENT_AMOUNT).get(); + String insurancePayment = paydateString + " " + paymentAmount; + return checkAssignPolicyCommand(index, nameString, startDateString, endDateString, insurancePayment); + } + + private AssignPolicyCommand checkAssignPolicyCommand(Index index, String nameString, + String startDateString, String endDateString, + String insurancePayment) throws ParseException { + if (!Payment.isValidInsurancePayment(insurancePayment)) { + throw new ParseException(Payment.MESSAGE_CONSTRAINTS); + } + try { + Policy policy = new Policy(nameString, startDateString, endDateString, insurancePayment); + return new AssignPolicyCommand(index, policy); + } catch (CommandException e) { + throw new ParseException(e.getMessage()); + } + } + + /** + * 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()); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java index 75b1a9bf119..560b3b230b1 100644 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ b/src/main/java/seedu/address/logic/parser/CliSyntax.java @@ -9,7 +9,21 @@ public class CliSyntax { public static final Prefix PREFIX_NAME = new Prefix("n/"); public static final Prefix PREFIX_PHONE = new Prefix("p/"); public static final Prefix PREFIX_EMAIL = new Prefix("e/"); - public static final Prefix PREFIX_ADDRESS = new Prefix("a/"); + public static final Prefix PREFIX_ADDRESS = new Prefix("addr/"); public static final Prefix PREFIX_TAG = new Prefix("t/"); + public static final Prefix PREFIX_BIRTHDAY = new Prefix("b/"); + public static final Prefix PREFIX_SEARCH_APPOINTMENT = new Prefix("a/"); + public static final Prefix PREFIX_SEARCH_POLICY = new Prefix("p/"); + + public static final Prefix PREFIX_APPOINTMENT = new Prefix("appt/"); + public static final Prefix PREFIX_POLICY = new Prefix("po/"); + public static final Prefix PREFIX_POLICY_NAME = new Prefix("pon/"); + public static final Prefix PREFIX_POLICY_START_DATE = new Prefix("pos/"); + public static final Prefix PREFIX_POLICY_END_DATE = new Prefix("poe/"); + public static final Prefix PREFIX_NEXT_PAYMENT_DATE = new Prefix("paydate/"); + public static final Prefix PREFIX_PAYMENT_AMOUNT = new Prefix("amt/"); + + + } diff --git a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java index 3527fe76a3e..0f35a4a6aa8 100644 --- a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java @@ -1,10 +1,12 @@ package seedu.address.logic.parser; import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_POLICY; import seedu.address.commons.core.index.Index; import seedu.address.logic.commands.DeleteCommand; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Name; /** * Parses input arguments and creates a new DeleteCommand object @@ -17,13 +19,27 @@ public class DeleteCommandParser implements Parser { * @throws ParseException if the user input does not conform the expected format */ public DeleteCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_POLICY); + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_POLICY); try { - Index index = ParserUtil.parseIndex(args); - return new DeleteCommand(index); + String[] splitArgs = args.trim().split("\\s+"); + Index targetIndex = ParserUtil.parseIndex(splitArgs[0]); + + if (splitArgs.length > 1 && splitArgs[1].startsWith("po/")) { + Index policyIndex = ParserUtil.parseIndex(splitArgs[1].substring(3)); + return new DeleteCommand(targetIndex, policyIndex); + } else { + return new DeleteCommand(targetIndex); + } } catch (ParseException pe) { - throw new ParseException( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE), pe); + try { + Name name = ParserUtil.parseName(args, String.format(MESSAGE_INVALID_COMMAND_FORMAT, + DeleteCommand.MESSAGE_USAGE)); + return new DeleteCommand(name); + } catch (ParseException pe2) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE), pe2); + } } } - } diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/seedu/address/logic/parser/EditCommandParser.java index 46b3309a78b..accb985a2f0 100644 --- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/EditCommandParser.java @@ -3,13 +3,17 @@ import static java.util.Objects.requireNonNull; import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_APPOINTMENT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_BIRTHDAY; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_POLICY; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; import java.util.Collection; import java.util.Collections; +import java.util.Map; import java.util.Optional; import java.util.Set; @@ -17,6 +21,7 @@ import seedu.address.logic.commands.EditCommand; import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Policy; import seedu.address.model.tag.Tag; /** @@ -32,7 +37,8 @@ public class EditCommandParser implements Parser { public EditCommand parse(String args) throws ParseException { requireNonNull(args); ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, + PREFIX_BIRTHDAY, PREFIX_APPOINTMENT, PREFIX_TAG, PREFIX_POLICY); Index index; @@ -42,7 +48,8 @@ public EditCommand parse(String args) throws ParseException { throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE), pe); } - argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS); + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, + PREFIX_BIRTHDAY, PREFIX_ADDRESS); EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor(); @@ -58,7 +65,15 @@ public EditCommand parse(String args) throws ParseException { if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { editPersonDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); } + if (argMultimap.getValue(PREFIX_BIRTHDAY).isPresent()) { + editPersonDescriptor.setBirthday(ParserUtil.parseBirthday(argMultimap.getValue(PREFIX_BIRTHDAY).get())); + } + if (argMultimap.getValue(PREFIX_APPOINTMENT).isPresent()) { + editPersonDescriptor.setAppointment(ParserUtil.parseAppointment( + argMultimap.getValue(PREFIX_APPOINTMENT).get())); + } parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); + parsePoliciesForEdit(argMultimap.getAllValues(PREFIX_POLICY)).ifPresent(editPersonDescriptor::setPolicies); if (!editPersonDescriptor.isAnyFieldEdited()) { throw new ParseException(EditCommand.MESSAGE_NOT_EDITED); @@ -82,4 +97,16 @@ private Optional> parseTagsForEdit(Collection tags) throws Pars return Optional.of(ParserUtil.parseTags(tagSet)); } + /** + * Parses {@code Collection policyArgs} into a {@code List} if {@code policyArgs} is non-empty. + */ + private Optional> parsePoliciesForEdit(Collection policyArgs) throws ParseException { + assert policyArgs != null; + + if (policyArgs.isEmpty()) { + return Optional.empty(); + } + + return Optional.of(ParserUtil.parsePolicies(policyArgs)); + } } diff --git a/src/main/java/seedu/address/logic/parser/PaidCommandParser.java b/src/main/java/seedu/address/logic/parser/PaidCommandParser.java new file mode 100644 index 00000000000..7d06ade3adf --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/PaidCommandParser.java @@ -0,0 +1,33 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_POLICY; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.PaidCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new PaidCommand object. + */ +public class PaidCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the PaidCommand + * and returns a PaidCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public PaidCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_POLICY); + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_POLICY); + try { + Index personIndex = ParserUtil.parseIndex(argMultimap.getPreamble()); + Index policyIndex = ParserUtil.parseIndex(argMultimap.getValue(PREFIX_POLICY).orElseThrow(() -> + new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, PaidCommand.MESSAGE_USAGE)))); + return new PaidCommand(personIndex, policyIndex); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, PaidCommand.MESSAGE_USAGE), pe); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java index b117acb9c55..7e7b48aa71e 100644 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java @@ -1,20 +1,36 @@ package seedu.address.logic.parser; import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_DUPLICATE_POLICY_INDEX; +import static seedu.address.logic.Messages.MESSAGE_INVALID_POLICY_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NEXT_PAYMENT_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PAYMENT_AMOUNT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_POLICY_END_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_POLICY_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_POLICY_START_DATE; import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; +import java.util.Map; import java.util.Set; +import java.util.stream.Stream; import seedu.address.commons.core.index.Index; import seedu.address.commons.util.StringUtil; +import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.person.Address; +import seedu.address.model.person.Appointment; +import seedu.address.model.person.Birthday; import seedu.address.model.person.Email; import seedu.address.model.person.Name; +import seedu.address.model.person.Payment; import seedu.address.model.person.Phone; +import seedu.address.model.person.Policy; import seedu.address.model.tag.Tag; + /** * Contains utility methods used for parsing strings in the various *Parser classes. */ @@ -50,6 +66,24 @@ public static Name parseName(String name) throws ParseException { return new Name(trimmedName); } + /** + * Parses a {@code String name} into a {@code Name} with a custom error message. + * Leading and trailing whitespaces will be trimmed. + * + * @param name The name to be parsed. + * @param customErrorMessage The custom error message to be used if the name is invalid. + * @return A {@code Name} object if the given {@code name} is valid. + * @throws ParseException if the given {@code name} is invalid. + */ + public static Name parseName(String name, String customErrorMessage) throws ParseException { + requireNonNull(name); + String trimmedName = name.trim(); + if (!Name.isValidName(trimmedName)) { + throw new ParseException(customErrorMessage); + } + return new Name(trimmedName); + } + /** * Parses a {@code String phone} into a {@code Phone}. * Leading and trailing whitespaces will be trimmed. @@ -121,4 +155,113 @@ public static Set parseTags(Collection tags) throws ParseException } return tagSet; } + + /** + * Parses {@code String birthday} into a {@code Birthday}. + */ + public static Birthday parseBirthday(String birthday) throws ParseException { + requireNonNull(birthday); + String trimmedBirthday = birthday.trim(); + System.out.println(trimmedBirthday); + if (!Birthday.isValidBirthday(trimmedBirthday)) { + throw new ParseException(Birthday.MESSAGE_CONSTRAINTS); + } + return new Birthday(birthday); + } + + /** + * Parses {@code String appointment} into a {@code Appointment}. + */ + public static Appointment parseAppointment(String appointment) throws ParseException { + requireNonNull(appointment); + String trimmedAppointment = appointment.trim(); + System.out.println(trimmedAppointment); + if (!Appointment.isValidAppointment(trimmedAppointment)) { + throw new ParseException(Appointment.MESSAGE_CONSTRAINTS); + } + return new Appointment(appointment); + } + + + /** + * Parses a {@code String policyArgs} into a {@code Policy}. + * + * @throws ParseException if the given {@code policyArgs} is invalid. + */ + public static Policy parsePolicy(String policyArgs) throws ParseException { + requireNonNull(policyArgs); + String trimmedPolicy = policyArgs.trim(); + + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(trimmedPolicy, PREFIX_POLICY_NAME, PREFIX_POLICY_START_DATE, + PREFIX_POLICY_END_DATE, PREFIX_NEXT_PAYMENT_DATE, PREFIX_PAYMENT_AMOUNT); + + if (!arePrefixesPresent(argMultimap, PREFIX_POLICY_NAME, PREFIX_POLICY_START_DATE, + PREFIX_POLICY_END_DATE, PREFIX_NEXT_PAYMENT_DATE, PREFIX_PAYMENT_AMOUNT)) { + throw new ParseException(String.format(MESSAGE_INVALID_POLICY_FORMAT)); + } + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_POLICY_NAME, PREFIX_POLICY_START_DATE, + PREFIX_POLICY_END_DATE, PREFIX_NEXT_PAYMENT_DATE, PREFIX_PAYMENT_AMOUNT); + + String nameString = argMultimap.getValue(PREFIX_POLICY_NAME).get(); + String startDateString = argMultimap.getValue(PREFIX_POLICY_START_DATE).get(); + String endDateString = argMultimap.getValue(PREFIX_POLICY_END_DATE).get(); + String paydateString = argMultimap.getValue(PREFIX_NEXT_PAYMENT_DATE).get(); + String paymentAmount = argMultimap.getValue(PREFIX_PAYMENT_AMOUNT).get(); + String insurancePayment = paydateString + " " + paymentAmount; + if (!Payment.isValidInsurancePayment(insurancePayment)) { + throw new ParseException(Payment.MESSAGE_CONSTRAINTS); + } + if (!Policy.isValidPolicy(nameString, startDateString, endDateString, insurancePayment)) { + throw new ParseException(Policy.MESSAGE_CONSTRAINTS); + } + try { + return new Policy(nameString, startDateString, endDateString, insurancePayment); + } catch (CommandException e) { + throw new ParseException(e.getMessage()); + } + + } + + /** + * Parses {@code Collection policies} into a {@code List}. + */ + public static Map parsePolicies(Collection policies) throws ParseException { + requireNonNull(policies); + final Map policyMap = new HashMap<>(); + final Set toEditIndexSet = new HashSet<>(); + + for (String policyArgs : policies) { + String trimmedPolicy = policyArgs.trim(); + + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(trimmedPolicy, PREFIX_POLICY_NAME); + + Index index; + + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_POLICY_FORMAT), pe); + } + + if (toEditIndexSet.contains(index.getZeroBased())) { + throw new ParseException(MESSAGE_DUPLICATE_POLICY_INDEX); + } + + toEditIndexSet.add(index.getZeroBased()); + policyMap.put(index, parsePolicy(policyArgs)); + } + + return policyMap; + } + + /** + * 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()); + } } diff --git a/src/main/java/seedu/address/logic/parser/SearchCommandParser.java b/src/main/java/seedu/address/logic/parser/SearchCommandParser.java new file mode 100644 index 00000000000..d5343d2de9e --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/SearchCommandParser.java @@ -0,0 +1,98 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.Messages.MESSAGE_MISSING_PREFIX_SEARCH; +import static seedu.address.logic.parser.CliSyntax.PREFIX_BIRTHDAY; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SEARCH_APPOINTMENT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SEARCH_POLICY; + +import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.SearchAppointmentCommand; +import seedu.address.logic.commands.SearchBirthdayCommand; +import seedu.address.logic.commands.SearchPolicyCommand; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a SearchBirthdayCommand or SearchAppointmentCommand object. + */ +public class SearchCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments and determines whether it's a birthday or an appointment search. + * Returns the appropriate command for execution. + * + * @throws ParseException if the user input does not conform to the expected format. + */ + public Command parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException(MESSAGE_MISSING_PREFIX_SEARCH); + } + + // Check if the command starts with a recognized prefix + if (trimmedArgs.contains(PREFIX_BIRTHDAY.getPrefix())) { + String date = trimmedArgs.substring(PREFIX_BIRTHDAY.getPrefix().length()).trim(); + return parseBirthdayCommand(date); + } else if (trimmedArgs.contains(PREFIX_SEARCH_APPOINTMENT.getPrefix())) { + String dateTime = trimmedArgs.substring(PREFIX_SEARCH_APPOINTMENT.getPrefix().length()).trim(); + return parseAppointmentCommand(dateTime); + } else if (trimmedArgs.contains(PREFIX_SEARCH_POLICY.getPrefix())) { + String policyName = trimmedArgs.substring(PREFIX_SEARCH_POLICY.getPrefix().length()).trim(); + return parsePolicyCommand(policyName); + } else { + throw new ParseException("Invalid prefix. Use 'b/' for birthday or 'a/' for appointment" + + " or 'p/' for policy."); + } + } + + /** + * Parses the birthday search arguments and returns a SearchBirthdayCommand. + */ + private SearchBirthdayCommand parseBirthdayCommand(String date) throws ParseException { + if (date.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SearchBirthdayCommand.MESSAGE_USAGE)); + } + + try { + return new SearchBirthdayCommand(date); + } catch (CommandException e) { + throw new ParseException( + String.format(e.getMessage() + "\n" + SearchBirthdayCommand.MESSAGE_USAGE)); + } + } + + /** + * Parses the appointment search arguments and returns a SearchAppointmentCommand. + */ + private SearchAppointmentCommand parseAppointmentCommand(String dateTime) throws ParseException { + if (dateTime.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SearchAppointmentCommand.MESSAGE_USAGE)); + } + + try { + return new SearchAppointmentCommand(dateTime); + } catch (CommandException e) { + throw new ParseException( + String.format(e.getMessage() + "\n" + SearchAppointmentCommand.MESSAGE_USAGE)); + } + } + + /** + * Parses the policy search arguments and returns a SearchPolicyCommand. + */ + private SearchPolicyCommand parsePolicyCommand(String policyName) throws ParseException { + if (policyName.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SearchPolicyCommand.MESSAGE_USAGE)); + } + + try { + return new SearchPolicyCommand(policyName); + } catch (CommandException e) { + throw new ParseException( + String.format(e.getMessage() + "\n" + SearchPolicyCommand.MESSAGE_USAGE)); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/SortCommandParser.java b/src/main/java/seedu/address/logic/parser/SortCommandParser.java new file mode 100644 index 00000000000..31a81c4cb02 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/SortCommandParser.java @@ -0,0 +1,35 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.logic.commands.SortCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new SortCommand object. + */ +public class SortCommandParser implements Parser { + + @Override + public SortCommand parse(String args) throws ParseException { + requireNonNull(args); + String[] splitArgs = args.trim().split(" "); + if (splitArgs.length != 2) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, SortCommand.MESSAGE_USAGE)); + } + + String parameter = splitArgs[0]; + String order = splitArgs[1]; + + if (!SortCommand.isValidParameter(parameter)) { + throw new ParseException(SortCommand.MESSAGE_INVALID_PARAMETER); + } + + if (!SortCommand.isValidOrder(order)) { + throw new ParseException(SortCommand.MESSAGE_INVALID_ORDER); + } + + return new SortCommand(parameter, order); + } +} diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java index 73397161e84..d55af06307b 100644 --- a/src/main/java/seedu/address/model/AddressBook.java +++ b/src/main/java/seedu/address/model/AddressBook.java @@ -2,6 +2,7 @@ import static java.util.Objects.requireNonNull; +import java.util.Comparator; import java.util.List; import javafx.collections.ObservableList; @@ -94,6 +95,14 @@ public void removePerson(Person key) { persons.remove(key); } + /** + * Sorts the persons list using the given comparator. + * @param comparator The comparator to use for sorting. + */ + public void sortPersons(Comparator comparator) { + persons.sort(comparator); + } + //// util methods @Override diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index d54df471c1f..87aa41c8761 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -1,10 +1,12 @@ package seedu.address.model; import java.nio.file.Path; +import java.util.Comparator; import java.util.function.Predicate; import javafx.collections.ObservableList; import seedu.address.commons.core.GuiSettings; +import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.person.Person; /** @@ -84,4 +86,35 @@ public interface Model { * @throws NullPointerException if {@code predicate} is null. */ void updateFilteredPersonList(Predicate predicate); + + /** + * Sorts the filtered person list using the given comparator. + */ + void sortPersonList(Comparator comparator); + + /** + * Saves the current state of the address book. + */ + void commitAddressBook(); + + /** + * Restores the previous state of the address book. + * @throws CommandException if there are no states to undo. + */ + void undoAddressBook() throws CommandException; + + /** + * Restores the state of the address book to the state before the last undo. + */ + void redoAddressBook(); + + /** + * Returns true if there are previous states to restore. + */ + boolean canUndoAddressBook(); + + /** + * Returns true if there are undone states to restore. + */ + boolean canRedoAddressBook(); } diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index 57bc563fde6..79de1608377 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/seedu/address/model/ModelManager.java @@ -4,6 +4,7 @@ import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; import java.nio.file.Path; +import java.util.Comparator; import java.util.function.Predicate; import java.util.logging.Logger; @@ -11,6 +12,7 @@ import javafx.collections.transformation.FilteredList; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; +import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.person.Person; /** @@ -19,7 +21,7 @@ public class ModelManager implements Model { private static final Logger logger = LogsCenter.getLogger(ModelManager.class); - private final AddressBook addressBook; + private final VersionedAddressBook versionedAddressBook; private final UserPrefs userPrefs; private final FilteredList filteredPersons; @@ -31,9 +33,9 @@ public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyUserPrefs userPrefs logger.fine("Initializing with address book: " + addressBook + " and user prefs " + userPrefs); - this.addressBook = new AddressBook(addressBook); + this.versionedAddressBook = new VersionedAddressBook(addressBook); this.userPrefs = new UserPrefs(userPrefs); - filteredPersons = new FilteredList<>(this.addressBook.getPersonList()); + this.filteredPersons = new FilteredList<>(this.versionedAddressBook.getPersonList()); } public ModelManager() { @@ -79,36 +81,62 @@ public void setAddressBookFilePath(Path addressBookFilePath) { @Override public void setAddressBook(ReadOnlyAddressBook addressBook) { - this.addressBook.resetData(addressBook); + this.versionedAddressBook.resetData(addressBook); } @Override public ReadOnlyAddressBook getAddressBook() { - return addressBook; + return versionedAddressBook; } @Override public boolean hasPerson(Person person) { requireNonNull(person); - return addressBook.hasPerson(person); + return versionedAddressBook.hasPerson(person); } - @Override - public void deletePerson(Person target) { - addressBook.removePerson(target); - } @Override public void addPerson(Person person) { - addressBook.addPerson(person); + versionedAddressBook.addPerson(person); updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); } @Override public void setPerson(Person target, Person editedPerson) { requireAllNonNull(target, editedPerson); + versionedAddressBook.setPerson(target, editedPerson); + } + + @Override + public void sortPersonList(Comparator comparator) { + requireNonNull(comparator); + versionedAddressBook.sortPersons(comparator); + } + + @Override + public void commitAddressBook() { + versionedAddressBook.commit(); + } + + @Override + public void undoAddressBook() throws CommandException { + versionedAddressBook.undo(); + } + + @Override + public void redoAddressBook() { + versionedAddressBook.redo(); + } + + @Override + public boolean canUndoAddressBook() { + return versionedAddressBook.canUndo(); + } - addressBook.setPerson(target, editedPerson); + @Override + public boolean canRedoAddressBook() { + return versionedAddressBook.canRedo(); } //=========== Filtered Person List Accessors ============================================================= @@ -140,9 +168,14 @@ public boolean equals(Object other) { } ModelManager otherModelManager = (ModelManager) other; - return addressBook.equals(otherModelManager.addressBook) + return versionedAddressBook.equals(otherModelManager.versionedAddressBook) && userPrefs.equals(otherModelManager.userPrefs) && filteredPersons.equals(otherModelManager.filteredPersons); } + @Override + public void deletePerson(Person target) { + versionedAddressBook.removePerson(target); + } + } diff --git a/src/main/java/seedu/address/model/VersionedAddressBook.java b/src/main/java/seedu/address/model/VersionedAddressBook.java new file mode 100644 index 00000000000..8f34d6062cb --- /dev/null +++ b/src/main/java/seedu/address/model/VersionedAddressBook.java @@ -0,0 +1,78 @@ +package seedu.address.model; + +import java.util.ArrayList; +import java.util.List; + +/** + * Represents an address book that maintains a history of its states to support undo and redo operations. + */ +public class VersionedAddressBook extends AddressBook { + private final List addressBookStateList; + private int currentStatePointer; + + /** + * Creates a VersionedAddressBook with the initial state. + * + * @param initialState The initial state of the address book. + */ + public VersionedAddressBook(ReadOnlyAddressBook initialState) { + super(initialState); + addressBookStateList = new ArrayList<>(); + addressBookStateList.add(new AddressBook(initialState)); + currentStatePointer = 0; + } + + /** + * Saves the current state of the address book to the state list. + */ + public void commit() { + removeStatesAfterCurrentPointer(); + addressBookStateList.add(new AddressBook(this)); + currentStatePointer++; + } + + /** + * Removes states after the current pointer to maintain the correct history. + */ + private void removeStatesAfterCurrentPointer() { + addressBookStateList.subList(currentStatePointer + 1, addressBookStateList.size()).clear(); + } + + /** + * Restores the address book to its previous state. + */ + public void undo() { + if (canUndo()) { + currentStatePointer--; + resetData(addressBookStateList.get(currentStatePointer)); + } + } + + /** + * Restores the address book to its next state. + */ + public void redo() { + if (canRedo()) { + currentStatePointer++; + resetData(addressBookStateList.get(currentStatePointer)); + } + } + + /** + * Returns true if there are states to undo. + * + * @return True if there are states to undo, false otherwise. + */ + public boolean canUndo() { + return currentStatePointer > 0; + } + + /** + * Returns true if there are states to redo. + * + * @return True if there are states to redo, false otherwise. + */ + public boolean canRedo() { + return currentStatePointer < addressBookStateList.size() - 1; + } +} diff --git a/src/main/java/seedu/address/model/person/Address.java b/src/main/java/seedu/address/model/person/Address.java index 469a2cc9a1e..b062f50d58c 100644 --- a/src/main/java/seedu/address/model/person/Address.java +++ b/src/main/java/seedu/address/model/person/Address.java @@ -37,6 +37,21 @@ public static boolean isValidAddress(String test) { return test.matches(VALIDATION_REGEX); } + /** + * Normalizes the address by removing all non-alphanumeric characters + * (such as spaces, commas, and hyphens) and converting all letters to lowercase. + * + * For example: + * - "311, Clementi Ave 5, unit 02-2" will become "311clementiave5unit022" + * - "123 Main St - Apt 4B" will become "123mainstapt4b" + * + * @return A normalized version of the address as a lowercase alphanumeric string. + */ + public String normaliseAddress() { + return value.replaceAll("[^a-zA-Z0-9]", "").toLowerCase(); + + } + @Override public String toString() { return value; @@ -54,7 +69,7 @@ public boolean equals(Object other) { } Address otherAddress = (Address) other; - return value.equals(otherAddress.value); + return this.normaliseAddress().equals(otherAddress.normaliseAddress()); } @Override diff --git a/src/main/java/seedu/address/model/person/Appointment.java b/src/main/java/seedu/address/model/person/Appointment.java new file mode 100644 index 00000000000..af2aeaa1ba2 --- /dev/null +++ b/src/main/java/seedu/address/model/person/Appointment.java @@ -0,0 +1,84 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +/** + * Represents a Person's appointment in the address book. + */ +public class Appointment { + + public static final String MESSAGE_CONSTRAINTS = + "Appointments should be in the format 'yyyy-MM-dd HH:mm', " + + "must be a valid date and time, and must be scheduled after today's date."; + + /* + * The appointment date and time should in the format 'yyyy-MM-dd HH:mm'. + * Example: 2023-01-31 13:00 + */ + public static final String VALIDATION_REGEX = "\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}"; + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + + public final LocalDateTime date; + + public final String value; + + /** + * Constructs an {@code Appointment}. + * + * @param appointmentDateStr A valid date and time string. + */ + public Appointment(String appointmentDateStr) { + requireNonNull(appointmentDateStr); + checkArgument(isValidAppointment(appointmentDateStr), MESSAGE_CONSTRAINTS); + date = parseDateTime(appointmentDateStr); + value = date.format(FORMATTER); + } + + /** + * Parses the given date and time string into a {@code LocalDateTime}. + * + * @param dateTimeStr The date and time string to parse. + * @return A {@code LocalDateTime} object representing the date and time. + */ + private static LocalDateTime parseDateTime(String dateTimeStr) { + return LocalDateTime.parse(dateTimeStr, FORMATTER); + } + + /** + * Returns true if a given string is a valid appointment date and time. + */ + public static boolean isValidAppointment(String test) { + requireNonNull(test); + if (!test.matches(VALIDATION_REGEX)) { + return false; + } + try { + LocalDateTime appointmentDateTime = LocalDateTime.parse(test, FORMATTER); + return appointmentDateTime.toLocalDate().isAfter(LocalDate.now()); + } catch (DateTimeParseException e) { + return false; + } + } + + @Override + public String toString() { + return date.format(FORMATTER); + } + + @Override + public int hashCode() { + return date.hashCode(); + } + + @Override + public boolean equals(Object other) { + return other == this || (other instanceof Appointment && date.equals(((Appointment) other).date)); + } + +} diff --git a/src/main/java/seedu/address/model/person/Birthday.java b/src/main/java/seedu/address/model/person/Birthday.java new file mode 100644 index 00000000000..ba80a0a6def --- /dev/null +++ b/src/main/java/seedu/address/model/person/Birthday.java @@ -0,0 +1,77 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +/** + * Represents a Person's birthday in the address book. + */ +public class Birthday { + + public static final String MESSAGE_CONSTRAINTS = + "Birthdays should be in the format 'yyyy-MM-dd', " + + "must be a valid date, and must be a date after today's date."; + + private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + public final String value; + + private final LocalDate date; + + + /** + * Constructs a {@code Birthday}. + * + * @param dateStr A valid date string. + */ + public Birthday(String dateStr) { + requireNonNull(dateStr); + checkArgument(isValidBirthday(dateStr), MESSAGE_CONSTRAINTS); + date = parseDate(dateStr); + this.value = date.format(FORMATTER); + } + + /** + * Parses the given date string into a {@code LocalDate}. + * + * @param dateStr The date string to parse. + * @return A {@code LocalDate} object representing the date. + */ + private static LocalDate parseDate(String dateStr) { + return LocalDate.parse(dateStr, FORMATTER); + } + + /** + * Returns true if a given string is a valid birthday date. + */ + public static boolean isValidBirthday(String test) { + requireNonNull(test); + try { + LocalDate testBirthday = LocalDate.parse(test, FORMATTER); + return testBirthday.isBefore(LocalDate.now()); + } catch (DateTimeParseException e) { + return false; + } + } + + @Override + public int hashCode() { + return date.hashCode(); + } + + @Override + public boolean equals(Object other) { + return this == other // short circuit if same object + || (other instanceof Birthday // instanceof handles nulls + && date.equals(((Birthday) other).date)); + } + + @Override + public String toString() { + return date.format(FORMATTER); + } +} diff --git a/src/main/java/seedu/address/model/person/Payment.java b/src/main/java/seedu/address/model/person/Payment.java new file mode 100644 index 00000000000..59afa5a18d5 --- /dev/null +++ b/src/main/java/seedu/address/model/person/Payment.java @@ -0,0 +1,112 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +/** + * Represents a Person's insurance payment in the address book. + */ +public class Payment { + public static final String MESSAGE_CONSTRAINTS = "Insurance payments should be in the format 'yyyy-MM-dd amount' " + + "\nAmount is a positive number with up to two decimal places!"; + + /* + * The insurance payment should be in the format 'yyyy-MM-dd amount'. + * Example: 2023-11-01 300.00 + */ + public static final String VALIDATION_REGEX = "\\d{4}-\\d{2}-\\d{2} \\d+(\\.\\d{1,2})?"; + + + + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + private static final String FULLY_PAID_PAYMENT = BigDecimal.ZERO.toString() + " " + LocalDate.MAX.toString(); + + public final String value; + private LocalDate paymentDueDate; + private BigDecimal amount; + + /** + * Constructs an {@code InsurancePayment}. + * + * @param insurancePayment A valid insurance payment string. + */ + public Payment(String insurancePayment) { + requireNonNull(insurancePayment); + checkArgument(isValidInsurancePayment(insurancePayment), MESSAGE_CONSTRAINTS); + value = insurancePayment; + if (value.equals(FULLY_PAID_PAYMENT)) { + paymentDueDate = LocalDate.MAX; + amount = BigDecimal.ZERO; + } else { + String[] parts = insurancePayment.trim().split("\\s+"); + paymentDueDate = LocalDate.parse(parts[0], DATE_FORMATTER); + amount = new BigDecimal(parts[1]); + } + } + + /** + * Returns true if a given string is a valid insurance payment. + */ + public static boolean isValidInsurancePayment(String test) { + requireNonNull(test); + if (test.equals(FULLY_PAID_PAYMENT)) { + return true; + } + if (!test.matches(VALIDATION_REGEX)) { + return false; + } + String[] parts = test.trim().split("\\s+"); + if (parts.length != 2) { + return false; + } + try { + LocalDate.parse(parts[0], DATE_FORMATTER); + BigDecimal amount = new BigDecimal(parts[1]); + return amount.compareTo(BigDecimal.ZERO) > 0; + } catch (DateTimeParseException | NumberFormatException e) { + return false; + } + } + + /** + * Updates the payment due date of the insurance payment. + * @param policy + */ + public void updatePaymentDueDate(Policy policy) { + if (policy.isExpiringSoon()) { + paymentDueDate = LocalDate.MAX; + amount = BigDecimal.ZERO; + } else { + paymentDueDate = paymentDueDate.plusYears(1); + } + } + + public LocalDate getPaymentDueDate() { + return paymentDueDate; + } + + @Override + public String toString() { + if (amount.equals(BigDecimal.ZERO) || paymentDueDate.equals(LocalDate.MAX)) { + return "Fully Paid"; + } + return String.format("$%.2f due on %s", amount, paymentDueDate); + } + + @Override + public boolean equals(Object other) { + return other == this || (other instanceof Payment && value.equals(((Payment) other).value)); + } + + @Override + public int hashCode() { + return value.hashCode(); + } + +} diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java index abe8c46b535..7b6bf93d889 100644 --- a/src/main/java/seedu/address/model/person/Person.java +++ b/src/main/java/seedu/address/model/person/Person.java @@ -2,8 +2,11 @@ import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import java.util.ArrayList; import java.util.Collections; +import java.util.Comparator; import java.util.HashSet; +import java.util.List; import java.util.Objects; import java.util.Set; @@ -20,20 +23,28 @@ public class Person { private final Name name; private final Phone phone; private final Email email; + private final Appointment appointment; + private final Birthday birthday; // Data fields private final Address address; private final Set tags = new HashSet<>(); + private List policies = new ArrayList<>(); + /** * Every field must be present and not null. */ - public Person(Name name, Phone phone, Email email, Address address, Set tags) { - requireAllNonNull(name, phone, email, address, tags); + public Person(Name name, Phone phone, Email email, Address address, Set tags, + Appointment appointment, Birthday birthday) { + requireAllNonNull(name, phone, email, address, appointment, birthday); this.name = name; this.phone = phone; this.email = email; this.address = address; + this.appointment = appointment; + this.birthday = birthday; + this.tags.addAll(tags); } @@ -53,6 +64,14 @@ public Address getAddress() { return address; } + public Appointment getAppointment() { + return appointment; + } + + public Birthday getBirthday() { + return birthday; + } + /** * Returns an immutable tag set, which throws {@code UnsupportedOperationException} * if modification is attempted. @@ -61,8 +80,124 @@ public Set getTags() { return Collections.unmodifiableSet(tags); } + + /** + * Assigns a policy to the list of policies. + * + * @param policy the policy to be assigned + * @return true if the policy was successfully added, false if the policy already exists + */ + public boolean assignPolicy(Policy policy) { + for (Policy p : policies) { + if (p.isSamePolicy(policy)) { + return false; + } + } + policies.add(policy); + return true; + + } + + public void removePolicy(Policy policy) { + policies.remove(policy); + } + + /** + * Returns a string representation of all assigned policies. + * + * @return a string containing the details of each policy, separated by new lines + */ + public String getPoliciesString() { + StringBuilder sb = new StringBuilder(); + int count = 1; + for (Policy policy : policies) { + sb.append(String.format("%s.", count)); + sb.append(policy.toString()).append("\n"); + count++; + } + return sb.toString(); + } + + + + /** + * Returns a comparator that compares persons by name in lexicographic order. + */ + public static Comparator getNameComparator() { + return Comparator.comparing(person -> person.getName().fullName.toLowerCase()); + } + + /** + * Returns a comparator that compares persons by appointment date in chronological order. + */ + public static Comparator getAppointmentDateComparator() { + return Comparator.comparing(person -> person.getAppointment().value); + } + + /** + * Returns a comparator that compares persons by birthday in chronological order. + */ + public static Comparator getBirthdayComparator() { + return Comparator.comparing(person -> person.getBirthday().value); + } + + /** + * Returns a comparator that compares persons by next payment date in chronological order. + */ + public static Comparator getPayDateComparator() { + return (person1, person2) -> { + boolean person1HasPolicies = person1.getPolicies().size() > 0; + boolean person2HasPolicies = person2.getPolicies().size() > 0; + + if (!person1HasPolicies && !person2HasPolicies) { + return 0; + } else if (!person1HasPolicies) { + return 1; + } else if (!person2HasPolicies) { + return -1; + } else { + return person1.getPolicies().get(0).getPolicyPaymentDueDate() + .compareTo(person2.getPolicies().get(0).getPolicyPaymentDueDate()); + } + }; + } + + /** + * Returns an immutable policy list, which throws {@code UnsupportedOperationException} + * if modification is attempted. + */ + public List getPolicies() { + policies.sort(Policy.getPolicyPaymentDueDateComparator()); + return Collections.unmodifiableList(policies); + } + + /** + * Sets the policy list for this object. + * + * @param policies the list of {@code Policy} objects to be set. This list will replace + * any existing policies. + */ + public void setPolicies(List policies) { + this.policies = policies; + } + + /** + * Returns the policy with the given name, or null if no such policy exists. + * + * @param policyName the name of the policy to search for + * @return the policy with the given name, or null if no such policy exists + */ + public Policy getPolicyByName(String policyName) { + for (Policy policy : policies) { + if (policy.getPolicyName().equals(policyName)) { + return policy; + } + } + return null; + } + /** - * Returns true if both persons have the same name. + * Returns true if both persons have the same name and address. * This defines a weaker notion of equality between two persons. */ public boolean isSamePerson(Person otherPerson) { @@ -71,7 +206,9 @@ public boolean isSamePerson(Person otherPerson) { } return otherPerson != null - && otherPerson.getName().equals(getName()); + && otherPerson.getName().equals(getName()) + && otherPerson.getAddress().normaliseAddress() + .equals(getAddress().normaliseAddress()); } /** @@ -94,13 +231,14 @@ public boolean equals(Object other) { && phone.equals(otherPerson.phone) && email.equals(otherPerson.email) && address.equals(otherPerson.address) - && tags.equals(otherPerson.tags); + && tags.equals(otherPerson.tags) + && birthday.equals(otherPerson.birthday); } @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, birthday, appointment); } @Override @@ -110,7 +248,10 @@ public String toString() { .add("phone", phone) .add("email", email) .add("address", address) + .add("birthday", birthday) + .add("appointment", appointment) .add("tags", tags) + .add("policies", policies) .toString(); } diff --git a/src/main/java/seedu/address/model/person/Phone.java b/src/main/java/seedu/address/model/person/Phone.java index d733f63d739..84357ad5e6f 100644 --- a/src/main/java/seedu/address/model/person/Phone.java +++ b/src/main/java/seedu/address/model/person/Phone.java @@ -11,8 +11,9 @@ 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,}"; + "Phone numbers should only contain numbers, and it should be at least 3 digits " + + "but at most 8 digits long"; + public static final String VALIDATION_REGEX = "\\d{3,8}"; public final String value; /** diff --git a/src/main/java/seedu/address/model/person/Policy.java b/src/main/java/seedu/address/model/person/Policy.java new file mode 100644 index 00000000000..684176a6072 --- /dev/null +++ b/src/main/java/seedu/address/model/person/Policy.java @@ -0,0 +1,245 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.Comparator; +import java.util.HashMap; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import seedu.address.logic.commands.exceptions.CommandException; +/** + * Represents an insurance policy in the address book. + */ +public class Policy { + + public static final String MESSAGE_CONSTRAINTS = "Policy details should be in the format 'policyName startDate " + + "endDate' paydate amountDue, where dates are in 'yyyy-MM-dd' format."; + + public static final String START_DATE_MUST_BE_AFTER_THE_NINETIES = "Insurance has been around in the 1900s! " + + "Please input a date after that"; + public static final String END_DATE_BEFORE_START_DATE = "End date cannot be before start date!"; + public static final String PREMIUM_DUE_DATE_BEFORE_START_DATE = "Premium due date cannot " + + "cannot be before start date!"; + public static final String PREMIUM_DUE_DATE_AFTER_END_DATE = "Premium due date cannot be after end date!"; + public static final String START_EQ_END = "Start date and end date cannot be the same!"; + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + private static final String TOSTRINGFORMATTER = "([\\w\\s]+)\\s+\\((\\d{4}-\\d{2}-\\d{2})\\s*" + + "to\\s*(\\d{4}-\\d{2}-\\d{2})\\)\\s*" + + "(?:\\$([0-9.]+)\\s*due\\s*on\\s*(\\d{4}-\\d{2}-\\d{2})|(?Fully Paid))"; + + private static final String POLICY_NAME = "policyName"; // Policy name + private static final String START_DATE = "startDate"; // Start date of the policy + private static final String END_DATE = "endDate"; // End date of the policy + private static final String PAYMENT_DATE = "paymentDate"; // Date of the payment + private static final String PAYMENT_AMOUNT = "paymentAmount"; // Amount of the payment + + private static final LocalDate FIRST_INSURANCE_DATE = LocalDate.parse("1900-01-01", DATE_FORMATTER); + private final String policyName; + private final LocalDate startDate; + private final LocalDate endDate; + private final Payment payment; + + + /** + * Constructs a {@code Policy}. + * + * @param policyName Name of the policy. + * @param startDateStr Start date of the policy. + * @param endDateStr End date of the policy. + */ + public Policy(String policyName, String startDateStr, String endDateStr, + String insurancePayment) throws CommandException { + checkArgument(isValidPolicy(policyName, startDateStr, + endDateStr, insurancePayment), MESSAGE_CONSTRAINTS); + this.policyName = policyName.trim(); + this.startDate = parseDate(startDateStr); + this.endDate = parseDate(endDateStr); + this.payment = new Payment(insurancePayment); + validatePolicy(); + } + + /** + * Constructs a Policy object with the given policy description. + * The policy description must adhere to a valid format and include the policy name, start date, end date, + * payment amount, and payment date. It extracts and assigns these details to the respective fields. + * + * @param policyDescription A string describing the policy details, including the name, start date, end date, + * payment amount, and payment date. + */ + public Policy(String policyDescription) throws CommandException { + checkArgument(isValidPolicy(policyDescription), MESSAGE_CONSTRAINTS); + Map policyDetails = extractPolicyDetails(policyDescription); + this.policyName = policyDetails.get(POLICY_NAME); + this.startDate = parseDate(policyDetails.get(START_DATE)); + this.endDate = parseDate(policyDetails.get(END_DATE)); + String paymentAmount = policyDetails.get(PAYMENT_AMOUNT); + String paymentDate = policyDetails.get(PAYMENT_DATE); + this.payment = new Payment(paymentDate + " " + paymentAmount); + validatePolicy(); + } + + private void validatePolicy() throws CommandException { + if (this.getPolicyPaymentDueDate().isEqual(LocalDate.MAX)) { + ; + } else if (this.startDate.isBefore(FIRST_INSURANCE_DATE)) { + throw new CommandException(START_DATE_MUST_BE_AFTER_THE_NINETIES); + } else if (this.endDate.isEqual(this.startDate)) { + throw new CommandException(START_EQ_END); + } else if (this.endDate.isBefore(this.startDate)) { + throw new CommandException(END_DATE_BEFORE_START_DATE); + } else if (this.payment.getPaymentDueDate().isBefore(this.startDate)) { + throw new CommandException(PREMIUM_DUE_DATE_BEFORE_START_DATE); + } else if (this.payment.getPaymentDueDate().isAfter(this.endDate)) { + throw new CommandException(PREMIUM_DUE_DATE_AFTER_END_DATE); + } + } + + /** + * Parses a date string into a {@code LocalDate}. + * + * @param dateStr The date string to parse. + * @return A {@code LocalDate} object representing the date. + */ + private static LocalDate parseDate(String dateStr) { + return LocalDate.parse(dateStr, DATE_FORMATTER); + } + + /** + * Returns true if the given policy details are valid. + */ + public static boolean isValidPolicy(String policyName, String startDateStr, + String endDateStr, String payment) { + requireNonNull(policyName); + requireNonNull(startDateStr); + requireNonNull(endDateStr); + requireNonNull(payment); + + // Check policyName is non-empty + if (policyName.trim().isEmpty()) { + return false; + } + try { + LocalDate.parse(startDateStr, DATE_FORMATTER); + LocalDate.parse(endDateStr, DATE_FORMATTER); + return true; + } catch (DateTimeParseException e) { + return false; + } + + } + + /** + * Returns true if the given policy string is valid. + */ + public static boolean isValidPolicy(String policyDescription) { + Map policyDetails = extractPolicyDetails(policyDescription); + if (policyDetails == null) { + return false; + } + String policyName = policyDetails.get(POLICY_NAME); + String startDate = policyDetails.get(START_DATE); + String endDate = policyDetails.get(END_DATE); + String paymentAmount = policyDetails.get(PAYMENT_AMOUNT); + String paymentDate = policyDetails.get(PAYMENT_DATE); + return isValidPolicy(policyName, startDate, endDate, paymentDate + " " + paymentAmount); + } + + + public String getPolicyName() { + return policyName; + } + + public void updateNextPaymentDate() { + payment.updatePaymentDueDate(this); + } + + public LocalDate getPolicyPaymentDueDate() { + return payment.getPaymentDueDate(); + } + + public boolean isExpiringSoon() { + return payment.getPaymentDueDate().plusYears(1).isAfter(endDate); + } + + public boolean isFullyPaid() { + return payment.getPaymentDueDate().equals(LocalDate.MAX); + } + + /** + * Returns a comparator that compares two policies based on their payment due dates. + * Policies that are fully paid will be considered last. + */ + public static Comparator getPolicyPaymentDueDateComparator() { + return Comparator.comparing(Policy::getPolicyPaymentDueDate); + } + + /** + * Extracts policy details from the provided policy description string using a regular expression pattern. + * The string must match the format defined by the {@code TOSTRINGFORMATTER} pattern, and the details + * will be extracted in the following order: policy name, start date, end date, payment amount, and payment date. + * + * @param policyDescription The string describing the policy details, + * expected to match the format specified by the regex pattern. + * @return A list of strings containing the extracted policy details + * or {@code null} if no match is found in the policy description. + */ + public static Map extractPolicyDetails(String policyDescription) { + // Compile the regex pattern + Pattern pattern = Pattern.compile(TOSTRINGFORMATTER); + Matcher matcher = pattern.matcher(policyDescription); + + // Create a HashMap to store the extracted details + Map policyDetails = new HashMap<>(); + + // If a match is found, extract and add details to the HashMap + if (matcher.find()) { + policyDetails.put(POLICY_NAME, matcher.group(1)); + policyDetails.put(START_DATE, matcher.group(2)); + policyDetails.put(END_DATE, matcher.group(3)); + if (matcher.group(4) != null && matcher.group(5) != null) { + // Handle the case when there is an amount due + policyDetails.put(PAYMENT_AMOUNT, matcher.group(4)); + policyDetails.put(PAYMENT_DATE, matcher.group(5)); + } else if (matcher.group("paid") != null) { + policyDetails.put(PAYMENT_AMOUNT, LocalDate.MAX.toString()); + policyDetails.put(PAYMENT_DATE, BigDecimal.ZERO.toString()); + } + + return policyDetails; + } + return null; + } + public boolean isSamePolicy(Policy policy) { + return policyName.equals(policy.getPolicyName()); + } + + @Override + public int hashCode() { + return policyName.hashCode() + startDate.hashCode() + + endDate.hashCode() + payment.hashCode(); + } + + + @Override + public boolean equals(Object other) { + return this == other + || (other instanceof Policy + && policyName.equals(((Policy) other).policyName) + && startDate.equals(((Policy) other).startDate) + && endDate.equals(((Policy) other).endDate) + && payment.equals(((Policy) other).payment)); + } + + @Override + public String toString() { + return policyName + " (" + startDate + " to " + endDate + ") " + payment.toString(); + } +} diff --git a/src/main/java/seedu/address/model/person/UniquePersonList.java b/src/main/java/seedu/address/model/person/UniquePersonList.java index cc0a68d79f9..45917b6159a 100644 --- a/src/main/java/seedu/address/model/person/UniquePersonList.java +++ b/src/main/java/seedu/address/model/person/UniquePersonList.java @@ -3,6 +3,7 @@ import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import java.util.Comparator; import java.util.Iterator; import java.util.List; @@ -104,6 +105,14 @@ public ObservableList asUnmodifiableObservableList() { return internalUnmodifiableList; } + /** + * Sorts the list according to the given comparator. + */ + public void sort(Comparator comparator) { + requireNonNull(comparator); + internalList.sort(comparator); + } + @Override public Iterator iterator() { return internalList.iterator(); diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java index 1806da4facf..da637756e44 100644 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ b/src/main/java/seedu/address/model/util/SampleDataUtil.java @@ -7,6 +7,8 @@ import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.person.Address; +import seedu.address.model.person.Appointment; +import seedu.address.model.person.Birthday; import seedu.address.model.person.Email; import seedu.address.model.person.Name; import seedu.address.model.person.Person; @@ -18,25 +20,37 @@ */ public class SampleDataUtil { public static Person[] getSamplePersons() { - return new Person[] { + return new Person[]{ new Person(new Name("Alex Yeoh"), new Phone("87438807"), new Email("alexyeoh@example.com"), - new Address("Blk 30 Geylang Street 29, #06-40"), - getTagSet("friends")), + new Address("Blk 30 Geylang Street 29, #06-40"), + getTagSet("friends"), + new Appointment("2025-10-15 14:00"), // sample appointment + new Birthday("1990-05-20")), // sample birthday new Person(new Name("Bernice Yu"), new Phone("99272758"), new Email("berniceyu@example.com"), - new Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), - getTagSet("colleagues", "friends")), + new Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), + getTagSet("colleagues", "friends"), + new Appointment("2025-11-01 09:00"), + new Birthday("1985-09-12")), new Person(new Name("Charlotte Oliveiro"), new Phone("93210283"), new Email("charlotte@example.com"), - new Address("Blk 11 Ang Mo Kio Street 74, #11-04"), - getTagSet("neighbours")), + new Address("Blk 11 Ang Mo Kio Street 74, #11-04"), + getTagSet("neighbours"), + new Appointment("2025-12-20 16:00"), + new Birthday("1992-07-30")), new Person(new Name("David Li"), new Phone("91031282"), new Email("lidavid@example.com"), - new Address("Blk 436 Serangoon Gardens Street 26, #16-43"), - getTagSet("family")), + new Address("Blk 436 Serangoon Gardens Street 26, #16-43"), + getTagSet("family"), + new Appointment("2025-09-25 11:00"), + new Birthday("1988-11-22")), new Person(new Name("Irfan Ibrahim"), new Phone("92492021"), new Email("irfan@example.com"), - new Address("Blk 47 Tampines Street 20, #17-35"), - getTagSet("classmates")), + new Address("Blk 47 Tampines Street 20, #17-35"), + getTagSet("classmates"), + new Appointment("2025-10-30 10:30"), + new Birthday("1995-03-10")), new Person(new Name("Roy Balakrishnan"), new Phone("92624417"), new Email("royb@example.com"), - new Address("Blk 45 Aljunied Street 85, #11-31"), - getTagSet("colleagues")) + new Address("Blk 45 Aljunied Street 85, #11-31"), + getTagSet("colleagues"), + new Appointment("2025-12-01 15:00"), + new Birthday("1991-08-14")) }; } diff --git a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java index bd1ca0f56c8..fc0b24e9e47 100644 --- a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java +++ b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java @@ -11,10 +11,13 @@ import seedu.address.commons.exceptions.IllegalValueException; import seedu.address.model.person.Address; +import seedu.address.model.person.Appointment; +import seedu.address.model.person.Birthday; import seedu.address.model.person.Email; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.Policy; import seedu.address.model.tag.Tag; /** @@ -28,22 +31,33 @@ class JsonAdaptedPerson { private final String phone; private final String email; private final String address; + private final String appointment; + private final String birthday; private final List tags = new ArrayList<>(); + private final List policies = new ArrayList<>(); /** * Constructs a {@code JsonAdaptedPerson} with the given person details. */ @JsonCreator public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone") String phone, - @JsonProperty("email") String email, @JsonProperty("address") String address, - @JsonProperty("tags") List tags) { + @JsonProperty("email") String email, @JsonProperty("address") String address, + @JsonProperty("appointment") String appointment, + @JsonProperty("birthday") String birthday, + @JsonProperty("tags") List tags, + @JsonProperty("policies") List policies) { this.name = name; this.phone = phone; this.email = email; this.address = address; + this.birthday = birthday; + this.appointment = appointment; if (tags != null) { this.tags.addAll(tags); } + if (policies != null) { + this.policies.addAll(policies); + } } /** @@ -54,9 +68,14 @@ public JsonAdaptedPerson(Person source) { phone = source.getPhone().value; email = source.getEmail().value; address = source.getAddress().value; + birthday = source.getBirthday().value; + appointment = source.getAppointment().value; tags.addAll(source.getTags().stream() .map(JsonAdaptedTag::new) .collect(Collectors.toList())); + policies.addAll(source.getPolicies().stream() + .map(JsonAdaptedPolicies::new) + .collect(Collectors.toList())); } /** @@ -66,6 +85,12 @@ public JsonAdaptedPerson(Person source) { */ public Person toModelType() throws IllegalValueException { final List personTags = new ArrayList<>(); + final List policiesTags = new ArrayList<>(); + + for (JsonAdaptedPolicies policy: policies) { + policiesTags.add(policy.toModelType()); + } + for (JsonAdaptedTag tag : tags) { personTags.add(tag.toModelType()); } @@ -103,7 +128,32 @@ public Person toModelType() throws IllegalValueException { final Address modelAddress = new Address(address); final Set modelTags = new HashSet<>(personTags); - return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags); - } + + + if (appointment == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + Appointment.class.getSimpleName())); + } + if (!Appointment.isValidAppointment(appointment)) { + throw new IllegalValueException(String.format(Appointment.MESSAGE_CONSTRAINTS)); + } + final Appointment modelAppointment = new Appointment(appointment); + + if (birthday == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + Birthday.class.getSimpleName())); + } + + if (!Birthday.isValidBirthday(birthday)) { + throw new IllegalValueException(String.format(Birthday.MESSAGE_CONSTRAINTS)); + } + + final Birthday modelBirthday = new Birthday(birthday); + + Person model = new Person(modelName, modelPhone, modelEmail, modelAddress, + modelTags, modelAppointment, modelBirthday); + model.setPolicies(policiesTags); + return model; + } } diff --git a/src/main/java/seedu/address/storage/JsonAdaptedPolicies.java b/src/main/java/seedu/address/storage/JsonAdaptedPolicies.java new file mode 100644 index 00000000000..489d4ab394b --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedPolicies.java @@ -0,0 +1,53 @@ +package seedu.address.storage; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.person.Policy; +import seedu.address.model.tag.Tag; + + + +/** + * Jackson-friendly version of {@link Tag}. + */ +class JsonAdaptedPolicies { + + private final String policyDescription; + + /** + * Constructs a {@code JsonAdaptedTag} with the given {@code tagName}. + */ + @com.fasterxml.jackson.annotation.JsonCreator + public JsonAdaptedPolicies(String policyDescription) { + this.policyDescription = policyDescription; + } + + /** + * Converts a given {@code Tag} into this class for Jackson use. + */ + public JsonAdaptedPolicies(Policy source) { + policyDescription = source.toString(); + } + + @com.fasterxml.jackson.annotation.JsonValue + public String getPolicyDescription() { + return policyDescription; + } + + /** + * Converts this Jackson-friendly adapted tag object into the model's {@code Tag} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted tag. + */ + public Policy toModelType() throws IllegalValueException { + if (!Policy.isValidPolicy(policyDescription)) { + throw new IllegalValueException(Policy.MESSAGE_CONSTRAINTS); + } + try { + return new Policy(policyDescription); + } catch (CommandException e) { + throw new IllegalValueException(policyDescription + " " + e.getMessage()); + } + } + +} diff --git a/src/main/java/seedu/address/ui/CommandBox.java b/src/main/java/seedu/address/ui/CommandBox.java index 9e75478664b..bd277c3b76e 100644 --- a/src/main/java/seedu/address/ui/CommandBox.java +++ b/src/main/java/seedu/address/ui/CommandBox.java @@ -69,6 +69,15 @@ private void setStyleToIndicateCommandFailure() { styleClass.add(ERROR_STYLE_CLASS); } + /** + * Clear command box. + */ + @FXML + private void clearCommandBox() { + commandTextField.clear(); + commandTextField.requestFocus(); + } + /** * Represents a function that can execute commands. */ @@ -81,5 +90,4 @@ public interface CommandExecutor { */ CommandResult execute(String commandText) throws CommandException, ParseException; } - } diff --git a/src/main/java/seedu/address/ui/HelpWindow.java b/src/main/java/seedu/address/ui/HelpWindow.java index 3f16b2fcf26..d9ee2db99a2 100644 --- a/src/main/java/seedu/address/ui/HelpWindow.java +++ b/src/main/java/seedu/address/ui/HelpWindow.java @@ -1,9 +1,12 @@ package seedu.address.ui; +import java.awt.Desktop; +import java.net.URI; import java.util.logging.Logger; import javafx.fxml.FXML; import javafx.scene.control.Button; +import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; import javafx.scene.input.Clipboard; import javafx.scene.input.ClipboardContent; @@ -15,8 +18,8 @@ */ public class HelpWindow extends UiPart { - public static final String USERGUIDE_URL = "https://se-education.org/addressbook-level3/UserGuide.html"; - public static final String HELP_MESSAGE = "Refer to the user guide: " + USERGUIDE_URL; + public static final String USERGUIDE_URL = "https://ay2425s1-cs2103t-w10-3.github.io/tp/UserGuide.html"; + public static final String HELP_MESSAGE = "Refer to the user guide:"; private static final Logger logger = LogsCenter.getLogger(HelpWindow.class); private static final String FXML = "HelpWindow.fxml"; @@ -27,6 +30,9 @@ public class HelpWindow extends UiPart { @FXML private Label helpMessage; + @FXML + private Hyperlink ugLink; + /** * Creates a new HelpWindow. * @@ -35,6 +41,7 @@ public class HelpWindow extends UiPart { public HelpWindow(Stage root) { super(FXML, root); helpMessage.setText(HELP_MESSAGE); + ugLink.setText(USERGUIDE_URL); } /** @@ -99,4 +106,28 @@ private void copyUrl() { url.putString(USERGUIDE_URL); clipboard.setContent(url); } + + /** + * Opens the user guide URL in the default web browser. + *

+ * After attempting to open the URL, the hyperlink's visited state is reset + * to {@code false}. + * + * @throws Exception if an error occurs while attempting to open the URL + */ + @FXML + private void goToUrl() { + try { + URI uri = new URI(USERGUIDE_URL); + + if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { + Desktop.getDesktop().browse(uri); + } + } catch (Exception e) { + e.printStackTrace(); + } + + // Set condition back to not visited + ugLink.setVisited(false); + } } diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java index 79e74ef37c0..e06b30e963f 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/address/ui/MainWindow.java @@ -1,14 +1,18 @@ package seedu.address.ui; +import java.util.Optional; import java.util.logging.Logger; import javafx.event.ActionEvent; import javafx.fxml.FXML; +import javafx.scene.control.Label; import javafx.scene.control.MenuItem; import javafx.scene.control.TextInputControl; +import javafx.scene.control.TextInputDialog; 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; @@ -193,4 +197,31 @@ private CommandResult executeCommand(String commandText) throws CommandException throw e; } } + + /** + * Displays a confirmation dialog with the specified message. + * The dialog prompts the user to confirm an action by typing "y". + * + * @param message The message to display in the confirmation dialog. + * @return {@code true} if the user types "y" to confirm, {@code false} otherwise. + */ + public static boolean showConfirmationDialog(String message) { + TextInputDialog dialog = new TextInputDialog(); + dialog.setTitle("Confirmation"); + dialog.setHeaderText(null); + + // Create a label with wrapping for the confirmation message + Label contentLabel = new Label(message + "\n(\"y\" to confirm or any other key to cancel)"); + contentLabel.setWrapText(true); // Enable text wrapping for long messages + contentLabel.setMaxWidth(400); + + // Get the dialog's content pane and add the label above the existing input box + VBox content = new VBox(contentLabel, dialog.getEditor()); + content.setSpacing(10); + dialog.getDialogPane().setContent(content); + + // Show the dialog and capture the result + Optional result = dialog.showAndWait(); + return result.isPresent() && result.get().trim().equalsIgnoreCase("y"); + } } diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java index 094c42cda82..954201cf1cf 100644 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ b/src/main/java/seedu/address/ui/PersonCard.java @@ -4,9 +4,13 @@ import javafx.fxml.FXML; import javafx.scene.control.Label; +import javafx.scene.image.Image; import javafx.scene.layout.FlowPane; +import javafx.scene.layout.GridPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Region; +import javafx.scene.paint.ImagePattern; +import javafx.scene.shape.Circle; import seedu.address.model.person.Person; /** @@ -39,7 +43,15 @@ public class PersonCard extends UiPart { @FXML private Label email; @FXML + private Label birthday; + @FXML + private Label appointment; + @FXML + private GridPane policies; + @FXML private FlowPane tags; + @FXML + private Circle profilePic; /** * Creates a {@code PersonCode} with the given {@code Person} and index to display. @@ -47,13 +59,31 @@ public class PersonCard extends UiPart { public PersonCard(Person person, int displayedIndex) { super(FXML); this.person = person; + setProfilePic("/images/profile_icon.png"); id.setText(displayedIndex + ". "); name.setText(person.getName().fullName); phone.setText(person.getPhone().value); address.setText(person.getAddress().value); email.setText(person.getEmail().value); + birthday.setText("Birthday: " + person.getBirthday().toString()); + appointment.setText("Next Appointment: " + person.getAppointment().value); + person.getPolicies().forEach(policy -> { + int index = policies.getChildren().size(); + String policyLabel = (index + 1) + ". " + policy; + + policies.add(new Label(policyLabel), 0, index); + }); person.getTags().stream() .sorted(Comparator.comparing(tag -> tag.tagName)) .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); } + + /** + * Sets profile picture with the given image URL. + */ + private void setProfilePic(String imageUrl) { + Image img = new Image(imageUrl); + ImagePattern imgPattern = new ImagePattern(img); + profilePic.setFill(imgPattern); + } } diff --git a/src/main/java/seedu/address/ui/UiManager.java b/src/main/java/seedu/address/ui/UiManager.java index fdf024138bc..216e7f0d560 100644 --- a/src/main/java/seedu/address/ui/UiManager.java +++ b/src/main/java/seedu/address/ui/UiManager.java @@ -65,7 +65,7 @@ void showAlertDialogAndWait(Alert.AlertType type, String title, String headerTex private static void showAlertDialogAndWait(Stage owner, AlertType type, String title, String headerText, String contentText) { final Alert alert = new Alert(type); - alert.getDialogPane().getStylesheets().add("view/DarkTheme.css"); + alert.getDialogPane().getStylesheets().add("view/MainWindow.css"); alert.initOwner(owner); alert.setTitle(title); alert.setHeaderText(headerText); diff --git a/src/main/resources/images/bg_image.jpg b/src/main/resources/images/bg_image.jpg new file mode 100644 index 00000000000..18219ce9986 Binary files /dev/null and b/src/main/resources/images/bg_image.jpg differ diff --git a/src/main/resources/images/profile_icon.png b/src/main/resources/images/profile_icon.png new file mode 100644 index 00000000000..cb8d81087ee Binary files /dev/null and b/src/main/resources/images/profile_icon.png differ diff --git a/src/main/resources/images/x_icon.png b/src/main/resources/images/x_icon.png new file mode 100644 index 00000000000..381cfe28a82 Binary files /dev/null and b/src/main/resources/images/x_icon.png differ diff --git a/src/main/resources/view/CommandBox.fxml b/src/main/resources/view/CommandBox.fxml index 124283a392e..52a21772911 100644 --- a/src/main/resources/view/CommandBox.fxml +++ b/src/main/resources/view/CommandBox.fxml @@ -1,9 +1,10 @@ + - - + + +