diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 6ff220b5196..751e811bad3 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -43,3 +43,5 @@ jobs: uses: codecov/codecov-action@v3 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} +# with: +# fail_ci_if_error: false diff --git a/README.md b/README.md index 13f5c77403f..28d559afaf7 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,22 @@ -[![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/AY2324S2-CS2103T-T15-1/tp/workflows/Java%20CI/badge.svg)](https://github.com/AY2324S2-CS2103T-T15-1/tp/actions) + +[![codecov](https://codecov.io/gh/AY2324S2-CS2103T-T15-1/tp/graph/badge.svg?token=TJTWL1WNJF)](https://codecov.io/gh/AY2324S2-CS2103T-T15-1/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. +# MediCLI: Hospital Patient Management System + +MediCLI is a patient management system designed for hospitals. MediCLI enables the management of patient, doctor, and appointment data. + +## Example usages: +1. Used by hospital clerks to manage hospital records for relevant stake holders. +2. Core functions offered include add, delete, query for patients, doctors and appointments. + +## Context of the project +* The project simulates an ongoing software project for a desktop application (called _MediCLI_) used for managing contact details. + * It is **written in OOP fashion**. It is meant as a project for a SE module to teach basic SE principles. * 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#https://se-education.org/#contributing) for more info. +* For the detailed documentation of this project, see the **[MediCLI Product Website](https://ay2324s2-cs2103t-t15-1.github.io/tp/UserGuide.html)**. + +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 a2951cc709e..c06541939a6 100644 --- a/build.gradle +++ b/build.gradle @@ -66,7 +66,11 @@ dependencies { } shadowJar { - archiveFileName = 'addressbook.jar' + archiveFileName = 'MediCLI.jar' +} + +run { + enableAssertions = true } defaultTasks 'clean', 'test' diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 1c9514e966a..6cb67af27d0 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -9,51 +9,54 @@ You can reach us at the email `seer[at]comp.nus.edu.sg` ## Project team -### John Doe +### Zhiyang Lu - + -[[homepage](http://www.comp.nus.edu.sg/~damithch)] -[[github](https://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[homepage](www.linkedin.com/in/zhiyanglu)] +[[github](https://github.com/alfaloo)] +[[portfolio](https://github.com/alfaloo/CV/blob/main/Zhiyang_Lu_Resume.pdf)] -* Role: Project Advisor +* Role: Deliverables and Deadline / Integration +* Responsibilities: Ensure project deliverables are done on time and in the right format. In charge of versioning of the code, maintaining the code repository, integrating various parts of the software to create a whole. -### Jane Doe +### Cheng-Yu Dong - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] -* Role: Team Lead -* Responsibilities: UI +[[github](https://github.com/officialchengyud)] +[[portfolio](www.linkedin.com/in/dongchengyu)] -### Johnny Doe +* Role: Code Quality / Integration +* Responsibilities: Looks after code quality, ensures adherence to coding standards, etc. In charge of versioning of the code, maintaining the code repository, integrating various parts of the software to create a whole. - +### Lim Jia Wei -[[github](http://github.com/johndoe)] [[portfolio](team/johndoe.md)] + -* Role: Developer -* Responsibilities: Data +[[github](http://github.com/Kappaccinoh)] [[portfolio](https://www.linkedin.com/in/jia-wei-lim-747037181/)] -### Jean Doe +* Role: Scheduling / Testing +* Responsibilities: Ensures the testing of the project is done properly and on time. In charge of defining, assigning, and tracking project tasks. - +### Archit Goswami -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] + -* Role: Developer -* Responsibilities: Dev Ops + Threading +[[github](http://github.com/ararchch)] +[[portfolio](https://www.linkedin.com/in/architgos)] -### James Doe +* Role: Team Lead, Documentation +* Responsibilities: Coordinating with team members, ensuring code is well documented. - +### Eugene Luke Sim Ek Jin -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] + -* Role: Developer -* Responsibilities: UI +[[github](http://github.com/alteqa)] +[[portfolio](https://www.linkedin.com/in/eugene-sim-866677188/)] + +* Role: Documentation, Testing, Intellij / Github Expert +* Responsibilities: Ensures the testing of the project is done properly and on time. Responsible for the quality of various project documents. +* Helps other team member with matters related to Intellij, Github and Sourcetree tool. diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 1b56bb5d31b..8982943b26c 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -9,7 +9,22 @@ title: Developer Guide ## **Acknowledgements** -* {list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +### Dong Cheng-Yu's Acknowledgements +- Used ChatGPT to assist in writing Javadocs for AddDoctorCommandParser, AddDoctorCommand, and DeleteAppointmentCommandParser. + +### Lu Zhiyang's Acknowledgements +- Used ChatGPT to assist in writing Javadocs for AddPatientCommandParser, AddPatientCommand, EditAppointmentCommandParser, EditAppointmentCommand. + +### Lim Jia Wei's Acknowledgements +- Used ChatGPT to assist in writing Javadocs for QueryDoctorAppointmentCommand, QueryPatientAppointmentCommand, QueryDoctorAppointmentCommandParser, QueryPatientAppointmentCommandParser, QueryPatientCommand and QueryPatientCommandParser. +- Used ChatGPT to assist in writing parts of User Guide and Developer Guide and test code. + +### Sim Eugene's Acknowledgements +- Used ChatGPT to assist in writing Javadocs for + EditCommand, EditCommandParser and QueryDoctorCommand + +### Goswami Archit's Acknowledgements +- Used ChatGPT to assist in writing Javadocs for getPersonByNric(), addAppointment(), hasAppointment(), deleteAppointment() methods, and to write some javafx code -------------------------------------------------------------------------------------------------------------------- @@ -23,20 +38,20 @@ Refer to the guide [_Setting up and getting started_](SettingUp.md).
-:bulb: **Tip:** The `.puml` files used to create diagrams in this document `docs/diagrams` folder. Refer to the [_PlantUML Tutorial_ at se-edu/guides](https://se-education.org/guides/tutorials/plantUml.html) to learn how to create and edit diagrams. +:bulb: **Tip:** The `.puml` files used to create diagrams in this document are in the `docs/diagrams` folder. Refer to the [_PlantUML Tutorial_ at se-edu/guides](https://se-education.org/guides/tutorials/plantUml.html) to learn how to create and edit diagrams.
### Architecture -The ***Architecture Diagram*** given above explains the high-level design of the App. +The ***Architecture Diagram*** given above explains the high-level design of MediCLI and the various components. Given below is a quick overview of main components and how they interact with each other. **Main components of the architecture** -**`Main`** (consisting of classes [`Main`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/Main.java) and [`MainApp`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/MainApp.java)) is in charge of the app launch and shut down. +**`Main`** (consisting of classes [`Main`](https://github.com/AY2324S2-CS2103T-T15-1/tp/blob/master/src/main/java/seedu/address/Main.java) and [`MainApp`](https://github.com/AY2324S2-CS2103T-T15-1/tp/blob/master/src/main/java/seedu/address/MainApp.java)) is in charge of the app launch and shut down. * At app launch, it initializes the other components in the correct sequence, and connects them up with each other. * At shut down, it shuts down the other components and invokes cleanup methods where necessary. @@ -51,7 +66,7 @@ The bulk of the app's work is done by the following four components: **How the architecture components interact with each other** -The *Sequence Diagram* below shows how the components interact with each other for the scenario where the user issues the command `delete 1`. +The *Sequence Diagram* below shows how the components interact with each other for the scenario where the user issues the command `addpatient`. @@ -68,28 +83,28 @@ The sections below give more details of each component. ### UI component -The **API** of this component is specified in [`Ui.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/ui/Ui.java) +The **API** of this component is specified in [`Ui.java`](https://github.com/AY2324S2-CS2103T-T15-1/tp/blob/master/src/main/java/seedu/address/ui/Ui.java) ![Structure of the UI Component](images/UiClassDiagram.png) -The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `PersonListPanel`, `StatusBarFooter` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class which captures the commonalities between classes that represent parts of the visible GUI. +The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `PersonListPanel`, `AppointmentListPanel`, `StatusBarFooter` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class which captures the commonalities between classes that represent parts of the visible GUI. -The `UI` component uses the JavaFx UI framework. The layout of these UI parts are defined in matching `.fxml` files that are in the `src/main/resources/view` folder. For example, the layout of the [`MainWindow`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/ui/MainWindow.java) is specified in [`MainWindow.fxml`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/resources/view/MainWindow.fxml) +The `UI` component uses the JavaFx UI framework. The layout of these UI parts are defined in matching `.fxml` files that are in the `src/main/resources/view` folder. For example, the layout of the [`MainWindow`](https://github.com/AY2324S2-CS2103T-T15-1/tp/blob/master/src/main/java/seedu/address/ui/MainWindow.java) is specified in [`MainWindow.fxml`](https://github.com/AY2324S2-CS2103T-T15-1/tp/blob/master/src/main/resources/view/MainWindow.fxml) The `UI` component, * executes user commands using the `Logic` component. * listens for changes to `Model` data so that the UI can be updated with the modified data. * keeps a reference to the `Logic` component, because the `UI` relies on the `Logic` to execute commands. -* depends on some classes in the `Model` component, as it displays `Person` object residing in the `Model`. +* depends on some classes in the `Model` component, as it displays `Person` (`Doctor` or `Patient`) and `Appointment` objects residing in the `Model`. ### Logic component -**API** : [`Logic.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/logic/Logic.java) +**API** : [`Logic.java`](https://github.com/AY2324S2-CS2103T-T15-1/tp/tree/master/src/main/java/seedu/address/logic/Logic.java) 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. @@ -100,9 +115,9 @@ The sequence diagram below illustrates the interactions within the `Logic` compo How the `Logic` component works: -1. When `Logic` is called upon to execute a command, it is passed to an `AddressBookParser` object which in turn creates a parser that matches the command (e.g., `DeleteCommandParser`) and uses it to parse the command. -1. This results in a `Command` object (more precisely, an object of one of its subclasses e.g., `DeleteCommand`) which is executed by the `LogicManager`. -1. The command can communicate with the `Model` when it is executed (e.g. to delete a person).
+1. When `Logic` is called upon to execute a command, it is passed to an `AddressBookParser` object which in turn creates a parser that matches the command (e.g., `AddDoctorCommandParser`) and uses it to parse the command. +1. This results in a `Command` object (more precisely, an object of one of its subclasses e.g., `AddDoctorCommand`) which is executed by the `LogicManager`. +1. The command can communicate with the `Model` when it is executed (e.g. to add a doctor).
Note that although this is shown as a single step in the diagram above (for simplicity), in the code it can take several interactions (between the command object and the `Model`) to achieve. 1. The result of the command execution is encapsulated as a `CommandResult` object which is returned back from `Logic`. @@ -111,29 +126,21 @@ Here are the other classes in `Logic` (omitted from the class diagram above) tha 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. +* 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., `AddPatientCommandParser`) which uses the other classes shown above to parse the user command and create a `XYZCommand` object (e.g., `AddPatientCommand`) which the `AddressBookParser` returns back as a `Command` object. +* All `XYZCommandParser` classes (e.g., `AddPatientCommandParser`, `DeleteCommandParser`, ...) inherit from the `Parser` interface so that they can be treated similarly where possible e.g, during testing. ### Model component **API** : [`Model.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/model/Model.java) - The `Model` component, -* stores the address book data i.e., all `Person` objects (which are contained in a `UniquePersonList` object). -* stores the currently 'selected' `Person` objects (e.g., results of a search query) as a separate _filtered_ list which is exposed to outsiders as an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change. +* stores the address book data i.e., all `Person` derivative objects (which are contained in a `UniquePersonList` object) and +* all `Appointment` objects (which are contained in a `UniqueAppointmentList` object) stores the currently 'selected' `Person` objects (e.g., results of a search query, either a `Patient` or `Doctor` instance) and `Appointment` object (e.g results of an query) as a separate _filtered_ list which is exposed to outsiders as an unmodifiable `ObservableList` and `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change. * stores a `UserPref` object that represents the user’s preferences. This is exposed to the outside as a `ReadOnlyUserPref` objects. * does not depend on any of the other three components (as the `Model` represents data entities of the domain, they should make sense on their own without depending on other components) -
:information_source: **Note:** An alternative (arguably, a more OOP) model is given below. It has a `Tag` list in the `AddressBook`, which `Person` references. This allows `AddressBook` to only require one `Tag` object per unique tag, instead of each `Person` needing their own `Tag` objects.
- - - -
- - ### Storage component **API** : [`Storage.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/storage/Storage.java) @@ -143,7 +150,7 @@ The `Model` component, The `Storage` component, * can save both address book data and user preference data in JSON format, and read them back into corresponding objects. * inherits from both `AddressBookStorage` and `UserPrefStorage`, which means it can be treated as either one (if only the functionality of only one is needed). -* depends on some classes in the `Model` component (because the `Storage` component's job is to save/retrieve objects that belong to the `Model`) +* depends on some classes in the `Model` component (because the `Storage` component's job is to save/retrieve objects that belong to the `Model` such as `Appointment` and `Patient`) ### Common classes @@ -153,95 +160,302 @@ Classes used by multiple components are in the `seedu.addressbook.commons` packa ## **Implementation** -This section describes some noteworthy details on how certain features are implemented. +This section describes some noteworthy details on how certain features are implemented. + +The section is structured as follows: +- We first dive into the implementation of the `Appointment` - a special entity type that MediCLI tracks. +- We then explore what happens when the `execute` method of different types of `Commands` is called. +- Finally, we look at how MediCLI deals with incorrect user inputs for operations on both `Persons` and `Appointments` + +Through this, we hope to give you a thorough overview of how MediCLI's main features are implemented. + +### Understanding `Appointments`: A new entity type +MediCLI offers support for appointments, represented by the `Appointment` class on top of existing support for Doctors and Patients. +At the crux of it, the `Appointment` class aims to reflect the essence of a medical appointment in real life, which involves a doctor and a patient and takes place at a specific time. + +The class diagram below displays the structure of the `Appointment` class. + + +As visible, the `Appointment` class contains references to the following classes: +- **`Nric`**: a doctor's & a patient's NRIC number +- **`AppointmentDateTime`**: Date and time of the appointment + +The appointment class must have reference to exactly 2 `Nric` objects and 1 `AppointmentDateTime` object. + +Below is an object diagram demonstrating a possible appointment object. + + +In the object diagram you see that two instances of the Nric class have been instantiated, one as `doctorNric`, and one as `patientNric`. This of course is along with the `appointmentDateTime`. + +An instance of the `Appointment` class can only be created if the date & time of the appointment is >= the current date and time. This is enforced through the `isValidAppointmentDateTime` method in the `Appointment` class. + +#### Context and thought process behind implementation: +Implementing `Appointments` naturally involved many design decisions, and here we have attempted to outline the thought process behind our current implementation: +* One key focus of the `Appointment` implementation was to keep it as similar to the implementation of `Patients` and `Doctors`. +* The idea is that at the end of the day, the `Appointment` is simply another type of entry being tracked. +* Nevertheless, it is natural that both in the UI and backend, we would want to differentiate the `Appointment` entries from the `Patient`/`Doctor` entries to ensure that the system is more flexible and easy to expand on in the future. +* Hence, while similar in terms of the functionality, a lot of the infrastructure to handle `Appointments` was built parallel to the one for `Patient`/`Doctor` entries. + * For instance, there is a separate `UniqueAppointmentList` class for storing and manipulating `Appointments` that functions very similar to the equivalent list for `Patient`/`Doctor` entries (`UniquePersonList`). + +#### Implementation and justification: +* Based on the thought process, the approach taken was to ensure MediCLI had the same way of handling `Appointments` and `Patients`/`Doctors`. +* The overall structure including how `Appointments` are stored, managed etc. is largely similar to support debugging and improve readability and comprehension. +* In other words, if you understand how MediCLI manages `Patients`/`Doctors`, you will also understand how it manages `Appointments`. +* Some differences are however inevitable and have been listed below: + * `Appointment` objects include `doctorNric`, `patientNric` as attributes. A `Doctor` and `Patient` with the corresponding NRIC number must already exist before the `Appointment` was created. + * `Appointments` are stored in a separate list in the backend, called the `UniqueAppointmentList`, to allow for different operations and flexibility down the line. + * In terms of the UI, `Appointments` appear in a separate column to ensure that the user is able to clearly distinguish between them. + + +#### Alternatives considered +* One key alternative we looked at was storing `Appointment` objects with `Patient` and `Doctor` objects as part of the same list i.e. `UniquePersonList`. +* This would mean changing the `Person` class to a different one such as `Entry` and have all three of `Patient` , `Doctor` and `Appointment` extend from the `Entry` class. +* We decided against this because we thought that it was not the most OOP friendly solution and would not allow for flexibility down the line. + * Eg: what if we wanted to add a feature that showed all `Appointments` for a set of `Patients` between a set of dates? Having them in the same list would be unintuitive and make the filtration and display quite cumbersome. +* Furthermore, it might get confusing for the user if everything was dumped into the same list for them to sieve through. Perhaps the user was only concerned with looking up `Patients` in which case the `Appointments` would simply be added clutter. +* The increased level of integration would also be problems for implementation and testing as existing functionality would have to be adapated, exposing the system to more risks and potential for bugs. + * Eg: the class in question would have to change from `Person` to `Entry` in a number of different places. + +### Understanding how MediCLI executes different types of commands +MediCLI currently supports four main types of functionality, namely: add, edit, query, and delete. Each of these functionalities are supported for both people (patient & doctor) and appointments through different command words. +However, different commands of the same functionality are analougous to each other to enhance readablity and expandability. +Furthermore, all of the commands extend the main `Command` class, thus the primary difference between each of them is within their `execute` methods. For their general implementation, please refer the "Logic component" section above. +This section will explore the implemention of the `execute` methods for each of the four functionalities. + +#### Add Functionality +Note: Add `patient` and `doctor` has been grouped together as they are very similar in implementation. +This reduces repetition of information and increases clarity. + +Adds a new `Patient` or `Doctor` entry by indicating their `NRIC`, `Name`, `DoB`, and `Phone`. +This command is implemented through the `AddPatientCommand` for patient and `AddDoctorCommand` for doctor class which both extend the `Command` class. + +Add command execution sequence: +* Step 1. The `execute` method of the `AddPatientCommand` is called. +* Step 2. The method calls the `hasPerson` method of `Model` to check if there are any duplicate patients. + * If there is a duplicate person, the method throws `CommandException` and calls the `log` method of `logger` to log the incident. +* Step 3. The `addPerson` method of `Model` is then called and the control is passed back to the `execute` method. +* Step 4. The `log` method of `logger` is then called to log the successful command execution. +* Step 5. A new `CommandResult` object with the success message is then created and returned by `execute`. + +This is the sequence of command execution for `execute` in `AddPatientCommand`, however `AddDoctorCommand` and `AddAppointmentCommand` follow similar design patterns within the execute command. + + +#### Edit Functionality + +Edits a `doctor` or `patient` entry by indicating their `Index`. +This command is implemented through the `EditCommand` class which extends the `Command` class. + +This is the sequence of command execution for `execute` in `EditCommand`, however `EditAppointmentCommand` follow a similar design pattern within the `execute` command. +* Step 1. The `execute` method of the `EditCommand` is called. +* Step 2. The method calls the `getFilteredPersonList` method of `Model` and returns the list. +* Step 3. The command checks whether the index of the command is valid by comparing the value returned by the `getZeroBased` method of `index` to the size of the list. + * If the index is invalid, the command throws `CommandException`. +* Step 4. The `createEditedPerson` method is called and returns a new edited person. +* Step 5. The method calls the `hasPerson` method of `Model` to check if there are any duplicate persons. + * If there are duplicates, the command throws `CommandException`. +* Step 6. The `setPerson` method of `Model` is called and control is then passed back to the `execute` method. +* Step 7. The `updateFilteredPersonList` method of `Model` is called to update the list. +* Step 8. A new `CommandResult` with the success message is then created and returned by `execute`. + +The sequence diagram below demonstrates the command execution steps for `execute` method in `EditCommand`. + + + +#### Querying Functionality +This section describes the general sequence for commands that query entities. MediCLI has 5 different commands that serve this function: `find`, `patient`, `doctor`, `apptforpatient` and `apptfordoctor`. +Although this section describes only the `patient` command, each of the other commands, while lined with different predicates and have different requirements for their parameters, possesses similar implementation. Hence, the flow of method calls between classes are generally similar, and all 5 commands that query entities are described here together in one section. + +* Step 1. The `execute()` method is called in an instance of `QueryPatientCommand`. +* Step 2. The instance of `QueryPatientCommand` calls the `updateFilteredPersonList()` method with the `PatientContainsKeywordsPredicate` in an instance of the `Model` class, which filters out entries and returns only patients that match the keywords entered. Note that the other commands listed here will have their respective `predicate` requirements and implementations. +* Step 3. Control is passed to an instance of the `Logger` class, which calls the method `log()` to log a "Success" message. +* Step 4. The instance of `QueryPatientCommand` calls the constructor method of the `CommandResult` class, which returns the final result. +* Step 5. Control is returned to the caller with the final result. + +The sequence diagram below describes the interaction between the various components during the `execute` method of the `patient` command, which uses the `QueryPatientCommand` on the backend. + + + +Why is this implemented this way? +1. All query command closely resembles the structure of the `find` command. Each of the commands here have either stricter (i.e have stricter parameter requirements e.g `apptforpatient` and `apptfordoctor`) or looser (i.e searching for more fields e.g `patient` and `doctor`) predicates, but all generally have the same flow of control between the `Command`, `Logger` and `Model` classes. +2. Conceptually, each of the 5 commands listed here are searching for different entities, and are hence split into separate commands despite having very similar structures. + +Alternative implementations considered +1. The behaviour of `patient` and `doctor`, `apptforpatient` and `apptfordoctor` have similar requirements in terms of parameters, with the main difference being either `patient` or `doctor` typed classes. We might consider combining each pair into one command, and using flags to distinguish the desired class (e.g a -doctor or -patient flag to indicate we wish to search for only `doctor` and `patient` entries respectively) so as to avoid the need to create additional commands. However, we felt that at the time of implementation, separating the commands would be a cleaner strategy, and combining methods might overcomplicate the implementation. +2. Even if we did proceed with combining, the combined command was to be overloaded with flags, we foresaw that the creation of distinct commands to fit the flags parsed were unavoidable. As such, it was prudent to start with the implementation of the distinct commands first, and leave the possibility of combining as a later increment. + +#### Delete Functionality + +Deletes a `doctor` or `patient` entry by indicating their `Index`. +This command is implemented through the `DeleteCommand` class which extends the `Command` class. + +* Step 1. The `execute` method of the `DeleteCommand` is called. +* Step 2. The `getFilteredPersonList` method of `Model` is called and finds its length. +* Step 3. The `getZeroBased` method of `Index` is called to convert the provided index to its zero-based equivalent. +* Step 4. The provided index is checked against the length of the current list of people. + * If the provided index is out of bounds, a `CommandException` is thrown. +* Step 4. The `deletePerson` method of `Model` is called to remove the designated person from the list of people. +* Step 5. An instance of `CommandResult` is created with a success message for the execution of `DeleteCommand`. +* Step 6. The instance of `CommandResult` is returned. + +The sequence diagram below closely describes the interaction between the various components during the `execute` method of `DeleteCommand`. + + + +### Incorrect user input handling process +In this section, we will use the _add_ commands for `patients`/`doctors` and `appointments` to demonstrate how MediCLI handles incorrect inputs from the user. + +#### Incorrect input handling process for commands that involve operations on the `Person` class +The activity diagram below demonstrates this incorrect input handling process of a command that operates on the `Person` class. +Specifically, we look at the process of adding a `Person` to MediCLI i.e. `addpatient` or `adddoctor` commands, potential mistakes that the user might make, and how they are handled by MediCLI. + + + +As visible, an incorrect input by the user can result in the following types of errors depending on the type of mistake: +- **Command word is incorrect**: informs the user that command word is unowkown. +- **Missing required field arguments**: informs the user of invalid command format. +- **Invalid field arguments**: highlights to the user that invalid field arguments were provided. +- **`Patient`/`Doctor` with corresponding attributes already exists in the system**: informs the user that `Patient`/`Doctor` already exists in the system. + + +#### Incorrect user input handling process for the `Appointment` class +The activity diagram below demonstrates this incorrect input handling process of a command that operates on the `Appointment` class. +Specifically, we look at the process of adding an `Appointment` to MediCLI i.e. `addappt` command, potential mistakes that the user might make, and how they are handled by MediCLI. + + + +As visible, an incorrect input by the user can result in the following types of errors depending on the type of mistake: +- **Command word is incorrect**: informs the user that command word is unowkown +- **Missing required field arguments**: informs the user of invalid command format +- **Invalid field arguments**: highlights to the user that invalid field arguments were provided +- **`Patient`/`Doctor` involved in the `Appointment` does not exist in MediCLI**: informs the user that `Patient`/`Doctor` involved are not registered +- **Provided date & time of the `Appointment` is in the past**: informs the user that appointments cannot be scheduled in the past. +- **`Appointment` with corresponding attributes already exists in MediCLI**: informs the user that `Appointment` already exists ### \[Proposed\] Undo/redo feature + #### Proposed 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: -* `VersionedAddressBook#commit()` — Saves the current address book state in its history. -* `VersionedAddressBook#undo()` — Restores the previous address book state from its history. -* `VersionedAddressBook#redo()` — Restores a previously undone address book state from its history. +The proposed undo/redo mechanism is facilitated by `VersionedMediCLI`. It extends `MediCLI` with an undo/redo history, stored internally as an `MediCLIStateList` and `currentStatePointer`. Additionally, it implements the following operations: + + +* `VersionedMediCLI#commit()` — Saves the current MediCLI state in its history. + +* `VersionedMediCLI#undo()` — Restores the previous MediCLI state from its history. + +* `VersionedMediCLI#redo()` — Restores a previously undone MediCLI state from its history. + + +These operations are exposed in the `Model` interface as `Model#commitMediCLI()`, `Model#undoMediCLI()` and `Model#redoMediCLI()` respectively. -These operations are exposed in the `Model` interface as `Model#commitAddressBook()`, `Model#undoAddressBook()` and `Model#redoAddressBook()` respectively. Given below is an example usage scenario and how the undo/redo mechanism behaves at each step. -Step 1. The user launches the application for the first time. The `VersionedAddressBook` will be initialized with the initial address book state, and the `currentStatePointer` pointing to that single address book state. + +Step 1. The user launches the application for the first time. The `VersionedMediCLI` will be initialized with the initial MediCLI state, and the `currentStatePointer` pointing to that single MediCLI state. + ![UndoRedoState0](images/UndoRedoState0.png) -Step 2. The user executes `delete 5` command to delete the 5th person in the address book. The `delete` command calls `Model#commitAddressBook()`, causing the modified state of the address book after the `delete 5` command executes to be saved in the `addressBookStateList`, and the `currentStatePointer` is shifted to the newly inserted address book state. + +Step 2. The user executes `delete 5` command to delete the 5th person in the MediCLI. The `delete` command calls `Model#commitMediCLI()`, causing the modified state of the MediCLI after the `delete 5` command executes to be saved in the `MediCLIStateList`, and the `currentStatePointer` is shifted to the newly inserted address book state. + ![UndoRedoState1](images/UndoRedoState1.png) -Step 3. The user executes `add n/David …​` to add a new person. The `add` command also calls `Model#commitAddressBook()`, causing another modified address book state to be saved into the `addressBookStateList`. + +Step 3. The user executes `addpatient i/S1234567A n/John Doe d/2003-01-30 p/98765432` to add a new person. The `add` command also calls `Model#commitMediCLI()`, causing another modified MediCLI state to be saved into the `MediCLIStateList`. + ![UndoRedoState2](images/UndoRedoState2.png) -
:information_source: **Note:** If a command fails its execution, it will not call `Model#commitAddressBook()`, so the address book state will not be saved into the `addressBookStateList`. + +
:information_source: **Note:** If a command fails its execution, it will not call `Model#commitMediCLI()`, so the MediCLI state will not be saved into the `MediCLIStateList`. +
-Step 4. The user now decides that adding the person was a mistake, and decides to undo that action by executing the `undo` command. The `undo` command will call `Model#undoAddressBook()`, which will shift the `currentStatePointer` once to the left, pointing it to the previous address book state, and restores the address book to that state. + +Step 4. The user now decides that adding the person was a mistake, and decides to undo that action by executing the `undo` command. The `undo` command will call `Model#undoMediCLI()`, which will shift the `currentStatePointer` once to the left, pointing it to the previous MediCLI state, and restores the MediCLI to that state. + ![UndoRedoState3](images/UndoRedoState3.png) -
:information_source: **Note:** If the `currentStatePointer` is at index 0, pointing to the initial AddressBook state, then there are no previous AddressBook states to restore. The `undo` command uses `Model#canUndoAddressBook()` to check if this is the case. If so, it will return an error to the user rather + +
:information_source: **Note:** If the `currentStatePointer` is at index 0, pointing to the initial MediCLI state, then there are no previous MediCLI states to restore. The `undo` command uses `Model#canUndoMediCLI()` to check if this is the case. If so, it will return an error to the user rather + than attempting to perform the undo. +
+ The following sequence diagram shows how an undo operation goes through the `Logic` component: + ![UndoSequenceDiagram](images/UndoSequenceDiagram-Logic.png) +
:information_source: **Note:** The lifeline for `UndoCommand` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. +
+ Similarly, how an undo operation goes through the `Model` component is shown below: + ![UndoSequenceDiagram](images/UndoSequenceDiagram-Model.png) -The `redo` command does the opposite — it calls `Model#redoAddressBook()`, which shifts the `currentStatePointer` once to the right, pointing to the previously undone state, and restores the address book to that state. -
:information_source: **Note:** If the `currentStatePointer` is at index `addressBookStateList.size() - 1`, pointing to the latest address book state, then there are no undone AddressBook states to restore. The `redo` command uses `Model#canRedoAddressBook()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the redo. +The `redo` command does the opposite — it calls `Model#redoMediCLI()`, which shifts the `currentStatePointer` once to the right, pointing to the previously undone state, and restores the MediCLI to that state. + + +
:information_source: **Note:** If the `currentStatePointer` is at index `MediCLIStateList.size() - 1`, pointing to the latest MediCLI state, then there are no undone MediCLI states to restore. The `redo` command uses `Model#canRedoMediCLI()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the redo. +
-Step 5. The user then decides to execute the command `list`. Commands that do not modify the address book, such as `list`, will usually not call `Model#commitAddressBook()`, `Model#undoAddressBook()` or `Model#redoAddressBook()`. Thus, the `addressBookStateList` remains unchanged. + +Step 5. The user then decides to execute the command `list`. Commands that do not modify the MediCLI, such as `list`, will usually not call `Model#commitMediCLI()`, `Model#undoMediCLI()` or `Model#redoMediCLI()`. Thus, the `MediCLIStateList` remains unchanged. + ![UndoRedoState4](images/UndoRedoState4.png) -Step 6. The user executes `clear`, which calls `Model#commitAddressBook()`. Since the `currentStatePointer` is not pointing at the end of the `addressBookStateList`, all address book states after the `currentStatePointer` will be purged. Reason: It no longer makes sense to redo the `add n/David …​` command. This is the behavior that most modern desktop applications follow. + +Step 6. The user executes `clear`, which calls `Model#commitMediCLI()`. Since the `currentStatePointer` is not pointing at the end of the `MediCLIStateList`, all MediCLI states after the `currentStatePointer` will be purged. Reason: It no longer makes sense to redo the `addpatient i/S1234567A n/John Doe d/2003-01-30 p/98765432` command. This is the behavior that most modern desktop applications follow. + ![UndoRedoState5](images/UndoRedoState5.png) + The following activity diagram summarizes what happens when a user executes a new command: + + #### Design considerations: + **Aspect: How undo & redo executes:** -* **Alternative 1 (current choice):** Saves the entire address book. + +* **Alternative 1 (current choice):** Saves the entire MediCLI. + * Pros: Easy to implement. + * Cons: May have performance issues in terms of memory usage. + * **Alternative 2:** 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}_ + itself. -### \[Proposed\] Data archiving + * Pros: Will use less memory (e.g. for `delete`, just save the person being deleted). -_{Explain here how the data archiving feature will be implemented}_ + * Cons: We must ensure that the implementation of each individual command are correct. -------------------------------------------------------------------------------------------------------------------- @@ -262,121 +476,673 @@ _{Explain here how the data archiving feature will be implemented}_ **Target user profile**: -* has a need to manage a significant number of contacts +* hospital clerks who deal with hospital related registration/administrative/management tasks +* has a need to manage a significant number of client details (patients/doctors/appointments) +* deals with many real time live updates, some being time-critical * prefer desktop apps over other types -* can type fast +* can type fast and accurately * 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**: + +MediCLI is a hospital management system that offers clerks the ability to execute querying/updating/creating/deleting commands faster than a typical mouse/GUI driven hospital management system, significantly speeding up the data entry/update/retrieval process. + +As clerks likely execute many such commands each day, this offers a significant amount of time savings when considered on the whole. While this would be beneficial in any situation, this is especially important in a hospital setting where the stakes are much higher, and staff are required to perform at a very high level of efficiency with limited room for error ### 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…​ | +|-----------|--------------------------------------------|--------------------------------------------|-------------------------------------------------------------------| +| `* * *` | hospital clerk | add patients | handle incoming patients when handling emergency call-ins | +| `* * *` | hospital clerk | delete patients | remove old patients to prevent clogging of system | +| `* * *` | hospital clerk | add doctors | register new doctors as they get hired | +| `* * *` | hospital clerk | delete doctors | remove previous doctors that have left the hospital | +| `* * *` | hospital clerk | create appointments | arrange a meeting time between a doctor and a patient | +| `* * *` | hospital clerk | delete appointments | remove a meeting time if either party becomes unavailable | +| `* * *` | hospital clerk | query patient by name | retrieve their relevant information | +| `* * *` | hospital clerk | query doctor by name | retrieve their relevant information | +| `* *` | hospital clerk | query appointment by patient | look up what appointments a patient has to attend | +| `* *` | hospital clerk | query appointment by doctor | look up what appointments a doctor has to service | +| `*` | hospital clerk | query patient by other fields | retrieve patient information through other fields if they call-in | +| `*` | hospital clerk | find available timings to book appointment | schedule a time that suits both the patient and doctor | -*{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 `MediCLI` and the **Actor** is the `hospital clerk`, unless specified otherwise) -**Use case: Delete a person** +(Note: For all use cases, if you enter the command format wrongly, MediCLI will show an error message and return to step 1.) + +**Use case: Add a patient** **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. Hospital clerk needs to add a patient +2. Hospital clerk enters patient data +3. MediCLI adds the patient into database - Use case ends. +Use case ends. + +**Extensions** + +* 2a. The entered patient data is not in the correct format + * 2a1. MediCLI shows an error message. + + Use case resumes at step 1. + +* 2b. The entered patient is already in the database + * 2b1. MediCLI shows an error message. + + Use case resumes at step 1. + +**Use case: Delete a patient** + + +**MSS** + +1. Hospital clerk requests to list persons +2. MediCLI shows a list of persons +3. Hospital clerk requests to delete a specific patient in the list +4. MediCLI deletes the patient + +Use case ends. **Extensions** * 2a. The list is empty. - Use case ends. + Use case resumes at step 1. + +* 3a. The given patient index is invalid. + * 3a1. MediCLI shows an error message. + + Use case resumes at step 1. + +**Use case: Create an appointment** + +**MSS** + +1. Hospital clerk needs to create appointment between doctor and patient +2. Hospital clerk enters doctor and patient details +3. MediCLI creates the appointment + + Use case ends. + + +**Extensions** + +* 2a. The entered doctor or patient detail is invalid. + * 2a1. MediCLI will show an error message. + + Use case resumes at step 1. + +* 2b. The entered appointment date is invalid + * 2b1. MediCLI will show an error message. + + Use case resumes at step 1. + +**Use case: Delete an appointment** -* 3a. The given index is invalid. +**MSS** + +1. Hospital clerk needs to delete appointment between doctor and patient +2. Hospital clerk enters appointment index +3. MediCLI deletes the appointment + + Use case ends. + +**Extensions** - * 3a1. AddressBook shows an error message. +* 2a. The entered appointment index is invalid. + * 2a1. MediCLI shows an error message. - Use case resumes at step 2. + Use case resumes at step 1. + +**Use case: Query patient by name** + +**MSS** + +1. Hospital clerk needs to search for patient +2. Hospital clerk enters patient name +3. MediCLI lists patients with supplied name + +Use case ends. + +**Extensions** + +* 3a. The list is empty + + Use case resumes at step 1. + +**Use case: Query appointments by patient** + +**MSS** + +1. Hospital clerk needs to search for appointment by patient +2. Hospital clerk enters patient name +3. MediCLI lists relevant appointments + +Use case ends. + +**Extensions** + +* 3a. The list is empty + + Use case resumes at step 1. + +**Use case: Query appointments by doctor** + +**MSS** + +1. Hospital clerk needs to search for appointment by doctor +2. Hospital clerk enters doctor name +3. MediCLI lists relevant appointments + +Use case ends. + +**Extensions** -*{More to be added}* +* 3a. The list is empty + + Use case resumes at step 1. ### Non-Functional Requirements 1. Should work on any _mainstream OS_ as long as it has Java `11` or above installed. -2. Should be able to hold up to 1000 persons without a noticeable sluggishness in performance for typical usage. +2. Should be able to hold up to 1000 medical staff without a noticeable sluggishness in performance for typical usage. 3. A user with above average typing speed for regular English text (i.e. not code, not system admin commands) should be able to accomplish most of the tasks faster using commands than using the mouse. - -*{More to be added}* +4. MediCLI should be easy to integrate with existing medical database systems so that staff can immediately switch to the new app. +5. Comprehensive documentation should be provided, including user guides, command references, and troubleshooting resources. +6. MediCLI should not need an internet connection to run. +7. The GUI for MediCLI should be well organised, purpose oriented and easy to understand for users of any knowledge level. +8. MediCLI should handle the majority of common user errors and give the users suggestions to mitigate these errors. +9. MediCLI does not support concurrent usage between multiple users. +10. MediCLI does not support languages other than English. ### Glossary -* **Mainstream OS**: Windows, Linux, Unix, MacOS -* **Private contact detail**: A contact detail that is not meant to be shared with others +* **Private contact detail**: A contact detail that is not meant to be shared with others. +* **CLI**: Command Line Interface, a way of interacting with a computer program where the user enters commands into a terminal or command prompt. +* **GUI**: Graphical User Interface, a way of interacting with a computer program using graphical elements such as windows, buttons, and menus. +* **JSON**: JSON: JavaScript Object Notation, a lightweight data interchange format used to store and exchange data. +* **API**: Application Programming Interface, a set of rules and protocols for building and interacting with software applications. +* **UI**: User Interface, the visual part of a computer program that allows users to interact with it. +* **XML**: Extensible Markup Language, a markup language that defines rules for encoding documents in a format that is both human-readable and machine-readable. +* **MSS**: Main Success Scenario, the primary flow of events in a use case that leads to the desired outcome. -------------------------------------------------------------------------------------------------------------------- -## **Appendix: Instructions for manual testing** +## **Appendix: Instructions for Manual Testing** -Given below are instructions to test the app manually. +This section provides guidance for testers to navigate through the user-testable features of MediCLI. It includes important test inputs along with the expected test results that can be copied and pasted into the app for testing purposes. -
:information_source: **Note:** These instructions only provide a starting point for testers to work on; -testers are expected to do more *exploratory* testing. +
:information_source: **INFO**: These instructions only provide a starting point for testers to work on. Testers are expected to do more *exploratory* testing.
-
### Launch and shutdown +#### Initial launch +Steps: +1. Download the jar file and copy into an empty folder. +2. Double-click the jar file. + +Expected Outcome: +* Shows the GUI with a set of sample contacts. +* The window size may not be optimum. + +#### Saving window preferences +Steps: +1. Resize the window to an optimum size. +2. Move the window to a different location. +3. Close the window. +4. Re-launch the app by double-clicking the jar file.
+ +Expected Outcome: +* The most recent window size and location is retained. + +#### Closing MediCLI +Steps: +1. Execute the `exit` command, or simply close the window. + +Expected Outcome: +* MediCLI closes without any errors. + + +### Person Related Commands +#### Adding a Patient : `addpatient` + +Steps: +1. Execute the `addpatient` command with valid NRIC, name, DOB, and phone number. +2. Verify that the patient is successfully added to the system. +3. Try adding a patient with an existing NRIC and verify that the command fails. +4. Attempt to add a patient with invalid or missing fields and confirm appropriate error handling. + +Valid Inputs: +* Valid NRIC, name, DOB, and phone number. +* Example: `addpatient i/S1234567A n/David Li d/2000-01-01 p/98765432` + +Expected Outcome: +* Patient is successfully added to the system. + +Invalid Inputs: +* Missing or invalid fields (e.g. invalid NRIC format, missing name). +* Example: `addpatient i/1234567A n/ d/2000-01-01 p/12345678` + +Expected Error: +* Command fails with the 'Invalid command format' error message indicating the required command format. + +#### Adding a Doctor : `adddoctor` +Steps: +1. Use the `adddoctor` command with valid NRIC, name, DOB, and phone number. +2. Verify that the doctor is added to the system. +3. Test adding a doctor with an existing NRIC and check if the command fails. +4. Test adding a doctor with invalid or missing fields and observe error handling. + +Valid Inputs: +* Valid NRIC, name, DOB, and phone number. +* Example: `adddoctor i/S1234567A n/Dr. Jane Smith d/1975-05-15 p/98765432` + +Expected Outcome: +* Doctor is successfully added to the system. + +Invalid Inputs: +* Missing or invalid fields (e.g. invalid phone number). +* Example: `adddoctor i/S1234567A n/Dr. Jane Smith d/1975-05-15 p/1234567` + +Expected Error: +* Command fails with the 'Invalid command format' error message indicating the required command format. + +#### Editing a Person : `edit` +Steps: +1. Execute the `edit` command with the index of an existing person. +2. Update one or more fields (NRIC, name, DOB, or phone number) and confirm changes. +3. Test editing without changing any values and ensure it's handled correctly. +4. Try editing with an invalid index and verify error handling. + +Valid Inputs: +* Index of an existing person and valid fields to update. +* Example: `edit 1 n/John Smith` + +Expected Outcome: +* Person's name has been successfully updated to John Smith. + +Invalid Inputs: +* Invalid index or missing fields. +* Example: `edit 0 n/John Smith` +* Example: `edit 1` + +Expected Error: +* Command fails with the appropriate error message indicating the invalid index or missing fields. + +#### Finding Persons by Name : `find` +Steps: +1. Use the `find` command with keywords to search for both patients and doctors. +2. Ensure the command returns expected results based on the provided keywords. +3. Try different combinations of keywords and verify the search results. + +Valid Inputs: +* Keywords matching existing persons' names. +* Example: `find John` + +Expected Outcome: +* List of persons matching the keywords is displayed. + +Invalid Inputs: +* No matching keywords or invalid syntax. +* Example: `find 123` + +Expected Error: +* Results display indicates '0 persons listed', and the Persons panel is empty. + +#### Finding Persons by all Fields : `patient`, `doctor` +Steps: +1. Use the `patient` command with keywords to search for patients only. +2. Ensure the command returns expected results based on the provided keywords. +3. Similarly, use the `doctor` command to search for doctors. +4. Try different combinations of keywords and verify the search results. + +Valid Inputs: +* Keywords exactly matching or substring matching existing persons' nric, name, date of birth, or phone number. +* Example: `patient S1234` +* Example: `patient Doe` +* Example: `doctor 30 Jan` +* Example: `doctor 98765432` + +Expected Outcome: +* List of patients or doctors exactly matching or substring matching the keywords is displayed. + +Invalid Inputs: +* No matching keywords or invalid syntax. +* Example: `patient` +* Example: `doctor @` + +Expected Error: +* Command fails with the appropriate error message indicating the required command format. +* Or the results display indicates '0 persons listed', and the Persons panel is empty. + +#### Deleting a Person (delete) +Steps: +1. Use the `list` command to display a list of persons. +2. Execute the `delete` command with the index of a person to delete them. +3. Confirm that the person is removed from MediCLI. +4. Verify that associated appointments are also deleted recursively. +5. Test deleting a person with an invalid index and observe error handling. + +Valid Inputs: + * Index of an existing person. + * Example: `delete 1` + +Expected Outcome: + * Person is successfully deleted from the system. + +Invalid Inputs: +* Invalid index. +* Example: `delete 0` + +Expected Error: +* Command fails with appropriate error message indicating the required command format and parameter requirements. + +### Appointment Related Commands +#### Adding an Appointment : `addappt` +Steps: +1. Execute the `addappt` command with valid datetime, doctor's NRIC, and patient's NRIC. +2. Ensure the appointment is successfully added to the system. +3. Test adding an appointment with invalid datetime or NRICs and verify error handling. + +Valid Inputs: +* Valid datetime, doctor's NRIC, and patient's NRIC. +* Example: `addappt ad/2024-08-11 12:00 dn/S1234567A pn/S7654321B` + +Expected Outcome: +* Appointment is successfully added to the system. + +Invalid Inputs: +* Missing or invalid datetime, doctor's NRIC, or patient's NRIC. +* Example: `addappt ad/2024-08-11 dn/S1234567A pn/S7654321B` + +Expected Error: +* Command fails with appropriate error message indicating the required command format. + +#### Editing an Appointment : `editappt` +Steps: +1. Use the editappt command with the index of an existing appointment. +2. Update the datetime of the appointment and confirm changes. +3. Test editing without changing any values and ensure it's handled correctly. +4. Try editing with an invalid index and verify error handling. + +Valid Inputs: +* Index of an existing appointment and valid datetime to update. +* Example: `editappt 1 ad/2024-08-12 14:00` + +Expected Outcome: +* Appointment datetime is successfully updated. + +Invalid Inputs: +* Invalid index or missing datetime. +* Example: `editappt 0 ad/2024-08-12 14:00` + +Expected Error: +* Command fails with appropriate error message indicating the required command format. + +#### Querying Appointments by Patient's NRIC : `apptforpatient` +Steps: +1. Execute the `apptforpatient` command with a patient's exact NRIC. +2. Verify that all appointments involving the specified patient are listed. +3. Test with different patient NRICs and confirm the results. + +Valid Inputs: +* Patient's NRIC. +* Example: `apptforpatient S7654321B` + +Expected Outcome: +* List of appointments involving the specified patient is displayed. + +Invalid Inputs: +* No matching patient NRIC, missing NRIC or invalid NRIC. +* Example: `apptforpatient S1234567A` +* Example: `apptforpatient` +* Example: `apptforpatient S123456` + +Expected Error: +* Command fails with appropriate error message indicating the required command format. +* Or the results display indicates '0 appointments listed', and the appointments panel is empty. + +#### Querying Appointments by Doctor's NRIC : `apptfordoctor` +Steps: +1. Use the `apptfordoctor` command with a doctor's NRIC. +2. Ensure that all appointments involving the specified doctor are listed. +3. Test with different doctor NRICs and verify the results. + +Valid Inputs: +* Doctor's NRIC. +* Example: `apptfordoctor S1234567A` + +Expected Outcome: +* List of appointments involving the specified doctor is displayed. + +Invalid Inputs: +* No matching patient NRIC, missing NRIC or invalid NRIC. +* Example: `apptfordoctor S1234567A` +* Example: `apptfordoctor` +* Example: `apptfordoctor S123456` + +Expected Error: +* Command fails with appropriate error message indicating the required command format. +* Or the results display indicates '0 appointments listed', and the appointments panel is empty. + +#### Deleting an Appointment : `deleteappt` +Steps: +1. Use the `list` command to display a list of appointments. +2. Execute the `deleteappt` command with the index of an appointment to delete it. +3. Confirm that the appointment is removed from the system. +4. Test deleting an appointment with an invalid index and observe error handling. + +Valid Inputs: +* Index of an existing appointment. +* Example: `deleteappt 1` + +Expected Outcome: +* Appointment is successfully deleted from the system. + +Invalid Inputs: +* Invalid index. +* Example: `deleteappt 0` + +Expected Error: +* Command fails with appropriate error message indicating the required command format. + +### Miscellaneous Commands +#### Viewing Help : `help` +Steps: +1. Execute the `help` command and ensure the help message is displayed. +2. Verify that the 'Help' pop up is displayed, and click the 'Copy URL' button. +3. Verify that pasting the URL in your browser leads you to MediCLI's updated User-Guide page. + +Valid Inputs: +* None + +Expected Outcome: +* 'Help' pop up is displayed successfully. + +Invalid Inputs: +* None + +Expected Error: +* None + +#### Listing All Persons and Appointments : `list` +Steps: +1. Use the `list` command to display all persons and appointments. +2. Confirm that the Persons Panel and the Appointments Panel includes all existing patients, doctors, and appointments existing in MediCLI. + +Valid Inputs: +* None + +Expected Outcome: +* All persons and appointments are displayed. + +Invalid Inputs: +* None + +Expected Error: +* None + +#### Clearing All Entries : `clear` +Steps: +1. Execute the `clear` command and confirm if all data is wiped from MediCLI. +2. Ensure there is no remaining data after executing this command. +3. Verify that there is no confirmation prompt and data deletion is immediate. + +Valid Inputs: +* None + +Expected Outcome: +* All data is wiped from MediCLI. +* Results display indicates that 'MediCLI's storage has been cleared!' + +Invalid Inputs: +* None + +Expected Error: +* None + +#### Exiting the Program : `exit` +Steps: +1. Execute the `exit` command and ensure the program exits gracefully. + +Valid Inputs: +* None + +Expected Outcome: +* Program exits without errors. + +Invalid Inputs: +* None + +Expected Error: +* None + +-------------------------------------------------------------------------------------------------------------------- + +## **Appendix: Effort** + +### Difficulty level: +If AB3 was at a difficulty level of 5/10, `MediCLI` reached was at a level of 8/10. This is most significantly because of the following reasons: +- Expansion of `Person` entity type to include `Patient` and `Doctor` entities. +- Inclusion of a completely new `Appointment` entity type. + +The expansion into `Patient` and `Doctor` required us to expand the suite of commands to account for them, as well as modify the backend methods to account for the changes. +However, the `Appointment` class required us to build a completely new parallel infrastructure from UI to the commands to the storage and significantly increased the challenge and difficulty of the project. + +### Challenges faced: +We faced multiple challenges in the project, most significant of which are highlighted below: +- Developing the `Appointments` feature: As a completely new entity type, we had to build this from the ground up while also integrating it the best way we could with the existing AB3 codebase, which was a big challenge. Even simple things like the `addAppointment` command required a large amount of implementation effort. Beyond just the functional code, writing tests for this and making sure that a sufficient portion was tested using automated tests was quite difficult as we had no existing code that we could build off-of, or modify. +- Achieving sufficient test coverage - as we have a fairly large amount of functional LOC written, a natural consequence is that we had to write a lot of test code to ensure that they were sufficiently tested. This was a challenge, especially in the beginning when we were just familiarising ourselves with automated tests. +- UI enhancements: As `javafx` was a completely new framework for everyone in our team, we found it rather difficult to actually make the UI enhancements that we wanted. Particularly we had to make a completely new `card` for appointments and we made changes to the general look and feel to make it more suitable for a hospital, which was a significant step away from the AB3 UI, hence the difficulty. +- Finally, another difficulty we faced was actually considering the edge-cases and various scenarios that may arise from interactions between the different entities we were managing: + - Let's say you have a `Doctor` with multiple `Appointments`, what do you do if the user deletes the `Doctor` from MediCLI? We decided that the most appropriate approach would be to recursively delete all `Appointments` associated with the doctor. + - Similarly, what if you want to add an `Appointment` but the `Patient` involved doesn't exist in the records? We had to implement checks to prevent this. + - Many other similar issues resulting from the interactions of different entities were noted and we found it quite challenging to actually identify and resolve all of them so they didn't become a bug or feature flaw down the line. + +### Effort required: +In the previous sections we have already elaborated a little on the challenges and difficulties and talked about the efforts we put into addressing them. As such, for this section we have focused on quantifying the overall project effort. + +On the whole, the project took us the entire duration that was offered, with most of us writing upwards of 1000 lines of functional code. The `Appointments` feature was naturally the biggest cause of this and it led to a number of issues that were quite effort-intensive to resolve. +On a weekly basis, each of us invested about 8 hours on average in writing code, writing automated tests, documenting and manually testing the feature. + +For team meetings, we would meet up twice a week, one time at the start to distribute work for the week and discuss deliverables, and one time in the end to wrap up the milestone/work for the week and submit the deliverables. Altogether we spent about 3 hours in meetings per week. + +### Achievements: +Some achievements we are particulary proud of are: +- Robustness of `Appointments` feature: We tried to consider all the edge cases and problematic aspects of this and preemptively prevent them to make sure the user has a seamless experience. +- UI improvements: We are quite happy with the way `Appointments` and `Patients`/`Doctors` are integrated into the same window to allow for easy visualisation with minimal use of the mouse. We are also quite proud of the new clean and minimalist interface which we think reflects a hospital setting quite well. +- Variety of commands: We also are happy with the suite of commands that we offer to users. We focused on the most high-impact commands and ensured that our product offers all CRUD capabilities such that it may actually be used in a functional setting for a small clinic/hospital. +- Comprehensive testing: While we do not claim to be bug-free, we did a comprehensive amount of testing including testing our product ourselves manually, through automated tests, and even asking other teams to perform their own UAT on our product to identify pain-points/bugs/feature flaws (that we went on to address). + +-------------------------------------------------------------------------------------------------------------------- + +## **Appendix: Planned Enhancements** + +The MediCLI development team (consisting of 5 members) acknowledges the presense of known feature flaws in our system. +Thus, we have planned the following 10 enhancements to be added in the near future. +Please find them organised into their respective categories. + +### Appointment Functionality Enhancements + +1. Adding an end time to appointments + +Currently, the MediCLI system only stores the date and start time of an appointment. +However, we recognise that in a fast-paced environment like a hospital, it'd be beneficial to also be able to indicate an end time for appointments. +This is so that the doctor can be safely booked by another patient without worrying about potential clashes in appointment timings. +* Updated Command Format - `addappt sdt/STARTDATETIME [edt/ENDDATETIME] dn/DOCTORNRIC pn/PATIENTNRIC` +* Example Input - `addappt sdt/2024-05-20 10:00 edt/2024-05-20 11:00 dn/S1234567A pn/S1234567B` + +2. More robust appointment timing validation. + +Currently, the MediCLI system allows two appointments with the same doctor/patient and date-time to be simultaneously stored in the system. +However, it is clearly impossible for a patient or doctor to attend two different appointments at the same time. +Thus, we plan to implement a more robust appointment validation system to ensure that appointments with clashing or unrealistic timings can not be entered. + +3. Marking old appointments as completed. -1. Initial launch +Even though the MediCLI system does not allow appointments to be made in the future, it nonetheless retains entry of completed appointments. +However, there is currently no visual distinction between future, completed, and missed appointments. This can be rather confusing for the hospital clerks. +Thus, we plan to add a label (just like the patient/doctor labels) in the top right corner of each appointment card to help better distinguish them. +* New Command Format - `markappt index s/STATUS` (`STATUS` can be any one of {`completed`, `missed`}) +* Example Input - `markappt 1 s/completed` +* Example Input - `markappt 2 s/missed` - 1. Download the jar file and copy into an empty folder +### Parameter Checking Enhancements - 1. Double-click the jar file Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum. +4. Accommodate names with symbols and/or special characters. -1. Saving window preferences +The name parameter is currently restricted to just alphabetical characters and white-space. +However, we recognise the existence of names that contain symbols and other special characters. +In the future, we plan to implement a more accommodating constraint that allows UTF-8 characters instead. +This means that names of other languages will be accepted as well. - 1. Resize the window to an optimum size. Move the window to a different location. Close the window. +5. Allow foreign patients/doctors to be added to the system. - 1. Re-launch the app by double-clicking the jar file.
- Expected: The most recent window size and location is retained. +The current constraints for the NRIC and phone number parameters reflect the Singaporean format. +However, we recognise that for foreign users, this can be rather limiting. +Thus, in the future, we plan on introducing more refined parameter checking that allows international NRIC and phone number formats. -1. _{ more test cases …​ }_ +6. Ensure each person being added to the system is unique. -### Deleting a person +While the current MediCLI system already checks to ensure every person added is unique, it is only done by comparing the NRIC of the person. +However, this should not be the only checking condition. Two entries with the same name, date of birth, and/or phone number should also be flagged as non-unique. +Thus, we will devise a more holistic assessment criterion to ensure no duplicates are allowed. -1. Deleting a person while all persons are being shown +### User Interface Enhancements - 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. +7. Refine the user interface when the window size is minimised. - 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. +The current MediCLI system is not particularly flexible when it comes to window sizing. +Users on smaller screens may encounter the issue of scrolling being disabled or labels being truncated if a long name is entered. +In the future, we plan to make the UI more adaptive and friendly to smaller screens. - 1. Test case: `delete 0`
- Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. +8Standardise displayed information. - 1. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
- Expected: Similar to previous. +For certain fields, the MediCLI system simply displays the text exactly as entered by the user. +However, this can introduce inconsistencies in capitalisation (especially with the NRIC field) when displayed in the user interface. +We plan on standardising these fields by automatically capitalising the users' input. -1. _{ more test cases …​ }_ +### Feature Enhancements -### Saving data +9. More advanced search options -1. Dealing with missing/corrupted data files +Currently, the `find`, `patient`, and `doctor` commands return all entries whose details contain any of the given keywords. +However, this implementation is not particularly effective if the user would like to search for a person that matches all the provided keywords exactly +(e.g. when searching for a person by full name). In the future, we plan to add more advanced search options to allow for easy querying of information. +* Updated Command Format - `find t/TYPE KEYWORD`, `patient t/TYPE KEYWORD`, `doctor t/TYPE KEYWORD` (`TYPE` can be any one of {`full`, `partial`}). +* Example Input - `find t/full John Doe` +* Example Input - `doctor t/partial Smith Li` - 1. _{explain how to simulate a missing/corrupted file, and the expected behavior}_ +10. More detailed error messages. -1. _{ more test cases …​ }_ +Some of the current error messages are not the most informative +(e.g. If two patient NRICs are provided when creating an appointment, the system only prompts `This appointment is invalid due to invalid inputs.`). +To decrease the learning curve for our system, we plan to replace all ambiguous error messages with more informative versions, e.g. `Please make sure the NRIC provided belongs to a person of the correct type as indicated by the prefix.`. diff --git a/docs/SettingUp.md b/docs/SettingUp.md index 275445bd551..1f77e4236a3 100644 --- a/docs/SettingUp.md +++ b/docs/SettingUp.md @@ -45,7 +45,7 @@ If you plan to use Intellij IDEA (highly recommended): 1. **Learn the design** - When you are ready to start coding, we recommend that you get some sense of the overall design by reading about [AddressBook’s architecture](DeveloperGuide.md#architecture). + When you are ready to start coding, we recommend that you get some sense of the overall design by reading about [MediCLI’s architecture](DeveloperGuide.md#architecture). 1. **Do the tutorials** These tutorials will help you get acquainted with the codebase. diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 7abd1984218..2a737a7d58e 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -1,153 +1,523 @@ --- layout: page -title: User Guide +title: MediCLI 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. +## Welcome to MediCLI! +Welcome to MediCLI - the solution to all your hospital management needs! +MediCLI is a desktop-based application that streamlines the management of patients, doctors, and appointments within a hospital. By combining the efficiency of a Command Line Interface (CLI) with an intuitive and comprehensive visual display, MediCLI presents itself as a robust solution for hospital clerks and administrators such as yourself. This guide will equip you with all the knowledge to become a MediCLI power user and truly transform your hospital management experience. + + +## Who can benefit from MediCLI? +MediCLI is tailored built for hospital clerks/administrators or anyone who manages the relevant stakeholders in a hospital setting and seeks to optimize their workflow. + +### Prior knowledge: +MediCLI will be particularly beneficial for you are either (or both of): +- a fast-typer or, +- and familiar with a CLI, +enabling you to get up to speed and perform tasks swiftly and efficiently. + +That being said, even if you do not meet the above criteria, do not fret! Follow this guide, practice a little, and in only a couple of days you too can take full advantage of MediCLI's features to manage doctors, patients, and appointments seamlessly. + +## Table of Contents * Table of Contents {:toc} +--------------------------------------------------------- +## Purpose of UG +This User Guide (UG) guide provides step-by-step instructions, explanations, and tips to help users make the most out of MediCLI regardless of experience. + +#### For new users +This UG offers a comprehensive overview of the features on offer, and provides you with a step-by-step guide on how to get started. + +#### For experienced MediCLI users: +You may skip to the features section which elaborates on the individual commands that you can run to get the most out of the system. + +## How to use this UG +As you read through this MediCLI User Guide, you will come across a variety of different types of text formats. The table below will explain to you what they mean. + +| Text Format | What it means | +|--------------------|----------------------------------------------------------------------------------------------------------| +| [hyperlink]() | Text in blue are hyperlinks and will take you to a different page. | +| `command` | Text in lowercase with a grey background box are MediCLI commands. | +| `FIELD` | Text in uppercase with a grey background box are inputs to MediCLI commands | +| `[OPTIONAL_FIELD]` | Text in uppercase with a grey background box and square brackets are optional inputs to MediCLI commands | + +Take note of these text-boxes, as they give you important information for using MediCLI. +
:bulb: **TIP**: Tip call-outs give you helpful pointers in MediCLI!
+
:information_source: **INFO**: Info call-outs give you information about MediCLI that you can take note of!
+
:exclamation: **DANGER**: Danger call-outs like this contain dangerous actions you should remember when using MediCLI to avoid errors!
+ -------------------------------------------------------------------------------------------------------------------- +## Key Product Information + +### Product Description +MediCLI is a Java-based desktop application that allows you to manage your hospital with ease. Let's now explore the product in more detail. + +### Overview of main features + +#### Entities that you can manage with MediCLI +MediCLI allows you to manage three types of entities that are common in a hospital setting: +- **Doctors**: The doctors employed by your hospital +- **Patients**: The patients that are treated at your hospital +- **Appointments**: A medical appointment between a doctor and a patient that takes place at a specified time + +#### Operations that you can perform with MediCLI +MediCLI allows you to perform the following operations of each of the entities above: +- Add: Add entities to the application +- Delete: Delete entities from the application +- Query: Lookup an entity in the application based on some criteria +- Edit: Edit the attributes of entities + +#### Other notable features +On top of the primary entities and operations highlighted above, MediCLI also provides the following capabilities: +- Persistent storage of information across restarts of the application +- A clean and minimalist display to view relevant information on entities +- Thorough error messages and in-app prompts guide you on how to overcome any issues you may run into. + + +## Quick Start Guide + +Ready to step into the world of MediCLI? This section will provide detailed information on how users can get started, +which includes basic system requirements, installation instructions, an overview of the main window, +and a tutorial on using the command-line interface (CLI). + +### System Compatibility + +MediCLI is written with the Java programming language on the backend and JavaFX on the front end. +Therefore, a device with Java version 11 or above and JavaFX version 17 or above installed is required to run MediCLI. + +Compatible Operating Systems: +* Any device running Windows, macOS, or Ubuntu with sufficient Java and JavaFX compatibility. + +Recommended Minimum System Requirements: +* 2-core CPU running at 2.40 GHz +* 4GB RAM +* 2GB free disc space + +### Installation Instructions + +1. Please make sure the computer you are using meets the system compatibility specified above. + +1. Download the latest `MediCLI.jar` from [here](https://github.com/AY2324S2-CS2103T-T15-1/tp/releases). + +
:information_source: **INFO**: The MediCLI jar file can be found at the bottom of the release notes
+ +1. We recommend you copy the file into the folder you want to use as the _home folder_ for MediCLI. This is because running the application will create additional storage and logging files. + +1. Congratulations! You now have MediCLI successfully downloaded on your computer. + +### Starting up MediCLI + +Once you have installed MediCLI onto your computer (refer to the sub-section above), navigate to the instructions specific to your operating system below. + +#### Windows + +1. Open File Explorer and navigate to the home folder containing the MediCLI jar file. + +2. Double-click on the MediCLI application and it should start up!
+ + ![Ui](images/WindowsStartup.png) + +#### macOS + +1. Open Finder and navigate to the home folder containing the MediCLI jar file. + +2. Double-click on the MediCLI application and it should start up!
+ + ![Ui](images/macOSStartup.png) + +#### CLI Alternative Solution + +1. Open a command terminal, `cd` into the home folder containing the MediCLI jar file, and use the `java -jar MediCLI.jar` command to run the application.
+ A GUI similar to the one below should appear in a few seconds. Note how the app contains some sample data.
-## Quick start + ![Ui](images/InitialState.png) -1. Ensure you have Java `11` or above installed in your Computer. +### Overview of MediCLI Main Window -1. Download the latest `addressbook.jar` from [here](https://github.com/se-edu/addressbook-level3/releases). +MediCLI has 4 primary components in its main window. Detailed descriptions of each can be found below. -1. Copy the file to the folder you want to use as the _home folder_ for your AddressBook. +![Ui](images/GUI.png) -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) +Command Input - This is where you will type your commands. + +Results Display - MediCLI will respond to you here with either a success message or a detailed description of what went wrong. + +
:bulb: **TIP**: The results display may be too narrow to show the entire message. You can scroll in the results display to see the whole message. +All error messages due to invalid formatting will end with an example usage.
+ +Persons Panel - This is where you will see a list of the filtered patients and patients. + +Appointments Panel - This is where you will see a list of the filtered patients and patients. + +### How to use the command line interface (CLI) + +MediCLI is operated using typed commands to the command line interface (CLI). Do not worry if you do not understand CLI yet; here we will explain to you the formats of text commands and how to use them. + +![Ui](images/cli_format.png) + +| CLI Format | What it means | +|-------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Command | The command tells MediCLI what action you want to perform. | +| Index | Certain MediCLI commands have an `INDEX` field, which is a number that is assigned to a particular patient, doctor, or appointment. Index must be larger than 1 and can be up to the maximum number of patients/doctors or appointments as listed in the MediCLI GUI. | +| Parameter Prefix | Fields typically have a prefix like `i/` or `n/` followed by the field content. This tells MediCLI what field you are entering. | +| Command Parameter | The command parameter is the parameter prefix followed by field content. For example, the command parameter to enter NRIC would be `i/S1234567A` | + +
:bulb: **TIP**: If you forget the command format, you can simply type the command word and press enter. MediCLI will prompt you with the format and an example.
+
:information_source: **INFO**: Not all MediCLI commands have fields! For example, the command to clear all data is simply `clear`.
+ +### Quick Tutorial on a Sample Use Case 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: + +
:exclamation: **DANGER**: 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.
+ +Some example commands you can try (Assuming MediCLI is opened for the first time and is in its initial state with the default sample data): * `list` : Lists all contacts. - * `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` : Adds a contact named `John Doe` to the Address Book. + * `adddoctor i/S1234567B n/Amy Smith d/2003-01-30 p/98765432` : Adds a doctor named `Amy Smith` to the MediCLI system. + + * `addappt ad/2024-06-09 10:15 dn/S1234567B pn/S1234567A` : Schedules an appointment between the doctor `Amy Smith` and the patient `John Doe`. - * `delete 3` : Deletes the 3rd contact shown in the current list. +
:information_source: **INFO**: MediCLI cannot schedule an appointment in the past, so change the date-time field if necessary.
- * `clear` : Deletes all contacts. + * `delete 2` : Deletes the 2nd person currently listed in the MediCLI system (patient named David Li). * `exit` : Exits the app. 1. Refer to the [Features](#features) below for details of each command. --------------------------------------------------------------------------------------------------------------------- - ## Features
**:information_source: Notes about the command format:**
-* 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`. +* Words in `UPPER_CASE` are the parameters to be supplied by you.
+ e.g. in `addpatient i/NRIC n/NAME d/DOB p/PHONE`, `NAME` is a parameter which can be used as `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`. - -* 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. + e.g. `edit INDEX [i/NRIC] [n/NAME] [p/PHONE] [d/DOB]` can be used as `edit 1 n/John Doe` or as `edit 1 i/t1234567s`. * 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. + e.g. if the command specifies `n/NAME p/PHONE`, `p/PHONE n/NAME` is also acceptable. * 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` +### Person Related Commands -Shows a message explaning how to access the help page. +#### Adding a patient : `addpatient` -![help message](images/helpMessage.png) +Adds a patient into the MediCLI system. -Format: `help` +Format: `addpatient i/NRIC n/NAME d/DOB p/PHONE` +Field Constraints: +* **NRIC** : Follows the correct Singapore NRIC format. Begin with one of S, T, G, F, or M, followed by 7 numerical digits, then end with an alphabetical letter. This field is non-case-sensitive. +* **NAME** : Only contain alphabetical characters and spaces. This field is non-case-sensitive. +* **DOB** : Only contain numerical characters in the format yyyy-MM-dd. Acceptable date range is from 1900 January 1st to today's date. +* **PHONE** : Only contain numerical characters and of exactly 8 digits long. -### Adding a person: `add` +Command Constraints: +* All of the above fields (NRIC, NAME, DOB, and PHONE) are compulsory and must be non-empty. +* Command fails if there already exists a person (patient or doctor) in the MediCLI system that has the same NRIC as the one given. +* The ordering of the fields does not influence the command. -Adds a person to the address book. +Examples: +* `addpatient i/T0334567A n/John Doe d/2003-01-30 p/98765432` +* `addpatient n/Amy Smith i/S8054321B p/87654321 d/1980-12-05` + +![add_patient_result](images/addPatient.png) + +#### Adding a doctor : `adddoctor` + +Adds a doctor into the MediCLI system. + +Format: `adddoctor i/NRIC n/NAME d/DOB p/PHONE` + +Field Constraints: +* **NRIC** : Follows the correct Singapore NRIC format. Begin with one of S, T, G, F, or M, followed by 7 numerical digits, then end with an alphabetical letter. This field is non-case-sensitive. +* **NAME** : Only contain alphabetical characters and spaces. This field is non-case-sensitive. +* **DOB** : Only contain numerical characters in the format yyyy-MM-dd. Acceptable date range is from 1900 January 1st to today's date. +* **PHONE** : Only contain numerical characters and of exactly 8 digits long. + +Command Constraints: +* All of the above fields (NRIC, NAME, DOB, and PHONE) are compulsory and must be non-empty. +* Command fails if there already exists a person (patient or doctor) in the MediCLI system that has the same NRIC as the one given. +* The ordering of the fields does not influence the command. + +Examples: +* `adddoctor i/T0334567A n/John Doe d/2003-01-30 p/98765432` +* `adddoctor n/Amy Smith i/S8054321B p/87654321 d/1980-12-05` + +![add_doctor_result](images/addDoctor.png) + +#### Editing a person : `edit` + +Edits an existing person in the MediCLI system. Edits the patient or doctor at the specified `INDEX`. The index refers to the index number shown in the displayed person list. +Existing values will be updated to the input values. + +
:information_source: **INFO**: Editing a patient or doctor and not changing any of the values of the parameters is allowed and is considered a valid edit by the system.
+
:bulb: **TIP**: Editing a patient or doctor will recursively update the relevant details of all appointments related to the patient or doctor.
+ +Format: `edit INDEX [i/NRIC] [n/NAME] [p/PHONE] [d/DOB]` + +Field Constraints: +* **NRIC** : Follows the correct Singapore NRIC format. Begin with one of S, T, G, F, or M, followed by 7 numerical digits, then end with an alphabetical letter. This field is non-case-sensitive. +* **NAME** : Only contain alphabetical characters and spaces. This field is non-case-sensitive. +* **DOB** : Only contain numerical characters in the format yyyy-MM-dd. Acceptable date range is from 1900 January 1st to today's date. +* **PHONE** : Only contain numerical characters and of exactly 8 digits long. + +Command Constraints: +* The index **must be a positive integer** 1, 2, 3, …​ +* At least one of the optional fields must be provided. +* Command fails if there already exists a person (patient or doctor) in the MediCLI system that has the same NRIC as the one given. +* The ordering of the fields does not influence the command. -Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​` +Examples: +* `edit 1 i/S1234567A n/Betsy Crower` Edits the NRIC and name of the 1st person to be `s1234567a` and `Betsy Crower` respectively. -
:bulb: **Tip:** -A person can have any number of tags (including 0) -
+![add_appointment_result](images/editPerson.png) + + +#### Finding both doctor and patient by name : `find` + +Find `Patient`(s) or `Doctor`(s) whose details contain any of the given keywords. + +Format for querying patients or doctors: `find KEYWORD [MORE_KEYWORDS]` + +Command Constraints: + +* 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 field is searched. +* Both full words and substrings will be matched e.g. `Han` will match `Hans` +* Patients and Doctors matching at least one keyword will be returned (i.e. logical 'OR' search). + e.g. `Hans Bo` will return `Hans Gruber`, `Bo Yang` 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 John` returns `john` and `John Doe` +* `find John David` returns patient `John Doe`, doctor `David Li`
-### Listing all persons : `list` +![result for 'find John David'](images/findPerson.png) -Shows a list of all persons in the address book. +
:bulb: **TIP**: You can use the find command to filter people for commands that require a person's `INDEX`.
-Format: `list` +#### Querying patients by name : `patient` -### Editing a person : `edit` +Find `Patient`(s) whose details contain any of the given keywords. -Edits an existing person in the address book. +Format for querying Patients: `patient KEYWORD [MORE_KEYWORDS]` -Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` +Command Constraints: -* 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. +* 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` +* All person fields are searched and matched (Name, NRIC, Phone Number, DoB). +* Both full words and substrings will be matched e.g. `Han` will match `Hans` +* Patients matching at least one keyword will be returned (i.e. `OR` search). + e.g. `Hans Bo` will return `Hans Gruber`, `Bo Yang` 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. +* `patient John David` returns `patient` with name `John Doe` and `patient` with name `David Li` +* `patient S1234` returns `patient` with `Nric` `S1234567A`, `patient` with `Nric` `S1234765Q` +* `patient 30 Jan` returns `patient` with `DoB` `30 January 1990`, `patient` with `DoB` `30 January 2001`
-### Locating persons by name: `find` +![result for 'patient alex david'](images/findPatient.png) -Finds persons whose names contain any of the given keywords. -Format: `find KEYWORD [MORE_KEYWORDS]` +#### Querying doctors by name : `doctor` -* The search is case-insensitive. e.g `hans` will match `Hans` +Find `Doctors`(s) whose details contain any of the given keywords. + +Format for querying Doctors: `doctor KEYWORD [MORE_KEYWORDS]` + +Command Constraints: + +* 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). +* All person fields are searched and matched (Name, NRIC, Phone Number, DoB). +* Both full words and substrings will be matched e.g. `Han` will match `Hans` +* Doctors matching at least one keyword will be returned (i.e. logical 'or' search). e.g. `Hans Bo` will return `Hans Gruber`, `Bo Yang` Examples: -* `find John` returns `john` and `John Doe` -* `find alex david` returns `Alex Yeoh`, `David Li`
- ![result for 'find alex david'](images/findAlexDavidResult.png) +* `doctor John David` returns `doctor` with name `John Doe` and `doctor` with name `David Li` +* `doctor S1234` returns `doctor` with `Nric` `S1234567A`, `doctor` with `Nric` `S1234765Q` +* `doctor 30 Jan` returns `doctor` with `DoB` `30 January 1990`, `doctor` with `DoB` `30 January 2001`
+ +![result for 'doctor alex david'](images/findDoctor.png) -### Deleting a person : `delete` -Deletes the specified person from the address book. +#### Deleting a doctor or patient : `delete` -Format: `delete INDEX` +Deletes the specified doctor / patient from the MediCLI system. Note that all associated appointments with this doctor / patient will also be recursively deleted. Please exercise caution when using the delete command and removing a patient or a doctor from MediCLI, as this action cannot be undone. -* Deletes the person at the specified `INDEX`. -* The index refers to the index number shown in the displayed person list. +* Deletes the doctor / patient at the specified `INDEX`. +* The index refers to the index number shown in the displayed doctor and patient list. * The index **must be a positive integer** 1, 2, 3, …​ Examples: -* `list` followed by `delete 2` deletes the 2nd person in the address book. -* `find Betsy` followed by `delete 1` deletes the 1st person in the results of the `find` command. +* `list` followed by `delete 2` deletes the 2nd doctor / patient in the MediCLI system. +* `patient John` followed by `delete 1` deletes the 1st patient in the results of the `patient` search command. +* `doctor Steve` followed by `delete 2` deletes the 2nd doctor in the results of the `doctor` search command. -### Clearing all entries : `clear` +![result for 'delete 1'](images/deletePerson.png) -Clears all entries from the address book. -Format: `clear` +### Appointment Related Commands -### Exiting the program : `exit` +#### Adding an appointment : `addappt` + +Adds an appointment to MediCLI. Appointments are between a doctor with the specified `DOCTOR_NRIC` and a patient with the `PATIENT_NRIC` on a specific date and time. + +
:information_source: **INFO**: Note that while you cannot create a new appointment with the date and time in the past, appointments that were valid when created but are now past their date and time will be allowed to remain in the system. This is an intended feature to allow the hospital admins to track a patient / doctors past appointments.
+ +Format: `addappt ad/DATETIME dn/DOCTOR_NRIC pn/PATIENT_NRIC` + +
:bulb: **TIP**: You can use the patient and doctor commands to retrieve their NRIC number if you only remember their name.
+ +Field Constraints: +- **DATETIME**: Input must be in the format `yyyy-MM-dd HH:mm`. Specified date and time must be later than the current date and time. i.e. appointment cannot be scheduled in the past. +- **DOCTOR_NRIC**: Follows the correct Singapore NRIC format. Begin with one of S, T, G, F, or M, followed by 7 numerical digits, then end with an alphabetical letter. This field is non-case-sensitive. +- **PATIENT_NRIC**: Follows the correct Singapore NRIC format. Begin with one of S, T, G, F, or M, followed by 7 numerical digits, then end with an alphabetical letter. This field is non-case-sensitive. + +Command Constraints: +- All of the above fields (`DATETIME`, `DOCTOR_NRIC`, `PATIENT_NRIC`) are compulsory and must be non-empty. +- A doctor with the specified `DOCTOR_NRIC` must already exist in the MediCLI System. +- A patient with the specified `PATIENT_NRIC` must already exist in the MediCLI System. + +Examples: +- `addappt ad/2024-08-11 23:50 dn/S1234567A pn/S1234567B` +- `addappt ad/2025-04-09 11:10 dn/S8054321B pn/T0334567A` + +![add_appointment_result](images/addAppointment.png) + +#### Editing an appointment : `editappt` +Edits an existing appointment in the MediCLI system. Edits the appointment at the specified `INDEX`. The index refers to the index number shown in the displayed appointments list. +Existing values will be updated to the input values. + +
:information_source: **INFO**: Editing an appointment and not changing any of the values of the parameters is allowed and is considered a valid edit by the system.
+ +Format: `editappt INDEX ad/DATETIME` + +* Edits the appointment at the specified `INDEX`. The index refers to the index number shown in the displayed appointment list. The index **must be a positive integer** 1, 2, 3, …​ +* Existing values will be updated to the input values. + +Field Constraints: +* **DATETIME** : Input must be in the format `yyyy-MM-dd HH:mm`. Specified date and time must be later than the current date and time. i.e. appointment cannot be scheduled in the past. + +Command Constraints: +* The index **must be a positive integer** 1, 2, 3, …​ +* At least one of the optional fields must be provided. + +Examples: + +* `editappt 1 ad/2025-04-09 11:00` Edits the appointment date and time of the first appointment in the appointment list to `2025-04-09 11:00` + +![add_appointment_result](images/editAppointment.png) + + +#### Querying appointments by patient's NRIC : `apptforpatient` + +Format: `apptforpatient KEYWORD [MORE_KEYWORDS]` + +Command Constraints: +* The search is case-insensitive. e.g. `s1234562a` will match `S1234562A` +* The order of the keywords does not matter. e.g. `S1234562A S1234561A` will +match appointments that involve `S1234562A` and `S1234561A`. +* Only the NRIC field of `Patient` is searched and matched. +* Only exact NRICs will be matched e.g. `S123456` will not match `S1234562A` +* Appointments with `Patient`s whose NRICs match at least one keyword will be returned (i.e. `OR` search). + +Example: +* `apptforpatient s0123456a` returns all `Appointment` entries that `Patient` with `Nric` `S0123456A` is involved in. + +* All `Appointment`s listed +![result for 'list'](images/findAppointmentInitialPatient.png) + +* Only `Appointment`s with `Patient` of `Nric` `S0123456A` +![result for 'apptforpatient S0123456A'](images/findAppointmentResultPatient.png) + + +#### Querying appointments by doctor's NRIC : `apptfordoctor` + +Format: `apptfordoctor KEYWORD [MORE_KEYWORDS]` + +Command Constraints: + +* The search is case-insensitive. e.g. `s1234562a` will match `S1234562A` +* The order of the keywords does not matter. e.g. `S1234562A S1234561A` will +match appointments that involve `S1234562A` and `S1234561A`. + * Only the NRIC field of `Doctor` is searched and matched. +* Only exact NRICs will be matched e.g. `S123456` will not match `S1234562A` +* Appointments with `Doctor`s whose NRICs match at least one keyword will be returned (e.g. `OR` search). + +Example: +* `apptfordoctor s1234561a` returns all `Appointment` object(s) that `Doctor` with NRIC `S1234561A` is involved in. + +* All `Appointment`s listed +![result for 'list'](images/findAppointmentInitialDoctor.png) + +* Only `Appointment`s with `Doctor` of `Nric` `S1234561A` +![result for 'apptfordoctor S1234561A'](images/findAppointmentResultDoctor.png) + + +#### Deleting appointment : `deleteappt` + +Deletes the specified appointment from the MediCLI system. + +Format: `deleteappt INDEX` + +* Deletes the appointment at the specified `INDEX`. +* The index refers to the index number shown in the displayed appointments list. +* The index **must be a positive integer** 1, 2, 3, …​ + +Examples: +* `list` followed by `deleteappt 2` deletes the 2nd appointment in the MediCLI system. +* `apptforpatient S1234567A` followed by `deleteappt 1` deletes the 1st appointment in the results of the `apptforpatient` search command. +* `apptfordoctor S1234567B` followed by `deleteappt 2` deletes the 2nd appointment in the results of the `apptfordoctor` search command. + +Visual Guide +* All appointments listed after running `list` + ![result for 'list'](images/deleteApptInitialState.png) +* After running `deleteappt` with `Index` of `1` + ![result for 'deleteappt 1'](images/deleteApptFinalState.png) + + +### Miscellaneous Commands + +#### Viewing help : `help` + +Shows a message explaining how to access the help page. + +![help message](images/helpMessage.png) + +Format: `help` + +#### Listing all persons : `list` + +Shows a list of all persons (patients & doctors) and appointments in the MediCLI system. + +Format: `list` + +#### Clearing all entries : `clear` + +Clears all entries from MediCLI. + +
:exclamation: **DANGER**: This command will wipe the entire data from the system upon being executed. This action is irreversible and no confirmation prompt is given. Please be very purposeful and cautious when you use this.
+ +Format: `clear` + +![result for 'clear'](images/clear.png) + +#### Exiting the program : `exit` Exits the program. @@ -155,33 +525,131 @@ Format: `exit` ### Saving the data -AddressBook data are saved in the hard disk automatically after any command that changes the data. There is no need to save manually. +MediCLI data are saved in the hard disk automatically after any command that changes the data. There is no need to save manually. ### Editing the data file -AddressBook data are saved automatically as a JSON file `[JAR file location]/data/addressbook.json`. Advanced users are welcome to update data directly by editing that data file. +MediCLI data are saved automatically as a JSON file `[JAR file location]/data/MediCLI.json`. Advanced users are welcome to update data directly by editing that data file. -
:exclamation: **Caution:** -If your changes to the data file makes its format invalid, AddressBook will discard all data and start with an empty data file at the next run. Hence, it is recommended to take a backup of the file before editing it.
-Furthermore, certain edits can cause the AddressBook to behave in unexpected ways (e.g., if a value entered is outside of the acceptable range). Therefore, edit the data file only if you are confident that you can update it correctly. +
:exclamation: **DANGER**: +If your changes to the data file make its format invalid, MediCLI will discard all data and start with an empty data file at the next run. Hence, it is recommended to take a backup of the file before editing it.
+Furthermore, certain edits can cause the MediCLI 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) + +1. **Q**: How do I transfer my data to another Computer?
+**A**: Install the app on the other computer and overwrite the empty data file it creates with the file that contains the data of your previous MediCLI home folder. + +2. **Q**: Can I use MediCLI on different operating systems?
+**A**: Yes, MediCLI is compatible with multiple operating systems as long as you have Java 11 or above installed. You can run it on Windows, macOS, or Linux. + +3. **Q**: Is there a limit to the number of patients, doctors, or appointments I can add to MediCLI?
+**A**: There is no built-in limit to the number of entries you can add to MediCLI. However, the performance may be affected if you add an extremely large number of entries. -_Details coming soon ..._ +4. **Q**: Can I customise the appearance or theme of the interface in MediCLI?
+**A**: Currently, there is no option to customise the appearance or theme of the interface in MediCLI. It has a default interface optimised for efficiency and usability. + +5. **Q**: Does MediCLI support multi-user access or user authentication?
+**A**: No, MediCLI is designed for single-user access only. It does not have built-in support for multi-user access or user authentication. + +6. **Q**: Can I export data from MediCLI to other formats like CSV or Excel?
+**A**: Currently, there is no built-in feature to export data from MediCLI to other formats. However, you can manually extract data from the JSON file if needed. -------------------------------------------------------------------------------------------------------------------- -## FAQ +## Known Issues + +1. **Issue**: When using multiple screens, if the MediCLI application is moved to a secondary screen and later switched to using only the primary screen, the graphical user interface (GUI) may open off-screen upon application launch. + + **Impact**: Users may find it challenging to interact with the application as the GUI is rendered off-screen, making it inaccessible and difficult to use. + + **Workaround**: To resolve this issue, users can delete the preferences.json file generated by the application before launching MediCLI again. This action resets the application preferences, ensuring that the GUI opens within the visible area of the primary screen. +2. **Issue**: MediCLI may experience performance degradation when handling a large number of entries, such as patients, doctors, or appointments. + + **Impact**: Users may notice slower response times or delays when adding, editing, or deleting entries, especially in cases with a large dataset. + + **Workaround**: Users can optimise performance by limiting the number of entries stored in MediCLI or by periodically archiving old data to reduce the dataset size. +3. **Issue**: Editing data directly in the data file may lead to unexpected behavior or data corruption. + + **Impact**: Users who manually edit the JSON data file used by MediCLI may inadvertently introduce errors or inconsistencies, resulting in data loss or application crashes. + + **Workaround**: It's recommended to avoid directly editing the data file unless absolutely necessary. Users should exercise caution and make backups before making any changes to the data file. +4. **Issue**: MediCLI does not provide built-in data export functionality to formats like CSV or Excel. + + **Impact**: Users may face challenges when trying to export data from MediCLI for analysis or reporting purposes, especially if they rely on external tools or software that require specific file formats. + + **Workaround**: Users can manually extract data from the JSON data file used by MediCLI and convert it to the desired format using third-party tools or scripts. Alternatively, they can explore custom export solutions or request this feature from the developers. + +5. **Issue**: When the name entered into the system is too lengthy, MedicCLI truncates the name and adds ellipses. -**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. + **Impact**: Users may face challenges reading or finding long names when reading from MediCLI, especially on smaller displays. + + **Workaround**: Users can reduce the amount of characters typed into the name field or search for longer names based on the first few characters shown instead of the entire name. + +6. **Issue**: When the name entered into the system contains acute accents (e.g. `Aimée`), Chinese characters, or dash `-`, the system will reject it. + + **Impact**: Users may face challenges entering information about patient / doctors with foreign names into MediCLI. + + **Workaround**: Users can replace the accented characters with normal alphabets (e.g. `é` with `e`), omit the dash and romanize Chinese names. + +7. **Issue**: MediCLI will only accept Singaporean NRIC + + **Impact**: Users may face challenges entering information about patients who are not from Singapore (e.g. those without NRIC). + + **Workaround**: There is currently no workaround, but the MediCLI development team will add this feature enhancement in the near future. +8. **Issue**: MediCLI currently allows scheduling of appointments between the same doctor but different patients at identical times (e.g. overlapping appointments). + + **Impact**: Users may schedule two or more patients with the same doctor at the same time when the doctor can only see one patient at any given time. + + **Workaround**: Users can first look up the appointments of the doctor and visually confirm the doctor is free at a given time before scheduling a patient with them. -------------------------------------------------------------------------------------------------------------------- -## Known issues +## Glossary + +**Alphanumeric**: A combination of alphabetic characters and numerical digits. (e.g S1234567A) + +**Backend**: The part of a software system that handles data processing and logic execution, typically hidden from the user. + +**CLI (Command Line Interface)**: A text-based interface for interacting with a computer program through commands typed into a terminal or console. + +**Command Terminal**: A text-based interface where users can input commands to perform various tasks on a computer system. + +**Desktop Application**: Software designed to be run on desktop or laptop computers, providing functionality without requiring a web browser. + +**Entities**: Objects or elements with distinct and independent existence within a system, often represented in databases or software architectures. MediCLI has 3 entities; Patient, Doctor, Appointment. + +**GUI (Graphical User Interface)**: An interface that allows users to interact with electronic devices through graphical icons and visual indicators. + +**Hard Disk**: A component of a computer system responsible for long-term storage of data. + +**Home Folder**: The default directory or folder on a computer system where a user's personal files and data are stored. + +**Java 11**: A version of the Java programming language and platform, released in September 2018, known for its long-term support (LTS). + +**JavaFX**: A software platform and GUI toolkit for Java applications, providing a rich set of features for building interactive user interfaces. + +**Jar File**: A Java Archive file format used to package Java class files, associated metadata, and resources into a single file. + +**Json File**: A file format used for storing and exchanging data in a human-readable and machine-parseable format, based on JavaScript Object Notation (JSON). + +**Logging Files**: Files generated by software applications to record events, actions, or errors for troubleshooting and analysis purposes. + +**macOS**: The operating system developed by Apple Inc. for its Macintosh line of computers. + +**NRIC (National Registration Identity Card)**: A unique identification document issued to citizens and permanent residents of certain countries. + +**Recursively Deleted**: The process of removing entities from a filesystem in a recursive manner. (e.g If an Appointment ‘A’ was associated or ‘linked’ with Person ‘P’, the deletion of ‘P’ will also trigger the deletion of the Appointment ‘A’) + +**Ubuntu**: A popular Linux distribution based on Debian, known for its ease of use and community-driven development. + +**Windows**: A series of operating systems developed by Microsoft, widely used on personal computers and servers. + + -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. -------------------------------------------------------------------------------------------------------------------- @@ -189,10 +657,19 @@ _Details coming soon ..._ Action | Format, Examples --------|------------------ -**Add** | `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​`
e.g., `add n/James Ho p/22224444 e/jamesho@example.com a/123, Clementi Rd, 1234665 t/friend t/colleague` -**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` +**[Add Patient](#adding-a-patient--addpatient)** | `addpatient i/NRIC n/NAME d/DOB p/PHONE_NUMBER`
e.g., `addpatient i/S1234567A n/John Doe d/2003-01-30 p/98765432` +**[Add Doctor](#adding-a-doctor--adddoctor)** | `adddoctor i/NRIC n/NAME d/DOB p/PHONE_NUMBER`
e.g., `adddoctor i/S1234567A n/John Doe d/2003-01-30 p/98765432` +**[Edit Person](#editing-a-person--edit)** | `edit INDEX [n/NAME] [p/PHONE] [i/NRIC] [d/DOB]`
e.g.,`edit 1 p/91234567 n/Betsy Crower` +**[Find Person](#finding-both-doctor-and-patient-by-name--find)** | `find KEYWORD [MORE_KEYWORDS]`
e.g., `find Doe Li` +**[Query Patient](#querying-patients-by-name--patient)** | `patient KEYWORD [MORE_KEYWORDS]`
e.g., `patient James Jake` +**[Query Doctor](#querying-doctors-by-name--doctor)** | `doctor KEYWORD [MORE_KEYWORDS]`
e.g., `doctor John Doe` +**[Delete Person](#deleting-a-doctor-or-patient--delete)** | `delete INDEX`
e.g., `delete 3` +**[Add Appointment](#adding-an-appointment--addappt)** | `addappt ad/DATETIME dn/DOCTOR_NRIC pn/PATIENT_NRIC`
e.g., `addappt ad/2024-08-11 23:50 dn/S1234567A pn/S1234567B` +**[Edit Appointment](#editing-an-appointment--editappt)** | `editappt INDEX ad/DATETIME`
e.g.,`editappt 1 ad/2024-04-09 10:10` +**[Query Appointment by Patient](#querying-appointments-by-patients-nric--apptforpatient)** | `apptforpatient KEYWORD [MORE_KEYWORDS]`
e.g., `apptforpatient S1234567A` +**[Query Appointment by Doctor](#querying-appointments-by-doctors-nric--apptfordoctor)** | `apptfordoctor KEYWORD [MORE_KEYWORDS]`
e.g., `apptfordoctor S7654321A` +**[Delete Appointment](#deleting-appointment--deleteappt)** | `deleteappt INDEX`
e.g., `deleteappt 3` +**[Help](#viewing-help--help)** | `help` +**[List](#listing-all-persons--list)** | `list` +**[Clear](#clearing-all-entries--clear)** | `clear` +**[Exit](#exiting-the-program--exit)** | `exit` diff --git a/docs/_config.yml b/docs/_config.yml index 6bd245d8f4e..d30494e5e40 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,4 +1,4 @@ -title: "AB-3" +title: "MediCLI" theme: minima header_pages: @@ -8,7 +8,7 @@ header_pages: markdown: kramdown -repository: "se-edu/addressbook-level3" +repository: "AY2324S2-CS2103T-T15-1/tp" github_icon: "images/github-icon.png" plugins: diff --git a/docs/_layouts/page.html b/docs/_layouts/page.html index 01e4b2a93b8..3740360ca5e 100644 --- a/docs/_layouts/page.html +++ b/docs/_layouts/page.html @@ -2,7 +2,7 @@ layout: default ---
- +

{{ page.title | escape }}

diff --git a/docs/_sass/minima/_base.scss b/docs/_sass/minima/_base.scss index 0d3f6e80ced..77456f76a73 100644 --- a/docs/_sass/minima/_base.scss +++ b/docs/_sass/minima/_base.scss @@ -288,7 +288,7 @@ table { text-align: center; } .site-header:before { - content: "AB-3"; + content: "MediCLI"; font-size: 32px; } } diff --git a/docs/assets/css/style.scss b/docs/assets/css/style.scss index b5ec6976efa..0ff03ded082 100644 --- a/docs/assets/css/style.scss +++ b/docs/assets/css/style.scss @@ -10,3 +10,17 @@ height: 21px; width: 21px } + +h2, h3 { + font-weight: bold; + color: #257ec7; +} + +.post-title { + color: #257ec7; + font-weight: bold; +} + +#logo { + margin-bottom: 15px; +} diff --git a/docs/diagrams/AddAppoiintmentActivityDiagram.puml b/docs/diagrams/AddAppoiintmentActivityDiagram.puml new file mode 100644 index 00000000000..4b98b808b4c --- /dev/null +++ b/docs/diagrams/AddAppoiintmentActivityDiagram.puml @@ -0,0 +1,38 @@ +@startuml +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 + +start +:User inputs text command to add an appointment; + +if () then ([command word does not exist]) + :Show error message\nfor unknown command word; +else ([else]) + if () then ([missing required fields]) + :Show error message indicating\n invalid command format; + else ([else]) + if () then ([invalid field arguments]) + :Show error message\nhighlighting invalid field\narguments provided; + else ([else]) + if () then ([doctor/patient does not exist]) + :Show error message\nindicating that the doctor/patient is not registered; + else ([else]) + if () then ([appointment date is invalid]) + :Show error message indicating that\n the appointment cannot be scheduled in the past; + else ([else]) + if () then ([Duplicate appointment detected]) + :Show error message\nindicating duplicate appointment; + else ([else]) + :Add the appointment\ninto the appointment list; + :Update the 'appointment' panel\nin the GUI; + :Show success message\nwith appointments' information; + endif; + endif + endif + endif + endif +endif + +stop +@enduml diff --git a/docs/diagrams/AddAppointmentSequenceDiagram.puml b/docs/diagrams/AddAppointmentSequenceDiagram.puml new file mode 100644 index 00000000000..3c348e8aa85 --- /dev/null +++ b/docs/diagrams/AddAppointmentSequenceDiagram.puml @@ -0,0 +1,65 @@ +@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 ":AddAppointmentCommandParser" as AddAppointmentCommandParser LOGIC_COLOR +participant "e:AddAppointmentCommand" as AddAppointmentCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("addappt dn/...") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("addappt dn/...") +activate AddressBookParser + +create AddAppointmentCommandParser +AddressBookParser -> AddAppointmentCommandParser +activate AddAppointmentCommandParser + +create AddAppointmentCommand +AddAppointmentCommandParser -> AddAppointmentCommand : : parse("addappt dn/...") +activate AddAppointmentCommand + +AddAppointmentCommand --> AddAppointmentCommandParser +deactivate AddAppointmentCommand + +AddAppointmentCommandParser --> AddressBookParser +deactivate AddAppointmentCommandParser + +'Hidden arrow to position the destroy marker below the end of the activation bar. +AddAppointmetnCommandParser -[hidden]-> AddressBookParser +destroy AddAppointmentCommandParser + +AddressBookParser --> LogicManager +deactivate AddressBookParser + +LogicManager -> AddAppointmentCommand : execute() +activate AddAppointmentCommand + +AddAppointmentCommand -> Model : addAppointment(toAdd) +activate Model + +Model --> AddAppointmentCommand +deactivate Model + +create CommandResult +AddAppointmentCommand -> CommandResult +activate CommandResult + +CommandResult --> AddAppointmentCommand : result +deactivate CommandResult + +AddAppointmentCommand --> LogicManager : result +deactivate AddAppointmentCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/AddPatientCommandExecuteSequenceDiagram.puml b/docs/diagrams/AddPatientCommandExecuteSequenceDiagram.puml new file mode 100644 index 00000000000..0a6cef51d0b --- /dev/null +++ b/docs/diagrams/AddPatientCommandExecuteSequenceDiagram.puml @@ -0,0 +1,63 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":AddPatientCommand" as AddPatientCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Commons LOGGER_COLOR_T1 +participant ":Logger" as Logger LOGGER_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> AddPatientCommand : execute() +activate AddPatientCommand + + +alt model.hasPerson(toAdd) + AddPatientCommand -> Model: hasPerson(toAdd) + activate Model + Model -> AddPatientCommand + deactivate Model + + AddPatientCommand -> Logger: log(Level.INFO, "Duplicate") + activate Logger + Logger -> Logger: log + Logger -> AddPatientCommand + deactivate Logger + + [<--AddPatientCommand: throw CommandException() +else else + AddPatientCommand -> Model : hasPerson(toAdd) + activate Model + + Model --> AddPatientCommand + + AddPatientCommand -> Model : addPerson(toAdd) + + Model --> AddPatientCommand + deactivate Model + + AddPatientCommand -> Logger: log(Level.INFO, "Success") + activate Logger + Logger -> Logger: log + Logger -> AddPatientCommand + deactivate Logger + + create CommandResult + AddPatientCommand -> CommandResult + activate CommandResult + + CommandResult --> AddPatientCommand : result + deactivate CommandResult + + [<--AddPatientCommand : result + deactivate AddPatientCommand +end + +@enduml diff --git a/docs/diagrams/AddPersonActivityDiagram.puml b/docs/diagrams/AddPersonActivityDiagram.puml new file mode 100644 index 00000000000..89d7c86ed96 --- /dev/null +++ b/docs/diagrams/AddPersonActivityDiagram.puml @@ -0,0 +1,30 @@ +@startuml +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 + +start +:User inputs text command to add a patient/doctor; + +if () then ([command word does not exist]) + :Show error message\nfor unknown command; +else ([else]) + if () then ([missing required fields]) + :Show error message\nfor missing required fields; + else ([else]) + if () then ([invalid field arguments]) + :Show error message\nindicating invalid field\narguments provided; + else ([else]) + if () then ([Duplicate patient/doctor detected]) + :Show error message\nindicating duplicate patient/doctor; + else ([else]) + :Add the patient/doctor\ninto the persons list; + :Update the 'person' panel\nin the GUI; + :Show success message\nwith patient/doctor's information; + endif; + endif + endif +endif + +stop +@enduml diff --git a/docs/diagrams/AppointmentClassDiagram.puml b/docs/diagrams/AppointmentClassDiagram.puml new file mode 100644 index 00000000000..2b7cb8e7d55 --- /dev/null +++ b/docs/diagrams/AppointmentClassDiagram.puml @@ -0,0 +1,19 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor UI_COLOR_T4 +skinparam classBackgroundColor UI_COLOR + +Class "Appointment" as Appointment +Class Nric +Class AppointmentDateTime + + + +Appointment -down-> "1" Nric : Doctor's NRIC\t +Appointment -down-> "1" Nric : Patient's NRIC\t\t +Appointment -down-> "\t1" AppointmentDateTime + + +@enduml + diff --git a/docs/diagrams/AppointmentObjectDiagram.puml b/docs/diagrams/AppointmentObjectDiagram.puml new file mode 100644 index 00000000000..ebd1a45846a --- /dev/null +++ b/docs/diagrams/AppointmentObjectDiagram.puml @@ -0,0 +1,18 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor UI_COLOR_T4 +skinparam classBackgroundColor UI_COLOR + +object "__:Appointment__" as appointment +object "__doctorNric:Nric__" as doctorNric +object "__patientNric:Nric__" as patientNric +object "__appointmentDateTime:AppointmentDateTime__" as appointmentDate + + +appointment -down-> doctorNric +appointment -down-> patientNric +appointment -down-> appointmentDate + + +@enduml diff --git a/docs/diagrams/ArchitectureSequenceDiagram.puml b/docs/diagrams/ArchitectureSequenceDiagram.puml index 48b6cc4333c..4a0fb4b1afb 100644 --- a/docs/diagrams/ArchitectureSequenceDiagram.puml +++ b/docs/diagrams/ArchitectureSequenceDiagram.puml @@ -8,13 +8,13 @@ Participant ":Logic" as logic LOGIC_COLOR Participant ":Model" as model MODEL_COLOR Participant ":Storage" as storage STORAGE_COLOR -user -[USER_COLOR]> ui : "delete 1" +user -[USER_COLOR]> ui : "addpatient i/T1..." activate ui UI_COLOR -ui -[UI_COLOR]> logic : execute("delete 1") +ui -[UI_COLOR]> logic : execute("addpatient i/T1...") activate logic LOGIC_COLOR -logic -[LOGIC_COLOR]> model : deletePerson(p) +logic -[LOGIC_COLOR]> model : addPatient(p) activate model MODEL_COLOR model -[MODEL_COLOR]-> logic diff --git a/docs/diagrams/DeleteAppointmentActivityDiagram.puml b/docs/diagrams/DeleteAppointmentActivityDiagram.puml new file mode 100644 index 00000000000..ebaaabe7790 --- /dev/null +++ b/docs/diagrams/DeleteAppointmentActivityDiagram.puml @@ -0,0 +1,26 @@ +@startuml +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 + +start +:User enters command to delete appointment; + +if () then ([command is invalid]) + :Show error message\nfor invalid command; +else ([else]) + if () then ([missing required fields]) + :Show error message\nfor missing required fields; + else ([else]) + if () then ([Invalid appointment index detected]) + :Show error message\nindicating invalid Appointment index; + else ([else]) + :Remove appointment\nfrom the appointment list; + :Update the 'appointment' panel\nin the GUI; + :Show success message\nwith removed appointment information; + endif; + endif +endif + +stop +@enduml diff --git a/docs/diagrams/DeleteAppointmentSequenceDiagram.puml b/docs/diagrams/DeleteAppointmentSequenceDiagram.puml new file mode 100644 index 00000000000..60bc07bd892 --- /dev/null +++ b/docs/diagrams/DeleteAppointmentSequenceDiagram.puml @@ -0,0 +1,65 @@ +@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 ":DeleteAppointmentCommandParser" as DeleteAppointmentCommandParser LOGIC_COLOR +participant "e:DeleteAppointmentCommand" as DeleteAppointmentCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("deleteappt i/...") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("deleteappt i/...") +activate AddressBookParser + +create DeleteAppointmentCommandParser +AddressBookParser -> DeleteAppointmentCommandParser +activate DeleteAppointmentCommandParser + +create DeleteAppointmentCommand +DeleteAppointmentCommandParser -> DeleteAppointmentCommand : : parse("deleteappt i/...") +activate DeleteAppointmentCommand + +DeleteAppointmentCommand --> DeleteAppointmentCommandParser +deactivate DeleteAppointmentCommand + +DeleteAppointmentCommandParser --> AddressBookParser +deactivate DeleteAppointmentCommandParser + +'Hidden arrow to position the destroy marker below the end of the activation bar. +DeleteAppointmentCommandParser -[hidden]-> AddressBookParser +destroy DeleteAppointmentCommandParser + +AddressBookParser --> LogicManager +deactivate AddressBookParser + +LogicManager -> DeleteAppointmentCommand : execute() +activate DeleteAppointmentCommand + +DeleteAppointmentCommand -> Model : deleteAppointment(toRemove) +activate Model + +Model --> DeleteAppointmentCommand +deactivate Model + +create CommandResult +DeleteAppointmentCommand -> CommandResult +activate CommandResult + +CommandResult --> DeleteAppointmentCommand : result +deactivate CommandResult + +DeleteAppointmentCommand --> LogicManager : result +deactivate DeleteAppointmentCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/DeleteCommandExecuteSequenceDiagram.puml b/docs/diagrams/DeleteCommandExecuteSequenceDiagram.puml new file mode 100644 index 00000000000..e229d2e0a54 --- /dev/null +++ b/docs/diagrams/DeleteCommandExecuteSequenceDiagram.puml @@ -0,0 +1,56 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":DeleteCommand" as DeleteCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Commons LOGGER_COLOR_T1 +participant ":Index" as Index LOGGER_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> DeleteCommand : execute() +activate DeleteCommand + +DeleteCommand -> Model: getFilteredPersonList() +activate Model +Model -> DeleteCommand +deactivate Model + +alt index.getZeroBased() >= lastShownList.size() + DeleteCommand -> Index: getZeroBased() + activate Index + Index -> DeleteCommand + deactivate Index + + [<--DeleteCommand: throw CommandException() +else else + DeleteCommand -> Index: getZeroBased() + activate Index + Index -> DeleteCommand + deactivate Index + + DeleteCommand -> Model : deletePerson(personToDelete) + activate Model + + Model --> DeleteCommand + deactivate Model + + create CommandResult + DeleteCommand -> CommandResult + activate CommandResult + + CommandResult --> DeleteCommand : result + deactivate CommandResult + + [<--DeleteCommand : result + deactivate DeleteCommand +end + +@enduml diff --git a/docs/diagrams/DeletePersonActivityDiagram.puml b/docs/diagrams/DeletePersonActivityDiagram.puml new file mode 100644 index 00000000000..2aeb1992af5 --- /dev/null +++ b/docs/diagrams/DeletePersonActivityDiagram.puml @@ -0,0 +1,26 @@ +@startuml +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 + +start +:User enters command to delete doctor or patient; + +if () then ([command is invalid]) + :Show error message\nfor invalid command; +else ([else]) + if () then ([missing required fields]) + :Show error message\nfor missing required fields; + else ([else]) + if () then ([Invalid person index detected]) + :Show error message\nindicating invalid Person index; + else ([else]) + :Remove patient/doctor\nfrom the persons list\nalso removes any appointments\nassociated with the deleted patient/doctor; + :Update the 'person' panel\nand appointments panel\n in the GUI; + :Show success message\nwith removed doctor/patient information; + endif; + endif +endif + +stop +@enduml diff --git a/docs/diagrams/DeletePersonSequenceDiagram.puml b/docs/diagrams/DeletePersonSequenceDiagram.puml new file mode 100644 index 00000000000..368d809e7a4 --- /dev/null +++ b/docs/diagrams/DeletePersonSequenceDiagram.puml @@ -0,0 +1,65 @@ +@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 ":DeleteCommandParser" as DeleteCommandParser LOGIC_COLOR +participant "e:DeleteCommand" as DeleteCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("delete i/...") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("delete i/...") +activate AddressBookParser + +create DeleteCommandParser +AddressBookParser -> DeleteCommandParser +activate DeleteCommandParser + +create DeleteCommand +DeleteCommandParser -> DeleteCommand : : parse("delete i/...") +activate DeleteCommand + +DeleteCommand --> DeleteCommandParser +deactivate DeleteCommand + +DeleteCommandParser --> AddressBookParser +deactivate DeleteCommandParser + +'Hidden arrow to position the destroy marker below the end of the activation bar. +DeleteCommandParser -[hidden]-> AddressBookParser +destroy DeleteCommandParser + +AddressBookParser --> LogicManager +deactivate AddressBookParser + +LogicManager -> DeleteCommand : execute() +activate DeleteCommand + +DeleteCommand -> Model : removePerson(toRemove) +activate Model + +Model --> DeleteCommand +deactivate Model + +create CommandResult +DeleteCommand -> CommandResult +activate CommandResult + +CommandResult --> DeleteCommand : result +deactivate CommandResult + +DeleteCommand --> LogicManager : result +deactivate DeleteCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/EditAppointmentActivityDiagram.puml b/docs/diagrams/EditAppointmentActivityDiagram.puml new file mode 100644 index 00000000000..54066f7aab1 --- /dev/null +++ b/docs/diagrams/EditAppointmentActivityDiagram.puml @@ -0,0 +1,26 @@ +@startuml +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 + +start +:User enters command to edit appointment; + +if () then ([command is invalid]) + :Show error message\nfor invalid command; +else ([else]) + if () then ([missing required fields]) + :Show error message\nfor missing required fields; + else ([else]) + if () then ([Invalid appointment index detected]) + :Show error message\nindicating invalid Appointment index; + else ([else]) + :edit appointment\nin the appointment list; + :Update the 'appointment' panel\nin the GUI; + :Show success message\nwith edited appointment information; + endif; + endif +endif + +stop +@enduml diff --git a/docs/diagrams/EditAppointmentSequenceDiagram.puml b/docs/diagrams/EditAppointmentSequenceDiagram.puml new file mode 100644 index 00000000000..02e383cee56 --- /dev/null +++ b/docs/diagrams/EditAppointmentSequenceDiagram.puml @@ -0,0 +1,65 @@ +@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 ":EditAppointmentCommandParser" as EditAppointmentCommandParser LOGIC_COLOR +participant "e:EditAppointmentCommand" as EditAppointmentCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("editappt i/...") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("editappt i/...") +activate AddressBookParser + +create EditAppointmentCommandParser +AddressBookParser -> EditAppointmentCommandParser +activate EditAppointmentCommandParser + +create EditAppointmentCommand +EditAppointmentCommandParser -> EditAppointmentCommand : : parse("editappt i/...") +activate EditAppointmentCommand + +EditAppointmentCommand --> EditAppointmentCommandParser +deactivate EditAppointmentCommand + +EditAppointmentCommandParser --> AddressBookParser +deactivate EditAppointmentCommandParser + +'Hidden arrow to position the destroy marker below the end of the activation bar. +EditAppointmentCommandParser -[hidden]-> AddressBookParser +destroy EditAppointmentCommandParser + +AddressBookParser --> LogicManager +deactivate AddressBookParser + +LogicManager -> EditAppointmentCommand : execute() +activate EditAppointmentCommand + +EditAppointmentCommand -> Model : setAppointment(toEdit) +activate Model + +Model --> EditAppointmentCommand +deactivate Model + +create CommandResult +EditAppointmentCommand -> CommandResult +activate CommandResult + +CommandResult --> EditAppointmentCommand : result +deactivate CommandResult + +EditAppointmentCommand --> LogicManager : result +deactivate EditAppointmentCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/EditCommandExecuteSequenceDiagram.puml b/docs/diagrams/EditCommandExecuteSequenceDiagram.puml new file mode 100644 index 00000000000..49a91d62da0 --- /dev/null +++ b/docs/diagrams/EditCommandExecuteSequenceDiagram.puml @@ -0,0 +1,73 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":EditCommand" as EditCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Commons LOGGER_COLOR_T1 +participant ":Index" as Index LOGGER_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> EditCommand : execute() +activate EditCommand + +EditCommand -> Model: getFilteredPersonList() +activate Model +Model -> EditCommand +deactivate Model + +alt index.getZeroBased() >= lastShownList.size() + EditCommand -> Index: getZeroBased() + activate Index + Index -> EditCommand + deactivate Index + + [<--EditCommand: throw CommandException() +else else + EditCommand -> Index: getZeroBased() + activate Index + Index -> EditCommand + deactivate Index + + EditCommand -> EditCommand: createEditedPerson(personToEdit, editPersonDescriptor) + alt model.hasPerson(editedPerson) + EditCommand -> Model: hasPerson(editedPerson) + activate Model + Model -> EditCommand + deactivate Model + [<--EditCommand: throw CommandException() + else else + EditCommand -> Model : hasPerson(editedPerson) + activate Model + + Model --> EditCommand + + EditCommand -> Model : setPerson(personToEdit, editedPerson) + + Model --> EditCommand + + EditCommand -> Model : updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + + Model --> EditCommand + deactivate Model + + create CommandResult + EditCommand -> CommandResult + activate CommandResult + + CommandResult --> EditCommand : result + deactivate CommandResult + + [<--EditCommand : result + deactivate EditCommand + end +end + +@enduml diff --git a/docs/diagrams/EditPersonActivityDiagram.puml b/docs/diagrams/EditPersonActivityDiagram.puml new file mode 100644 index 00000000000..5611c189610 --- /dev/null +++ b/docs/diagrams/EditPersonActivityDiagram.puml @@ -0,0 +1,26 @@ +@startuml +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 + +start +:User enters command to edit doctor or patient; + +if () then ([command is invalid]) + :Show error message\nfor invalid command; +else ([else]) + if () then ([missing required fields]) + :Show error message\nfor missing required fields; + else ([else]) + if () then ([Invalid person index detected]) + :Show error message\nindicating invalid Person index; + else ([else]) + :Edit patient/doctor\nfrom the persons list\nalso updates any appointments\nassociated with the edited patient/doctor; + :Update the 'person' panel\nand appointments panel\n in the GUI; + :Show success message\nwith edited doctor/patient information; + endif; + endif +endif + +stop +@enduml diff --git a/docs/diagrams/EditPersonSequenceDiagram.puml b/docs/diagrams/EditPersonSequenceDiagram.puml new file mode 100644 index 00000000000..3febefb3602 --- /dev/null +++ b/docs/diagrams/EditPersonSequenceDiagram.puml @@ -0,0 +1,65 @@ +@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:EditCommand" as EditCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("edit i/...") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("edit i/...") +activate AddressBookParser + +create EditCommandParser +AddressBookParser -> EditCommandParser +activate EditCommandParser + +create EditCommand +EditCommandParser -> EditCommand : : parse("edit i/...") +activate EditCommand + +EditCommand --> EditCommandParser +deactivate EditCommand + +EditCommandParser --> AddressBookParser +deactivate EditCommandParser + +'Hidden arrow to position the destroy marker below the end of the activation bar. +EditCommandParser -[hidden]-> AddressBookParser +destroy EditCommandParser + +AddressBookParser --> LogicManager +deactivate AddressBookParser + +LogicManager -> EditCommand : execute() +activate EditCommand + +EditCommand -> Model : setPerson(toEdit) +activate Model + +Model --> EditCommand +deactivate Model + +create CommandResult +EditCommand -> CommandResult +activate CommandResult + +CommandResult --> EditCommand : result +deactivate CommandResult + +EditCommand --> LogicManager : result +deactivate EditCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/FindActivityDiagram.puml b/docs/diagrams/FindActivityDiagram.puml new file mode 100644 index 00000000000..58e61c4c62e --- /dev/null +++ b/docs/diagrams/FindActivityDiagram.puml @@ -0,0 +1,20 @@ +@startuml +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 + +start +:User enters command to query a person,\nthe person can be either a doctor or patient.; + + +if () then ([missing required fields]) + :Show error message\nfor missing required fields; +else ([else]) + :Search the person from person list; + :Update the 'person' panel\nin the GUI to display the list; + :Show success message\nwith found person(s) information; +endif + + +stop +@enduml diff --git a/docs/diagrams/FindPersonSequenceDiagram.puml b/docs/diagrams/FindPersonSequenceDiagram.puml new file mode 100644 index 00000000000..1d1c497b8d6 --- /dev/null +++ b/docs/diagrams/FindPersonSequenceDiagram.puml @@ -0,0 +1,65 @@ +@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 ":FindCommandParser" as FindCommandParser LOGIC_COLOR +participant "e:FindCommand" as FindCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("find ...") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand("find ...") +activate AddressBookParser + +create FindCommandParser +AddressBookParser -> FindCommandParser +activate FindCommandParser + +create FindCommand +FindCommandParser -> FindCommand : : parse("find ...") +activate FindCommand + +FindCommand --> FindCommandParser +deactivate FindCommand + +FindCommandParser --> AddressBookParser +deactivate FindCommandParser + +'Hidden arrow to position the destroy marker below the end of the activation bar. +FindCommandParser -[hidden]-> AddressBookParser +destroy FindCommandParser + +AddressBookParser --> LogicManager +deactivate AddressBookParser + +LogicManager -> FindCommand : execute() +activate FindCommand + +FindCommand -> Model : find(person) +activate Model + +Model --> FindCommand +deactivate Model + +create CommandResult +FindCommand -> CommandResult +activate CommandResult + +CommandResult --> FindCommand : result +deactivate CommandResult + +FindCommand --> LogicManager : result +deactivate FindCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/LogicClassDiagram.puml b/docs/diagrams/LogicClassDiagram.puml index 58b4f602ce6..68c552e1dfe 100644 --- a/docs/diagrams/LogicClassDiagram.puml +++ b/docs/diagrams/LogicClassDiagram.puml @@ -1,8 +1,8 @@ @startuml !include style.puml skinparam arrowThickness 1.1 -skinparam arrowColor LOGIC_COLOR_T4 -skinparam classBackgroundColor LOGIC_COLOR +skinparam arrowColor LOGIC_COLOR_T5 +skinparam classBackgroundColor LOGIC_COLOR_T6 package Logic as LogicPackage { @@ -39,7 +39,7 @@ LogicManager --> Storage Storage --[hidden] Model Command .[hidden]up.> Storage Command .right.> Model -note right of XYZCommand: XYZCommand = AddCommand, \nFindCommand, etc +note right of XYZCommand: XYZCommand = AddPatientCommand, \nDeleteAppointmentCommand, etc Logic ..> CommandResult LogicManager .down.> CommandResult diff --git a/docs/diagrams/ModelClassDiagram.puml b/docs/diagrams/ModelClassDiagram.puml index 0de5673070d..ff937ab63c6 100644 --- a/docs/diagrams/ModelClassDiagram.puml +++ b/docs/diagrams/ModelClassDiagram.puml @@ -13,12 +13,23 @@ Class ModelManager Class UserPrefs Class UniquePersonList -Class Person -Class Address -Class Email +Class UniqueAppointmentList + +Class "{Abstract}\nPerson" as Person +Class Patient +Class Doctor + +Class Appointment +Class AppointmentDate +Class AppointmentID + +Class Type +Class NRIC Class Name +Class DoB Class Phone -Class Tag + +Class "<>\nType\n\nPatient\nDoctor" Class I #FFFFFF } @@ -36,19 +47,22 @@ ModelManager -right-> "1" UserPrefs UserPrefs .up.|> ReadOnlyUserPrefs AddressBook *--> "1" UniquePersonList -UniquePersonList --> "~* all" Person -Person *--> Name -Person *--> Phone -Person *--> Email -Person *--> Address -Person *--> "*" Tag +AddressBook *--> "1" UniqueAppointmentList +UniquePersonList ---> "~* all" Person +UniqueAppointmentList -down--> "~* all" Appointment +Person *---> Type +Person *---> NRIC +Person *---> Name +Person *---> DoB +Person *---> Phone -Person -[hidden]up--> I -UniquePersonList -[hidden]right-> I +Doctor .right.|> Person +Patient .--|> Person -Name -[hidden]right-> Phone -Phone -[hidden]right-> Address -Address -[hidden]right-> Email +Appointment *--> AppointmentDate +Appointment *--> AppointmentID +Appointment *--> "2(Patient & Doctor)" NRIC ModelManager --> "~* filtered" Person +ModelManager --> "~* filtered" Appointment @enduml diff --git a/docs/diagrams/QueryDoctorActivityDiagram.puml b/docs/diagrams/QueryDoctorActivityDiagram.puml new file mode 100644 index 00000000000..d9ab467d880 --- /dev/null +++ b/docs/diagrams/QueryDoctorActivityDiagram.puml @@ -0,0 +1,20 @@ +@startuml +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 + +start +:User enters command to query doctor(s); + + +if () then ([missing required fields]) + :Show error message\nfor missing required fields; +else ([else]) + :Search doctor(s) from person list; + :Update the 'person' panel\nin the GUI to display the list; + :Show success message\nwith queried doctor(s) information; +endif + + +stop +@enduml diff --git a/docs/diagrams/QueryDoctorAppointmentActivityDiagram.puml b/docs/diagrams/QueryDoctorAppointmentActivityDiagram.puml new file mode 100644 index 00000000000..c61fd3323e7 --- /dev/null +++ b/docs/diagrams/QueryDoctorAppointmentActivityDiagram.puml @@ -0,0 +1,20 @@ +@startuml +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 + +start +:User enters command to query appointment with the associated doctor; + + +if () then ([missing required fields]) + :Show error message\nfor missing required fields; +else ([else]) + :Search Appointments(s) from person list; + :Update the 'appointment' panel\nin the GUI to display the list; + :Show success message\nwith queried appointment information; +endif + + +stop +@enduml diff --git a/docs/diagrams/QueryPatientActivityDiagram.puml b/docs/diagrams/QueryPatientActivityDiagram.puml new file mode 100644 index 00000000000..813328b9bc5 --- /dev/null +++ b/docs/diagrams/QueryPatientActivityDiagram.puml @@ -0,0 +1,20 @@ +@startuml +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 + +start +:User enters command to query patient(s); + + +if () then ([missing required fields]) + :Show error message\nfor missing required fields; +else ([else]) + :Search patient(s) from person list; + :Update the 'person' panel\nin the GUI to display the list; + :Show success message\nwith queried patient(s) information; +endif + + +stop +@enduml diff --git a/docs/diagrams/QueryPatientAppointmentActivityDiagram.puml b/docs/diagrams/QueryPatientAppointmentActivityDiagram.puml new file mode 100644 index 00000000000..b57083e3ae4 --- /dev/null +++ b/docs/diagrams/QueryPatientAppointmentActivityDiagram.puml @@ -0,0 +1,20 @@ +@startuml +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 + +start +:User enters command to query appointments with associated patient; + + +if () then ([missing required fields]) + :Show error message\nfor missing required fields; +else ([else]) + :Search Appointment(s) from appointment list; + :Update the 'appointment' panel\nin the GUI to display the list; + :Show success message\nwith queried appointment information; +endif + + +stop +@enduml diff --git a/docs/diagrams/QueryPatientCommandExecuteSequenceDiagram.puml b/docs/diagrams/QueryPatientCommandExecuteSequenceDiagram.puml new file mode 100644 index 00000000000..42129318192 --- /dev/null +++ b/docs/diagrams/QueryPatientCommandExecuteSequenceDiagram.puml @@ -0,0 +1,44 @@ +@startuml +!include style.puml +skinparam ArrowFontStyle plain + +box Logic LOGIC_COLOR_T1 +participant ":QueryPatientCommand" as QueryPatientCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Commons LOGGER_COLOR_T1 +participant ":Logger" as Logger LOGGER_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> QueryPatientCommand : execute() +activate QueryPatientCommand + +QueryPatientCommand -> Model : updateFilteredPersonList(PatientContainsKeywordsPredicate) +activate Model + +Model --> QueryPatientCommand + +deactivate Model + +QueryPatientCommand -> Logger: log(Level.INFO, "Success") +activate Logger +Logger -> Logger: log +Logger -> QueryPatientCommand +deactivate Logger + +create CommandResult +QueryPatientCommand -> CommandResult +activate CommandResult + +CommandResult --> QueryPatientCommand : result +deactivate CommandResult + +[<--QueryPatientCommand : result +deactivate QueryPatientCommand + +@enduml diff --git a/docs/diagrams/QueryPersonActivityDiagram.puml b/docs/diagrams/QueryPersonActivityDiagram.puml new file mode 100644 index 00000000000..378a7567373 --- /dev/null +++ b/docs/diagrams/QueryPersonActivityDiagram.puml @@ -0,0 +1,20 @@ +@startuml +skin rose +skinparam ActivityFontSize 15 +skinparam ArrowFontSize 12 + +start +:User enters command to query doctor or patient; + + +if () then ([missing required fields]) + :Show error message\nfor missing required fields; +else ([else]) + :Search Doctor/Patient from person list; + :Update the 'person' panel\nin the GUI to display the list; + :Show success message\nwith removed doctor/patient information; +endif + + +stop +@enduml diff --git a/docs/diagrams/UiClassDiagram.puml b/docs/diagrams/UiClassDiagram.puml index 95473d5aa19..93202158bc5 100644 --- a/docs/diagrams/UiClassDiagram.puml +++ b/docs/diagrams/UiClassDiagram.puml @@ -13,6 +13,8 @@ Class HelpWindow Class ResultDisplay Class PersonListPanel Class PersonCard +Class AppointmentListPanel +Class AppointmentCard Class StatusBarFooter Class CommandBox } @@ -33,25 +35,32 @@ UiManager -down-> "1" MainWindow MainWindow *-down-> "1" CommandBox MainWindow *-down-> "1" ResultDisplay MainWindow *-down-> "1" PersonListPanel +MainWindow *-down-> "1" AppointmentListPanel MainWindow *-down-> "1" StatusBarFooter MainWindow --> "0..1" HelpWindow PersonListPanel -down-> "*" PersonCard +AppointmentListPanel -down-> "*" AppointmentCard + MainWindow -left-|> UiPart ResultDisplay --|> UiPart CommandBox --|> UiPart PersonListPanel --|> UiPart PersonCard --|> UiPart +AppointmentListPanel --|> UiPart +AppointmentCard --|> UiPart StatusBarFooter --|> UiPart HelpWindow --|> UiPart PersonCard ..> Model +AppointmentCard ..> Model UiManager -right-> Logic MainWindow -left-> Logic PersonListPanel -[hidden]left- HelpWindow +AppointmentListPanel -[hidden]left- HelpWindow HelpWindow -[hidden]left- CommandBox CommandBox -[hidden]left- ResultDisplay ResultDisplay -[hidden]left- StatusBarFooter diff --git a/docs/diagrams/UndoRedoState0.puml b/docs/diagrams/UndoRedoState0.puml index 43a45903ac9..64f8430c59d 100644 --- a/docs/diagrams/UndoRedoState0.puml +++ b/docs/diagrams/UndoRedoState0.puml @@ -7,9 +7,9 @@ skinparam ClassBackgroundColor #FFFFAA title Initial state package States { - class State1 as "ab0:AddressBook" - class State2 as "ab1:AddressBook" - class State3 as "ab2:AddressBook" + class State1 as "mc0:MediCLI" + class State2 as "mc1:MediCLI" + class State3 as "mc2:MediCLI" } State1 -[hidden]right-> State2 State2 -[hidden]right-> State3 diff --git a/docs/diagrams/UndoRedoState1.puml b/docs/diagrams/UndoRedoState1.puml index 5a41e9e1651..1fcb1c3215c 100644 --- a/docs/diagrams/UndoRedoState1.puml +++ b/docs/diagrams/UndoRedoState1.puml @@ -7,9 +7,9 @@ skinparam ClassBackgroundColor #FFFFAA title After command "delete 5" package States <> { - class State1 as "ab0:AddressBook" - class State2 as "ab1:AddressBook" - class State3 as "ab2:AddressBook" + class State1 as "mc0:MediCLI" + class State2 as "mc1:MediCLI" + class State3 as "mc2:MediCLI" } State1 -[hidden]right-> State2 diff --git a/docs/diagrams/UndoRedoState2.puml b/docs/diagrams/UndoRedoState2.puml index ad32fce1b0b..1e0d94380bd 100644 --- a/docs/diagrams/UndoRedoState2.puml +++ b/docs/diagrams/UndoRedoState2.puml @@ -4,12 +4,12 @@ skinparam ClassFontColor #000000 skinparam ClassBorderColor #000000 skinparam ClassBackgroundColor #FFFFAA -title After command "add n/David" +title After command "addpatient i/S1234567A n/John Doe d/2003-01-30 p/98765432" package States <> { - class State1 as "ab0:AddressBook" - class State2 as "ab1:AddressBook" - class State3 as "ab2:AddressBook" + class State1 as "mc0:MediCLI" + class State2 as "mc1:MediCLI" + class State3 as "mc2:MediCLI" } State1 -[hidden]right-> State2 diff --git a/docs/diagrams/UndoRedoState3.puml b/docs/diagrams/UndoRedoState3.puml index 9187a690036..995b7cc9a32 100644 --- a/docs/diagrams/UndoRedoState3.puml +++ b/docs/diagrams/UndoRedoState3.puml @@ -7,9 +7,9 @@ skinparam ClassBackgroundColor #FFFFAA title After command "undo" package States <> { - class State1 as "ab0:AddressBook" - class State2 as "ab1:AddressBook" - class State3 as "ab2:AddressBook" + class State1 as "mc0:MediCLI" + class State2 as "mc1:MediCLI" + class State3 as "mc2:MediCLI" } State1 -[hidden]right-> State2 diff --git a/docs/diagrams/UndoRedoState4.puml b/docs/diagrams/UndoRedoState4.puml index 2bc631ffcd0..687d79bc326 100644 --- a/docs/diagrams/UndoRedoState4.puml +++ b/docs/diagrams/UndoRedoState4.puml @@ -7,9 +7,9 @@ skinparam ClassBackgroundColor #FFFFAA title After command "list" package States <> { - class State1 as "ab0:AddressBook" - class State2 as "ab1:AddressBook" - class State3 as "ab2:AddressBook" + class State1 as "mc0:MediCLI" + class State2 as "mc1:MediCLI" + class State3 as "mc2:MediCLI" } State1 -[hidden]right-> State2 diff --git a/docs/diagrams/UndoRedoState5.puml b/docs/diagrams/UndoRedoState5.puml index e77b04104aa..28401717b69 100644 --- a/docs/diagrams/UndoRedoState5.puml +++ b/docs/diagrams/UndoRedoState5.puml @@ -7,9 +7,9 @@ skinparam ClassBackgroundColor #FFFFAA title After command "clear" package States <> { - class State1 as "ab0:AddressBook" - class State2 as "ab1:AddressBook" - class State3 as "ab3:AddressBook" + class State1 as "mc0:MediCLI" + class State2 as "mc1:MediCLI" + class State3 as "mc3:MediCLI" } State1 -[hidden]right-> State2 @@ -18,5 +18,5 @@ State2 -[hidden]right-> State3 class Pointer as "Current State" #FFFFFF Pointer -up-> State3 -note right on link: State ab2 deleted. +note right on link: State mc2 deleted. @end diff --git a/docs/diagrams/style.puml b/docs/diagrams/style.puml index f7d7347ae84..389181b855c 100644 --- a/docs/diagrams/style.puml +++ b/docs/diagrams/style.puml @@ -18,6 +18,8 @@ !define LOGIC_COLOR_T2 #6A6ADC !define LOGIC_COLOR_T3 #1616B0 !define LOGIC_COLOR_T4 #101086 +!define LOGIC_COLOR_T5 #3260a8 +!define LOGIC_COLOR_T6 #267afc !define MODEL_COLOR #9D0012 !define MODEL_COLOR_T1 #F97181 @@ -31,6 +33,9 @@ !define STORAGE_COLOR_T3 #806600 !define STORAGE_COLOR_T2 #544400 +!define LOGGER_COLOR #cc0099 +!define LOGGER_COLOR_T1 #ff99ff + !define USER_COLOR #000000 skinparam Package { diff --git a/docs/images/AddAppointmentActivityDiagram.png b/docs/images/AddAppointmentActivityDiagram.png new file mode 100644 index 00000000000..c2ae4c8ee7a Binary files /dev/null and b/docs/images/AddAppointmentActivityDiagram.png differ diff --git a/docs/images/AddAppointmentSequenceDiagram.png b/docs/images/AddAppointmentSequenceDiagram.png new file mode 100644 index 00000000000..0d47cb03de2 Binary files /dev/null and b/docs/images/AddAppointmentSequenceDiagram.png differ diff --git a/docs/images/AddPatientCommandExecuteSequenceDiagram.png b/docs/images/AddPatientCommandExecuteSequenceDiagram.png new file mode 100644 index 00000000000..073bc4758d9 Binary files /dev/null and b/docs/images/AddPatientCommandExecuteSequenceDiagram.png differ diff --git a/docs/images/AddPatientSequenceDiagram.png b/docs/images/AddPatientSequenceDiagram.png new file mode 100644 index 00000000000..e576baac12d Binary files /dev/null and b/docs/images/AddPatientSequenceDiagram.png differ diff --git a/docs/images/AddPersonActivityDiagram.png b/docs/images/AddPersonActivityDiagram.png new file mode 100644 index 00000000000..22935dca37d Binary files /dev/null and b/docs/images/AddPersonActivityDiagram.png differ diff --git a/docs/images/AppointmentClassDiagram.png b/docs/images/AppointmentClassDiagram.png new file mode 100644 index 00000000000..408e6153ef8 Binary files /dev/null and b/docs/images/AppointmentClassDiagram.png differ diff --git a/docs/images/AppointmentObjectDiagram.png b/docs/images/AppointmentObjectDiagram.png new file mode 100644 index 00000000000..206abc11891 Binary files /dev/null and b/docs/images/AppointmentObjectDiagram.png differ diff --git a/docs/images/ArchitectureSequenceDiagram.png b/docs/images/ArchitectureSequenceDiagram.png index 37ad06a2803..1f17d9d863b 100644 Binary files a/docs/images/ArchitectureSequenceDiagram.png and b/docs/images/ArchitectureSequenceDiagram.png differ diff --git a/docs/images/DeleteAppointmentActivityDiagram.png b/docs/images/DeleteAppointmentActivityDiagram.png new file mode 100644 index 00000000000..da75fc672b2 Binary files /dev/null and b/docs/images/DeleteAppointmentActivityDiagram.png differ diff --git a/docs/images/DeleteAppointmentSequenceDiagram.png b/docs/images/DeleteAppointmentSequenceDiagram.png new file mode 100644 index 00000000000..bed94321c27 Binary files /dev/null and b/docs/images/DeleteAppointmentSequenceDiagram.png differ diff --git a/docs/images/DeleteCommandExecuteSequenceDiagram.png b/docs/images/DeleteCommandExecuteSequenceDiagram.png new file mode 100644 index 00000000000..ccec3679b44 Binary files /dev/null and b/docs/images/DeleteCommandExecuteSequenceDiagram.png differ diff --git a/docs/images/DeletePersonActivityDiagram.png b/docs/images/DeletePersonActivityDiagram.png new file mode 100644 index 00000000000..18107c5e600 Binary files /dev/null and b/docs/images/DeletePersonActivityDiagram.png differ diff --git a/docs/images/DeletePersonSequenceDiagram.png b/docs/images/DeletePersonSequenceDiagram.png new file mode 100644 index 00000000000..08eca7e7914 Binary files /dev/null and b/docs/images/DeletePersonSequenceDiagram.png differ diff --git a/docs/images/EditAppointmentActivityDiagram.png b/docs/images/EditAppointmentActivityDiagram.png new file mode 100644 index 00000000000..0d614dfee6f Binary files /dev/null and b/docs/images/EditAppointmentActivityDiagram.png differ diff --git a/docs/images/EditAppointmentSequenceDiagram.png b/docs/images/EditAppointmentSequenceDiagram.png new file mode 100644 index 00000000000..b62244b81dc Binary files /dev/null and b/docs/images/EditAppointmentSequenceDiagram.png differ diff --git a/docs/images/EditCommandExecuteSequenceDiagram.png b/docs/images/EditCommandExecuteSequenceDiagram.png new file mode 100644 index 00000000000..be34ddf86df Binary files /dev/null and b/docs/images/EditCommandExecuteSequenceDiagram.png differ diff --git a/docs/images/EditPersonActivityDiagram.png b/docs/images/EditPersonActivityDiagram.png new file mode 100644 index 00000000000..9b343328aeb Binary files /dev/null and b/docs/images/EditPersonActivityDiagram.png differ diff --git a/docs/images/EditPersonSequenceDiagram.png b/docs/images/EditPersonSequenceDiagram.png new file mode 100644 index 00000000000..d7cc00f6833 Binary files /dev/null and b/docs/images/EditPersonSequenceDiagram.png differ diff --git a/docs/images/FindActivityDiagram.png b/docs/images/FindActivityDiagram.png new file mode 100644 index 00000000000..7bacc37593f Binary files /dev/null and b/docs/images/FindActivityDiagram.png differ diff --git a/docs/images/FindPersonSequenceDiagram.png b/docs/images/FindPersonSequenceDiagram.png new file mode 100644 index 00000000000..f11a348a14d Binary files /dev/null and b/docs/images/FindPersonSequenceDiagram.png differ diff --git a/docs/images/GUI.png b/docs/images/GUI.png new file mode 100644 index 00000000000..94e4a71ff05 Binary files /dev/null and b/docs/images/GUI.png differ diff --git a/docs/images/InitialState.png b/docs/images/InitialState.png new file mode 100644 index 00000000000..5186d193473 Binary files /dev/null and b/docs/images/InitialState.png differ diff --git a/docs/images/LogicClassDiagram.png b/docs/images/LogicClassDiagram.png index fe91c69efe7..8fcdf5c5ae2 100644 Binary files a/docs/images/LogicClassDiagram.png and b/docs/images/LogicClassDiagram.png differ diff --git a/docs/images/ModelClassDiagram.png b/docs/images/ModelClassDiagram.png index a19fb1b4ac8..b6c5d6436d5 100644 Binary files a/docs/images/ModelClassDiagram.png and b/docs/images/ModelClassDiagram.png differ diff --git a/docs/images/ModelClassDiagramHandDrawn.png b/docs/images/ModelClassDiagramHandDrawn.png new file mode 100644 index 00000000000..e3080c734a3 Binary files /dev/null and b/docs/images/ModelClassDiagramHandDrawn.png differ diff --git a/docs/images/QueryDoctorActivityDiagram.png b/docs/images/QueryDoctorActivityDiagram.png new file mode 100644 index 00000000000..627fa0d0c79 Binary files /dev/null and b/docs/images/QueryDoctorActivityDiagram.png differ diff --git a/docs/images/QueryDoctorAppointmentActivityDiagram.png b/docs/images/QueryDoctorAppointmentActivityDiagram.png new file mode 100644 index 00000000000..0e0ad807f9a Binary files /dev/null and b/docs/images/QueryDoctorAppointmentActivityDiagram.png differ diff --git a/docs/images/QueryPatientActivityDiagram.png b/docs/images/QueryPatientActivityDiagram.png new file mode 100644 index 00000000000..11efa4398b2 Binary files /dev/null and b/docs/images/QueryPatientActivityDiagram.png differ diff --git a/docs/images/QueryPatientAppointmentActivityDiagram.png b/docs/images/QueryPatientAppointmentActivityDiagram.png new file mode 100644 index 00000000000..b6687d568ac Binary files /dev/null and b/docs/images/QueryPatientAppointmentActivityDiagram.png differ diff --git a/docs/images/QueryPatientCommandExecuteSequenceDiagram.png b/docs/images/QueryPatientCommandExecuteSequenceDiagram.png new file mode 100644 index 00000000000..e6c2e4701e4 Binary files /dev/null and b/docs/images/QueryPatientCommandExecuteSequenceDiagram.png differ diff --git a/docs/images/QueryPersonActivityDiagram.png b/docs/images/QueryPersonActivityDiagram.png new file mode 100644 index 00000000000..d3944a6913d Binary files /dev/null and b/docs/images/QueryPersonActivityDiagram.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 5bd77847aa2..627fd729e05 100644 Binary files a/docs/images/Ui.png and b/docs/images/Ui.png differ diff --git a/docs/images/UiClassDiagram.png b/docs/images/UiClassDiagram.png index 11f06d68671..ae0200f11dd 100644 Binary files a/docs/images/UiClassDiagram.png and b/docs/images/UiClassDiagram.png differ diff --git a/docs/images/UndoRedoState0.png b/docs/images/UndoRedoState0.png index c5f91b58533..d570dd18346 100644 Binary files a/docs/images/UndoRedoState0.png and b/docs/images/UndoRedoState0.png differ diff --git a/docs/images/UndoRedoState1.png b/docs/images/UndoRedoState1.png index 2d3ad09c047..d877638cb61 100644 Binary files a/docs/images/UndoRedoState1.png and b/docs/images/UndoRedoState1.png differ diff --git a/docs/images/UndoRedoState2.png b/docs/images/UndoRedoState2.png index 20853694e03..23e36a0da30 100644 Binary files a/docs/images/UndoRedoState2.png and b/docs/images/UndoRedoState2.png differ diff --git a/docs/images/UndoRedoState3.png b/docs/images/UndoRedoState3.png index 1a9551b31be..dce5ddd2716 100644 Binary files a/docs/images/UndoRedoState3.png and b/docs/images/UndoRedoState3.png differ diff --git a/docs/images/UndoRedoState4.png b/docs/images/UndoRedoState4.png index 46dfae78c94..0bfd39ceb4b 100644 Binary files a/docs/images/UndoRedoState4.png and b/docs/images/UndoRedoState4.png differ diff --git a/docs/images/UndoRedoState5.png b/docs/images/UndoRedoState5.png index f45889b5fdf..c4c762824b4 100644 Binary files a/docs/images/UndoRedoState5.png and b/docs/images/UndoRedoState5.png differ diff --git a/docs/images/WindowsStartup.png b/docs/images/WindowsStartup.png new file mode 100644 index 00000000000..7d88d37e5fc Binary files /dev/null and b/docs/images/WindowsStartup.png differ diff --git a/docs/images/addAppointment.png b/docs/images/addAppointment.png new file mode 100644 index 00000000000..87752eeb579 Binary files /dev/null and b/docs/images/addAppointment.png differ diff --git a/docs/images/addDoctor.png b/docs/images/addDoctor.png new file mode 100644 index 00000000000..a775b0ba894 Binary files /dev/null and b/docs/images/addDoctor.png differ diff --git a/docs/images/addPatient.png b/docs/images/addPatient.png new file mode 100644 index 00000000000..01c07a2fff4 Binary files /dev/null and b/docs/images/addPatient.png differ diff --git a/docs/images/alfaloo.png b/docs/images/alfaloo.png new file mode 100644 index 00000000000..0cf10ba14dd Binary files /dev/null and b/docs/images/alfaloo.png differ diff --git a/docs/images/alteqa.png b/docs/images/alteqa.png new file mode 100644 index 00000000000..96d2732e72f Binary files /dev/null and b/docs/images/alteqa.png differ diff --git a/docs/images/ararchch.png b/docs/images/ararchch.png new file mode 100644 index 00000000000..ab636e04c8a Binary files /dev/null and b/docs/images/ararchch.png differ diff --git a/docs/images/clear.png b/docs/images/clear.png new file mode 100644 index 00000000000..5ae8c9a0066 Binary files /dev/null and b/docs/images/clear.png differ diff --git a/docs/images/cli_format.png b/docs/images/cli_format.png new file mode 100644 index 00000000000..73351fe716e Binary files /dev/null and b/docs/images/cli_format.png differ diff --git a/docs/images/deleteApptFinalState.png b/docs/images/deleteApptFinalState.png new file mode 100644 index 00000000000..a08b804ff3e Binary files /dev/null and b/docs/images/deleteApptFinalState.png differ diff --git a/docs/images/deleteApptInitialState.png b/docs/images/deleteApptInitialState.png new file mode 100644 index 00000000000..69699e07a77 Binary files /dev/null and b/docs/images/deleteApptInitialState.png differ diff --git a/docs/images/deletePerson.png b/docs/images/deletePerson.png new file mode 100644 index 00000000000..b9261670c20 Binary files /dev/null and b/docs/images/deletePerson.png differ diff --git a/docs/images/editAppointment.png b/docs/images/editAppointment.png new file mode 100644 index 00000000000..f5bcdf51f6c Binary files /dev/null and b/docs/images/editAppointment.png differ diff --git a/docs/images/editPerson.png b/docs/images/editPerson.png new file mode 100644 index 00000000000..189b2db43eb Binary files /dev/null and b/docs/images/editPerson.png differ diff --git a/docs/images/findAlexDavidResult.png b/docs/images/findAlexDavidResult.png deleted file mode 100644 index 235da1c273e..00000000000 Binary files a/docs/images/findAlexDavidResult.png and /dev/null differ diff --git a/docs/images/findAlexDavidResultDoctor.png b/docs/images/findAlexDavidResultDoctor.png new file mode 100644 index 00000000000..069a7203739 Binary files /dev/null and b/docs/images/findAlexDavidResultDoctor.png differ diff --git a/docs/images/findAlexDavidResultPatient.png b/docs/images/findAlexDavidResultPatient.png new file mode 100644 index 00000000000..46fbea794e3 Binary files /dev/null and b/docs/images/findAlexDavidResultPatient.png differ diff --git a/docs/images/findAppointmentInitialDoctor.png b/docs/images/findAppointmentInitialDoctor.png new file mode 100644 index 00000000000..5b641f46782 Binary files /dev/null and b/docs/images/findAppointmentInitialDoctor.png differ diff --git a/docs/images/findAppointmentInitialPatient.png b/docs/images/findAppointmentInitialPatient.png new file mode 100644 index 00000000000..6cd9b331957 Binary files /dev/null and b/docs/images/findAppointmentInitialPatient.png differ diff --git a/docs/images/findAppointmentResultDoctor.png b/docs/images/findAppointmentResultDoctor.png new file mode 100644 index 00000000000..26aeabd94d4 Binary files /dev/null and b/docs/images/findAppointmentResultDoctor.png differ diff --git a/docs/images/findAppointmentResultPatient.png b/docs/images/findAppointmentResultPatient.png new file mode 100644 index 00000000000..62b2f9b43a1 Binary files /dev/null and b/docs/images/findAppointmentResultPatient.png differ diff --git a/docs/images/findDoctor.png b/docs/images/findDoctor.png new file mode 100644 index 00000000000..1f9d194c1ea Binary files /dev/null and b/docs/images/findDoctor.png differ diff --git a/docs/images/findPatient.png b/docs/images/findPatient.png new file mode 100644 index 00000000000..eaaf0fb3118 Binary files /dev/null and b/docs/images/findPatient.png differ diff --git a/docs/images/findPerson.png b/docs/images/findPerson.png new file mode 100644 index 00000000000..e96a653be0d Binary files /dev/null and b/docs/images/findPerson.png differ diff --git a/docs/images/helpMessage.png b/docs/images/helpMessage.png index b1f70470137..735f47f9ed3 100644 Binary files a/docs/images/helpMessage.png and b/docs/images/helpMessage.png differ diff --git a/docs/images/kappaccinoh.png b/docs/images/kappaccinoh.png new file mode 100644 index 00000000000..12f09fd9722 Binary files /dev/null and b/docs/images/kappaccinoh.png differ diff --git a/docs/images/macOSStartup.png b/docs/images/macOSStartup.png new file mode 100644 index 00000000000..9739e394f30 Binary files /dev/null and b/docs/images/macOSStartup.png differ diff --git a/docs/images/medicli_logo.png b/docs/images/medicli_logo.png new file mode 100644 index 00000000000..ce6be66f767 Binary files /dev/null and b/docs/images/medicli_logo.png differ diff --git a/docs/images/officialchengyud.png b/docs/images/officialchengyud.png new file mode 100644 index 00000000000..1dde8cc8f27 Binary files /dev/null and b/docs/images/officialchengyud.png differ diff --git a/docs/index.md b/docs/index.md index 7601dbaad0d..8494ebd01cb 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,6 +1,6 @@ --- layout: page -title: AddressBook Level-3 +title: MediCLI --- [![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). +**MediCLI is a desktop application for managing your contact details.** While it has a GUI, most of the user interactions happen using a CLI (Command Line Interface). -* If you are interested in using AddressBook, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). -* If you are interested about developing AddressBook, the [**Developer Guide**](DeveloperGuide.html) is a good place to start. +* If you are interested in using MediCLI, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). +* If you are interested about developing MediCLI, the [**Developer Guide**](DeveloperGuide.html) is a good place to start. **Acknowledgements** diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java index 3d6bd06d5af..f9274615d95 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/seedu/address/MainApp.java @@ -21,6 +21,10 @@ import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.ReadOnlyUserPrefs; import seedu.address.model.UserPrefs; +import seedu.address.model.appointment.Appointment; +import seedu.address.model.person.Person; +import seedu.address.model.person.Type; +import seedu.address.model.person.exceptions.PersonNotFoundException; import seedu.address.model.util.SampleDataUtil; import seedu.address.storage.AddressBookStorage; import seedu.address.storage.JsonAddressBookStorage; @@ -36,7 +40,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, 2, 1, true); private static final Logger logger = LogsCenter.getLogger(MainApp.class); @@ -84,6 +88,20 @@ private Model initModelManager(Storage storage, ReadOnlyUserPrefs userPrefs) { + " populated with a sample AddressBook."); } initialData = addressBookOptional.orElseGet(SampleDataUtil::getSampleAddressBook); + AddressBook ab = (AddressBook) initialData; + for (Appointment appt : initialData.getAppointmentList()) { + try { + Person doctor = ab.getPersonByNric(appt.getDoctorNric()); + Person patient = ab.getPersonByNric(appt.getPatientNric()); + + if (!(doctor.getType() == Type.DOCTOR) || !(patient.getType() == Type.PATIENT)) { + throw new PersonNotFoundException(); + } + } catch (PersonNotFoundException e) { + initialData = new AddressBook(); + break; + } + } } catch (DataLoadingException e) { logger.warning("Data file at " + storage.getAddressBookFilePath() + " could not be loaded." + " Will be starting with an empty AddressBook."); diff --git a/src/main/java/seedu/address/commons/core/LogsCenter.java b/src/main/java/seedu/address/commons/core/LogsCenter.java index 8cf8e15a0f0..aa8a6405cfc 100644 --- a/src/main/java/seedu/address/commons/core/LogsCenter.java +++ b/src/main/java/seedu/address/commons/core/LogsCenter.java @@ -20,7 +20,7 @@ public class LogsCenter { private static final int MAX_FILE_COUNT = 5; private static final int MAX_FILE_SIZE_IN_BYTES = (int) (Math.pow(2, 20) * 5); // 5MB - private static final String LOG_FILE = "addressbook.log"; + private static final String LOG_FILE = "medicli.log"; private static final Logger logger; // logger for this class private static Logger baseLogger; // to be used as the parent of all other loggers created by this class. private static Level currentLogLevel = Level.INFO; diff --git a/src/main/java/seedu/address/commons/util/IdUtil.java b/src/main/java/seedu/address/commons/util/IdUtil.java new file mode 100644 index 00000000000..2d5109369bb --- /dev/null +++ b/src/main/java/seedu/address/commons/util/IdUtil.java @@ -0,0 +1,128 @@ +package seedu.address.commons.util; + +import static java.util.Objects.requireNonNull; + +import java.util.EnumMap; +import java.util.HashSet; +import java.util.Random; + +import jdk.jshell.spi.ExecutionControl; + +/** + * Generates unique String IDs for patients, doctors, and appointments. + * + * At the moment the ID util is underutilised, but we have kept it in the code for future adaptation. + * When the class was orignially defined, we envisioned it being essential to our use case, however, as development + * progressed, we realised that it would not add much significant value at least until v1.4 of our product. + * However, it can serve a purpose down the line, so we have left it in, despite it not being fleshed out completely. + * + * Currently only appointments are assigned an ID upon being created, but they ID itself does not serve a purpose. + */ +public class IdUtil { + + /** + * Enum containing all possible entity types in our system. + * + * Associated characters are the first letter of each type of entity. + */ + public enum Entities { + PATIENT("p"), + DOCTOR("d"), + APPOINTMENT("a"); + + private final String letter; + Entities(String letter) { + this.letter = letter; + } + + /** + * Returns letter associated with entity. + * @return String letter + */ + public String getLetter() { + return letter; + } + + /** + * Gets entity object associated with character. + * + * @param c character in question + * @return Entities entity object associated with input character + */ + protected static Entities getEntityFromChar(char c) { + if (c == 'a') { + return Entities.APPOINTMENT; + } else if (c == 'p') { + return Entities.PATIENT; + } else if (c == 'd') { + return Entities.DOCTOR; + } + throw new IllegalArgumentException("Invalid character input - no corresponding entity"); + } + } + + // EnumMap storing entities and their corresponding used up ids. + private static final EnumMap> allIds = new EnumMap<>(Entities.class); + + /** + * Generates a new id based on input entity. + * + * @param entity type of id to generate. + * @return String id. + */ + public static String generateNewId(Entities entity) { + HashSet idSet = allIds.get(entity); + if (idSet == null) { + idSet = new HashSet<>(); + allIds.put(entity, idSet); + } + + Random random = new Random(); + String initId = String.valueOf(random.nextInt(90000000) + 10000000); + while (idSet.contains(initId)) { + initId = String.valueOf(random.nextInt(90000000) + 10000000);; + } + + idSet.add(initId); + assert initId.length() == 8 : "All numeric portions of IDs must be 8 digits long"; + + return entity.getLetter() + initId; + } + + /** + * Deletes Id that is inputted. + * + * @param id String id to delete. + */ + public static void deleteId(String id) { + requireNonNull(id); + char firstChar = id.substring(0, 1).charAt(0); + assert firstChar == 'a' || firstChar == 'p' || firstChar == 'd' : "IDs can only start with these 3 letters"; + Entities entity = Entities.getEntityFromChar(firstChar); + HashSet idSet = allIds.get(entity); + idSet.remove(id.substring(1, id.length())); + } + + /** + * Returns allIds as an unmodifiable map. + * + * @return unmodifiable map containing ids. + */ + public static boolean hasId(String id) { + requireNonNull(id); + Entities entity = Entities.getEntityFromChar(id.substring(0, 1).charAt(0)); + HashSet idSet = allIds.get(entity); + return idSet.contains(id.substring(1, id.length())); + } + + /** + * Updates map with initial values from storage. + * To be implemented in the future as it does not affect or impact current functionality. + * + * @throws ExecutionControl.NotImplementedException as it is not implemented yet and shouldn't be used. + */ + public static void initalMapUpdate() throws ExecutionControl.NotImplementedException { + throw new ExecutionControl.NotImplementedException("to be implemented"); + } + +} diff --git a/src/main/java/seedu/address/commons/util/StringUtil.java b/src/main/java/seedu/address/commons/util/StringUtil.java index 61cc8c9a1cb..a5ab841bdaa 100644 --- a/src/main/java/seedu/address/commons/util/StringUtil.java +++ b/src/main/java/seedu/address/commons/util/StringUtil.java @@ -38,6 +38,28 @@ public static boolean containsWordIgnoreCase(String sentence, String word) { .anyMatch(preppedWord::equalsIgnoreCase); } + /** + * Checks if the given sentence contains the specified substring, ignoring case. + * + * @param sentence The sentence to search within. + * @param substring The substring to search for. + * @return True if the sentence contains the substring, ignoring case; false otherwise. + * @throws NullPointerException if either the sentence or the substring is null. + * @throws IllegalArgumentException if the substring parameter is empty. + */ + public static boolean containsSubstringIgnoreCase(String sentence, String substring) { + requireNonNull(sentence); + requireNonNull(substring); + + String preppedSubstring = substring.trim(); + checkArgument(!preppedSubstring.isEmpty(), "Substring parameter cannot be empty"); + + String preppedSentence = sentence.toLowerCase(); + String preppedSubstringLower = preppedSubstring.toLowerCase(); + + return preppedSentence.contains(preppedSubstringLower); + } + /** * Returns a detailed message of the t, including the stack trace. */ diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java index 92cd8fa605a..ea6ca8f527a 100644 --- a/src/main/java/seedu/address/logic/Logic.java +++ b/src/main/java/seedu/address/logic/Logic.java @@ -8,6 +8,7 @@ import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.appointment.Appointment; import seedu.address.model.person.Person; /** @@ -33,6 +34,9 @@ public interface Logic { /** Returns an unmodifiable view of the filtered list of persons */ ObservableList getFilteredPersonList(); + /** Returns an unmodifiable view of the filtered list of appointments */ + ObservableList getFilteredAppointmentList(); + /** * Returns the user prefs' address book file path. */ diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java index 5aa3b91c7d0..3d89f79cf82 100644 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ b/src/main/java/seedu/address/logic/LogicManager.java @@ -15,6 +15,7 @@ import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.Model; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.appointment.Appointment; import seedu.address.model.person.Person; import seedu.address.storage.Storage; @@ -45,7 +46,6 @@ public LogicManager(Model model, Storage storage) { @Override public CommandResult execute(String commandText) throws CommandException, ParseException { logger.info("----------------[USER COMMAND][" + commandText + "]"); - CommandResult commandResult; Command command = addressBookParser.parseCommand(commandText); commandResult = command.execute(model); @@ -71,6 +71,11 @@ public ObservableList getFilteredPersonList() { return model.getFilteredPersonList(); } + @Override + public ObservableList getFilteredAppointmentList() { + return model.getFilteredAppointmentList(); + } + @Override public Path getAddressBookFilePath() { return model.getAddressBookFilePath(); diff --git a/src/main/java/seedu/address/logic/Messages.java b/src/main/java/seedu/address/logic/Messages.java index ecd32c31b53..991cd29fa28 100644 --- a/src/main/java/seedu/address/logic/Messages.java +++ b/src/main/java/seedu/address/logic/Messages.java @@ -5,6 +5,7 @@ import java.util.stream.Stream; import seedu.address.logic.parser.Prefix; +import seedu.address.model.appointment.Appointment; import seedu.address.model.person.Person; /** @@ -15,10 +16,14 @@ 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_APPOINTMENT_DISPLAYED_INDEX = + "The appointment index 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_APPOINTMENTS_LISTED_OVERVIEW = "%1$d appointments listed!"; + /** * Returns an error message indicating the duplicate prefixes. */ @@ -36,16 +41,32 @@ public static String getErrorMessageForDuplicatePrefixes(Prefix... duplicatePref */ public static String format(Person person) { final StringBuilder builder = new StringBuilder(); - builder.append(person.getName()) + builder.append("NRIC: ") + .append(person.getNric()) + .append("; Name: ") + .append(person.getName()) + .append("; DoB: ") + .append(person.getDoB()) .append("; Phone: ") .append(person.getPhone()) - .append("; Email: ") - .append(person.getEmail()) - .append("; Address: ") - .append(person.getAddress()) - .append("; Tags: "); - person.getTags().forEach(builder::append); + .append(";"); return builder.toString(); } + /** + * Formats appointment for display in result box. + * + * @param appointment the appointment in question. + * @return String formatted string as per requirements. + */ + public static String format(Appointment appointment) { + final StringBuilder builder = new StringBuilder(); + builder.append("Date: ") + .append(appointment.getAppointmentDateTime()) + .append("; Doctor: ") + .append(appointment.getDoctorNric()) + .append("; Patient: ") + .append(appointment.getPatientNric()); + return builder.toString(); + } } diff --git a/src/main/java/seedu/address/logic/commands/AddAppointmentCommand.java b/src/main/java/seedu/address/logic/commands/AddAppointmentCommand.java new file mode 100644 index 00000000000..346435a266a --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AddAppointmentCommand.java @@ -0,0 +1,106 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DOCTORNRIC; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PATIENTNRIC; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +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.appointment.Appointment; +import seedu.address.model.appointment.exceptions.InvalidAppointmentException; +import seedu.address.model.person.exceptions.PersonNotFoundException; + +/** + * Command to add an appointment to MediCLI + */ +public class AddAppointmentCommand extends Command { + + + public static final String COMMAND_WORD = "addappt"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds an appointment to the MediCLI system.\n" + + "Parameters: " + + PREFIX_DATE + "DATE-TIME " + + PREFIX_DOCTORNRIC + "DOCTOR NRIC " + + PREFIX_PATIENTNRIC + "PATIENT NRIC\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_DATE + "2024-04-09 10:15 " + + PREFIX_DOCTORNRIC + "S7777888T " + + PREFIX_PATIENTNRIC + "T0000111U"; + + public static final String MESSAGE_SUCCESS = "New Appointment added: %1$s"; + public static final String MESSAGE_DUPLICATE_APPOINTMENT = "This appointment already exists in the MediCLI"; + + private static final Logger logger = LogsCenter.getLogger(AddAppointmentCommand.class); + + private final Appointment toAdd; + + /** + * Creates an AddCommand to add the specified {@code Appointment} + */ + public AddAppointmentCommand(Appointment appointment) { + requireNonNull(appointment); + toAdd = appointment; + } + + /** + * Method that executes command when called by performing checks then adding to the list. + * + * @param model the model in which the command is executed + * @return CommandResult resulting from command execution + */ + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + logger.log(Level.INFO, "going to add appointment"); + + if (model.hasAppointment(toAdd)) { + logger.log(Level.INFO, "appointment was not added as it is in system"); + throw new CommandException(MESSAGE_DUPLICATE_APPOINTMENT); + } + + try { + logger.log(Level.INFO, "checking if appointment is valid"); + if (!model.isValidAppointment(toAdd)) { + logger.log(Level.INFO, "appointment was not added as it is invalid"); + throw new InvalidAppointmentException(); + } + } catch (PersonNotFoundException e) { + throw new CommandException("The provided Doctor / Patient is not registered in the system"); + } + + model.addAppointment(toAdd); + logger.log(Level.INFO, "appointment was added to system"); + return new CommandResult(String.format(MESSAGE_SUCCESS, Messages.format(toAdd))); + } + + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof AddAppointmentCommand)) { + return false; + } + + AddAppointmentCommand otherAddCommand = (AddAppointmentCommand) other; + return toAdd.isSameAppointment(otherAddCommand.toAdd); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("toAdd", toAdd) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddDoctorCommand.java similarity index 56% rename from src/main/java/seedu/address/logic/commands/AddCommand.java rename to src/main/java/seedu/address/logic/commands/AddDoctorCommand.java index 5d7185a9680..35db815f331 100644 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ b/src/main/java/seedu/address/logic/commands/AddDoctorCommand.java @@ -1,51 +1,52 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; -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_DOB; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NRIC; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import java.util.logging.Level; +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; 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.Doctor; /** * Adds a person to the address book. */ -public class AddCommand extends Command { +public class AddDoctorCommand extends Command { - public static final String COMMAND_WORD = "add"; + public static final String COMMAND_WORD = "adddoctor"; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a person to the address book. " + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a doctor to the address book.\n" + "Parameters: " + + PREFIX_NRIC + "NRIC " + PREFIX_NAME + "NAME " - + PREFIX_PHONE + "PHONE " - + PREFIX_EMAIL + "EMAIL " - + PREFIX_ADDRESS + "ADDRESS " - + "[" + PREFIX_TAG + "TAG]...\n" + + PREFIX_DOB + "DOB " + + PREFIX_PHONE + "PHONE\n" + "Example: " + COMMAND_WORD + " " + + PREFIX_NRIC + "S1234567A " + 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_DOB + "2003-01-30 " + + PREFIX_PHONE + "98765432"; - public static final String MESSAGE_SUCCESS = "New person added: %1$s"; + public static final String MESSAGE_SUCCESS = "New doctor added: %1$s"; public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book"; + private static Logger logger = LogsCenter.getLogger(AddDoctorCommand.class); - private final Person toAdd; + private final Doctor toAdd; /** * Creates an AddCommand to add the specified {@code Person} */ - public AddCommand(Person person) { - requireNonNull(person); - toAdd = person; + public AddDoctorCommand(Doctor doctor) { + requireNonNull(doctor); + toAdd = doctor; } @Override @@ -53,10 +54,12 @@ public CommandResult execute(Model model) throws CommandException { requireNonNull(model); if (model.hasPerson(toAdd)) { + logger.log(Level.INFO, "Duplicate person detected! (when executing command: adddoctor)"); throw new CommandException(MESSAGE_DUPLICATE_PERSON); } model.addPerson(toAdd); + logger.log(Level.INFO, "Doctor successfully added (when executing command: adddoctor)"); return new CommandResult(String.format(MESSAGE_SUCCESS, Messages.format(toAdd))); } @@ -67,11 +70,11 @@ public boolean equals(Object other) { } // instanceof handles nulls - if (!(other instanceof AddCommand)) { + if (!(other instanceof AddDoctorCommand)) { return false; } - AddCommand otherAddCommand = (AddCommand) other; + AddDoctorCommand otherAddCommand = (AddDoctorCommand) other; return toAdd.equals(otherAddCommand.toAdd); } diff --git a/src/main/java/seedu/address/logic/commands/AddPatientCommand.java b/src/main/java/seedu/address/logic/commands/AddPatientCommand.java new file mode 100644 index 00000000000..cfae9130358 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AddPatientCommand.java @@ -0,0 +1,87 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DOB; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NRIC; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +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.Patient; + +/** + * Adds a person to the address book. + */ +public class AddPatientCommand extends Command { + + public static final String COMMAND_WORD = "addpatient"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a patient to the address book.\n" + + "Parameters: " + + PREFIX_NRIC + "NRIC " + + PREFIX_NAME + "NAME " + + PREFIX_DOB + "DOB " + + PREFIX_PHONE + "PHONE\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_NRIC + "S1234567A " + + PREFIX_NAME + "John Doe " + + PREFIX_DOB + "2003-01-30 " + + PREFIX_PHONE + "98765432"; + + public static final String MESSAGE_SUCCESS = "New patient added: %1$s"; + public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book"; + private static Logger logger = LogsCenter.getLogger(AddPatientCommand.class); + + private final Patient toAdd; + + /** + * Creates an AddCommand to add the specified {@code Person} + */ + public AddPatientCommand(Patient patient) { + requireNonNull(patient); + toAdd = patient; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + if (model.hasPerson(toAdd)) { + logger.log(Level.INFO, "Duplicate person detected! (when executing command: addpatient)"); + throw new CommandException(MESSAGE_DUPLICATE_PERSON); + } + + model.addPerson(toAdd); + logger.log(Level.INFO, "Patient successfully added (when executing command: addpatient)"); + return new CommandResult(String.format(MESSAGE_SUCCESS, Messages.format(toAdd))); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof AddPatientCommand)) { + return false; + } + + AddPatientCommand otherAddCommand = (AddPatientCommand) other; + return toAdd.equals(otherAddCommand.toAdd); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("toAdd", toAdd) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/address/logic/commands/ClearCommand.java index 9c86b1fa6e4..77d48cff31f 100644 --- a/src/main/java/seedu/address/logic/commands/ClearCommand.java +++ b/src/main/java/seedu/address/logic/commands/ClearCommand.java @@ -11,7 +11,7 @@ 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_SUCCESS = "MediCLI's storage has been cleared!"; @Override diff --git a/src/main/java/seedu/address/logic/commands/DeleteAppointmentCommand.java b/src/main/java/seedu/address/logic/commands/DeleteAppointmentCommand.java new file mode 100644 index 00000000000..5235d2e550f --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/DeleteAppointmentCommand.java @@ -0,0 +1,78 @@ +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 seedu.address.Main; +import seedu.address.commons.core.LogsCenter; +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.appointment.Appointment; +/** + * Deletes an appointment identified using it's displayed index from mediCLI. + */ +public class DeleteAppointmentCommand extends Command { + + public static final String COMMAND_WORD = "deleteappt"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Deletes the appointment identified by the index number used in the displayed appointment list.\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_DELETE_APPOINTMENT_SUCCESS = "Deleted Appointment: %1$s"; + private static Logger logger = LogsCenter.getLogger(Main.class); + + private final Index targetIndex; + + public DeleteAppointmentCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredAppointmentList(); + + // Check for valid index + if (targetIndex.getZeroBased() >= lastShownList.size()) { + logger.log(Level.INFO, "Specified index is not valid! (when executing command: deleteappt)"); + throw new CommandException(Messages.MESSAGE_INVALID_APPOINTMENT_DISPLAYED_INDEX); + } + + Appointment appointmentToDelete = lastShownList.get(targetIndex.getZeroBased()); + + model.deleteAppointment(appointmentToDelete); + + return new CommandResult(String.format(MESSAGE_DELETE_APPOINTMENT_SUCCESS, + Messages.format(appointmentToDelete))); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof DeleteAppointmentCommand)) { + return false; + } + + DeleteAppointmentCommand otherDeleteCommand = (DeleteAppointmentCommand) other; + return targetIndex.equals(otherDeleteCommand.targetIndex); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("targetIndex", targetIndex) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java index 1135ac19b74..64416d8ab8f 100644 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ b/src/main/java/seedu/address/logic/commands/DeleteCommand.java @@ -3,13 +3,18 @@ import static java.util.Objects.requireNonNull; import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import seedu.address.Main; +import seedu.address.commons.core.LogsCenter; 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.Type; /** * Deletes a person identified using it's displayed index from the address book. @@ -20,10 +25,13 @@ public class DeleteCommand extends Command { public static final String MESSAGE_USAGE = COMMAND_WORD + ": Deletes the person identified by the index number used in the displayed person list.\n" + + "*Note that this also deletes all appointments associated with the person in the appointments list.\n" + "Parameters: INDEX (must be a positive integer)\n" + "Example: " + COMMAND_WORD + " 1"; - public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted Person: %1$s"; + public static final String MESSAGE_DELETE_PATIENT_SUCCESS = "Deleted Patient: %1$s"; + public static final String MESSAGE_DELETE_DOCTOR_SUCCESS = "Deleted Doctor: %1$s"; + private static Logger logger = LogsCenter.getLogger(Main.class); private final Index targetIndex; @@ -37,12 +45,17 @@ public CommandResult execute(Model model) throws CommandException { List lastShownList = model.getFilteredPersonList(); if (targetIndex.getZeroBased() >= lastShownList.size()) { + logger.log(Level.INFO, "Specified index is not valid! (when executing command: delete)"); throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); } Person personToDelete = lastShownList.get(targetIndex.getZeroBased()); model.deletePerson(personToDelete); - return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, Messages.format(personToDelete))); + logger.log(Level.INFO, "Person succesfully deleted. (when executing command: delete)"); + String message = (personToDelete.getType() == Type.PATIENT + ? MESSAGE_DELETE_PATIENT_SUCCESS + : MESSAGE_DELETE_DOCTOR_SUCCESS); + return new CommandResult(String.format(message, Messages.format(personToDelete))); } @Override diff --git a/src/main/java/seedu/address/logic/commands/EditAppointmentCommand.java b/src/main/java/seedu/address/logic/commands/EditAppointmentCommand.java new file mode 100644 index 00000000000..19ec65e7dae --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/EditAppointmentCommand.java @@ -0,0 +1,214 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_APPOINTMENTS; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; + +import seedu.address.Main; +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.CollectionUtil; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.Model; +import seedu.address.model.appointment.Appointment; +import seedu.address.model.appointment.AppointmentDateTime; +import seedu.address.model.person.Nric; + +/** + * Edits the details of an existing person in the address book. + */ +public class EditAppointmentCommand extends Command { + + public static final String COMMAND_WORD = "editappt"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the details of the appointment identified " + + "by the index number used in the displayed appointment list. " + + "Existing values will be overwritten by the input values.\n" + + "Parameters: INDEX (must be a positive integer) " + + PREFIX_DATE + "DATE\n" + + "Example: " + COMMAND_WORD + " 1 " + + PREFIX_DATE + "2024-04-09 10:00"; + + public static final String MESSAGE_EDIT_APPOINTMENT_SUCCESS = "Edited Appointment: %1$s"; + public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; + public static final String MESSAGE_DUPLICATE_APPOINTMENT = "This appointment already exists in the address book."; + private static Logger logger = LogsCenter.getLogger(Main.class); + + private final Index index; + private final EditAppointmentDescriptor editAppointmentDescriptor; + + /** + * @param index of the person in the filtered person list to edit + * @param editAppointmentDescriptor details to edit the person with + */ + public EditAppointmentCommand(Index index, EditAppointmentDescriptor editAppointmentDescriptor) { + requireNonNull(index); + requireNonNull(editAppointmentDescriptor); + + this.index = index; + this.editAppointmentDescriptor = new EditAppointmentDescriptor(editAppointmentDescriptor); + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredAppointmentList(); + + if (index.getZeroBased() >= lastShownList.size()) { + logger.log(Level.INFO, "Specified index is not valid! (when executing command: editappt)"); + throw new CommandException(Messages.MESSAGE_INVALID_APPOINTMENT_DISPLAYED_INDEX); + } + + Appointment appointmentToEdit = lastShownList.get(index.getZeroBased()); + + Appointment editedAppointment = createEditedAppointment(appointmentToEdit, editAppointmentDescriptor); + if (appointmentToEdit.isSameAppointment(editedAppointment) && model.hasAppointment(editedAppointment)) { + logger.log(Level.INFO, "Duplicate appointment detected! (when executing command: editappt)"); + throw new CommandException(MESSAGE_DUPLICATE_APPOINTMENT); + } + + model.setAppointment(appointmentToEdit, editedAppointment); + model.updateFilteredAppointmentList(PREDICATE_SHOW_ALL_APPOINTMENTS); + logger.log(Level.INFO, "Edit appointment success! (when executing command: editappt)"); + return new CommandResult(String.format(MESSAGE_EDIT_APPOINTMENT_SUCCESS, Messages.format(editedAppointment))); + } + + /** + * Creates and returns a {@code Appointment} with modified details of {@code appointmentToEdit}. + * Modified appointment is edited with {@code editAppointmentDescriptor}. + * + * @param appointmentToEdit the appointment to edit. + * @param editAppointmentDescriptor the descriptor to edit according to. + * @return Appointment with the details of appointmentToEdit. + * @throws CommandException if new inputs are invalid. + */ + private static Appointment createEditedAppointment( + Appointment appointmentToEdit, + EditAppointmentDescriptor editAppointmentDescriptor) throws CommandException { + assert appointmentToEdit != null; + + Nric doctorNric = appointmentToEdit.getDoctorNric(); + Nric patientNric = appointmentToEdit.getPatientNric(); + AppointmentDateTime updatedDateTime = editAppointmentDescriptor + .getDate().orElse(appointmentToEdit.getAppointmentDateTime()); + + try { + return new Appointment(doctorNric, patientNric, updatedDateTime, false); + } catch (ParseException e) { + throw new CommandException("Unable to edit appointment due to invalid inputs"); + } + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditAppointmentCommand)) { + return false; + } + + EditAppointmentCommand otherEditAppointmentCommand = (EditAppointmentCommand) other; + return index.equals(otherEditAppointmentCommand.index) + && editAppointmentDescriptor.equals(otherEditAppointmentCommand.editAppointmentDescriptor); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("index", index) + .add("editAppointmentDescriptor", editAppointmentDescriptor) + .toString(); + } + + /** + * Stores the details to edit the appointment with. Each non-empty field value will replace the + * corresponding field value of the person. + */ + public static class EditAppointmentDescriptor { + private Nric doctorNric; + private Nric patientNric; + private AppointmentDateTime apptdatetime; + + public EditAppointmentDescriptor() {} + + /** + * Copy constructor. + * A defensive copy of {@code tags} is used internally. + */ + public EditAppointmentDescriptor(EditAppointmentDescriptor toCopy) { + if (toCopy == null) { // Defensive Coding + throw new IllegalArgumentException(); + } + setDateTime(toCopy.apptdatetime); + setDoctorNric(toCopy.doctorNric); + setPatientNric(toCopy.patientNric); + } + + /** + * Returns true if at least one field is edited. + */ + public boolean isAnyFieldEdited() { + return CollectionUtil.isAnyNonNull(apptdatetime); + } + + public void setDateTime(AppointmentDateTime dateTime) { + this.apptdatetime = dateTime; + } + + public Optional getDate() { + return Optional.ofNullable(apptdatetime); + } + + public void setDoctorNric(Nric nric) { + this.doctorNric = nric; + } + + public Optional getDoctorNric() { + return Optional.ofNullable(doctorNric); + } + + public void setPatientNric(Nric nric) { + this.patientNric = nric; + } + + public Optional getPatientNric() { + return Optional.ofNullable(patientNric); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditAppointmentDescriptor)) { + return false; + } + + EditAppointmentDescriptor otherEditAppointmentDescriptor = (EditAppointmentDescriptor) other; + return Objects.equals(apptdatetime, otherEditAppointmentDescriptor.apptdatetime) + && Objects.equals(doctorNric, otherEditAppointmentDescriptor.doctorNric) + && Objects.equals(patientNric, otherEditAppointmentDescriptor.patientNric); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("apptdatetime", apptdatetime) + .toString(); + } + } +} diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java index 4b581c7331e..1baba51a002 100644 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ b/src/main/java/seedu/address/logic/commands/EditCommand.java @@ -1,62 +1,69 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; -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_DOB; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NRIC; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_APPOINTMENTS; import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; -import java.util.Collections; -import java.util.HashSet; +import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.Optional; -import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; +import seedu.address.Main; +import seedu.address.commons.core.LogsCenter; import seedu.address.commons.core.index.Index; import seedu.address.commons.util.CollectionUtil; import seedu.address.commons.util.ToStringBuilder; import seedu.address.logic.Messages; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; +import seedu.address.model.appointment.Appointment; +import seedu.address.model.appointment.AppointmentContainsDoctorPredicate; +import seedu.address.model.appointment.AppointmentContainsPatientPredicate; +import seedu.address.model.person.DoB; +import seedu.address.model.person.Doctor; import seedu.address.model.person.Name; +import seedu.address.model.person.Nric; +import seedu.address.model.person.Patient; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; + /** - * Edits the details of an existing person in the address book. + * Editcommand class enables user to edit a doctor or patient in the person list */ 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 " + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the details of the patient or doctor 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) " + "[" + PREFIX_NAME + "NAME] " + "[" + PREFIX_PHONE + "PHONE] " - + "[" + PREFIX_EMAIL + "EMAIL] " - + "[" + PREFIX_ADDRESS + "ADDRESS] " - + "[" + PREFIX_TAG + "TAG]...\n" + + "[" + PREFIX_NRIC + "NRIC] " + + "[" + PREFIX_DOB + "DOB]\n" + "Example: " + COMMAND_WORD + " 1 " + PREFIX_PHONE + "91234567 " - + PREFIX_EMAIL + "johndoe@example.com"; + + PREFIX_NRIC + "T0123452K"; public static final String MESSAGE_EDIT_PERSON_SUCCESS = "Edited Person: %1$s"; public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book."; + private static Logger logger = LogsCenter.getLogger(Main.class); private final Index index; private final EditPersonDescriptor editPersonDescriptor; /** - * @param index of the person in the filtered person list to edit + * @param index of the person in the filtered person list to edit * @param editPersonDescriptor details to edit the person with */ public EditCommand(Index index, EditPersonDescriptor editPersonDescriptor) { @@ -73,18 +80,46 @@ public CommandResult execute(Model model) throws CommandException { List lastShownList = model.getFilteredPersonList(); if (index.getZeroBased() >= lastShownList.size()) { + logger.log(Level.WARNING, "Index not within valid parameters! (when executing command: edit)"); throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); } Person personToEdit = lastShownList.get(index.getZeroBased()); Person editedPerson = createEditedPerson(personToEdit, editPersonDescriptor); + assert editedPerson != null; if (!personToEdit.isSamePerson(editedPerson) && model.hasPerson(editedPerson)) { + logger.log(Level.WARNING, "Duplicate person detected! (when executing command: edit)"); throw new CommandException(MESSAGE_DUPLICATE_PERSON); } model.setPerson(personToEdit, editedPerson); model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + + AppointmentContainsDoctorPredicate predicateDoctor = + new AppointmentContainsDoctorPredicate(Arrays.asList(personToEdit.getNric().nric)); + model.updateFilteredAppointmentList(predicateDoctor); + + for (Appointment appt : model.getFilteredAppointmentList()) { + if (appt.getDoctorNric().equals(personToEdit.getNric())) { + appt.setDoctorNric(editedPerson.getNric()); + assert appt.getDoctorNric() == editedPerson.getNric(); + } + } + + AppointmentContainsPatientPredicate predicatePatient = + new AppointmentContainsPatientPredicate(Arrays.asList(personToEdit.getNric().nric)); + model.updateFilteredAppointmentList(predicatePatient); + + for (Appointment appt : model.getFilteredAppointmentList()) { + if (appt.getPatientNric().equals(personToEdit.getNric())) { + appt.setPatientNric(editedPerson.getNric()); + assert appt.getPatientNric() == editedPerson.getNric(); + } + } + + model.updateFilteredAppointmentList(PREDICATE_SHOW_ALL_APPOINTMENTS); + logger.log(Level.INFO, "Edit person success! (when executing command: edit)"); return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(editedPerson))); } @@ -94,14 +129,17 @@ public CommandResult execute(Model model) throws CommandException { */ private static Person createEditedPerson(Person personToEdit, EditPersonDescriptor editPersonDescriptor) { assert personToEdit != null; - Name updatedName = editPersonDescriptor.getName().orElse(personToEdit.getName()); Phone updatedPhone = editPersonDescriptor.getPhone().orElse(personToEdit.getPhone()); - Email updatedEmail = editPersonDescriptor.getEmail().orElse(personToEdit.getEmail()); - Address updatedAddress = editPersonDescriptor.getAddress().orElse(personToEdit.getAddress()); - Set updatedTags = editPersonDescriptor.getTags().orElse(personToEdit.getTags()); - - return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags); + Nric updatedNric = editPersonDescriptor.getNric().orElse(personToEdit.getNric()); + DoB updatedDob = editPersonDescriptor.getDob().orElse(personToEdit.getDoB()); + if (personToEdit instanceof Patient) { + return new Patient(updatedNric, updatedName, updatedDob, updatedPhone); + } else if (personToEdit instanceof Doctor) { + return new Doctor(updatedNric, updatedName, updatedDob, updatedPhone); + } else { + return null; + } } @Override @@ -135,29 +173,31 @@ public String toString() { public static class EditPersonDescriptor { private Name name; private Phone phone; - private Email email; - private Address address; - private Set tags; + private Nric nric; + private DoB dob; - public EditPersonDescriptor() {} + public EditPersonDescriptor() { + } /** * Copy constructor. * A defensive copy of {@code tags} is used internally. */ public EditPersonDescriptor(EditPersonDescriptor toCopy) { + if (toCopy == null) { // Defensive Coding + throw new IllegalArgumentException(); + } setName(toCopy.name); setPhone(toCopy.phone); - setEmail(toCopy.email); - setAddress(toCopy.address); - setTags(toCopy.tags); + setNric(toCopy.nric); + setDob(toCopy.dob); } /** * 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, nric, dob); } public void setName(Name name) { @@ -176,37 +216,20 @@ public Optional getPhone() { return Optional.ofNullable(phone); } - public void setEmail(Email email) { - this.email = email; - } - - public Optional getEmail() { - return Optional.ofNullable(email); - } - - public void setAddress(Address address) { - this.address = address; + public void setNric(Nric nric) { + this.nric = nric; } - public Optional
getAddress() { - return Optional.ofNullable(address); + public Optional getNric() { + return Optional.ofNullable(nric); } - /** - * Sets {@code tags} to this object's {@code tags}. - * A defensive copy of {@code tags} is used internally. - */ - public void setTags(Set tags) { - this.tags = (tags != null) ? new HashSet<>(tags) : null; + public void setDob(DoB dob) { + this.dob = dob; } - /** - * Returns an unmodifiable tag set, which throws {@code UnsupportedOperationException} - * if modification is attempted. - * Returns {@code Optional#empty()} if {@code tags} is null. - */ - public Optional> getTags() { - return (tags != null) ? Optional.of(Collections.unmodifiableSet(tags)) : Optional.empty(); + public Optional getDob() { + return Optional.ofNullable(dob); } @Override @@ -220,12 +243,11 @@ public boolean equals(Object other) { return false; } - EditPersonDescriptor otherEditPersonDescriptor = (EditPersonDescriptor) other; - return Objects.equals(name, otherEditPersonDescriptor.name) - && Objects.equals(phone, otherEditPersonDescriptor.phone) - && Objects.equals(email, otherEditPersonDescriptor.email) - && Objects.equals(address, otherEditPersonDescriptor.address) - && Objects.equals(tags, otherEditPersonDescriptor.tags); + EditPersonDescriptor otherEditPatientDescriptor = (EditPersonDescriptor) other; + return Objects.equals(name, otherEditPatientDescriptor.name) + && Objects.equals(phone, otherEditPatientDescriptor.phone) + && Objects.equals(nric, otherEditPatientDescriptor.nric) + && Objects.equals(dob, otherEditPatientDescriptor.dob); } @Override @@ -233,10 +255,10 @@ public String toString() { return new ToStringBuilder(this) .add("name", name) .add("phone", phone) - .add("email", email) - .add("address", address) - .add("tags", tags) + .add("nric", nric) + .add("dob", dob) .toString(); } } } + diff --git a/src/main/java/seedu/address/logic/commands/ListCommand.java b/src/main/java/seedu/address/logic/commands/ListCommand.java index 84be6ad2596..82b63901697 100644 --- a/src/main/java/seedu/address/logic/commands/ListCommand.java +++ b/src/main/java/seedu/address/logic/commands/ListCommand.java @@ -1,6 +1,7 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_APPOINTMENTS; import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; import seedu.address.model.Model; @@ -12,13 +13,14 @@ public class ListCommand extends Command { public static final String COMMAND_WORD = "list"; - public static final String MESSAGE_SUCCESS = "Listed all persons"; + public static final String MESSAGE_SUCCESS = "Listed all persons and all appointments"; @Override public CommandResult execute(Model model) { requireNonNull(model); model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + model.updateFilteredAppointmentList(PREDICATE_SHOW_ALL_APPOINTMENTS); return new CommandResult(MESSAGE_SUCCESS); } } diff --git a/src/main/java/seedu/address/logic/commands/QueryDoctorAppointmentCommand.java b/src/main/java/seedu/address/logic/commands/QueryDoctorAppointmentCommand.java new file mode 100644 index 00000000000..89af23b3bb5 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/QueryDoctorAppointmentCommand.java @@ -0,0 +1,77 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.model.Model; +import seedu.address.model.appointment.AppointmentContainsDoctorPredicate; + +/** + * Represents a command for querying appointments for a specific doctor. + * The command searches for appointments of doctors whose NRICs contain any of the specified keywords + * (case-insensitive) and displays them as a list with index numbers. + */ +public class QueryDoctorAppointmentCommand extends Command { + + public static final String COMMAND_WORD = "apptfordoctor"; + private static final Logger logger = Logger.getLogger(QueryDoctorAppointmentCommand.class.getName()); + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all appointments of doctors whose " + + "nrics contain any of the specified keywords (case-insensitive) and displays them as a " + + "list with index numbers.\n" + + "Parameters: KEYWORD [MORE_KEYWORDS]...(Keywords have to be the " + + "exact NRICs of the doctor(s) in question)\n" + + "Example: " + COMMAND_WORD + " T1234567A S7654321A"; + + private final AppointmentContainsDoctorPredicate predicate; + + /** + * Constructs a QueryDoctorAppointmentCommand with the given predicate. + * + * @param predicate The predicate to be used for querying doctor appointments. + * @throws NullPointerException if the predicate is null. + */ + public QueryDoctorAppointmentCommand(AppointmentContainsDoctorPredicate predicate) { + requireNonNull(predicate, "Predicate cannot be null in QueryDoctorAppointmentCommand constructor."); + this.predicate = predicate; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model, "Model cannot be null in execute method of QueryDoctorAppointmentCommand."); + + logger.log(Level.INFO, "Executing QueryDoctorAppointmentCommand"); + + model.updateFilteredAppointmentList(predicate); + int numberOfAppointments = model.getFilteredAppointmentList().size(); + logger.log(Level.INFO, "Number of appointments found: " + numberOfAppointments); + + return new CommandResult( + String.format(Messages.MESSAGE_APPOINTMENTS_LISTED_OVERVIEW, numberOfAppointments)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof QueryDoctorAppointmentCommand)) { + return false; + } + + QueryDoctorAppointmentCommand otherQueryDoctorAppointmentCommand = (QueryDoctorAppointmentCommand) other; + return predicate.equals(otherQueryDoctorAppointmentCommand.predicate); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("predicate", predicate) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/QueryDoctorCommand.java b/src/main/java/seedu/address/logic/commands/QueryDoctorCommand.java new file mode 100644 index 00000000000..d9cc497e120 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/QueryDoctorCommand.java @@ -0,0 +1,69 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.model.Model; +import seedu.address.model.person.DoctorContainsKeywordsPredicate; + +/** + * Queries and returns all doctors whose name matches the input string. + * Keyword matching is case insensitive. + * Query more than one name at a time is supported + */ +public class QueryDoctorCommand extends Command { + + public static final String COMMAND_WORD = "doctor"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all doctors whose name, " + + "NRIC, DoB or phone number contains any of " + + "the specified keywords (case-insensitive) and displays them as a list with index numbers.\n" + + "Parameters: KEYWORD [MORE_KEYWORDS]...\n" + + "Example: " + COMMAND_WORD + " alice bob charlie"; + + private static Logger logger = LogsCenter.getLogger(QueryDoctorCommand.class); + + private final DoctorContainsKeywordsPredicate predicate; + + public QueryDoctorCommand(DoctorContainsKeywordsPredicate predicate) { + this.predicate = predicate; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model); + logger.log(Level.INFO, "Executing QueryDoctorCommand"); + model.updateFilteredPersonList(predicate); + int numberOfDoctors = model.getFilteredPersonList().size(); + logger.log(Level.INFO, "Number of Doctor found: " + numberOfDoctors); + return new CommandResult( + String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size())); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof QueryDoctorCommand)) { + return false; + } + + QueryDoctorCommand otherQueryDoctorCommand = (QueryDoctorCommand) other; + return predicate.equals(otherQueryDoctorCommand.predicate); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("predicate", predicate) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/QueryPatientAppointmentCommand.java b/src/main/java/seedu/address/logic/commands/QueryPatientAppointmentCommand.java new file mode 100644 index 00000000000..ca4b48db875 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/QueryPatientAppointmentCommand.java @@ -0,0 +1,76 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.model.Model; +import seedu.address.model.appointment.AppointmentContainsPatientPredicate; + +/** + * Represents a command for querying appointments for a specific patient. + * The command searches for appointments of patients whose NRICs contain any of the specified keywords + * (case-insensitive) and displays them as a list with index numbers. + */ +public class QueryPatientAppointmentCommand extends Command { + public static final String COMMAND_WORD = "apptforpatient"; + private static final Logger logger = Logger.getLogger(QueryPatientAppointmentCommand.class.getName()); + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all appointments of patients whose " + + "nrics contain any of the specified keywords (case-insensitive) and displays them as a " + + "list with index numbers.\n" + + "Parameters: KEYWORD [MORE_KEYWORDS]...(Keywords have to be the " + + "exact NRICs of the patient(s) in question)\n" + + "Example: " + COMMAND_WORD + " T1234567A S7654321A"; + + private final AppointmentContainsPatientPredicate predicate; + + /** + * Constructs a QueryPatientAppointmentCommand with the given predicate. + * + * @param predicate The predicate to be used for querying patient appointments. + * @throws NullPointerException if the predicate is null. + */ + public QueryPatientAppointmentCommand(AppointmentContainsPatientPredicate predicate) { + requireNonNull(predicate, "Predicate cannot be null in QueryPatientAppointmentCommand constructor."); + this.predicate = predicate; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model, "Model cannot be null in execute method of QueryPatientAppointmentCommand."); + + logger.log(Level.INFO, "Executing QueryPatientAppointmentCommand"); + + model.updateFilteredAppointmentList(predicate); + int numberOfAppointments = model.getFilteredAppointmentList().size(); + logger.log(Level.INFO, "Number of appointments found: " + numberOfAppointments); + + return new CommandResult( + String.format(Messages.MESSAGE_APPOINTMENTS_LISTED_OVERVIEW, numberOfAppointments)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof QueryPatientAppointmentCommand)) { + return false; + } + + QueryPatientAppointmentCommand otherQueryPatientAppointmentCommand = (QueryPatientAppointmentCommand) other; + return predicate.equals(otherQueryPatientAppointmentCommand.predicate); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("predicate", predicate) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/QueryPatientCommand.java b/src/main/java/seedu/address/logic/commands/QueryPatientCommand.java new file mode 100644 index 00000000000..83d613cad53 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/QueryPatientCommand.java @@ -0,0 +1,77 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.Messages; +import seedu.address.model.Model; +import seedu.address.model.person.PatientContainsKeywordsPredicate; + +/** + * Queries and returns all patients whose name, NRIC, DoB and phone number matches + * or substring matches the input string. + * Keyword matching is case-insensitive. + * Query more than one name, nric, date of birth and phone number at a time is supported + */ +public class QueryPatientCommand extends Command { + + public static final String COMMAND_WORD = "patient"; + private static final Logger logger = Logger.getLogger(QueryPatientCommand.class.getName()); + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all patients whose name, " + + "NRIC, DoB or phone number contains any of " + + "the specified keywords (case-insensitive) and displays them as a list with index numbers.\n" + + "Parameters: KEYWORD [MORE_KEYWORDS]...\n" + + "Example: " + COMMAND_WORD + " alice bob ethan"; + + private final PatientContainsKeywordsPredicate predicate; + + /** + * Constructs a QueryPatientCommand with the given predicate. + * + * @param predicate The predicate to be used for querying patients. + * @throws NullPointerException if the predicate is null. + */ + public QueryPatientCommand(PatientContainsKeywordsPredicate predicate) { + requireNonNull(predicate, "Predicate cannot be null in QueryPatientCommand constructor."); + this.predicate = predicate; + } + + @Override + public CommandResult execute(Model model) { + requireNonNull(model, "Model cannot be null in execute method of QueryPatientCommand."); + + logger.log(Level.INFO, "Executing QueryPatientCommand"); + + model.updateFilteredPersonList(predicate); + int numberOfPatients = model.getFilteredPersonList().size(); + logger.log(Level.INFO, "Number of patients found: " + numberOfPatients); + + return new CommandResult( + String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, numberOfPatients)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof QueryPatientCommand)) { + return false; + } + + QueryPatientCommand otherQueryPatientCommand = (QueryPatientCommand) other; + return predicate.equals(otherQueryPatientCommand.predicate); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("predicate", predicate) + .toString(); + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddAppointmentCommandParser.java b/src/main/java/seedu/address/logic/parser/AddAppointmentCommandParser.java new file mode 100644 index 00000000000..0d7e0825167 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AddAppointmentCommandParser.java @@ -0,0 +1,55 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DOCTORNRIC; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PATIENTNRIC; + +import java.util.stream.Stream; + +import seedu.address.logic.commands.AddAppointmentCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.appointment.Appointment; +import seedu.address.model.appointment.AppointmentDateTime; +import seedu.address.model.person.Nric; + +/** + * Parses addAppointment Command. + */ +public class AddAppointmentCommandParser { + + /** + * Parses the given {@code String} of arguments in the context of the AddAppointmentCommand. + * Returns an AddAppointmentCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format. + */ + public AddAppointmentCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_DATE, PREFIX_DOCTORNRIC, PREFIX_PATIENTNRIC); + + if (!argMultimap.getPreamble().isEmpty() + || !arePrefixesPresent(argMultimap, PREFIX_DATE, PREFIX_DOCTORNRIC, PREFIX_PATIENTNRIC)) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddAppointmentCommand.MESSAGE_USAGE)); + } + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_DATE, PREFIX_DOCTORNRIC, PREFIX_PATIENTNRIC); + AppointmentDateTime appointmentDateTime = + ParserUtil.parseAppointmentDateTime(argMultimap.getValue(PREFIX_DATE).get()); + Nric doctorNric = ParserUtil.parseNric(argMultimap.getValue(PREFIX_DOCTORNRIC).get()); + Nric patientNric = ParserUtil.parseNric(argMultimap.getValue(PREFIX_PATIENTNRIC).get()); + + Appointment appointment = new Appointment(doctorNric, patientNric, appointmentDateTime, false); + return new AddAppointmentCommand(appointment); + } + + /** + * 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/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddDoctorCommandParser.java similarity index 52% rename from src/main/java/seedu/address/logic/parser/AddCommandParser.java rename to src/main/java/seedu/address/logic/parser/AddDoctorCommandParser.java index 4ff1a97ed77..947aa345289 100644 --- a/src/main/java/seedu/address/logic/parser/AddCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/AddDoctorCommandParser.java @@ -1,53 +1,48 @@ package seedu.address.logic.parser; 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_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DOB; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NRIC; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; -import java.util.Set; import java.util.stream.Stream; -import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.AddDoctorCommand; import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; +import seedu.address.model.person.DoB; +import seedu.address.model.person.Doctor; import seedu.address.model.person.Name; -import seedu.address.model.person.Person; +import seedu.address.model.person.Nric; import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; /** * Parses input arguments and creates a new AddCommand object */ -public class AddCommandParser implements Parser { +public class AddDoctorCommandParser implements Parser { /** * Parses the given {@code String} of arguments in the context of the AddCommand * and returns an AddCommand object for execution. * @throws ParseException if the user input does not conform the expected format */ - public AddCommand parse(String args) throws ParseException { + public AddDoctorCommand parse(String args) throws ParseException { ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); - - if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ADDRESS, PREFIX_PHONE, PREFIX_EMAIL) + ArgumentTokenizer.tokenize(args, PREFIX_NRIC, PREFIX_NAME, PREFIX_DOB, PREFIX_PHONE); + if (!arePrefixesPresent(argMultimap, PREFIX_NRIC, PREFIX_NAME, PREFIX_DOB, PREFIX_PHONE) || !argMultimap.getPreamble().isEmpty()) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddDoctorCommand.MESSAGE_USAGE)); } - argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS); + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NRIC, PREFIX_NAME, PREFIX_DOB, PREFIX_PHONE); + Nric nric = ParserUtil.parseNric(argMultimap.getValue(PREFIX_NRIC).get()); Name name = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()); + DoB dob = ParserUtil.parseDoB(argMultimap.getValue(PREFIX_DOB).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()); - Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); - Person person = new Person(name, phone, email, address, tagList); + Doctor doctor = new Doctor(nric, name, dob, phone); - return new AddCommand(person); + return new AddDoctorCommand(doctor); } /** diff --git a/src/main/java/seedu/address/logic/parser/AddPatientCommandParser.java b/src/main/java/seedu/address/logic/parser/AddPatientCommandParser.java new file mode 100644 index 00000000000..0b0216e4f9b --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AddPatientCommandParser.java @@ -0,0 +1,56 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DOB; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NRIC; +import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; + +import java.util.stream.Stream; + +import seedu.address.logic.commands.AddPatientCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.DoB; +import seedu.address.model.person.Name; +import seedu.address.model.person.Nric; +import seedu.address.model.person.Patient; +import seedu.address.model.person.Phone; +/** + * Parses input arguments and creates a new AddCommand object + */ +public class AddPatientCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddCommand + * and returns an AddCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public AddPatientCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NRIC, PREFIX_NAME, PREFIX_DOB, PREFIX_PHONE); + + if (!arePrefixesPresent(argMultimap, PREFIX_NRIC, PREFIX_NAME, PREFIX_DOB, PREFIX_PHONE) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddPatientCommand.MESSAGE_USAGE)); + } + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NRIC, PREFIX_NAME, PREFIX_DOB, PREFIX_PHONE); + Nric nric = ParserUtil.parseNric(argMultimap.getValue(PREFIX_NRIC).get()); + Name name = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()); + DoB dob = ParserUtil.parseDoB(argMultimap.getValue(PREFIX_DOB).get()); + Phone phone = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get()); + + Patient patient = new Patient(nric, name, dob, phone); + + return new AddPatientCommand(patient); + } + + /** + * 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/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java index 3149ee07e0b..99a5a99f2fb 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -8,15 +8,23 @@ import java.util.regex.Pattern; import seedu.address.commons.core.LogsCenter; -import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.AddAppointmentCommand; +import seedu.address.logic.commands.AddDoctorCommand; +import seedu.address.logic.commands.AddPatientCommand; import seedu.address.logic.commands.ClearCommand; import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.DeleteAppointmentCommand; import seedu.address.logic.commands.DeleteCommand; +import seedu.address.logic.commands.EditAppointmentCommand; import seedu.address.logic.commands.EditCommand; import seedu.address.logic.commands.ExitCommand; import seedu.address.logic.commands.FindCommand; import seedu.address.logic.commands.HelpCommand; import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.QueryDoctorAppointmentCommand; +import seedu.address.logic.commands.QueryDoctorCommand; +import seedu.address.logic.commands.QueryPatientAppointmentCommand; +import seedu.address.logic.commands.QueryPatientCommand; import seedu.address.logic.parser.exceptions.ParseException; /** @@ -53,8 +61,14 @@ public Command parseCommand(String userInput) throws ParseException { switch (commandWord) { - case AddCommand.COMMAND_WORD: - return new AddCommandParser().parse(arguments); + case AddPatientCommand.COMMAND_WORD: + return new AddPatientCommandParser().parse(arguments); + + case AddDoctorCommand.COMMAND_WORD: + return new AddDoctorCommandParser().parse(arguments); + + case EditAppointmentCommand.COMMAND_WORD: + return new EditAppointmentCommandParser().parse(arguments); case EditCommand.COMMAND_WORD: return new EditCommandParser().parse(arguments); @@ -71,12 +85,30 @@ public Command parseCommand(String userInput) throws ParseException { case ListCommand.COMMAND_WORD: return new ListCommand(); + case QueryDoctorCommand.COMMAND_WORD: + return new QueryDoctorCommandParser().parse(arguments); + + case QueryPatientCommand.COMMAND_WORD: + return new QueryPatientCommandParser().parse(arguments); + case ExitCommand.COMMAND_WORD: return new ExitCommand(); case HelpCommand.COMMAND_WORD: return new HelpCommand(); + case AddAppointmentCommand.COMMAND_WORD: + return new AddAppointmentCommandParser().parse(arguments); + + case QueryDoctorAppointmentCommand.COMMAND_WORD: + return new QueryDoctorAppointmentCommandParser().parse(arguments); + + case QueryPatientAppointmentCommand.COMMAND_WORD: + return new QueryPatientAppointmentCommandParser().parse(arguments); + + case DeleteAppointmentCommand.COMMAND_WORD: + return new DeleteAppointmentCommandParser().parse(arguments); + default: logger.finer("This user input caused a ParseException: " + userInput); throw new ParseException(MESSAGE_UNKNOWN_COMMAND); diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java index 75b1a9bf119..e37824284af 100644 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ b/src/main/java/seedu/address/logic/parser/CliSyntax.java @@ -6,10 +6,17 @@ public class CliSyntax { /* Prefix definitions */ + public static final Prefix PREFIX_NRIC = new Prefix("i/"); public static final Prefix PREFIX_NAME = new Prefix("n/"); + public static final Prefix PREFIX_DOB = new Prefix("d/"); 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_TAG = new Prefix("t/"); + + public static final Prefix PREFIX_DATE = new Prefix("ad/"); + public static final Prefix PREFIX_PATIENTNRIC = new Prefix("pn/"); + public static final Prefix PREFIX_DOCTORNRIC = new Prefix("dn/"); } diff --git a/src/main/java/seedu/address/logic/parser/DeleteAppointmentCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteAppointmentCommandParser.java new file mode 100644 index 00000000000..e6cb2fa87d3 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/DeleteAppointmentCommandParser.java @@ -0,0 +1,30 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.DeleteAppointmentCommand; +import seedu.address.logic.parser.exceptions.ParseException; + + +/** + * Parses input arguments and creates a new DeleteAppointmentCommand object + */ +public class DeleteAppointmentCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the DeleteAppointmentCommand + * and returns a DeleteAppointmentCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public DeleteAppointmentCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new DeleteAppointmentCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteAppointmentCommand.MESSAGE_USAGE), pe); + } + } + +} diff --git a/src/main/java/seedu/address/logic/parser/EditAppointmentCommandParser.java b/src/main/java/seedu/address/logic/parser/EditAppointmentCommandParser.java new file mode 100644 index 00000000000..684c631c475 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/EditAppointmentCommandParser.java @@ -0,0 +1,52 @@ +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_DATE; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.EditAppointmentCommand; +import seedu.address.logic.commands.EditAppointmentCommand.EditAppointmentDescriptor; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new EditCommand object + */ +public class EditAppointmentCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the EditCommand + * and returns an EditCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public EditAppointmentCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_DATE); + + Index index; + + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + EditAppointmentCommand.MESSAGE_USAGE), pe); + } + + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_DATE); + + EditAppointmentDescriptor editAppointmentDescriptor = new EditAppointmentDescriptor(); + + if (argMultimap.getValue(PREFIX_DATE).isPresent()) { + editAppointmentDescriptor.setDateTime( + ParserUtil.parseAppointmentDateTime(argMultimap.getValue(PREFIX_DATE).get())); + } + + if (!editAppointmentDescriptor.isAnyFieldEdited()) { + throw new ParseException(EditAppointmentCommand.MESSAGE_NOT_EDITED); + } + + return new EditAppointmentCommand(index, editAppointmentDescriptor); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/seedu/address/logic/parser/EditCommandParser.java index 46b3309a78b..fc49a617b46 100644 --- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/EditCommandParser.java @@ -2,22 +2,14 @@ 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_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DOB; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_NRIC; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; - -import java.util.Collection; -import java.util.Collections; -import java.util.Optional; -import java.util.Set; import seedu.address.commons.core.index.Index; import seedu.address.logic.commands.EditCommand; -import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.tag.Tag; /** * Parses input arguments and creates a new EditCommand object @@ -27,12 +19,13 @@ public class EditCommandParser implements Parser { /** * Parses the given {@code String} of arguments in the context of the EditCommand * and returns an EditCommand object for execution. + * * @throws ParseException if the user input does not conform the expected format */ 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_NRIC, PREFIX_DOB); Index index; @@ -42,9 +35,9 @@ 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_NRIC, PREFIX_DOB); - EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor(); + EditCommand.EditPersonDescriptor editPersonDescriptor = new EditCommand.EditPersonDescriptor(); if (argMultimap.getValue(PREFIX_NAME).isPresent()) { editPersonDescriptor.setName(ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get())); @@ -52,13 +45,12 @@ public EditCommand parse(String args) throws ParseException { if (argMultimap.getValue(PREFIX_PHONE).isPresent()) { editPersonDescriptor.setPhone(ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get())); } - if (argMultimap.getValue(PREFIX_EMAIL).isPresent()) { - editPersonDescriptor.setEmail(ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get())); + if (argMultimap.getValue(PREFIX_NRIC).isPresent()) { + editPersonDescriptor.setNric(ParserUtil.parseNric(argMultimap.getValue(PREFIX_NRIC).get())); } - if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { - editPersonDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); + if (argMultimap.getValue(PREFIX_DOB).isPresent()) { + editPersonDescriptor.setDob(ParserUtil.parseDoB(argMultimap.getValue(PREFIX_DOB).get())); } - parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); if (!editPersonDescriptor.isAnyFieldEdited()) { throw new ParseException(EditCommand.MESSAGE_NOT_EDITED); @@ -66,20 +58,4 @@ public EditCommand parse(String args) throws ParseException { return new EditCommand(index, editPersonDescriptor); } - - /** - * Parses {@code Collection tags} into a {@code Set} if {@code tags} is non-empty. - * If {@code tags} contain only one element which is an empty string, it will be parsed into a - * {@code Set} containing zero tags. - */ - private Optional> parseTagsForEdit(Collection tags) throws ParseException { - assert tags != null; - - if (tags.isEmpty()) { - return Optional.empty(); - } - Collection tagSet = tags.size() == 1 && tags.contains("") ? Collections.emptySet() : tags; - return Optional.of(ParserUtil.parseTags(tagSet)); - } - } diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java index b117acb9c55..c3c1e5d9c55 100644 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java @@ -2,18 +2,16 @@ import static java.util.Objects.requireNonNull; -import java.util.Collection; -import java.util.HashSet; -import java.util.Set; - import seedu.address.commons.core.index.Index; import seedu.address.commons.util.StringUtil; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.appointment.AppointmentDateTime; import seedu.address.model.person.Address; +import seedu.address.model.person.DoB; import seedu.address.model.person.Email; import seedu.address.model.person.Name; +import seedu.address.model.person.Nric; import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; /** * Contains utility methods used for parsing strings in the various *Parser classes. @@ -35,6 +33,33 @@ public static Index parseIndex(String oneBasedIndex) throws ParseException { return Index.fromOneBased(Integer.parseInt(trimmedIndex)); } + /** + * Parses the given National Registration Identification Card (NRIC) string into a {@code NRIC} object. + *

+ * The input string is required to be non-null, and the parsing is performed after trimming any + * leading or trailing spaces. If the trimmed NRIC string does not meet the validity constraints specified + * in the {@code NRIC} class (as determined by {@link Nric#isValidNric(String)}), a {@code ParseException} + * is thrown with the corresponding error message. + *

+ *

+ * Example of a valid NRIC string: "S1234567A". + *

+ * + * @param nric The National Registration Identification Card (NRIC) string to be parsed. + * @return A {@code NRIC} object representing the parsed NRIC. + * @throws ParseException If the input string is null, empty, or does not meet the validity constraints. + * @see Nric#isValidNric(String) + * @see Nric + */ + public static Nric parseNric(String nric) throws ParseException { + requireNonNull(nric); + String trimmedNric = nric.trim(); + if (!Nric.isValidNric(trimmedNric)) { + throw new ParseException(Nric.MESSAGE_CONSTRAINTS); + } + return new Nric(trimmedNric); + } + /** * Parses a {@code String name} into a {@code Name}. * Leading and trailing whitespaces will be trimmed. @@ -50,6 +75,33 @@ public static Name parseName(String name) throws ParseException { return new Name(trimmedName); } + /** + * Parses the given date of birth (DoB) string into a {@code DoB} object. + *

+ * The input string is required to be non-null, and the parsing is performed after trimming any leading + * or trailing spaces. If the trimmed DoB string does not meet the validity constraints specified in the + * {@code DoB} class (as determined by {@link DoB#isValidDoB(String)}), a {@code ParseException} is thrown + * with the corresponding error message. + *

+ *

+ * Example of a valid DoB string: "3 January 2000". + *

+ * + * @param dob The date of birth string to be parsed. + * @return A {@code DoB} object representing the parsed date of birth. + * @throws ParseException If the input string is null, empty, or does not meet the validity constraints. + * @see DoB#isValidDoB(String) + * @see DoB + */ + public static DoB parseDoB(String dob) throws ParseException { + requireNonNull(dob); + String trimmedDoB = dob.trim(); + if (!DoB.isValidDoB(trimmedDoB)) { + throw new ParseException(DoB.MESSAGE_CONSTRAINTS); + } + return new DoB(trimmedDoB); + } + /** * Parses a {@code String phone} into a {@code Phone}. * Leading and trailing whitespaces will be trimmed. @@ -96,29 +148,22 @@ public static Email parseEmail(String email) throws ParseException { } /** - * Parses a {@code String tag} into a {@code Tag}. - * Leading and trailing whitespaces will be trimmed. + * Parses AppointmentDateTime from string to return an AppointmentDateTime object. * - * @throws ParseException if the given {@code tag} is invalid. + * @param apptDateTime String to parse. + * @return instance of AppointmentDateTime. + * @throws ParseException if string is invalid date. */ - public static Tag parseTag(String tag) throws ParseException { - requireNonNull(tag); - String trimmedTag = tag.trim(); - if (!Tag.isValidTagName(trimmedTag)) { - throw new ParseException(Tag.MESSAGE_CONSTRAINTS); + public static AppointmentDateTime parseAppointmentDateTime(String apptDateTime) throws ParseException { + requireNonNull(apptDateTime); + String trimmedDate = apptDateTime.trim(); + if (trimmedDate.length() < 16) { + throw new ParseException(AppointmentDateTime.MESSAGE_CONSTRAINTS); } - return new Tag(trimmedTag); - } - - /** - * Parses {@code Collection tags} into a {@code Set}. - */ - public static Set parseTags(Collection tags) throws ParseException { - requireNonNull(tags); - final Set tagSet = new HashSet<>(); - for (String tagName : tags) { - tagSet.add(parseTag(tagName)); + String datetime = trimmedDate.substring(0, 11) + trimmedDate.substring(11).strip(); + if (!AppointmentDateTime.isValidDate(datetime)) { + throw new ParseException(AppointmentDateTime.MESSAGE_CONSTRAINTS); } - return tagSet; + return new AppointmentDateTime(datetime); } } diff --git a/src/main/java/seedu/address/logic/parser/QueryDoctorAppointmentCommandParser.java b/src/main/java/seedu/address/logic/parser/QueryDoctorAppointmentCommandParser.java new file mode 100644 index 00000000000..6c7322b8d61 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/QueryDoctorAppointmentCommandParser.java @@ -0,0 +1,39 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.Arrays; +import java.util.logging.Level; +import java.util.logging.Logger; + +import seedu.address.logic.commands.QueryDoctorAppointmentCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.appointment.AppointmentContainsDoctorPredicate; + +/** + * Parses input arguments and creates a new QueryDoctorAppointmentCommand object + */ +public class QueryDoctorAppointmentCommandParser implements Parser { + private static final Logger logger = Logger.getLogger(QueryDoctorAppointmentCommandParser.class.getName()); + + /** + * Parses the given {@code String} of arguments in the context of the QueryDoctorAppointmentCommand + * and returns a QueryDoctorAppointmentCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public QueryDoctorAppointmentCommand parse(String args) throws ParseException { + logger.log(Level.INFO, "Parsing QueryDoctorAppointmentCommand arguments: " + args); + String trimmedArgs = args.trim(); + + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, QueryDoctorAppointmentCommand.MESSAGE_USAGE)); + } + + String[] nameKeywords = trimmedArgs.split("\\s+"); + logger.log(Level.INFO, "Name keywords: " + Arrays.toString(nameKeywords)); + assert nameKeywords.length > 0 : "Name keywords array cannot be empty"; + + return new QueryDoctorAppointmentCommand(new AppointmentContainsDoctorPredicate(Arrays.asList(nameKeywords))); + } +} diff --git a/src/main/java/seedu/address/logic/parser/QueryDoctorCommandParser.java b/src/main/java/seedu/address/logic/parser/QueryDoctorCommandParser.java new file mode 100644 index 00000000000..ef93ffb1edc --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/QueryDoctorCommandParser.java @@ -0,0 +1,33 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.Arrays; + +import seedu.address.logic.commands.QueryDoctorCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.DoctorContainsKeywordsPredicate; + +/** + * Parses input arguments and creates a new QueryDoctorCommand object + */ +public class QueryDoctorCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the FindCommand + * and returns a FindCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public QueryDoctorCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, QueryDoctorCommand.MESSAGE_USAGE)); + } + + String[] nameKeywords = trimmedArgs.split("\\s+"); + + return new QueryDoctorCommand(new DoctorContainsKeywordsPredicate(Arrays.asList(nameKeywords))); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/QueryPatientAppointmentCommandParser.java b/src/main/java/seedu/address/logic/parser/QueryPatientAppointmentCommandParser.java new file mode 100644 index 00000000000..12a4ab5e014 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/QueryPatientAppointmentCommandParser.java @@ -0,0 +1,39 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.Arrays; +import java.util.logging.Level; +import java.util.logging.Logger; + +import seedu.address.logic.commands.QueryPatientAppointmentCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.appointment.AppointmentContainsPatientPredicate; + +/** + * Parses input arguments and creates a new QueryPatientAppointmentCommand object + */ +public class QueryPatientAppointmentCommandParser implements Parser { + private static final Logger logger = Logger.getLogger(QueryPatientAppointmentCommandParser.class.getName()); + + /** + * Parses the given {@code String} of arguments in the context of the QueryPatientAppointmentCommand + * and returns a QueryPatientAppointmentCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public QueryPatientAppointmentCommand parse(String args) throws ParseException { + logger.log(Level.INFO, "Parsing QueryPatientAppointmentCommand arguments: " + args); + String trimmedArgs = args.trim(); + + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, QueryPatientAppointmentCommand.MESSAGE_USAGE)); + } + + String[] nameKeywords = trimmedArgs.split("\\s+"); + logger.log(Level.INFO, "Name keywords: " + Arrays.toString(nameKeywords)); + assert nameKeywords.length > 0 : "Name keywords array cannot be empty"; + + return new QueryPatientAppointmentCommand(new AppointmentContainsPatientPredicate(Arrays.asList(nameKeywords))); + } +} diff --git a/src/main/java/seedu/address/logic/parser/QueryPatientCommandParser.java b/src/main/java/seedu/address/logic/parser/QueryPatientCommandParser.java new file mode 100644 index 00000000000..7c41d7084cf --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/QueryPatientCommandParser.java @@ -0,0 +1,43 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.Arrays; +import java.util.logging.Level; +import java.util.logging.Logger; + +import seedu.address.logic.commands.QueryPatientCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.PatientContainsKeywordsPredicate; + +/** + * Parses input arguments and creates a new QueryPatientCommand object + */ +public class QueryPatientCommandParser implements Parser { + private static final Logger logger = Logger.getLogger(QueryPatientCommandParser.class.getName()); + + /** + * Parses the given {@code String} of arguments in the context of the QueryPatientCommand + * and returns a QueryPatientCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public QueryPatientCommand parse(String args) throws ParseException { + logger.log(Level.INFO, "Parsing QueryPatientCommand arguments: " + args); + String trimmedArgs = args.trim(); + + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, QueryPatientCommand.MESSAGE_USAGE)); + } + + String[] nameKeywords = trimmedArgs.split("\\s+"); + logger.log(Level.INFO, "Name keywords: " + Arrays.toString(nameKeywords)); + assert nameKeywords.length > 0 : "Name keywords array cannot be empty"; + + try { + return new QueryPatientCommand(new PatientContainsKeywordsPredicate(Arrays.asList(nameKeywords))); + } catch (IllegalArgumentException e) { + throw new ParseException(e.getMessage(), e); + } + } +} diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java index 73397161e84..ff4fca8f233 100644 --- a/src/main/java/seedu/address/model/AddressBook.java +++ b/src/main/java/seedu/address/model/AddressBook.java @@ -6,8 +6,13 @@ import javafx.collections.ObservableList; import seedu.address.commons.util.ToStringBuilder; +import seedu.address.model.appointment.Appointment; +import seedu.address.model.appointment.UniqueAppointmentList; +import seedu.address.model.appointment.exceptions.DuplicateAppointmentException; +import seedu.address.model.person.Nric; import seedu.address.model.person.Person; import seedu.address.model.person.UniquePersonList; +import seedu.address.model.person.exceptions.PersonNotFoundException; /** * Wraps all data at the address-book level @@ -16,6 +21,8 @@ public class AddressBook implements ReadOnlyAddressBook { private final UniquePersonList persons; + private final UniqueAppointmentList appointments; + /* * The 'unusual' code block below is a non-static initialization block, sometimes used to avoid duplication @@ -26,6 +33,7 @@ public class AddressBook implements ReadOnlyAddressBook { */ { persons = new UniquePersonList(); + appointments = new UniqueAppointmentList(); } public AddressBook() {} @@ -48,13 +56,32 @@ public void setPersons(List persons) { this.persons.setPersons(persons); } + /** + * Replaces the contents of the appointment list with {@code appointments}. + * {@code appointments} must not contain duplicate appointments. + */ + public void setAppointments(List appointments) throws DuplicateAppointmentException { + this.appointments.setAppointments(appointments); + } + + /** + * Replaces the contents of the appointment list with {@code appointments}. + * This method does not throw exception because it is only called when resetting data. + * + * @param appointments the appointments to update with no duplicates. + */ + public void setAppointmentsExistingBook(List appointments) { + this.appointments.setAppointmentsExistingBook(appointments); + } + /** * Resets the existing data of this {@code AddressBook} with {@code newData}. */ public void resetData(ReadOnlyAddressBook newData) { requireNonNull(newData); - setPersons(newData.getPersonList()); + + setAppointmentsExistingBook(newData.getAppointmentList()); } //// person-level operations @@ -67,6 +94,19 @@ public boolean hasPerson(Person person) { return persons.contains(person); } + /** + * Returns person (if any) with the provided NRIC in MediCLI. + * + * @param nricObj the NRIC to lookup. + * @return Person with corresponding NRIC. + * @throws PersonNotFoundException if no such Person exists. + */ + public Person getPersonByNric(Nric nricObj) throws PersonNotFoundException { + requireNonNull(nricObj); + return persons.getPersonByNric(nricObj); + } + + /** * Adds a person to the address book. * The person must not already exist in the address book. @@ -82,18 +122,65 @@ public void addPerson(Person p) { */ public void setPerson(Person target, Person editedPerson) { requireNonNull(editedPerson); - persons.setPerson(target, editedPerson); } /** - * Removes {@code key} from this {@code AddressBook}. + * Replaces the given appointment {@code target} in the list with {@code editedAppointment}. + * {@code target} must exist in the address book. + * The appointment identity of {@code editedAppointment} must not be the same + * as another existing appointment in the address book. + */ + public void setAppointment(Appointment target, Appointment editedAppointment) { + requireNonNull(editedAppointment); + appointments.setAppointment(target, editedAppointment); + } + + /** + * Removes {@code key} from this {@code AddressBook} and removes all appointments + * which involve Person {@code key}, both patient and doctor. * {@code key} must exist in the address book. */ public void removePerson(Person key) { + List appointmentsToDelete = getAppointmentByPerson(key); + appointmentsToDelete.forEach(appointments::remove); persons.remove(key); } + public List getAppointmentByPerson(Person person) { + return appointments.contains(person); + } + + + /** + * Adds the specified appointment to the list of appointments. + * + * @param appointment The appointment to add to the list. It must not be null. + */ + public void addAppointment(Appointment appointment) { + appointments.add(appointment); + } + + /** + * Removes the specified appointment from the list of appointments. + * + * @param appointment The appointment to be removed from the list. It must not be null. + */ + public void deleteAppointment(Appointment appointment) { + appointments.remove(appointment); + } + + /** + * Checks if the specified appointment is present in the list of appointments. + * + * @param appointment The appointment to check in the list. It must not be null. + * @return true if the appointment is present in the list, false otherwise. + */ + public boolean hasAppointment(Appointment appointment) { + return appointments.contains(appointment); + } + + //// util methods @Override @@ -108,6 +195,11 @@ public ObservableList getPersonList() { return persons.asUnmodifiableObservableList(); } + @Override + public ObservableList getAppointmentList() { + return appointments.asUnmodifiableObservableList(); + } + @Override public boolean equals(Object other) { if (other == this) { diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index d54df471c1f..a183a40e2ca 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -5,6 +5,7 @@ import javafx.collections.ObservableList; import seedu.address.commons.core.GuiSettings; +import seedu.address.model.appointment.Appointment; import seedu.address.model.person.Person; /** @@ -13,6 +14,7 @@ public interface Model { /** {@code Predicate} that always evaluate to true */ Predicate PREDICATE_SHOW_ALL_PERSONS = unused -> true; + Predicate PREDICATE_SHOW_ALL_APPOINTMENTS = unused -> true; /** * Replaces user prefs data with the data in {@code userPrefs}. @@ -53,10 +55,15 @@ public interface Model { ReadOnlyAddressBook getAddressBook(); /** - * Returns true if a person with the same identity as {@code person} exists in the address book. + * Returns true if a person with the same identity as {@code person} exists in MediCLI. */ boolean hasPerson(Person person); + /** + * Returns true if an appointment with the same identity as {@code appointment} exists in MediCLI. + */ + boolean hasAppointment(Appointment appointment); + /** * Deletes the given person. * The person must exist in the address book. @@ -65,10 +72,23 @@ public interface Model { /** * Adds the given person. - * {@code person} must not already exist in the address book. + * {@code person} must not already exist in MediCLI. */ void addPerson(Person person); + /** + * Deletes the given appointment. + * The appointment must exist in MediCLI. + */ + void deleteAppointment(Appointment appointment); + + /** + * Adds the given appointment. + * {@code appointment} must not already exist in MediCLI. + */ + void addAppointment(Appointment appointment); + + /** * Replaces the given person {@code target} with {@code editedPerson}. * {@code target} must exist in the address book. @@ -76,6 +96,14 @@ public interface Model { */ void setPerson(Person target, Person editedPerson); + /** + * Replaces the given appointment {@code target} with {@code editedAppointment}. + * {@code target} must exist in the address book. + * The appointment identity of {@code editedAppointment} must + * not be the same as another existing appointment in the address book. + */ + void setAppointment(Appointment target, Appointment editedAppointment); + /** Returns an unmodifiable view of the filtered person list */ ObservableList getFilteredPersonList(); @@ -84,4 +112,21 @@ public interface Model { * @throws NullPointerException if {@code predicate} is null. */ void updateFilteredPersonList(Predicate predicate); + + /** + * Checks if appointment is valid and returns boolean with the answer. + * + * @param toAdd Appointment to check + * @return boolean indicating is appointment is valid. + */ + boolean isValidAppointment(Appointment toAdd); + + /** Returns an unmodifiable view of the appointment list */ + ObservableList getFilteredAppointmentList(); + + /** + * Updates the filter of the filtered appointment list to filter by the given {@code predicate}. + * @throws NullPointerException if {@code predicate} is null. + */ + void updateFilteredAppointmentList(Predicate predicate); } diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index 57bc563fde6..dac7d8eb469 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/seedu/address/model/ModelManager.java @@ -11,7 +11,11 @@ import javafx.collections.transformation.FilteredList; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; +import seedu.address.model.appointment.Appointment; +import seedu.address.model.person.Nric; import seedu.address.model.person.Person; +import seedu.address.model.person.Type; +import seedu.address.model.person.exceptions.PersonNotFoundException; /** * Represents the in-memory model of the address book data. @@ -22,6 +26,7 @@ public class ModelManager implements Model { private final AddressBook addressBook; private final UserPrefs userPrefs; private final FilteredList filteredPersons; + private final FilteredList filteredAppointments; /** * Initializes a ModelManager with the given addressBook and userPrefs. @@ -34,6 +39,7 @@ public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyUserPrefs userPrefs this.addressBook = new AddressBook(addressBook); this.userPrefs = new UserPrefs(userPrefs); filteredPersons = new FilteredList<>(this.addressBook.getPersonList()); + filteredAppointments = new FilteredList<>(this.addressBook.getAppointmentList()); } public ModelManager() { @@ -104,6 +110,39 @@ public void addPerson(Person person) { updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); } + /** + * Checks if the specified appointment is present in the list of appointments. + * + * @param appointment The appointment to check in the list. It must not be null. + * @return true if the appointment is present in the list, false otherwise. + */ + @Override + public boolean hasAppointment(Appointment appointment) { + requireNonNull(appointment); + return addressBook.hasAppointment(appointment); + } + + /** + * Removes the specified appointment from the list of appointments. + * + * @param appointment The appointment to be removed from the list. It must not be null. + */ + @Override + public void deleteAppointment(Appointment appointment) { + addressBook.deleteAppointment(appointment); + } + + /** + * Adds the specified appointment to the list of appointments. + * + * @param appointment The appointment to add to the list. It must not be null. + */ + @Override + public void addAppointment(Appointment appointment) { + addressBook.addAppointment(appointment); + updateFilteredAppointmentList(PREDICATE_SHOW_ALL_APPOINTMENTS); + } + @Override public void setPerson(Person target, Person editedPerson) { requireAllNonNull(target, editedPerson); @@ -111,6 +150,12 @@ public void setPerson(Person target, Person editedPerson) { addressBook.setPerson(target, editedPerson); } + @Override + public void setAppointment(Appointment target, Appointment editedAppointment) { + requireAllNonNull(target, editedAppointment); + addressBook.setAppointment(target, editedAppointment); + } + //=========== Filtered Person List Accessors ============================================================= /** @@ -122,12 +167,23 @@ public ObservableList getFilteredPersonList() { return filteredPersons; } + @Override + public ObservableList getFilteredAppointmentList() { + return filteredAppointments; + } + @Override public void updateFilteredPersonList(Predicate predicate) { requireNonNull(predicate); filteredPersons.setPredicate(predicate); } + @Override + public void updateFilteredAppointmentList(Predicate predicate) { + requireNonNull(predicate); + filteredAppointments.setPredicate(predicate); + } + @Override public boolean equals(Object other) { if (other == this) { @@ -145,4 +201,21 @@ public boolean equals(Object other) { && filteredPersons.equals(otherModelManager.filteredPersons); } + /** + * Checks if an appointment is valid by comparing if doctor and patient involved exist. + * @param appointment appointment to check validity of. + * @return boolean indicating if appointment is valid. + */ + public boolean isValidAppointment(Appointment appointment) throws PersonNotFoundException { + Nric doctorNric = appointment.getDoctorNric(); + Nric patientNric = appointment.getPatientNric(); + Person doctor = addressBook.getPersonByNric(doctorNric); + Person patient = addressBook.getPersonByNric(patientNric); + + if (doctor.getType() == Type.DOCTOR && patient.getType() == Type.PATIENT) { + return true; + } + + return false; + } } diff --git a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java index 6ddc2cd9a29..2e54ac7551b 100644 --- a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java +++ b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java @@ -1,6 +1,7 @@ package seedu.address.model; import javafx.collections.ObservableList; +import seedu.address.model.appointment.Appointment; import seedu.address.model.person.Person; /** @@ -14,4 +15,10 @@ public interface ReadOnlyAddressBook { */ ObservableList getPersonList(); + /** + * Returns an unmodifiable view of the appointments list. + * This list will not contain any duplicate appointments. + */ + ObservableList getAppointmentList(); + } diff --git a/src/main/java/seedu/address/model/UserPrefs.java b/src/main/java/seedu/address/model/UserPrefs.java index 6be655fb4c7..20036a239bc 100644 --- a/src/main/java/seedu/address/model/UserPrefs.java +++ b/src/main/java/seedu/address/model/UserPrefs.java @@ -14,7 +14,7 @@ public class UserPrefs implements ReadOnlyUserPrefs { private GuiSettings guiSettings = new GuiSettings(); - private Path addressBookFilePath = Paths.get("data" , "addressbook.json"); + private Path addressBookFilePath = Paths.get("data" , "medicli.json"); /** * Creates a {@code UserPrefs} with default values. diff --git a/src/main/java/seedu/address/model/appointment/Appointment.java b/src/main/java/seedu/address/model/appointment/Appointment.java new file mode 100644 index 00000000000..d8bdb0e0684 --- /dev/null +++ b/src/main/java/seedu/address/model/appointment/Appointment.java @@ -0,0 +1,186 @@ +package seedu.address.model.appointment; + +import static seedu.address.commons.util.AppUtil.checkArgument; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.logging.Level; +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.util.ToStringBuilder; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.appointment.exceptions.InvalidAppointmentException; +import seedu.address.model.person.Nric; +import seedu.address.model.person.Person; + +/** + * Appointment class that describes an appointment in MediCLI. + */ +public class Appointment { + + private static final Logger logger = LogsCenter.getLogger(Appointment.class); + + private static final String MESSAGE_CONSTRAINTS_INVALID_DATE = + "Appointments should not be scheduled in the past."; + + // The doctor in charge of the appointment + private Nric doctorNric; + + // The patient benefiting from the appointment + private Nric patientNric; + + // The date of the appointment + private final AppointmentDateTime appointmentDateTime; + + /** + * Constructs a new appointment instance + * @param doctorNric doctor in charge + * @param patientNric patient of the appointment + * @param appointmentDateTime date of the appointment + * @param isInitialised a boolean value indication whether this was initialised by the json file + * @throws ParseException + */ + public Appointment(Nric doctorNric, Nric patientNric, AppointmentDateTime appointmentDateTime, + Boolean isInitialised) throws ParseException { + requireAllNonNull(doctorNric, patientNric, appointmentDateTime); + logger.log(Level.INFO, "Going to create new appointment instance"); + + if (!isInitialised) { + try { + checkArgument(isValidAppointmentDateTime(appointmentDateTime), MESSAGE_CONSTRAINTS_INVALID_DATE); + } catch (IllegalArgumentException e) { + logger.log(Level.INFO, "Appointment parameter check failed"); + throw new ParseException(e.getMessage()); + } + } + + this.doctorNric = doctorNric; + this.patientNric = patientNric; + this.appointmentDateTime = appointmentDateTime; + } + + /** + * Checks if appointment is valid by comparing appointment date against current date. + * A valid new appointment can only be in the future, not the past. + * + * @param appointmentDate Date to check validity of + * @return boolean if appointment is valid or not + */ + public boolean isValidAppointmentDateTime(AppointmentDateTime appointmentDate) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + AppointmentDateTime currentDateTime = new AppointmentDateTime(LocalDateTime.now().format(formatter)); + return appointmentDate.compareTo(currentDateTime) > -1; + } + + /** + * Gets Nric of doctor in charge. + * @return Nric of doctor in charge. + */ + public Nric getDoctorNric() { + return doctorNric; + } + + /** + * Sets the doctor nric to input nric. + * + * @param nric the new doctor nric. + * @throws InvalidAppointmentException if nric is null. + */ + public void setDoctorNric(Nric nric) throws InvalidAppointmentException { + // If multiplicity is violated, throw exception. Appointment cannot have null doctor nric. + if (nric == null) { + throw new InvalidAppointmentException(); + } + this.doctorNric = nric; + } + + /** + * Gets nric of the patient of the appointment. + * @return nric of patient of the appointment. + */ + public Nric getPatientNric() { + return patientNric; + } + + /** + * Sets the patient nric to input nric. + * + * @param nric the new patient nric. + * @throws InvalidAppointmentException if nric is null. + */ + public void setPatientNric(Nric nric) throws InvalidAppointmentException { + // If multiplicity is violated, throw exception. Appointment cannot have null patient nric. + if (nric == null) { + throw new InvalidAppointmentException(); + } + this.patientNric = nric; + } + + + /** + * Gets date & time of the appointment. + * + * @return date & time of the appointment. + */ + public AppointmentDateTime getAppointmentDateTime() { + return appointmentDateTime; + } + + /** + * Checks if appointment is same as input one by comparing persons involved and date. + * + * @param appt input appointment to compare current appointment against. + * @return boolean indicating if appointments are the same or not. + */ + public boolean isSameAppointment(Appointment appt) { + if (appt == this) { + return true; + } + + return appt != null + && appt.getDoctorNric().equals(this.getDoctorNric()) + && appt.getPatientNric().equals(this.getPatientNric()) + && appt.getAppointmentDateTime().equals(this.getAppointmentDateTime()); + } + + /** + * Checks if the given {@code Person} is associated with this appointment either as a doctor or a patient. + * + * @param person The {@code Person} to check if associated with this appointment. + * @return {@code true} if the person's NRIC matches either the doctor's NRIC or the patient's NRIC, + * {@code false} otherwise. + */ + public boolean appointmentContainsPerson(Person person) { + return person.getNric().equals(this.doctorNric) + || person.getNric().equals(this.patientNric); + } + + @Override + public boolean equals(Object appt) { + if (appt == this) { + return true; + } + + if (!(appt instanceof Appointment)) { + return false; + } + + Appointment appointment = (Appointment) appt; + + return appt != null + && appointment.getDoctorNric().equals(this.getDoctorNric()) + && appointment.getPatientNric().equals(this.getPatientNric()) + && appointment.getAppointmentDateTime().equals(this.getAppointmentDateTime()); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("Date", getAppointmentDateTime()) + .add("Doctor", getDoctorNric()) + .add("Patient", getPatientNric()) + .toString(); + } +} diff --git a/src/main/java/seedu/address/model/appointment/AppointmentContainsDoctorPredicate.java b/src/main/java/seedu/address/model/appointment/AppointmentContainsDoctorPredicate.java new file mode 100644 index 00000000000..a9fc64f5718 --- /dev/null +++ b/src/main/java/seedu/address/model/appointment/AppointmentContainsDoctorPredicate.java @@ -0,0 +1,57 @@ +package seedu.address.model.appointment; + +import java.util.List; +import java.util.function.Predicate; +import java.util.logging.Level; +import java.util.logging.Logger; + +import seedu.address.commons.util.StringUtil; +import seedu.address.commons.util.ToStringBuilder; + +/** + * Represents a Predicate used to test if an Appointment contains specified doctor keywords. + */ +public class AppointmentContainsDoctorPredicate implements Predicate { + private static final Logger logger = Logger.getLogger(AppointmentContainsDoctorPredicate.class.getName()); + private final List keywords; + + public AppointmentContainsDoctorPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Appointment appointment) { + assert appointment != null : "Appointment cannot be null"; + logger.log(Level.INFO, "Testing appointment: " + appointment); + + boolean result = keywords.stream() + .anyMatch(keyword -> { + boolean contains = StringUtil.containsWordIgnoreCase(appointment.getDoctorNric().nric, keyword); + logger.log(Level.INFO, "Keyword: " + keyword + " contains in appointment: " + contains); + return contains; + }); + + logger.log(Level.INFO, "Test result: " + result); + return result; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof AppointmentContainsDoctorPredicate)) { + return false; + } + + AppointmentContainsDoctorPredicate otherPredicate = (AppointmentContainsDoctorPredicate) other; + return keywords.equals(otherPredicate.keywords); + } + + @Override + public String toString() { + return new ToStringBuilder(this).add("keywords", keywords).toString(); + } +} diff --git a/src/main/java/seedu/address/model/appointment/AppointmentContainsPatientPredicate.java b/src/main/java/seedu/address/model/appointment/AppointmentContainsPatientPredicate.java new file mode 100644 index 00000000000..be8d7a17f39 --- /dev/null +++ b/src/main/java/seedu/address/model/appointment/AppointmentContainsPatientPredicate.java @@ -0,0 +1,53 @@ +package seedu.address.model.appointment; + +import java.util.List; +import java.util.function.Predicate; +import java.util.logging.Level; +import java.util.logging.Logger; + +import seedu.address.commons.util.StringUtil; +import seedu.address.commons.util.ToStringBuilder; + +/** + * Represents a Predicate used to test if an Appointment contains specified patient keywords. + */ +public class AppointmentContainsPatientPredicate implements Predicate { + private static final Logger logger = Logger.getLogger(AppointmentContainsPatientPredicate.class.getName()); + private final List keywords; + + public AppointmentContainsPatientPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Appointment appointment) { + assert appointment != null : "Appointment cannot be null"; + logger.log(Level.INFO, "Testing appointment: " + appointment); + return keywords.stream() + .anyMatch(keyword -> { + boolean contains = StringUtil.containsWordIgnoreCase(appointment.getPatientNric().nric, keyword); + logger.log(Level.INFO, "Keyword: " + keyword + " contains in appointment: " + contains); + return contains; + }); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof AppointmentContainsPatientPredicate)) { + return false; + } + + AppointmentContainsPatientPredicate otherPredicate = (AppointmentContainsPatientPredicate) other; + return keywords.equals(otherPredicate.keywords); + } + + @Override + public String toString() { + return new ToStringBuilder(this).add("keywords", keywords).toString(); + } +} diff --git a/src/main/java/seedu/address/model/appointment/AppointmentDateTime.java b/src/main/java/seedu/address/model/appointment/AppointmentDateTime.java new file mode 100644 index 00000000000..3411bfe264b --- /dev/null +++ b/src/main/java/seedu/address/model/appointment/AppointmentDateTime.java @@ -0,0 +1,110 @@ +package seedu.address.model.appointment; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +/** + * Class encapsulating an appointment's date/time and corresponding methods. + */ +public class AppointmentDateTime { + + // Message to output in case constraints are not met + public static final String MESSAGE_CONSTRAINTS = + "Appointment date-time should be in the format of yyyy-MM-dd HH:mm."; + + // Variable storing appointment date in a local datetime instance + public final LocalDateTime appointmentDateTime; + + /** + * Constructs new AppointmentDate object using an input date string in yyyy-MM-dd HH:mm format. + * @param dateStr input string to be stored. + */ + public AppointmentDateTime(String dateStr) { + assert dateStr.length() == 16 : "Appointment date-time string is of incorrect length"; + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + requireNonNull(dateStr); + checkArgument(isValidDate(dateStr), MESSAGE_CONSTRAINTS); + this.appointmentDateTime = LocalDateTime.parse(dateStr, formatter); + } + + /** + * Overloaded constructor that constructs a new instance using a LocalDateTime rather than datetime string. + * @param dateTime LocalDateTime instance to construct AppointmentDate around. + */ + public AppointmentDateTime(LocalDateTime dateTime) { + requireNonNull(dateTime); + this.appointmentDateTime = dateTime; + } + + /** + * Checks if a provided input date string is in a valid format. + * + * @param dateStr input date string. + * @return boolean indicating if format is valid or not. + */ + public static boolean isValidDate(String dateStr) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + try { + LocalDateTime temp = LocalDateTime.parse(dateStr, formatter); + } catch (DateTimeParseException e) { + return false; + } + return true; + } + + /** + * Returns string version of appointment date for printing. + * + * @return String stringed appointment date. + */ + @Override + public String toString() { + return DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm").format(this.appointmentDateTime); + } + + /** + * Checks if input object is practically equal to this AppointmentDate object. + * + * @param obj input object. + * @return boolean indicating if compared objects are equal. + */ + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + + // instanceof handles nulls + if (!(obj instanceof AppointmentDateTime)) { + return false; + } + + AppointmentDateTime ad = (AppointmentDateTime) obj; + return appointmentDateTime.equals(ad.appointmentDateTime); + } + + /** + * Returns hashcode of appointment date. + * + * @return int hashcode. + */ + @Override + public int hashCode() { + return appointmentDateTime.hashCode(); + } + + /** + * Compares two AppointmentDate instances together. + * + * @param compareValue value to compare with current instance. + * @return integer reflecting whether compareValue is greater, less, or equal. + */ + public int compareTo(AppointmentDateTime compareValue) { + return this.appointmentDateTime.compareTo(compareValue.appointmentDateTime); + } + +} diff --git a/src/main/java/seedu/address/model/appointment/UniqueAppointmentList.java b/src/main/java/seedu/address/model/appointment/UniqueAppointmentList.java new file mode 100644 index 00000000000..623627eefc7 --- /dev/null +++ b/src/main/java/seedu/address/model/appointment/UniqueAppointmentList.java @@ -0,0 +1,179 @@ +package seedu.address.model.appointment; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seedu.address.model.appointment.exceptions.DuplicateAppointmentException; +import seedu.address.model.person.Person; + +/** + * A list of appointments that enforces uniqueness between its elements and does not allow nulls. + * An appointment is considered unique by comparing using {@code Appointment#isSameAppointment(Appointment)}. + * As such, adding and updating of appointments uses Appointment#isSameAppointment(Appointment) for equality + * so as to ensure that the Appointment being added or updated is + * unique in terms of identity in the UniqueAppointmentList. + * Supports a minimal set of list operations. + * + * @see Appointment#isSameAppointment(Appointment) + */ +public class UniqueAppointmentList implements Iterable { + private final ObservableList internalList = FXCollections.observableArrayList(); + private final ObservableList internalUnmodifiableList = + FXCollections.unmodifiableObservableList(internalList); + + /** + * Returns true if the list contains an equivalent appointment as the given argument. + * @param toCheck Appointment to check for in the list. + * @return boolean indicating if appointment is contained. + */ + public boolean contains(Appointment toCheck) { + requireNonNull(toCheck); + return internalList.stream().anyMatch(toCheck::isSameAppointment); + } + + /** + * Returns a list of appointments if the appointment contains the person. Checked via person's NRIC + * @param person target person. + * @return list of appointments. + */ + public List contains(Person person) { + requireNonNull(person); + return internalList.stream() + .filter(appointment -> appointment.appointmentContainsPerson(person)) + .collect(Collectors.toList()); + } + + /** + * Adds an appointment to the list. + * The appointment must not already exist in the list. + * + * @param toAdd Appointment toAdd + */ + public void add(Appointment toAdd) { + requireNonNull(toAdd); + internalList.add(toAdd); + } + + /** + * Replaces the appointment {@code target} in the list with {@code editedAppointment}. + * {@code target} must exist in the list. + * The appointment details of {@code editedAppointment} must not be the same as another + * existing appointment in the list. + * + * @param target the Appointment to replace. + * @param editedAppointment the Appointment to edit. + */ + public void setAppointment(Appointment target, Appointment editedAppointment) { + requireAllNonNull(target, editedAppointment); + + int index = internalList.indexOf(target); + + internalList.set(index, editedAppointment); + } + + /** + * Removes the equivalent appointment from the list. + * The appointment must exist in the list. + * + * @param toRemove the Appointment to remove from the list. + */ + public void remove(Appointment toRemove) { + requireNonNull(toRemove); + assert internalList.contains(toRemove) + : "Internal list should contain toRemove as check is done prior to method call"; + internalList.remove(toRemove); + } + + + /** + * Replaces the contents of this list with {@code appointments}. + * {@code appointments} must not contain duplicate appointments. + * + * @param appointments the list of Appointments to replace the current list with. + */ + public void setAppointments(List appointments) throws DuplicateAppointmentException { + requireAllNonNull(appointments); + if (!appointmentsAreUnique(appointments)) { + throw new DuplicateAppointmentException(); + } + + internalList.setAll(appointments); + } + + /** + * Replaces the contents of this list with {@code appointments}. + * {@code appointments} must not contain duplicate appointments. + * + * This method does not throw DuplicateAppointmentException because it is only called when resetting the data. + * @param appointments + */ + public void setAppointmentsExistingBook(List appointments) { + requireAllNonNull(appointments); + boolean isAllAppointmentsUnique = appointmentsAreUnique(appointments); + assert isAllAppointmentsUnique == true : "when this method is called appointments should be unique"; + internalList.setAll(appointments); + } + + /** + * Returns the backing list as an unmodifiable {@code ObservableList}. + * + * @return ObservableList the backing list + */ + public ObservableList asUnmodifiableObservableList() { + return internalUnmodifiableList; + } + + @Override + public Iterator iterator() { + return internalList.iterator(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof UniqueAppointmentList)) { + return false; + } + + UniqueAppointmentList otherUniqueAppointmentList = (UniqueAppointmentList) other; + return internalList.equals(otherUniqueAppointmentList.internalList); + } + + @Override + public int hashCode() { + return internalList.hashCode(); + } + + @Override + public String toString() { + return internalList.toString(); + } + + /** + * Returns true if {@code appointments} contains only unique appointments. + * + * @param appointments the list of appointments to check for uniqueness. + * @return boolean value indicating if appointments are unique. + */ + private boolean appointmentsAreUnique(List appointments) { + + for (int i = 0; i < appointments.size() - 1; i++) { + for (int j = i + 1; j < appointments.size(); j++) { + if (appointments.get(i).isSameAppointment(appointments.get(j))) { + return false; + } + } + } + return true; + } +} diff --git a/src/main/java/seedu/address/model/appointment/exceptions/AppointmentNotFoundException.java b/src/main/java/seedu/address/model/appointment/exceptions/AppointmentNotFoundException.java new file mode 100644 index 00000000000..4131a12ad17 --- /dev/null +++ b/src/main/java/seedu/address/model/appointment/exceptions/AppointmentNotFoundException.java @@ -0,0 +1,16 @@ +package seedu.address.model.appointment.exceptions; + +import seedu.address.logic.commands.exceptions.CommandException; + +/** + * Signals that the operation is unable to find the specified appointment. + */ +public class AppointmentNotFoundException extends CommandException { + + /** + * Creates new instance of AppointmentNotFoundException. + */ + public AppointmentNotFoundException() { + super("Unable to locate appointment"); + } +} diff --git a/src/main/java/seedu/address/model/appointment/exceptions/DuplicateAppointmentException.java b/src/main/java/seedu/address/model/appointment/exceptions/DuplicateAppointmentException.java new file mode 100644 index 00000000000..917df103fee --- /dev/null +++ b/src/main/java/seedu/address/model/appointment/exceptions/DuplicateAppointmentException.java @@ -0,0 +1,17 @@ +package seedu.address.model.appointment.exceptions; + +import seedu.address.logic.commands.exceptions.CommandException; + +/** + * Signals that the operation will result in duplicate Appointments. + * (Appointments are considered duplicates if they have the same identity). + */ +public class DuplicateAppointmentException extends CommandException { + + /** + * Creates new instance of DuplicateAppointmentException. + */ + public DuplicateAppointmentException() { + super("Operation would result in duplicate appointments"); + } +} diff --git a/src/main/java/seedu/address/model/appointment/exceptions/InvalidAppointmentException.java b/src/main/java/seedu/address/model/appointment/exceptions/InvalidAppointmentException.java new file mode 100644 index 00000000000..71523045c6f --- /dev/null +++ b/src/main/java/seedu/address/model/appointment/exceptions/InvalidAppointmentException.java @@ -0,0 +1,16 @@ +package seedu.address.model.appointment.exceptions; + +import seedu.address.logic.commands.exceptions.CommandException; + +/** + * Signals that the operation is unable to find the specified appointment. + */ +public class InvalidAppointmentException extends CommandException { + + /** + * Creates new instance of appointment not found exception. + */ + public InvalidAppointmentException() { + super("This appointment is invalid due to invalid inputs."); + } +} diff --git a/src/main/java/seedu/address/model/person/DoB.java b/src/main/java/seedu/address/model/person/DoB.java new file mode 100644 index 00000000000..5166b909b5c --- /dev/null +++ b/src/main/java/seedu/address/model/person/DoB.java @@ -0,0 +1,79 @@ +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.FormatStyle; + +/** + * Represents a Person's date of birth in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidDoB(String)} + */ +public class DoB { + + public static final String MESSAGE_CONSTRAINTS = + "Date of births should only contain numeric characters in the format yyyy-mm-dd. " + + "Acceptable date range is from 1900 Janurary 1st to today's date."; + + /* + * The first character of the address must not be a whitespace, + * otherwise " " (a blank string) becomes a valid input. + */ + + public final LocalDate dateOfBirth; + + /** + * Constructs a {@code DoB}. + * + * @param dob A valid date of birth. + */ + public DoB(String dob) { + requireNonNull(dob); + assert dob.length() == 10 : "Person dob string is of incorrect length"; + checkArgument(isValidDoB(dob), MESSAGE_CONSTRAINTS); + dateOfBirth = LocalDate.parse(dob); + } + + /** + * Returns true if a given string is a valid name. + */ + public static boolean isValidDoB(String test) { + LocalDate first = LocalDate.parse("1900-01-01"); + LocalDate last = LocalDate.now(); + try { + LocalDate date = LocalDate.parse(test); + return !date.isBefore(first) && !date.isAfter(last); + } catch (java.time.format.DateTimeParseException e) { + return false; + } + } + + + @Override + public String toString() { + return dateOfBirth.format(DateTimeFormatter.ofLocalizedDate(FormatStyle.LONG)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof DoB)) { + return false; + } + + DoB otherDoB = (DoB) other; + return dateOfBirth.equals(otherDoB.dateOfBirth); + } + + @Override + public int hashCode() { + return dateOfBirth.hashCode(); + } + +} diff --git a/src/main/java/seedu/address/model/person/Doctor.java b/src/main/java/seedu/address/model/person/Doctor.java new file mode 100644 index 00000000000..461cef94760 --- /dev/null +++ b/src/main/java/seedu/address/model/person/Doctor.java @@ -0,0 +1,59 @@ +package seedu.address.model.person; + +import java.util.Objects; + +import seedu.address.commons.util.ToStringBuilder; + +/** + * Represents a Doctor in the address book. + * Guarantees: details are present and not null, field values are validated, immutable. + */ +public class Doctor extends Person { + + /** + * Every field must be present and not null. + */ + public Doctor(Nric nric, Name name, DoB dob, Phone phone) { + super(Type.DOCTOR, nric, name, dob, phone); + } + + /** + * Returns true if both persons have the same identity and data fields. + * This defines a stronger notion of equality between two persons. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof Doctor)) { + return false; + } + + Doctor otherPerson = (Doctor) other; + return getNric().equals(otherPerson.getNric()) + && getName().equals(otherPerson.getName()) + && getDoB().equals(otherPerson.getDoB()) + && getPhone().equals(otherPerson.getPhone()); + } + + @Override + public int hashCode() { + // use this method for custom fields hashing instead of implementing your own + return Objects.hash(Type.DOCTOR, getNric(), getName(), getDoB(), getPhone()); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("type", Type.DOCTOR) + .add("nric", getNric()) + .add("name", getName()) + .add("dob", getDoB()) + .add("phone", getPhone()) + .toString(); + } + +} diff --git a/src/main/java/seedu/address/model/person/DoctorContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/DoctorContainsKeywordsPredicate.java new file mode 100644 index 00000000000..d5a78ec9915 --- /dev/null +++ b/src/main/java/seedu/address/model/person/DoctorContainsKeywordsPredicate.java @@ -0,0 +1,52 @@ +package seedu.address.model.person; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.StringUtil; +import seedu.address.commons.util.ToStringBuilder; + +/** + * Tests that a {@code Patient}'s {@code Name} matches any of the keywords given. + */ +public class DoctorContainsKeywordsPredicate implements Predicate { + private final List keywords; + + public DoctorContainsKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Person doctor) { + if (doctor.getType() != Type.DOCTOR) { + return false; + } + + return keywords.stream().anyMatch(keyword -> + StringUtil.containsSubstringIgnoreCase(doctor.getNric().nric, keyword) + || StringUtil.containsSubstringIgnoreCase(doctor.getName().fullName, keyword) + || StringUtil.containsSubstringIgnoreCase(doctor.getDoB().dateOfBirth.toString(), keyword) + || StringUtil.containsSubstringIgnoreCase(doctor.getPhone().value, keyword) + ); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof DoctorContainsKeywordsPredicate)) { + return false; + } + + DoctorContainsKeywordsPredicate otherPredicate = (DoctorContainsKeywordsPredicate) other; + return keywords.equals(otherPredicate.keywords); + } + + @Override + public String toString() { + return new ToStringBuilder(this).add("keywords", keywords).toString(); + } +} diff --git a/src/main/java/seedu/address/model/person/Name.java b/src/main/java/seedu/address/model/person/Name.java index 173f15b9b00..406caf63d8f 100644 --- a/src/main/java/seedu/address/model/person/Name.java +++ b/src/main/java/seedu/address/model/person/Name.java @@ -10,13 +10,13 @@ public class Name { public static final String MESSAGE_CONSTRAINTS = - "Names should only contain alphanumeric characters and spaces, and it should not be blank"; + "Names should only contain alphabetical characters and spaces, and it should not be blank"; /* * The first character of the address must not be a whitespace, * otherwise " " (a blank string) becomes a valid input. */ - public static final String VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} ]*"; + public static final String VALIDATION_REGEX = "[a-zA-Z][a-zA-Z ]*"; public final String fullName; diff --git a/src/main/java/seedu/address/model/person/Nric.java b/src/main/java/seedu/address/model/person/Nric.java new file mode 100644 index 00000000000..9a659ae5ccf --- /dev/null +++ b/src/main/java/seedu/address/model/person/Nric.java @@ -0,0 +1,68 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Person's nric in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidNric(String)} + */ +public class Nric { + + public static final String MESSAGE_CONSTRAINTS = + "NRIC should only contain alphanumeric characters of length 9 and match the correct format."; + + /* + * The first character of the address must not be a whitespace, + * otherwise " " (a blank string) becomes a valid input. + */ + public static final String VALIDATION_REGEX = "^[STFGMstfgm][0-9]{7}[A-Za-z]$"; + + public final String nric; + + /** + * Constructs a {@code NRIC}. + * + * @param nric A valid nric. + */ + public Nric(String nric) { + requireNonNull(nric); + assert nric.length() == 9 : "Person nric string is of incorrect length"; + checkArgument(isValidNric(nric), MESSAGE_CONSTRAINTS); + this.nric = nric; + } + + /** + * Returns true if a given string is a valid name. + */ + public static boolean isValidNric(String test) { + return test.matches(VALIDATION_REGEX); + } + + + @Override + public String toString() { + return nric; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof Nric)) { + return false; + } + + Nric otherNric = (Nric) other; + return nric.toLowerCase().equals(otherNric.nric.toLowerCase()); + } + + @Override + public int hashCode() { + return nric.hashCode(); + } + +} diff --git a/src/main/java/seedu/address/model/person/Patient.java b/src/main/java/seedu/address/model/person/Patient.java new file mode 100644 index 00000000000..3c90167b4e7 --- /dev/null +++ b/src/main/java/seedu/address/model/person/Patient.java @@ -0,0 +1,59 @@ +package seedu.address.model.person; + +import java.util.Objects; + +import seedu.address.commons.util.ToStringBuilder; + +/** + * Represents a Patient in the address book. + * Guarantees: details are present and not null, field values are validated, immutable. + */ +public class Patient extends Person { + + /** + * Every field must be present and not null. + */ + public Patient(Nric nric, Name name, DoB dob, Phone phone) { + super(Type.PATIENT, nric, name, dob, phone); + } + + /** + * Returns true if both persons have the same identity and data fields. + * This defines a stronger notion of equality between two persons. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof Patient)) { + return false; + } + + Patient otherPerson = (Patient) other; + return getNric().equals(otherPerson.getNric()) + && getName().equals(otherPerson.getName()) + && getDoB().equals(otherPerson.getDoB()) + && getPhone().equals(otherPerson.getPhone()); + } + + @Override + public int hashCode() { + // use this method for custom fields hashing instead of implementing your own + return Objects.hash(Type.PATIENT, getNric(), getName(), getDoB(), getPhone()); + } + + @Override + public String toString() { + return new ToStringBuilder(this) + .add("type", Type.PATIENT) + .add("nric", getNric()) + .add("name", getName()) + .add("dob", getDoB()) + .add("phone", getPhone()) + .toString(); + } + +} diff --git a/src/main/java/seedu/address/model/person/PatientContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/PatientContainsKeywordsPredicate.java new file mode 100644 index 00000000000..9e37fd67d38 --- /dev/null +++ b/src/main/java/seedu/address/model/person/PatientContainsKeywordsPredicate.java @@ -0,0 +1,52 @@ +package seedu.address.model.person; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.StringUtil; +import seedu.address.commons.util.ToStringBuilder; + +/** + * Tests that a {@code Patient}'s {@code Name} matches any of the keywords given. + */ +public class PatientContainsKeywordsPredicate implements Predicate { + private final List keywords; + + public PatientContainsKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Person patient) { + if (patient.getType() != Type.PATIENT) { + return false; + } + + return keywords.stream().anyMatch(keyword -> + StringUtil.containsSubstringIgnoreCase(patient.getNric().nric, keyword) + || StringUtil.containsSubstringIgnoreCase(patient.getName().fullName, keyword) + || StringUtil.containsSubstringIgnoreCase(patient.getDoB().dateOfBirth.toString(), keyword) + || StringUtil.containsSubstringIgnoreCase(patient.getPhone().value, keyword) + ); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof PatientContainsKeywordsPredicate)) { + return false; + } + + PatientContainsKeywordsPredicate otherPredicate = (PatientContainsKeywordsPredicate) other; + return keywords.equals(otherPredicate.keywords); + } + + @Override + public String toString() { + return new ToStringBuilder(this).add("keywords", keywords).toString(); + } +} diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java index abe8c46b535..580f61e1200 100644 --- a/src/main/java/seedu/address/model/person/Person.java +++ b/src/main/java/seedu/address/model/person/Person.java @@ -2,65 +2,47 @@ import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; -import java.util.Collections; -import java.util.HashSet; -import java.util.Objects; -import java.util.Set; - -import seedu.address.commons.util.ToStringBuilder; -import seedu.address.model.tag.Tag; - /** * Represents a Person in the address book. * Guarantees: details are present and not null, field values are validated, immutable. */ -public class Person { +public abstract class Person { // Identity fields + private final Type type; + private final Nric nric; private final Name name; + private final DoB dob; private final Phone phone; - private final Email email; - - // Data fields - private final Address address; - private final Set tags = new HashSet<>(); /** * 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(Type type, Nric nric, Name name, DoB dob, Phone phone) { + requireAllNonNull(type, nric, name, dob, phone); + this.type = type; + this.nric = nric; this.name = name; + this.dob = dob; this.phone = phone; - this.email = email; - this.address = address; - this.tags.addAll(tags); } + public Type getType() { + return type; + } + public Nric getNric() { + return nric; + } public Name getName() { return name; } - + public DoB getDoB() { + return dob; + } public Phone getPhone() { return phone; } - public Email getEmail() { - return email; - } - - public Address getAddress() { - return address; - } - - /** - * Returns an immutable tag set, which throws {@code UnsupportedOperationException} - * if modification is attempted. - */ - public Set getTags() { - return Collections.unmodifiableSet(tags); - } - /** * Returns true if both persons have the same name. * This defines a weaker notion of equality between two persons. @@ -71,47 +53,6 @@ public boolean isSamePerson(Person otherPerson) { } return otherPerson != null - && otherPerson.getName().equals(getName()); + && otherPerson.getNric().equals(getNric()); } - - /** - * Returns true if both persons have the same identity and data fields. - * This defines a stronger notion of equality between two persons. - */ - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof Person)) { - return false; - } - - Person otherPerson = (Person) other; - return name.equals(otherPerson.name) - && phone.equals(otherPerson.phone) - && email.equals(otherPerson.email) - && address.equals(otherPerson.address) - && tags.equals(otherPerson.tags); - } - - @Override - public int hashCode() { - // use this method for custom fields hashing instead of implementing your own - return Objects.hash(name, phone, email, address, tags); - } - - @Override - public String toString() { - return new ToStringBuilder(this) - .add("name", name) - .add("phone", phone) - .add("email", email) - .add("address", address) - .add("tags", tags) - .toString(); - } - } diff --git a/src/main/java/seedu/address/model/person/Phone.java b/src/main/java/seedu/address/model/person/Phone.java index d733f63d739..cd0dd9ae1be 100644 --- a/src/main/java/seedu/address/model/person/Phone.java +++ b/src/main/java/seedu/address/model/person/Phone.java @@ -11,8 +11,8 @@ 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 exactly 8 digits long"; + public static final String VALIDATION_REGEX = "\\d{8}"; public final String value; /** @@ -22,6 +22,7 @@ public class Phone { */ public Phone(String phone) { requireNonNull(phone); + assert phone.length() == 8 : "Person phone string is of incorrect length"; checkArgument(isValidPhone(phone), MESSAGE_CONSTRAINTS); value = phone; } diff --git a/src/main/java/seedu/address/model/person/Type.java b/src/main/java/seedu/address/model/person/Type.java new file mode 100644 index 00000000000..cb43a837eef --- /dev/null +++ b/src/main/java/seedu/address/model/person/Type.java @@ -0,0 +1,16 @@ +package seedu.address.model.person; + +/** + * Enumeration representing different types of entities in the system. + * This enum defines two constants: {@code PATIENT} and {@code DOCTOR}, + * which represent distinct roles or types of entities within the system. + * The {@code toString()} method is overridden to return the name of the enum constant. + */ +public enum Type { + PATIENT, + DOCTOR; + + public String toString() { + return name(); + } +} diff --git a/src/main/java/seedu/address/model/person/UniquePersonList.java b/src/main/java/seedu/address/model/person/UniquePersonList.java index cc0a68d79f9..a05cd78c48f 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.ArrayList; import java.util.Iterator; import java.util.List; @@ -11,6 +12,7 @@ import seedu.address.model.person.exceptions.DuplicatePersonException; import seedu.address.model.person.exceptions.PersonNotFoundException; + /** * A list of persons that enforces uniqueness between its elements and does not allow nulls. * A person is considered unique by comparing using {@code Person#isSamePerson(Person)}. As such, adding and updating of @@ -36,6 +38,17 @@ public boolean contains(Person toCheck) { return internalList.stream().anyMatch(toCheck::isSamePerson); } + /** + * Check if list contains person with the nric in question. + * + * @param nricToCheck String nric in question. + * @return boolean indicating if person is in list. + */ + public boolean containsNric(String nricToCheck) { + requireNonNull(nricToCheck); + return internalList.stream().anyMatch(x -> x.getNric().equals(new Nric(nricToCheck))); + } + /** * Adds a person to the list. * The person must not already exist in the list. @@ -147,4 +160,26 @@ private boolean personsAreUnique(List persons) { } return true; } + + /** + * Retrieves a {@code Person} object from the list of persons based on the specified NRIC. + * The method iterates through an internal list of persons, comparing the NRIC of each person + * with the given NRIC object. If a match is found, the corresponding {@code Person} object + * is returned. + * + * @param nricObj The NRIC object used to identify the person. It must not be null. + * @return The {@code Person} object that matches the given NRIC. + * @throws PersonNotFoundException If no person with the given NRIC can be found in the list. + */ + public Person getPersonByNric(Nric nricObj) throws PersonNotFoundException { + ArrayList personList = new ArrayList(internalList); + + for (Person p : personList) { + if (p.getNric().equals(nricObj)) { + return p; + } + } + + throw new PersonNotFoundException(); + } } diff --git a/src/main/java/seedu/address/model/tag/Tag.java b/src/main/java/seedu/address/model/tag/Tag.java deleted file mode 100644 index f1a0d4e233b..00000000000 --- a/src/main/java/seedu/address/model/tag/Tag.java +++ /dev/null @@ -1,62 +0,0 @@ -package seedu.address.model.tag; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; - -/** - * Represents a Tag in the address book. - * Guarantees: immutable; name is valid as declared in {@link #isValidTagName(String)} - */ -public class Tag { - - public static final String MESSAGE_CONSTRAINTS = "Tags names should be alphanumeric"; - public static final String VALIDATION_REGEX = "\\p{Alnum}+"; - - public final String tagName; - - /** - * Constructs a {@code Tag}. - * - * @param tagName A valid tag name. - */ - public Tag(String tagName) { - requireNonNull(tagName); - checkArgument(isValidTagName(tagName), MESSAGE_CONSTRAINTS); - this.tagName = tagName; - } - - /** - * Returns true if a given string is a valid tag name. - */ - public static boolean isValidTagName(String test) { - return test.matches(VALIDATION_REGEX); - } - - @Override - public boolean equals(Object other) { - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof Tag)) { - return false; - } - - Tag otherTag = (Tag) other; - return tagName.equals(otherTag.tagName); - } - - @Override - public int hashCode() { - return tagName.hashCode(); - } - - /** - * Format state as text for viewing. - */ - public String toString() { - return '[' + tagName + ']'; - } - -} diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java index 1806da4facf..c1e060f7356 100644 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ b/src/main/java/seedu/address/model/util/SampleDataUtil.java @@ -1,17 +1,13 @@ package seedu.address.model.util; -import java.util.Arrays; -import java.util.Set; -import java.util.stream.Collectors; - import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; +import seedu.address.model.person.DoB; import seedu.address.model.person.Name; +import seedu.address.model.person.Nric; +import seedu.address.model.person.Patient; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; /** * Contains utility methods for populating {@code AddressBook} with sample data. @@ -19,24 +15,26 @@ public class SampleDataUtil { public static Person[] getSamplePersons() { 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 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 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 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 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 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 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 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 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 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 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 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 Patient(new Nric("S1234567A"), new Name("John Doe"), new DoB("2002-01-30"), new Phone("92624417")), + new Patient(new Nric("S0123456A"), new Name("David Li"), new DoB("2003-04-28"), new Phone("87438807")) }; } @@ -48,13 +46,4 @@ public static ReadOnlyAddressBook getSampleAddressBook() { return sampleAb; } - /** - * Returns a tag set containing the list of strings given. - */ - public static Set getTagSet(String... strings) { - return Arrays.stream(strings) - .map(Tag::new) - .collect(Collectors.toSet()); - } - } diff --git a/src/main/java/seedu/address/storage/JsonAdaptedAppointment.java b/src/main/java/seedu/address/storage/JsonAdaptedAppointment.java new file mode 100644 index 00000000000..9fb7015d6d0 --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedAppointment.java @@ -0,0 +1,108 @@ +package seedu.address.storage; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.appointment.Appointment; +import seedu.address.model.appointment.AppointmentDateTime; +import seedu.address.model.person.Nric; + +/** + * Jackson-friendly version of {@link Appointment}. + */ +class JsonAdaptedAppointment { + + public static final String MISSING_FIELD_MESSAGE_FORMAT = "Appointment's %s field is missing!"; + + private final String doctorNric; + private final String patientNric; + private final String appointmentDateTime; + + /** + * Constructs a {@code JsonAdaptedAppointment} with the given appointment details. + */ + @JsonCreator + public JsonAdaptedAppointment(@JsonProperty("doctorNric") String doctorNric, + @JsonProperty("patientNric") String patientNric, + @JsonProperty("appointmentDateTime") String appointmentDateTime) { + this.doctorNric = doctorNric; + this.patientNric = patientNric; + this.appointmentDateTime = appointmentDateTime; + } + + /** + * Converts a given {@code Appointment} into this class for Jackson use. + */ + public JsonAdaptedAppointment(Appointment source) { + doctorNric = source.getDoctorNric().toString(); + patientNric = source.getPatientNric().toString(); + appointmentDateTime = source.getAppointmentDateTime().toString(); + } + + /** + * Converts this Jackson-friendly adapted appointment object into the model's {@code Appointment} object. + * + * @return The converted {@code Appointment} object. + * @throws IllegalValueException if there were any data constraints violated in the adapted appointment. + * */ + public Appointment toModelType() throws IllegalValueException { + if (doctorNric == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Nric.class.getSimpleName())); + } + if (!Nric.isValidNric(doctorNric)) { + throw new IllegalValueException(Nric.MESSAGE_CONSTRAINTS); + } + final Nric modelDoctorNric = new Nric(doctorNric); + + if (patientNric == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Nric.class.getSimpleName())); + } + if (!Nric.isValidNric(patientNric)) { + throw new IllegalValueException(Nric.MESSAGE_CONSTRAINTS); + } + final Nric modelPatientNric = new Nric(patientNric); + + if (appointmentDateTime == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + AppointmentDateTime.class.getSimpleName())); + } + + if (!AppointmentDateTime.isValidDate(appointmentDateTime)) { + throw new IllegalValueException(AppointmentDateTime.MESSAGE_CONSTRAINTS); + } + + final AppointmentDateTime modelAppointmentDateTime = new AppointmentDateTime(appointmentDateTime); + + return new Appointment(modelDoctorNric, modelPatientNric, + modelAppointmentDateTime, true); + } + + + /** + * Indicates whether some other object is "equal to" JsonAdaptedAppointment. + * + * @param obj the reference object with which to compare. + * @return {@code true} if this object is the same as the obj argument; {@code false} otherwise. + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (!(obj instanceof JsonAdaptedAppointment)) { + return false; + } + + JsonAdaptedAppointment jsonAdaptedAppt = (JsonAdaptedAppointment) obj; + + + return jsonAdaptedAppt != null + && jsonAdaptedAppt.doctorNric.equals(this.doctorNric) + && jsonAdaptedAppt.patientNric.equals(this.patientNric) + && jsonAdaptedAppt.appointmentDateTime.equals(this.appointmentDateTime); + + } + +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java index bd1ca0f56c8..675058e93d4 100644 --- a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java +++ b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java @@ -1,21 +1,16 @@ package seedu.address.storage; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; +import seedu.address.model.person.DoB; +import seedu.address.model.person.Doctor; import seedu.address.model.person.Name; +import seedu.address.model.person.Nric; +import seedu.address.model.person.Patient; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; /** * Jackson-friendly version of {@link Person}. @@ -24,39 +19,35 @@ class JsonAdaptedPerson { public static final String MISSING_FIELD_MESSAGE_FORMAT = "Person's %s field is missing!"; + private final String type; + private final String nric; private final String name; + private final String dob; private final String phone; - private final String email; - private final String address; - private final List tags = 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) { + public JsonAdaptedPerson(@JsonProperty("type") String type, @JsonProperty("nric") String nric, + @JsonProperty("name") String name, @JsonProperty("dob") String dob, + @JsonProperty("phone") String phone) { + this.type = type; + this.nric = nric; this.name = name; + this.dob = dob; this.phone = phone; - this.email = email; - this.address = address; - if (tags != null) { - this.tags.addAll(tags); - } } /** * Converts a given {@code Person} into this class for Jackson use. */ public JsonAdaptedPerson(Person source) { + type = source.getType().toString(); + nric = source.getNric().nric; name = source.getName().fullName; + dob = source.getDoB().dateOfBirth.toString(); phone = source.getPhone().value; - email = source.getEmail().value; - address = source.getAddress().value; - tags.addAll(source.getTags().stream() - .map(JsonAdaptedTag::new) - .collect(Collectors.toList())); } /** @@ -65,10 +56,13 @@ public JsonAdaptedPerson(Person source) { * @throws IllegalValueException if there were any data constraints violated in the adapted person. */ public Person toModelType() throws IllegalValueException { - final List personTags = new ArrayList<>(); - for (JsonAdaptedTag tag : tags) { - personTags.add(tag.toModelType()); + if (nric == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Nric.class.getSimpleName())); + } + if (!Nric.isValidNric(nric)) { + throw new IllegalValueException(Nric.MESSAGE_CONSTRAINTS); } + final Nric modelNric = new Nric(nric); if (name == null) { throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName())); @@ -78,6 +72,14 @@ public Person toModelType() throws IllegalValueException { } final Name modelName = new Name(name); + if (dob == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, DoB.class.getSimpleName())); + } + if (!DoB.isValidDoB(dob)) { + throw new IllegalValueException(DoB.MESSAGE_CONSTRAINTS); + } + final DoB modelDoB = new DoB(dob); + if (phone == null) { throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Phone.class.getSimpleName())); } @@ -86,24 +88,16 @@ public Person toModelType() throws IllegalValueException { } final Phone modelPhone = new Phone(phone); - if (email == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Email.class.getSimpleName())); - } - if (!Email.isValidEmail(email)) { - throw new IllegalValueException(Email.MESSAGE_CONSTRAINTS); - } - final Email modelEmail = new Email(email); - - if (address == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Address.class.getSimpleName())); - } - if (!Address.isValidAddress(address)) { - throw new IllegalValueException(Address.MESSAGE_CONSTRAINTS); + switch (type) { + case "PATIENT": + return new Patient(modelNric, modelName, modelDoB, modelPhone); + case "DOCTOR": + return new Doctor(modelNric, modelName, modelDoB, modelPhone); + default: + break; } - final Address modelAddress = new Address(address); - final Set modelTags = new HashSet<>(personTags); - return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags); + return null; } } diff --git a/src/main/java/seedu/address/storage/JsonAdaptedTag.java b/src/main/java/seedu/address/storage/JsonAdaptedTag.java deleted file mode 100644 index 0df22bdb754..00000000000 --- a/src/main/java/seedu/address/storage/JsonAdaptedTag.java +++ /dev/null @@ -1,48 +0,0 @@ -package seedu.address.storage; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonValue; - -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.tag.Tag; - -/** - * Jackson-friendly version of {@link Tag}. - */ -class JsonAdaptedTag { - - private final String tagName; - - /** - * Constructs a {@code JsonAdaptedTag} with the given {@code tagName}. - */ - @JsonCreator - public JsonAdaptedTag(String tagName) { - this.tagName = tagName; - } - - /** - * Converts a given {@code Tag} into this class for Jackson use. - */ - public JsonAdaptedTag(Tag source) { - tagName = source.tagName; - } - - @JsonValue - public String getTagName() { - return tagName; - } - - /** - * 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 Tag toModelType() throws IllegalValueException { - if (!Tag.isValidTagName(tagName)) { - throw new IllegalValueException(Tag.MESSAGE_CONSTRAINTS); - } - return new Tag(tagName); - } - -} diff --git a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java index 5efd834091d..6691b3c26d8 100644 --- a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java +++ b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java @@ -11,6 +11,7 @@ import seedu.address.commons.exceptions.IllegalValueException; import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.appointment.Appointment; import seedu.address.model.person.Person; /** @@ -20,15 +21,23 @@ class JsonSerializableAddressBook { public static final String MESSAGE_DUPLICATE_PERSON = "Persons list contains duplicate person(s)."; + public static final String MESSAGE_DUPLICATE_APPOINTMENTS = "Appointment list contains duplicate appointment(s)."; private final List persons = new ArrayList<>(); + private final List appointments = new ArrayList<>(); /** - * Constructs a {@code JsonSerializableAddressBook} with the given persons. + * Constructs a {@code JsonSerializableAddressBook} with the given persons and appointments. */ @JsonCreator - public JsonSerializableAddressBook(@JsonProperty("persons") List persons) { + public JsonSerializableAddressBook(@JsonProperty("persons") List persons, + @JsonProperty("appointments") List appointments) { + + assert persons != null : "persons should not be null"; this.persons.addAll(persons); + + assert appointments != null : "appointments should not be null"; + this.appointments.addAll(appointments); } /** @@ -38,6 +47,8 @@ public JsonSerializableAddressBook(@JsonProperty("persons") List { + + private static final String FXML = "AppointmentListCard.fxml"; + + /** + * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. + * As a consequence, UI elements' variable names cannot be set to such keywords + * or an exception will be thrown by JavaFX during runtime. + * + * @see The issue on AddressBook level 4 + */ + private final Appointment appointment; + + @FXML + private HBox cardPane; + @FXML + private Label id; + @FXML + private Label doctorNric; + @FXML + private Label patientNric; + @FXML + private Label appointmentDateTime; + @FXML + private Label appointmentId; + + /** + * Creates a {@code AppointmentCard} with the given {@code Appointment} and index to display. + */ + public AppointmentCard(Appointment appt, int displayedIndex) { + super(FXML); + this.appointment = appt; + id.setText(displayedIndex + ". "); + doctorNric.setText(appointment.getDoctorNric().nric); + patientNric.setText(appointment.getPatientNric().nric); + appointmentDateTime.setText(appointment.getAppointmentDateTime().toString()); + //appointmentId.setText(appointment.getAppointmentId().appointmentId); + } +} diff --git a/src/main/java/seedu/address/ui/AppointmentListPanel.java b/src/main/java/seedu/address/ui/AppointmentListPanel.java new file mode 100644 index 00000000000..84a3f4903fe --- /dev/null +++ b/src/main/java/seedu/address/ui/AppointmentListPanel.java @@ -0,0 +1,49 @@ +package seedu.address.ui; + +import java.util.logging.Logger; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.Region; +import seedu.address.commons.core.LogsCenter; +import seedu.address.model.appointment.Appointment; + +/** + * Panel containing the list of Appointments. + */ +public class AppointmentListPanel extends UiPart { + private static final String FXML = "AppointmentListPanel.fxml"; + private final Logger logger = LogsCenter.getLogger(AppointmentListPanel.class); + + @FXML + private ListView appointmentListView; + + /** + * Creates a {@code AppointmentListPanel} with the given {@code ObservableList}. + */ + public AppointmentListPanel(ObservableList appointmentList) { + super(FXML); + appointmentListView.setItems(appointmentList); + appointmentListView.setCellFactory(listView -> new AppointmentListViewCell()); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Appointment} using a {@code AppointmentCard}. + */ + class AppointmentListViewCell extends ListCell { + @Override + protected void updateItem(Appointment appt, boolean empty) { + super.updateItem(appt, empty); + + if (empty || appt == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new AppointmentCard(appt, getIndex() + 1).getRoot()); + } + } + } + +} diff --git a/src/main/java/seedu/address/ui/HelpWindow.java b/src/main/java/seedu/address/ui/HelpWindow.java index 3f16b2fcf26..e15481b9c28 100644 --- a/src/main/java/seedu/address/ui/HelpWindow.java +++ b/src/main/java/seedu/address/ui/HelpWindow.java @@ -15,7 +15,7 @@ */ public class HelpWindow extends UiPart { - public static final String USERGUIDE_URL = "https://se-education.org/addressbook-level3/UserGuide.html"; + public static final String USERGUIDE_URL = "https://ay2324s2-cs2103t-t15-1.github.io/tp/UserGuide.html"; public static final String HELP_MESSAGE = "Refer to the user guide: " + USERGUIDE_URL; private static final Logger logger = LogsCenter.getLogger(HelpWindow.class); diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java index 79e74ef37c0..7b830e2c943 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/address/ui/MainWindow.java @@ -32,6 +32,8 @@ public class MainWindow extends UiPart { // Independent Ui parts residing in this Ui container private PersonListPanel personListPanel; + + private AppointmentListPanel appointmentListPanel; private ResultDisplay resultDisplay; private HelpWindow helpWindow; @@ -44,6 +46,9 @@ public class MainWindow extends UiPart { @FXML private StackPane personListPanelPlaceholder; + @FXML + private StackPane appointmentListPanelPlaceholder; + @FXML private StackPane resultDisplayPlaceholder; @@ -113,6 +118,9 @@ void fillInnerParts() { personListPanel = new PersonListPanel(logic.getFilteredPersonList()); personListPanelPlaceholder.getChildren().add(personListPanel.getRoot()); + appointmentListPanel = new AppointmentListPanel(logic.getFilteredAppointmentList()); + appointmentListPanelPlaceholder.getChildren().add(appointmentListPanel.getRoot()); + resultDisplay = new ResultDisplay(); resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot()); diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java index 094c42cda82..7391ea5fa4b 100644 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ b/src/main/java/seedu/address/ui/PersonCard.java @@ -1,16 +1,13 @@ package seedu.address.ui; -import java.util.Comparator; - import javafx.fxml.FXML; import javafx.scene.control.Label; -import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Region; import seedu.address.model.person.Person; /** - * An UI component that displays information of a {@code Person}. + * A UI component that displays information of a {@code Person}. */ public class PersonCard extends UiPart { @@ -29,17 +26,17 @@ public class PersonCard extends UiPart { @FXML private HBox cardPane; @FXML - private Label name; - @FXML private Label id; @FXML - private Label phone; + private Label type; @FXML - private Label address; + private Label nric; @FXML - private Label email; + private Label name; + @FXML + private Label dob; @FXML - private FlowPane tags; + private Label phone; /** * Creates a {@code PersonCode} with the given {@code Person} and index to display. @@ -48,12 +45,10 @@ public PersonCard(Person person, int displayedIndex) { super(FXML); this.person = person; id.setText(displayedIndex + ". "); + type.setText(person.getType().toString()); + nric.setText(person.getNric().nric); name.setText(person.getName().fullName); + dob.setText(person.getDoB().toString()); phone.setText(person.getPhone().value); - address.setText(person.getAddress().value); - email.setText(person.getEmail().value); - person.getTags().stream() - .sorted(Comparator.comparing(tag -> tag.tagName)) - .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); } } diff --git a/src/main/java/seedu/address/ui/UiManager.java b/src/main/java/seedu/address/ui/UiManager.java index fdf024138bc..4821d57507b 100644 --- a/src/main/java/seedu/address/ui/UiManager.java +++ b/src/main/java/seedu/address/ui/UiManager.java @@ -20,7 +20,7 @@ public class UiManager implements Ui { public static final String ALERT_DIALOG_PANE_FIELD_ID = "alertDialogPane"; private static final Logger logger = LogsCenter.getLogger(UiManager.class); - private static final String ICON_APPLICATION = "/images/address_book_32.png"; + private static final String ICON_APPLICATION = "/images/medicli_logo.png"; private Logic logic; private MainWindow mainWindow; diff --git a/src/main/resources/images/doctorIcon.png b/src/main/resources/images/doctorIcon.png new file mode 100644 index 00000000000..fbdf4b47107 Binary files /dev/null and b/src/main/resources/images/doctorIcon.png differ diff --git a/src/main/resources/images/medicli_logo.png b/src/main/resources/images/medicli_logo.png new file mode 100644 index 00000000000..d07a31ff4a6 Binary files /dev/null and b/src/main/resources/images/medicli_logo.png differ diff --git a/src/main/resources/images/patientIcon.png b/src/main/resources/images/patientIcon.png new file mode 100644 index 00000000000..f34f431753c Binary files /dev/null and b/src/main/resources/images/patientIcon.png differ diff --git a/src/main/resources/view/AppointmentListCard.fxml b/src/main/resources/view/AppointmentListCard.fxml new file mode 100644 index 00000000000..791c62a9eb5 --- /dev/null +++ b/src/main/resources/view/AppointmentListCard.fxml @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/AppointmentListPanel.fxml b/src/main/resources/view/AppointmentListPanel.fxml new file mode 100644 index 00000000000..d4d0f872a64 --- /dev/null +++ b/src/main/resources/view/AppointmentListPanel.fxml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index 36e6b001cd8..0f9e5ace0a2 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -333,8 +333,8 @@ } #resultDisplay .content { - -fx-background-color: transparent, #383838, transparent, #383838; - -fx-background-radius: 0; + /*-fx-background-color: transparent, #383838, transparent, #383838;*/ + /*-fx-background-radius: 0;*/ } #tags { @@ -344,7 +344,7 @@ #tags .label { -fx-text-fill: white; - -fx-background-color: #3e7b91; + /*-fx-background-color: #3e7b91;*/ -fx-padding: 1 3 1 3; -fx-border-radius: 2; -fx-background-radius: 2; diff --git a/src/main/resources/view/Extensions.css b/src/main/resources/view/Extensions.css index bfe82a85964..abb687197fa 100644 --- a/src/main/resources/view/Extensions.css +++ b/src/main/resources/view/Extensions.css @@ -5,7 +5,7 @@ .list-cell:empty { /* Empty cells will not have alternating colours */ - -fx-background: #383838; + -fx-background: white; } .tag-selector { diff --git a/src/main/resources/view/HelpWindow.fxml b/src/main/resources/view/HelpWindow.fxml index e01f330de33..1fc9719dabb 100644 --- a/src/main/resources/view/HelpWindow.fxml +++ b/src/main/resources/view/HelpWindow.fxml @@ -9,7 +9,8 @@ - + diff --git a/src/main/resources/view/LightTheme.css b/src/main/resources/view/LightTheme.css new file mode 100644 index 00000000000..7dcaef2375b --- /dev/null +++ b/src/main/resources/view/LightTheme.css @@ -0,0 +1,422 @@ +.background { + -fx-background-color: #e2e5ef; + background-color: #e2e5ef; /* Used in the default.html file */ +} + +.label { + -fx-font-size: 11pt; + -fx-font-family: "Segoe UI Semibold"; + -fx-text-fill: #052a3a; + -fx-opacity: 0.9; +} + +.label-bright { + -fx-font-size: 11pt; + -fx-font-family: "Segoe UI Semibold"; + -fx-text-fill: #052a3a; + -fx-opacity: 1; +} + +.label-header { + -fx-font-size: 32pt; + -fx-font-family: "Segoe UI Light"; + -fx-text-fill: #052a3a; + -fx-opacity: 1; +} + +.text-field { + -fx-font-size: 12pt; + -fx-font-family: "Segoe UI Semibold"; +} + +.tab-pane { + -fx-padding: 0 0 0 1; +} + +.tab-pane .tab-header-area { + -fx-padding: 0 0 0 0; + -fx-min-height: 0; + -fx-max-height: 0; +} + +.table-view { + -fx-base: white; + -fx-control-inner-background: white; + -fx-background-color: white; + -fx-table-cell-border-color: transparent; + -fx-table-header-border-color: transparent; + -fx-padding: 5; +} + +.table-view .column-header-background { + -fx-background-color: transparent; +} + +.table-view .column-header, .table-view .filler { + -fx-size: 35; + -fx-border-width: 0 0 1 0; + -fx-background-color: transparent; + -fx-border-color: transparent; + -fx-border-insets: 0 10 1 0; +} + +.table-view .column-header .label { + -fx-font-size: 20pt; + -fx-font-family: "Segoe UI Light"; + -fx-text-fill: #052a3a; + -fx-alignment: center-left; + -fx-opacity: 1; +} + +.table-view:focused .table-row-cell:filled:focused:selected { + -fx-background-color: -fx-focus-color; +} + +.split-pane:horizontal .split-pane-divider { + -fx-background-color: white; + -fx-border-color: transparent; +} + +.split-pane { + -fx-border-radius: 1; + -fx-border-width: 1; + -fx-background-color: white; +} + +.list-view { + -fx-background-insets: 0; + -fx-padding: 0; + -fx-background-color: white; +} + +.list-cell { + -fx-label-padding: 0 0 0 0; + -fx-graphic-text-gap : 0; + -fx-padding: 0 0 0 0; + -fx-background-radius: 15px; + -fx-border-radius: 15px; + -fx-background-insets: 0px, 2px; +} + +.list-cell:filled:even { + -fx-background-color: white; +} + +.list-cell:filled:odd { + -fx-background-color: white; +} + +.list-cell:filled:selected { +} + +.list-cell:filled:selected #cardPane { + /*-fx-border-color: #052a3a;*/ + -fx-border-width: 1; +} + +.list-cell .label { + -fx-text-fill: #052a3a; +} + +.cell_big_label { + -fx-font-family: "Segoe UI SemiBold"; + -fx-font-size: 18px; + -fx-text-fill: #052a3a; + -fx-font-weight: bold; +} + +.cell_small_label { + -fx-font-family: "Segoe UI"; + -fx-font-size: 13px; + -fx-text-fill: #414141 !important; +} + +.cell_type_label { + -fx-font-family: "Segoe UI"; + -fx-font-size: 13px; + -fx-text-fill: #052a3a; +} + +.stack-pane { + -fx-background-color: white; +} + +.person-list-pane { + -fx-background-color: white; +} + +.pane-with-border { + -fx-background-color: white; + /*-fx-border-color: #052a3a;*/ + -fx-border-top-width: 1px; +} + +#commandResult { + -fx-border-radius: 10px; + -fx-background-radius: 10px; + /*-fx-background-color: #fdfdfd;*/ + + /*-fx-background-radius: 15px;*/ + /*-fx-border-radius: 15px;*/ +} + +#resultDisplay { + -fx-border-radius: 10px; + -fx-background-radius: 10px; + -fx-font-size: 11pt; +} + +.status-bar { + /*-fx-background-color: #f1f4f4;*/ +} + +.result-display { + -fx-background-color: white; + -fx-font-family: "Segoe UI Light"; + -fx-font-size: 13pt; + -fx-text-fill: #052a3a; +} + +.result-display .label { + -fx-text-fill: #052a3a; +} + +.status-bar .label { + -fx-font-family: "Segoe UI Light"; + -fx-text-fill: #052a3a; + -fx-padding: 4px; + -fx-pref-height: 30px; +} + +.status-bar-with-border { + -fx-background-color: #e2e5ef; + -fx-border-color: #052a3a; + -fx-border-width: 1px; +} + +.status-bar-with-border .label { + -fx-text-fill: #052a3a; +} + +.grid-pane { + -fx-background-color: #e2e5ef; + -fx-border-color: #e2e5ef; + -fx-border-width: 1px; +} + +.grid-pane .stack-pane { + -fx-background-color: white; +} + +.context-menu { + -fx-background-color: #e2e5ef; +} + +.context-menu .label { + -fx-text-fill: #052a3a; +} + +.menu-bar { + -fx-background-color: #f1f4f4; +} + +.menu-bar .label { + -fx-font-size: 14pt; + -fx-font-family: "Segoe UI Light"; + -fx-text-fill: #052a3a; + -fx-opacity: 0.9; +} + +.menu .left-container { + -fx-background-color: #e2e5ef; +} + +/* + * Metro style Push Button + * Author: Pedro Duque Vieira + * http://pixelduke.wordpress.com/2012/10/23/jmetro-windows-8-controls-on-java/ + */ +.button { + -fx-padding: 5 22 5 22; + -fx-border-color: #e2e5ef; + -fx-border-width: 2; + -fx-background-radius: 0; + -fx-background-color: #e2e5ef; + -fx-font-family: "Segoe UI", Helvetica, Arial, sans-serif; + -fx-font-size: 11pt; + -fx-text-fill: #052a3a; + -fx-background-insets: 0 0 0 0, 0, 1, 2; +} + +.button:hover { + -fx-background-color: #e2e5ef; +} + +.button:pressed, .button:default:hover:pressed { + -fx-background-color: white; + -fx-text-fill: #052a3a; +} + +.button:focused { + -fx-border-color: #052a3a, #052a3a; + -fx-border-width: 1, 1; + -fx-border-style: solid, segments(1, 1); + -fx-border-radius: 0, 0; + -fx-border-insets: 1 1 1 1, 0; +} + +.button:disabled, .button:default:disabled { + -fx-opacity: 0.4; + -fx-background-color: #e2e5ef; + -fx-text-fill: #052a3a; +} + +.button:default { + -fx-background-color: -fx-focus-color; + -fx-text-fill: #052a3a; +} + +.button:default:hover { + -fx-background-color: derive(-fx-focus-color, 30%); +} + +.dialog-pane { + -fx-background-color: #e2e5ef; +} + +.dialog-pane > *.button-bar > *.container { + -fx-background-color: #e2e5ef; +} + +.dialog-pane > *.label.content { + -fx-font-size: 14px; + -fx-font-weight: bold; + -fx-text-fill: #052a3a; +} + +.dialog-pane:header *.header-panel { + -fx-background-color: #e2e5ef; +} + +.dialog-pane:header *.header-panel *.label { + -fx-font-size: 18px; + -fx-font-style: italic; + -fx-fill: white; + -fx-text-fill: #052a3a; +} + +.scroll-bar { + -fx-background-color: white; +} + +.scroll-bar .thumb { + -fx-background-color: #f5f5f5; + -fx-background-insets: 3; +} + +.scroll-bar .increment-button, .scroll-bar .decrement-button { + -fx-background-color: transparent; + -fx-padding: 0 0 0 0; +} + +.scroll-bar .increment-arrow, .scroll-bar .decrement-arrow { + -fx-shape: " "; +} + +.scroll-bar:vertical .increment-arrow, .scroll-bar:vertical .decrement-arrow { + -fx-padding: 1 8 1 8; +} + +.scroll-bar:horizontal .increment-arrow, .scroll-bar:horizontal .decrement-arrow { + -fx-padding: 8 1 8 1; +} + +#cardPane { + -fx-background-insets: 0px, 0px, 5px, 0px; + -fx-background-color: transparent; + -fx-border-width: 1px; + -fx-border-radius: 5px; + -fx-border-color: #e2e5ef; +} + +#commandTypeLabel { + -fx-font-size: 11px; + -fx-text-fill: #F70D1A; +} + +#commandTextField { + -fx-fill-height: 20px; + -fx-background-color: #f5f5f5; + -fx-background-insets: 0; + /*-fx-border-color: #052a3a;*/ + /*-fx-border-insets: 0;*/ + /*-fx-border-width: 1;*/ + -fx-font-family: "Segoe UI Light"; + -fx-font-size: 13pt; + -fx-text-fill: #052a3a; + -fx-background-radius: 8px; + -fx-border-radius: 8px; +} + +#filterField, #personListPanel, #personWebpage { + /*-fx-effect: innershadow(gaussian, #e2e5ef, 10, 0, 0, 0);*/ +} + + +#tags { + -fx-hgap: 7; + -fx-vgap: 3; +} + +#tags .label { + -fx-text-fill: #052a3a; + /*-fx-background-color: #3e7b91;*/ + -fx-padding: 1 3 1 3; + -fx-border-radius: 2; + -fx-background-radius: 2; + -fx-font-size: 11; +} + +#personListPanelPlaceholder, #appointmentListPanelPlaceholder { + -fx-background-color: white; +} + +/* Textarea */ + +.text-area { + -fx-background-insets: 0; + -fx-background-color: transparent, white, transparent, white; + -fx-border-color: #e2e5ef; + -fx-border-radius: 10px; + -fx-background-radius: 10px; +} + +.text-area .content { + -fx-background-color: transparent, white, transparent, white; + -fx-border-radius: 10px; + -fx-background-radius: 10px; +} + +.text-area:focused .content { + -fx-background-color: transparent, white, transparent, white; +} + +.text-area:focused { + -fx-highlight-fill: #7ecfff; +} + +.text-area .content { + -fx-border-radius: 10px; + -fx-background-radius: 10px; + -fx-padding: 10px; + -fx-text-fill: gray; + -fx-highlight-fill: #7ecfff; +} + +/* Others */ + +#typeText { + -fx-background-color: #dcdcdc; + -fx-padding: 3px 10px 3px 10px; + -fx-background-radius: 10px; +} diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml index 7778f666a0a..bb78b49d147 100644 --- a/src/main/resources/view/MainWindow.fxml +++ b/src/main/resources/view/MainWindow.fxml @@ -12,14 +12,14 @@ + title="MediCLI" minWidth="450" minHeight="600" onCloseRequest="#handleExit"> - + - + @@ -35,7 +35,7 @@ - + @@ -46,12 +46,24 @@ - - - - - - + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/PersonListCard.fxml b/src/main/resources/view/PersonListCard.fxml index f5e812e25e6..43153ae2ec9 100644 --- a/src/main/resources/view/PersonListCard.fxml +++ b/src/main/resources/view/PersonListCard.fxml @@ -7,30 +7,56 @@ + + - - - - - - - - - - - - - + + + diff --git a/src/main/resources/view/PersonListPanel.fxml b/src/main/resources/view/PersonListPanel.fxml index a1bb6bbace8..b436063213a 100644 --- a/src/main/resources/view/PersonListPanel.fxml +++ b/src/main/resources/view/PersonListPanel.fxml @@ -2,7 +2,14 @@ + + - - + + + + + + + diff --git a/src/main/resources/view/ResultDisplay.fxml b/src/main/resources/view/ResultDisplay.fxml index 01b691792a9..ef27f4c0eaf 100644 --- a/src/main/resources/view/ResultDisplay.fxml +++ b/src/main/resources/view/ResultDisplay.fxml @@ -1,9 +1,6 @@ - - -