diff --git a/.gitignore b/.gitignore index 823d175eb670..c8e31b0cdb8a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ lib/* *.log *.log.* *.csv -config.json +/config.json src/test/data/sandbox/ preferences.json .DS_Store diff --git a/README.adoc b/README.adoc index fa50028b99d0..b8f4bae2bafb 100644 --- a/README.adoc +++ b/README.adoc @@ -1,10 +1,9 @@ -= Address Book (Level 4) += QuickDocs ifdef::env-github,env-browser[:relfileprefix: docs/] -https://travis-ci.org/se-edu/addressbook-level4[image:https://travis-ci.org/se-edu/addressbook-level4.svg?branch=master[Build Status]] -https://ci.appveyor.com/project/damithc/addressbook-level4[image:https://ci.appveyor.com/api/projects/status/3boko2x2vr5cc3w2?svg=true[Build status]] -https://coveralls.io/github/se-edu/addressbook-level4?branch=master[image:https://coveralls.io/repos/github/se-edu/addressbook-level4/badge.svg?branch=master[Coverage Status]] -https://www.codacy.com/app/damith/addressbook-level4?utm_source=github.com&utm_medium=referral&utm_content=se-edu/addressbook-level4&utm_campaign=Badge_Grade[image:https://api.codacy.com/project/badge/Grade/fc0b7775cf7f4fdeaf08776f3d8e364a[Codacy Badge]] +https://travis-ci.org/CS2103-AY1819S2-W09-4/main[image:https://travis-ci.org/CS2103-AY1819S2-W09-4/main.svg?branch=master[Build Status]] +https://coveralls.io/github/CS2103-AY1819S2-W09-4[image:https://coveralls.io/repos/github/CS2103-AY1819S2-W09-4/main/badge.svg?branch=master[Coverage Status]] + ifdef::env-github[] image::docs/images/Ui.png[width="600"] @@ -14,26 +13,37 @@ ifndef::env-github[] image::images/Ui.png[width="600"] endif::[] -* This is a desktop Address Book application. It has a GUI but most of the user interactions happen using a CLI (Command Line Interface). -* It is a Java sample application intended for students learning Software Engineering while using Java as the main programming language. -* It is *written in OOP fashion*. It provides a *reasonably well-written* code example that is *significantly bigger* (around 6 KLoC)than what students usually write in beginner-level SE modules. -* What's different from https://github.com/se-edu/addressbook-level3[level 3]: -** A more sophisticated GUI that includes a list panel and an in-built Browser. -** More test cases, including automated GUI testing. -** Support for _Build Automation_ using Gradle and for _Continuous Integration_ using Travis CI. +== What is QuickDocs + +*QuickDocs* is an all-in-one clinic management application intended for doctors operating family clinics in Singapore. It can +facilitate and expedite clinical operations to help doctors overcome manpower limitations in their clinics, and improve the +quality of healthcare services provided to the patients. + +*QuickDocs* is a desktop application with a graphical user interface (GUI) but most of the user interactions happen using +a CLI (Command Line Interface). Therefore it is intended for doctors who can type fast and prefer a CLI to perform +tasks. + +== What can QuickDocs do? + +*QuickDocs* can helps doctors with: + +* Managing patient data +* Conducting consultation sessions with patients +* Organizing patient appointment dates +* Setting personal reminders +* Maintaining the clinic's medicine inventory +* Generating useful statistics on the clinic's operations == Site Map * <> * <> -* <> * <> * <> == Acknowledgements -* Some parts of this sample application were inspired by the excellent http://code.makery.ch/library/javafx-8-tutorial/[Java FX tutorial] by -_Marco Jakob_. +* QuickDocs is developed using the base code of https://github.com/nus-cs2103-AY1819S2/addressbook-level4/blob/master/README.adoc[AddressBook Level 4] provided by by the https://github.com/se-edu[se-edu team]. * Libraries used: https://github.com/TestFX/TestFX[TextFX], https://github.com/FasterXML/jackson[Jackson], https://github.com/google/guava[Guava], https://github.com/junit-team/junit5[JUnit5] == Licence : link:LICENSE[MIT] diff --git a/_reposense/config.json b/_reposense/config.json new file mode 100644 index 000000000000..97f3b5668991 --- /dev/null +++ b/_reposense/config.json @@ -0,0 +1,25 @@ +{ + "authors": + [ + { + "githubId": "simjiazhi", + "displayName": "SIM...ZHI", + "authorNames": ["simjiazhi"] + }, + { + "githubId": "bentwj", + "displayName": "TAN...IAN", + "authorNames": ["bentwj"] + }, + { + "githubId": "ongaaron96", + "displayName": "ONG...RON", + "authorNames": ["ongaaron96", "Aaron Ong"] + }, + { + "githubId": "Xue-Chenyang", + "displayName": "XUE...ANG", + "authorNames": ["Xue-Chenyang"] + } + ] +} diff --git a/build.gradle b/build.gradle index 4f2949b6e774..65ba21300484 100644 --- a/build.gradle +++ b/build.gradle @@ -28,7 +28,7 @@ if (JavaVersion.current() == JavaVersion.VERSION_1_10 } // Specifies the entry point of the application -mainClassName = 'seedu.address.MainApp' +mainClassName = 'quickdocs.MainApp' sourceCompatibility = JavaVersion.VERSION_1_9 targetCompatibility = JavaVersion.VERSION_1_9 @@ -77,7 +77,7 @@ dependencies { } shadowJar { - archiveName = 'addressbook.jar' + archiveName = 'QuickDocs.jar' destinationDir = file("${buildDir}/jar/") } @@ -152,16 +152,16 @@ test { } if (runNonGuiTests) { - test.include 'seedu/address/**' + test.include 'quickdocs/**' } if (runGuiTests) { test.include 'systemtests/**' - test.include 'seedu/address/ui/**' + test.include 'quickdocs/ui/**' } if (!runGuiTests) { - test.exclude 'seedu/address/ui/**' + test.exclude 'quickdocs/ui/**' } } } @@ -202,7 +202,7 @@ asciidoctor { idprefix: '', // for compatibility with GitHub preview idseparator: '-', 'site-root': "${sourceDir}", // must be the same as sourceDir, do not modify - 'site-name': 'AddressBook-Level4', + 'site-name': 'QuickDocs', 'site-githuburl': 'https://github.com/se-edu/addressbook-level4', 'site-seedu': true, // delete this line if your project is not a fork (not a SE-EDU project) ] diff --git a/docs/AboutUs.adoc b/docs/AboutUs.adoc index e647ed1e715a..928210b70355 100644 --- a/docs/AboutUs.adoc +++ b/docs/AboutUs.adoc @@ -4,53 +4,46 @@ :imagesDir: images :stylesDir: stylesheets -AddressBook - Level 4 was developed by the https://se-edu.github.io/docs/Team.html[se-edu] team. + -_{The dummy content given below serves as a placeholder to be used by future forks of the project.}_ + -{empty} + +Quickdocs was developed by the W09-4 team. + + We are a team based in the http://www.comp.nus.edu.sg[School of Computing, National University of Singapore]. == Project Team -=== John Doe -image::damithc.jpg[width="150", align="left"] -{empty}[http://www.comp.nus.edu.sg/~damithch[homepage]] [https://github.com/damithc[github]] [<>] +=== Sim Jia Zhi +image::simjiazhi.png[width="150", align="left"] +{empty}[https://github.com/simjiazhi[github]] [<>] -Role: Project Advisor +Role: Team Lead and deliverables + +Responsibilities: Patient and consultation management module, UI design ''' -=== John Roe -image::lejolly.jpg[width="150", align="left"] -{empty}[http://github.com/lejolly[github]] [<>] +=== Tan Wenjian +image::bentwj.png[width="150", align="left"] +{empty}[http://github.com/bentwj[github]] [<>] -Role: Team Lead + -Responsibilities: UI +Role: Code Quality and Documentation + +Responsibilities: Administration and statistics module, Logic implementation ''' -=== Johnny Doe -image::yijinl.jpg[width="150", align="left"] -{empty}[http://github.com/yijinl[github]] [<>] +=== Ong You Sheng Aaron +image::ongaaron96.png[width="150", align="left"] +{empty}[http://github.com/ongaaron96[github]] [<>] -Role: Developer + -Responsibilities: Data +Role: Testing and Integration + +Responsibilities: Appointment and reminder management module, Storage Management ''' -=== Johnny Roe -image::m133225.jpg[width="150", align="left"] -{empty}[http://github.com/m133225[github]] [<>] +=== Xue Chenyang +image::xue-chenyang.png[width="150", align="left"] +{empty}[http://github.com/Xue-Chenyang[github]] [<>] -Role: Developer + -Responsibilities: Dev Ops + Threading +Role: Scheduling and Tracking + +Responsibilities: Medical inventory management module, Model Management ''' -=== Benson Meier -image::yl_coder.jpg[width="150", align="left"] -{empty}[http://github.com/yl-coder[github]] [<>] - -Role: Developer + -Responsibilities: UI - ''' diff --git a/docs/ContactUs.adoc b/docs/ContactUs.adoc index 5de5363abffd..8a52f123434b 100644 --- a/docs/ContactUs.adoc +++ b/docs/ContactUs.adoc @@ -2,6 +2,6 @@ :site-section: ContactUs :stylesDir: stylesheets -* *Bug reports, Suggestions* : Post in our https://github.com/se-edu/addressbook-level4/issues[issue tracker] if you noticed bugs or have suggestions on how to improve. +* *Bug reports, Suggestions* : Post in our https://github.com/CS2103-AY1819S2-W09-4/main/issues[issue tracker] if you noticed bugs or have suggestions on how to improve. * *Contributing* : We welcome pull requests. Follow the process described https://github.com/oss-generic/process[here] -* *Email us* : You can also reach us at `damith [at] comp.nus.edu.sg` +* *Email us* : You can also reach us at `cs2103tw094 [at] gmail [DOT] com` diff --git a/docs/DeveloperGuide.adoc b/docs/DeveloperGuide.adoc index 8b92d5fb7e62..e16e8706b2c0 100644 --- a/docs/DeveloperGuide.adoc +++ b/docs/DeveloperGuide.adoc @@ -1,4 +1,4 @@ -= AddressBook Level 4 - Developer Guide += QuickDocs - Developer Guide :site-section: DeveloperGuide :toc: :toc-title: @@ -13,9 +13,38 @@ ifdef::env-github[] :warning-caption: :warning: :experimental: endif::[] -:repoURL: https://github.com/se-edu/addressbook-level4/tree/master +:repoURL: https://github.com/CS2103-AY1819S2-W09-4/main/tree/master -By: `Team SE-EDU`      Since: `Jun 2016`      Licence: `MIT` +By: `W09-4`      Since: `Jan 2019`      Licence: `MIT` + +== Introduction + +=== What is QuickDocs + +QuickDocs is an all in one solution where doctors can have greater control in facilitating patient consultations, organizing appointments, +and monitoring financial and inventory records in private clinics. + +=== Purpose + +The developer guide covers the software architecture, design decisions and implementation details of the features in QuickDocs. + +=== Audience + +This document aims to provide guidance for both software developers and software testers working on QuickDocs. + +=== How to use the Developer Guide + +Here are the notations used in the document: + +[TIP] +This is a tip. Useful information pertaining to the features will be written here. +[NOTE] +This is a note. Additional information that further explains a feature will be written here. + +[WARNING] +This is a warning. Special information that requires additional attention to be paid will be written here to prevent operational issues from happening. + +`filepath`, `command` and `method` are all formatted with a grey background. Example: `help`, `getCurrentConsultation()` +and `logic\LogicManager` == Setting up @@ -47,14 +76,10 @@ Do not disable them. If you have disabled them, go to `File` > `Settings` > `Plu . Click `OK` to accept the default settings . Open a console and run the command `gradlew processResources` (Mac/Linux: `./gradlew processResources`). It should finish with the `BUILD SUCCESSFUL` message. + This will generate all resources required by the application and tests. -. Open link:{repoURL}/src/main/java/seedu/address/ui/MainWindow.java[`MainWindow.java`] and check for any code errors -.. Due to an ongoing https://youtrack.jetbrains.com/issue/IDEA-189060[issue] with some of the newer versions of IntelliJ, code errors may be detected even if the project can be built and run successfully -.. To resolve this, place your cursor over any of the code section highlighted in red. Press kbd:[ALT + ENTER], and select `Add '--add-modules=...' to module compiler options` for each error -. Repeat this for the test folder as well (e.g. check link:{repoURL}/src/test/java/seedu/address/ui/HelpWindowTest.java[`HelpWindowTest.java`] for code errors, and if so, resolve it the same way) === Verifying the setup -. Run the `seedu.address.MainApp` and try a few commands +. Run the `w09.quickdocs.MainApp` and try a few commands . <> to ensure they all pass. === Configurations to do before writing code @@ -109,14 +134,14 @@ When you are ready to start coding, === Architecture .Architecture Diagram -image::Architecture.png[width="600"] +image::Architecture2.png[width="600"] The *_Architecture Diagram_* given above explains the high-level design of the App. Given below is a quick overview of each component. [TIP] The `.pptx` files used to create diagrams in this document can be found in the link:{repoURL}/docs/diagrams/[diagrams] folder. To update a diagram, modify the diagram in the pptx file, select the objects of the diagram, and choose `Save as picture`. -`Main` has only one class called link:{repoURL}/src/main/java/seedu/address/MainApp.java[`MainApp`]. It is responsible for, +`Main` has only one class called link:{repoURL}/src/main/java/quickdocs//MainApp.java[`MainApp`]. It is responsible for, * At app launch: Initializes the components in the correct sequence, and connects them up with each other. * At shut down: Shuts down the components and invokes cleanup method where necessary. @@ -146,10 +171,10 @@ image::LogicClassDiagram.png[width="800"] [discrete] ==== 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 `padd ...` (values are not added for brevity). -.Component interactions for `delete 1` command -image::SDforDeletePerson.png[width="800"] +.Component interactions for `addpat ...` command +image::SDforAddPatient.png[width="800"] The sections below give more details of each component. @@ -157,18 +182,37 @@ The sections below give more details of each component. === UI component .Structure of the UI Component -image::UiClassDiagram.png[width="800"] +image::QDUiClassDiagram.png[width="800"] + +*API* : link:{repoURL}/src/main/java/quickdocs/ui/RootLayoutController.java[`RootLayoutController.java`] + +The UI aspect of QuickDocs is controlled by a single `rootLayoutController` that is responsible for handling the user interactions with +the interface. It is composed of the `resultDisplay`, `userInputField`, `inputFeedbackArea`, `reminderListPanel` and +the `currentSessionLabel` controls. + -*API* : link:{repoURL}/src/main/java/seedu/address/ui/Ui.java[`Ui.java`] +The `UI` component uses JavaFx UI framework. RootLayout is defined in the matching `.fxml` file that is in link:{repoURL}/src/main/resources/view[src/main/resources/view] folder. -The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `PersonListPanel`, `StatusBarFooter`, `BrowserPanel` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class. +.Overview of QuickDoc's user interface +image::uioverview.png[width="800"] -The `UI` component uses 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 link:{repoURL}/src/main/java/seedu/address/ui/MainWindow.java[`MainWindow`] is specified in link:{repoURL}/src/main/resources/view/MainWindow.fxml[`MainWindow.fxml`] +1. `resultDisplay` will reflect the results of the command entered +2. `userInputField` is where the user can enter their commands +3. Should the command fail due to erroneous command input, instructions to rectify the command will be displayed on the `inputFeedbackArea` +4. Appointments and Reminders are displayed on the `ReminderListPanel`. Appointments are coloured blue, +medicine alarms are coloured red and other reminders are coloured beige. +5. Current consultation sessions will be indicated on the `currentSessionLabel`. -The `UI` component, +Since the commands entered by the user is done through the user interface, the `UI` component interacts with the +`Logic` specifically for the execution of commands. For details pertaining to the execution of commands, please refer to +the section on the <, logic component>> + +The flow of how the interaction between the two components are as follows: + +1. The user types the command line on the `userInputField` and presses `Enter` +2. The command line is sent to the `logic` component for execution +3. Any results or issues encountered during the process of execution will be returned from the `Logic` component to the `UI` component +4. `UI` will reflect the results or issues faced on the `resultDisplay` and the `inputFeedbackArea` respectively -* Executes user commands using the `Logic` component. -* Listens for changes to `Model` data so that the UI can be updated with the modified data. [[Design-Logic]] === Logic component @@ -178,780 +222,1915 @@ The `UI` component, image::LogicClassDiagram.png[width="800"] *API* : -link:{repoURL}/src/main/java/seedu/address/logic/Logic.java[`Logic.java`] +link:{repoURL}/src/main/java/quickdocs/logic/Logic.java[`Logic.java`] -. `Logic` uses the `AddressBookParser` class to parse the user command. +. `Logic` uses the `QuickDocsParser` class to parse the user command. . This results in a `Command` object which is executed by the `LogicManager`. -. The command execution can affect the `Model` (e.g. adding a person). +. The command execution can affect the `Model` (e.g. adding a patient). . The result of the command execution is encapsulated as a `CommandResult` object which is passed back to the `Ui`. . In addition, the `CommandResult` object can also instruct the `Ui` to perform certain actions, such as displaying help to the user. -Given below is the Sequence Diagram for interactions within the `Logic` component for the `execute("delete 1")` API call. +Given below is the Sequence Diagram for interactions within the `Logic` component for the `execute("statistics 012019")` API call. -.Interactions Inside the Logic Component for the `delete 1` Command -image::DeletePersonSdForLogic.png[width="800"] +.Interactions Inside the Logic Component for the `statistics 012019` Command +image::StatisticsCommandSDForLogic.png[width="800"] [[Design-Model]] === Model component +The figure below describes the architecture of the model component of this application. + +[[Model_diagram]] .Structure of the Model Component -image::ModelClassDiagram.png[width="800"] +image::Model_diagram.png[width="800"] -*API* : link:{repoURL}/src/main/java/seedu/address/model/Model.java[`Model.java`] +*API* : link:{repoURL}/src/main/java/quickdocs/model/Model.java[`Model.java`] The `Model`, * stores a `UserPref` object that represents the user's preferences. -* stores the Address Book data. -* exposes 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. +* consists of a QuickDocs object which contains all the data, and lists of managers of sub-modules retrieved from the QuickDocs object. * does not depend on any of the other three components. -[NOTE] -As a more OOP model, we can store a `Tag` list in `Address Book`, which `Person` can reference. This would allow `Address Book` to only require one `Tag` object per unique `Tag`, instead of each `Person` needing their own `Tag` object. An example of how such a model may look like is given below. + - + -image:ModelClassBetterOopDiagram.png[width="800"] +The `QuickDocs`, -[[Design-Storage]] -=== Storage component +* is the ultimate unit storing all sub-models and data for this application. +* is the class in charge of interacting with the storage component responsible for converting application data to files for storage. +* see <> for detailed explanation of the role of `QuickDocs` in Storage component. -.Structure of the Storage Component -image::StorageClassDiagram.png[width="800"] +The following sections illustrate the design of managers of each sub-module. -*API* : link:{repoURL}/src/main/java/seedu/address/storage/Storage.java[`Storage.java`] +[[Design-Model-MedicineManager]] +==== Model for Medicine module -The `Storage` component, +QuickDocs supports customized organization of medicine inventory. + -* can save `UserPref` objects in json format and read it back. -* can save the Address Book data in json format and read it back. +The figure below illustrates the implementation of the inventory system for medicine. -[[Design-Commons]] -=== Common classes +[[MedicineManager_diagram]] +.Structure of the MedicineManager +image::MedicineManager_diagram.png[width="800"] -Classes used by multiple components are in the `seedu.addressbook.commons` package. +In medicine module, information about a medicine is encapsulated into the `Medicine` class. + -== Implementation +`Directory` is a container for medicines, and sub-directories as well. + -This section describes some noteworthy details on how certain features are implemented. +The `MedicineManager` keeps a list of reference of all unique medicines in the storage, so that no two medicine in the storage could share the same name to avoid confusion. + -// tag::undoredo[] -=== Undo/Redo feature -==== Current Implementation +[NOTE] +All occurrences of medicines with the same name across different directories point to the same medicine in the list of unique medicine in MedicineManager. -The undo/redo mechanism is facilitated by `VersionedAddressBook`. -It extends `AddressBook` with an undo/redo history, stored internally as an `addressBookStateList` and `currentStatePointer`. -Additionally, it implements the following operations: +As the directory-medicine structure resembles the tree data structure, it is possible to support tree-like operations, such as setting the same threshold for the "subtree" of a directory. -* `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. +[[Design-Model-PatientManager]] +==== Model for Patient Management -These operations are exposed in the `Model` interface as `Model#commitAddressBook()`, `Model#undoAddressBook()` and `Model#redoAddressBook()` respectively. +The figure below illustrates how is a patient represented and how are patients are stored in QuickDocs. -Given below is an example usage scenario and how the undo/redo mechanism behaves at each step. +[[PatientManager_diagram]] +.Structure of PatientManager +image::PatientManager_diagram.png[width='800'] -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. +A patient in QuickDocs consists of an address, name, NRIC, Contact, Email, Date of Birth, Gender and any number of tags. + -image::UndoRedoStartingStateListDiagram.png[width="800"] +[NOTE] +Specially, no two patients in QuickDocs can share the same NRIC number. In other words, the patients in QuickDocs are easily identified with their unique NRIC numbers. -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. +The `PatientManager` keeps a list of patients by chronological order of addition. PatientManager supports searching patients by NRIC, name and tags. -image::UndoRedoNewCommand1StateListDiagram.png[width="800"] +[[Design-Model-ConsultationManager]] +==== Model for ConsultationManager -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`. +The figure below illustrates how consultations with patients are recorded and organized in QuickDocs. -image::UndoRedoNewCommand2StateListDiagram.png[width="800"] +[[ConsultationManager_diagram]] +.Structure of ConsultationManager +image::ConsultationManager_diagram.png[width='800'] -[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`. +A `Consultation` in QuickDocs is defined to one patient and it consists of an optional `Diagnosis` and a list of `Prescription` of medicine. + -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. +A diagnosis is then consisting of an assessment, the final conclusion of patient's illness, and a list of symptoms. + -image::UndoRedoExecuteUndoStateListDiagram.png[width="800"] +Past consultations are kept as a list in `ConsultationManager`, and the manager supports listing consultations of the same patient by his/her NRIC. -[NOTE] -If the `currentStatePointer` is at index 0, pointing to the initial address book state, then there are no previous address book 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 than attempting to perform the undo. +[[Design-Model-RecordManager]] +==== Model for StatisticsManager -The following sequence diagram shows how the undo operation works: +Every monetary transaction happened in the clinic, such as prescriptions to patients, is recorded by QuickDocs, and statistics report could be generated upon user requests. + -image::UndoRedoSequenceDiagram.png[width="800"] +The figure below illustrates how such records are organized in QuickDocs, and how the statistics reports are generated. + -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. +[[StatisticsManager_diagram]] +.Structure of StatisticsManager +image::StatisticsManager_diagram.png[width='800'] -[NOTE] -If the `currentStatePointer` is at index `addressBookStateList.size() - 1`, pointing to the latest address book state, then there are no undone address book 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. +Monetary transactions in the clinics are categorized to two forms, i.e purchasing of medicine and revenue from consulting patients. + -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. +Both forms have corresponding classes to record such transactions. Every successful execution of purchase medicine command and every successful consultation will create its corresponding record. + -image::UndoRedoNewCommand3StateListDiagram.png[width="800"] +`MonthStatistics` holds records of purchases of medicines and consultations happened in a particular month. + -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. We designed it this way because it no longer makes sense to redo the `add n/David ...` command. This is the behavior that most modern desktop applications follow. +The overall `StatisticsManager` has a list of MonthStatistics arranged in chronological order. -image::UndoRedoNewCommand4StateListDiagram.png[width="800"] +[[Design-Model-AppointmentManager]] +==== Model for AppointmentManager -The following activity diagram summarizes what happens when a user executes a new command: +The Appointment module manages time slots for appointment requests from the patients. + -image::UndoRedoActivityDiagram.png[width="650"] +The figure below illustrates how AppointmentManager is organized. + -==== Design Considerations +[[AppointmentManager_diagram]] +.Structure of AppointmentManager +image::AppointmentManager_diagram.png[width='800] -===== Aspect: How undo & redo executes +A `Slot` is used to represent a time block during clinic's opening hour available for appointments. + -* **Alternative 1 (current choice):** Saves the entire address book. -** Pros: Easy to implement. -** Cons: May have performance issues in terms of memory usage. -* **Alternative 2:** Individual command knows how to undo/redo by 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. +`Appointment` extends slot and each appointment is assigned exactly one patient. + -===== Aspect: Data structure to support the undo/redo commands +`AppointmentManager` holds a list of appointments which can then be operated on upon user commands. -* **Alternative 1 (current choice):** Use a list to store the history of address book states. -** Pros: Easy for new Computer Science student undergraduates to understand, who are likely to be the new incoming developers of our project. -** Cons: Logic is duplicated twice. For example, when a new command is executed, we must remember to update both `HistoryManager` and `VersionedAddressBook`. -* **Alternative 2:** Use `HistoryManager` for undo/redo -** Pros: We do not need to maintain a separate list, and just reuse what is already in the codebase. -** Cons: Requires dealing with commands that have already been undone: We must remember to skip these commands. Violates Single Responsibility Principle and Separation of Concerns as `HistoryManager` now needs to do two different things. -// end::undoredo[] +[[Design-Model-ReminderManager]] +==== Model for ReminderManager -// tag::dataencryption[] -=== [Proposed] Data Encryption +QuickDocs supports reminding our users about upcoming appointments and warns users about medicines that is low in stock. + -_{Explain here how the data encryption feature will be implemented}_ +QuickDocs also supports customized reminders that could be set up by the users themselves. + -// end::dataencryption[] +The figure below illustrates how reminder module is implemented. -=== Logging +[[ReminderManager_diagram]] +.Structure of ReminderManager +image::ReminderManager_diagram.png[width='800'] -We are using `java.util.logging` package for logging. The `LogsCenter` class is used to manage the logging levels and logging destinations. +`Reminder` extends from slot, and has a starting date and end date. Users are free to customize reminders' title and comments for user-initiated reminders. + -* The logging level can be controlled using the `logLevel` setting in the configuration file (See <>) -* The `Logger` for a class can be obtained using `LogsCenter.getLogger(Class)` which will log messages according to the specified logging level -* Currently log messages are output through: `Console` and to a `.log` file. +`ReminderManager` keeps a list of reminders sorted by the date of reminder. Reminders that expires, i.e passed the end date, will automatically be hidden from the panel list of reminders shown to the user. + -*Logging Levels* +Upon every subtraction or addition of medicine quantity in the inventory, the `ModelManager` calls the `ReminderManager` to check the sufficiency of medicine against the set threshold and update the reminder panel accordingly, so that the reminders for medicines in low stock is managed automatically upon every change in medicine quantity. -* `SEVERE` : Critical problem detected which may possibly cause the termination of the application -* `WARNING` : Can continue, but with caution -* `INFO` : Information showing the noteworthy actions by the App -* `FINE` : Details that is not usually noteworthy but may be useful in debugging e.g. print the actual list instead of just its size +[[Design-Storage]] +=== Storage component +The `Storage` component of QuickDocs allows data to be saved and read from a json file. -[[Implementation-Configuration]] -=== Configuration +==== Structure of the Storage Component +.Structure of the Storage Component +image::StorageClassDiagram.png[width="800"] -Certain properties of the application can be controlled (e.g user prefs file location, logging level) through the configuration file (default: `config.json`). +*API* : link:{repoURL}/src/main/java/quickdocs/storage/Storage.java[`Storage.java`] -== Documentation +The `Storage` component, -We use asciidoc for writing documentation. +* can save `UserPref` objects in json format and read it back. +* can save all the QuickDocs data in json format into a single json file and read it back +** this json file contains 8 different lists, with each list consisting of only one of the 8 main `JsonAdapted` Class objects. + +==== When does QuickDocs read your data? +All data are stored in a json file, with a default filepath `data/quickdocs.json`. This filepath can be +customised in the `preferences.json` file. + +When QuickDocs is launched, all information in the `quickdocs.json` file will be read. As mentioned in the previous +section, the json file contains 8 different lists, and each list will have their information converted to their +corresponding model types by their respective `toModelType()` methods of the 8 different `JsonAdapted` classes. + +These converted objects will then be added into their respective class managers. +As mentioned in the <> section, the `QuickDocs` class is responsible for storing +all these data as it holds all the different class managers. + +Note that there are only 6 managers but there are 8 `JsonAdapted` classes. This is because converted +`JsonAdaptedStatistics` and `JsonAdaptedMonthStatistics` objects are both stored in the `StatisticsManager`. +Similarly, both `JsonAdaptedMedicine` and `JsonAdaptedDirectory` objects are stored in `MedicineManager`. + +===== Starting QuickDocs with no data +If the `quickdocs.json` is not found, or contains any errors such that any information cannot be converted to its +corresponding model type, QuickDocs will be launched in a clean slate. If it is the case that the json file +is erroneous, it will not be deleted, however it will be overwritten if any saving occurs during the current session. + +==== When does QuickDocs save your data? +QuickDocs saves data whenever there is modification of any information in the current session. + +Note that the user mainly interacts with QuickDocs by executing commands, and only some user commands will modify +its data. For example, commands such as `listmed`, to list medicines, or `listapp`, to list appointments, +will not affect the data. However, commands such as `editpat`, to edit a patient's particulars, or `addapp`, to add +an appointment, will change the information stored in QuickDocs. + +Hence, only methods that modifies data will indicate to the `QuickDocs` class that a modification occurred. All methods +that interact with the various class managers are contained in the `ModelManager` class, which holds a reference to +the main `QuickDocs` object and references to all class managers. The following are the steps taken when one of +these methods, in this case `Model#addApp()`, is called, which leads to data being saved: + +. `Model#addApp()` adds the provided `Appointment` into `AppointmentManager`. +. `Model#addApp()` then calls `QuickDocs#indicateModification()`, providing the `boolean true` argument. This +indicates that a change in data has occurred, which did happened since a new `Appointment` object had been added. +. `LogicManager#execute()` checks if any modification occurred, through the `QuickDocs#isModified()` method, which +in this case returns `true`. +. `LogicManager#execute()` then saves the new modified QuickDocs data by calling `Storage#saveQuickDocs()`, providing +it with the `QuickDocs` object. The new modified data will now overwrite all data stored in the `quickdocs.json` file. + +===== Design considerations +We chose this implementation to ensure that no data will be lost due to any unforeseen circumstances. For example, +if QuickDocs is closed unexpectedly, no data will be lost as any change in information has already been saved when +that modification occurred. -[NOTE] -We chose asciidoc over Markdown because asciidoc, although a bit more complex than Markdown, provides more flexibility in formatting. +[[Design-Commons]] +=== Common classes -=== Editing Documentation +Classes used by multiple components are in the `quickdocs.commons` package. -See <> to learn how to render `.adoc` files locally to preview the end result of your edits. -Alternatively, you can download the AsciiDoc plugin for IntelliJ, which allows you to preview the changes you have made to your `.adoc` files in real-time. +== Implementation -=== Publishing Documentation +This section describes the details on how certain features are implemented. -See <> to learn how to deploy GitHub Pages using Travis. +=== Patient management module -=== Converting Documentation to PDF format +The patient management module consists of these commands: -We use https://www.google.com/chrome/browser/desktop/[Google Chrome] for converting documentation to PDF format, as Chrome's PDF engine preserves hyperlinks used in webpages. +1. adding a patient record `addpat` +2. editing a patient record `editpat` +3. deleting a patient record `deletepat` +4. listing a patient record (by name, nric, tags or index) `listpat` -Here are the steps to convert the project documentation files to PDF format. +The operations involved in the patient management modules involve the use of the `model\Patient\PatientManager.java` +class. The `PatientManager` maintains a list of patient records, which are loaded from the `quickdocs.json` file through the +<> component. -. Follow the instructions in <> to convert the AsciiDoc files in the `docs/` directory to HTML format. -. Go to your generated HTML files in the `build/docs` folder, right click on them and select `Open with` -> `Google Chrome`. -. Within Chrome, click on the `Print` option in Chrome's menu. -. Set the destination to `Save as PDF`, then click `Save` to save a copy of the file in PDF format. For best results, use the settings indicated in the screenshot below. +==== Adding a patient -.Saving documentation as PDF files in Chrome -image::chrome_save_as_pdf.png[width="300"] +Patient records consist of `Name`, `NRIC`, `Email`, `Address`, `Contact`, `Gender`, `Dob` (Date of Birth) and `tagList` fields. The `addpat` command +require users to enter the value of these fields prepended by prefixes. The prefixes are used to separate the parameters and assign the +values to these fields. -[[Docs-SiteWideDocSettings]] -=== Site-wide Documentation Settings +image::dg-consultation/patientcreation.png[width="800"] -The link:{repoURL}/build.gradle[`build.gradle`] file specifies some project-specific https://asciidoctor.org/docs/user-manual/#attributes[asciidoc attributes] which affects how all documentation files within this project are rendered. +[NOTE] +tagList can contain multiple or no tags at all. -[TIP] -Attributes left unset in the `build.gradle` file will use their *default value*, if any. +==== Editing a patient -[cols="1,2a,1", options="header"] -.List of site-wide attributes -|=== -|Attribute name |Description |Default value +To edit a patient, a `PatientEditedFields` is first created. It consist of all the fields of a `Patient` object but all its values +are null initially. This means that only when the user enter a value for a specific field will it be assigned to the `PatientEditedFields`. -|`site-name` -|The name of the website. -If set, the name will be displayed near the top of the page. -|_not set_ +A temporary `Patient` object is then created with the values of the existing patient record to be edited. The `PatientEditedFields` will then +be checked against this temporary patient object and replace the fields which are non-null. -|`site-githuburl` -|URL to the site's repository on https://github.com[GitHub]. -Setting this will add a "View on GitHub" link in the navigation bar. -|_not set_ +image::dg-consultation/patientediting.png[width="800"] -|`site-seedu` -|Define this attribute if the project is an official SE-EDU project. -This will render the SE-EDU navigation bar at the top of the page, and add some SE-EDU-specific navigation items. -|_not set_ +An additional check for NRIC will be done on the list of patient records to ensure that the editing of NRIC does not cause a conflict +with existing Patient records. When this additional check is passed, the temporary patient object will replace the existing patient record +designated for editing. -|=== +==== Deleting patient records -[[Docs-PerFileDocSettings]] -=== Per-file Documentation Settings +Each patient have a unique NRIC value. This is how QuickDocs differentiate between the different patient +records in the patient list in the `PatientManager` class. -Each `.adoc` file may also specify some file-specific https://asciidoctor.org/docs/user-manual/#attributes[asciidoc attributes] which affects how the file is rendered. +To delete a patient record, the `deletepat` and a nric is specified. The patient list will be iterated and the record whose +NRIC matches the specified value will be removed. -Asciidoctor's https://asciidoctor.org/docs/user-manual/#builtin-attributes[built-in attributes] may be specified and used as well. +==== Listing patient records -[TIP] -Attributes left unset in `.adoc` files will use their *default value*, if any. +Since the patient records are stored in a list, their position in the list (index) can be used to view the details of a specific patient record. -[cols="1,2a,1", options="header"] -.List of per-file attributes, excluding Asciidoctor's built-in attributes -|=== -|Attribute name |Description |Default value +The user can narrow down their patient record searches using the names, nric and tags assigned to each patient, and this results in a sublist +of patient records, with their index reflected to be shown on the main display of QuickDocs. The specific session can then be viewed by calling `listpat` along +with the index. -|`site-section` -|Site section that the document belongs to. -This will cause the associated item in the navigation bar to be highlighted. -One of: `UserGuide`, `DeveloperGuide`, ``LearningOutcomes``{asterisk}, `AboutUs`, `ContactUs` +Internally, a `ListCommand` can +be created using four different constructors and each of them have a `constructedBy` field. The constructedBy field will indicate +whether the search is done by indexing, or filtering by name, nric or tags. -_{asterisk} Official SE-EDU projects only_ -|_not set_ +1. If indexing is used, `getPatientAtIndex()` is called during the execution of the List command to simply retrieve the record +in the patient list, at the position specified. -|`no-site-header` -|Set this attribute to remove the site navigation bar. -|_not set_ +2. If name is used, `findPatientsByName()` will be called, and patient records whose names containing the sequence that the user entered will be retrieved. -|=== +3. If the nric is used, `findPatientsByNric()` is called and all patient records whose NRIC starts with the sequence supplied will be retrieved. -=== Site Template +4. If tag is used, `findPatientsByTag` is called, retrieving all patient records tied with the tag specified. -The files in link:{repoURL}/docs/stylesheets[`docs/stylesheets`] are the https://developer.mozilla.org/en-US/docs/Web/CSS[CSS stylesheets] of the site. -You can modify them to change some properties of the site's design. +Lastly, if `listpat` is called without any search parameters, QuickDocs will simply list the first 50 patients in the patient list. -The files in link:{repoURL}/docs/templates[`docs/templates`] controls the rendering of `.adoc` files into HTML5. -These template files are written in a mixture of https://www.ruby-lang.org[Ruby] and http://slim-lang.com[Slim]. +=== Consultation management module -[WARNING] -==== -Modifying the template files in link:{repoURL}/docs/templates[`docs/templates`] requires some knowledge and experience with Ruby and Asciidoctor's API. -You should only modify them if you need greater control over the site's layout than what stylesheets can provide. -The SE-EDU team does not provide support for modified template files. -==== +The consultation module consists of: -[[Testing]] -== Testing +1. *The consultation process* +2. Listing of past patient <> -=== Running Tests +The listing of past patient consultation records is similar to the listing of patient records explained earlier, the only difference +being it can only be filtered down by NRIC and viewed using indexing. -There are three ways to run tests. +The *consultation process* on the other hand, comprises of four stages: starting, diagnosis, prescribing and ending the consultation session. +It leverages on the actions done in the patient module and forms the bulk of the processes in the consultation module. -[TIP] -The most reliable way to run tests is the 3rd one. The first two methods might fail some GUI tests due to platform/resolution-specific idiosyncrasies. +The following section will provide a more in-depth exploration of how the consultation process is implemented. This includes: -*Method 1: Using IntelliJ JUnit test runner* +* The explanation of the design and mechanism behind the consultation process +* The decision making process of selecting the current implementation -* To run all tests, right-click on the `src/test/java` folder and choose `Run 'All Tests'` -* To run a subset of tests, you can right-click on a test package, test class, or a test and choose `Run 'ABC'` +==== Current Implementation for the consultation process -*Method 2: Using Gradle* +The consultation process comprises of four stages: -* Open a console and run the command `gradlew clean allTests` (Mac/Linux: `./gradlew clean allTests`) +1. starting the consultation with a selected patient +2. entering the symptoms, assessment of the patient's current condition +3. entering the medicine to be prescribed +4. ending the consultation -[NOTE] -See <> for more info on how to run tests using Gradle. +The consultation process is facilitated by the `ConsultationManager.java` class. +The ConsultationManager class holds the current consultation session and a list of past +consultation records for every patients. -*Method 3: Using Gradle (headless)* +Methods in the ConsultationManager comprises of: -Thanks to the https://github.com/TestFX/TestFX[TestFX] library we use, our GUI tests can be run in the _headless_ mode. In the headless mode, GUI tests do not show up on the screen. That means the developer can do other things on the Computer while the tests are running. +* `createConsultation(Patient)` -- Starts a consultation session with the current selected patient +* `diagnosePatient(Diagnosis)` -- Record symptoms patient mentioned and the assessment of the current condition. +* `prescribeMedicine(List of Prescriptions)` -- Prescribe the medicine and the quantities to be administered. +* `endConsultation()` -- Ends the consultation session. No further edits can be made to both prescription and diagnosis. -To run tests in headless mode, open a console and run the command `gradlew clean headless allTests` (Mac/Linux: `./gradlew clean headless allTests`) +Both `diagnosePatient` and `prescribeMedicine` are repeatable. The values entered during the repeated command will simply replace +the existing diagnosis / prescription. -=== Types of tests +[NOTE] +QuickDocs only permit one ongoing consultation. During diagnosis and prescription, changes are only made to the current consultation +session. The previous consultations should not be edited to prevent falsification of medical records. The current consultation session +can only end after both the diagnosis and prescription are finalized. -We have two types of tests: +Given below is an example usage scenario: -. *GUI Tests* - These are tests involving the GUI. They include, -.. _System Tests_ that test the entire App by simulating user actions on the GUI. These are in the `systemtests` package. -.. _Unit tests_ that test the individual components. These are in `seedu.address.ui` package. -. *Non-GUI Tests* - These are tests not involving the GUI. They include, -.. _Unit tests_ targeting the lowest level methods/classes. + -e.g. `seedu.address.commons.StringUtilTest` -.. _Integration tests_ that are checking the integration of multiple code units (those code units are assumed to be working). + -e.g. `seedu.address.storage.StorageManagerTest` -.. Hybrids of unit and integration tests. These test are checking multiple code units as well as how the are connected together. + -e.g. `seedu.address.logic.LogicManagerTest` -=== Troubleshooting Testing -**Problem: `HelpWindowTest` fails with a `NullPointerException`.** +*Step 1.* A previously registered patient arrives and the doctor starts the session by +entering the consult command in this manner: `consult r/NRIC of the patient`. A message to indicate +the start of the consultation will be shown in the results display. -* Reason: One of its dependencies, `HelpWindow.html` in `src/main/resources/docs` is missing. -* Solution: Execute Gradle task `processResources`. +* if the patient is new and his or her details are not recorded in QuickDocs, the command will not be executed and the doctor will be alerted +that the consultation cannot continue since no patient records with the entered Nric can be found. An invalid nric entered will also prompt the +same response -== Dev Ops +image::dg-consultation/consultation1.png[width="800"] -=== Build Automation +*Step 2.* The patient will tell the doctor what are his / her ailments. The doctor will record the symptoms +down. The doctor will then make the assessment of the illness the patient is having and execute the command by clicking +on the `Enter` on the keyboard. -See <> to learn how to use Gradle for build automation. +* The symptoms and assessment have to be prepended by the `s/` and `a/` prefix respectively +* The command entered by the doctor will look something like this: `diagnose s/constant coughing s/sore throat a/throat infection` -=== Continuous Integration +image::dg-consultation/consultation2.png[width="800"] -We use https://travis-ci.org/[Travis CI] and https://www.appveyor.com/[AppVeyor] to perform _Continuous Integration_ on our projects. See <> and <> for more details. +*Step 3.* Should the patient inform the doctor of additional symptoms after the diagnosis is given, the doctor can simply press +the up and down key to display the previously entered command on the userInput area. The doctor can then add the new symptom in and +press `Enter`, replacing the previously recorded diagnosis. -=== Coverage Reporting +image::dg-consultation/consultation3.png[width="800"] -We use https://coveralls.io/[Coveralls] to track the code coverage of our projects. See <> for more details. +*Step 4.* The doctor will then add the medicine to the prescription list, followed by the quantities. Medicine are prepended by the `m/` prefix while +quantities are prefixed by `q/`.The order of the quantity entered corresponds with the order the medicine is added in the command: -=== Documentation Previews -When a pull request has changes to asciidoc files, you can use https://www.netlify.com/[Netlify] to see a preview of how the HTML version of those asciidoc files will look like when the pull request is merged. See <> for more details. +* `prescribe m/Dextromethorphan m/ibuprofen q/1 q/2` In this case q/1 represents one unit of Dextromethorphan cough syrup is issued while +2 units of ibuprofen (inflammatory tablets) are issued to the patient +* Alternatively, the doctor can enter the quantity right after the medicine: `prescribe m/Dextromethorphan q/1 m/ibuprofen q/2` -=== Making a Release +If any of the medicine issued are insufficient to complete the prescription, or is simply not in the inventory, a message will be displayed in +the inputFeedback area. The command will not be executed and remains in the userInput text field. The doctor can then make the changes to the command. -Here are the steps to create a new release. +image::dg-consultation/consultation4.png[width="800"] -. Update the version number in link:{repoURL}/src/main/java/seedu/address/MainApp.java[`MainApp.java`]. -. Generate a JAR file <>. -. Tag the repo with the version number. e.g. `v0.1` -. https://help.github.com/articles/creating-releases/[Create a new release using GitHub] and upload the JAR file you created. +*Step 5.* Just like the diagnosis command, prescription can be replaced by reentering the command. -=== Managing Dependencies +image::dg-consultation/consultation5.png[width="800"] -A project often depends on third-party libraries. For example, Address Book depends on the https://github.com/FasterXML/jackson[Jackson library] for JSON parsing. Managing these _dependencies_ can be automated using Gradle. For example, Gradle can download the dependencies automatically, which is better than these alternatives: +*Step 6.* After explaining the medicine intake to the patient, the doctor can then end the consultation session on QuickDocs by using the command +`endconsult`. No further changes to the consultation records can be made from this point on. -[loweralpha] -. Include those libraries in the repo (this bloats the repo size) -. Require developers to download those libraries manually (this creates extra work for developers) +The following sequence diagrams summarizes what happens when a user perform the entire consultation process, starting with the session initialisation: -[[GetStartedProgramming]] -[appendix] -== Suggested Programming Tasks to Get Started +image::dg-consultation/consultationSD1.png[width="800"] -Suggested path for new programmers: +Followed by the adding of the diagnosis: -1. First, add small local-impact (i.e. the impact of the change does not go beyond the component) enhancements to one component at a time. Some suggestions are given in <>. +image::dg-consultation/diagnosisSD.png[width="800"] -2. Next, add a feature that touches multiple components to learn how to implement an end-to-end feature across all components. <> explains how to go about adding such a feature. +prescribing the medicine to tackle the patient's condition: -[[GetStartedProgramming-EachComponent]] -=== Improving each component +image::dg-consultation/prescriptionSD.png[width="800"] -Each individual exercise in this section is component-based (i.e. you would not need to modify the other components to get it to work). +finally, saving the consultation record into QuickDocs: -[discrete] -==== `Logic` component +image::dg-consultation/endconsultSD.png[width="800"] -*Scenario:* You are in charge of `logic`. During dog-fooding, your team realize that it is troublesome for the user to type the whole command in order to execute a command. Your team devise some strategies to help cut down the amount of typing necessary, and one of the suggestions was to implement aliases for the command words. Your job is to implement such aliases. -[TIP] -Do take a look at <> before attempting to modify the `Logic` component. +==== Design considerations -. Add a shorthand equivalent alias for each of the individual commands. For example, besides typing `clear`, the user can also type `c` to remove all persons in the list. -+ -**** -* Hints -** Just like we store each individual command word constant `COMMAND_WORD` inside `*Command.java` (e.g. link:{repoURL}/src/main/java/seedu/address/logic/commands/FindCommand.java[`FindCommand#COMMAND_WORD`], link:{repoURL}/src/main/java/seedu/address/logic/commands/DeleteCommand.java[`DeleteCommand#COMMAND_WORD`]), you need a new constant for aliases as well (e.g. `FindCommand#COMMAND_ALIAS`). -** link:{repoURL}/src/main/java/seedu/address/logic/parser/AddressBookParser.java[`AddressBookParser`] is responsible for analyzing command words. -* Solution -** Modify the switch statement in link:{repoURL}/src/main/java/seedu/address/logic/parser/AddressBookParser.java[`AddressBookParser#parseCommand(String)`] such that both the proper command word and alias can be used to execute the same intended command. -** Add new tests for each of the aliases that you have added. -** Update the user guide to document the new aliases. -** See this https://github.com/se-edu/addressbook-level4/pull/785[PR] for the full solution. -**** +1. In a neighbourhood clinic setting, doctors usually tend to only one patient at a time. This is why QuickDocs only allow a single +ongoing session in the consultation process. -[discrete] -==== `Model` component +2. In Singapore, every person is given a unique NRIC / FIN number regardless of their citizenship statuses. As such the NRIC is used to +search for the patient records to start the consultation session. -*Scenario:* You are in charge of `model`. One day, the `logic`-in-charge approaches you for help. He wants to implement a command such that the user is able to remove a particular tag from everyone in the address book, but the model API does not support such a functionality at the moment. Your job is to implement an API method, so that your teammate can use your API to implement his command. +3. The prescription and diagnosis commands are made to override their previous states to ease the modification of consultation data. +Doctors can simply use the command history to navigate to the previous command entered, make the changes and then execute the command. This +allow them to simply add a few words to change consultation data rather than re-entering the entire command line. -[TIP] -Do take a look at <> before attempting to modify the `Model` component. +4. Prescription can actually be added before the diagnosis is recorded. The doctor could be expecting a patient for regular checkup and prepare the +prescription before the patient enters the room. If the condition remains the same as before, the doctor can simply enter the diagnosis to complete the +consultation session, cutting down the time spent on the consultation session. -. Add a `removeTag(Tag)` method. The specified tag will be removed from everyone in the address book. -+ -**** -* Hints -** The link:{repoURL}/src/main/java/seedu/address/model/Model.java[`Model`] and the link:{repoURL}/src/main/java/seedu/address/model/AddressBook.java[`AddressBook`] API need to be updated. -** Think about how you can use SLAP to design the method. Where should we place the main logic of deleting tags? -** Find out which of the existing API methods in link:{repoURL}/src/main/java/seedu/address/model/AddressBook.java[`AddressBook`] and link:{repoURL}/src/main/java/seedu/address/model/person/Person.java[`Person`] classes can be used to implement the tag removal logic. link:{repoURL}/src/main/java/seedu/address/model/AddressBook.java[`AddressBook`] allows you to update a person, and link:{repoURL}/src/main/java/seedu/address/model/person/Person.java[`Person`] allows you to update the tags. -* Solution -** Implement a `removeTag(Tag)` method in link:{repoURL}/src/main/java/seedu/address/model/AddressBook.java[`AddressBook`]. Loop through each person, and remove the `tag` from each person. -** Add a new API method `deleteTag(Tag)` in link:{repoURL}/src/main/java/seedu/address/model/ModelManager.java[`ModelManager`]. Your link:{repoURL}/src/main/java/seedu/address/model/ModelManager.java[`ModelManager`] should call `AddressBook#removeTag(Tag)`. -** Add new tests for each of the new public methods that you have added. -** See this https://github.com/se-edu/addressbook-level4/pull/790[PR] for the full solution. -**** +==== Alternatives considered -[discrete] -==== `Ui` component +Prior to the current implementation, a few options for the overall consultation process was considered: -*Scenario:* You are in charge of `ui`. During a beta testing session, your team is observing how the users use your address book application. You realize that one of the users occasionally tries to delete non-existent tags from a contact, because the tags all look the same visually, and the user got confused. Another user made a typing mistake in his command, but did not realize he had done so because the error message wasn't prominent enough. A third user keeps scrolling down the list, because he keeps forgetting the index of the last person in the list. Your job is to implement improvements to the UI to solve all these problems. +[cols="1,2a,1, 1", options="header"] +|=== +|Alternative |Description |Pros | Cons +// row 1 +|*Consultation as one single command* +|Doctor enter `consult` followed by all the symptoms, assessment, prescriptions + and then execute +| Consultation is now restricted to just one class -[TIP] -Do take a look at <> before attempting to modify the `UI` component. +The consultation creation will truly be one-shot +| -. Use different colors for different tags inside person cards. For example, `friends` tags can be all in brown, and `colleagues` tags can be all in yellow. -+ -**Before** -+ -image::getting-started-ui-tag-before.png[width="300"] -+ -**After** -+ -image::getting-started-ui-tag-after.png[width="300"] -+ -**** -* Hints -** The tag labels are created inside link:{repoURL}/src/main/java/seedu/address/ui/PersonCard.java[the `PersonCard` constructor] (`new Label(tag.tagName)`). https://docs.oracle.com/javase/8/javafx/api/javafx/scene/control/Label.html[JavaFX's `Label` class] allows you to modify the style of each Label, such as changing its color. -** Use the .css attribute `-fx-background-color` to add a color. -** You may wish to modify link:{repoURL}/src/main/resources/view/DarkTheme.css[`DarkTheme.css`] to include some pre-defined colors using css, especially if you have experience with web-based css. -* Solution -** You can modify the existing test methods for `PersonCard` 's to include testing the tag's color as well. -** See this https://github.com/se-edu/addressbook-level4/pull/798[PR] for the full solution. -*** The PR uses the hash code of the tag names to generate a color. This is deliberately designed to ensure consistent colors each time the application runs. You may wish to expand on this design to include additional features, such as allowing users to set their own tag colors, and directly saving the colors to storage, so that tags retain their colors even if the hash code algorithm changes. -**** - -. Modify link:{repoURL}/src/main/java/seedu/address/commons/events/ui/NewResultAvailableEvent.java[`NewResultAvailableEvent`] such that link:{repoURL}/src/main/java/seedu/address/ui/ResultDisplay.java[`ResultDisplay`] can show a different style on error (currently it shows the same regardless of errors). -+ -**Before** -+ -image::getting-started-ui-result-before.png[width="200"] -+ -**After** -+ -image::getting-started-ui-result-after.png[width="200"] -+ -**** -* Hints -** link:{repoURL}/src/main/java/seedu/address/commons/events/ui/NewResultAvailableEvent.java[`NewResultAvailableEvent`] is raised by link:{repoURL}/src/main/java/seedu/address/ui/CommandBox.java[`CommandBox`] which also knows whether the result is a success or failure, and is caught by link:{repoURL}/src/main/java/seedu/address/ui/ResultDisplay.java[`ResultDisplay`] which is where we want to change the style to. -** Refer to link:{repoURL}/src/main/java/seedu/address/ui/CommandBox.java[`CommandBox`] for an example on how to display an error. -* Solution -** Modify link:{repoURL}/src/main/java/seedu/address/commons/events/ui/NewResultAvailableEvent.java[`NewResultAvailableEvent`] 's constructor so that users of the event can indicate whether an error has occurred. -** Modify link:{repoURL}/src/main/java/seedu/address/ui/ResultDisplay.java[`ResultDisplay#handleNewResultAvailableEvent(NewResultAvailableEvent)`] to react to this event appropriately. -** You can write two different kinds of tests to ensure that the functionality works: -*** The unit tests for `ResultDisplay` can be modified to include verification of the color. -*** The system tests link:{repoURL}/src/test/java/systemtests/AddressBookSystemTest.java[`AddressBookSystemTest#assertCommandBoxShowsDefaultStyle() and AddressBookSystemTest#assertCommandBoxShowsErrorStyle()`] to include verification for `ResultDisplay` as well. -** See this https://github.com/se-edu/addressbook-level4/pull/799[PR] for the full solution. -*** Do read the commits one at a time if you feel overwhelmed. -**** - -. Modify the link:{repoURL}/src/main/java/seedu/address/ui/StatusBarFooter.java[`StatusBarFooter`] to show the total number of people in the address book. -+ -**Before** -+ -image::getting-started-ui-status-before.png[width="500"] -+ -**After** -+ -image::getting-started-ui-status-after.png[width="500"] -+ -**** -* Hints -** link:{repoURL}/src/main/resources/view/StatusBarFooter.fxml[`StatusBarFooter.fxml`] will need a new `StatusBar`. Be sure to set the `GridPane.columnIndex` properly for each `StatusBar` to avoid misalignment! -** link:{repoURL}/src/main/java/seedu/address/ui/StatusBarFooter.java[`StatusBarFooter`] needs to initialize the status bar on application start, and to update it accordingly whenever the address book is updated. -* Solution -** Modify the constructor of link:{repoURL}/src/main/java/seedu/address/ui/StatusBarFooter.java[`StatusBarFooter`] to take in the number of persons when the application just started. -** Use link:{repoURL}/src/main/java/seedu/address/ui/StatusBarFooter.java[`StatusBarFooter#handleAddressBookChangedEvent(AddressBookChangedEvent)`] to update the number of persons whenever there are new changes to the addressbook. -** For tests, modify link:{repoURL}/src/test/java/guitests/guihandles/StatusBarFooterHandle.java[`StatusBarFooterHandle`] by adding a state-saving functionality for the total number of people status, just like what we did for save location and sync status. -** For system tests, modify link:{repoURL}/src/test/java/systemtests/AddressBookSystemTest.java[`AddressBookSystemTest`] to also verify the new total number of persons status bar. -** See this https://github.com/se-edu/addressbook-level4/pull/803[PR] for the full solution. -**** +Input will be verbose, easy for the doctor to make mistakes -[discrete] -==== `Storage` component +Harder to spot and navigate to the erroneous part to make changes -*Scenario:* You are in charge of `storage`. For your next project milestone, your team plans to implement a new feature of saving the address book to the cloud. However, the current implementation of the application constantly saves the address book after the execution of each command, which is not ideal if the user is working on limited internet connection. Your team decided that the application should instead save the changes to a temporary local backup file first, and only upload to the cloud after the user closes the application. Your job is to implement a backup API for the address book storage. +No room for the doctor to make changes as the consultation could have ended with erroneous information recorded +// row 2 +|*Iterative consultation creation* +|Doctor enter `consult`. -[TIP] -Do take a look at <> before attempting to modify the `Storage` component. +Doctor get prompted to enter symptoms and assessment. -. Add a new method `backupAddressBook(ReadOnlyAddressBook)`, so that the address book can be saved in a fixed temporary location. -+ -**** -* Hint -** Add the API method in link:{repoURL}/src/main/java/seedu/address/storage/AddressBookStorage.java[`AddressBookStorage`] interface. -** Implement the logic in link:{repoURL}/src/main/java/seedu/address/storage/StorageManager.java[`StorageManager`] and link:{repoURL}/src/main/java/seedu/address/storage/JsonAddressBookStorage.java[`JsonAddressBookStorage`] class. -* Solution -** See this https://github.com/se-edu/addressbook-level4/pull/594[PR] for the full solution. -**** +Doctor get prompted to enter prescription. -[[GetStartedProgramming-RemarkCommand]] -=== Creating a new command: `remark` +Consultation is ended once prescription is recorded +| Less likely to enter erroneous data as consultation is now broken down to different stages -By creating this command, you will get a chance to learn how to implement a feature end-to-end, touching all major components of the app. +| The consultation will take a longer time to be completed -*Scenario:* You are a software maintainer for `addressbook`, as the former developer team has moved on to new projects. The current users of your application have a list of new feature requests that they hope the software will eventually have. The most popular request is to allow adding additional comments/notes about a particular contact, by providing a flexible `remark` field for each contact, rather than relying on tags alone. After designing the specification for the `remark` command, you are convinced that this feature is worth implementing. Your job is to implement the `remark` command. +Doctor can only diagnose and prescribe during the session, while other related actions +(such as listing past records) can only be done after the consultation -==== Description -Edits the remark for a person specified in the `INDEX`. + -Format: `remark INDEX r/[REMARK]` +// row 3 +| *Separate commands for start, diagnose, prescribe and end* -Examples: +*(Chosen implementation)* +| +Doctors begin and end session with `consult` and `endconsult`. -* `remark 1 r/Likes to drink coffee.` + -Edits the remark for the first person to `Likes to drink coffee.` -* `remark 1 r/` + -Removes the remark for the first person. +Prescriptions and diagnosis can be added or replaced using the `diagnose` and `prescribe` commands before the session ends. +| The editing involve the replacement of the current diagnosis or prescription entry, commands can be reused to perform both +add and edit operations. -==== Step-by-step Instructions +Editing remain one shot and fast as users can make use of the command history to make changes to a previously entered command to +make changes. -===== [Step 1] Logic: Teach the app to accept 'remark' which does nothing -Let's start by teaching the application how to parse a `remark` command. We will add the logic of `remark` later. +Flexibility in recording consultation details, instead of having to go through the start, diagnose, prescribe, end order +strictly. -**Main:** +Room for other commands to be executed while a session is ongoing. -. Add a `RemarkCommand` that extends link:{repoURL}/src/main/java/seedu/address/logic/commands/Command.java[`Command`]. Upon execution, it should just throw an `Exception`. -. Modify link:{repoURL}/src/main/java/seedu/address/logic/parser/AddressBookParser.java[`AddressBookParser`] to accept a `RemarkCommand`. +| +Potentially more commands will be called when compared to the other options. -**Tests:** +diagnosis and prescription commands entered could be verbose and doctors can make mistakes easily. -. Add `RemarkCommandTest` that tests that `execute()` throws an Exception. -. Add new test method to link:{repoURL}/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java[`AddressBookParserTest`], which tests that typing "remark" returns an instance of `RemarkCommand`. +| *Fragment diagnosis and prescription commands even more* +| Same as the third alternative, but there are commands specific to the adding of symptoms in diagnosis, adding of medicine in prescription. -===== [Step 2] Logic: Teach the app to accept 'remark' arguments -Let's teach the application to parse arguments that our `remark` command will accept. E.g. `1 r/Likes to drink coffee.` +Separate commands for editing the symptoms added or medicine prescribed +| +Shorter commands to add symptoms or prescribe medicine. Less mistakes will be made. -**Main:** +Doctors only need to edit specific entries instead of retyping or navigating to previously entered command and make changes. -. Modify `RemarkCommand` to take in an `Index` and `String` and print those two parameters as the error message. -. Add `RemarkCommandParser` that knows how to parse two arguments, one index and one with prefix 'r/'. -. Modify link:{repoURL}/src/main/java/seedu/address/logic/parser/AddressBookParser.java[`AddressBookParser`] to use the newly implemented `RemarkCommandParser`. +| +Way more commands to be entered by users. -**Tests:** +Even more commands and methods to be written, there will be a higher possibility of bugs arising from the increase in +code volume. -. Modify `RemarkCommandTest` to test the `RemarkCommand#equals()` method. -. Add `RemarkCommandParserTest` that tests different boundary values -for `RemarkCommandParser`. -. Modify link:{repoURL}/src/test/java/seedu/address/logic/parser/AddressBookParserTest.java[`AddressBookParserTest`] to test that the correct command is generated according to the user input. +|=== -===== [Step 3] Ui: Add a placeholder for remark in `PersonCard` -Let's add a placeholder on all our link:{repoURL}/src/main/java/seedu/address/ui/PersonCard.java[`PersonCard`] s to display a remark for each person later. +Although the selected option require more input and lengthier commands, it guarantees the flexibility and efficiency QuickDocs +aim to deliver for doctors in neighbourhood clinics. -**Main:** +These are some of the considerations taken before the decision was made: -. Add a `Label` with any random text inside link:{repoURL}/src/main/resources/view/PersonListCard.fxml[`PersonListCard.fxml`]. -. Add FXML annotation in link:{repoURL}/src/main/java/seedu/address/ui/PersonCard.java[`PersonCard`] to tie the variable to the actual label. +1. Since QuickDocs aim to provide a single interface for doctors to perform clinical operations more efficiently, the consultation +process will require one shot commands to fulfill the efficiency requirement of the overall product. -**Tests:** +2. It is highly possible for doctors to make mistakes with the one-shot commands, especially when there are so many parameters involved +in a single command. Therefore the implementation must provide a convenient form of error recovery. -. Modify link:{repoURL}/src/test/java/guitests/guihandles/PersonCardHandle.java[`PersonCardHandle`] so that future tests can read the contents of the remark label. +3. There could be interleaving operations between the modules, such as viewing past consultation records or +checking medicine inventory in the midst of the consultation. The implementation must be flexible enough +to allow cross module commands during a consultation. -===== [Step 4] Model: Add `Remark` class -We have to properly encapsulate the remark in our link:{repoURL}/src/main/java/seedu/address/model/person/Person.java[`Person`] class. Instead of just using a `String`, let's follow the conventional class structure that the codebase already uses by adding a `Remark` class. +Although the selected option require more user input and involve lengthier commands, a doctor with fast typing speed will be able to +circumvent the issues of slightly more verbose command lines easily. -**Main:** +If the doctor enters an erroneous command or simply want to make changes, the command history can be used in conjunction with the +one shot commands to make changes quickly. -. Add `Remark` to model component (you can copy from link:{repoURL}/src/main/java/seedu/address/model/person/Address.java[`Address`], remove the regex and change the names accordingly). -. Modify `RemarkCommand` to now take in a `Remark` instead of a `String`. +The selected option also do not restrict doctors to just consultation-related commands. He or she can perform other operations such as checking the inventory +or view free appointment slots during the consultation itself. -**Tests:** +The selected implementation guarantees the flexibility and efficiency that +QuickDocs aim to deliver for doctors in neighbourhood clinics. -. Add test for `Remark`, to test the `Remark#equals()` method. +=== Appointment management module +The Appointment module provides the user with greater control over his/her scheduled appointments by organising them +neatly and preventing any clash of appointment timings. Listed below are the commands that the appointment module features: -===== [Step 5] Model: Modify `Person` to support a `Remark` field -Now we have the `Remark` class, we need to actually use it inside link:{repoURL}/src/main/java/seedu/address/model/person/Person.java[`Person`]. +. Adding an appointment, `addapp` +. Listing appointments, `listapp` +. Deleting an appointment, `deleteapp` +. Listing free appointment slots, `freeapp` -**Main:** +These features are supported by the `AppointmentManager` class, which stores all created appointments in an `ArrayList`. +On QuickDocs launch, existing appointments are read from the `quickdocs.json` file through the <> component. -. Add `getRemark()` in link:{repoURL}/src/main/java/seedu/address/model/person/Person.java[`Person`]. -. You may assume that the user will not be able to use the `add` and `edit` commands to modify the remarks field (i.e. the person will be created without a remark). -. Modify link:{repoURL}/src/main/java/seedu/address/model/util/SampleDataUtil.java/[`SampleDataUtil`] to add remarks for the sample data (delete your `data/addressbook.json` so that the application will load the sample data when you launch it.) +[[app]] +==== Appointments +An `Appointment` is a subclass of the `Slot` class, and has the following 5 compulsory fields: -===== [Step 6] Storage: Add `Remark` field to `JsonAdaptedPerson` class -We now have `Remark` s for `Person` s, but they will be gone when we exit the application. Let's modify link:{repoURL}/src/main/java/seedu/address/storage/JsonAdaptedPerson.java[`JsonAdaptedPerson`] to include a `Remark` field so that it will be saved. +* `Patient patient` - the patient who made this appointment +* `LocalDate date` - the date of this appointment +* `LocalTime start` - the time this appointment starts +* `LocalTime end` - the time this appointment ends +* `String comment` - any other details for this appointment -**Main:** +[[addapp]] +==== Adding an appointment +The user can add an appointment to his/her schedule to keep track of future meetings, by using the `addapp` command. +All 5 fields of an appointment, as mentioned in the <> section, must be specified together with +the command. -. Add a new JSON field for `Remark`. +===== Input validation +These fields are parsed by the `AddAppCommandParser` class, creating an `AddAppCommand` object, which then carries out +the following steps before adding the appointment into QuickDocs: -**Tests:** +. Retrieve the `patient`, if he/she exists, by calling `Model#getPatientByNric()`. +Note that the user specifies the `patient` of the appointment by providing the patient's NRIC, hence the need for this step. +. Check if the `start` and `end` timings are valid. The appointment timing has to be within office hours (9am to 6pm) +and the `start` time must not be after or equal to the `end` time. +. Create the appointment using the given fields and check if this appointment has any conflict in timing with other +existing appointments, by calling `Model#hasTimeConflicts()`. +. Finally, add the appointment into `AppointmentManager` by calling `Model#addApp()`. -. Fix `invalidAndValidPersonAddressBook.json`, `typicalPersonsAddressBook.json`, `validAddressBook.json` etc., such that the JSON tests will not fail due to a missing `remark` field. +[NOTE] +If any of the steps 1 to 3 fails, a `CommandException` will be thrown and the corresponding exception message will +be shown to the user. -===== [Step 6b] Test: Add withRemark() for `PersonBuilder` -Since `Person` can now have a `Remark`, we should add a helper method to link:{repoURL}/src/test/java/seedu/address/testutil/PersonBuilder.java[`PersonBuilder`], so that users are able to create remarks when building a link:{repoURL}/src/main/java/seedu/address/model/person/Person.java[`Person`]. +===== Adding the appointment into AppointmentManager +Listed below are the steps taken when the `Model#addApp()` method is called. Note that +when an appointment is added, a reminder tailored for this appointment will also be added. This was implemented to +ensure that the user does not forget about the appointment in the future. -**Tests:** +. Add the given appointment into the `AppointmentManager` by calling its `addAppointment()` method. +* `AppointmentManager#addAppointment()` takes in the appointment to be added and adds it into the `ArrayList` of +appointments in its sorted position. This ensures that this list of appointments is always sorted by date and time, +with the earliest appointment at the start of the list. +. Create a reminder tailored to this appointment by calling `Model#createRemFromApp()`. +. Add the newly created reminder into the `ReminderManager` by calling `Model#addRem()`. -. Add a new method `withRemark()` for link:{repoURL}/src/test/java/seedu/address/testutil/PersonBuilder.java[`PersonBuilder`]. This method will create a new `Remark` for the person that it is currently building. -. Try and use the method on any sample `Person` in link:{repoURL}/src/test/java/seedu/address/testutil/TypicalPersons.java[`TypicalPersons`]. +[NOTE] +You can learn more about reminders in the <> section. -===== [Step 7] Ui: Connect `Remark` field to `PersonCard` -Our remark label in link:{repoURL}/src/main/java/seedu/address/ui/PersonCard.java[`PersonCard`] is still a placeholder. Let's bring it to life by binding it with the actual `remark` field. +All these steps that are executed when the `addapp` command is called can be summarized in the +Sequence Diagrams shown below: -**Main:** +.Sequence diagram when `addapp` is called +image::dg-appointment/addapp_SD.png[width="800"] -. Modify link:{repoURL}/src/main/java/seedu/address/ui/PersonCard.java[`PersonCard`]'s constructor to bind the `Remark` field to the `Person` 's remark. +{sp} + -**Tests:** +.Sequence diagram reference: add appointment to AppointmentManager +image::dg-appointment/addapp_SD_ref.png[width="800"] -. Modify link:{repoURL}/src/test/java/seedu/address/ui/testutil/GuiTestAssert.java[`GuiTestAssert#assertCardDisplaysPerson(...)`] so that it will compare the now-functioning remark label. +==== Listing appointments +The user can list his/her past or future appointments using the `listapp` command. The user can either provide a range +of dates to list out all appointments in those dates, or provide an NRIC to list out all appointments for the patient +with the given NRIC. -===== [Step 8] Logic: Implement `RemarkCommand#execute()` logic -We now have everything set up... but we still can't modify the remarks. Let's finish it up by adding in actual logic for our `remark` command. +[NOTE] +If the user does not specify a `FORMAT` and `DATE`, `FORMAT` will default to `week` and `DATE` will default to the +current date, meaning that the current week's appointments will be displayed. + +[[listapp]] +===== Listing by dates +The user can specify the range of dates by providing a `FORMAT` (`day`, `week`, or `month`) and a `DATE`, which means to +list all appointments on the `FORMAT` (day/month/week) of `DATE`. The following steps will then be taken: + +. The `ListAppCommandParser` class parses these two parameters into `LocalDate start` and `LocalDate end`, and creates a +`ListAppCommand` object. `start` and `end` represents the start and end dates of the range of dates of appointments +to be listed. +. `ListAppCommand` will then be executed, calling `Model#listApp()`, providing it with the `start` and `end` dates. +. `Model#listApp()` then calls `AppointmentManager#listAppointments()`, with the same 2 arguments, which will return a +`String` of all appointment information within the given range of dates. + +===== Listing by patient's NRIC +In this case, the user only provides the NRIC of a patient together with the `listapp` command. The following steps +will then be taken: + +. The `ListAppCommandParser` class parses the user input into an `NRIC` object, creating a `ListAppCommand` object with this +`NRIC` field. +. `ListAppCommand` executes: +* Firstly, it retrieves the patient with the provided `NRIC`, if exists, by calling `Model#getPatientByNric()`. +* `Model#listApp()` will then be called, providing it with the patient retrieved. +. `Model#listApp()` then calls `AppointmentManager#listAppointments()`, providing it with the patient, which returns +a `String` of all the given patient's appointment information. -**Main:** +[NOTE] +`Model#listApp()` and `AppointmentManager#listAppointments()` are overloaded methods, having different method +signatures based on their parameters. One implementation takes in two `LocalDate` parameters, `start` and `end`, while +the other implementation takes in a single `Patient` object. This allows the same method name to be called, and list +appointments by either providing a range of dates or a valid patient respectively. + +[[deleteapp]] +==== Deleting appointments +The user can delete any appointments created using the `deleteapp` command. Since there cannot be any clash in timings +for appointments, any appointment can be identified uniquely by its date and start time. Hence the user can specify +the appointment to be deleted only with those two fields, after which the following steps are taken: + +. The `DeleteAppCommandParser` class parses the two parameters into `LocalDate date` and `LocalTime start` that specifies +the date and start time of the appointment to be deleted respectively. It then creates a `DeleteAppCommand` object with +these two fields. +. `DeleteAppCommand` executes: +* Firstly, it checks if `start` is a valid timing, checking if it is within office hours (9am to 6pm). +* Next, it retrieves the specified appointment, if it exists, by calling `Model#getAppointment()`, providing it +with `date` and `start`. +* Finally, `Model#deleteAppointment()` is called, providing it with the appointment retrieved. +. `Model#deleteAppointment()` then calls `AppointmentManager#deleteAppointment()`, providing it with the appointment. +`AppointmentManager#deleteAppointment()` will then remove the appointment from the `ArrayList` of appointments stored +in `AppointmentManager`. + +==== Free appointment slots +Before deciding on an appointment timing, the user can execute the `freeapp` command to list out all the timings available for +a new appointment booking. + +===== Command format: `freeapp f/FORMAT d/DATE` +We can see that the `freeapp` command takes in two parameters: + + +. `FORMAT`: can be `day`, `week`, or `month` + +. `DATE`: a valid date + +This command can be roughly translated to: + +_"Search for free appointment slots on the `FORMAT` (day/month/week) of `DATE`."_ + +The `FreeAppCommandParser` class will parse these two parameters into two dates, `LocalDate start` and `LocalDate end`, +representing the start and end dates of the search range for free appointment slots. `FreeAppCommandParser` then +constructs a `FreeAppCommand` object with the `start` and `end` fields. -. Replace the logic in `RemarkCommand#execute()` (that currently just throws an `Exception`), with the actual logic to modify the remarks of a person. +[NOTE] +If the user does not specify a `FORMAT` and `DATE`, `FORMAT` will default to `month` and `DATE` will default to the +next month's date, meaning that free appointment slots for the whole of the following month will be displayed. -**Tests:** +===== Current Implementation +The search is facilitated by the `AppointmentManager` class which stores all created `Appointments` in an `ArrayList`. +`AppointmentManager` contains the method `listFreeSlots()` which firstly calls `AppointmentManager#getFreeSlots()`. +`getFreeSlots()` is the main method that implements the logic behind `freeapp`. -. Update `RemarkCommandTest` to test that the `execute()` logic works. +Given below are the steps taken when `listFreeSlots()` is called. -==== Full Solution +Step 1. The method `listFreeSlots()` takes in the two arguments, `start` and `end`, which have been mentioned previously. +Firstly, `listFreeSlots()` calls `getFreeSlots()`, providing it with the same two arguments, to retrieve a `List` of +free `Slots` before it can parse them into a `String`. -See this https://github.com/se-edu/addressbook-level4/pull/599[PR] for the step-by-step solution. +.Given search range from start to end date +image::dg-appointment/freeapp1.png[width="800"] -[appendix] -== Product Scope +{sp} + +Step 2. In `getFreeSlots()`, we first retrieve the existing appointments that are within this given search range +by using the method `AppointmentManager#getAppointments()`. -*Target user profile*: +.Retrieved appointments in the search range +image::dg-appointment/freeapp2.png[width="800"] -* has a need to manage a significant number of contacts -* prefer desktop apps over other types -* can type fast -* prefers typing over mouse input -* is reasonably comfortable using CLI apps +{sp} + +Step 3. Next, we look at all the appointments that are present on the `start` date, as shown in the diagram below. +These appointments are sorted by date and time, with the earliest appointment on the left and the latest on the right. -*Value proposition*: manage contacts faster than a typical mouse/GUI driven app +.Selected appointments on start date +image::dg-appointment/freeapp3.png[width="800"] -[appendix] -== User Stories +[NOTE] +Since the appointments are already sorted, we do not need to search through the whole appointment list to +find appointments present on the `start` date. We can simply go through the list from the beginning +until we reach an appointment date that is not equals to `start`. -Priorities: High (must have) - `* * \*`, Medium (nice to have) - `* \*`, Low (unlikely to have) - `*` +{sp} + +Step 4. We fill in each empty 'gap' between any two appointments by creating a `Slot` object. -[width="59%",cols="22%,<23%,<25%,<30%",options="header",] -|======================================================================= -|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 +Each `Slot` object represents a single time period on a single date. It has three attributes: + -|`* * *` |user |add a new person | +* `LocalDate date` - the date of this time slot. +* `LocalTime start` - the start time of this time slot. +* `LocalTime end` - the end time of this time slot. -|`* * *` |user |delete a person |remove entries that I no longer need +In this `freeapp` context, these slots created represents a time period without any scheduled appointments. -|`* * *` |user |find a person by name |locate details of persons without having to go through the entire list +.Slots created to fill in empty time slots +image::dg-appointment/freeapp4.png[width="800"] -|`* *` |user |hide <> by default |minimize chance of someone else seeing them by accident +[NOTE] +Slots will only be created for timings during office hours (09:00 to 18:00). This is to prevent any possible +inconvenience caused if the user accidentally decides on a timing outside of office hours. +(Even though there will be an office hour constraint when the user eventually creates the appointment.) -|`*` |user with many persons in the address book |sort persons by name |locate a person easily -|======================================================================= +{sp} + +Step 5. We repeat Steps 3 and 4, replacing the `start` date with the remaining dates until the `end` date. +All slots created will be added into an `ArrayList` of free slots, `freeSlots`. -_{More to be added}_ +.All empty time slots filled +image::dg-appointment/freeapp5.png[width="800"] -[appendix] -== Use Cases +{sp} + +Step 6. After all the slots are added, we return `freeSlots` to the caller function `listFreeSlots()`, +to generate a `String` that represents all the free slots to be appended onto the main display of the UI. -(For all use cases below, the *System* is the `AddressBook` and the *Actor* is the `user`, unless specified otherwise) +{sp} + -[discrete] -=== Use case: Delete person +We can summarize the steps taken after the `freeapp` command is called in the Sequence Diagram below: -*MSS* +.Sequence diagram when `freeapp` is called +image::dg-appointment/freeapp_SD.png[width="800"] -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 -+ -Use case ends. -*Extensions* +===== Design Considerations +Listed below are some of the considerations we took when designing the `freeapp` command. -[none] -* 2a. The list is empty. -+ -Use case ends. +1. This feature was implemented for the convenience of the user in choosing a valid appointment slot with his/her patient. +It is more intuitive to decide on an appointment slot based on all the empty slots shown, rather than listing out +all existing appointments using `listapp` and then figuring out what timings are available from there. -* 3a. The given index is invalid. -+ -[none] -** 3a1. AddressBook shows an error message. -+ -Use case resumes at step 2. +2. We require the user to specify the search range by listing the `FORMAT` and `DATE` instead of the the `start` and +`end` dates directly, to make the command more user friendly. The user does not have to be bothered with the exact +range of dates to search, and can simply specify a rough date and be provided with information for the neighbouring +dates if the `FORMAT` given is `week` or `month`. Moreover, if the user wants to list all free slots for the whole +month, they do not have to check what the last date of the month is in order to specify the end date. -_{More to be added}_ +===== Alternatives Considered +Listed below are the methods considered to implement the `freeapp` command. -[appendix] -== Non Functional Requirements +[cols="1,2,2,3", options="header"] +|=== +|Alternative |Description |Pros | Cons +// row 1 +|*Maintain a permanent list of free slots* +|Maintain a list of free slots for a pre-determined range (e.g. next three months) instead of creating a new list +every time `appfree` is called. +|It will be quicker to search for free slots as the list is already created. We simply need to filter the list +with the given search range and print out the resulting filtered slots. +|Tedious work needs to be done to maintain this permanent list of free slots, as it has to be modified whenever an +appointment is added or deleted. + +Also, if the given search range is not within the range of this consistent list of free slots, +this list will still have to be created from scratch, defeating the purpose of maintaining this permanent list. +// row 2 +|*Generate free slots only when required* + +(Chosen implementation) +|We will only generate a list of free slots when the `freeapp` command is called. This list will be a one-time use +only and will not be stored in QuickDocs storage. +|The user is given the flexibility to specify the range of dates to list the free slots, as this list is generated +on the spot, and is not limited to the dates of a pre-determined list. +|Since the generated list of free slots is not stored, extra work will be done in generating the same free slots +when the next `freeapp` is called, that has a range of dates which overlaps the previous `freeapp` dates. +|=== -. Should work on any <> as long as it has Java `9` or higher installed. -. Should be able to hold up to 1000 persons without a noticeable sluggishness in performance for typical usage. -. 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. +We decided to implement the second method, as it is more straightforward. Here are the reasons why: + + +. The first implementation is actually just an extension of the chosen implementation as it still requires a way +to generate a list of free slots, either when QuickDocs is launched or when the user requests a search range outside +of the pre-determined list. +. The first implementation additionally requires more effort to maintain this permanent list whenever the +list of appointments is modified, which is not straightforward to implement. For example, we need a method to merge +two free slots when an appointment is deleted, and another method to split a free slot into two when an appointment +is added. +. The benefit of a permanent list of free slots is the quicker execution time of `freeapp`, which will typically +only be called a small number of times (around 10) a day, when the user books an appointment slot with his/her patient. +The total time saved on executing `freeapp` a small number of times is therefore negligible. +. QuickDocs already has plenty of data to be stored, such as appointments, consultations and medicine records. +The minimal benefits that a permanent list of free slots provide does not justify its additional storage cost. + +[[reminder-module]] +=== Reminder management module +The Reminder module provides the user with a way to keep track of future tasks, to-dos, or appointments. The reminders +will be displayed on the reminder side bar, and are colour coded as such: + +* [blue]#Blue#: Reminder for a scheduled appointment +* [red]#Red#: Reminder to stock up on a medicine +* [yellow]#Beige#: Any other personal reminders + +Listed below are the commands that the reminder module features: + +. Adding a reminder, `addrem` +. Listing reminders, `listrem` +. Deleting a reminder, `deleterem` + +These features are supported by the `ReminderManager` class, which stores all created reminders in an `ArrayList`. +On QuickDocs launch, existing reminders are read from the `quickdocs.json` file through the +<> component. + +[[rem]] +==== Reminders +A `Reminder` is a subclass of the `Slot` class, and has the following 5 fields: + +* `String title` - the title/header for this reminder +* `LocalDate date` - the date for this reminder +* `LocalTime start` - the start time for this reminder +* `LocalTime end` - the end time for this reminder +* `String comment` - any other details for this reminder + +Only the `title`, `date` and `start` attributes are compulsory fields for a reminder. + +==== Adding a reminder +The user can take note of a task to do by creating a reminder using the `addrem` command. As mentioned in the +<> section, only the `title`, `date` and `start` attributes are compulsory fields and must be +specified together with the `addrem` command. Fields `end` and `comment` are optional. Below are the steps taken +after the user executes the `addrem` command. + +. The `AddRemCommandParser` class parses the user input into 3 to 5 `Reminder` fields. +. The `Reminder` object is constructed by the parser with the given fields. +. An `AddRemCommand` object is created, providing it with the reminder object to be added. +. `AddRemCommand` executes: +* Firstly, it checks if there is a duplicate reminder using the `Model#duplicateRem()` method, which subsequently +calls `ReminderManager#hasDuplicateReminder()`. +* If there are no duplicates, `Model#addRem()` is called, providing it with the given reminder. +. `Model#addRem()` then calls `ReminderManager#addReminder()`, providing it with the given reminder. +`ReminderManager#addReminder()` will then add the reminder into the `ArrayList` of reminders stored +in `ReminderManager`. -_{More to be added}_ +[NOTE] +The added reminder will only appear on the reminder sidebar if the date of the reminder is within the range of dates +that the sidebar is currently displaying. More information can be found in the next section, <>. + +===== Automatic generation of reminders +Some reminders will be created and added automatically. + +. Appointment reminders +* When an appointment is added, a reminder for this appointment will be generated automatically. +More information can be found in the <> section. +. Low medicine alarm reminders +* The user is able to set a threshold for each medicine in the inventory by using the `alarm` command. When the quantity +of a medicine drops below its threshold, a reminder will be created to alert the user of the low medicine quantity. More +information can be found in the <> section. + +[[listrem]] +==== Listing reminders +The reminders displayed on the reminder sidebar can be filtered using the `listrem` command. + +===== Displaying reminders on the sidebar +Similar to the <> command, the user can specify a range of dates by providing a `FORMAT` +(`day`, `week`, or `month`) and a `DATE`, which means to display all reminders on the `FORMAT` (day/month/week) of `DATE`. +The following steps will then be taken: + +. The `ListRemCommandParser` class parses these two parameters into `LocalDate start` and `LocalDate end`, and creates a +`ListRemCommand` object. `start` and `end` represents the start and end dates of the range of dates of reminders +to be displayed on the sidebar. +. `ListRemCommand` executes: +* Firstly, a `ReminderWithinDatesPredicate` object will be created with the given `start` and `end` dates. +This predicate is used to update the `FilteredList` of reminders contained in the `ModelManager` class. +* Next, `Model#updateFilteredReminderList()` is called, providing it with the created predicate. +. The `FilteredList` of reminders will be updated to match the range of dates given and the reminder sidebar is updated. -[appendix] -== Glossary +[NOTE] +If the user does not specify a `FORMAT` and `DATE`, `FORMAT` will default to `week` and `DATE` will default to the +current date, meaning that the current week's reminders will be displayed. + +[[listrem_single]] +===== Display a single reminder +The `listrem` command can also be used to display the details of a single reminder onto the main display. This is useful +when the `title` or `comment` of a reminder is too long to be displayed fully on the sidebar. The user can do so by +providing the index of the reminder, as shown in the sidebar, together with the `listrem` command. + +. The `ListRemCommandParser` class parses the user input into an `Index` object, and creates a `ListRemCommand` object +consisting of the `Index` field. +. `ListRemCommand` executes: +* Firstly, it retrieves the currently displayed `List` of reminders by calling `Model#getFilteredReminderList()`. +* Next, it checks if the given `Index` is valid, checking if the `Index` is present in the current `List` +of reminders. +* If the `Index` given is valid, the reminder to be displayed is retrieved using the `List#get()` method on the `List` +of reminders. +. The information of the selected reminder is then appended onto the main display of the UI. + +Displaying a single reminder can also be done by clicking on the reminder in the sidebar with a mouse. This feature +is supported by the `ReminderListPanel` class which is a UI component for the reminder sidebar. It contains the +`Node#setOnMouseClicked()` method that takes in an `EventHandler`, which is specified to display the reminder +details when the reminder is clicked. + +==== Deleting a reminder +Deleting a reminder is simple, as the user only needs to specify the index of the reminder shown on the sidebar, +together with the `deleterem` command. The process, when `deleterem` is called, described below is similar to the +process when `listrem` is called to <>. + +. The `DeleteRemCommandParser` class parses the user input into an `Index` object, and creates a `DeleteRemCommand` +object consisting of the `Index` field. +. `DeleteRemCommand` executes: +* Firstly, it retrieves the currently displayed `List` of reminders by calling `Model#getFilteredReminderList()`. +* Next, it checks if the given `Index` is valid, checking if the `Index` is present in the current `List` +of reminders. +* If the `Index` given is valid, the reminder to be deleted is retrieved using the `List#get()` method on the `List` +of reminders. +* `Model#deleteReminder()` is called, providing it with the reminder to be deleted. +. `Model#deleteReminder()` subsequently calls `ReminderManager#delete()` to delete the given reminder from the +`ArrayList` of reminders stored in `ReminderManager`. -[[mainstream-os]] Mainstream OS:: -Windows, Linux, Unix, OS-X +[NOTE] +Reminders are automatically created when an appointment is added, as mentioned <>. However, when the +reminder of an appointment is deleted, the appointment will NOT be deleted. Use the <> command +to delete an appointment. -[[private-contact-detail]] Private contact detail:: -A contact detail that is not meant to be shared with others +=== Record & Statistics feature === +==== Current implementation ==== +The statistics command is started through the command `stats START_MMYY [END_MMYY]`. +The two MMYY corresponds to a range of dates. The end range is optional, -[appendix] -== Product Survey +=== Administrative and Statistics Module +The administrative and statistics module currently consists of 2 commands: + -*Product Name* +1. setting the consultation fee `setconsultfee` + +2. querying the statistics `statistics` + -Author: ... +This 2 commands makes use of the classes located in filepath `model\record`. -Pros: +==== Consultation fee +The consultation fee of the clinic is stored as a BigDecimal in the StatisticsManager of QuickDocs, which is loaded from the +quickdocs.json file through the <> component. The consultation fee is used for calculating +financial statistics for any ConsultationRecord objects. -* ... -* ... +==== Querying statistics +The statistics command is started through the command `stats START_MMYYYY [END_MMYYYY]`. +The two MMYYYY corresponds to a range of dates. The end range is optional, +and is defaulted to the start range by the StatisticsCommandParser if it does not exist. + -Cons: +The start date is not allowed to be before January 2019, and the end date cannot be before the start date. Hence, +QuickDocs currently does not support adding old records before January 2019 due to the implementation of the +StatisticsManager. This will be explained in the section below. + +[NOTE] +MMYYYY is a string, e.g. "012019", which stands for January 2019. It is parsed by StatisticsCommandParser into a +YearMonth object. + -* ... -* ... +==== Statistics and Record - Current Implementation -[appendix] -== Instructions for Manual Testing +The statistics class stores 6 types of information: + -Given below are instructions to test the app manually. +1. Number of consultations + +2. Medicines prescribed + +3. Symptoms diagnosed + +4. Revenue + +5. Expenditure + +6. Profit + + +Number of consultations is stored as an int, while the financial variables are stored using BigDecimals. The number of +medicines prescribed and symptoms diagnosed are stored by using a HashMap. + [NOTE] -These instructions only provide a starting point for testers to work on; testers are expected to do more _exploratory_ testing. +Implementation of additional statistics will be done through adding additional relevant variable fields. + +The implementation of Statistics and Record has 3 parts: + + +1. Creation of the Record + +2. Adding the Record + +3. Retrieving the Statistics + + +===== 1. Creation of the Record +In order for the statistics to be keep tracked of, Record objects are used to retrieve information that the +StatisticsManager will make use of. The Record class is an abstract class that only has 1 abstract method, +`toStatistics(StatisticsManager sm)`, which will generate a Statistics object. +Each child class of Record is for a specific operation in QuickDocs, where the implementation +`toStatistics(StatisticsManager sm)` will generate a Statistics object that stores relevant information pertaining to +that specific operation. The StatisticsManager is passed in to retrieve the any variable that the Record might require +to calculate the statistics, e.g., ConsultationRecord requires the consultationFee variable in StatisticsManager. + +Currently, there are only 2 child classes of Record, ConsultationRecord and MedicinePurchaseRecord. ConsultationRecords +are created when the a consultation session ends from the EndConsultCommand, while MedicinePurchaseRecord are created +when a medicine is purchased via the PurchaseMedicineCommand. The commands will create the Record, and call ModelManager's +`addRecord(record, clock)` function, which will then result in ModelManager calling StatisticsManager's `record(record, clock)` +function. The clock used is the system clock, to retrieve the current YearMonth of the Record created. The sequence diagram +below illustrates an example ConsultationRecord being created. + +.Sequence diagram for sample ConsultationRecord creation +image::RecordCreationSD.png[width="800"] + +===== 2. Adding the record +The StatisticsManager holds an ArrayList of MonthStatistics, where a MonthStatistics object contains the YearMonth, and +the Statistics object of that YearMonth. Each MonthStatistics object will be initialised with the zero Statistics object, +where all the variables are 0 or contains no elements (not null). The ArrayList starts with a MonthStatistics with the +YearMonth 2019 January, and every subsequent index will contain the MonthStatistics with the subsequent month, e.g., +the 4th index contains the MonthStatistics with YearMonth 2019 May. + +When the StatisticsManager adds a new Record by the `record(record, clock)` function, it will first retrieve the +YearMonth from the `clock` variable passed in. Next, it will update the size of the ArrayList by calling its own method +`updateListSize(clock)`, which is a wrapper for `updateListSize(YearMonth)`. Afterwards, StatisticsManager will find the +correct index of the MonthStatistics ArrayList to add the record in. In the current implementation, the record is not +actually stored. Instead, the record will be converted to a Statistics object which is then merged with the +MonthStatistics's own Statistics object. The MonthStatistics's Statistics object will then be reassigned with the newly +merged Statistics object. The sequence diagram below illustrates an example ConsultationRecord being added. + +.Sequence diagram for adding a sample ConsultationRecord +image::SDForAddingRecords.png[width="800"] + +===== 3. Retrieving the Statistics + +When the StatisticsCommand queries for the statistics for a range +of months, Logic will call the ModelManager's `getStatistics(FROM_YEARMONTH, TO_YEARMONTH)`, which then calls +StatisticsManager's `getStatistics(FROM_YEARMONTH, TO_YEARMONTH)`. StatisticsManager will convert the YearMonth objects to +their respective indexes with the StatisticsManager's `getYearMonthIndex(YearMonth)` function. + +StatisticsManager will then obtain the statistics for each of the queried months, and merge them together into a new +Statistics object. StatisticsManager will then return the Statistics back to the ModelManager, which would then return +it to the StatisticsCommand, which would then return the CommandResult with the statistics converted to a String to the +LogicManager. + +==== Statistics and Record - Design considerations + +1. The statistics are stored in months as the design only allows the doctor to query within a minimum timespan of 1 month. +Hence, it was decided that the statistics to be stored in months in a chronological order with an ArrayList for ease of +retrieval. + + +2. Currently, as QuickDOcs is developed in 2019, and there are no plans to allow the doctor to add in past records, +the first index in the array of MonthStatistics is allocated to January 2019. Any MMYYYY value before 012019 will not be +allowed. + +3. The MonthStatistics objects are stored in an ArrayList as it might be desirable for a MonthStatistics with the zero +statistics to exist (all variables 0 or no elements). Such a case might happen when the doctor goes on vacation for the +whole month. In addition, it would be easy to retrieve the MonthStatistics object of a specific MMYYYY by indexing. + +==== Statistics and Record - Alternatives Considered +The following table lists out the alternatives designs considered for implementing the storage of the Records and Statistics. +[cols="1,2a,1, 1", options="header"] +|=== +|Alternative |Description |Pros |Cons +// row 1 +|*Storing of individual records for each month (Alternative chosen)* +|Individual records are stored within the MonthStatistics, along with the Statistics. When the Statistics for a +specific month is queried, update the latest statistics and return it. +|Individual records are kept, which could potentially be used for other calculations or features. +|Storing of individual records is extremely costly in terms of space +// row 2 +|*Storing the merged statistics of all the records for each month* +|When a new record is added, it is coverted to a Statistics object which is then merged with the current Statistics object stored. +|Only one Statistics object needs to be stored, which saves a lot of storage space. +|The individual records are unable to be retrieved. However, the current implementation has no need to retrieve individual records. +|=== + +[[med]] +=== Storing medicines in inventory -=== Launch and Shutdown +One essential aspect of clinic management is about managing medicine storage of the clinic. QuickDocs' medicine management module supports customized medicine organization via a browser-like directory format. -. Initial launch +==== Current implementation -.. Download the jar file and copy into an empty folder -.. Double-click the jar file + - Expected: Shows the GUI with a set of sample contacts. The window size may not be optimum. +The current implementation takes a similar form as the Windows file browser. The user is free to determine for himself/herself how he/she wants the medicines to be arranged. -. Saving window preferences +*Code:* {repoURL}/src/main/java/quickdocs/model/medicine/MedicineManager.java[MedicineManager.java] -.. Resize the window to an optimum size. Move the window to a different location. Close the window. -.. Re-launch the app by double-clicking the jar file. + - Expected: The most recent window size and location is retained. +To organize the inventory, the following methods in `MedicineManager` are used: + -_{ more test cases ... }_ +* `addDirectory(new directory's name, path of parent directory)` -- Adds a new directory with the given name to the parent directory corresponding to the path. +* `addMedicine(name, quantity, path of parent directory, price)` -- Adds a new Medicine with given name, quantity and price to the parent directory corresponding to the path given. +* `addExistingMedicineToDirectory(medicine, path of parent directory)` -- Assuming the medicine already exists, add a reference of this medicine under the directory corresponding to the path. -=== Deleting a person +[NOTE] +The current implementation does not allow multiple medicines with the same to exist simultaneously. However, one medicine could be placed in multiple directories. -. Deleting a person while all persons are listed +[NOTE] +Both directories and medicines' names are case-insensitive. -.. Prerequisites: List all persons using the `list` command. Multiple persons in the list. -.. 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. -.. Test case: `delete 0` + - Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. -.. Other incorrect delete commands to try: `delete`, `delete x` (where x is larger than the list size) _{give more}_ + - Expected: Similar to previous. +From the initial empty state of the storage, the users could arrange their storage in these following ways: -_{ more test cases ... }_ +1. The initial empty storage consists of an empty directory named as "root". The user can then add directories and medicines into the storage. -=== Saving data +2. The `MedicineManager` keeps a list of sorted unique medicine in the inventory. -. Dealing with missing/corrupted data files +3. The user could add a new directory via `adddirec` command by specifying the path of the directory he/she wants to add into and the name of new directory. -.. _{explain how to simulate a missing/corrupted file and the expected behavior}_ +4. The user could add new/existing medicine to a specific directory via the "addMed" command. -_{ more test cases ... }_ +* 1. If there already exists a medicine with the same name in the storage, and the quantity and price is not specified in the command arguments, the existing medicine will be placed in the directory specified. + +* 2. Otherwise, a new medicine with the specified name, quantity and price will be created and added to the specified directory. + +{nbsp} + + +Given below is an example of organizing medicine from an initial empty QuickDocs. + +Step 1: Initially, the storage only consists of an empty directory called root. + + +The list of unique medicine in `MedicineManager` is empty. + +[[medicineModule_example1]] +image::medicineModule_example1.png[width='800'] + +{nbsp} + + +Step 2: Via `adddirec root Internal`, a new directory called "Internal" is added under root. + + +The list of unique medicine is still empty. + +[[medicineModule_example2]] +image::medicineModule_example2.png[width='800'] + +{nbsp} + + +Step 3: Via a few more `adddirec` commands, the figure below is an illustration of a sample inventory's framework. + + +The list of unique medicine is still empty. + +[[medicineModule_example3.png]] +image::medicineModule_example3.png[width='800'] + +{nbsp} + + +Step 4: Now the user can add new medicines into the storage via `addmed root\Internal\General paracetamol p/40 q/50`. + + +[[medicineModule_example4]] +image::medicineModule_example4.png[width='800'] + +The list of unique medicine is also updated. + +[[listOfMedicine_example1]] +image::listOfMedicine_example1.png[width='800'] + +{nbsp} + + +Step 5: Via a few more `addmed` commands, some more new medicines are added to the inventory. The following figure shows the result after that + +[[medicineModule_example5]] +image::medicineModule_example5.png[width='800'] + +The list of unique medicine is also updated. + +[[listOfMedicine_example2]] +image::listOfMedicine_example2.png[width='800'] + +{nbsp} + + +Step 6: Now, the user found out that aspirin can also be used to treat high blood pressure and decides to put it under "Cardiology" as well. + + +Via the `addmed root\Internal\Cardiology aspirin`, a reference to the existing aspirin medicine will be placed under the "Cardiology" directory. + + +The figure below shows the result of this command. + +[[medicineModule_example6]] +image::medicineModule_example6.png[width='800'] + +However, the list of unique medicine is not changed, as now new medicine is added. + +[[listOfMedicine_example3]] +image:listOfMedicine_example2.png[width='800'] + +This six-step example illustrates the basic implementation of how medicines and directories are organized in QuickDocs. + +- - - + +When typing the directory path in the command box in the ui, QuickDocs supports intelligent suggestions about the next field. + + +After the user entered at least one `\` character to indicate he is inputting a path, the suggestion mode will be turned on. + + +The user could press Page Up / Page Down bottom to iterate to the previous or the next valid name of sub-directory or medicine in alphabetical order, given that the path given before the previous `\` character is valid. + +Using the above sample inventory as an example: + + +[[medicineModule_example7]] +image::medicineModule_example7.png[width='800'] + +* When the user types in `addmed root\`, the suggestion mode is turned on. + + +* The user may not want to type in the full name of the directories, so when he types in `addmed root\in`, he could then press Page Down to iterate to the next valid name in alphabetical order, which is "Internal". + + +* The command box is then automatically filled with `addmed root\Internal` + + +* Similarly, if the user decides to traverse to the previous valid name, he could do so by press Page Up. And the command box will automatically be filled with `addmed root\External`. + + +The figure below illustrates how this feature is implemented to make user's life more convenient. + + +[[suggestion_diagram]] +.Sequence diagram illustrating the implementation of suggestion mode +image::suggestion_diagram.png[width='800] + +- - - + +QuickDocs also supports setting alarm level for medicines. Every time a medicine's storage falls below the designated level, a reminder is thrown. + + +To convenient the users, QuickDocs allow not only threshold setting for individual medicines, but also threshold setting for directories. + + +Taking the above sample inventory as an example: + + +[[medicineModule_example8]] +image::medicineModule_example7.png[width='800'] + +Setting a threshold for a directory is effectively the same as setting the threshold for every medicine in the "subtree" of that directory. This is down by a tree-like traversal. + + +For example, `alarm root\Internal 400` command sets the alarm level of all medicine in the subtree of "Internal" directory to 400. + + +[[medicineModule_example8]] +image::medicineModule_example8.png[width='800] + +==== Design consideration + +1. The current implementation takes into consideration that the users may wish to have some freedom in determining the arrangement of medicine. + +2. When prescribing medicines, a directory system that step by step leads to the desired medicine is to the convenience of the user. + +3. By arranging the medicine by folders, it is then possible to support massive manipulation of medicine by directories. + +4. Additionally, it is impossible to expect the doctor to always remember the full name of medicines correctly. There is a need for an easier way to identify medicines to operate on besides requiring the user to type in full names every time. + +5. Considering that even a small private clinic may have a considerably large set of medicine available in their storage, massive operation on a large set of medicine should be made possible besides operations on single medicine. + +==== Alternatives considered + +The table illustrates some of the alternatives I considered during development of this medicine module, the relative advantages they have over the current implementation, and why they are not selected at the end. + +[cols="1, 1, 2a, 2a", options="header"] +|=== +|Alternative |Description |Comparative advantages |Reasons for not adopting +// row 1 +|*Store medicines as a simple ArrayList* +|When users add a new medicine, just append a new medicine to the ArrayList. + + +During operations on medicines, use name of medicine as a key to select the wanted medicine. + +| * Simpler command format: The user could type in less arguments for the same commands. + +* No maintainability issue: There is no constraint to the data structure, therefore there is no need to cross check all modules to ensure the constraints are maintained. Less likely to occur bugs. + +| * The user must type in full name of medicines correctly to identify the correct medicine, This contradicts our belief that doctors are unlikely to know the full name of all his/her medicine. + +* There is no freedom for the user to organize his/her medicine. When the user wants to view or operate on all medicine that share some common traits, he/she is not able to do so. + +// row 2 +|*Use a hash map to store the medicines* +| Use medicine name as the key and the medicine as the value. Search for the key to identify and operate on medicines. +| * Searching and identifying using names as key is more time-efficient compared to the current implementation, especially when data size is large. + + +* Simpler command format, less fields to input per command. + +| * There is no room for customized arrangement of medicine. + + +* The user need to type in the correct full name of medicines to be able to identify medicines. This adds on extra difficulties for users to use the application. + +* Does not support massive operations on similar medicines. Users need to repeat the same operations on many medicines. + +//row 3 +|*Store medicines in a list with tags* +| When adding medicines, require tags from the users so that searching and identifying medicine is easier. +| * Easier to list all medicines with the same tag. + + +* Similar structure with patient module. Less efforts needed on both the developers' side and users' side to familiarise themselves with the data structure. +| * Potential lengthy commands if a medicine is widely applicable to many situations such that it may have a lot tags. + + +* Have to either input the full name of medicine or recall the tag correctly to retrieve correct medicine. When there is a large set of medicines and tags, this alternatives provides little extra convenience as compared to the two alternatives above. + +|=== + +Since QuickDocs aims to provide the most convenient experience given a large set of medicine in a clinic inventory, the medicine management module needs to provide a model that makes both typing commands, identifying the correct medicine and massive operation possible. + + +Combined with the <>, the current design is the best way to implement all of the three. + +=== Logging + +We are using `java.util.logging` package for logging. The `LogsCenter` class is used to manage the logging levels and logging destinations. + +* The logging level can be controlled using the `logLevel` setting in the configuration file (See <>) +* The `Logger` for a class can be obtained using `LogsCenter.getLogger(Class)` which will log messages according to the specified logging level +* Currently log messages are output through: `Console` and to a `.log` file. + +*Logging Levels* + +* `SEVERE` : Critical problem detected which may possibly cause the termination of the application +* `WARNING` : Can continue, but with caution +* `INFO` : Information showing the noteworthy actions by the App +* `FINE` : Details that is not usually noteworthy but may be useful in debugging e.g. print the actual list instead of just its size + +[[Implementation-Configuration]] +=== Configuration + +Certain properties of the application can be controlled (e.g user prefs file location, logging level) through the configuration file (default: `config.json`). + +== Documentation + +We use asciidoc for writing documentation. + +[NOTE] +We chose asciidoc over Markdown because asciidoc, although a bit more complex than Markdown, provides more flexibility in formatting. + +=== Editing Documentation + +See <> to learn how to render `.adoc` files locally to preview the end result of your edits. +Alternatively, you can download the AsciiDoc plugin for IntelliJ, which allows you to preview the changes you have made to your `.adoc` files in real-time. + +=== Publishing Documentation + +See <> to learn how to deploy GitHub Pages using Travis. + +=== Converting Documentation to PDF format + +We use https://www.google.com/chrome/browser/desktop/[Google Chrome] for converting documentation to PDF format, as Chrome's PDF engine preserves hyperlinks used in webpages. + +Here are the steps to convert the project documentation files to PDF format. + +. Follow the instructions in <> to convert the AsciiDoc files in the `docs/` directory to HTML format. +. Go to your generated HTML files in the `build/docs` folder, right click on them and select `Open with` -> `Google Chrome`. +. Within Chrome, click on the `Print` option in Chrome's menu. +. Set the destination to `Save as PDF`, then click `Save` to save a copy of the file in PDF format. For best results, use the settings indicated in the screenshot below. + +.Saving documentation as PDF files in Chrome +image::chrome_save_as_pdf.png[width="300"] + +[[Docs-SiteWideDocSettings]] +=== Site-wide Documentation Settings + +The link:{repoURL}/build.gradle[`build.gradle`] file specifies some project-specific https://asciidoctor.org/docs/user-manual/#attributes[asciidoc attributes] which affects how all documentation files within this project are rendered. + +[TIP] +Attributes left unset in the `build.gradle` file will use their *default value*, if any. + +[cols="1,2a,1", options="header"] +.List of site-wide attributes +|=== +|Attribute name |Description |Default value + +|`site-name` +|The name of the website. +If set, the name will be displayed near the top of the page. +|_not set_ + +|`site-githuburl` +|URL to the site's repository on https://github.com[GitHub]. +Setting this will add a "View on GitHub" link in the navigation bar. +|_not set_ + +|`site-seedu` +|Define this attribute if the project is an official SE-EDU project. +This will render the SE-EDU navigation bar at the top of the page, and add some SE-EDU-specific navigation items. +|_not set_ + +|=== + +[[Docs-PerFileDocSettings]] +=== Per-file Documentation Settings + +Each `.adoc` file may also specify some file-specific https://asciidoctor.org/docs/user-manual/#attributes[asciidoc attributes] which affects how the file is rendered. + +Asciidoctor's https://asciidoctor.org/docs/user-manual/#builtin-attributes[built-in attributes] may be specified and used as well. + +[TIP] +Attributes left unset in `.adoc` files will use their *default value*, if any. + +[cols="1,2a,1", options="header"] +.List of per-file attributes, excluding Asciidoctor's built-in attributes +|=== +|Attribute name |Description |Default value + +|`site-section` +|Site section that the document belongs to. +This will cause the associated item in the navigation bar to be highlighted. +One of: `UserGuide`, `DeveloperGuide`, ``LearningOutcomes``{asterisk}, `AboutUs`, `ContactUs` + +_{asterisk} Official SE-EDU projects only_ +|_not set_ + +|`no-site-header` +|Set this attribute to remove the site navigation bar. +|_not set_ + +|=== + +=== Site Template + +The files in link:{repoURL}/docs/stylesheets[`docs/stylesheets`] are the https://developer.mozilla.org/en-US/docs/Web/CSS[CSS stylesheets] of the site. +You can modify them to change some properties of the site's design. + +The files in link:{repoURL}/docs/templates[`docs/templates`] controls the rendering of `.adoc` files into HTML5. +These template files are written in a mixture of https://www.ruby-lang.org[Ruby] and http://slim-lang.com[Slim]. + +[WARNING] +==== +Modifying the template files in link:{repoURL}/docs/templates[`docs/templates`] requires some knowledge and experience with Ruby and Asciidoctor's API. +You should only modify them if you need greater control over the site's layout than what stylesheets can provide. +The SE-EDU team does not provide support for modified template files. +==== + +[[Testing]] +== Testing + +=== Running Tests + +There are three ways to run tests. + +[TIP] +The most reliable way to run tests is the 3rd one. The first two methods might fail some GUI tests due to platform/resolution-specific idiosyncrasies. + +*Method 1: Using IntelliJ JUnit test runner* + +* To run all tests, right-click on the `src/test/java` folder and choose `Run 'All Tests'` +* To run a subset of tests, you can right-click on a test package, test class, or a test and choose `Run 'ABC'` + +*Method 2: Using Gradle* + +* Open a console and run the command `gradlew clean allTests` (Mac/Linux: `./gradlew clean allTests`) + +[NOTE] +See <> for more info on how to run tests using Gradle. + +*Method 3: Using Gradle (headless)* + +Thanks to the https://github.com/TestFX/TestFX[TestFX] library we use, our GUI tests can be run in the _headless_ mode. In the headless mode, GUI tests do not show up on the screen. That means the developer can do other things on the Computer while the tests are running. + +To run tests in headless mode, open a console and run the command `gradlew clean headless allTests` (Mac/Linux: `./gradlew clean headless allTests`) + +=== Types of tests + +We have two types of tests: + +. *GUI Tests* - These are tests involving the GUI. They include, +.. _System Tests_ that test the entire App by simulating user actions on the GUI. These are in the `systemtests` package. +.. _Unit tests_ that test the individual components. These are in `quickdocs.ui` package. +. *Non-GUI Tests* - These are tests not involving the GUI. They include, +.. _Unit tests_ targeting the lowest level methods/classes. + +e.g. `quickdocs.model.PatientTest` +.. _Integration tests_ that are checking the integration of multiple code units (those code units are assumed to be working). + +e.g. `StorageManagerTest` +.. Hybrids of unit and integration tests. These test are checking multiple code units as well as how the are connected together. + +e.g. `LogicManagerTest` + +== Dev Ops + +=== Build Automation + +See <> to learn how to use Gradle for build automation. + +=== Continuous Integration + +We use https://travis-ci.org/[Travis CI] and https://www.appveyor.com/[AppVeyor] to perform _Continuous Integration_ on our projects. See <> and <> for more details. + +=== Coverage Reporting + +We use https://coveralls.io/[Coveralls] to track the code coverage of our projects. See <> for more details. + +=== Documentation Previews +When a pull request has changes to asciidoc files, you can use https://www.netlify.com/[Netlify] to see a preview of how the HTML version of those asciidoc files will look like when the pull request is merged. See <> for more details. + +=== Making a Release + +Here are the steps to create a new release. + +. Update the version number in link:{repoURL}/src/main/java/quickdocs/MainApp.java[`MainApp.java`]. +. Generate a JAR file <>. +. Tag the repo with the version number. e.g. `v0.1` +. https://help.github.com/articles/creating-releases/[Create a new release using GitHub] and upload the JAR file you created. + +[appendix] +== Product Scope + +*Target user profile*: + +* doctors operating small neighbourhood clinics in Singapore +* have minimal assistants or employees to assist with tasks +* handle the majority of the clinic's operations themselves +* prefer desktop apps over other types +* can type fast +* prefers typing over mouse input +* is reasonably comfortable using CLI apps + +*Value proposition*: allow doctors to accomplish greater management of their clinics with minimal manpower more conveniently. + +[appendix] +== User Stories + +Priorities: High (must have) - `* * \*`, Medium (nice to have) - `* \*`, Low (unlikely to have) - `*` + +[width="59%",cols="22%,<23%,<25%,<30%",options="header",] + +|=== + +|Priority |As a ... |I want to ... |So that I can... +|`* * *` |doctor |allocate appointments for patients |Prevent clashes in schedules + +|`* * *` |doctor |view patient’s contact details |Call and follow up on them + +|`* * *` |doctor |record patient particulars | register new walk-in patients to start a consultation session + +|`* * *` |doctor |record and view patients past medical records |diagnose them better for current and subsequent consultations + +|`* * *` |doctor |view the available time slots quickly | reserve an appointment slot for my patients requiring long term care. + +|`* * *` |doctor |view monthly statistics about patients, finances and inventory|make better decisions on how to run my clinic + +|`* * *` |forgetful or busy doctor |get reminders of when I am expecting patients|prepare to diagnose them + +|`* *` |busy doctor | get reminders whe my medicine is running low | refill my medicine and prevent shortages during prescriptions + +|`* *` |doctor |view my patients’ appointment details |send reminders to them + +|`* *` |doctor handling patients with chronic illnesses|view detailed information about medicine in my storage|give out prescription that tackles the patients’ symptoms better + +|`* *` |doctor |encrypt the patient and medical data |Protect sensitive information like medical history from getting stolen + +|`* *`|doctor|find out the most common symptom diagnosed|prepare enough medicine to deal with seasonal illnesses + +|`*` |doctor |export patient diagnosis and details |facilitate external providers’ medical care + +|`*` |doctor |lock the application |Prevent unauthorised accesses to the application + +|`*` |forgetful doctor |keep track of my medical license duration |renew it on time + +|`*` |newly trained doctor | search for details of a medical condition |explain to my patients better + +|=== + +[appendix] +== Use Cases + +(For all use cases below, the *System* is `Quickdocs` and the *Actor* is the `doctor`, unless specified otherwise) + +[discrete] +=== Use case: Consultation + +*MSS* + +1. User enter consultation command followed by NRIC +2. Quickdocs show prompt that indicates to user that consultation for that patient started +3. User enter diagnosis command with symptoms and assessment +4. Quickdocs indicate to user that the symptoms and assessment are recorded +5. User enter prescription command with medicine and quantity +6. Quickdocs indicate to user the medicine and quantity to be administered for current patient +7. User enter end consultation command +8. Quickdocs indicate that consultation for current patient ended ++ +Use case ends. + +*Extensions* + +[none] +* 1a. User enter invalid NRIC +[none] +** 1a1. Quickdocs alert user that no patient with entered NRIC exist to start a consultation session with ++ +Use case ends here. +[none] +* 3a. User left out symptoms or assessment when diagnosis patient +[none] +** 3a1. Quickdocs alert user that some details are left out and prompt user to modify command ++ +Use case resumes from step 3. +[none] +* 5a. User left out quantities for certain medicine +[none] +** 5a1. Quickdocs alert user that some medicine do not have quantities and prompt them to reenter command ++ +Use case resumes from step 5. ++ +[none] +* 5b. User left out quantities for certain medicine +[none] +** 5b1. Quickdocs alert user that additional quantities are provided and prompt user to reenter command ++ +Use case resumes from step 5. ++ +[none] +* 7a. User end consultation when diagnosis is not completed +[none] +** 7a1. Quickdocs alert user that the session is missing a diagnosis +** 7a2. User will resume perform step 3 and 4 since they were skipped ++ +Use case ends here. ++ +[none] +* 7b. User end consultation when prescription is not given +[none] +** 7b1. Quickdocs alert user that the session is missing a prescription +** 7b2. User will resume perform step 5 and 6 since they were skipped ++ +Use case ends here. + +[discrete] +=== Use case: View patient medical record + +*MSS* + +1. User enter command to view patient history with search criteria +2. Quickdocs show list of patient’s consultation records +3. Quickdocs prompt user to enter index +4. User enter index of record he or she wants to see +5. Quickdocs show selected patient record +6. User enters end to stop looking at record +7. Use case repeat from case 2 until user enters “end” again after step 6 +8. Quickdocs shows message to inform user he or she is no longer looking at patient records ++ +Use case ends. + +*Extensions* + +[none] +* 1a. User enter invalid index +[none] +** 1a1. Quickdocs alert user that the index is invalid +** 1a2. Quickdocs exit view patients record ++ +Use case ends here. +[none] +* 1b. No patient record created yet +[none] +** 1b2. Quickdocs alert user that no patient is created yet, suggest to create a new patient record first ++ +Use case ends. +[none] +* 1c. User enter a patient’s name that is unique in the storage ++ +Use case resumes from step 1. +[none] +* 1d. User enter a non unique patient’s name +[none] +** 1d1. Quickdocs show list of patients with the same name, and prompt index +** 1d2. User refine search criteria, either by entering index or full name of the patient ++ +Use case resumes from step 1. +[none] +* 4a. User enter invalid medical record index +[none] +** 4a1. Quickdocs alert user that index entered was invalid ++ +Use case resumes from step 3. +[none] +* 6a. User enter command apart from “end” +[none] +** Quickdocs prompt user that command was invalid and inform them that “end” will exit view ++ +Use case resumes from step 5. + +[discrete] +=== Use case: Allocating an appointment slot + +*MSS* + +1. User enter command to list all free slots, specifying the date and viewing format +2. User discusses and agrees on an appointment slot with patient +3. User search for patient’s NRIC by viewing patient records with search criteria +4. User enter command to add appointment slot, specifying patient's NRIC, date, start and end time +5. Quickdocs displays a successful message, showing the details of the newly created appointment slot ++ +Use case ends + +*Extensions* + +[none] +* 1a. User enters invalid keyword when specifying date or format +[none] +** 1a1. Quickdocs displays an error message ++ +Use case resumes from step 1. +[none] +* 1b. User does not enter any keywords +[none] +** 1b1. Quickdocs displays all free slots for the current week ++ +Use case resumes from step 2. +[none] +* 4a. User enters invalid NRIC, date or time +[none] +** 4a1. Quickdocs displays an error message ++ +Use case resumes from step 4. +[none] +* 5b. Quickdocs displays an error message, showing clashes in timing with another appointment slot ++ +Use case resumes from step 4. +[none] +* 5b. Patient wants to change appointment slot timing +[none] +** 5b1. User enter command to delete appointment slot, specifying date and start time +** 5b2. Appointment specified deleted ++ +Use case resumes from step 1. + +[discrete] +=== Use case: Viewing clinic statistics + +*MSS* + +1. User requests to view statistics, specifying the start and end YearMonths +2. Quickdocs displays the statistics ++ +Use case ends. + +*Extensions* + +[none] +* 1a. User requests to view statistics with valid arguments. ++ +Use case resumes at step 2. +* 1b. User requests to view statistics with invalid arguments. +[none] +** 1b1. Quickdocs shows an error message. ++ +Use case ends. + +[discrete] +=== Use case: Setting alarm threshold for medicines + +*MSS* + +1. User enters "alarm" and the path to the interested medicine and the desired alarm quantity. +2. QuickDcos set the threshold for the medicine accordingly. +3. QuickDocs refreshes the reminder sidebar if needed. ++ +Use case ends. + +*Extensions* + +[none] +* 1a. User types in a path leading to a directory. +* 2a. All medicines under the subtree of that directory is set to the desired quantity. ++ +Use case resumes at step 3. + + +[appendix] +== Non Functional Requirements + +. Should work on any <> as long as it has Java `9` or higher installed. +. Should be able to hold up to 1000 patients and their consultation records without a noticeable sluggishness in performance for typical usage. +. 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. +. Response time for commands should be below 3 seconds so that clinical operations can be expedited +. Commands should be easy to pick up for novice users, they can remember it more quickly and start using them immediately +. Commands should be made convenient for expert users as well +. Data stored can be easily transferred to another device installed with Quickdocs for operation continuation. +. Data stored, especially patient records and particulars, need to be encrypted to prevent unauthorised access and misuse. + +[appendix] +== Glossary + +[[mainstream-os]] Mainstream OS:: +Windows, Linux, Unix, OS-X + +[[private-contact-detail]] Private contact detail:: +A contact detail that is not meant to be shared with others + +[[patient_records]] Patient Records:: +A data entry consisting of a patient's particulars, which includes name, NRIC, email address, home address, +gender, contact number, date of birth and the list tags assigned to him or her. + +[[consultation_records]] Consultation Records:: +A data entry consisting of the diagnosis given and medicine prescribed for a single patient during a consultation +session with the doctor. + +[appendix] +== Instructions for Manual Testing + +Given below are instructions to test the app manually. + +[NOTE] +These instructions only provide a starting point for testers to work on; testers are expected to do more _exploratory_ testing. + +=== Adding patient records + +. Add a new <> + +.. Prerequisites: patient to be added must not have the same NRIC as an existing patient's NRIC, the +`listpat` command can be used to check if there are conflicting NRIC +.. Test case 1: `addpat n/Mohd Hamiru Bin Hamza r/S9876543C a/1 Tampines Street e/mhbh@gmail.com c/92344321 g/M d/1998-07-06` + + Expected: Patient with name "Mohd Hamiru Bin Hamza" with NRIC S9876542C added +.. Test case 2: `add n/Nurul Huda Binte Hamza r/S9876543C a/3 Tampines Street e/nhbh@gmail.com c/93124432 g/F d/1998-11-02` + + Expected: Patient will not be added, an error message will be shown to indicate that a patient with the same NRIC existed in the records +.. Other incorrect `addpat` commands to try: `addpat` with missing parameters such as gender and date of birth, `addpat` with name containing of symbols, +`addpat` with invalid NRIC values (8 numbers or invalid last letters). + +=== Editing patient records + +. Edit the fields of an existing patient record + +.. Prerequisites: use the `listpat` command to check for existing patients to be edited, or simply add one using `addpat` +if there isn't any. You need at least 2 patients to perform this manual test. +.. Test case1: `editPat EXISTING_NRIC n/Peter Tan g/M d/1990-09-01` + + Expected: selected patient will have his or her name changed to "Peter Tan", gender changed to "M" and date of birth changed + to 1990-09-01. +.. Test case2: `editPat EXISTING_NRIC n/Perry Tan r/ANOTHER_EXISTING_NRIC` + + Expected: An error message will be shown to indicate that the edit to the current patient will cause it to have a conflicting NRIC + with another patient's NRIC +.. Test case 3: `editPat EXISTING_NRIC` + + Expected: An error message will be shown to indicate that there is nothing to edit for the current selected patient +.. Other incorrect `editpat` commands to try: name with numbers, `editpat` with a non existent NRIC, `editPat` without an NRIC (without prefix) + +=== Starting a consultation + +. Start a consultation for a registered patient + +.. Prerequisites: Existing patients must already be stored in QuickDocs, use `listpat` to check for both existing and non existing + patients' NRIC +.. Test case 1: `consult r/EXISTING_NRIC` + + Expected: A message to indicate the start of the consultation session will be shown at the main display. A label will be displayed + at the bottom right, displaying the message along with the patient's NRIC as well. +.. Test case 2: After test case 1, enter `consult r/ANOTHER_EXISTING_NRIC` + + Expected: An error message will be shown to alert you that there is already an ongoing consultation session. +.. Test case 3: exit QuickDocs, and then enter `consult r/NON_EXISTING_NRIC` + + Expected: since there are no patients with the NRIC, an error message will be shown to indicate that the consultation session cannot start + for a non-existing patient + +=== Ending a consultation + +. End a consultation session after providing the prescription and diagnosis. A consultation session can only end after + both the diagnosis and prescription have been recorded. + +.. Prerequisites: consultation session must already been started for a patient, medicine to be assigned for prescription is already + stored in QuickDocs +.. Test case 1: after starting a consultation, `endconsult` immediately + + Expected: Error message will be shown to alert you that a diagnosis have not been provided. +.. Test case 2: add the diagnosis and then `endconsult` after test case 1 + + Expected: Error message will be shown to alert you that a prescription have not been given to end the consultation session. +.. Test case 3: add the prescription and then `endconsult` after test case 2 + + Expected: A message indicating that the current consultation session have ended. The ongoing session label at the bottom right + of QuickDocs is also removed. + +=== Adding and removing an appointment + +. Add an appointment for a registered patient + +.. Prerequisites: +... Appointment you are adding must not have conflicting timing with other existing appointments. + You can use the `freeapp` command to find an available time slot for any date. +... The patient allocated to the appointment must be registered in QuickDocs. You can use the `listpat` command to + search for existing patients to be allocated the new appointment. +.. Test case 1: `addapp r/EXISTING_NRIC d/2019-10-23 s/16:00 e/17:00 c/Weekly checkup` + + Expected: A message to indicate that the appointment was successfully added will be shown on the main display, + together with the appointment details. A reminder for this appointment will also be created and you can see it in the reminder + sidebar on the right, if the date of the appointment is in the current week. +.. Test case 2: After test case 1, enter `addapp r/EXISTING_NRIC d/2019-10-23 s/16:30 e/17:30 c/Weekly checkup` + + Expected: since this new appointment a clash in timing with the appointment added in test case 1, an error message will be shown + to indicate this conflict and the appointment will not be added. +.. Test case 3: `addapp r/EXISTING_NRIC d/23-10-2019 s/16:00 e/17:00 c/Weekly checkup` + + Expected: Error message will be shown to alert you that the date is in the wrong format, as the correct format is YYYY-MM-DD. +.. Other incorrect `addapp` commands to try: date without `-` between month and day, invalid start and end time (start time must be before + end time; appointment must be within office hours of 9am to 6pm), any missing prefixes, any missing parameters. + +. Delete an existing appointment +.. Prerequisites: appointment to be deleted must already be added in QuickDocs. You can use `listapp` command to list the existing appointments + for a given date. We will assume that the appointment in Test case 1 for adding an appointment has been added. +.. Test case 1: `deleteapp d/2019-10-23 s/16:00` + + Expected: A message to indicate that the appointment was successfully deleted will be shown on the main display. + The reminder created for this appointment will also be deleted. If this reminder was displayed on the reminder sidebar, it will be removed. +.. Test case 2: `deleteapp d/2030-10-23 s/16:00` + + Expected: Assuming that there is no appointment on the given date and time, you will be informed through the error message shown. +.. Other incorrect `deleteapp` commands to try: date without `-` between month and day, invalid start time (start time must be + within office hours of 9am to 6pm), any missing prefixes, any missing parameters. + +=== Querying Statistics + +. Querying the statistics +.. Prerequisites: Your system clock should be synchronised with the current internet time. +.. Test case 1: `stats 012019` + + Expected: The statistics for the month January 2019 should be listed, regardless if there has been any records added. +.. Test case 2: `stats 122018` + + Expected: An error message will be shown, indicating that the command format is wrong. +.. Test case 3: `stats 012019 022019` + + Expected: The statistics for the range of month January 2019 to February 2019 should be listed, regardless if there has been any records added. +.. Test case 4: `stats 012999` + + Expected: An error message will be shown, indicating that the command format is wrong. +.. Other incorrect `stats` commands to try: end date before start date, invalid MMYYYY numbers (e.g., 000000, 132019). + All these commands should lead to an error message being shown. + +=== Adding a new directory for medicine storage + +[NOTE] +The following tests assume the tester starts with an empty storage. + +. Add a new directory under the root directory + +.. Test case 1: `adddirec root NEW_DIRECTORY` + + Expected: A success message confirming the successful addition will be shown in the main display. A new directory called 'NEW_DIRECTORY' will be added to root directory, which is verifiable via `listmed root` command. +.. Test case 2:`adddirec root NEW DIRECTORY` + + Expected: As the new directory's name includes a white space which is not allowed, no new directory will be added and an error message will show up. +.. Test case 3:`adddirec root\Internal NEW_DIRECTORY` + + Expected: As the tester starts with an empty storage, there is no existing directory at root\Internal. No new directory will be added, and an error message will show up saying "No directory is found at the given path." +.. Other incorrect `adddirec` commands to try: commands with missing new directory name + +=== Adding a medicine into a directory + +[NOTE] +The following tests assume that `root\Internal\General` is a valid path, i,e there is already a directory 'General' under directory 'Internal' under root directory. + +. Add a new medicine into storage +.. Prerequisites: There is no medicine with the same name as the to-add medicine in the entire storage. +.. Test case 1: `addmed root\Internal\General paracetamol p/2.4 q/40` + + Expected: A new medicine called 'paracetamol' with initial quantity 40 and price 2.4 will be added under 'Flu' directory. A success message confirming this addition will show in the main display. +.. Test case 2: `addmed root\Internal\General paracetamol p/2.4 q/40` when there is an existing medicne named 'paracetamol' in the storage. + + Expected: The addition will fails, as there already exists a medicine with same name. No change will occur and an error message will show up. +.. Test case 3: `addmed root\Internal\General paracetamol p/2.4 q/34.5` + + Expected: The addition will fail. As the quantity in QuickDocs are in terms of units, no fractional number is allowed in the quantity field. An error message will show up. +.. Other incorrect `addmed` commands to try: missing any prefixes, invalid path +. Add an existing medicine into a different directory +.. Prerequisites: There is already an existing medicine called 'paracetamol' somewhere in the storage. +.. Test case 1: `addmed root\Internal\General paracetamol` + + Expected: A new reference to the same medicine will be added to root\Internal\General. A success message will show in the main display. +.. Test case 2: `addmed root\Internal\General paracetamol` when there is no existing medicine with name 'paracetamol' + + Expected: No change will occur. As there is no such existing medicine, an error message will show up. +.. Test case 3: `addmed root\Internal\General paracetamol` when root\Internal\General already contains a reference to paracetamol. + + Expected: As the same medicine cannot be placed into the same directory twice, no change will happen. An error message will show up. +.. Other incorrect `addmed` command to try: When there is a subdirectory under the same directory with the same name as the to-add medicine. diff --git a/docs/UserGuide.adoc b/docs/UserGuide.adoc index 7e0070e12f49..07f55910b017 100644 --- a/docs/UserGuide.adoc +++ b/docs/UserGuide.adoc @@ -1,4 +1,4 @@ -= AddressBook Level 4 - User Guide += QuickDocs - User Guide :site-section: UserGuide :toc: :toc-title: @@ -12,249 +12,1403 @@ ifdef::env-github[] :tip-caption: :bulb: :note-caption: :information_source: endif::[] -:repoURL: https://github.com/se-edu/addressbook-level4 +:repoURL: https://github.com/CS2103-AY1819S2-W09-4/main/tree/master -By: `Team SE-EDU` Since: `Jun 2016` Licence: `MIT` +By: `W09-4` Since: `Feb 2019` Licence: `MIT` == Introduction -AddressBook Level 4 (AB4) is for those who *prefer to use a desktop app for managing contacts*. More importantly, AB4 is *optimized for those who prefer to work with a Command Line Interface* (CLI) while still having the benefits of a Graphical User Interface (GUI). If you can type fast, AB4 can get your contact management tasks done faster than traditional GUI apps. Interested? Jump to the <> to get started. Enjoy! +QuickDocs is an all in one solution where doctors can have greater control in facilitating patient consultations, organizing appointments, and monitoring financial and inventory records in private clinics. + +Let us explore how QuickDocs can facilitate and expedite clinical operations in your neighbourhood clinic. == Quick Start . Ensure you have Java version `9` or later installed in your Computer. -. Download the latest `addressbook.jar` link:{repoURL}/releases[here]. -. Copy the file to the folder you want to use as the home folder for your Address Book. -. Double-click the file to start the app. The GUI should appear in a few seconds. -+ -image::Ui.png[width="790"] -+ -. Type the command in the command box and press kbd:[Enter] to execute it. + -e.g. typing *`help`* and pressing kbd:[Enter] will open the help window. -. Some example commands you can try: - -* *`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. -* **`delete`**`3` : deletes the 3rd contact shown in the current list -* *`exit`* : exits the app - -. Refer to <> for details of each command. +. Download the jar file and put it into a folder you desire. +. Type a command into the command box and press enter to execute it. +[NOTE] +For the full list of commands, you can head to <> + +=== Interacting with QuickDocs + +.The user interface for QuickDocs +image::ui_explanation.png[width="600"] + +. This is the *command input* box , this is where you can type and execute the various commands +QuickDocs possesses to perform clinical operations. +. If you make mistakes and enter an invalid command, instructions on how you can rectify your commands +will be shown in this *input feedback* box. +. After the execution of your command, you can view the results on the *main display* area +. You can view your upcoming appointments for the day, or urgent reminders that you made earlier in the +*reminders and appointments* panel +* Appointments are coloured blue, medicine alarms are coloured red +and other reminders are coloured beige. +. During a consultation session, QuickDocs will also indicate that a session is ongoing through +this *current consultation label* + +[[Notation]] +== Notations used + +Here are some notations used in this user guide: + +[TIP] +This is a tip. Useful information pertaining to the features will be written here. +[NOTE] +This is a note. Additional information that further explains a feature will be written here. + + +`command` This is a user input to the application. It is formatted with a gray background. Example: `help` [[Features]] == Features +*QuickDoc's Command Format* + + +`command prefix/VALUE [prefix/OPTIONAL VALUE] ...` + +//* Commands are in lowercase, you can view all of them using the help command. +* Command represent the operation you want to perform such as adding patients, listing medicine inventory and so on. +* Values are the mandatory user input required for the execution of commands. +* Optional values are non-compulsory values that can be added in certain commands. They can be added multiple times after the command, prefixes and values. + +[TIP] +All commands have aliases, which can be used to execute the same command with fewer keystrokes e.g. `statistics` can be replaced by `stats`. -==== -*Command Format* +{nbsp} + -* Words in `UPPER_CASE` are the parameters to be supplied by the user e.g. in `add n/NAME`, `NAME` is a parameter which can be used as `add n/John Doe`. -* Items in square brackets are optional e.g `n/NAME [t/TAG]` can be used as `n/John Doe t/friend` or as `n/John Doe`. -* Items with `…`​ after them can be used multiple times including zero times e.g. `[t/TAG]...` can be used as `{nbsp}` (i.e. 0 times), `t/friend`, `t/friend t/family` etc. -* Parameters can be in any order e.g. if the command specifies `n/NAME p/PHONE_NUMBER`, `p/PHONE_NUMBER n/NAME` is also acceptable. -==== +[[help,Help]] +=== Viewing help -=== Viewing help : `help` +Whenever you want to find out in-depth explanation on how to execute a specific command, you can always refer to this +user guide by entering the `help` command on QuickDocs. Format: `help` -=== Adding a person: `add` +{nbsp} + + +''' + +=== Patient Management +The Patient Management module features commands involving the registration, update, searching +and removal of patient records in QuickDocs. + +Using just a few keystrokes, you can manage your patient records in a more organized and efficient manner. + +''' + +[[addpat, Add patient]] +==== Adding patient: `addpat` + +You can register new patients and start storing their records in QuickDocs using the `addpat` command. + +Patient details that can be stored include: name, NRIC, email, address, gender, date of birth, contact number. + +[TIP] +You can add a tag to a patient to indicate his or her long term illnesses. You can add multiple tags to the patient to ease searching. + + +{sp}+ + +*Format:* `addpat n/NAME r/NRIC e/EMAIL a/ADDRESS g/GENDER d/DATE OF BIRTH c/CONTACT [t/tag]...` + + +*Alias{nbsp}{nbsp}{nbsp}{nbsp}:* `ap` +[TIP] +The order of input does not matter as long as all the necessary fields are captured. + + +{sp}+ + +*Examples:* + +* `addpat n/Gary Goh r/S7332803I g/M d/1973-04-01 a/20 Upper Changi Road c/92347654 e/ggoh@gmail.com t/highbloodpressure t/diabetes` + +* `addpat n/Chan Mei Hua r/S8865281I d/1998-05-09 g/F c/92341221 a/2 Simei Street e/cmh@gmail.com` + +{sp}+ +*Result:* + +After entering the `addpat` command, QuickDocs will show the details of the recently added patient on the +main display as demonstrated in the diagram below: + +.The main display after addpat command +image::ap_after.png[width="600"] + +''' + +[[editpat, Edit patient]] +==== Editing patient : `editpat` + +If you made a mistake when registering a patient, fret not! You can always make changes to the patient record with the `editpat` command. +All the fields in the record can be edited, including the NRIC and tags. + +[NOTE] +The tags entered during an edit patient command will override all the tags a patient record currently possess. + +{sp} + +*Format:* `editpat NRIC [n/NAME] [a/Address] [r/NRIC] …` + + +*Alias{nbsp}{nbsp}{nbsp}{nbsp}:* `ep` + + +[NOTE] +The first NRIC to specify the record does not require a prefix. + +The prefixes used for the `editpat command` is the same as the ones used in the <> section. + + +{sp} + + +*Example:* + + +* `editpat S7332803I n/Gary Doh e/gdoh@gmail.com` + +This changes the email and name of the patient with NRIC: S7332803I to Gary Doh and gdoh@gmail.com respectively. + +{sp} + +*Result:* + + +After the `editpat` is performed, you can view the changes made to the patient record in the main display. + +.Result of an edit command, on a patient's name and email +image::ep_after.png[width="600"] + +{sp} + +Since every citizen, permanent residents or foreigners in Singapore have their unique NRIC / FIN numbers, duplicate NRIC are not +allowed in QuickDocs. + +When you edit a patient's NRIC/FIN and there is an existing patient with that NRIC, the current edit +will not be executed. You will also be notified of the detection of duplicated NRIC entries through the input feedback box, +in the manner shown in the following picture. + +.Error message shown when QuickDocs detected a duplicate NRIC in the patient records +image::ep_conflict.png[width="600"] + +''' + +[[listpat, List patients]] +==== List patient details : `listpat` + +To view a patient's detail, you can use the `listpat` command. You can search and filter patient records +by their names, NRIC and even their tags. + +* QuickDocs only support one filter per search currently. +* If more than one filter is entered (i.e. `listpat n/NAME r/NRIC`, only the first search criteria will be used +(in this case, name) + +For example, if you want to check the patient's NRIC, you can always turn to `listpat` to help narrow down your search. +Entering listpat with `r/S92` and QuickDocs will present you with patient records whose NRIC starts with S92. + +* You can similarly filter down your search using tags or parts of a patient name in the same manner. +* A specific patient's record can be displayed by calling `listpat` with the full name or full nric + +[NOTE] +Each patient record stored within QuickDocs have a record number, known as an *INDEX*. The +Index can also be used to narrow down patient searches to a single record. + +{sp} + +*Format:* `listpat [PREFIX / KEYWORD] …` + + +*Alias{nbsp}{nbsp}{nbsp}{nbsp}:* `lp` + + +{sp} + + +*Examples and Results:* + +* `listpat` + + +If no keywords are entered, `listpat` will simply display the first 50 patients record stored. + +.executing the listpat command without any matching criteria +image::lp_noargs.png[width="600"] + +{sp} + +* `listpat n/P` + + +Suppose you want to find a patient record whose name starts with "P", you can use `listpat` along +with a sequence of characters to search for the specific patient record you are interested in. + +.Listing patients with matching name +image::lp_name_after.png[width="600"] + +{sp} + +* `listpat r/S92` + + +You can narrow down your patient search by providing a NRIC sequence as well. For example, +in the figure below, you can retrieve all the patients whose NRIC starts with "S92". + +.Listing patients with matching NRIC +image::lp_nric_after.png[width="600"] + +{sp} + +After narrowing down your search, you will be able to use the specific NRIC number of a particular patient to view the in-depth +patient particulars. + +.Showing specific patient's record using the full NRIC +image::lp_nric_specific.png[width="600"] + +{sp} + +* `listpat t/diabetes` + + +The tags you assigned to each patient can also be used to expedite the `listpat` command. For example, diabetic patients can be +listed using the `listpat` command along with "t/diabetes" value.' + +[NOTE] +Tags specified for the search must be full sequences (i.e. Diabetes, highbloodpressure/, etc). QuickDocs do not +permit partial tag search as of v1.4. + +.Listing patients by specific tag +image::lp_tag_after.png[width="600"] + +{sp} + +* `listpat 5` + +Did you notice that when QuickDocs display multiple patient records during filtering, each record starts with +a number? This is the index of the patient record, indicating the position the record is stored within QuickDoc's patient record storage. + +You can make use of the index number to view specific patient records as well. + +.Patient search using record index +image::lp_index.png[width="600"] + +''' + +[[deletepat, Delete patient]] +==== Deleting patient `deletepat` + +When a patient is no longer visiting your clinic (i.e. moved residency), you can choose to remove their details from QuickDocs. + +{sp} + +*Format:* `deletepat r/NRIC` + + +*Alias{nbsp}{nbsp}{nbsp}{nbsp}:* `dp` + + +{sp} + + +*Example:* `deletepat r/S7338031I` + +{sp} + +*Result:* + +By specifying the NRIC along with the `deletepat` command, you will delete the patient with that specific NRIC. If you have difficulty finding the specific NRIC, you can refer to the <> command section to help you retrieve the patient's NRIC. + +.Deleting patient records using the specific NRIC +image::dp_after.png[width="600"] + +{nbsp} + + +''' + + + +=== Consultation Management + +With QuickDocs, you no longer need to navigate to different menus just to record symptoms of the patient's illness, or assigning medicine to tackle +the patient's current condition. + +Using the Consultation Management module, you can now handle the prescription and diagnosis stages of each consultation session +in one go. + +'''' + +[[consult, Start consultation]] +==== Start consultation: `consult` + +After a patient is registered, whenever he or she visits your clinic, you can start recording the diagnosis and medicine prescribed by starting a +consultation session on QuickDocs. + +Consultation is started after the NRIC of the patient is provided, you can revisit the <> section to help identify the NRIC +of specific patients + + +[NOTE] +QuickDocs only permit one ongoing consultation session at any given time. The +<> and <> steps must be completed before a consultation can end. Otherwise +you can always use the <> command to exit an ongoing session without saving any details recorded. + +{sp} + +*Format:* `consult r/NRIC` + + +*Alias{nbsp}{nbsp}{nbsp}{nbsp}:* `c` + + +{sp} + + +*Example:* + +* `consult r/S9876542C` + + +*Result:* + +Once you started a consultation session for the patient, a message will be displayed on the main display area to indicate that the consultation session +have started for the current patient. + +.Starting a consultation session on QuickDocs +image::consult_after.png[width="600"] + +{sp}+ +A text indicating that the consultation session have started for the particular patient will also +appear at the bottom right corner of QuickDocs, so that you will be aware that a current consultation session is ongoing even after +subsequent commands. + +{sp}+ +[NOTE] +While a consultation session is ongoing, It does not mean that you are limited to just entering the consultation commands. You can still enter command that are not within the consultation module (i.e. + listing patients, checking the medicine inventory, etc). + + +''' + +[[diagnose, Diagnose patient]] +==== Diagnosing a patient: `diagnose` + +After the consultation session has began, you can start recording the patient's ailments. The various symptoms the patients have can be recorded down, +along with the final assessment of the illness the patient is currently having. + +To complete the diagnosis, the record must have one assessment and at least one symptom. + +[NOTE] +This command requires you to have a consultation session already active. See: <>. + +{sp}+ +*Format:* `diagnose s/SYMPTOM [s/SYMPTOM] ... a/ASSESSMENT` + + +*Alias{nbsp}{nbsp}{nbsp}{nbsp}:* `d` + + +{sp}+ + +*Example:* + +* `diagnose s/runny nose s/sore throat s/phlegmy cough a/influenza` + + +The order of symptoms and assessment does not matter here. You record as many symptoms per diagnosis. + +{sp} + +*Results:* + +After entering the command, QuickDocs will show the symptoms and assessment recorded on the main display as demonstrated +in the following diagram. + +.Result of the diagnose command +image::diagnose_after.png[width="600"] + +{sp} + +If you made a mistake when entering the diagnosis, you can always rectify it by re-entering the `diagnose` command with the correct +symptoms and assessment. This will replace the current erroneous diagnosis with the one you have just entered. + +.Editing the diagnosis +image::diagnose_edit_after.png[width="600"] + +{sp}+ +[TIP] +Whenever you make a mistake entering a command, you can always press the UP and DOWN buttons on your keyboard to cycle through the past +commands you have entered into QuickDocs. This allow you to easily navigate to the erroneous command you have entered, make changes and then re-enter +the command again to rectify your errors. + +''' + +[[prescribe, Prescribe medicine]] +==== Prescribing medicine for a patient: `prescribe` + +After you are done recording the symptoms and assessing the illness of the patient, you can start prescribing medicine to your patient. + +For each medicine prescribed, the quantity must be specified. Like the <> command, you can always reenter the command to override +the current prescription should there be any errors made. + +A minimum of one medicine and one quantity is required to record a prescription entry. The order of quantity entered corresponds to the order of the medicine entered. + + +[NOTE] +This command requires you to have a consultation session already active. See: <>. + +[NOTE] +The medicine to be prescribed must be present in the inventory and its quantity must be sufficient for +the assignment during the presciption stage. + +Check out the <> section for information on adding medicine to the inventory, +and <> section on the steps to check a medicine's amount. + +{sp} + +*Format:* `prescribe m/MEDICINE [m/MEDICINE] ... q/QUANTITY [q/QUANTITY]` + +or{nbsp}{nbsp}{nbsp}{nbsp}{nbsp}{nbsp}{nbsp}{nbsp} *:* `prescribe m/MEDICINE q/QUANTITY [m/MEDICINE] [q/QUANTITY] ...` + + +*Alias{nbsp}{nbsp}{nbsp}{nbsp}:* `p` + + +{sp} + + +*Example:* + +* `prescribe m/penicillin q/1 m/Afrin spray q/1 m/ibuprofen q/2` + + Prescription now consist of 1 unit of penicillin, 1 unit of afrin spray and 2 units of ibuprofen. + +* `prescribe m/penicillin m/Afrin spray m/ibuprofen q/1 q/1 q/2` + + Same as above example, quantity ordered based on medicine order. + +{sp} + +*Results:* + +After the medicine-quantity pairings are entered, the prescription to address the patient's current condition will be displayed on the main display area of QuickDocs. Changes can still be made to the prescription as long as the consultation session is still ongoing. + +.Prescribing medicines to tackle the patient's current conditions +image::prescription_after.png[width="600"] + +''' + +[[endconsult, End consultation]] +==== End consultation: `endconsult` + +Once the diagnosis and prescription are finalized, you can end the consultation with a simple `endconsult` command. +No further changes to the diagnosis and prescription details can be made after this point. + +{sp} + +*Format:* `endconsult` + + +*Alias{nbsp}{nbsp}{nbsp}{nbsp}:* `ec` + +{sp} + +*Result:* + +Once you entered the `endconsult` command, a message to indicate the end of the consultation session is also shown on the main display area. +The label for the ongoing consultation session is also removed after the command is entered. + +Most importantly, the consultation record (diagnosis and prescription) is now stored. + +.Results of an endconsult command +image::endconsult.png[width="600"] + +''' + +[[abort, Abort consultation]] +==== Aborting a consultation session: `abort` + +If you started a consultation session by mistake, or the patient does not seemed to require a consultation, +you can always abort the consultation session without providing the diagnosis and prescription details. + +{sp} + +*Format:* `abort` + + +*Alias{nbsp}{nbsp}{nbsp}{nbsp}:* `ab` + +{sp} + +*Result:* + +When you enter the `abort` command, a message is also shown on the main display area to indicate that the current session is aborted. +The label for the ongoing consultation session is also removed. + +.Results of an abort command +image::abortconsult.PNG[width="600"] + +''' + +[[listconsult, List consultations]] +==== List consultation: `listconsult` + +Whenever you want to revisit a specific consultation record, you can filter it down by the patient before narrowing it down +to the specific session. + +You can use QuickDocs to list out all the past consultation sessions of a single patient by first specifying his or her NRIC, +and then view the consultation details by passing in the session's index. + + +{sp} + +*Format:* `listconsult INDEX` + +or{nbsp}{nbsp}{nbsp}{nbsp}{nbsp}{nbsp}{nbsp}{nbsp} *:* `listconsult r/NRIC` + + +*Alias{nbsp}{nbsp}{nbsp}{nbsp}:* `lc` + + +{sp} + + +*Examples and Results:* + +* `listconsult r/S9876542C` + + +Specifying the NRIC after the `listconsult` command will display a list of consultation records belonging to +the patient with the specified NRIC. + +Note that the indexes are prepended on each of the past consultation records listed. + +.Displaying past visits of a particular patient +image::listconsult_after.png[width="600"] + +{sp}+ + +* `listconsult 1` + + +image::listconsult_index.png[width="600"] + +You can then call `listconsult` again with the consultation record's index to view the specific consultation session details. QuickDocs will display the time of the consultation, and also the diagnosis and prescription given to the patient. + +.Revisiting a specific consultation session +image::listconsult_index.png[width="600"] + +{sp}+ + +''' + +=== Medicine Management +You can manage your medicine storage all by your wishes! QuickDocs allows you to organise your medicines in a directory format much like the folders in Windows OS! + + +You can design and name your directories by functionalities, pricing, classifications or anything in your mind! + + +Initially, the medicine storage only contains one root directory named as "root" by default. All the rest is then up to you to decide! + +[TIP] +Through QuickDocs, you could place the same medicine under different directories. For example, medicine paracetamol could be placed under "fever" and "headache" simultaneously. + +[TIP] +Following the same logic, you may not be able to store two different medicine with the same name in QuickDocs. All instances of medicine with the same name will refer to the same medicine. + +- - - + +==== Suggestion mode in command typing + +In QuickDocs, as medicines are stored in directories format, you will need to key in the path to your interested directory/medicine from time to time. + + +For your convenience, QuickDocs actually provides a quick cut to key in these paths so that you do not need to type in every character by yourself! + + +As long as you are typing a command in medicine module and you have typed the name of the first parent directory followed by a `\` character, the suggestion mode is automatically turn on! + + +So what is suggestions mode? Why is that useful? The following example will illustrate the convenience it brings to you. + +* Suppose you have a directory 'Flu' under directory 'General' under directory 'Internal' under the root directory 'root' as illustrated below: + +[[suggestion_ug1]] +.Display of detailed information +image::suggestion_ug1.png[width='600'] + +* Now you want to type this command `listmed root\Internal\General\Flu` , which is a command to see detailed information about that directory, from scratch. + +* What you can do is to first type `listmed root\` to trigger the suggestion mode. + +.To trigger the suggestion mode +image::suggestion_ug2.png[width='600'] + +* You can then press the page-down key to iterate through all subdirectories under 'root'. The input box will automatically be filled for you. + +.After pressing Page Down once +image::suggestion_ug3.png[width='600'] + +* In this example, you can arrive at `listmed root\Internal` in just two Page Down keys. + +.After pressing Page Down twice +image::suggestion_ug4.png[width='600'] + +* Doing this recursively at every stage, you can quickly arrive at the desired `listmed root\Internal\General\Flu`. + +* Suppose now you want to view the detailed information about a medicine called 'guaifenesin' under directory 'Flu'. You entered `listmed root\Internal\General\Flu\guai` only to realize that you forget the spelling of guaifenesin. + +* Do not worry! You can press page-up / page-down as well. Page up will bring you to the last valid sub-directory / medicine name as compared to your input according to alphabetic order. Page down will bring you to the next valid input. + +.Before pressing Page Up / Page Down +image::suggestion_ug5.png[width='600'] + +.After pressiong Page Up, brings you to the last valid medicine: diphenhydramine +image::suggestion_ug6.png[width='600'] + +.After pressing Page Down, brings you to your desired : guaifenesin +image::suggestion_ug7.png[width='600'] + +{nbsp} + + +In short, instead of typing out every characters, you can iterate through your directories fast and easy when typing commands using the page-up and page-down keys! + +[NOTE] +In QuickDocs, names of directories and medicines are case-insensitive. So what appears in the suggestions may be in different cases from the actual name, but they will work the same. + +- - - + +[[adddirec, Add directory]] +==== Add new directory into storage: `adddirec` + +By providing the path pointing to a specific directory, you can add a new directory under that directory. + + +The new directory will be an empty directory whose name is up to you to decide. + +[NOTE] +The new directory's name should not contain white spaces or "\" characters. + +{sp} + +*Format:* `adddirec [PATH OF THE PARENT DIRECORY] [NAME OF NEW DIRECTORY]` + + +*Alias{nbsp}{nbsp}{nbsp}{nbsp}:* `ad` + +{sp} + + +*Example:* + + +* `adddirec root fever` followed by `adddirec root\fever oral` + + +*Result:* + + +These commands add a new directory called fever to root, and then add another new directory called oral under fever. + + +There will be success messages confirming every successful addition of directories. + + +[[adddirec_after]] +.Result of sample add directory command +image::adddirec_after.png[width="600"] -Adds a person to the address book + -Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]...` +- - - + +[[addmed, Add medicine]] +==== Add medicine into storage: `addmed` + +Through this command, you can either add a new medicine into the storage, or to place an existing medicine under another directory. + + +{sp} + + +You can add a new medicine by specifying where it should go to, its initial quantity and its price + + +*Format:* `addmed [PATH OF DIRECTORY TO ADD TO] [MEDICINE_NAME] [q/QUANTITY] [p/PRICE]` + + +*Alias{nbsp}{nbsp}{nbsp}{nbsp}:* `am` + +[NOTE] +Directories and medicines under the same directory are now allowed to share the same name (case-insensitive). + +{sp} + + +*Example:* + + +`addmed root\fever paracetamol q/50 p/9.99` + +*Result:* + + +After you enter the command, if it is executed successfully, a confirmation message will appear. + +[[addmed_newmedicien_after]] +.Success message of sample addmed command +image::addmed_newmedicien_after.png[width="600"] + +{sp} + + +[NOTE] +If you entered a medicine name that already exists in the storage in this format, a error message will pop up. + + +[[addmed_newMedicine_existing]] +.Error message of adding an existing medicine in wrong format +image::addmed_newMedicine_existing.png[width="600"] + +{sp} + + +You can also place an existing medicine into a directory. + + +*Format:* `addmed [PATH] [MEDICINE_NAME]` + +{sp} + + +*Example:* + + +`addmed root\headache paracetamol` + + +Assuming there already exists a medicine called paracetamol in the storage, you can place this medicine under root\headache via this command. + + +*Result:* + + +After you enter the command, if it is executed successfully, a confirmation message showing the detailed information of that medicine will appear. + +[[addmed_existing]] +.Success message of sample addmed command +image::addmed_existing.png[width="600"] + +{sp} + + +[NOTE] +If you entered a medicine name that has not existed in the storage in this format, an error message will pop up. + + +[[addmed_existing_nonExisting]] +.Error message for adding new medicine in wrong format +image::addmed_existing_nonExisting.png[width="600"] + +In this case, no medicine called aspirin has yet existed in the storage. + +- - - + +[[listmed, List medicines]] +==== List medicine storage: `listmed` + +Whenever you want to see what is inside your medicine storage, you can always use this command to view the detailed information about any directory or medicine through this command. + + + +{sp} + + +*Format:* `listmed [PATH]` + +*Alias{nbsp}{nbsp}{nbsp}{nbsp}:* `lm` + +{sp} + + +*Example:* + + +`listmed root\fever` + +*Result:* + + +You will be able to view a detailed description of all the medicines and directories under root\fever. + +[[listmed_after]] +.Response for a sample listmed command viewing a directory +image::listmed_after.png[width="600"] + +{sp} + + +*Example:* + + +`listmed root\fever\paracetamol` + +*Result:* + + +You will be displayed a detailed description of medicine paracetamol which is placed under root\fever. + +[[listmed_med_after]] +.Response for a sample listmed command viewing a medicine +image::listmed_med_after.png[width="600"] + +- - - + +[[alarm, Set alert threshold for medicine]] +==== Setting automatic notification for low stock: `alarm` + +To further ease your management of the clinic, QuickDocs could automatically alert you should any of your medicine is running low in storage! + + +All you need to do is to set an alarm level for the medicine you are concerned with. + + +[TIP] +In addition, QuickDocs can make your life even easier! You can set an alarm level for a directory so that every medicine placed under that directory and all its sub-directories will immediately have that alarm level. + +{sp} + + +*Format:* `alarm [PATH OF DIRECTORY OR MEDICINE]` + + +{sp} + + +*Example:* + +`alarm root\fever 60` + +{sp} + + +*Result:* + +As the path points to a medicine, the sample command set the alarm level for aspirin to 60. + + +As the storage only has 50 units of aspirin, a reminder is shown on the right. + +[[alarm_medicine_after]] +.Response for the sample alarm command +image::alarm_medicine_after.png[width='600'] + +{sp} + + +*Example:* + +`alarm root\high_blood_pressure\aspirin` + +{sp} + + +*Result:* + +As the path points to a directory, the sample command set the alarm level for all medicine under "fever" directory and all its subdirectories to 60. + + +As the medicine paracetamol under "fever" has only 50 units, which is below the limit, a reminder is shown on the right. + +[[alarm_directory_after]] +.Responses for the sample alarm command +image::alarm_directory_after.png[width='600'] + +- - - + +[[buymed, Buy a medicine]] +==== Recording purchases of medicine: `buymed` + +As a doctor running your own clinic, you will certainly purchase medicine from time to time to keep your medicine storage updated. + + +This command allows you to record every purchase you made in the simplest way! + [TIP] -A person can have any number of tags (including 0) +You can access the medicine via two ways. You can either provide the full path to the medicine, or provide the name of medicine only. + +*Format:* `buymed [MEDICINE_NAME] [AMOUNT] [UNIT_COST]` + +or{nbsp}{nbsp}{nbsp}{nbsp}{nbsp}{nbsp}{nbsp}{nbsp} *:* `buymed [PATH_OF_MEDICINE] [AMOUNT] [UNIT_COST]` + +*Alias{nbsp}{nbsp}{nbsp}{nbsp}:* `bm` + +{sp} + + +*Example:* + +`buymed aspirin 20 4.3` + + +{sp} + + +*Result:* + +Using this command, you update the amount of aspirin in the storage form 34 to 54. + + +As the amount of aspirin now exceed the minimum threshold set, which is 50, the reminder for low storage automatically disappears. + -Examples: +The following figures illustrates the states of QuickDocs before and after you executing that command. -* `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` +[[buymed_before]] +.The state before purchasing medicine +image::buymed_before.png[width='600'] -=== Listing all persons : `list` +{sp} + -Shows a list of all persons in the address book. + -Format: `list` +[[buymed_after]] +.The state after purchasing medicine. Note that the reminder is gone. +image::buymed_after.png[width='600'] -=== Editing a person : `edit` +{sp} + -Edits an existing person in the address book. + -Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]...` +[NOTE] +To make your life easier, QuickDocs automatically records every purchase you made via this command so that you can check your clinic's statistics any time. + +- - - + +[[setprice, Set price for a medicine]] +==== Setting Price for a medicine: `setprice` + +Through QuickDocs, you have hundred percent freedom to set the price of your medicine via this simple command! + -**** -* 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 price you set will be used in your prescriptions. -Examples: +[TIP] +Similar to buymed command, you can either provide the full path to your interested medicine or the name of it only. -* `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. +*Format:* `setprice [PATH_OF_MEDICINE] [PRICE]` + +or{nbsp}{nbsp}{nbsp}{nbsp}{nbsp}{nbsp}{nbsp}{nbsp} *:* `setprice [NAME_OF_MEDICINE] [PRICE]` -=== Locating persons by name: `find` +*Alias{nbsp}{nbsp}{nbsp}{nbsp}:* `sp` -Finds persons whose names contain any of the given keywords. + -Format: `find KEYWORD [MORE_KEYWORDS]` +{sp} + -**** -* The search is case insensitive. e.g `hans` will match `Hans` -* The order of the keywords does not matter. e.g. `Hans Bo` will match `Bo Hans` -* Only the name is searched. -* Only full words will be matched e.g. `Han` will not match `Hans` -* Persons matching at least one keyword will be returned (i.e. `OR` search). e.g. `Hans Bo` will return `Hans Gruber`, `Bo Yang` -**** +*Example:* -Examples: +`setprice aspirin 10.50` -* `find John` + -Returns `john` and `John Doe` -* `find Betsy Tim John` + -Returns any person having names `Betsy`, `Tim`, or `John` +{sp} + -=== Deleting a person : `delete` +*Result:* -Deletes the specified person from the address book. + -Format: `delete INDEX` +This command sets the unit price of aspirin to $10.50. You will then have no need to key in the price during each prescription, this price will automatically be used. -**** -* Deletes the person at the specified `INDEX`. -* The index refers to the index number shown in the displayed person list. -* The index *must be a positive integer* 1, 2, 3, ... -**** +[[setprice_after]] +.Response for the sample setprice command +image::setprice_after.png[width='600'] -Examples: +- - - -* `list` + -`delete 2` + -Deletes the 2nd person in the address book. -* `find Betsy` + -`delete 1` + -Deletes the 1st person in the results of the `find` command. +==== View the detailed information about any medicine: [coming in V2.0] +Using external APIs, the users could view more detailed information about any medicine. -=== Selecting a person : `select` +''' +=== Appointments and Reminders +The Appointment and Reminder module give you more control over your busy schedule, featuring commands such as adding, removing, +and searching appointments and reminders. There is also a command to list out all free appointment timings to help you choose +your appointment timings more wisely. -Selects the person identified by the index number used in the displayed person list. + -Format: `select INDEX` +You will never forget about any appointments or tasks again! + -**** -* Selects the person and loads the Google search page 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, ...` -**** +''' +[[addapp]] +==== Adding appointments: `addapp` +After a consultation session, you may want to schedule a follow-up appointment with your patient. You can do so +with `addapp` to create an appointment with an existing patient in QuickDocs to add to your schedule. -Examples: +[TIP] +The `<>` command may be useful for you to first list out all free appointment timings for a given range of dates +before choosing an appropriate appointment timing! -* `list` + -`select 2` + -Selects the 2nd person in the address book. -* `find Betsy` + -`select 1` + -Selects the 1st person in the results of the `find` command. +[NOTE] +To ensure that you do not forget about any future appointments, QuickDocs will automatically create a reminder for +the added appointment. You may not notice this reminder as it will only appear on your reminder sidebar closer to the date +of the actual appointment (on the week of the appointment date). -=== Listing entered commands : `history` +{sp} + +*Format:* `addapp r/NRIC d/DATE s/START e/END c/COMMENT` + -Lists all the commands that you have entered in reverse chronological order. + -Format: `history` +*Alias{nbsp}{nbsp}{nbsp}{nbsp}:* `aa` + [NOTE] -==== -Pressing the kbd:[↑] and kbd:[↓] arrows will display the previous and next input respectively in the command box. -==== +The format for a valid date is strictly `YYYY-MM-DD` (including the dashes) and a valid time is strictly `HH:MM` (including the colon). +If unsure, you may refer to the example below. + +{sp} + +*Example and results:* + +* `addapp r/S6394980I d/2019-07-23 s/16:00 e/17:00 c/Weekly checkup` + + +This adds an appointment allocated to the patient with NRIC S6394980I, on 23rd July 2019, from 4pm to 5pm. You can +include any comments or notes you want to this appointment, which is 'Weekly checkup' in this case. + +If the addition of the appointment was successful, QuickDocs will show the details of the added appointment on the +main display as demonstrated in the diagram below: + +.The main display after adding an appointment +image::ug-app_rem/addapp_success.png[width="600"] + +{sp} + + +The addition of the appointment could have failed as there is a conflict in timing with another existing appointment. +QuickDocs will display a message in the input feedback box notifying you of this error, as demonstrated in the diagram below. + +.Adding an appointment that has conflicting timing with an existing appointment +image::ug-app_rem/addapp_clash.png[width="600"] +{sp} + -// tag::undoredo[] -=== Undoing previous command : `undo` -Restores the address book to the state before the previous _undoable_ command was executed. + -Format: `undo` +Another reason for failing to add an appointment could be due to the appointment timing being outside of office hours +which is from 9am to 6pm. QuickDocs will alert you of this error, as demonstrated in the diagram below: + +.Adding an appointment that is not within the office hours, from 9am to 6pm +image::ug-app_rem/addapp_officehour.png[width="600"] + +''' +[[listapp]] +==== Listing appointments: `listapp` +You can list all past and future appointments that you have added using the `listapp` command. The appointments will be ordered and +listed, with the earliest appointment at the top and the latest at the bottom. You can filter the appointments +you want to see either by specifying a `FORMAT` and a `DATE`, or by specifying an existing patient's `NRIC`. + +{sp} + +*Format:* `listapp f/FORMAT d/DATE` + +or{nbsp}{nbsp}{nbsp}{nbsp}{nbsp}{nbsp}{nbsp} *:* `listapp r/NRIC` + +*Alias{nbsp}{nbsp}{nbsp}{nbsp}:* `la` + [NOTE] -==== -Undoable commands: those commands that modify the address book's content (`add`, `delete`, `edit` and `clear`). -==== +The valid keywords for `FORMAT` are only `day`, `week`, or `month`. + +{sp} + +*Examples:* -Examples: +* `listapp` + -* `delete 1` + -`list` + -`undo` (reverses the `delete 1` command) + +By default, `listapp` will list all appointments scheduled in the current week if there are no keywords provided. + +{sp} + -* `select 1` + -`list` + -`undo` + -The `undo` command fails as there are no undoable commands executed previously. +* `listapp f/day d/2019-07-23` + -* `delete 1` + -`clear` + -`undo` (reverses the `clear` command) + -`undo` (reverses the `delete 1` command) + +Lists all appointments scheduled on 23rd July 2019. + +{sp} + -=== Redoing the previously undone command : `redo` +* `listapp f/week d/2019-07-23` + -Reverses the most recent `undo` command. + -Format: `redo` +Lists all appointments scheduled on the given week of 23rd July 2019, which is from 22nd to 28th July. A week starts on a +Monday and ends on a Sunday. + +{sp} + -Examples: +* `listapp f/month d/2019-07-23` + -* `delete 1` + -`undo` (reverses the `delete 1` command) + -`redo` (reapplies the `delete 1` command) + +Lists all appointments scheduled in the month of the given date, July 2019. + +{sp} + -* `delete 1` + -`redo` + -The `redo` command fails as there are no `undo` commands executed previously. +* `listapp r/S9123456A` + -* `delete 1` + -`clear` + -`undo` (reverses the `clear` command) + -`undo` (reverses the `delete 1` command) + -`redo` (reapplies the `delete 1` command) + -`redo` (reapplies the `clear` command) + -// end::undoredo[] +Lists all appointments allocated to the patient with NRIC S9123456A, if this patient is registered in QuickDocs. + +{sp} + -=== Clearing all entries : `clear` +*Result:* -Clears all entries from the address book. + -Format: `clear` +The filtered appointments will be ordered by time and listed on the main display of QuickDocs, as demonstrated in the diagram below: -=== Exiting the program : `exit` +.Listing appointments scheduled on the week of 23rd July 2019. +image::ug-app_rem/listapp_week.png[width="600"] -Exits the program. + -Format: `exit` +''' +[[deleteapp]] +==== Deleting appointments: `deleteapp` +If the patient would like to change the appointment date, or if there is a mistake in the appointment details, you can +delete the existing scheduled appointment with `deleteapp` and then create a new appointment. +*Deletion of an appointment is final!* -=== Saving the data +[NOTE] +QuickDocs will automatically delete the corresponding reminder created for the deleted appointment, if it still exists, +so you don't have to worry about any discrepancies with your reminder sidebar! + +{sp} + +*Format:* `deleteapp d/DATE s/START` + -Address book data are saved in the hard disk automatically after any command that changes the data. + -There is no need to save manually. +*Alias{nbsp}{nbsp}{nbsp}{nbsp}:* `da` + + +[NOTE] +Since there cannot be any overlapping appointment timings, you are able to uniquely identify any appointment with +just the `DATE` and `START` time of the appointment. -// tag::dataencryption[] -=== Encrypting data files `[coming in v2.0]` +{sp} + +*Example and results*: -_{explain how the user can enable/disable data encryption}_ -// end::dataencryption[] +* `deleteapp d/2019-07-23 s/16:00` + -== FAQ +This command will delete the appointment created on 23rd July 2019 with a start time of 4pm, if it exists. If successful, +QuickDocs will display the details of the deleted appointment on the main display, as demonstrated in the diagram below: -*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 Address Book folder. +.Deleting an existing appointment in QuickDocs successfully +image::ug-app_rem/deleteapp_success.png[width="600"] +{sp} + + +If the given appointment was not found, QuickDocs will display an error message on the input feedback display, +as demonstrated in the diagram below. Use the `<>` command to find existing appointments for a given date! + +.Deleting a non-existent appointment in QuickDocs +image::ug-app_rem/deleteapp_failure.png[width="600"] + +''' +[[freeapp]] +==== List free appointment slots: `freeapp` +[[fa, freeapp]] +Unsure of what appointment timings are available in your schedule? Instead of using `<>` to display all existing +appointments, use `freeapp` instead to display a more intuitive list of free appointment slots - +QuickDocs does the thinking for you! + +The free appointment slots will be ordered and listed, with the earliest date at the top and the latest at the bottom. +You can filter the free slots you want to see by specifying a `FORMAT` and a `DATE`. + +{sp} + +*Format:* `freeapp f/FORMAT d/DATE` + + +*Alias{nbsp}{nbsp}{nbsp}{nbsp}:* `fa` + + +[NOTE] +Similar to `<>`, the valid keywords for `FORMAT` are only `day`, `week`, or `month`. + +{sp} + +*Examples:* + + +* `freeapp` + + +By default, `freeapp` will list all free appointment slots for the next month if there are no keywords provided. + +{sp} + + +* `freeapp f/day d/2019-07-23` + + +Lists all free appointment slots on 23rd July 2019. + +{sp} + + +* `freeapp f/week d/2019-07-23` + + +Lists all free appointment slots on the given week of 23rd July 2019, which is from 22nd to 28th July. A week starts on a +Monday and ends on a Sunday. + +{sp} + + +* `freeapp f/month d/2019-07-23` + + +Lists all free appointment slots in the month of the given date, July 2019. + +{sp} + + +*Result:* + + +The free appointment slots will be ordered by date and time and will be listed on the main display of QuickDocs, +as demonstrated in the diagram below: + +.Listing free appointment slots on the week of 23rd July 2019. +image::ug-app_rem/freeapp_week.png[width="600"] + +''' +[[addrem]] +==== Adding reminders: `addrem` +Need to take note of a task to do in the future? You can create a reminder for an event or task with `addrem`, +and the reminder will show up on your sidebar closer to the date of the event (the week of the reminder date). +Never forget about your tasks ever again! + +{sp} + +*Format:* `addrem t/TITLE d/DATE s/START [e/END] [c/COMMENT]` + + +*Alias{nbsp}{nbsp}{nbsp}{nbsp}:* `ar` + + +[NOTE] +`END` time and `COMMENT` are optional fields for a reminder. + +{sp} + +*Example:* + +* `addrem t/Check financial records d/2019-07-31 s/18:00 e/18:30 c/Monthly check` + + +This command will create a reminder to 'Check financial records' which is a 'Monthly check', on 31st July 2019 +from 6pm to 6:30pm. +{sp} + + +*Result:* + +If the addition of the reminder was successful, QuickDocs will show the details of the added reminder on the +main display as demonstrated in the diagram below: + +.The main display after adding a reminder +image::ug-app_rem/addrem_success.png[width="600"] + +''' +[[listrem]] +==== Listing reminders: `listrem` +To view your past or future reminders on the sidebar, you can do so using the `listrem` command. +The reminders will be ordered and listed, with the earliest reminder at the top and the latest at the bottom. +You can filter the reminders you want to see by specifying a `FORMAT` and a `DATE`. + +[NOTE] +On startup, QuickDocs will automatically display the reminders for the current week on the sidebar. + +Are the titles or comments too long to be displayed fully on the sidebar? You can also use `listrem` to display all the +details of a single reminder by specifying its `INDEX` that is shown on the sidebar. + +[TIP] +If you are tired of typing, you could also click the reminder +with your mouse and all its details will be displayed as well! + +{sp} + +*Format:* `listrem f/FORMAT d/DATE` + +or{nbsp}{nbsp}{nbsp}{nbsp}{nbsp}{nbsp}{nbsp} *:* `listrem i/INDEX` + +*Alias{nbsp}{nbsp}{nbsp}{nbsp}:* `lr` + + +{sp} + +*Examples and results:* + +* `listrem` + + +By default, `listrem` will list all reminders in the current week if there are no keywords provided. + +{sp} + + +* `listrem f/day d/2019-07-31` + + +Lists all reminders on 31st July 2019. + +{sp} + + +* `listrem f/week d/2019-07-31` + + +Lists all reminders on the given week of 31st July 2019, which is from 29th July to 4th August. A week starts on a +Monday and ends on a Sunday. + +{sp} + + +* `listrem f/month d/2019-07-31` + + +Lists all reminders in the month of the given date, July 2019. + +{sp} + + +The filtered reminders will be ordered by date and time and will be listed on the reminder sidebar, +as demonstrated in the diagram below: + +.Listing reminders scheduled on the week of 31st July 2019. +image::ug-app_rem/listrem_week.png[width="600"] + +{sp} + + +* `listrem i/3` + + +In the screenshot above, the details of the third reminder is not fully displayed! +Use the above command to display, on the main display, the details of the third reminder listed on the sidebar as +demonstrated in the diagram below: + +.Displaying details of the third reminder. +image::ug-app_rem/listrem_single.png[width="600"] + + +''' +[[deleterem]] +==== Deleting reminders: `deleterem` +If you would like to clear out your reminder sidebar after a task is completed, you can do so using the `deleterem` command. +Each reminder on the sidebar are identified with an `INDEX`, which you can use to specify the reminder to be deleted. +*Deletion of a reminder is final!* + +{sp} + +*Format:* `deleterem INDEX` + + +*Alias{nbsp}{nbsp}{nbsp}{nbsp}:* `dr` + + +[NOTE] +We do not need to include the `INDEX` prefix, `i/`, as opposed to the `<>` command. This is +because `deleterem` only accepts one parameter, `INDEX`, while `<>` can accept more than one. + +{sp} + +*Example and results:* + +* `deleterem 3` + + +This command will delete the third reminder listed on the sidebar, if it exists. If successful, QuickDocs will display +the details of the deleted reminder on the main display, as demonstrated in the diagram below: + +.Deleting an existing reminder in QuickDocs successfully +image::ug-app_rem/deleterem_success.png[width="600"] + +{sp} + + +If the given `INDEX` is not present in the sidebar, QuickDocs will display an error message on the input feedback display, +as demonstrated in the diagram below. + +.Invalid index provided to deleterem +image::ug-app_rem/deleterem_failure.png[width="600"] + +''' +[[ntime]] +==== Timing of notification: `ntime` [coming in v2.0] +Did you know that QuickDocs displays a pop-up notification for you when a reminder is approaching? +You can adjust how long before the actual reminder start time to receive this notification using `ntime`. + +{sp} + +*Format:* `ntime t/TIME` + + +*Alias{nbsp}{nbsp}{nbsp}{nbsp}:* `nt` + + +[NOTE] +`TIME` is specified in minutes. + +{sp} + +*Example:* + +* `ntime t/30` + + +Receives pop-up notifications 30 minutes before the actual reminder start time. + +''' +[[sendemail]] +==== Sending email reminders to patients: `sendemail` [coming in v2.0] +With QuickDoc's reminder sidebar, you will never forget about your appointments - however your patients still might! +To prevent this, `sendemail` allows you to send an auto-generated email reminder to patients regarding an approaching +appointment date. + +{sp} + +*Format:* `sendemail d/DATE s/START` + + +*Alias{nbsp}{nbsp}{nbsp}{nbsp}:* `se` + + +{sp} + +*Example:* + +* `sendemail d/2019-07-23 s/16:00` + + +Sends an email to the patient allocated to the appointment on 23rd July 2019 at 4pm. + +''' +=== Administration and Statistics +The Administration and Statistics module feature commands that are related to any Administrative tasks such as setting +the clinic's consultation fee, and the Statistics command to get information about your clinic recorded through the use of QuickDocs. + +''' +[[setconsultfee, Set consultation fee]] +==== Setting consultation fee: `setconsultfee` +This command allows you to change your consultation fee in QuickDocs to what you charge for each consultation. +Setting up of your consultation fee is necessary for QuickDocs to generate accurate financial statistics pertaining +to your consultation sessions. Once your consultation fee is set, QuickDocs will remember it for you. Hence, you +only need to enter this command once, until you decide to change your consultation fee. + + +[NOTE] +The default consultation fee in QuickDocs in S$30.00. + +{sp}+ +*Format:* `setconsultfee AMOUNT` + + +*Alias{nbsp}{nbsp}{nbsp}{nbsp}:* `setfee` + +*Examples:* + + +* `setconsultfee 30` + +Sets the consultation fee to $30.00. + +* `setconsultfee $32.50` + +Sets the consultation fee to $32.50. + + +''' +[[statistics, View statistics]] +==== Viewing statistics: `statistics` + +You are able to view the statistics of the clinic for a specific month, or between a range of months. + +This command will show you 6 items in order: + + +. Number of consultations + +. Most common medicines prescribed + +. Most common symptoms diagnosed + +. Revenue + +. Expenditure + +. Profit + + +[NOTE] +In order for the calculated statistics pertaining to consultation finances to be accurate, you must have already +entered your clinic's consultation fee beforehand. See: <>. + +[NOTE] +When there is a tie between the most commonly prescribed medicine, or most commonly diagnosed symptoms, all of them +will be listed. + + +{empty} + +*Format:* `statistics FROM_MMYYYY [TO_MMYYYY]` + + +*Alias{nbsp}{nbsp}{nbsp}{nbsp}:* `stats` + + +[NOTE] +If `TO_MMYYYY` is not specified, it will be defaulted to be equal to `FROM_MMYYYY`. + + +*Examples and Results:* + +* `statistics 012019` + +View the statistics for January 2019. + +* `stats 012019 042019` + +View the statistics from January 2019 to April 2019. + +In the image below, the *display* area will return the statistics of the queried range of months. + + +1. The result will show the range of months that were queried, followed by the 6 items mentioned earlier in order. +2. When there are more than one most common symptom diagnosed, QuickDocs will list them all out. + +.Result example for command `statistics 012019 042019` +image::statistics_sample.png[width="600"] + +''' + +==== Generate prescription list [coming in v2.0] + +The prescription list can be printed out for the pharmacists to expedite medicine preparation. The pharmacists can refer +to the printed prescription list and prepare the medicines for the patient to collect after payment. + +''' + +==== Generate medical certificate: [coming in v2.0] + +You can print out a customised medical certificate for the patient, listing the symptoms and also the number of sick leave days. + +''' + +==== Generate invoice: [coming in v2.0] + +Generates the invoice for a given prescription. After selecting the patient, select the consultation to view in detail and then select the desired prescription. + +{empty} + + +== Frequently Asked Questions (FAQ) +[qanda] +*How do I save the data after I add or change something?*:: + Saving is done automatically in QuickDocs. Whenever a command that adds, modifies, or deletes data is executed, the result of the execution is automatically saved. Hence, there is no need to save manually. + +{empty} + == Command Summary +The *Command Summary* lists down all the possible commands that can be entered in QuickDocs. This section is meant to be +a reference point for ease of search. The words in `command` format are the input to QuickDocs. +[NOTE] +This is the summary of all the possible commands for QuickDocs. For detailed explanations of each command, you should head to <>, or click on the links of each commands. + +*General Commands* + + +* *<>* : `help` + + +*Patient Management* + + +* *<>* : `addpat` or `ap` + +* *<>* : `editpat` or `ep` + +* *<>* : `listpat` or `lp` + +* *<>* : `deletepat` or `dp` + + +*Consultation Management* + + +* *<>* : `consult` or `c` + +* *<>* : `diagnose` or `d` + +* *<>* : `prescribe` or `p` + +* *<>* : `endconsult` or `ec` + +* *<>* : `abort` or `ab` + +* *<>* : `listconsult` or `lc` + + +*Medicine Management* + + +* *<>* : `adddirec` or `ad` + +* *<>* : `addmed` or `am` + +* *<>* : `listmed` or `lm` + +* *<>* : `alarm` + +* *<>* : `buymed` or `bm` + +* *<>* : `setprice` or `sp` + + +*Appointment and Reminders* + + +* *<>* : `addapp` or `aa` + +* *<>* : `listapp` or `la` + +* *<>* : `deleteapp` or `da` + +* *<>* : `freeapp` or `fa` + +* *<>* : `addrem` or `ar` + +* *<>* : `listrem` or `lr` + +* *<>* : `deleterem` or `dr` + +* *<>* : `ntime` or `nt` *[coming in v2.0]* + +* *<>* : `sendemail` or `se` *[coming in v2.0]* + + +*Administration and Statistics* + -* *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` -* *Select* : `select INDEX` + -e.g.`select 2` -* *History* : `history` -* *Undo* : `undo` -* *Redo* : `redo` +* *<>* : `statistics` or `stats` + +* *<>* : `setconsultfee` or `setfee` + diff --git a/docs/diagrams/MedicinManager.png b/docs/diagrams/MedicinManager.png new file mode 100644 index 000000000000..16d2dc8935f3 Binary files /dev/null and b/docs/diagrams/MedicinManager.png differ diff --git a/docs/diagrams/Model.png b/docs/diagrams/Model.png new file mode 100644 index 000000000000..afdb17cc2ba5 Binary files /dev/null and b/docs/diagrams/Model.png differ diff --git a/docs/diagrams/QuickDocs & AppointmentManager.png b/docs/diagrams/QuickDocs & AppointmentManager.png new file mode 100644 index 000000000000..34ec97c273f2 Binary files /dev/null and b/docs/diagrams/QuickDocs & AppointmentManager.png differ diff --git a/docs/diagrams/QuickDocsDiagrams.pptx b/docs/diagrams/QuickDocsDiagrams.pptx new file mode 100644 index 000000000000..2bd17f5318c4 Binary files /dev/null and b/docs/diagrams/QuickDocsDiagrams.pptx differ diff --git a/docs/diagrams/RecordManager.png b/docs/diagrams/RecordManager.png new file mode 100644 index 000000000000..d070f84c7b43 Binary files /dev/null and b/docs/diagrams/RecordManager.png differ diff --git a/docs/diagrams/ReminderManager.png b/docs/diagrams/ReminderManager.png new file mode 100644 index 000000000000..a41ef3b6539c Binary files /dev/null and b/docs/diagrams/ReminderManager.png differ diff --git a/docs/images/AppointmentManager_diagram.png b/docs/images/AppointmentManager_diagram.png new file mode 100644 index 000000000000..da92ac46d4fc Binary files /dev/null and b/docs/images/AppointmentManager_diagram.png differ diff --git a/docs/images/Architecture2.png b/docs/images/Architecture2.png new file mode 100644 index 000000000000..8729214b7e0d Binary files /dev/null and b/docs/images/Architecture2.png differ diff --git a/docs/images/ConsultationManager_diagram.png b/docs/images/ConsultationManager_diagram.png new file mode 100644 index 000000000000..290748b0d217 Binary files /dev/null and b/docs/images/ConsultationManager_diagram.png differ diff --git a/docs/images/LogicClassDiagram.png b/docs/images/LogicClassDiagram.png index f4ecf65b3193..be410db77051 100644 Binary files a/docs/images/LogicClassDiagram.png and b/docs/images/LogicClassDiagram.png differ diff --git a/docs/images/LogicClassDiagram_old.png b/docs/images/LogicClassDiagram_old.png new file mode 100644 index 000000000000..f4ecf65b3193 Binary files /dev/null and b/docs/images/LogicClassDiagram_old.png differ diff --git a/docs/images/MedicineManager.png b/docs/images/MedicineManager.png new file mode 100644 index 000000000000..16d2dc8935f3 Binary files /dev/null and b/docs/images/MedicineManager.png differ diff --git a/docs/images/MedicineManager_diagram.png b/docs/images/MedicineManager_diagram.png new file mode 100644 index 000000000000..38567a2e2041 Binary files /dev/null and b/docs/images/MedicineManager_diagram.png differ diff --git a/docs/images/Model.png b/docs/images/Model.png new file mode 100644 index 000000000000..afdb17cc2ba5 Binary files /dev/null and b/docs/images/Model.png differ diff --git a/docs/images/Model_diagram.png b/docs/images/Model_diagram.png new file mode 100644 index 000000000000..dae384f17cd5 Binary files /dev/null and b/docs/images/Model_diagram.png differ diff --git a/docs/images/PatientManager_diagram.png b/docs/images/PatientManager_diagram.png new file mode 100644 index 000000000000..4897e7dfc522 Binary files /dev/null and b/docs/images/PatientManager_diagram.png differ diff --git a/docs/images/QDUiClassDiagram.png b/docs/images/QDUiClassDiagram.png new file mode 100644 index 000000000000..90ebdd481444 Binary files /dev/null and b/docs/images/QDUiClassDiagram.png differ diff --git a/docs/images/QuickDocs & AppointmentManager.png b/docs/images/QuickDocs & AppointmentManager.png new file mode 100644 index 000000000000..34ec97c273f2 Binary files /dev/null and b/docs/images/QuickDocs & AppointmentManager.png differ diff --git a/docs/images/RecordCreationSD.png b/docs/images/RecordCreationSD.png new file mode 100644 index 000000000000..cfbc6e2ba302 Binary files /dev/null and b/docs/images/RecordCreationSD.png differ diff --git a/docs/images/RecordManager.png b/docs/images/RecordManager.png new file mode 100644 index 000000000000..d070f84c7b43 Binary files /dev/null and b/docs/images/RecordManager.png differ diff --git a/docs/images/ReminderManager_diagram.png b/docs/images/ReminderManager_diagram.png new file mode 100644 index 000000000000..051743b015c8 Binary files /dev/null and b/docs/images/ReminderManager_diagram.png differ diff --git a/docs/images/SDForAddingRecords.png b/docs/images/SDForAddingRecords.png new file mode 100644 index 000000000000..b739e0d650dd Binary files /dev/null and b/docs/images/SDForAddingRecords.png differ diff --git a/docs/images/SDforAddPatient.png b/docs/images/SDforAddPatient.png new file mode 100644 index 000000000000..39ce01acbb48 Binary files /dev/null and b/docs/images/SDforAddPatient.png differ diff --git a/docs/images/StatisticsCommandSDForLogic.png b/docs/images/StatisticsCommandSDForLogic.png new file mode 100644 index 000000000000..61661b0ffe5e Binary files /dev/null and b/docs/images/StatisticsCommandSDForLogic.png differ diff --git a/docs/images/StatisticsManager_diagram.png b/docs/images/StatisticsManager_diagram.png new file mode 100644 index 000000000000..51621bb3685c Binary files /dev/null and b/docs/images/StatisticsManager_diagram.png differ diff --git a/docs/images/StorageClassDiagram.PNG b/docs/images/StorageClassDiagram.PNG new file mode 100644 index 000000000000..16ce1b33d90a Binary files /dev/null and b/docs/images/StorageClassDiagram.PNG differ diff --git a/docs/images/StorageClassDiagram.png b/docs/images/StorageClassDiagram.png deleted file mode 100644 index e5527ecac459..000000000000 Binary files a/docs/images/StorageClassDiagram.png and /dev/null differ diff --git a/docs/images/UI_xcyPPP.png b/docs/images/UI_xcyPPP.png new file mode 100644 index 000000000000..c65cb34aa554 Binary files /dev/null and b/docs/images/UI_xcyPPP.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 5ec9c527b49c..180c8a0a3dab 100644 Binary files a/docs/images/Ui.png and b/docs/images/Ui.png differ diff --git a/docs/images/UiOld.png b/docs/images/UiOld.png new file mode 100644 index 000000000000..69027fa9d645 Binary files /dev/null and b/docs/images/UiOld.png differ diff --git a/docs/images/abortconsult.PNG b/docs/images/abortconsult.PNG new file mode 100644 index 000000000000..1bfa8d923803 Binary files /dev/null and b/docs/images/abortconsult.PNG differ diff --git a/docs/images/adddirec_after.png b/docs/images/adddirec_after.png new file mode 100644 index 000000000000..a16070ccba15 Binary files /dev/null and b/docs/images/adddirec_after.png differ diff --git a/docs/images/addmed_existing.png b/docs/images/addmed_existing.png new file mode 100644 index 000000000000..172e933f46ca Binary files /dev/null and b/docs/images/addmed_existing.png differ diff --git a/docs/images/addmed_existing_nonExisting.png b/docs/images/addmed_existing_nonExisting.png new file mode 100644 index 000000000000..0db822840a63 Binary files /dev/null and b/docs/images/addmed_existing_nonExisting.png differ diff --git a/docs/images/addmed_newMedicine_existing.png b/docs/images/addmed_newMedicine_existing.png new file mode 100644 index 000000000000..f724aa956392 Binary files /dev/null and b/docs/images/addmed_newMedicine_existing.png differ diff --git a/docs/images/addmed_newmedicien_after.png b/docs/images/addmed_newmedicien_after.png new file mode 100644 index 000000000000..089019f446dd Binary files /dev/null and b/docs/images/addmed_newmedicien_after.png differ diff --git a/docs/images/alarm_directory_after.png b/docs/images/alarm_directory_after.png new file mode 100644 index 000000000000..b58b84aa4319 Binary files /dev/null and b/docs/images/alarm_directory_after.png differ diff --git a/docs/images/alarm_medicine_after.png b/docs/images/alarm_medicine_after.png new file mode 100644 index 000000000000..ca090d25217c Binary files /dev/null and b/docs/images/alarm_medicine_after.png differ diff --git a/docs/images/ap_after.png b/docs/images/ap_after.png new file mode 100644 index 000000000000..dc720d0124f2 Binary files /dev/null and b/docs/images/ap_after.png differ diff --git a/docs/images/bentwj.png b/docs/images/bentwj.png new file mode 100644 index 000000000000..9171b6bd1b06 Binary files /dev/null and b/docs/images/bentwj.png differ diff --git a/docs/images/buymed_after.png b/docs/images/buymed_after.png new file mode 100644 index 000000000000..ac4f8ae92e58 Binary files /dev/null and b/docs/images/buymed_after.png differ diff --git a/docs/images/buymed_before.png b/docs/images/buymed_before.png new file mode 100644 index 000000000000..a5a6f156e6d2 Binary files /dev/null and b/docs/images/buymed_before.png differ diff --git a/docs/images/consult_after.png b/docs/images/consult_after.png new file mode 100644 index 000000000000..5335e03890cd Binary files /dev/null and b/docs/images/consult_after.png differ diff --git a/docs/images/dg-appointment/addapp_SD.png b/docs/images/dg-appointment/addapp_SD.png new file mode 100644 index 000000000000..1f4163229091 Binary files /dev/null and b/docs/images/dg-appointment/addapp_SD.png differ diff --git a/docs/images/dg-appointment/addapp_SD_ref.png b/docs/images/dg-appointment/addapp_SD_ref.png new file mode 100644 index 000000000000..c4150637eddb Binary files /dev/null and b/docs/images/dg-appointment/addapp_SD_ref.png differ diff --git a/docs/images/dg-appointment/freeapp1.png b/docs/images/dg-appointment/freeapp1.png new file mode 100644 index 000000000000..fd633e88a691 Binary files /dev/null and b/docs/images/dg-appointment/freeapp1.png differ diff --git a/docs/images/dg-appointment/freeapp2.png b/docs/images/dg-appointment/freeapp2.png new file mode 100644 index 000000000000..95f3a22d5b57 Binary files /dev/null and b/docs/images/dg-appointment/freeapp2.png differ diff --git a/docs/images/dg-appointment/freeapp3.png b/docs/images/dg-appointment/freeapp3.png new file mode 100644 index 000000000000..5cc358ec14ce Binary files /dev/null and b/docs/images/dg-appointment/freeapp3.png differ diff --git a/docs/images/dg-appointment/freeapp4.png b/docs/images/dg-appointment/freeapp4.png new file mode 100644 index 000000000000..5a83c67198a7 Binary files /dev/null and b/docs/images/dg-appointment/freeapp4.png differ diff --git a/docs/images/dg-appointment/freeapp5.png b/docs/images/dg-appointment/freeapp5.png new file mode 100644 index 000000000000..8457d3c19164 Binary files /dev/null and b/docs/images/dg-appointment/freeapp5.png differ diff --git a/docs/images/dg-appointment/freeapp_SD.png b/docs/images/dg-appointment/freeapp_SD.png new file mode 100644 index 000000000000..860a787fe4d1 Binary files /dev/null and b/docs/images/dg-appointment/freeapp_SD.png differ diff --git a/docs/images/dg-consultation/consultation1.png b/docs/images/dg-consultation/consultation1.png new file mode 100644 index 000000000000..bd32cc771d0f Binary files /dev/null and b/docs/images/dg-consultation/consultation1.png differ diff --git a/docs/images/dg-consultation/consultation2.png b/docs/images/dg-consultation/consultation2.png new file mode 100644 index 000000000000..9914f034413c Binary files /dev/null and b/docs/images/dg-consultation/consultation2.png differ diff --git a/docs/images/dg-consultation/consultation3.png b/docs/images/dg-consultation/consultation3.png new file mode 100644 index 000000000000..edd80271227a Binary files /dev/null and b/docs/images/dg-consultation/consultation3.png differ diff --git a/docs/images/dg-consultation/consultation4.png b/docs/images/dg-consultation/consultation4.png new file mode 100644 index 000000000000..4785b4d0dcc6 Binary files /dev/null and b/docs/images/dg-consultation/consultation4.png differ diff --git a/docs/images/dg-consultation/consultation5.png b/docs/images/dg-consultation/consultation5.png new file mode 100644 index 000000000000..aec2d8973bf0 Binary files /dev/null and b/docs/images/dg-consultation/consultation5.png differ diff --git a/docs/images/dg-consultation/consultationSD1.png b/docs/images/dg-consultation/consultationSD1.png new file mode 100644 index 000000000000..e5e93851b594 Binary files /dev/null and b/docs/images/dg-consultation/consultationSD1.png differ diff --git a/docs/images/dg-consultation/diagnosisSD.png b/docs/images/dg-consultation/diagnosisSD.png new file mode 100644 index 000000000000..5b103de6f6d6 Binary files /dev/null and b/docs/images/dg-consultation/diagnosisSD.png differ diff --git a/docs/images/dg-consultation/endconsultSD.png b/docs/images/dg-consultation/endconsultSD.png new file mode 100644 index 000000000000..e5b990b55400 Binary files /dev/null and b/docs/images/dg-consultation/endconsultSD.png differ diff --git a/docs/images/dg-consultation/patientcreation.png b/docs/images/dg-consultation/patientcreation.png new file mode 100644 index 000000000000..ecf5dbcc7460 Binary files /dev/null and b/docs/images/dg-consultation/patientcreation.png differ diff --git a/docs/images/dg-consultation/patientediting.png b/docs/images/dg-consultation/patientediting.png new file mode 100644 index 000000000000..bbb91f7e5156 Binary files /dev/null and b/docs/images/dg-consultation/patientediting.png differ diff --git a/docs/images/dg-consultation/prescriptionSD.png b/docs/images/dg-consultation/prescriptionSD.png new file mode 100644 index 000000000000..191ab5fb3161 Binary files /dev/null and b/docs/images/dg-consultation/prescriptionSD.png differ diff --git a/docs/images/diagnose_after.png b/docs/images/diagnose_after.png new file mode 100644 index 000000000000..76311e8dc23d Binary files /dev/null and b/docs/images/diagnose_after.png differ diff --git a/docs/images/diagnose_edit_after.png b/docs/images/diagnose_edit_after.png new file mode 100644 index 000000000000..43b4171591bd Binary files /dev/null and b/docs/images/diagnose_edit_after.png differ diff --git a/docs/images/diagnose_edit_after_ppp.png b/docs/images/diagnose_edit_after_ppp.png new file mode 100644 index 000000000000..5cede3aae394 Binary files /dev/null and b/docs/images/diagnose_edit_after_ppp.png differ diff --git a/docs/images/dp_after.png b/docs/images/dp_after.png new file mode 100644 index 000000000000..730458f7129c Binary files /dev/null and b/docs/images/dp_after.png differ diff --git a/docs/images/endconsult.png b/docs/images/endconsult.png new file mode 100644 index 000000000000..7e044a77923f Binary files /dev/null and b/docs/images/endconsult.png differ diff --git a/docs/images/ep_after.png b/docs/images/ep_after.png new file mode 100644 index 000000000000..f7598a97191c Binary files /dev/null and b/docs/images/ep_after.png differ diff --git a/docs/images/ep_conflict.png b/docs/images/ep_conflict.png new file mode 100644 index 000000000000..5cd170524d7d Binary files /dev/null and b/docs/images/ep_conflict.png differ diff --git a/docs/images/listOfMedicine_example1.png b/docs/images/listOfMedicine_example1.png new file mode 100644 index 000000000000..cdf2eca3a3f7 Binary files /dev/null and b/docs/images/listOfMedicine_example1.png differ diff --git a/docs/images/listOfMedicine_example2.png b/docs/images/listOfMedicine_example2.png new file mode 100644 index 000000000000..d33f19554c8f Binary files /dev/null and b/docs/images/listOfMedicine_example2.png differ diff --git a/docs/images/listconsult_after.png b/docs/images/listconsult_after.png new file mode 100644 index 000000000000..46b62f4d54bf Binary files /dev/null and b/docs/images/listconsult_after.png differ diff --git a/docs/images/listconsult_before.png b/docs/images/listconsult_before.png new file mode 100644 index 000000000000..fbb5807021cb Binary files /dev/null and b/docs/images/listconsult_before.png differ diff --git a/docs/images/listconsult_index.png b/docs/images/listconsult_index.png new file mode 100644 index 000000000000..2c643b60b18f Binary files /dev/null and b/docs/images/listconsult_index.png differ diff --git a/docs/images/listmed_after.png b/docs/images/listmed_after.png new file mode 100644 index 000000000000..54388cf74863 Binary files /dev/null and b/docs/images/listmed_after.png differ diff --git a/docs/images/listmed_med_after.png b/docs/images/listmed_med_after.png new file mode 100644 index 000000000000..a5dbcbac8848 Binary files /dev/null and b/docs/images/listmed_med_after.png differ diff --git a/docs/images/lp_index.png b/docs/images/lp_index.png new file mode 100644 index 000000000000..85b9539edd55 Binary files /dev/null and b/docs/images/lp_index.png differ diff --git a/docs/images/lp_indexes.png b/docs/images/lp_indexes.png new file mode 100644 index 000000000000..4cd59b88da4c Binary files /dev/null and b/docs/images/lp_indexes.png differ diff --git a/docs/images/lp_name_after.png b/docs/images/lp_name_after.png new file mode 100644 index 000000000000..1fe37bc82dc4 Binary files /dev/null and b/docs/images/lp_name_after.png differ diff --git a/docs/images/lp_noargs.png b/docs/images/lp_noargs.png new file mode 100644 index 000000000000..0985572205e3 Binary files /dev/null and b/docs/images/lp_noargs.png differ diff --git a/docs/images/lp_nric_after.png b/docs/images/lp_nric_after.png new file mode 100644 index 000000000000..800924ee723b Binary files /dev/null and b/docs/images/lp_nric_after.png differ diff --git a/docs/images/lp_nric_specific.png b/docs/images/lp_nric_specific.png new file mode 100644 index 000000000000..ceeba9e153c7 Binary files /dev/null and b/docs/images/lp_nric_specific.png differ diff --git a/docs/images/lp_tag_after.png b/docs/images/lp_tag_after.png new file mode 100644 index 000000000000..59e55a6b410b Binary files /dev/null and b/docs/images/lp_tag_after.png differ diff --git a/docs/images/medicineModule_example1.png b/docs/images/medicineModule_example1.png new file mode 100644 index 000000000000..f91efb369f99 Binary files /dev/null and b/docs/images/medicineModule_example1.png differ diff --git a/docs/images/medicineModule_example2.png b/docs/images/medicineModule_example2.png new file mode 100644 index 000000000000..ae80af86fa01 Binary files /dev/null and b/docs/images/medicineModule_example2.png differ diff --git a/docs/images/medicineModule_example3.png b/docs/images/medicineModule_example3.png new file mode 100644 index 000000000000..a50172112b77 Binary files /dev/null and b/docs/images/medicineModule_example3.png differ diff --git a/docs/images/medicineModule_example4.png b/docs/images/medicineModule_example4.png new file mode 100644 index 000000000000..e12e9a4cb777 Binary files /dev/null and b/docs/images/medicineModule_example4.png differ diff --git a/docs/images/medicineModule_example5.png b/docs/images/medicineModule_example5.png new file mode 100644 index 000000000000..ff46120939df Binary files /dev/null and b/docs/images/medicineModule_example5.png differ diff --git a/docs/images/medicineModule_example6.png b/docs/images/medicineModule_example6.png new file mode 100644 index 000000000000..96c3a3d85e97 Binary files /dev/null and b/docs/images/medicineModule_example6.png differ diff --git a/docs/images/medicineModule_example7.png b/docs/images/medicineModule_example7.png new file mode 100644 index 000000000000..f597a1e9bdd4 Binary files /dev/null and b/docs/images/medicineModule_example7.png differ diff --git a/docs/images/medicineModule_example8.png b/docs/images/medicineModule_example8.png new file mode 100644 index 000000000000..ddbbcf8b57f3 Binary files /dev/null and b/docs/images/medicineModule_example8.png differ diff --git a/docs/images/ongaaron96.png b/docs/images/ongaaron96.png new file mode 100644 index 000000000000..5a2a10d7f256 Binary files /dev/null and b/docs/images/ongaaron96.png differ diff --git a/docs/images/prescription_after.png b/docs/images/prescription_after.png new file mode 100644 index 000000000000..ffa3e8f0a3e6 Binary files /dev/null and b/docs/images/prescription_after.png differ diff --git a/docs/images/prescription_after_ppp.png b/docs/images/prescription_after_ppp.png new file mode 100644 index 000000000000..2ae0c16cb37a Binary files /dev/null and b/docs/images/prescription_after_ppp.png differ diff --git a/docs/images/setprice_after.png b/docs/images/setprice_after.png new file mode 100644 index 000000000000..801a69f08e2b Binary files /dev/null and b/docs/images/setprice_after.png differ diff --git a/docs/images/simjiazhi.png b/docs/images/simjiazhi.png new file mode 100644 index 000000000000..fd30416edbe9 Binary files /dev/null and b/docs/images/simjiazhi.png differ diff --git a/docs/images/statistics_sample.png b/docs/images/statistics_sample.png new file mode 100644 index 000000000000..dd30a4e71b51 Binary files /dev/null and b/docs/images/statistics_sample.png differ diff --git a/docs/images/suggestion_diagram.png b/docs/images/suggestion_diagram.png new file mode 100644 index 000000000000..dbed6df8b6a9 Binary files /dev/null and b/docs/images/suggestion_diagram.png differ diff --git a/docs/images/suggestion_ug1.png b/docs/images/suggestion_ug1.png new file mode 100644 index 000000000000..aecf99cae418 Binary files /dev/null and b/docs/images/suggestion_ug1.png differ diff --git a/docs/images/suggestion_ug2.png b/docs/images/suggestion_ug2.png new file mode 100644 index 000000000000..d72836663130 Binary files /dev/null and b/docs/images/suggestion_ug2.png differ diff --git a/docs/images/suggestion_ug3.png b/docs/images/suggestion_ug3.png new file mode 100644 index 000000000000..b3df914ccd0b Binary files /dev/null and b/docs/images/suggestion_ug3.png differ diff --git a/docs/images/suggestion_ug4.png b/docs/images/suggestion_ug4.png new file mode 100644 index 000000000000..ff788f229480 Binary files /dev/null and b/docs/images/suggestion_ug4.png differ diff --git a/docs/images/suggestion_ug5.png b/docs/images/suggestion_ug5.png new file mode 100644 index 000000000000..050cd3c76ade Binary files /dev/null and b/docs/images/suggestion_ug5.png differ diff --git a/docs/images/suggestion_ug6.png b/docs/images/suggestion_ug6.png new file mode 100644 index 000000000000..c382af7434df Binary files /dev/null and b/docs/images/suggestion_ug6.png differ diff --git a/docs/images/suggestion_ug7.png b/docs/images/suggestion_ug7.png new file mode 100644 index 000000000000..9033edc5667d Binary files /dev/null and b/docs/images/suggestion_ug7.png differ diff --git a/docs/images/ug-app_rem/addapp_clash.png b/docs/images/ug-app_rem/addapp_clash.png new file mode 100644 index 000000000000..230b48b49360 Binary files /dev/null and b/docs/images/ug-app_rem/addapp_clash.png differ diff --git a/docs/images/ug-app_rem/addapp_officehour.png b/docs/images/ug-app_rem/addapp_officehour.png new file mode 100644 index 000000000000..e0d369e48e2c Binary files /dev/null and b/docs/images/ug-app_rem/addapp_officehour.png differ diff --git a/docs/images/ug-app_rem/addapp_success.png b/docs/images/ug-app_rem/addapp_success.png new file mode 100644 index 000000000000..7d4baa26e47d Binary files /dev/null and b/docs/images/ug-app_rem/addapp_success.png differ diff --git a/docs/images/ug-app_rem/addrem_success.png b/docs/images/ug-app_rem/addrem_success.png new file mode 100644 index 000000000000..a11069ec0f3f Binary files /dev/null and b/docs/images/ug-app_rem/addrem_success.png differ diff --git a/docs/images/ug-app_rem/deleteapp_failure.png b/docs/images/ug-app_rem/deleteapp_failure.png new file mode 100644 index 000000000000..95a79fd74d6e Binary files /dev/null and b/docs/images/ug-app_rem/deleteapp_failure.png differ diff --git a/docs/images/ug-app_rem/deleteapp_success.png b/docs/images/ug-app_rem/deleteapp_success.png new file mode 100644 index 000000000000..fa3bfc2bd256 Binary files /dev/null and b/docs/images/ug-app_rem/deleteapp_success.png differ diff --git a/docs/images/ug-app_rem/deleterem_failure.png b/docs/images/ug-app_rem/deleterem_failure.png new file mode 100644 index 000000000000..61ec2d93ab4b Binary files /dev/null and b/docs/images/ug-app_rem/deleterem_failure.png differ diff --git a/docs/images/ug-app_rem/deleterem_success.png b/docs/images/ug-app_rem/deleterem_success.png new file mode 100644 index 000000000000..a6517ff35fab Binary files /dev/null and b/docs/images/ug-app_rem/deleterem_success.png differ diff --git a/docs/images/ug-app_rem/freeapp_week.png b/docs/images/ug-app_rem/freeapp_week.png new file mode 100644 index 000000000000..523289b558c4 Binary files /dev/null and b/docs/images/ug-app_rem/freeapp_week.png differ diff --git a/docs/images/ug-app_rem/listapp_week.png b/docs/images/ug-app_rem/listapp_week.png new file mode 100644 index 000000000000..b0465286b9ce Binary files /dev/null and b/docs/images/ug-app_rem/listapp_week.png differ diff --git a/docs/images/ug-app_rem/listrem_single.png b/docs/images/ug-app_rem/listrem_single.png new file mode 100644 index 000000000000..4fbfa79e3469 Binary files /dev/null and b/docs/images/ug-app_rem/listrem_single.png differ diff --git a/docs/images/ug-app_rem/listrem_week.png b/docs/images/ug-app_rem/listrem_week.png new file mode 100644 index 000000000000..731976f6f9e1 Binary files /dev/null and b/docs/images/ug-app_rem/listrem_week.png differ diff --git a/docs/images/ui_explanation.png b/docs/images/ui_explanation.png new file mode 100644 index 000000000000..c1a828cdd74d Binary files /dev/null and b/docs/images/ui_explanation.png differ diff --git a/docs/images/uioverview.png b/docs/images/uioverview.png new file mode 100644 index 000000000000..4dd56956735f Binary files /dev/null and b/docs/images/uioverview.png differ diff --git a/docs/images/xue-chenyang.jpg b/docs/images/xue-chenyang.jpg new file mode 100644 index 000000000000..c5fb41d5b4e9 Binary files /dev/null and b/docs/images/xue-chenyang.jpg differ diff --git a/docs/images/xue-chenyang.png b/docs/images/xue-chenyang.png new file mode 100644 index 000000000000..c5fb41d5b4e9 Binary files /dev/null and b/docs/images/xue-chenyang.png differ diff --git a/docs/team/Xue-Chenyang.adoc b/docs/team/Xue-Chenyang.adoc new file mode 100644 index 000000000000..472ae031c5fa --- /dev/null +++ b/docs/team/Xue-Chenyang.adoc @@ -0,0 +1,321 @@ += Xue Chenyang - Project Portfolio +:imagesDir: ../images +:stylesDir: ../stylesheets +:xrefstyle: full +:experimental: +ifdef::env-github[] +:tip-caption: :bulb: +:note-caption: :information_source: +:highlight-caption: :warning: +endif::[] +:repoURL: https://github.com/CS2103-AY1819S2-W09-4/main/tree/master + +I am **Xue Chenyang**, a Computer Science undergraduate student from NUS School of Computing. This project portfolio page describes my contribution in my school project: QuickDocs in module CS2103T. + +== PROJECT: QuickDocs + +== What is QuickDocs? + +QuickDocs is a one-user all in one clinic management application developed by my team consisting of 4 students. + + +My team was assigned to transform an existing application (of around 10,000 lines of code) to a new application that has real-world applications. + + +My team decided to address the problem that private clinics in Singapore are facing conflicts between limited manpower and the enormous amount of trivial but complicated management chores to handle. + + +In QuickDocs, doctors can have greater control in facilitating patient consultations, organizing appointments, and monitoring financial and inventory records in private clinics. + +== Summary of my contributions + +My role in this project is to design and implement the medicine management module. The design of this module includes the data structure of medicine storage and the various commands essential to medicine management. + + +The following is a list of notation used in this document and their respective meaning. + + +[TIP] +This is a tip. Useful information pertaining to the features will be written here. +[NOTE] +This is a note. Additional information that further explains a feature will be written here. + + +`command` This is a user input to the application. It is formatted with a gray background. Example: `help` + + +=== Major enhancements + +|=== +|_This section illustrates my major contributions in this project, especially regarding to design and coding of medicine management module._ +|=== + +* *Enhancement 1:* I designed and implemented the medicine management module. + +** *What it does:* Medicine management is a key aspect of clinic management. My medicine management module allows the doctors to customize his/her medicine organization. It also supports a variety of commands to facilitate users in clinic management. + +** *Why is it needed:* With QuickDocs, doctors no longer need to go through tons of paperwork or sheets and sheets of Excel files to keep track of his medicine storage. All additions, modifications, prescriptions, and organizations of medicines could be done on one single interface so that QuickDocs provides its users with the most convenience possible in managing their clinics. + +** *Highlights:* The medicine module is designed is in a directory format, which supports quick search of medicines and mass operation of medicines without the need to type in lengthy commands. This will be explained further in details in <>. +** *Who / what to acknowledge:* My inspiration for organizing medicines in a directory format is adapted from the folder system in most of the common Operating systems such as Windows. + +* *Enhancement 2:* I added automatic completion feature to facilitate command typing. +** *What it does:* With this feature, when typing commands related to medicine storage, the users no longer need to remember and type in the path by themselves; instead, they can press page-up and page-down to iterate through all possible sub-directories. This feature is added to bring maximum convenience to the users. + +** *Highlights:* This feature requires seamless integration among three components of the product, namely the UI component, Logic component, and the Model component. In my implementation, I followed the principle of abstraction closely, so that this feature is independent of the implementations of lower level modules and will work well with both existing commands and possible future commands. + + +* *Code contributed:* [https://nus-cs2103-ay1819s2.github.io/cs2103-dashboard/#=undefined&search=Xue-Chenyang[Functional code]] + +=== Other contributions + +|=== +|_This section illustrates some other contributions I made in this project, including minor features added, project management and team collaboration._ +|=== + +* *Minor features implemented:* +** I implemented an automatic alarm system so that whenever a medicine's amount falls below a threshold set by the user, an automatic reminder will appear in the sidebar to remind the user to stock up that medicine. + +** In some commands in medicine management module, to accommodate users who know their medicines really well, I supported identifying medicines by their names only. + +* *Project management:* +** I assisted setting up of milestone objectives from v1.2 to v1.4. + +** Working with my team, I managed issue trackers on GitHub page regarding to medicine management module. + +* *Team collaboration:* +** I analyzed the architecture of QuickDocs' prototype application, AddressBook Level 4. With the knowledge, I helped my teammates in understanding how our team should code our product. +** I removed left over redundant AddressBook code for v1.4 releases to improve the efficiency and readability of the code. Additionally, I also refactored the names of packages in our products to more accurately reflect our product. +https://github.com/CS2103-AY1819S2-W09-4/main/pull/138[#138], +https://github.com/CS2103-AY1819S2-W09-4/main/pull/163[#163] +** I reviewed pull requests https://github.com/CS2103-AY1819S2-W09-4/main/pull/35[#35] and https://github.com/CS2103-AY1819S2-W09-4/main/pull/98[#98] to improve the quality of our code. + +== Contribution to the User Guide + +|=== +|_In this sections, I will show a sample section of my contribution to the User Guide._ + +_This sample section will cover the autocompletion feature and the most fundamental commands in medicine management, i.e adding medicines._ +|=== + +=== Suggestion mode in command typing + +In QuickDocs, as medicines are stored in directories format, you will need to key in the path to your interested directory/medicine from time to time. + + +For your convenience, QuickDocs actually provides a quick cut to key in these paths so that you do not need to type in every character by yourself! + + +As long as you are typing a command in medicine module and you have typed the name of the first parent directory followed by a `\` character, the suggestion mode is automatically turn on! + + +So what is suggestions mode? Why is that useful? The following example will illustrate the convenience it brings to you. + +* Suppose you have a directory 'Flu' under directory 'General' under directory 'Internal' under the root directory 'root' as illustrated below: + +[[suggestion_ug1]] +.Display of detailed information +image::suggestion_ug1.png[width='600'] + +* Now you want to type this command `listmed root\Internal\General\Flu` , which is a command to see detailed information about that directory, from scratch. + +* What you can do is to first type `listmed root\` to trigger the suggestion mode. + +* You can then press the page-down key to iterate through all subdirectories under 'root'. The input box will automatically be filled for you. + +* In this example, you can arrive at `listmed root\Internal` in just two Page Down keys. + +* Doing this recursively at every stage, you can quickly arrive at the desired `listmed root\Internal\General\Flu`. + +* Suppose now you want to view the detailed information about a medicine called 'guaifenesin' under directory 'Flu'. You entered `listmed root\Internal\General\Flu\guai` only to realize that you forget the spelling of guaifenesin. + +* Do not worry! You can press page-up / page-down as well. Page up will bring you to the last valid sub-directory / medicine name as compared to your input according to alphabetic order. Page down will bring you to the next valid input. + +.Before pressing Page Up / Page Down +image::suggestion_ug5.png[width='600'] + +.After pressiong Page Up, brings you to the last valid medicine: diphenhydramine +image::suggestion_ug6.png[width='600'] + +.After pressing Page Down, brings you to your desired : guaifenesin +image::suggestion_ug7.png[width='600'] + +{nbsp} + + +In short, instead of typing out every characters, you can iterate through your directories fast and easy when typing commands using the page-up and page-down keys! + +[NOTE] +In QuickDocs, names of directories and medicines are case-insensitive. So what appears in the suggestions may be in different cases from the actual name, but they will work the same. + +- - - + +[[addmed, Add medicine]] +=== Add medicine into storage: `addmed` + +Through this command, you can either add a new medicine into the storage, or to place an existing medicine under another directory. + + +You can add a new medicine by specifying where it should go to, its initial quantity and its price + + +*Format:* `addmed [PATH OF DIRECTORY TO ADD TO] [MEDICINE_NAME] [q/QUANTITY] [p/PRICE]` + + +*Alias{nbsp}{nbsp}{nbsp}{nbsp}:* `am` + +[NOTE] +Directories and medicines under the same directory are now allowed to share the same name (case-insensitive). + +*Example:* `addmed root\fever paracetamol q/50 p/9.99` + +*Result:* + +After you enter the command, if it is executed successfully, a confirmation message will appear. + +[[addmed_newmedicien_after]] +.Sucess message of sample addmed command +image::addmed_newmedicien_after.png[width="600"] + +[NOTE] +If you entered a medicine name that already exists in the storage in this format, a error message will pop up. + + +{sp} + + +You can also place an existing medicine into a directory. + + +*Format:* `addmed [PATH] [MEDICINE_NAME]` + +*Example:* `addmed root\headache paracetamol` + + +Assuming there already exists a medicine called paracetamol in the storage, you can place this medicine under root\headache via this command. + + +*Result:* + +After you enter the command, if it is executed successfully, a confirmation message showing the detailed information of that medicine will appear. + +[[addmed_existing]] +.Success message of sample addmed command +image::addmed_existing.png[width="600"] + +[NOTE] +If you entered a medicine name that has not existed in the storage in this format, an error message will pop up. + +_Click https://github.com/CS2103-AY1819S2-W09-4/main/blob/master/docs/UserGuide.adoc[here] to see the full User Guide._ + +== Contribution to the Developer Guide + +|=== +|_In this section, I will showcase a sample section of Developer Guide I contributed._ + +_The sample sections explains the implementations of medicine modules, the auto-completion features and the alarm system._ +|=== + +[[Develop_Guide]] +=== Storing medicines in inventory + +One essential aspect of clinic management is about managing medicine storage of the clinic. QuickDocs' medicine management module supports customized medicine organization via a browser-like directory format. + +==== Current implementation + +The current implementation takes a similar form as the Windows file browser. The user is free to determine for himself/herself how he/she wants the medicines to be arranged. + +*Code:* {repoURL}/src/main/java/quickdocs/model/medicine/MedicineManager.java[MedicineManager.java] + +[TIP] +The current implementation does not allow multiple medicines with the same to exist simultaneously. However, one medicine could be placed in multiple directories. + +[NOTE] +Both directories and medicines' names are case-insensitive. + +From the initial empty state of the storage, the users could arrange their storage in these following ways: + +1. The initial empty storage consists of an empty directory named as "root". The user can then add directories and medicines into the storage. + +2. The `MedicineManager` keeps a list of sorted unique medicine in the inventory. + +3. The user could add a new directory via `adddirec` command by specifying the path of the directory he/she wants to add into and the name of new directory. + +4. The user could add new/existing medicine to a specific directory via the "addMed" command. + +* 1. If there already exists a medicine with the same name in the storage, and the quantity and price is not specified in the command arguments, the existing medicine will be placed in the directory specified. + +* 2. Otherwise, a new medicine with the specified name, quantity and price will be created and added to the specified directory. + +- - - + +[[suggestion_mode]] +When typing the directory path in the command box in the ui, QuickDocs supports intelligent suggestions about the next field. + + +After the user entered at least one `\` character to indicate he is inputting a path, the suggestion mode will be turned on. + + +The user could press Page Up / Page Down bottom to iterate to the previous or the next valid name of sub-directory or medicine in alphabetical order, given that the path given before the previous `\` character is valid. + +Using a sample inventory below as an example: + + +[[medicineModule_example7]] +image::medicineModule_example7.png[width='600'] + +* When the user types in `addmed root\`, the suggestion mode is turned on. + + +* The user may not want to type in the full name of the directories, so when he types in `addmed root\in`, he could then press Page Down to iterate to the next valid name in alphabetical order, which is "Internal". + + +* The command box is then automatically filled with `addmed root\Internal` + + +* Similarly, if the user decides to traverse to the previous valid name, he could do so by press Page Up. And the command box will automatically be filled with `addmed root\External`. + + +The figure below illustrates how this feature is implemented to make user's life more convenient. + + +[[suggestion_diagram]] +.Sequence diagram illustrating the implementation of suggestion mode +image::suggestion_diagram.png[width='600'] + +- - - + +QuickDocs also supports setting alarm level for medicines. Every time a medicine's storage falls below the designated level, a reminder is thrown. + + +To convenient the users, QuickDocs allow not only threshold setting for individual medicines, but also threshold setting for directories. + + +Taking the same sample inventory as an example: + + +Setting a threshold for a directory is effectively the same as setting the threshold for every medicine in the "subtree" of that directory. This is down by a tree-like traversal. + + +For example, `alarm root\Internal 400` command sets the alarm level of all medicine in the subtree of "Internal" directory to 400. + + +[[medicineModule_example9]] +image::medicineModule_example8.png[width='600'] + +==== Design consideration + +1. The current implementation takes into consideration that the users may wish to have some freedom in determining the arrangement of medicine. + +2. When prescribing medicines, a directory system that step by step leads to the desired medicine is to the convenience of the user. + +3. By arranging the medicine by folders, it is then possible to support massive manipulation of medicine by directories. + +4. Additionally, it is impossible to expect the doctor to always remember the full name of medicines correctly. There is a need for an easier way to identify medicines to operate on besides requiring the user to type in full names every time. + +==== Alternatives considered + +The table illustrates some of the alternatives I considered during development of this medicine module, the relative advantages they have over the current implementation, and why they are not selected at the end. + +[cols="1, 1, 2a, 2a", options="header"] +|=== +|Alternative |Description |Comparative advantages |Reasons for not adopting +// row 1 +|*Store medicines as a simple ArrayList* +|When users add a new medicine, just append a new medicine to the ArrayList. +| * Simpler command format: The user could type in less arguments for the same commands. + +* No maintainability issue, less likely to occur bugs. + +| * Must type in full name of medicines correctly. + +* No freedom for the user to organize his/her medicine. Does not support massive operations. + +// row 2 +|*Use a hash map to store the medicines* +| Use medicine name as the key and the medicine as the value. +| * More time-efficient searching, especially when data size is large. + + +* Simpler command format, less fields to input per command. + +| * No room for customized arrangement of medicine. + + +* Need to type in the correct full name of medicines. + +* Does not support massive operations on similar medicines. + +//row 3 +|*Store medicines in a list with tags* +| Users add tags to medicines for searching. +| * Can list all medicines with the same tag. + + +* Similar structure with patient module. Less efforts needed. +| * Potential lengthy commands if a medicine has many tags. + + +* Have to input the full name of medicine or the tag correctly to retrieve correct medicine. When there is a large set of medicines and tags, this is not convenient. + +|=== + +Since QuickDocs aims to provide the most convenient experience given a large set of medicine in a clinic inventory, the medicine management module needs to provide a model that makes both typing commands, identifying the correct medicine and massive operation possible. + + +Combined with the <>, the current design is the best way to implement all of the three. + +{nbsp} + + +_Click https://github.com/CS2103-AY1819S2-W09-4/main/blob/master/docs/DeveloperGuide.adoc[here] to see the full Developer Guide._ diff --git a/docs/team/bentwj.adoc b/docs/team/bentwj.adoc new file mode 100644 index 000000000000..998ff0d30798 --- /dev/null +++ b/docs/team/bentwj.adoc @@ -0,0 +1,274 @@ += Tan Wenjian - Project Portfolio +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets +ifdef::env-github[] +:tip-caption: :bulb: +:note-caption: :information_source: +endif::[] +:warning-caption: :warning: +:repoURL: https://github.com/CS2103-AY1819S2-W09-4/main/tree/master + +Hello! Welcome to my project portfolio page (PPP). This PPP showcases the software development projects I have been +involved so far. Currently, there is only 1 project listed — QuickDocs. + +== PROJECT: QuickDocs + +== Introduction to QuickDocs + +QuickDocs is an all-in-one software solution for private clinic doctors facing manpower issues. Private clinics are +usually small in size, with just a single doctor and 1 or 2 assistants. However, there are many tasks that the clinic +needs to handle, such as patient consultation and management, organising appointments, keeping track of the medicine +inventory and keeping track of finances. + + +Hence, my team and I have decided to morph an existing AddressBook application into a clinic management application, +QuickDocs, that allows doctors to perform all the tasks above in a single application. + +== Summary of contributions + +In this section, the summary of my contributions will be listed out. Contributions includes code, documentation, and +other miscellaneous tasks that helped the team. + +I was responsible for developing the calculation and storage of statistical information in QuickDocs, mainly financial +and consultations information. + +=== Major Enhancements: + +* *Code Contributed:* [https://nus-cs2103-ay1819s2.github.io/cs2103-dashboard/#=undefined&search=bentwj[Functional code]] +* *Statistics command* +** *What it does:* Doctors can view statistics related to consultations and finances within a minimum time span of 1 month. +** *Why is it necessary:* +*** By viewing the consultation statistics of the clinic, doctors can view the most common +symptoms diagnosed and medicine prescribed. This allows them to look out for any possible trends in order to prepare +for the future. The doctors could stock up on frequently prescribed medicines or prepare his clinic to be able to handle +the most common illness in the past few months. +*** By viewing the financial statistics of the clinic, doctors can keep track of the revenue, expenditure, and profit +of the clinic related to consultations and medicines. In addition, they could also use this as a cross-reference to their +own accountings if desired. +** *Highlights:* The statistics generated are done automatically whenever the doctor ends a consultation session on +QuickDocs or adds purchased medicines to the QuickDocs medicine inventory. There is no need to manually save statistical +information in QuickDocs. The doctor simply needs to enter the `statistics` command with the date to retrieve the statistics. + +=== Minor Enhancements: + +* *Minor features developed* +** Developed command for setting the consultation fee in QuickDocs, which is required for accurate financial statistics +generation. +* *Miscellaneous tasks* +** *Team Collaboration:* +*** Was involved in setting up Travis CI as the integration tool for our project. +*** Reviewed Pull Requests: [https://github.com/CS2103-AY1819S2-W09-4/main/pull/129[#129]] +** *Documentation:* +*** Helped to ensure consistent format for user guide. +*** Compiled the command summary section for user guide. + +== Contributions to the User Guide + +|=== +|_The link:{repoURL}/docs/UserGuide.adoc[user guide] is meant to aid users of QuickDocs if they encounter any issues. +My additions to the user guide includes querying the statistics of the clinic, setting the consultaton fee, the FAQ, +and the Command Summary. Shown below are the section for querying the statistics and setting the consultation fee._ +|=== + +[[setconsultfee, Set consultation fee]] +==== Setting consultation fee: `setconsultfee` +This command allows you to change your consultation fee in QuickDocs to what you charge for each consultation. +Setting up of your consultation fee is necessary for QuickDocs to generate accurate financial statistics pertaining +to your consultation sessions. Once your consultation fee is set, QuickDocs will remember it for you. Hence, you +only need to enter this command once, until you decide to change your consultation fee. + + +[NOTE] +The default consultation fee in QuickDocs in S$30.00. + + +*Format:* `setconsultfee AMOUNT` + + +*Alias{nbsp}{nbsp}{nbsp}{nbsp}:* `setfee` + +*Examples:* + + +* `setconsultfee 30` + +Sets the consultation fee to $30.00. + +* `setconsultfee $32.50` + +Sets the consultation fee to $32.50. + + +{empty} + +[[statistics, View statistics]] +==== Viewing statistics: `statistics` + +You are able to view the statistics of the clinic for a specific month, or between a range of months. + +This command will show you 6 items in order: + + +. Number of consultations + +. Most common medicines prescribed + +. Most common symptoms diagnosed + +. Revenue + +. Expenditure + +. Profit + + +[NOTE] +In order for the calculated statistics pertaining to consultation finances to be accurate, you must have already +entered your clinic's consultation fee beforehand. See: <>. + +[NOTE] +When there is a tie between the most commonly prescribed medicine, or most commonly diagnosed symptoms, all of them +will be listed. + + +{empty} + +*Format:* `statistics FROM_MMYYYY [TO_MMYYYY]` + + +*Alias{nbsp}{nbsp}{nbsp}{nbsp}:* `stats` + + +[NOTE] +If `TO_MMYYYY` is not specified, it will be defaulted to be equal to `FROM_MMYYYY`. + + +*Examples and Results:* + +* `statistics 012019` + +View the statistics for January 2019. + +* `stats 012019 042019` + +View the statistics from January 2019 to April 2019. + +In the image below, the *display* area will return the statistics of the queried range of months. + + +1. The result will show the range of months that were queried, followed by the 6 items mentioned earlier in order. +2. When there are more than one most common symptom diagnosed, QuickDocs will list them all out. + +.Result example for command `statistics 012019 042019` +image::statistics_sample.png[width="600"] +{empty} + + +== Contributions to the Developer Guide + +|=== +|_The link:{repoURL}/docs/DeveloperGuide.adoc[developer guide] is meant to be a starting point for any current or future developers that will be working on QuickDocs. +My additions to the developer guide includes explaining the Logic component, and the implementation of the Statistics +and Record feature. The implementation of the Statistics and Records feature will be shown below._ +|=== + +=== Administrative and Statistics Module +The administrative and statistics module currently consists of 2 commands: + + +1. setting the consultation fee `setconsultfee` + +2. querying the statistics `statistics` + + +This 2 commands makes use of the classes located in filepath `model\record`. + +==== Consultation fee +The consultation fee of the clinic is stored as a BigDecimal in the StatisticsManager of QuickDocs, which is loaded from the +quickdocs.json file through the <> component. The consultation fee is used for calculating +financial statistics for any ConsultationRecord objects. + +==== Querying statistics +The statistics command is started through the command `stats START_MMYYYY [END_MMYYYY]`. +The two MMYYYY corresponds to a range of dates. The end range is optional, +and is defaulted to the start range by the StatisticsCommandParser if it does not exist. + + +The start date is not allowed to be before January 2019, and the end date cannot be before the start date. Hence, +QuickDocs currently does not support adding old records before January 2019 due to the implementation of the +StatisticsManager. This will be explained in the section below. + +[NOTE] +MMYYYY is a string, e.g. "012019", which stands for January 2019. It is parsed by StatisticsCommandParser into a +YearMonth object. + + +==== Statistics and Record - Current Implementation + +The statistics class stores 6 types of information: + + +1. Number of consultations + +2. Medicines prescribed + +3. Symptoms diagnosed + +4. Revenue + +5. Expenditure + +6. Profit + + +Number of consultations is stored as an int, while the financial variables are stored using BigDecimals. The number of +medicines prescribed and symptoms diagnosed are stored by using a HashMap. + + +[NOTE] +Implementation of additional statistics will be done through adding additional relevant variable fields. + +The implementation of Statistics and Record has 3 parts: + + +1. Creation of the Record + +2. Adding the Record + +3. Retrieving the Statistics + + +===== 1. Creation of the Record +In order for the statistics to be keep tracked of, Record objects are used to retrieve information that the +StatisticsManager will make use of. The Record class is an abstract class that only has 1 abstract method, +`toStatistics(StatisticsManager sm)`, which will generate a Statistics object. +Each child class of Record is for a specific operation in QuickDocs, where the implementation +`toStatistics(StatisticsManager sm)` will generate a Statistics object that stores relevant information pertaining to +that specific operation. The StatisticsManager is passed in to retrieve the any variable that the Record might require +to calculate the statistics, e.g., ConsultationRecord requires the consultationFee variable in StatisticsManager. + +Currently, there are only 2 child classes of Record, ConsultationRecord and MedicinePurchaseRecord. ConsultationRecords +are created when the a consultation session ends from the EndConsultCommand, while MedicinePurchaseRecord are created +when a medicine is purchased via the PurchaseMedicineCommand. The commands will create the Record, and call ModelManager's +`addRecord(record, clock)` function, which will then result in ModelManager calling StatisticsManager's `record(record, clock)` +function. The clock used is the system clock, to retrieve the current YearMonth of the Record created. The sequence diagram +below illustrates an example ConsultationRecord being created. + +.Sequence diagram for sample ConsultationRecord creation +image::RecordCreationSD.png[width="800"] + +===== 2. Adding the record +The StatisticsManager holds an ArrayList of MonthStatistics, where a MonthStatistics object contains the YearMonth, and +the Statistics object of that YearMonth. Each MonthStatistics object will be initialised with the zero Statistics object, +where all the variables are 0 or contains no elements (not null). The ArrayList starts with a MonthStatistics with the +YearMonth 2019 January, and every subsequent index will contain the MonthStatistics with the subsequent month, e.g., +the 4th index contains the MonthStatistics with YearMonth 2019 May. + +When the StatisticsManager adds a new Record by the `record(record, clock)` function, it will first retrieve the +YearMonth from the `clock` variable passed in. Next, it will update the size of the ArrayList by calling its own method +`updateListSize(clock)`, which is a wrapper for `updateListSize(YearMonth)`. Afterwards, StatisticsManager will find the +correct index of the MonthStatistics ArrayList to add the record in. In the current implementation, the record is not +actually stored. Instead, the record will be converted to a Statistics object which is then merged with the +MonthStatistics's own Statistics object. The MonthStatistics's Statistics object will then be reassigned with the newly +merged Statistics object. The sequence diagram below illustrates an example ConsultationRecord being added. + +.Sequence diagram for adding a sample ConsultationRecord +image::SDForAddingRecords.png[width="800"] + +===== 3. Retrieving the Statistics + +When the StatisticsCommand queries for the statistics for a range +of months, Logic will call the ModelManager's `getStatistics(FROM_YEARMONTH, TO_YEARMONTH)`, which then calls +StatisticsManager's `getStatistics(FROM_YEARMONTH, TO_YEARMONTH)`. StatisticsManager will convert the YearMonth objects to +their respective indexes with the StatisticsManager's `getYearMonthIndex(YearMonth)` function. + +StatisticsManager will then obtain the statistics for each of the queried months, and merge them together into a new +Statistics object. StatisticsManager will then return the Statistics back to the ModelManager, which would then return +it to the StatisticsCommand, which would then return the CommandResult with the statistics converted to a String to the +LogicManager. + +==== Statistics and Record - Design considerations + +1. The statistics are stored in months as the design only allows the doctor to query within a minimum timespan of 1 month. +Hence, it was decided that the statistics to be stored in months in a chronological order with an ArrayList for ease of +retrieval. + + +2. Currently, as QuickDOcs is developed in 2019, and there are no plans to allow the doctor to add in past records, +the first index in the array of MonthStatistics is allocated to January 2019. Any MMYYYY value before 012019 will not be +allowed. + +3. The MonthStatistics objects are stored in an ArrayList as it might be desirable for a MonthStatistics with the zero +statistics to exist (all variables 0 or no elements). Such a case might happen when the doctor goes on vacation for the +whole month. In addition, it would be easy to retrieve the MonthStatistics object of a specific MMYYYY by indexing. + +==== Statistics and Record - Alternatives Considered +The following table lists out the alternatives designs considered for implementing the storage of the Records and Statistics. +[cols="1,2a,1, 1", options="header"] +|=== +|Alternative |Description |Pros |Cons +// row 1 +|*Storing of individual records for each month (Alternative chosen)* +|Individual records are stored within the MonthStatistics, along with the Statistics. When the Statistics for a +specific month is queried, update the latest statistics and return it. +|Individual records are kept, which could potentially be used for other calculations or features. +|Storing of individual records is extremely costly in terms of space +// row 2 +|*Storing the merged statistics of all the records for each month* +|When a new record is added, it is coverted to a Statistics object which is then merged with the current Statistics object stored. +|Only one Statistics object needs to be stored, which saves a lot of storage space. +|The individual records are unable to be retrieved. However, the current implementation has no need to retrieve individual records. +|=== diff --git a/docs/team/ongaaron96.adoc b/docs/team/ongaaron96.adoc new file mode 100644 index 000000000000..f28d5b8bb325 --- /dev/null +++ b/docs/team/ongaaron96.adoc @@ -0,0 +1,395 @@ += Aaron Ong - Project Portfolio for QuickDocs +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets +:xrefstyle: full +ifdef::env-github[] +:tip-caption: :bulb: +:note-caption: :information_source: +endif::[] + +Hello everyone! I am Aaron, an undergraduate studying Computer Science in the National University of Singapore. This is my +portfolio that documents the various contributions I have made to a Software Engineering project that my team have +embarked on, QuickDocs. + +== What is QuickDocs? +QuickDocs is a desktop application that provides doctors with greater control in running their family clinics. +It assists them in their daily tasks such as facilitating patient consultations, managing medicine inventory, +scheduling appointments, keeping track of reminders and monitoring financial statistics. + +These features can be summarized into four main modules: + +* Patients and Consultations +* Medicine inventory +* Appointments and Reminders +* Administration and Statistics + +QuickDocs has a Graphical User Interface (GUI), however most of the user interaction takes place via a Command +Line Interface (CLI). It was built in Java and JavaFX and was morphed from an existing Address Book application. + +== Notations +Before you carry on, take note of these notations that will be used throughout this portfolio: + +* Text in `grey background` indicates either written code or user input. + +[TIP] +This is a tip, to inform you of useful information. + +[NOTE] +This is a note, to provide you with any additional information about a feature. + +== Summary of Contributions +|=== +|This section provides you with a summary of both technical and non-technical contributions that I have made +throughout the development of QuickDocs. +|=== + +Out of the four main modules that comprises QuickDocs, I was in charge of developing the Appointments and Reminders +module. + +*Appointments* + +Doctors are able to schedule appointments with their patients and ensure that they are organised +neatly, with no conflicting timings with existing ones. + +*Reminders* + +Doctors will no longer forget about upcoming to-dos or appointments in the midst of their busy schedule! They can +simply note their tasks down and be notified of them through their reminder sidebar. + +*Appointments and Reminders features* + +The following are the summarized features of this module: + +* `add`, `delete` and `list` commands for both Appointments and Reminders to register them into QuickDocs. +* Automatic generation of reminders for upcoming appointments or alerts to stock up for a particular medicine. +* The `freeapp` command that lists out all free timings available to book an appointment, given a search range of dates. +* Displaying reminders on the reminder sidebar of the User Interface (UI). + +=== Major Enhancement: Creation of `freeapp` command +** *What it does:* Doctors can list out all free appointment slots for a particular range of dates. +** *Why is it needed:* Before the doctor schedules an appointment with his/her patient, they must first agree on a +suitable date and time. Typically, the doctor has to look through his/her whole schedule of appointments and figure +out an appropriate timing from there. With `freeapp`, the doctor is only one command away from viewing all free +timings for a given range of dates. The doctor can then view these timings with the patient and quickly come to an +agreement - QuickDocs does the thinking instead! +** *Highlights:* To make `freeapp` even more convenient, the doctor does not have to be bothered with the specific dates +to search. Instead, he/she only has to provide a single `DATE` and a `FORMAT`, which can either be `day`, `week`, or +`month`. If the `FORMAT` specified was either `week` or `month`, `freeapp` will then list out the free timings for +all neighboring dates of the given `DATE`. + +=== Minor Enhancements: +* *Creation of the reminder sidebar* +** Reminders are displayed on the sidebar to make it more obvious to the doctor, and by default only the reminders for +the current week will be displayed. +** Reminders are colour-coded: +*** [blue]#Blue#: Reminders for appointments. +*** [red]#Red#: Reminders to stock up on a medicine. +*** [yellow]#Beige#: Any other personal reminders. +** Clicking a reminder on the sidebar will display all its information on the main display. +* *Automatic generation of reminders* +** When an appointment is added, a reminder will automatically be created for this appointment to ensure that the +doctor does not forget about it in the future. +** When a medicine's quantity drops below its specified threshold, a reminder will automatically be created to alert +the doctor. + +=== Other contributions: +* *Project Management:* +** Helped to manage team milestone goals and issue trackers on QuickDocs' GitHub project page. + +* *Designing QuickDoc's User Interface (UI):* +** At the start of the project, I assisted the team in designing the layout and structure of QuickDoc's UI +that is currently implemented. + +* *Team Collaboration:* +** During the initial stages of the project, the Storage mechanism was not yet implemented and we had to manually +execute commands to load QuickDocs with sample data for every new session. Hence I helped to create sample Patient, +Appointment, and Reminder data that were loaded at every QuickDocs launch: + https://github.com/CS2103-AY1819S2-W09-4/main/pull/55/commits/99d3b8767dafcd254ee53a47764f7365d7fce199[Sample Data] +** Helped to write the test cases for the Storage component after it was implemented by my teammate: + link:https://github.com/CS2103-AY1819S2-W09-4/main/pull/123/commits/4b332c1dfb308c1abd1612b7ca13a3635e4e69aa[JsonSerializableQuickDocsTest], +link:https://github.com/CS2103-AY1819S2-W09-4/main/pull/123/commits/472b3acf6e724fde67282388bd34397de0fb9275[JsonQuickDocsStorageTest] + +* *Documentation:* +** Documented the implementation of the Storage component in the Developer Guide. +** Provided formatting suggestions and implemented them for the User Guide and Developer Guide. + +* *Community Involvement:* +** Reviewed Pull Requests (PR) made by team members, providing non-trivial comments: https://github.com/CS2103-AY1819S2-W09-4/main/pull/105[#105] +** Tested another team's application, reporting bugs found and offering suggestions: + https://github.com/nus-cs2103-AY1819S2/pe-dry-run/issues/906[1], +https://github.com/nus-cs2103-AY1819S2/pe-dry-run/issues/734[2] and +https://github.com/nus-cs2103-AY1819S2/pe-dry-run/issues/567[3] + +* *Setting Up Tools:* +** Assisted in setting up the continuous integration plugin TravisCI for the team's QuickDocs repository on GitHub. + +=== Code Contributed: +You can view all my code contributions to QuickDocs in full by clicking the link + https://nus-cs2103-ay1819s2.github.io/cs2103-dashboard/#=undefined&search=ongaaron96[here]. + +== Contributions to the User Guide + +|=== +|This section consists of my additions to QuickDoc's User Guide, showcasing my ability to write documentation targeted +at end users that might have limited technical knowledge. I will only be showcasing a truncated version of the +commands relating to Appointments, however the full User Guide can be found + https://github.com/CS2103-AY1819S2-W09-4/main/blob/master/docs/UserGuide.adoc[here]. +|=== + + +=== Appointment and Reminders +The Appointment and Reminder module give you more control over your busy schedule, featuring commands such as adding, removing, +and searching appointments and reminders. There is also a command to list out all free appointment timings to help you choose +your appointment timings more wisely. + +You will never forget about any appointments or tasks again! + + +''' +[[addapp]] +==== Adding appointments: `addapp` +After a consultation session, you may want to schedule a follow-up appointment with your patient. You can do so +with `addapp` to create an appointment with an existing patient in QuickDocs to add to your schedule. + +[TIP] +The `<>` command may be useful for you to first list out all free appointment timings for a given range of dates +before choosing an appropriate appointment timing! + +[NOTE] +To ensure that you do not forget about any future appointments, QuickDocs will automatically create a reminder for +the added appointment. You may not notice this reminder as it will only appear on your reminder sidebar closer to the date +of the actual appointment (on the week of the appointment date). + +{sp} + +*Format:* `addapp r/NRIC d/DATE s/START e/END c/COMMENT` + + +*Alias{nbsp}{nbsp}{nbsp}{nbsp}:* `aa` + + +[NOTE] +The format for a valid date is strictly `YYYY-MM-DD` (including the dashes) and a valid time is strictly `HH:MM` (including the colon). +If unsure, you may refer to the example below. + +{sp} + +*Example and results:* + +* `addapp r/S6394980I d/2019-07-23 s/16:00 e/17:00 c/Weekly checkup` + + +This adds an appointment allocated to the patient with NRIC S6394980I, on 23rd July 2019, from 4pm to 5pm. You can +include any comments or notes you want to this appointment, which is 'Weekly checkup' in this case. + +If the addition of the appointment was successful, QuickDocs will show the details of the added appointment on the +main display as demonstrated in the diagram below: + +.The main display after adding an appointment +image::ug-app_rem/addapp_success.png[width="600"] + +{sp} + +The addition of the appointment could have failed as there is a conflict in timing with another existing appointment. +QuickDocs will display a message in the input feedback box notifying you of this error, as demonstrated in the diagram below. + +.Adding an appointment that has conflicting timing with an existing appointment +image::ug-app_rem/addapp_clash.png[width="600"] + +{sp} + +Another reason for failing to add an appointment could be due to the appointment timing being outside of office hours +which is from 9am to 6pm. QuickDocs will alert you of this error, as demonstrated in the diagram below: + +.Adding an appointment that is not within the office hours, from 9am to 6pm +image::ug-app_rem/addapp_officehour.png[width="600"] + +''' +[[freeapp]] +==== List free appointment slots: `freeapp` +[[fa, freeapp]] +Unsure of what appointment timings are available in your schedule? Instead of using `listapp` to display all existing +appointments, use `freeapp` instead to display a more intuitive list of free appointment slots - +QuickDocs does the thinking for you! + +The free appointment slots will be ordered and listed, with the earliest date at the top and the latest at the bottom. +You can filter the free slots you want to see by specifying a `FORMAT` and a `DATE`. + +{sp} + +*Format:* `freeapp f/FORMAT d/DATE` + + +*Alias{nbsp}{nbsp}{nbsp}{nbsp}:* `fa` + + +[NOTE] +Similar to `listapp`, the valid keywords for `FORMAT` are only `day`, `week`, or `month`. + +{sp} + +*Examples:* + + +* `freeapp` + + +By default, `freeapp` will list all free appointment slots for the next month if there are no keywords provided. + +{sp} + + +* `freeapp f/week d/2019-07-23` + + +Lists all free appointment slots on the given week of 23rd July 2019, which is from 22nd to 28th July. A week starts on a +Monday and ends on a Sunday. + + +{sp} + +*Result:* + + +The free appointment slots will be ordered by date and time and will be listed on the main display of QuickDocs, +as demonstrated in the diagram below: + +.Listing free appointment slots on the week of 23rd July 2019. +image::ug-app_rem/freeapp_week.png[width="600"] + +== Contributions to the Developer Guide + +|=== +|This section includes my additions to QuickDoc's Developer Guide, showcasing my ability to write technical +documentation targeted at future developers. I will be describing my implementation of the `freeapp` command, +however I also explained the technicalities of the Storage component and the Appointment and Reminder module, +more of which can be found +https://github.com/CS2103-AY1819S2-W09-4/main/blob/master/docs/DeveloperGuide.adoc[here]. +|=== + +=== Free appointment slots +Before deciding on an appointment timing, the user can execute the `freeapp` command to list out all the timings available for +a new appointment booking. + +==== Command format: `freeapp f/FORMAT d/DATE` +We can see that the `freeapp` command takes in two parameters: + + +. `FORMAT`: can be `day`, `week`, or `month` + +. `DATE`: a valid date + +This command can be roughly translated to: + +_"Search for free appointment slots on the `FORMAT` (day/month/week) of `DATE`."_ + +The `FreeAppCommandParser` class will parse these two parameters into two dates, `LocalDate start` and `LocalDate end`, +representing the start and end dates of the search range for free appointment slots. `FreeAppCommandParser` then +constructs a `FreeAppCommand` object with the `start` and `end` fields. + +[NOTE] +If the user does not specify a `FORMAT` and `DATE`, `FORMAT` will default to `month` and `DATE` will default to the +next month's date, meaning that free appointment slots for the whole of the following month will be displayed. + +==== Current Implementation +The search is facilitated by the `AppointmentManager` class which stores all created `Appointments` in an `ArrayList`. +`AppointmentManager` contains the method `listFreeSlots()` which firstly calls `AppointmentManager#getFreeSlots()`. +`getFreeSlots()` is the main method that implements the logic behind `freeapp`. + +Given below are the steps taken when `listFreeSlots()` is called. + +Step 1. The method `listFreeSlots()` takes in the two arguments, `start` and `end`, which have been mentioned previously. +Firstly, `listFreeSlots()` calls `getFreeSlots()`, providing it with the same two arguments, to retrieve a `List` of +free `Slots` before it can parse them into a `String`. + +.Given search range from start to end date +image::dg-appointment/freeapp1.png[width="800"] + +{sp} + +Step 2. In `getFreeSlots()`, we first retrieve the existing appointments that are within this given search range +by using the method `AppointmentManager#getAppointments()`. + +.Retrieved appointments in the search range +image::dg-appointment/freeapp2.png[width="800"] + +{sp} + +Step 3. Next, we look at all the appointments that are present on the `start` date, as shown in the diagram below. +These appointments are sorted by date and time, with the earliest appointment on the left and the latest on the right. + +.Selected appointments on start date +image::dg-appointment/freeapp3.png[width="800"] + +[NOTE] +Since the appointments are already sorted, we do not need to search through the whole appointment list to +find appointments present on the `start` date. We can simply go through the list from the beginning +until we reach an appointment date that is not equals to `start`. + +{sp} + +Step 4. We fill in each empty 'gap' between any two appointments by creating a `Slot` object. + +Each `Slot` object represents a single time period on a single date. It has three attributes: + + +* `LocalDate date` - the date of this time slot. +* `LocalTime start` - the start time of this time slot. +* `LocalTime end` - the end time of this time slot. + +In this `freeapp` context, these slots created represents a time period without any scheduled appointments. + +.Slots created to fill in empty time slots +image::dg-appointment/freeapp4.png[width="800"] + +[NOTE] +Slots will only be created for timings during office hours (09:00 to 18:00). This is to prevent any possible +inconvenience caused if the user accidentally decides on a timing outside of office hours. +(Even though there will be an office hour constraint when the user eventually creates the appointment.) + +{sp} + +Step 5. We repeat Steps 3 and 4, replacing the `start` date with the remaining dates until the `end` date. +All slots created will be added into an `ArrayList` of free slots, `freeSlots`. + +.All empty time slots filled +image::dg-appointment/freeapp5.png[width="800"] + +{sp} + +Step 6. After all the slots are added, we return `freeSlots` to the caller function `listFreeSlots()`, +to generate a `String` that represents all the free slots to be appended onto the main display of the UI. + +We can summarize the steps taken after the `freeapp` command is called in the Sequence Diagram below: + +.Sequence diagram when `freeapp` is called +image::dg-appointment/freeapp_SD.png[width="800"] + + +==== Design Considerations +Listed below are some of the considerations we took when designing the `freeapp` command. + +1. This feature was implemented for the convenience of the user in choosing a valid appointment slot with his/her patient. +It is more intuitive to decide on an appointment slot based on all the empty slots shown, rather than listing out +all existing appointments using `listapp` and then figuring out what timings are available from there. + +2. We require the user to specify the search range by listing the `FORMAT` and `DATE` instead of the the `start` and +`end` dates directly, to make the command more user friendly. The user does not have to be bothered with the exact +range of dates to search, and can simply specify a rough date and be provided with information for the neighbouring +dates if the `FORMAT` given is `week` or `month`. Moreover, if the user wants to list all free slots for the whole +month, they do not have to check what the last date of the month is in order to specify the end date. + +{sp} + + +==== Alternatives Considered +Listed below are the methods considered to implement the `freeapp` command. + +[cols="1,2,2,3", options="header"] +|=== +|Alternative |Description |Pros | Cons +// row 1 +|*Maintain a permanent list of free slots* +|Maintain a list of free slots for a pre-determined range (e.g. next three months) instead of creating a new list +every time `appfree` is called. +|It will be quicker to search for free slots as the list is already created. We simply need to filter the list +with the given search range and print out the resulting filtered slots. +|Tedious work needs to be done to maintain this permanent list of free slots, as it has to be modified whenever an +appointment is added or deleted. + +Also, if the given search range is not within the range of this consistent list of free slots, +this list will still have to be created from scratch, defeating the purpose of maintaining this permanent list. +// row 2 +|*Generate free slots only when required* + +(Chosen implementation) +|We will only generate a list of free slots when the `freeapp` command is called. This list will be a one-time use +only and will not be stored in QuickDocs storage. +|The user is given the flexibility to specify the range of dates to list the free slots, as this list is generated +on the spot, and is not limited to the dates of a pre-determined list. +|Since the generated list of free slots is not stored, extra work will be done in generating the same free slots +when the next `freeapp` is called, that has a range of dates which overlaps the previous `freeapp` dates. +|=== + +We decided to implement the second method, as it is more straightforward. Here are the reasons why: + + +. The first implementation is actually just an extension of the chosen implementation as it still requires a way +to generate a list of free slots, either when QuickDocs is launched or when the user requests a search range outside +of the pre-determined list. +. The first implementation additionally requires more effort to maintain this permanent list whenever the +list of appointments is modified, which is not straightforward to implement. For example, we need a method to merge +two free slots when an appointment is deleted, and another method to split a free slot into two when an appointment +is added. +. The benefit of a permanent list of free slots is the quicker execution time of `freeapp`, which will typically +only be called a small number of times (around 10) a day, when the user books an appointment slot with his/her patient. +The total time saved on executing `freeapp` a small number of times is therefore negligible. +. QuickDocs already has plenty of data to be stored, such as appointments, consultations and medicine records. +The minimal benefits that a permanent list of free slots provide does not justify its additional storage cost. diff --git a/docs/team/simjiazhi.adoc b/docs/team/simjiazhi.adoc new file mode 100644 index 000000000000..7aadd0f763c5 --- /dev/null +++ b/docs/team/simjiazhi.adoc @@ -0,0 +1,326 @@ += Sim Jia Zhi - Project Portfolio +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets +ifdef::env-github[] +:tip-caption: :bulb: +:note-caption: :information_source: +endif::[] +:warning-caption: :warning: +:repoURL: https://github.com/CS2103-AY1819S2-W09-4/main/tree/master + +I am an undergraduate from National University of Singapore, School of Computing (SOC). This is one of +the software development projects i am involved in during my tenure in SOC. + +== PROJECT: QuickDocs + +== What is QuickDocs? +For our software engineering module, we were tasked with transforming a working application (consisting of 10,000 lines of code) into a new product to tackle a real-world issue. + +Our team decided to address the problem of limited manpower in family clinics. We realized that Doctors usually have to handle a multitude of tasks when running their +clinics, and the lack of manpower can potentially affect the quality of healthcare services provided negatively. + +We decided to create *QuickDocs* to help doctors ensure their service quality despite their limited manpower. QuickDocs is an all-in-one +application that can help doctors manage their patient records, inventory, appointments and conduct consultations. QuickDocs can also generate useful +financial statistics for doctors to make more informed decisions for their businesses. + +For more information, you can visit our github project page for QuickDocs https://github.com/CS2103-AY1819S2-W09-4/main[here]. + +== Summary of contributions + +I am in charge of developing the patient and consultation modules for this project. + +* The patient module is responsible for registering, listing, updating and deleting patient records +* The consultation module is responsible for conducting the consultation session and listing of past consultation records +belonging to each patient. + +The consultation process not only expedite an actual clinical operation, it is also the most integrated feature i have +developed in QuickDocs. As such, it will be the main feature showcased in this document. + +=== Main Features developed + +* *Created the consultation process of QuickDocs* +* *Code Contributed:* [https://nus-cs2103-ay1819s2.github.io/cs2103-dashboard/#=undefined&search=simjiazhi[Functional code]] +** *What it does:* Doctors can record the patient symptoms and prescribe drugs to patients using one-shot commands on a single interface. +** *Why is it needed:* Doctors no longer need to navigate through multiple menus to perform these actions. Recording of symptoms and prescribing +medicine can be done with minimal keystrokes. This will help to expedite the consultation process, and the doctor can tend to other patients more quickly. +** *How is it convenient:* The diagnosis and prescription synergizes with the command history. Doctors can +simply navigate to the command previously entered, make changes before re-entering the diagnosis or prescription command to rectify errors or make changes. +** *Highlights:* The prescription command is integrated with the medicine inventory. If the clinic have insufficient amount of medicine to support the prescriptions, the +doctor can be alerted and the incomplete prescription can be prevented. +** *Who / what to acknowledge:* My inspiration for the replacement style of the diagnosis and prescription editing is drawn from the original https://github.com/nus-cs2103-AY1819S2/addressbook-level4[*AddressBook Level 4*]'s edit command. +It uses this mechanism to provide a more efficient way to make changes to the person's particulars. + +==== Other features and enhancements: +* *Created the functionalities for the patient management module* +** Features include the registration, update, search and deletion of patient particulars in QuickDocs +* *Adapted the history command from the original sample application for use in QuickDocs* +** Allow editing and repeating of commands to be expedited by allowing users to navigate to previously entered command + +=== Other contributions: + +As the team lead, I am also responsible for the overall project coordination, making sure the deliverables +are ready before the deadlines. + +* *Project management:* +** Managed releases from v1.2.1 to v1.4 (3 releases) for QuickDocs on GitHub +** Managed milestone goals and the issue tracker on QuickDocs' GitHub project page + +* *Team collaboration:* +** Assisted in setting up integration tools for QuickDocs' Github projects such as coveralls and TravisCI +** Helped team member to develop QuickDoc's storage mechanism, allowing data generated across all modules to be saved in the Json format: +https://github.com/CS2103-AY1819S2-W09-4/main/pull/95[#95] +** Created the UI for QuickDocs and provided means for the different modules to display results from entered commands: +https://github.com/CS2103-AY1819S2-W09-4/main/pull/28[#28] +** Reviewed pull requests https://github.com/CS2103-AY1819S2-W09-4/main/pull/103[#103] and https://github.com/CS2103-AY1819S2-W09-4/main/pull/125[#125] +to enhance the edits on the user guide made by team members + +* *Documentation and tools:* +** Created a tool for team members to generate diagrams for documentations. +** Designed templates for writing sections in both the user and developer guides for QuickDocs + +== Contributions to the User Guide + +|=== +|_The following section showcase my additions to QuickDocs' https://github.com/CS2103-AY1819S2-W09-4/main/blob/master/docs/UserGuide.adoc[user guide]. +As mentioned earlier, the content covered in this section will be pertaining to +the consultation process. The diagnosis and prescription forms the core of the consultation process and we shall look into how the +the *diagnose* and *prescription* commands can be performed in QuickDocs:_ + +|=== + +[[diagnose, Diagnose patient]] +==== Diagnosing a patient: `diagnose` + +After the consultation session has began, you can start recording the patient's ailments. The various symptoms the patients have can be recorded down, +along with the final assessment of the illness the patient is currently having. + +To complete the diagnosis, the record must have one assessment and at least one symptom. + +*Format:* `diagnose s/SYMPTOM [s/SYMPTOM] ... a/ASSESSMENT` + +*Alias{nbsp}{nbsp}{nbsp}{nbsp}:* `d` + + +*Example:* + +* `diagnose s/runny nose s/sore throat s/phlegmy cough a/influenza` + + +The order of symptoms and assessment does not matter here. You record as many symptoms per diagnosis. + +*Results:* + +After entering the command, QuickDocs will show the symptoms and assessment recorded on the main display as demonstrated +in the following diagram. + +.Result of the diagnose command +image::diagnose_after.png[width="600"] + +Whenever you make a mistake, you can always press the UP and DOWN button on your keyboard to navigate to the +commands you have entered previously. You can then rectify your errors by making the changes and re-entering the +`diagnose` command. Doing so will replace the current erroneous diagnosis with the correct symptoms and assessment you have just entered. + +.Editing the diagnosis +image::diagnose_edit_after_ppp.png[width="600"] + +''' + +[[prescribe, Prescribe medicine]] +==== Prescribing medicine for a patient: `prescribe` + +After you are done recording the symptoms and assessing the illness of the patient, you can start prescribing medicine to your patient. + +For each medicine prescribed, the quantity must be specified. Like the <> command, you can always reenter the command to override +the current prescription should there be any errors made. + +A minimum of one medicine and one quantity is required to record a prescription entry. The order of quantity entered corresponds to the order of the medicine entered. + + +*Format:* `prescribe m/MEDICINE [m/MEDICINE] ... q/QUANTITY [q/QUANTITY]` + +or{nbsp}{nbsp}{nbsp}{nbsp}{nbsp}{nbsp}{nbsp}{nbsp}{nbsp} *:* `prescribe m/MEDICINE q/QUANTITY [m/MEDICINE] [q/QUANTITY] ...` + +*Alias{nbsp}{nbsp}{nbsp}{nbsp}:* `p` + + +*Example:* + +* `prescribe m/penicillin q/1 m/Afrin spray q/1 m/ibuprofen q/2` + + Prescription now consist of 1 unit of penicillin, 1 unit of afrin spray and 2 units of ibuprofen. +* `prescribe m/penicillin m/Afrin spray m/ibuprofen q/1 q/1 q/2` + + Same as above example, quantity ordered based on medicine order. + +*Results:* + +After the medicine-quantity pairings are entered, the prescription to address the patient's current condition will be displayed on the main display area of QuickDocs. Changes can still be made to the prescription as long as the consultation session is still ongoing. + +.Prescribing medicines to tackle the patient's current conditions +image::prescription_after_ppp.png[width="600"] + +== Contributions to the Developer Guide + +|=== +|_For the https://github.com/CS2103-AY1819S2-W09-4/main/blob/master/docs/DeveloperGuide.adoc[developer guide], I was in charge of explaning the design of the *User Interface* and also the technicalities involved in my assigned module. An in-depth explanation +of how the commands of the *consultation process*, particularly its diagnosis and prescription parts, will be shown in this section:_ +|=== + +==== Consultation process current implementation: + +The consultation process comprises of four stages: + +1. starting the consultation with a selected patient +2. entering the symptoms, assessment of the patient's current condition +3. entering the medicine to be prescribed +4. ending the consultation + +The consultation process is facilitated by the ConsultationManager class. +The consultationManager class holds the current consultation session and a list of past +consultation records for all the patients. + +Methods in the ConsultationManager comprises of: + +* `createConsultation(Patient)` -- Starts a consultation session with the current selected patient +* `diagnosePatient(Diagnosis)` -- Record symptoms patient mentioned and the assessment of the current condition. +* `prescribeMedicine(List of Prescriptions)` -- Prescribe the medicine and the quantities to be administered. +* `endConsultation()` -- Ends the consultation session. No further edits can be made to both prescription and diagnosis. + +Both `diagnosePatient` and `prescribeMedicine` are repeatable. The values entered during the repeated command will simply replace +the existing diagnosis / prescription. + +[NOTE] +QuickDocs only permit one ongoing consultation. During diagnosis and prescription, changes are only made to the current consultation +session. The previous consultations should not be edited to prevent falsification of medical records. The current consultation session +can only end after both the diagnosis and prescription are finalized. + +Given below is an example usage scenario: + +*Step 1.* A previously registered patient arrives and the doctor starts the session by +entering the consult command in this manner: `consult r/NRIC of the patient`. A message to indicate +the start of the consultation will be shown in the results display. + +* if the patient is new and his or her details are not recorded in QuickDocs, the command will not be executed and the doctor will be alerted +that the consultation cannot continue since no patient records with the entered Nric can be found. An invalid nric entered will also prompt the +same response + +image::dg-consultation/consultation1.png[width="600"] + +*Step 2.* The patient will tell the doctor what are his / her ailments. The doctor will record the symptoms +down. The doctor will then make the assessment of the illness the patient is having and execute the command by clicking +on the `Enter` on the keyboard. + +* The symptoms and assessment have to be prepended by the `s/` and `a/` prefix respectively +* The command entered by the doctor will look something like this: `diagnose s/constant coughing s/sore throat a/throat infection` + +image::dg-consultation/consultation2.png[width="600"] + +*Step 3.* Should the patient inform the doctor of additional symptoms after the diagnosis is given, the doctor can simply press +the up and down key to display the previously entered command on the userInput area. The doctor can then add the new symptom in and +press `Enter`, replacing the previously recorded diagnosis. + +image::dg-consultation/consultation3.png[width="600"] + + +*Step 4.* The doctor will then add the medicine to the prescription list, followed by the quantities. Medicine are prepended by the `m/` prefix while +quantities are prefixed by `q/`.The order of the quantity entered corresponds with the order the medicine is added in the command: + +* `prescribe m/Dextromethorphan m/ibuprofen q/1 q/2` In this case q/1 represents one unit of Dextromethorphan cough syrup is issued while +2 units of ibuprofen (inflammatory tablets) are issued to the patient +* Alternatively, the doctor can enter the quantity right after the medicine: `prescribe m/Dextromethorphan q/1 m/ibuprofen q/2` + +If any of the medicine issued are insufficient to complete the prescription, or is simply not in the inventory, a message will be displayed in +the inputFeedback area. The command will not be executed and remains in the userInput text field. The doctor can then make the changes to the command. + +image::dg-consultation/consultation4.png[width="600"] + +*Step 5.* Just like the diagnosis command, prescription can be replaced by reentering the command. + +image::dg-consultation/consultation5.png[width="600"] + +*Step 6.* After explaining the medicine intake to the patient, the doctor can then end the consultation session on QuickDocs by using the command +`endconsult`. No further changes to the consultation records can be made from this point on. + +==== Design considerations + +1. In a family clinic setting, doctors usually tend to only one patient at a time. This is why QuickDocs only allow a single +ongoing session in the consultation process. + +2. In Singapore, every person is given a unique NRIC / FIN number regardless of their citizenship statuses. As such the NRIC is used to +search for the patient records to start the consultation session. + +3. The prescription and diagnosis commands are made to override their previous states to ease the modification of consultation data. +Doctors can simply use the command history to navigate to the previous command entered, make the changes and then execute the command. This +allow them to simply add a few words to change consultation data rather than re-entering the entire command line. + +4. Prescription can actually be added before the diagnosis is recorded. The doctor could be expecting a patient for regular checkup and prepare the +prescription before the patient enters the room. If the condition remains the same as before, the doctor can simply enter the diagnosis to complete the +consultation session, cutting down the time spent on the consultation session. + +==== Alternatives considered + +Prior to the current implementation, a few options for the overall consultation process was considered: + +[cols="15,20,35, 30", options="header"] +|=== +|Alternative |Description |Pros | Cons +// row 1 +|*Consultation as one single command* +|Doctor enter `consult` followed by all the symptoms, assessment, prescriptions + and then execute +| Consultation is now restricted to just one class + +The consultation creation will truly be one-shot +| + +Input will be verbose, easy for the doctor to make mistakes + +Harder to spot and navigate to the erroneous part to make changes + +No room for the doctor to make changes as the consultation could have ended with erroneous information recorded +// row 2 +|*Iterative consultation creation* +|Doctor enter `consult`. + +Subsequently QuickDocs will prompt the doctor to enter +the diagnosis, followed by the prescription before ending the session. + +| Less likely to enter erroneous data as consultation is now broken down to different stages + +| Commands will take a longer time to complete + +Doctors can only diagnose and prescribe during the session, while other related actions +(such as listing past records) can only be done after the consultation + +// row 3 +| *Separate commands for start, diagnose, prescribe and end* + +*(Alternative selected and implemented)* +| +Doctors begin and end session with `consult` and `endconsult`. + +Prescriptions and diagnosis can be added or replaced using the `diagnose` and `prescribe` commands before the session ends. +| commands can be reused to perform both add and edit operations. The command history can be used to make changes to a previously entered command and +edit the diagnosis or prescription. + +Consultation do not need to follow a strict order. + +Other commands can be executed while a session is ongoing. + +| +Potentially more commands will be called when compared to the other options. + +diagnosis and prescription commands entered could be verbose and doctors might make mistakes more easily. + +|=== + +Here are some of the considerations taken before the decision was made: + +1. Since QuickDocs aim to provide a single interface for doctors to perform clinical operations more efficiently, the consultation +process will require one shot commands to fulfill the efficiency requirement of the overall product. + +2. It is highly possible for doctors to make mistakes with the one-shot commands, especially when there are so many parameters involved +in a single command. Therefore the implementation must provide a convenient form of error recovery. + +3. There could be interleaving operations between the modules, such as viewing past consultation records or +checking medicine inventory in the midst of the consultation. The implementation must be flexible enough +to allow cross module commands during a consultation. + +The selected option features lengthier one-shot commands and this could lead to doctors making mistakes more easily. +However, doctors can recover from erroneous commands very quickly by making use of the command history. + +While a consultation session is ongoing, doctors can still perform operations beyond the consultation module as well, and not just be +restricted to diagnosing the patient or prescribing medicine. + +As the selected option guarantees the flexibility and efficiency QuickDocs aim to deliver for doctors in family clinics, +it is currently implemented to help doctors conduct their consultation sessions much more smoothly. diff --git a/src/main/java/seedu/address/AppParameters.java b/src/main/java/quickdocs/AppParameters.java similarity index 93% rename from src/main/java/seedu/address/AppParameters.java rename to src/main/java/quickdocs/AppParameters.java index ab552c398f3d..adaad836d568 100644 --- a/src/main/java/seedu/address/AppParameters.java +++ b/src/main/java/quickdocs/AppParameters.java @@ -1,4 +1,4 @@ -package seedu.address; +package quickdocs; import java.nio.file.Path; import java.nio.file.Paths; @@ -7,8 +7,8 @@ import java.util.logging.Logger; import javafx.application.Application; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.util.FileUtil; +import quickdocs.commons.core.LogsCenter; +import quickdocs.commons.util.FileUtil; /** * Represents the parsed command-line parameters given to the application. diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/quickdocs/MainApp.java similarity index 61% rename from src/main/java/seedu/address/MainApp.java rename to src/main/java/quickdocs/MainApp.java index a92d4d5d71f0..f53574cf772c 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/quickdocs/MainApp.java @@ -1,4 +1,4 @@ -package seedu.address; +package quickdocs; import java.io.IOException; import java.nio.file.Path; @@ -6,37 +6,41 @@ import java.util.logging.Logger; import javafx.application.Application; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.scene.image.Image; +import javafx.scene.layout.AnchorPane; import javafx.stage.Stage; -import seedu.address.commons.core.Config; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.core.Version; -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.commons.util.ConfigUtil; -import seedu.address.commons.util.StringUtil; -import seedu.address.logic.Logic; -import seedu.address.logic.LogicManager; -import seedu.address.model.AddressBook; -import seedu.address.model.Model; -import seedu.address.model.ModelManager; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.ReadOnlyUserPrefs; -import seedu.address.model.UserPrefs; -import seedu.address.model.util.SampleDataUtil; -import seedu.address.storage.AddressBookStorage; -import seedu.address.storage.JsonAddressBookStorage; -import seedu.address.storage.JsonUserPrefsStorage; -import seedu.address.storage.Storage; -import seedu.address.storage.StorageManager; -import seedu.address.storage.UserPrefsStorage; -import seedu.address.ui.Ui; -import seedu.address.ui.UiManager; +import quickdocs.commons.core.Config; +import quickdocs.commons.core.LogsCenter; +import quickdocs.commons.core.Version; +import quickdocs.commons.exceptions.DataConversionException; +import quickdocs.commons.util.ConfigUtil; +import quickdocs.commons.util.StringUtil; +import quickdocs.logic.Logic; +import quickdocs.logic.LogicManager; +import quickdocs.model.Model; +import quickdocs.model.ModelManager; +import quickdocs.model.QuickDocs; +import quickdocs.model.ReadOnlyUserPrefs; +import quickdocs.model.UserPrefs; +import quickdocs.storage.JsonQuickDocsStorage; +import quickdocs.storage.JsonUserPrefsStorage; +import quickdocs.storage.QuickDocsStorage; +import quickdocs.storage.Storage; +import quickdocs.storage.StorageManager; +import quickdocs.storage.UserPrefsStorage; +import quickdocs.ui.RootLayoutController; +import quickdocs.ui.Ui; +import quickdocs.ui.UiManager; /** + * * The main entry point to the application. */ public class MainApp extends Application { - public static final Version VERSION = new Version(0, 6, 0, true); + public static final Version VERSION = new Version(1, 4, 0, true); private static final Logger logger = LogsCenter.getLogger(MainApp.class); @@ -46,6 +50,10 @@ public class MainApp extends Application { protected Model model; protected Config config; + // seedu.address.quickdocs attributes + private Stage primaryStage; + private AnchorPane rootLayout; + @Override public void init() throws Exception { logger.info("=============================[ Initializing AddressBook ]==========================="); @@ -56,12 +64,14 @@ public void init() throws Exception { UserPrefsStorage userPrefsStorage = new JsonUserPrefsStorage(config.getUserPrefsFilePath()); UserPrefs userPrefs = initPrefs(userPrefsStorage); - AddressBookStorage addressBookStorage = new JsonAddressBookStorage(userPrefs.getAddressBookFilePath()); - storage = new StorageManager(addressBookStorage, userPrefsStorage); + + QuickDocsStorage quickDocsStorage = new JsonQuickDocsStorage(userPrefs.getQuickDocsFilePath()); + storage = new StorageManager(userPrefsStorage, quickDocsStorage); initLogging(config); model = initModelManager(storage, userPrefs); + model.updateFilteredReminderList(model.getCurrentWeekRemindersPredicate()); logic = new LogicManager(model, storage); @@ -74,23 +84,22 @@ public void init() throws Exception { * or an empty address book will be used instead if errors occur when reading {@code storage}'s address book. */ private Model initModelManager(Storage storage, ReadOnlyUserPrefs userPrefs) { - Optional addressBookOptional; - ReadOnlyAddressBook initialData; + + QuickDocs initialQuickDocs = new QuickDocs(); + Optional quickDocs; try { - addressBookOptional = storage.readAddressBook(); - if (!addressBookOptional.isPresent()) { - logger.info("Data file not found. Will be starting with a sample AddressBook"); + quickDocs = storage.readQuickDocs(); + if (!quickDocs.isPresent()) { + logger.info("Data file not found. Will be starting with a empty quickdocs"); } - initialData = addressBookOptional.orElseGet(SampleDataUtil::getSampleAddressBook); + initialQuickDocs = quickDocs.orElse(initialQuickDocs); } catch (DataConversionException e) { - logger.warning("Data file not in the correct format. Will be starting with an empty AddressBook"); - initialData = new AddressBook(); + logger.warning("Data file not in the correct format. Will be starting with an empty QuickDocs"); } catch (IOException e) { - logger.warning("Problem while reading from the file. Will be starting with an empty AddressBook"); - initialData = new AddressBook(); + logger.warning("Problem while reading from the file. Will be starting with an empty QuickDocs"); } - return new ModelManager(initialData, userPrefs); + return new ModelManager(initialQuickDocs, userPrefs); } private void initLogging(Config config) { @@ -165,11 +174,11 @@ protected UserPrefs initPrefs(UserPrefsStorage storage) { return initializedPrefs; } - @Override - public void start(Stage primaryStage) { - logger.info("Starting AddressBook " + MainApp.VERSION); - ui.start(primaryStage); - } + //@Override + //public void start(Stage primaryStage) { + //logger.info("Starting AddressBook " + MainApp.VERSION); + //ui.start(primaryStage); + //} @Override public void stop() { @@ -184,4 +193,48 @@ public void stop() { public static void main(String[] args) { launch(args); } + + //Quickdocs codes + + @Override + public void start(Stage primaryStage) { + this.primaryStage = primaryStage; + this.primaryStage.setTitle("QuickDocs"); + this.primaryStage.setMinWidth(400); + this.primaryStage.setMinHeight(300); + Image image = new Image("images/quickdocsicon_512.png"); + this.primaryStage.getIcons().add(image); + initRootLayout(); + } + + /** + * Display the root layout when application is launched + */ + public void initRootLayout() { + + + try { + // Load root layout from fxml file. + FXMLLoader loader = new FXMLLoader(); + + //this will work when the rootlayout fxml file is in the resources folder but cannot link to controller + rootLayout = loader.load(getClass().getClassLoader().getResourceAsStream("view/RootLayout.fxml")); + RootLayoutController rootLayoutController = loader.getController(); + rootLayoutController.initialiseRootLayout(logic); + // Show the scene containing the root layout. + Scene scene = new Scene(rootLayout); + primaryStage.setScene(scene); + primaryStage.show(); + rootLayoutController.setPrimaryStage(primaryStage); + rootLayoutController.fillReminderList(); + } catch (IOException e) { + e.printStackTrace(); + } + + } + + + + + } diff --git a/src/main/java/seedu/address/commons/core/Config.java b/src/main/java/quickdocs/commons/core/Config.java similarity index 97% rename from src/main/java/seedu/address/commons/core/Config.java rename to src/main/java/quickdocs/commons/core/Config.java index 911457455217..f2a8a737d405 100644 --- a/src/main/java/seedu/address/commons/core/Config.java +++ b/src/main/java/quickdocs/commons/core/Config.java @@ -1,4 +1,4 @@ -package seedu.address.commons.core; +package quickdocs.commons.core; import java.nio.file.Path; import java.nio.file.Paths; diff --git a/src/main/java/seedu/address/commons/core/GuiSettings.java b/src/main/java/quickdocs/commons/core/GuiSettings.java similarity index 98% rename from src/main/java/seedu/address/commons/core/GuiSettings.java rename to src/main/java/quickdocs/commons/core/GuiSettings.java index 5ace559ad156..54215c736e6e 100644 --- a/src/main/java/seedu/address/commons/core/GuiSettings.java +++ b/src/main/java/quickdocs/commons/core/GuiSettings.java @@ -1,4 +1,4 @@ -package seedu.address.commons.core; +package quickdocs.commons.core; import java.awt.Point; import java.io.Serializable; diff --git a/src/main/java/seedu/address/commons/core/LogsCenter.java b/src/main/java/quickdocs/commons/core/LogsCenter.java similarity index 99% rename from src/main/java/seedu/address/commons/core/LogsCenter.java rename to src/main/java/quickdocs/commons/core/LogsCenter.java index 431e7185e762..490b571c13b2 100644 --- a/src/main/java/seedu/address/commons/core/LogsCenter.java +++ b/src/main/java/quickdocs/commons/core/LogsCenter.java @@ -1,4 +1,4 @@ -package seedu.address.commons.core; +package quickdocs.commons.core; import java.io.IOException; import java.util.Arrays; diff --git a/src/main/java/seedu/address/commons/core/Messages.java b/src/main/java/quickdocs/commons/core/Messages.java similarity index 74% rename from src/main/java/seedu/address/commons/core/Messages.java rename to src/main/java/quickdocs/commons/core/Messages.java index 1deb3a1e4695..e7f551830e1c 100644 --- a/src/main/java/seedu/address/commons/core/Messages.java +++ b/src/main/java/quickdocs/commons/core/Messages.java @@ -1,4 +1,4 @@ -package seedu.address.commons.core; +package quickdocs.commons.core; /** * Container for user visible messages. @@ -8,6 +8,4 @@ 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_PERSONS_LISTED_OVERVIEW = "%1$d persons listed!"; - } diff --git a/src/main/java/seedu/address/commons/core/Version.java b/src/main/java/quickdocs/commons/core/Version.java similarity index 98% rename from src/main/java/seedu/address/commons/core/Version.java rename to src/main/java/quickdocs/commons/core/Version.java index e117f91b3b2e..1e23fd103ea7 100644 --- a/src/main/java/seedu/address/commons/core/Version.java +++ b/src/main/java/quickdocs/commons/core/Version.java @@ -1,4 +1,4 @@ -package seedu.address.commons.core; +package quickdocs.commons.core; import java.util.regex.Matcher; import java.util.regex.Pattern; diff --git a/src/main/java/seedu/address/commons/core/index/Index.java b/src/main/java/quickdocs/commons/core/index/Index.java similarity index 97% rename from src/main/java/seedu/address/commons/core/index/Index.java rename to src/main/java/quickdocs/commons/core/index/Index.java index 19536439c099..8e135ba179a2 100644 --- a/src/main/java/seedu/address/commons/core/index/Index.java +++ b/src/main/java/quickdocs/commons/core/index/Index.java @@ -1,4 +1,4 @@ -package seedu.address.commons.core.index; +package quickdocs.commons.core.index; /** * Represents a zero-based or one-based index. diff --git a/src/main/java/seedu/address/commons/exceptions/DataConversionException.java b/src/main/java/quickdocs/commons/exceptions/DataConversionException.java similarity index 84% rename from src/main/java/seedu/address/commons/exceptions/DataConversionException.java rename to src/main/java/quickdocs/commons/exceptions/DataConversionException.java index 1f689bd8e3f9..720a647dc5d7 100644 --- a/src/main/java/seedu/address/commons/exceptions/DataConversionException.java +++ b/src/main/java/quickdocs/commons/exceptions/DataConversionException.java @@ -1,4 +1,4 @@ -package seedu.address.commons.exceptions; +package quickdocs.commons.exceptions; /** * Represents an error during conversion of data from one format to another diff --git a/src/main/java/seedu/address/commons/exceptions/IllegalValueException.java b/src/main/java/quickdocs/commons/exceptions/IllegalValueException.java similarity index 93% rename from src/main/java/seedu/address/commons/exceptions/IllegalValueException.java rename to src/main/java/quickdocs/commons/exceptions/IllegalValueException.java index 19124db485c9..9bf0692422c2 100644 --- a/src/main/java/seedu/address/commons/exceptions/IllegalValueException.java +++ b/src/main/java/quickdocs/commons/exceptions/IllegalValueException.java @@ -1,4 +1,4 @@ -package seedu.address.commons.exceptions; +package quickdocs.commons.exceptions; /** * Signals that some given data does not fulfill some constraints. diff --git a/src/main/java/seedu/address/commons/util/AppUtil.java b/src/main/java/quickdocs/commons/util/AppUtil.java similarity index 94% rename from src/main/java/seedu/address/commons/util/AppUtil.java rename to src/main/java/quickdocs/commons/util/AppUtil.java index da90201dfd64..22439a9a5f6e 100644 --- a/src/main/java/seedu/address/commons/util/AppUtil.java +++ b/src/main/java/quickdocs/commons/util/AppUtil.java @@ -1,9 +1,9 @@ -package seedu.address.commons.util; +package quickdocs.commons.util; import static java.util.Objects.requireNonNull; import javafx.scene.image.Image; -import seedu.address.MainApp; +import quickdocs.MainApp; /** * A container for App specific utility functions diff --git a/src/main/java/quickdocs/commons/util/CollectionUtil.java b/src/main/java/quickdocs/commons/util/CollectionUtil.java new file mode 100644 index 000000000000..3b5a762b29ba --- /dev/null +++ b/src/main/java/quickdocs/commons/util/CollectionUtil.java @@ -0,0 +1,73 @@ +package quickdocs.commons.util; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; +import java.util.stream.Stream; + +/** + * Utility methods related to Collections + */ +public class CollectionUtil { + + /** @see #requireAllNonNull(Collection) */ + public static void requireAllNonNull(Object... items) { + requireNonNull(items); + Stream.of(items).forEach(Objects::requireNonNull); + } + + /** + * Throws NullPointerException if {@code items} or any element of {@code items} is null. + */ + public static void requireAllNonNull(Collection items) { + requireNonNull(items); + items.forEach(Objects::requireNonNull); + } + + /** + * Returns true if {@code items} contain any elements that are non-null. + */ + public static boolean isAnyNonNull(Object... items) { + return items != null && Arrays.stream(items).anyMatch(Objects::nonNull); + } + + + /** + * A supporting function to find an element with the special key in a sorted arraylist + * @param list the list to search the element with the key in + * @param test a function that gives -1 if the operand's field of interest is less than the key; + * 0 if equal; 1 if larger than + * @param the type of the element of the list + * @return if the element with key exists in the list, return Optional.of that element; else return Optional.empty() + */ + public static Optional binarySearch(ArrayList list, Function test) { + int left = 0; + int right = list.size() - 1; + while (true) { + if (left > right) { + return Optional.empty(); + } + if (left == right) { + if (test.apply(list.get(left)) == 0) { + return Optional.of(list.get(left)); + } else { + return Optional.empty(); + } + } + int mid = (left + right) / 2; + int testResult = test.apply(list.get(mid)); + if (testResult < 0) { + left = mid + 1; + } else if (testResult == 0) { + return Optional.of(list.get(mid)); + } else { + right = mid - 1; + } + } + } +} diff --git a/src/main/java/seedu/address/commons/util/ConfigUtil.java b/src/main/java/quickdocs/commons/util/ConfigUtil.java similarity index 77% rename from src/main/java/seedu/address/commons/util/ConfigUtil.java rename to src/main/java/quickdocs/commons/util/ConfigUtil.java index f7f8a2bd44c0..b39d9e66b4c0 100644 --- a/src/main/java/seedu/address/commons/util/ConfigUtil.java +++ b/src/main/java/quickdocs/commons/util/ConfigUtil.java @@ -1,11 +1,11 @@ -package seedu.address.commons.util; +package quickdocs.commons.util; import java.io.IOException; import java.nio.file.Path; import java.util.Optional; -import seedu.address.commons.core.Config; -import seedu.address.commons.exceptions.DataConversionException; +import quickdocs.commons.core.Config; +import quickdocs.commons.exceptions.DataConversionException; /** * A class for accessing the Config File. diff --git a/src/main/java/seedu/address/commons/util/FileUtil.java b/src/main/java/quickdocs/commons/util/FileUtil.java similarity index 98% rename from src/main/java/seedu/address/commons/util/FileUtil.java rename to src/main/java/quickdocs/commons/util/FileUtil.java index b1e2767cdd92..aa6aaf4c1fec 100644 --- a/src/main/java/seedu/address/commons/util/FileUtil.java +++ b/src/main/java/quickdocs/commons/util/FileUtil.java @@ -1,4 +1,4 @@ -package seedu.address.commons.util; +package quickdocs.commons.util; import java.io.IOException; import java.nio.file.Files; diff --git a/src/main/java/seedu/address/commons/util/InvalidationListenerManager.java b/src/main/java/quickdocs/commons/util/InvalidationListenerManager.java similarity index 98% rename from src/main/java/seedu/address/commons/util/InvalidationListenerManager.java rename to src/main/java/quickdocs/commons/util/InvalidationListenerManager.java index 70165336db6d..5704db6a9946 100644 --- a/src/main/java/seedu/address/commons/util/InvalidationListenerManager.java +++ b/src/main/java/quickdocs/commons/util/InvalidationListenerManager.java @@ -1,4 +1,4 @@ -package seedu.address.commons.util; +package quickdocs.commons.util; import static java.util.Objects.requireNonNull; diff --git a/src/main/java/seedu/address/commons/util/JsonUtil.java b/src/main/java/quickdocs/commons/util/JsonUtil.java similarity index 97% rename from src/main/java/seedu/address/commons/util/JsonUtil.java rename to src/main/java/quickdocs/commons/util/JsonUtil.java index 8ef609f055df..f0183b696365 100644 --- a/src/main/java/seedu/address/commons/util/JsonUtil.java +++ b/src/main/java/quickdocs/commons/util/JsonUtil.java @@ -1,4 +1,4 @@ -package seedu.address.commons.util; +package quickdocs.commons.util; import static java.util.Objects.requireNonNull; @@ -20,8 +20,8 @@ import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.exceptions.DataConversionException; +import quickdocs.commons.core.LogsCenter; +import quickdocs.commons.exceptions.DataConversionException; /** * Converts a Java object instance to JSON and vice versa diff --git a/src/main/java/seedu/address/commons/util/StringUtil.java b/src/main/java/quickdocs/commons/util/StringUtil.java similarity index 81% rename from src/main/java/seedu/address/commons/util/StringUtil.java rename to src/main/java/quickdocs/commons/util/StringUtil.java index 61cc8c9a1cb8..5d9017797afa 100644 --- a/src/main/java/seedu/address/commons/util/StringUtil.java +++ b/src/main/java/quickdocs/commons/util/StringUtil.java @@ -1,7 +1,7 @@ -package seedu.address.commons.util; +package quickdocs.commons.util; import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; +import static quickdocs.commons.util.AppUtil.checkArgument; import java.io.PrintWriter; import java.io.StringWriter; @@ -65,4 +65,18 @@ public static boolean isNonZeroUnsignedInteger(String s) { return false; } } + + /** + * Convert path to a String to display to User + * @param path the path field + * @return A string representation of the path + */ + public static String fromPathToString(String[] path) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < path.length - 1; i++) { + sb.append(path[i] + "\\"); + } + sb.append(path[path.length - 1]); + return sb.toString(); + } } diff --git a/src/main/java/seedu/address/logic/CommandHistory.java b/src/main/java/quickdocs/logic/CommandHistory.java similarity index 98% rename from src/main/java/seedu/address/logic/CommandHistory.java rename to src/main/java/quickdocs/logic/CommandHistory.java index 404675e43811..44a41cc71327 100644 --- a/src/main/java/seedu/address/logic/CommandHistory.java +++ b/src/main/java/quickdocs/logic/CommandHistory.java @@ -1,4 +1,4 @@ -package seedu.address.logic; +package quickdocs.logic; import static java.util.Objects.requireNonNull; diff --git a/src/main/java/quickdocs/logic/Logic.java b/src/main/java/quickdocs/logic/Logic.java new file mode 100644 index 000000000000..82e8fb414466 --- /dev/null +++ b/src/main/java/quickdocs/logic/Logic.java @@ -0,0 +1,55 @@ +package quickdocs.logic; + +import java.util.ArrayList; + +import javafx.beans.property.ReadOnlyProperty; +import javafx.collections.ObservableList; +import quickdocs.commons.core.GuiSettings; +import quickdocs.logic.commands.CommandResult; +import quickdocs.logic.commands.exceptions.CommandException; +import quickdocs.logic.parser.exceptions.ParseException; +import quickdocs.model.reminder.Reminder; + + +/** + * API of the Logic component + */ +public interface Logic { + /** + * Executes the command and returns the result. + * @param commandText The command as entered by the user. + * @return the result of the command execution. + * @throws CommandException If an error occurs during command execution. + * @throws ParseException If an error occurs during parsing. + */ + CommandResult execute(String commandText) throws CommandException, ParseException; + + /** + * Returns an unmodifiable view of the list of commands entered by the user. + * The list is ordered from the least recent command to the most recent command. + */ + ObservableList getHistory(); + /** + * Returns the user prefs' GUI settings. + */ + GuiSettings getGuiSettings(); + + /** + * Set the user prefs' GUI settings. + */ + void setGuiSettings(GuiSettings guiSettings); + + ObservableList getFilteredReminderList(); + + ReadOnlyProperty selectedReminderProperty(); + + void setSelectedReminder(Reminder reminder); + + ArrayList getDirectorySuggestions(String path); + + ArrayList getMedicineSuggestions(String path); + + boolean isDirectoryFormat(String rawArgs); + + boolean isMedicineAllowed(String rawArgs); +} diff --git a/src/main/java/quickdocs/logic/LogicManager.java b/src/main/java/quickdocs/logic/LogicManager.java new file mode 100644 index 000000000000..b180178b9812 --- /dev/null +++ b/src/main/java/quickdocs/logic/LogicManager.java @@ -0,0 +1,115 @@ +package quickdocs.logic; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.logging.Logger; + +import javafx.beans.property.ReadOnlyProperty; +import javafx.collections.ObservableList; +import quickdocs.commons.core.GuiSettings; +import quickdocs.commons.core.LogsCenter; +import quickdocs.logic.commands.Command; +import quickdocs.logic.commands.CommandResult; +import quickdocs.logic.commands.exceptions.CommandException; +import quickdocs.logic.parser.QuickDocsParser; +import quickdocs.logic.parser.exceptions.ParseException; +import quickdocs.model.Model; +import quickdocs.model.reminder.Reminder; +import quickdocs.storage.Storage; + +/** + * The main LogicManager of the app. + */ +public class LogicManager implements Logic { + public static final String FILE_OPS_ERROR_MESSAGE = "Could not save data to file: "; + private final Logger logger = LogsCenter.getLogger(LogicManager.class); + + private final Model model; + private final Storage storage; + private final CommandHistory history; + private final QuickDocsParser quickDocsParser; + + public LogicManager(Model model, Storage storage) { + this.model = model; + this.storage = storage; + history = new CommandHistory(); + quickDocsParser = new QuickDocsParser(); + } + + @Override + public CommandResult execute(String commandText) throws CommandException, ParseException { + logger.info("----------------[USER COMMAND][" + commandText + "]"); + + CommandResult commandResult; + try { + Command command = quickDocsParser.parseCommand(commandText); + commandResult = command.execute(model, history); + } finally { + history.add(commandText); + } + + if (model.getQuickDocs().isModified()) { + logger.info("QuickDocs modified, saving to file."); + try { + storage.saveQuickDocs(model.getQuickDocs()); + model.getQuickDocs().indicateModification(false); + } catch (IOException ioe) { + throw new CommandException(FILE_OPS_ERROR_MESSAGE + ioe, ioe); + } + } + + return commandResult; + } + + @Override + public ObservableList getHistory() { + return history.getHistory(); + } + + @Override + public GuiSettings getGuiSettings() { + return model.getGuiSettings(); + } + + @Override + public void setGuiSettings(GuiSettings guiSettings) { + model.setGuiSettings(guiSettings); + } + + @Override + public ObservableList getFilteredReminderList() { + return model.getFilteredReminderList(); + } + + @Override + public ReadOnlyProperty selectedReminderProperty() { + return model.selectedReminderProperty(); + } + + @Override + public void setSelectedReminder(Reminder reminder) { + model.setSelectedReminder(reminder); + } + + @Override + public ArrayList getDirectorySuggestions (String rawArgs) { + String rawPath = quickDocsParser.getArgument(rawArgs); + return model.getDirectorySuggestions(rawPath); + } + + @Override + public ArrayList getMedicineSuggestions (String rawArgs) { + String rawPath = quickDocsParser.getArgument(rawArgs); + return model.getMedicineSuggestions(rawPath); + } + + @Override + public boolean isDirectoryFormat(String rawArgs) { + return quickDocsParser.isDirectoryFormat(rawArgs); + } + + @Override + public boolean isMedicineAllowed(String rawArgs) { + return quickDocsParser.isMedicineAllowed(rawArgs); + } +} diff --git a/src/main/java/quickdocs/logic/commands/AbortConsultationCommand.java b/src/main/java/quickdocs/logic/commands/AbortConsultationCommand.java new file mode 100644 index 000000000000..7bd8778b77f4 --- /dev/null +++ b/src/main/java/quickdocs/logic/commands/AbortConsultationCommand.java @@ -0,0 +1,29 @@ +package quickdocs.logic.commands; + +import quickdocs.logic.CommandHistory; +import quickdocs.logic.commands.exceptions.CommandException; +import quickdocs.model.Model; + +/** + * Aborts the current consultation + */ +public class AbortConsultationCommand extends Command { + + public static final String COMMAND_WORD = "abort"; + public static final String COMMAND_ALIAS = "ab"; + + public static final String NO_CURRENT_CONSULTATION = "No ongoing session to abort\n"; + public static final String ABORT_CONSULT_FEEDBACK = "Consultation session aborted\n"; + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + + if (model.getCurrentConsultation() == null) { + throw new CommandException(NO_CURRENT_CONSULTATION); + } + + model.abortConsultation(); + + return new CommandResult(ABORT_CONSULT_FEEDBACK); + } +} diff --git a/src/main/java/quickdocs/logic/commands/AddAppCommand.java b/src/main/java/quickdocs/logic/commands/AddAppCommand.java new file mode 100644 index 000000000000..7404f7728a6e --- /dev/null +++ b/src/main/java/quickdocs/logic/commands/AddAppCommand.java @@ -0,0 +1,113 @@ +package quickdocs.logic.commands; + +import static java.util.Objects.requireNonNull; +import static quickdocs.logic.parser.AddAppCommandParser.PREFIX_COMMENT; +import static quickdocs.logic.parser.AddAppCommandParser.PREFIX_DATE; +import static quickdocs.logic.parser.AddAppCommandParser.PREFIX_END; +import static quickdocs.logic.parser.AddAppCommandParser.PREFIX_NRIC; +import static quickdocs.logic.parser.AddAppCommandParser.PREFIX_START; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.Optional; + +import quickdocs.logic.CommandHistory; +import quickdocs.logic.commands.exceptions.CommandException; +import quickdocs.model.Model; +import quickdocs.model.appointment.Appointment; +import quickdocs.model.appointment.AppointmentManager; +import quickdocs.model.patient.Nric; +import quickdocs.model.patient.Patient; + +/** + * Adds an {@code Appointment} to QuickDocs. + */ +public class AddAppCommand extends Command { + + public static final String COMMAND_WORD = "addapp"; + public static final String COMMAND_ALIAS = "aa"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds an appointment to QuickDocs. " + + "Parameters: " + + PREFIX_NRIC + "NRIC " + + PREFIX_DATE + "DATE " + + PREFIX_START + "START " + + PREFIX_END + "END " + + PREFIX_COMMENT + "COMMENT\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_NRIC + "S9625555I " + + PREFIX_DATE + "2019-10-23 " + + PREFIX_START + "16:00 " + + PREFIX_END + "17:00 " + + PREFIX_COMMENT + "Monthly checkup\n"; + + public static final String MESSAGE_SUCCESS = "Appointment added:\n%1$s\n"; + public static final String MESSAGE_CONFLICTING_APP = "There are clashes in the time slot chosen." + + " You can use the 'freeapp' command to find free slots!"; + public static final String MESSAGE_PATIENT_NOT_FOUND = "No patient with the given NRIC found"; + public static final String MESSAGE_START_EQUALS_END = "Appointment start time and end time should not be the same."; + public static final String MESSAGE_START_AFTER_END = "Appointment start time should not be after end time."; + public static final String MESSAGE_NON_OFFICE_HOURS = "Appointment timing is outside of office hours."; + + private final Nric nric; + private final LocalDate date; + private final LocalTime start; + private final LocalTime end; + private final String comment; + + /** + * Creates an {@code AddAppCommand} to add the specified {@code Appointment}. + */ + public AddAppCommand(Nric nric, LocalDate date, LocalTime start, LocalTime end, String comment) { + this.nric = nric; + this.date = date; + this.start = start; + this.end = end; + this.comment = comment; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + + // check if given NRIC is valid + Optional patientToAdd = model.getPatientByNric(nric); + if (!patientToAdd.isPresent()) { + throw new CommandException(MESSAGE_PATIENT_NOT_FOUND); + } + + // check if given start and end timings are valid + if (start.isBefore(AppointmentManager.OPENING_HOUR) || start.isAfter(AppointmentManager.CLOSING_HOUR) + || end.isBefore(AppointmentManager.OPENING_HOUR) || end.isAfter(AppointmentManager.CLOSING_HOUR)) { + throw new CommandException(MESSAGE_NON_OFFICE_HOURS); + } + + if (start.equals(end)) { + throw new CommandException(MESSAGE_START_EQUALS_END); + } + + if (start.isAfter(end)) { + throw new CommandException(MESSAGE_START_AFTER_END); + } + assert start.isBefore(end); + + // create the appointment and check if it has time conflicts with existing appointments + Appointment appToAdd = new Appointment(patientToAdd.get(), date, start, end, comment); + if (model.hasTimeConflicts(appToAdd)) { + throw new CommandException(MESSAGE_CONFLICTING_APP); + } + + model.addApp(appToAdd); + return new CommandResult(String.format(MESSAGE_SUCCESS, appToAdd)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof AddAppCommand // instanceof handles nulls + && nric.equals(((AddAppCommand) other).nric) + && date.equals(((AddAppCommand) other).date) + && start.equals(((AddAppCommand) other).start) + && end.equals(((AddAppCommand) other).end) + && comment.equals(((AddAppCommand) other).comment)); + } +} diff --git a/src/main/java/quickdocs/logic/commands/AddDirectoryCommand.java b/src/main/java/quickdocs/logic/commands/AddDirectoryCommand.java new file mode 100644 index 000000000000..877dfe9f34c9 --- /dev/null +++ b/src/main/java/quickdocs/logic/commands/AddDirectoryCommand.java @@ -0,0 +1,65 @@ +package quickdocs.logic.commands; + +import static java.util.Objects.requireNonNull; +import static quickdocs.commons.util.StringUtil.fromPathToString; + +import java.util.Arrays; + +import quickdocs.logic.CommandHistory; +import quickdocs.logic.commands.exceptions.CommandException; +import quickdocs.model.Model; + +/** + * A command to add new directory + */ +public class AddDirectoryCommand extends Command { + + public static final String COMMAND_WORD = "adddirec"; + public static final String COMMAND_ALIAS = "ad"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a new directory to the directory specified. " + + "Parameters: " + + "[path of directory separated by \\] " + + "[name of new directory]\n" + + "Example: " + COMMAND_WORD + " " + + "root\\TCM Herbs"; + + public static final String MESSAGE_SUCCESS = "New directory with name \"%1$s\" added to %2$s\n"; + + private final String[] path; + private final String name; + + public AddDirectoryCommand(String[] path, String directoryName) { + this.path = path; + this.name = directoryName; + } + + /** + * Add a new directory with the given name to a given directory + * @param model {@code Model} which the command should operate on. + * @param commandHistory {@code CommandHistory} which the command should operate on. + * @return A commandresult showing the result + * @throws CommandException + */ + public CommandResult execute(Model model, CommandHistory commandHistory) throws CommandException { + requireNonNull(model); + try { + model.addDirectory(name, path); + return new CommandResult(String.format(MESSAGE_SUCCESS, name, fromPathToString(path))); + } catch (Exception ex) { + throw new CommandException(ex.getMessage()); + } + } + + /** + * Checks whether other equals this + * @param other the object to check against + * @return if they are equal + */ + public boolean equals(Object other) { + return other == this + || (other instanceof AddDirectoryCommand + && Arrays.equals(this.path, ((AddDirectoryCommand) other).path)) + && this.name.equals(((AddDirectoryCommand) other).name); + } +} diff --git a/src/main/java/quickdocs/logic/commands/AddMedicineCommand.java b/src/main/java/quickdocs/logic/commands/AddMedicineCommand.java new file mode 100644 index 000000000000..0da91f2ee635 --- /dev/null +++ b/src/main/java/quickdocs/logic/commands/AddMedicineCommand.java @@ -0,0 +1,106 @@ +package quickdocs.logic.commands; + +import static java.util.Objects.requireNonNull; +import static quickdocs.commons.util.StringUtil.fromPathToString; +import static quickdocs.model.medicine.MedicineManager.ERROR_MESSAGE_NO_EXISTING_MED_FOUND; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Optional; + +import quickdocs.logic.CommandHistory; +import quickdocs.logic.commands.exceptions.CommandException; +import quickdocs.model.Model; +import quickdocs.model.medicine.Medicine; + +/** + * An command to add Medicine to the path specified. If no Medicine with same name yet exists, new medicine is added. + * If medicine with same name exists and quantity is not specified, add the existing medicine to desire directory. + */ +public class AddMedicineCommand extends Command { + + public static final String COMMAND_WORD = "addmed"; + public static final String COMMAND_ALIAS = "am"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a medicine to the specified directory.\n" + + "If no quantity specified and medicine with the same name already exists in the storage, " + + "add this medicine to the directory specified." + + "Parameters: " + + "[Directory path separated by \\] " + + "[Name of Medicine] " + + "[(For new medicine only)Price of Medicine] " + + "[(For new medicine only)Amount of Medicine]\n" + + "Example: " + COMMAND_WORD + " " + + "root\\TCM\\herbs " + + "Healroot " + + "p/3.41 q/50"; + + public static final String MESSAGE_SUCCESS_NEW_MED = + "New Medicine added: %1$s with quantity at %2$d and price at %3$s\n"; + public static final String MESSAGE_SUCCESS_EXISTING_MED = "Existing %1$s, added to %2$s\n"; + public static final String ERRORMESSAGE_INSUFFICIENTINFO_NEWMEDICINE = + "Only one field among price and quantity is supplied for new medicine"; + + private final String name; + private final Optional quantity; + private final String[] path; + private Optional price; + + + public AddMedicineCommand(String[] path, String medicineName, + Optional quantity, Optional price) { + this.name = medicineName; + this.quantity = quantity; + this.path = path; + this.price = price; + } + + /** + * If the command suggests there is a possiblity that medicine with same name already exists, + * check whether it is true. + * If medicine with same name already exists, add the medicine to desired directory + * Else, create new medicine to be place under the desired directory + * @param model {@code Model} which the command should operate on. + * @param history {@code CommandHistory} which the command should operate on. + * @return A commandresult showing what is done + * @throws CommandException + */ + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + try { + if ((price.isPresent() && !quantity.isPresent()) + || (!price.isPresent() && quantity.isPresent())) { + throw new CommandException(ERRORMESSAGE_INSUFFICIENTINFO_NEWMEDICINE); + } + boolean existing = !price.isPresent() && !quantity.isPresent(); + Optional findMedicine = model.findMedicine(name); + if (existing) { + if (findMedicine.isPresent()) { + model.addExistingMedicineToDirectory(findMedicine.get(), path); + String feedback = String.format(MESSAGE_SUCCESS_EXISTING_MED, + findMedicine.get().toString(), fromPathToString(path)); + return new CommandResult(feedback); + } else { + throw new CommandException(String.format(ERROR_MESSAGE_NO_EXISTING_MED_FOUND, name)); + } + } else { + model.addMedicine(name, quantity.get(), path, price.get()); + return new CommandResult(String.format(MESSAGE_SUCCESS_NEW_MED, name, + quantity.get(), price.get().toString())); + } + } catch (Exception ex) { + throw new CommandException(ex.getMessage()); + } + } + + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof AddMedicineCommand + && name.equals(((AddMedicineCommand) other).name) + && quantity.equals(((AddMedicineCommand) other).quantity) + && Arrays.equals(path, ((AddMedicineCommand) other).path) + && price.equals(((AddMedicineCommand) other).price)); + } +} diff --git a/src/main/java/quickdocs/logic/commands/AddPatientCommand.java b/src/main/java/quickdocs/logic/commands/AddPatientCommand.java new file mode 100644 index 000000000000..40f643850f39 --- /dev/null +++ b/src/main/java/quickdocs/logic/commands/AddPatientCommand.java @@ -0,0 +1,76 @@ +package quickdocs.logic.commands; + +import static quickdocs.logic.parser.AddPatientParser.PREFIX_ADDRESS; +import static quickdocs.logic.parser.AddPatientParser.PREFIX_CONTACT; +import static quickdocs.logic.parser.AddPatientParser.PREFIX_DOB; +import static quickdocs.logic.parser.AddPatientParser.PREFIX_EMAIL; +import static quickdocs.logic.parser.AddPatientParser.PREFIX_GENDER; +import static quickdocs.logic.parser.AddPatientParser.PREFIX_NAME; +import static quickdocs.logic.parser.AddPatientParser.PREFIX_NRIC; +import static quickdocs.logic.parser.AddPatientParser.PREFIX_TAG; + +import quickdocs.logic.CommandHistory; +import quickdocs.logic.commands.exceptions.CommandException; +import quickdocs.model.Model; +import quickdocs.model.patient.Patient; + +/** + * Command to add a patient record into QuickDocs + */ +public class AddPatientCommand extends Command { + public static final String COMMAND_WORD = "addpat"; + public static final String COMMAND_ALIAS = "ap"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a patient to QuickDocs.\n" + + "Parameters: " + + PREFIX_NAME + "NAME " + + PREFIX_NRIC + "NRIC " + + PREFIX_DOB + "DATE OF BIRTH " + + PREFIX_ADDRESS + "ADDRESS " + + PREFIX_EMAIL + "EMAIL " + + PREFIX_CONTACT + "CONTACT " + + PREFIX_GENDER + "GENDER " + + "[" + PREFIX_TAG + "TAG]...\n" + + "Note: You can enter the parameters in any order as long as the essential parameters are captured.\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_NAME + "John Doe " + + PREFIX_NRIC + "S9876543A " + + PREFIX_DOB + "1980-03-03 " + + PREFIX_ADDRESS + "311, Clementi Ave 2, #02-25 " + + PREFIX_EMAIL + "johnd@example.com " + + PREFIX_CONTACT + "92344321 " + + PREFIX_GENDER + "M " + + PREFIX_TAG + "highbloodpressure\n"; + public static final String CONFLICTING_NRIC = "Patient with same NRIC already existed"; + + private Patient patientToAdd; + + public AddPatientCommand(Patient patient) { + patientToAdd = patient; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + if (model.duplicatePatient(patientToAdd)) { + throw new CommandException(CONFLICTING_NRIC); + } + model.addPatient(patientToAdd); + + StringBuilder sb = new StringBuilder(); + sb.append("Patient Added:\n"); + sb.append("==============================\n"); + sb.append(patientToAdd.toString()); + CommandResult result = new CommandResult(sb.toString(), false, false); + return result; + } + + public Patient getPatientToAdd() { + return patientToAdd; + } + + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof AddPatientCommand + && patientToAdd.equals(((AddPatientCommand) other).getPatientToAdd())); + } +} diff --git a/src/main/java/quickdocs/logic/commands/AddRemCommand.java b/src/main/java/quickdocs/logic/commands/AddRemCommand.java new file mode 100644 index 000000000000..2eb94ad2fb51 --- /dev/null +++ b/src/main/java/quickdocs/logic/commands/AddRemCommand.java @@ -0,0 +1,67 @@ +package quickdocs.logic.commands; + +import static java.util.Objects.requireNonNull; +import static quickdocs.logic.parser.AddRemCommandParser.PREFIX_COMMENT; +import static quickdocs.logic.parser.AddRemCommandParser.PREFIX_DATE; +import static quickdocs.logic.parser.AddRemCommandParser.PREFIX_END; +import static quickdocs.logic.parser.AddRemCommandParser.PREFIX_START; +import static quickdocs.logic.parser.AddRemCommandParser.PREFIX_TITLE; + +import quickdocs.logic.CommandHistory; +import quickdocs.logic.commands.exceptions.CommandException; +import quickdocs.model.Model; +import quickdocs.model.reminder.Reminder; + +/** + * Adds a {@code Reminder} to QuickDocs. + */ +public class AddRemCommand extends Command { + + public static final String COMMAND_WORD = "addrem"; + public static final String COMMAND_ALIAS = "ar"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a reminder to QuickDocs. " + + "Parameters: " + + PREFIX_TITLE + "TITLE " + + PREFIX_DATE + "DATE " + + PREFIX_START + "START " + + "[" + PREFIX_END + "END] " + + "[" + PREFIX_COMMENT + "COMMENT]\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_TITLE + "Check financial records " + + PREFIX_DATE + "2019-05-22 " + + PREFIX_START + "18:00 " + + PREFIX_END + "18:30 " + + PREFIX_COMMENT + "Weekly check\n"; + + public static final String MESSAGE_SUCCESS = "Reminder added:\n%1$s\n"; + public static final String MESSAGE_DUPLICATE_REM = "This reminder has already been added"; + + private final Reminder toAdd; + + /** + * Creates an {@code AddRemCommand} to add the specified {@code Reminder} + */ + public AddRemCommand(Reminder toAdd) { + this.toAdd = toAdd; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + + // check if there is a duplicate reminder already added + if (model.duplicateRem(toAdd)) { + throw new CommandException(MESSAGE_DUPLICATE_REM); + } + + model.addRem(toAdd); + return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof AddRemCommand // instanceof handles nulls + && toAdd.equals(((AddRemCommand) other).toAdd)); + } +} diff --git a/src/main/java/quickdocs/logic/commands/AlarmCommand.java b/src/main/java/quickdocs/logic/commands/AlarmCommand.java new file mode 100644 index 000000000000..55c9b926c206 --- /dev/null +++ b/src/main/java/quickdocs/logic/commands/AlarmCommand.java @@ -0,0 +1,70 @@ +package quickdocs.logic.commands; + +import java.util.Arrays; +import java.util.Optional; + +import quickdocs.logic.CommandHistory; +import quickdocs.logic.commands.exceptions.CommandException; +import quickdocs.model.Model; +import quickdocs.model.medicine.Directory; +import quickdocs.model.medicine.Medicine; + +/** + * An Command setting alarm level for directory/medicine given by the path + */ +public class AlarmCommand extends Command { + + public static final String COMMAND_WORD = "alarm"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": set the alarm level for a specific medicine / every medicine under a directory. " + + "Parameters: " + + "Medicine / directory path separated by \\ " + + "Alarm level\n" + + "Example: " + COMMAND_WORD + " " + + "root\\TCM 30"; + + public static final String MESSAGE_SUCCESS = "Alarm level is set to %1$d for:\n%2$s\n"; + + private String[] path; + private int alarmLevel; + + public AlarmCommand(String[] path, int alarmLevel) { + this.path = path; + this.alarmLevel = alarmLevel; + } + + /** + * Set the alarm level to the quantity specifies by the user + * @param model {@code Model} which the command should operate on. + * @param history {@code CommandHistory} which the command should operate on. + * @return A CommandResult showing the operation done + * @throws CommandException + */ + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + try { + Optional directory = model.findDirectory(path); + if (!directory.isPresent()) { + Optional medicine = model.findMedicine(path); + if (!medicine.isPresent()) { + throw new CommandException("No medicine/directory found at the given path"); + } + model.setThreshold(medicine.get(), alarmLevel); + return new CommandResult(String.format(MESSAGE_SUCCESS, alarmLevel, medicine.get().toString())); + } else { + model.setThreshold(directory.get(), alarmLevel); + return new CommandResult(String.format(MESSAGE_SUCCESS, alarmLevel, directory.get().toString())); + } + } catch (Exception ex) { + throw new CommandException(ex.getMessage()); + } + } + + @Override + public boolean equals(Object other) { + return other == this || ( + other instanceof AlarmCommand + && Arrays.equals(this.path, ((AlarmCommand) other).path) + && this.alarmLevel == ((AlarmCommand) other).alarmLevel); + } +} diff --git a/src/main/java/seedu/address/logic/commands/Command.java b/src/main/java/quickdocs/logic/commands/Command.java similarity index 77% rename from src/main/java/seedu/address/logic/commands/Command.java rename to src/main/java/quickdocs/logic/commands/Command.java index 34e99d786ec6..65673516cd12 100644 --- a/src/main/java/seedu/address/logic/commands/Command.java +++ b/src/main/java/quickdocs/logic/commands/Command.java @@ -1,8 +1,8 @@ -package seedu.address.logic.commands; +package quickdocs.logic.commands; -import seedu.address.logic.CommandHistory; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.Model; +import quickdocs.logic.CommandHistory; +import quickdocs.logic.commands.exceptions.CommandException; +import quickdocs.model.Model; /** * Represents a command with hidden internal logic and the ability to be executed. diff --git a/src/main/java/seedu/address/logic/commands/CommandResult.java b/src/main/java/quickdocs/logic/commands/CommandResult.java similarity index 97% rename from src/main/java/seedu/address/logic/commands/CommandResult.java rename to src/main/java/quickdocs/logic/commands/CommandResult.java index 92f900b7916d..c0b0b114692d 100644 --- a/src/main/java/seedu/address/logic/commands/CommandResult.java +++ b/src/main/java/quickdocs/logic/commands/CommandResult.java @@ -1,4 +1,4 @@ -package seedu.address.logic.commands; +package quickdocs.logic.commands; import static java.util.Objects.requireNonNull; diff --git a/src/main/java/quickdocs/logic/commands/ConsultationCommand.java b/src/main/java/quickdocs/logic/commands/ConsultationCommand.java new file mode 100644 index 000000000000..616b6c603670 --- /dev/null +++ b/src/main/java/quickdocs/logic/commands/ConsultationCommand.java @@ -0,0 +1,52 @@ +package quickdocs.logic.commands; + +import quickdocs.logic.CommandHistory; +import quickdocs.logic.commands.exceptions.CommandException; +import quickdocs.logic.parser.ConsultationCommandParser; +import quickdocs.model.Model; +import quickdocs.model.patient.Patient; + +/** + * Starts a consultation session for the specified patient using his or her NRIC + */ +public class ConsultationCommand extends Command { + + public static final String COMMAND_WORD = "consult"; + public static final String COMMAND_ALIAS = "c"; + public static final String NO_PATIENT_FOUND = "No patient with NRIC: %s found.\n"; + public static final String CONSULTATION_START = "Consultation session for: %s started\n"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": starts a consultation session for a patient.\n" + + "Parameters: " + ConsultationCommandParser.PREFIX_NRIC + "NRIC of current patient.\n" + + "Example: " + COMMAND_WORD + " " + ConsultationCommandParser.PREFIX_NRIC + "S9237162A\n"; + + private String nric; + + public ConsultationCommand(String nric) { + this.nric = nric; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + Patient patient = model.getPatientByNric(nric); + if (patient == null) { + throw new CommandException(String.format(NO_PATIENT_FOUND, nric)); + } + + model.createConsultation(patient); + + String consultationResult = String.format(CONSULTATION_START, nric); + + return new CommandResult(consultationResult); + } + + public String getNric() { + return nric; + } + + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof ConsultationCommand + && nric.equals(((ConsultationCommand) other).getNric())); + } +} diff --git a/src/main/java/quickdocs/logic/commands/DeleteAppCommand.java b/src/main/java/quickdocs/logic/commands/DeleteAppCommand.java new file mode 100644 index 000000000000..bd7dea4666d2 --- /dev/null +++ b/src/main/java/quickdocs/logic/commands/DeleteAppCommand.java @@ -0,0 +1,74 @@ +package quickdocs.logic.commands; + +import static java.util.Objects.requireNonNull; +import static quickdocs.logic.parser.DeleteAppCommandParser.PREFIX_DATE; +import static quickdocs.logic.parser.DeleteAppCommandParser.PREFIX_START; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.Optional; + +import quickdocs.logic.CommandHistory; +import quickdocs.logic.commands.exceptions.CommandException; +import quickdocs.model.Model; +import quickdocs.model.appointment.Appointment; +import quickdocs.model.appointment.AppointmentManager; + + +/** + * Deletes an {@code Appointment} in QuickDocs. + */ +public class DeleteAppCommand extends Command { + + public static final String COMMAND_WORD = "deleteapp"; + public static final String COMMAND_ALIAS = "da"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Deletes an appointment in QuickDocs. " + + "Parameters: " + + PREFIX_DATE + "DATE " + + PREFIX_START + "START " + + "Example: " + COMMAND_WORD + " " + + PREFIX_DATE + "2019-10-23 " + + PREFIX_START + "16:00\n"; + + public static final String MESSAGE_SUCCESS = "Appointment deleted:\n%1$s\n"; + public static final String MESSAGE_APPOINTMENT_NOT_FOUND = "No appointment with the given date and time found."; + public static final String MESSAGE_NON_OFFICE_HOURS = "Appointment start time is outside of office hours."; + + private final LocalDate date; + private final LocalTime start; + + /** + * Creates a {@code DeleteAppCommand} to delete the specified {@code Appointment}. + */ + public DeleteAppCommand(LocalDate date, LocalTime start) { + this.date = date; + this.start = start; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + + // check if start time is within office hours + if (start.isBefore(AppointmentManager.OPENING_HOUR) || start.isAfter(AppointmentManager.CLOSING_HOUR)) { + throw new CommandException(MESSAGE_NON_OFFICE_HOURS); + } + + // check if given appointment exists in QuickDocs + Optional appointmentToDelete = model.getAppointment(date, start); + if (!appointmentToDelete.isPresent()) { + throw new CommandException(MESSAGE_APPOINTMENT_NOT_FOUND); + } + + model.deleteAppointment(appointmentToDelete.get()); + return new CommandResult(String.format(MESSAGE_SUCCESS, appointmentToDelete.get())); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof DeleteAppCommand // instanceof handles nulls + && date.equals(((DeleteAppCommand) other).date) + && start.equals(((DeleteAppCommand) other).start)); + } +} diff --git a/src/main/java/quickdocs/logic/commands/DeletePatientCommand.java b/src/main/java/quickdocs/logic/commands/DeletePatientCommand.java new file mode 100644 index 000000000000..028f5fd14bf5 --- /dev/null +++ b/src/main/java/quickdocs/logic/commands/DeletePatientCommand.java @@ -0,0 +1,61 @@ +package quickdocs.logic.commands; + +import java.util.Objects; + +import quickdocs.logic.CommandHistory; +import quickdocs.logic.commands.exceptions.CommandException; +import quickdocs.model.Model; +import quickdocs.model.patient.Nric; +import quickdocs.model.patient.Patient; + +/** + * Deletes a patient record from the patient list + */ +public class DeletePatientCommand extends Command { + + public static final String COMMAND_WORD = "deletepat"; + public static final String COMMAND_ALIAS = "dp"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": deletes a patient using the NRIC specified.\n" + + "Example: " + COMMAND_WORD + " r/S9637777A"; + public static final String NO_PATIENT_FOUND = "No patient with NRIC: %s found"; + public static final String PATIENT_DELETED = "Patient with NRIC: %s deleted.\n"; + + private Nric patientToDelete; + + public DeletePatientCommand(Nric toDelete) { + this.patientToDelete = toDelete; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + + Patient selectedPatient = model.getPatientByNric(patientToDelete.toString()); + + if (selectedPatient == null) { + throw new CommandException(String.format(NO_PATIENT_FOUND, patientToDelete.toString())); + } + + model.deletePatientByNric(patientToDelete.toString()); + + CommandResult result = new CommandResult(String.format(PATIENT_DELETED, + patientToDelete.toString())); + + return result; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (o == null || getClass() != o.getClass()) { + return false; + } + + DeletePatientCommand that = (DeletePatientCommand) o; + return Objects.equals(patientToDelete, that.patientToDelete); + } + +} diff --git a/src/main/java/quickdocs/logic/commands/DeleteRemCommand.java b/src/main/java/quickdocs/logic/commands/DeleteRemCommand.java new file mode 100644 index 000000000000..de4abb6c4606 --- /dev/null +++ b/src/main/java/quickdocs/logic/commands/DeleteRemCommand.java @@ -0,0 +1,61 @@ +package quickdocs.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import quickdocs.commons.core.index.Index; +import quickdocs.logic.CommandHistory; +import quickdocs.logic.commands.exceptions.CommandException; +import quickdocs.model.Model; +import quickdocs.model.reminder.Reminder; + +/** + * Deletes a {@code Reminder} identified using it's displayed {@code Index} from the reminder sidebar. + */ +public class DeleteRemCommand extends Command { + + public static final String COMMAND_WORD = "deleterem"; + public static final String COMMAND_ALIAS = "dr"; + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Deletes the reminder identified by the index number displayed in the reminder sidebar.\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_DELETE_REMINDER_SUCCESS = "Deleted reminder: %1$s"; + public static final String MESSAGE_INVALID_REMINDER_INDEX = "The reminder index provided is invalid."; + + private final Index targetIndex; + + /** + * Creates a {@code DeleteRemCommand} to delete the specified {@code Reminder}. + */ + public DeleteRemCommand(Index targetIndex) { + assert targetIndex.getOneBased() > 0; + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredReminderList(); + + // check if given targetIndex is valid + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(MESSAGE_INVALID_REMINDER_INDEX); + } + assert !lastShownList.isEmpty(); + + // retrieve selected reminder and delete it + Reminder reminderToDelete = lastShownList.get(targetIndex.getZeroBased()); + model.deleteReminder(reminderToDelete); + return new CommandResult(String.format(MESSAGE_DELETE_REMINDER_SUCCESS, reminderToDelete)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof DeleteRemCommand // instanceof handles nulls + && targetIndex.equals(((DeleteRemCommand) other).targetIndex)); // state check + } +} diff --git a/src/main/java/quickdocs/logic/commands/DiagnosePatientCommand.java b/src/main/java/quickdocs/logic/commands/DiagnosePatientCommand.java new file mode 100644 index 000000000000..787c33b797ce --- /dev/null +++ b/src/main/java/quickdocs/logic/commands/DiagnosePatientCommand.java @@ -0,0 +1,64 @@ +package quickdocs.logic.commands; + +import java.util.logging.Logger; + +import quickdocs.commons.core.LogsCenter; +import quickdocs.logic.CommandHistory; +import quickdocs.logic.commands.exceptions.CommandException; +import quickdocs.logic.parser.DiagnosePatientCommandParser; +import quickdocs.model.Model; +import quickdocs.model.consultation.Diagnosis; + +/** + * Creates or replace the diagnosis record of current consultation session. + * Diagnosis records the symptoms of the patient during the current consultation session, and + * also the assessment of the illness given by the doctor + */ +public class DiagnosePatientCommand extends Command { + public static final String COMMAND_WORD = "diagnose"; + public static final String COMMAND_ALIAS = "d"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Record the symptoms and assessment of the patient's" + + "current condition.\n" + + "Parameters: " + + DiagnosePatientCommandParser.PREFIX_SYMPTOM + "symptom 1 " + + DiagnosePatientCommandParser.PREFIX_SYMPTOM + "symptom 2 " + " ... " + + DiagnosePatientCommandParser.PREFIX_ASSESSMENT + "assessment\n" + + "Example: " + + DiagnosePatientCommandParser.PREFIX_SYMPTOM + "runny nose " + + DiagnosePatientCommandParser.PREFIX_SYMPTOM + "sore throat " + + DiagnosePatientCommandParser.PREFIX_ASSESSMENT + "flu\n"; + public static final String NO_ONGOING_CONSULTATION = "There is no ongoing consultation to record diagnosis\n"; + + private static final Logger logger = LogsCenter.getLogger(DiagnosePatientCommand.class); + + private Diagnosis patientDiagnosis; + + public DiagnosePatientCommand(Diagnosis diagnosis) { + this.patientDiagnosis = diagnosis; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + + // diagnosis can only be given when there's an ongoing consultation session + if (model.checkConsultation() == false) { + throw new CommandException(NO_ONGOING_CONSULTATION); + } + + model.diagnosePatient(patientDiagnosis); + logger.info("Diagnosis entered or replaced for current consultation"); + + return new CommandResult(patientDiagnosis.toString()); + } + + public Diagnosis getPatientDiagnosis() { + return patientDiagnosis; + } + + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof DiagnosePatientCommand + && patientDiagnosis.equals(((DiagnosePatientCommand) other).getPatientDiagnosis())); + } +} diff --git a/src/main/java/quickdocs/logic/commands/EditPatientCommand.java b/src/main/java/quickdocs/logic/commands/EditPatientCommand.java new file mode 100644 index 000000000000..cf409b5863d5 --- /dev/null +++ b/src/main/java/quickdocs/logic/commands/EditPatientCommand.java @@ -0,0 +1,131 @@ +package quickdocs.logic.commands; + +import java.util.ArrayList; + +import quickdocs.logic.CommandHistory; +import quickdocs.logic.commands.exceptions.CommandException; +import quickdocs.logic.parser.EditPatientParser; +import quickdocs.model.Model; +import quickdocs.model.patient.Address; +import quickdocs.model.patient.Contact; +import quickdocs.model.patient.Dob; +import quickdocs.model.patient.Email; +import quickdocs.model.patient.Gender; +import quickdocs.model.patient.Name; +import quickdocs.model.patient.Nric; +import quickdocs.model.patient.Patient; +import quickdocs.model.patient.PatientEditedFields; +import quickdocs.model.tag.Tag; + +/** + * Edits a current patient record's details + */ +public class EditPatientCommand extends Command { + + public static final String COMMAND_WORD = "editpat"; + public static final String COMMAND_ALIAS = "ep"; + public static final String NO_PATIENTS = "No patients records found to edit.\n"; + public static final String NO_PATIENT_FOUND = "No patient with NRIC: %s found.\n"; + public static final String CONFLICTING_NRIC = "Edited NRIC will conflict with another existing entry.\n"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits a patient by first specifying the NRIC.\n" + + "Parameters: " + + "ORIGINAL NRIC " + + EditPatientParser.PREFIX_NAME + "NEW NAME " + + EditPatientParser.PREFIX_NRIC + "NEW NRIC " + + EditPatientParser.PREFIX_DOB + "NEW DATE OF BIRTH " + + EditPatientParser.PREFIX_ADDRESS + "NEW ADDRESS " + + EditPatientParser.PREFIX_EMAIL + "NEW EMAIL " + + EditPatientParser.PREFIX_CONTACT + "NEW CONTACT " + + EditPatientParser.PREFIX_GENDER + "NEW GENDER " + + "[" + EditPatientParser.PREFIX_TAG + "NEW TAG]...\n" + + "Note: You only need to add the parameters for the details you want to edit.\n" + + "Example: " + COMMAND_WORD + " S9876543A " + + EditPatientParser.PREFIX_NAME + "John Doe " + + EditPatientParser.PREFIX_NRIC + "S9876542C " + + EditPatientParser.PREFIX_ADDRESS + "311, Clementi Ave 2, #02-26 " + + EditPatientParser.PREFIX_EMAIL + "johnd@example.com " + + EditPatientParser.PREFIX_CONTACT + "92344321 " + + EditPatientParser.PREFIX_GENDER + "M " + + EditPatientParser.PREFIX_TAG + "highbloodpressure\n"; + + private Nric patientToEdit; + + // edited fields will only capture the details to be edited + private PatientEditedFields editedFields; + + + public EditPatientCommand(Nric patientToEdit, PatientEditedFields editedFields) { + this.patientToEdit = patientToEdit; + this.editedFields = editedFields; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + if (model.isPatientListEmpty()) { + throw new CommandException(NO_PATIENTS); + } + + int index = model.getIndexByNric(patientToEdit); + if (index == -1) { + throw new CommandException(String.format(NO_PATIENT_FOUND, patientToEdit.toString())); + } + + Patient patient = model.getPatientAtIndex(index); + Patient editedPatient = createEditedPatient(patient, editedFields); + + // check if edited patient will have a conflicting nric with another patient record + if (model.checkDuplicatePatientAfterEdit(index, editedPatient)) { + throw new CommandException(CONFLICTING_NRIC); + } + + model.replacePatient(index, editedPatient); + + StringBuilder sb = new StringBuilder(); + sb.append("Patient edited:\n"); + sb.append("==============================\n"); + sb.append(editedPatient.toString()); + + return new CommandResult(sb.toString(), false, false); + } + + public Nric getPatientToEdit() { + return patientToEdit; + } + + public PatientEditedFields getEditedFields() { + return editedFields; + } + + /** + * Using the editedFields, create a new patient object that will replace the existing one + * currently in the PatientManager's patientList + * If editedFields have a value present, the new patient object will set its field's value to the editedField's one + * else its will use the original value instead + * + * @param patient the original patient object to be edited + * @param editedFields the editedFields created after parsing the user's entered parameters + * @return the patient object with edited values for the fields specified to be edited + */ + public Patient createEditedPatient(Patient patient, PatientEditedFields editedFields) { + + Name name = editedFields.getName().orElse(patient.getName()); + Nric nric = editedFields.getNric().orElse(patient.getNric()); + Email email = editedFields.getEmail().orElse(patient.getEmail()); + Address address = editedFields.getAddress().orElse(patient.getAddress()); + Contact contact = editedFields.getContact().orElse(patient.getContact()); + Gender gender = editedFields.getGender().orElse(patient.getGender()); + Dob dob = editedFields.getDob().orElse(patient.getDob()); + ArrayList tagList = editedFields.getTagList().orElse(patient.getTagList()); + + return new Patient(name, nric, email, address, contact, gender, dob, tagList); + } + + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof EditPatientCommand + && patientToEdit.toString().equals(((EditPatientCommand) other).getPatientToEdit().toString()) + && editedFields.equals(((EditPatientCommand) other).getEditedFields())); + } + +} diff --git a/src/main/java/quickdocs/logic/commands/EndConsultationCommand.java b/src/main/java/quickdocs/logic/commands/EndConsultationCommand.java new file mode 100644 index 000000000000..996020f04ea4 --- /dev/null +++ b/src/main/java/quickdocs/logic/commands/EndConsultationCommand.java @@ -0,0 +1,63 @@ +package quickdocs.logic.commands; + +import java.time.Clock; + +import quickdocs.logic.CommandHistory; +import quickdocs.logic.commands.exceptions.CommandException; +import quickdocs.model.Model; +import quickdocs.model.consultation.Consultation; +import quickdocs.model.consultation.Prescription; +import quickdocs.model.patient.Nric; +import quickdocs.model.record.ConsultationRecord; + +/** + * End the current consultation session and store the consultation details + */ +public class EndConsultationCommand extends Command { + public static final String COMMAND_WORD = "endconsult"; + public static final String COMMAND_ALIAS = "ec"; + public static final String NO_CONSULT_EXCEPTION = "There is no ongoing consultation"; + public static final String DIAGNOSIS_EXCEPTION = "No diagnosis given for current consultation yet"; + public static final String PRESCRIPTION_EXCEPTION = "No prescription given for current consultation yet"; + public static final String END_CONSULT_FEEDBACK = "Consultation for %s ended\n"; + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + + Consultation currentConsultation = model.getCurrentConsultation(); + + if (currentConsultation == null) { + throw new CommandException(NO_CONSULT_EXCEPTION); + } + + /** + * The current consultation session can end only when both the diagnosis and the prescription + * is provided + */ + if (currentConsultation.getDiagnosis() == null) { + throw new CommandException(DIAGNOSIS_EXCEPTION); + } + + if (currentConsultation.getPrescriptions() == null) { + throw new CommandException(PRESCRIPTION_EXCEPTION); + } + + /** + * The deduction of medicine from the inventory will only happen once the + * consultation is confirmed to have ended. + */ + for (Prescription prescription : currentConsultation.getPrescriptions()) { + model.executePrescription(prescription); + } + + ConsultationRecord record = new ConsultationRecord(currentConsultation.getPrescriptions(), + currentConsultation.getDiagnosis()); + model.addRecord(record, Clock.systemDefaultZone()); + + Nric patientNric = currentConsultation.getPatient().getNric(); + + model.endConsultation(); + + return new CommandResult(String.format(END_CONSULT_FEEDBACK, patientNric.getNric())); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ExitCommand.java b/src/main/java/quickdocs/logic/commands/ExitCommand.java similarity index 78% rename from src/main/java/seedu/address/logic/commands/ExitCommand.java rename to src/main/java/quickdocs/logic/commands/ExitCommand.java index 2240a3e4be1f..73abb2bbfeb2 100644 --- a/src/main/java/seedu/address/logic/commands/ExitCommand.java +++ b/src/main/java/quickdocs/logic/commands/ExitCommand.java @@ -1,7 +1,7 @@ -package seedu.address.logic.commands; +package quickdocs.logic.commands; -import seedu.address.logic.CommandHistory; -import seedu.address.model.Model; +import quickdocs.logic.CommandHistory; +import quickdocs.model.Model; /** * Terminates the program. diff --git a/src/main/java/quickdocs/logic/commands/FreeAppCommand.java b/src/main/java/quickdocs/logic/commands/FreeAppCommand.java new file mode 100644 index 000000000000..bdfdb58cff98 --- /dev/null +++ b/src/main/java/quickdocs/logic/commands/FreeAppCommand.java @@ -0,0 +1,61 @@ +package quickdocs.logic.commands; + +import static java.util.Objects.requireNonNull; +import static quickdocs.logic.parser.FreeAppCommandParser.PREFIX_DATE; +import static quickdocs.logic.parser.FreeAppCommandParser.PREFIX_FORMAT; + +import java.time.LocalDate; + +import quickdocs.logic.CommandHistory; +import quickdocs.model.Model; + +/** + * Lists all free appointment slots in QuickDocs on the main display of the UI. + */ +public class FreeAppCommand extends Command { + + public static final String COMMAND_WORD = "freeapp"; + public static final String COMMAND_ALIAS = "fa"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Lists all free appointment slots.\n" + + "Parameters: " + + "[" + PREFIX_FORMAT + "FORMAT] " + + "[" + PREFIX_DATE + "DATE]\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_FORMAT + "day " + + PREFIX_DATE + "2019-10-23"; + public static final String MESSAGE_SUCCESS = "Listed all free appointment slots from %1$s to %2$s\n"; + public static final String MESSAGE_NO_FREE_SLOTS = "There are no free appointment slots from %1$s to %2$s\n"; + + private final LocalDate start; + private final LocalDate end; + + /** + * Creates a {@code FreeAppCommand} to list free appointment slots within given search range. + */ + public FreeAppCommand(LocalDate start, LocalDate end) { + this.start = start; + this.end = end; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) { + requireNonNull(model); + + String result = model.freeApp(start, end); + if (result.isEmpty()) { + return new CommandResult(String.format(MESSAGE_NO_FREE_SLOTS, start, end) + result + "\n", + false, false); + } + + return new CommandResult(String.format(MESSAGE_SUCCESS, start, end) + result + "\n", + false, false); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof FreeAppCommand // instanceof handles nulls + && start.equals(((FreeAppCommand) other).start) + && end.equals(((FreeAppCommand) other).end)); + } +} diff --git a/src/main/java/seedu/address/logic/commands/HelpCommand.java b/src/main/java/quickdocs/logic/commands/HelpCommand.java similarity index 81% rename from src/main/java/seedu/address/logic/commands/HelpCommand.java rename to src/main/java/quickdocs/logic/commands/HelpCommand.java index f0ef78dddded..46cb975733c0 100644 --- a/src/main/java/seedu/address/logic/commands/HelpCommand.java +++ b/src/main/java/quickdocs/logic/commands/HelpCommand.java @@ -1,7 +1,7 @@ -package seedu.address.logic.commands; +package quickdocs.logic.commands; -import seedu.address.logic.CommandHistory; -import seedu.address.model.Model; +import quickdocs.logic.CommandHistory; +import quickdocs.model.Model; /** * Format full help instructions for every command for display. @@ -13,7 +13,7 @@ public class HelpCommand extends Command { public static final String MESSAGE_USAGE = COMMAND_WORD + ": Shows program usage instructions.\n" + "Example: " + COMMAND_WORD; - public static final String SHOWING_HELP_MESSAGE = "Opened help window."; + public static final String SHOWING_HELP_MESSAGE = "Opened help window.\n"; @Override public CommandResult execute(Model model, CommandHistory history) { diff --git a/src/main/java/seedu/address/logic/commands/HistoryCommand.java b/src/main/java/quickdocs/logic/commands/HistoryCommand.java similarity index 85% rename from src/main/java/seedu/address/logic/commands/HistoryCommand.java rename to src/main/java/quickdocs/logic/commands/HistoryCommand.java index dc3de1aad55e..dd4b9f4b2e00 100644 --- a/src/main/java/seedu/address/logic/commands/HistoryCommand.java +++ b/src/main/java/quickdocs/logic/commands/HistoryCommand.java @@ -1,12 +1,12 @@ -package seedu.address.logic.commands; +package quickdocs.logic.commands; import static java.util.Objects.requireNonNull; import java.util.ArrayList; import java.util.Collections; -import seedu.address.logic.CommandHistory; -import seedu.address.model.Model; +import quickdocs.logic.CommandHistory; +import quickdocs.model.Model; /** * Lists all the commands entered by user from the start of app launch. @@ -14,7 +14,7 @@ public class HistoryCommand extends Command { public static final String COMMAND_WORD = "history"; - public static final String MESSAGE_SUCCESS = "Entered commands (from most recent to earliest):\n%1$s"; + public static final String MESSAGE_SUCCESS = "Entered commands (from most recent to earliest):\n%1$s\n"; public static final String MESSAGE_NO_HISTORY = "You have not yet entered any commands."; @Override diff --git a/src/main/java/quickdocs/logic/commands/ListAppCommand.java b/src/main/java/quickdocs/logic/commands/ListAppCommand.java new file mode 100644 index 000000000000..3dcc53b7025c --- /dev/null +++ b/src/main/java/quickdocs/logic/commands/ListAppCommand.java @@ -0,0 +1,101 @@ +package quickdocs.logic.commands; + +import static java.util.Objects.requireNonNull; +import static quickdocs.logic.parser.ListAppCommandParser.PREFIX_DATE; +import static quickdocs.logic.parser.ListAppCommandParser.PREFIX_FORMAT; +import static quickdocs.logic.parser.ListAppCommandParser.PREFIX_NRIC; + +import java.time.LocalDate; +import java.util.Objects; +import java.util.Optional; + +import quickdocs.logic.CommandHistory; +import quickdocs.logic.commands.exceptions.CommandException; +import quickdocs.model.Model; +import quickdocs.model.patient.Nric; +import quickdocs.model.patient.Patient; + +/** + * Lists filtered {@code Appointment}(s) on the main display of the UI. + */ +public class ListAppCommand extends Command { + + public static final String COMMAND_WORD = "listapp"; + public static final String COMMAND_ALIAS = "la"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Lists filtered appointments.\n" + + "Parameters: " + + PREFIX_FORMAT + "FORMAT " + + PREFIX_DATE + "DATE\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_FORMAT + "day " + + PREFIX_DATE + "2019-10-23\n" + + "OR\n" + + "Parameters:" + PREFIX_NRIC + "NRIC\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_NRIC + "S9637645A"; + public static final String MESSAGE_SUCCESS_BY_DATE = + "Listing all appointments from %1$s to %2$s:\n" + + "============================================\n"; + public static final String MESSAGE_SUCCESS_BY_NRIC = + "Listing all appointments for %1$s:\n" + + "============================================\n"; + public static final String MESSAGE_PATIENT_NOT_FOUND = "No patient with the given NRIC found"; + + private final LocalDate start; + private final LocalDate end; + private final Nric nric; + + /** + * Creates a {@code ListAppCommand} to list {@code Appointment}(s) in the given search range of dates. + */ + public ListAppCommand(LocalDate start, LocalDate end) { + this.start = start; + this.end = end; + nric = null; + } + + /** + * Creates a {@code ListAppCommand} to list {@code Appointment}(s) made by a {@code Patient} with + * the given {@code Nric}. + */ + public ListAppCommand(Nric nric) { + this.nric = nric; + start = null; + end = null; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + + if (Optional.ofNullable(start).isPresent()) { + // list appointments in given search range of dates + assert end != null; + + String result = model.listApp(start, end); + return new CommandResult(String.format(MESSAGE_SUCCESS_BY_DATE, start, end) + result, false, false); + } else { + // list appointments made by the given patient + assert nric != null; + + Optional patientToList = model.getPatientByNric(nric); + if (!patientToList.isPresent()) { + throw new CommandException(MESSAGE_PATIENT_NOT_FOUND); + } + + String result = model.listApp(patientToList.get()); + return new CommandResult(String.format(MESSAGE_SUCCESS_BY_NRIC, patientToList.get().getName()) + result, + false, false); + } + } + + @Override + public boolean equals(Object other) { + // Objects.equals() to handle null fields + return other == this // short circuit if same object + || (other instanceof ListAppCommand // instanceof handles nulls + && Objects.equals(nric, ((ListAppCommand) other).nric) + && Objects.equals(start, ((ListAppCommand) other).start) + && Objects.equals(end, ((ListAppCommand) other).end)); + } +} diff --git a/src/main/java/quickdocs/logic/commands/ListConsultationCommand.java b/src/main/java/quickdocs/logic/commands/ListConsultationCommand.java new file mode 100644 index 000000000000..683d6104f3d7 --- /dev/null +++ b/src/main/java/quickdocs/logic/commands/ListConsultationCommand.java @@ -0,0 +1,123 @@ +package quickdocs.logic.commands; + +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.logging.Logger; + +import quickdocs.commons.core.LogsCenter; +import quickdocs.logic.CommandHistory; +import quickdocs.logic.commands.exceptions.CommandException; +import quickdocs.model.Model; +import quickdocs.model.consultation.Consultation; + +/** + * List previous consultation sessions of a single patient if the NRIC of the patient + * is supplied. If the index is supplied instead, then display the details of the + * specific consultation session + */ +public class ListConsultationCommand extends Command { + + public static final String COMMAND_WORD = "listconsult"; + public static final String COMMAND_ALIAS = "lc"; + public static final String NO_RECORDS = "No past consultation records found.\n"; + public static final String INVALID_INDEX = "Index entered is invalid.\n"; + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": list all past consultation records of a single patient using patient's NRIC " + + "or details of a single consultation record through its index.\n" + + "Parameters: " + + "INDEX OR NRIC\n" + + "Example: " + COMMAND_WORD + " r/S9237161A\n" + + "or: " + COMMAND_WORD + " 10\n"; + + private static final Logger logger = LogsCenter.getLogger(ListConsultationCommand.class); + + private int index; + private String nric; + + private int constructedBy; + + public ListConsultationCommand(int index) { + this.index = index; + constructedBy = 1; + logger.info("Listing consultation using index"); + } + + public ListConsultationCommand(String value) { + this.nric = value; + constructedBy = 2; + logger.info("Listing consultation using NRIC"); + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + + if (constructedBy == 1 && (index > model.getConsultationList().size() || index < 1)) { + throw new CommandException(INVALID_INDEX); + } + + if (constructedBy == 1) { + return new CommandResult(model.listConsultation(index).toString()); + } + + ArrayList consultations = model.listConsultation(nric); + String result = listingConsultations(consultations); + + return new CommandResult(result); + } + + /** + * List all the past consultation records of a single patient + */ + public String listingConsultations(ArrayList consultations) { + StringBuilder sb = new StringBuilder(); + sb.append("Listing consultation records\n"); + sb.append("====================\n"); + + if (consultations.isEmpty()) { + sb.append(NO_RECORDS); + return sb.toString(); + } + + for (Consultation con : consultations) { + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + + sb.append(1 + con.getIndex() + ") "); + sb.append(" visit for " + con.getDiagnosis().getAssessment() + " "); + sb.append(" on " + con.getSession().format(formatter)); + sb.append("\n"); + } + + return sb.toString(); + } + + public int getIndex() { + return index; + } + + public String getNric() { + return nric; + } + + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof ListConsultationCommand + && checkEquals((ListConsultationCommand) other)); + } + + /** + * Separate the checking of NRIC and index between 2 ListConsultationCommand + * + * @param other the other ListConsultationCommand to check equality against + */ + public boolean checkEquals(ListConsultationCommand other) { + + if (nric == null && other.getNric() == null) { + return getIndex() == other.getIndex(); + } + + return getNric().equals(other.getNric()); + } + +} diff --git a/src/main/java/quickdocs/logic/commands/ListPatientCommand.java b/src/main/java/quickdocs/logic/commands/ListPatientCommand.java new file mode 100644 index 000000000000..c8b4511e50e9 --- /dev/null +++ b/src/main/java/quickdocs/logic/commands/ListPatientCommand.java @@ -0,0 +1,194 @@ +package quickdocs.logic.commands; + +import java.util.logging.Logger; + +import quickdocs.commons.core.LogsCenter; +import quickdocs.logic.CommandHistory; +import quickdocs.logic.commands.exceptions.CommandException; +import quickdocs.model.Model; +import quickdocs.model.patient.Patient; +import quickdocs.model.tag.Tag; + +/** + * Returns either a specific patient's record or a list + * of patients with similar names, nric or tags + */ +public class ListPatientCommand extends Command { + + public static final String COMMAND_WORD = "listpat"; + public static final String COMMAND_ALIAS = "lp"; + public static final String NO_PATIENTS = "No medical records to list.\n"; + public static final String NO_PATIENT_FOUND_NAME = "No patient by the name: %s found"; + public static final String NO_PATIENT_FOUND_NRIC = "No patient by NRIC: %s found"; + public static final String NO_PATIENT_FOUND_TAG = "No patient with tag: %s found"; + + public static final String INVALID_INDEX = "Invalid index to find patient records.\n"; + public static final String MESSAGE_USAGE = + COMMAND_WORD + ": List patient details using a particular index, name or nric.\n" + + " A list of patients with the same tags can also be displayed by entering the tag.\n" + + " If no parameters are entered, QuickDocs will try to list at least 50 patients\n" + + "Parameters: " + + "INDEX OR" + + "n/NAME OR" + + "r/NRIC OR" + + "t/TAG\n" + + "Examples: " + COMMAND_WORD + "\n" + + COMMAND_WORD + "10" + "\n" + + COMMAND_WORD + "r/S9214538C" + "\n" + + COMMAND_WORD + "n/Tan Ah Kow" + "\n" + + COMMAND_WORD + "t/diabetes" + "\n"; + + private static final Logger logger = LogsCenter.getLogger(ListPatientCommand.class); + + // to indicate which constructor was used to create this command + private int constructedBy; + private int index = -1; + private String name; + private String nric; + private Tag tag; + + /** + * Indicates that the search is conducted by an index during the + * command's execution + * + * @param index 1-based index to select the patient record in the PatientManager's + * patientList + */ + public ListPatientCommand(int index) { + // for user entry, index is always 1 indexed. + // since patientmanager uses 0 indexing, index are adjusted here + logger.info("Listing patient by index"); + this.index = index - 1; + constructedBy = 1; + } + + public ListPatientCommand(String search, boolean byName) { + if (byName == true) { + logger.info("ListPatientCommand: Listing patient by name"); + name = search; + constructedBy = 2; + } else { + logger.info("ListPatientCommand: Listing patient by nric"); + nric = search; + constructedBy = 3; + } + } + + public ListPatientCommand(Tag tag) { + logger.info("ListPatientCommand: Listing patient by tag"); + this.tag = tag; + constructedBy = 4; + } + + public ListPatientCommand() { + logger.info("ListPatientCommand: Listing first 50 patients"); + constructedBy = 5; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + if (model.isPatientListEmpty()) { + throw new CommandException(NO_PATIENTS); + } + + // searches patient records via indexing + if (constructedBy == 1) { + + if (!model.checkValidIndex(index)) { + throw new CommandException(INVALID_INDEX); + } + + Patient patient = model.getPatientAtIndex(index); + return constructResult(patient.toString()); + } + + // handles patient record search by name sequences + if (constructedBy == 2) { + String result = model.findPatientsByName(name); + if (result.equals("No patient record found")) { + throw new CommandException(String.format(NO_PATIENT_FOUND_NAME, name)); + } + return constructResult(result); + } + + // handles patient record search by NRIC sequences + if (constructedBy == 3) { + String result = model.findPatientsByNric(nric); + if (result.equals("No patient record found")) { + throw new CommandException(String.format(NO_PATIENT_FOUND_NRIC, nric)); + } + return constructResult(result); + } + + // find patient records with the given tag + if (constructedBy == 4) { + String result = model.findPatientsByTag(tag); + if (result.equals("No patient record found")) { + throw new CommandException(String.format(NO_PATIENT_FOUND_TAG, tag)); + } + + return constructResult(result); + } + + // list as many patient records if no parameters are provided + String result = model.listFiftyPatients(); + return constructResult(result); + } + + /** + * format the list of patients or single patient record to a commandresult for display + */ + public CommandResult constructResult(String result) { + StringBuilder sb = new StringBuilder(); + sb.append("Listing patients:\n"); + sb.append("==============================\n"); + sb.append(result); + return new CommandResult(sb.toString(), false, false); + } + + public int getConstructedBy() { + return constructedBy; + } + + public int getIndex() { + return index; + } + + public String getName() { + return name; + } + + public String getNric() { + return nric; + } + + public Tag getTag() { + return tag; + } + + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof ListPatientCommand + && checkAttributes((ListPatientCommand) other)); + } + + /** + * Check the attributes (name, nric, tag search sequences) + * of two ListPatientCommand object and see if they are the same + */ + public boolean checkAttributes(ListPatientCommand other) { + switch (other.getConstructedBy()) { + case 1: + return index == other.getIndex(); + case 2: + return name.equals(other.getName()); + case 3: + return nric.equals(other.getNric()); + case 4: + return tag.equals(other.getTag()); + default: + return true; + } + } +} diff --git a/src/main/java/quickdocs/logic/commands/ListRemCommand.java b/src/main/java/quickdocs/logic/commands/ListRemCommand.java new file mode 100644 index 000000000000..148ffc4bec46 --- /dev/null +++ b/src/main/java/quickdocs/logic/commands/ListRemCommand.java @@ -0,0 +1,107 @@ +package quickdocs.logic.commands; + +import static java.util.Objects.requireNonNull; +import static quickdocs.logic.parser.ListRemCommandParser.PREFIX_DATE; +import static quickdocs.logic.parser.ListRemCommandParser.PREFIX_FORMAT; +import static quickdocs.logic.parser.ListRemCommandParser.PREFIX_INDEX; + +import java.time.LocalDate; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import quickdocs.commons.core.index.Index; +import quickdocs.logic.CommandHistory; +import quickdocs.logic.commands.exceptions.CommandException; +import quickdocs.model.Model; +import quickdocs.model.reminder.Reminder; +import quickdocs.model.reminder.ReminderWithinDatesPredicate; + +/** + * Lists filtered {@code Reminder}(s) on the reminder sidebar of the UI. + */ +public class ListRemCommand extends Command { + + public static final String COMMAND_WORD = "listrem"; + public static final String COMMAND_ALIAS = "lr"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Lists reminders on the reminder sidebar.\n" + + "Parameters: " + + PREFIX_FORMAT + "FORMAT " + + PREFIX_DATE + "DATE\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_FORMAT + "week " + + PREFIX_DATE + "2019-10-23\n" + + "OR\n" + + "Parameters: " + PREFIX_INDEX + "INDEX\n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_INDEX + "1"; + + public static final String MESSAGE_SUCCESS = "Listed all reminders from %1$s to %2$s on the sidebar.\n"; + public static final String MESSAGE_SINGLE_REMINDER_SUCCESS = "Displaying reminder #%1$s:\n" + + "============================================\n" + + "%2$s"; + public static final String MESSAGE_INVALID_REMINDER_INDEX = "The reminder index provided is invalid."; + + private final LocalDate start; + private final LocalDate end; + private final Index targetIndex; + + /** + * Creates a {@code ListRemCommand} to list {@code Reminder}(s) on the reminder sidebar + * in the given search range of dates. + */ + public ListRemCommand(LocalDate start, LocalDate end) { + this.start = start; + this.end = end; + targetIndex = null; + } + + /** + * Creates a {@code ListRemCommand} to display information of the selected {@code Reminder} based on + * the given {@code Index}. + */ + public ListRemCommand(Index targetIndex) { + this.start = null; + this.end = null; + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + + // List single reminder + if (Optional.ofNullable(targetIndex).isPresent()) { + assert start == null; + assert end == null; + + List lastShownList = model.getFilteredReminderList(); + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(MESSAGE_INVALID_REMINDER_INDEX); + } + assert !lastShownList.isEmpty(); + + Reminder reminderToList = lastShownList.get(targetIndex.getZeroBased()); + return new CommandResult(String.format(MESSAGE_SINGLE_REMINDER_SUCCESS, + targetIndex.getOneBased(), reminderToList)); + } + + // List reminder(s) on sidebar + assert start != null; + assert end != null; + + ReminderWithinDatesPredicate predicate = new ReminderWithinDatesPredicate(start, end); + model.updateFilteredReminderList(predicate); + return new CommandResult(String.format(MESSAGE_SUCCESS, start, end), false, false); + } + + @Override + public boolean equals(Object other) { + // Objects.equals() to handle null fields + return other == this // short circuit if same object + || (other instanceof ListRemCommand // instanceof handles nulls + && Objects.equals(start, ((ListRemCommand) other).start) + && Objects.equals(end, ((ListRemCommand) other).end) + && Objects.equals(targetIndex, ((ListRemCommand) other).targetIndex)); + } +} diff --git a/src/main/java/quickdocs/logic/commands/PrescriptionCommand.java b/src/main/java/quickdocs/logic/commands/PrescriptionCommand.java new file mode 100644 index 000000000000..bc6404173172 --- /dev/null +++ b/src/main/java/quickdocs/logic/commands/PrescriptionCommand.java @@ -0,0 +1,118 @@ +package quickdocs.logic.commands; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Optional; +import java.util.logging.Logger; + +import quickdocs.commons.core.LogsCenter; +import quickdocs.logic.CommandHistory; +import quickdocs.logic.commands.exceptions.CommandException; +import quickdocs.logic.parser.PrescriptionCommandParser; +import quickdocs.model.Model; +import quickdocs.model.consultation.Prescription; +import quickdocs.model.medicine.Medicine; + +/** + * Creates or replaces the current prescription record of the current consultation session. + * Prescription allows the user to administer the medicine and its quantity to the patient + * during the consultation session. + */ +public class PrescriptionCommand extends Command { + + public static final String COMMAND_WORD = "prescribe"; + public static final String COMMAND_ALIAS = "p"; + public static final String MEDICINE_NOT_FOUND = "Medicine: %s not found"; + public static final String INSUFFICIENT_MEDICINE = "Insufficient %s to prescribe"; + public static final String NO_ONGOING_CONSULTATION = "There is no ongoing consultation to prescribe medicine to\n"; + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Adds prescribed medicine and its quantities for current patient.\n" + + "Parameters: " + + PrescriptionCommandParser.PREFIX_MEDICINE + "MEDICINE 1 " + PrescriptionCommandParser.PREFIX_MEDICINE + + "MEDICINE 2 " + " ... " + + PrescriptionCommandParser.PREFIX_QUANTITY + "QUANTITY FOR MEDICINE 1 " + + PrescriptionCommandParser.PREFIX_QUANTITY + "QUANTITY FOR MEDICINE 2 " + " ... \n" + + "OR: " + PrescriptionCommandParser.PREFIX_MEDICINE + "MEDICINE 1 " + + PrescriptionCommandParser.PREFIX_QUANTITY + "QUANTITY FOR MEDICINE 1" + " ... \n" + + "Example: " + + COMMAND_WORD + " m/Ibuprofen q/1 m/Afrin Spray q/2\n"; + + private static final Logger logger = LogsCenter.getLogger(PrescriptionCommand.class); + + private ArrayList medicineList; + private ArrayList quantityList; + + public PrescriptionCommand(ArrayList medicineList, ArrayList quantityList) { + this.medicineList = medicineList; + this.quantityList = quantityList; + } + + public ArrayList getMedicineList() { + return medicineList; + } + + public ArrayList getQuantityList() { + return quantityList; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + + if (model.checkConsultation() == false) { + throw new CommandException(NO_ONGOING_CONSULTATION); + } + + ArrayList prescriptions = new ArrayList<>(); + + /** + * Check whether the medicine to be administered is currently present in the inventory and + * if it is present, check whether there is sufficient medicine to be prescribed. + * + * Throws CommandException when medicine to be prescribed is insufficient or not + * present. + */ + for (int i = 0; i < medicineList.size(); i++) { + + Optional foundMedicine = model.findMedicine(medicineList.get(i)); + + if (!foundMedicine.isPresent()) { + throw new CommandException(String.format(MEDICINE_NOT_FOUND, medicineList.get(i))); + } + + if (quantityList.get(i) > foundMedicine.get().getQuantity()) { + throw new CommandException(String.format(INSUFFICIENT_MEDICINE, medicineList.get(i))); + } + + prescriptions.add(new Prescription(foundMedicine.get(), quantityList.get(i))); + } + + model.prescribeMedicine(prescriptions); + + logger.info("Prescription entered or replaced for current consultation"); + + StringBuilder sb = new StringBuilder(); + sb.append("prescription:\n"); + sb.append("====================\n"); + for (Prescription prescription : prescriptions) { + sb.append(prescription.toString()); + } + return new CommandResult(sb.toString()); + } + + @Override + public boolean equals(Object other) { + return other == this || (other instanceof PrescriptionCommand // instanceof handles nulls + && checkAttributes((PrescriptionCommand) other)); + } + + /** + * Check whether the content of medicine list and quantity list is the same for two + * prescription commands. This method is used in testing. + * + * @param other the other PrescriptionCommand object + */ + public boolean checkAttributes(PrescriptionCommand other) { + return Arrays.equals(getMedicineList().toArray(), other.getMedicineList().toArray()) + && getQuantityList().equals(other.getQuantityList()); + } +} diff --git a/src/main/java/quickdocs/logic/commands/PurchaseMedicineCommand.java b/src/main/java/quickdocs/logic/commands/PurchaseMedicineCommand.java new file mode 100644 index 000000000000..975cc6704acd --- /dev/null +++ b/src/main/java/quickdocs/logic/commands/PurchaseMedicineCommand.java @@ -0,0 +1,28 @@ +package quickdocs.logic.commands; + +/** + * An abstract command to purchase medicine + */ +public abstract class PurchaseMedicineCommand extends Command { + + public static final String COMMAND_WORD = "buymed"; + public static final String COMMAND_ALIAS = "bm"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Purchases an existing medicine for certain quantity with certain unit price. " + + "Parameters: " + + "[Medicine path separated by \\] " + + "[Quantity of the purchase] " + + "[Cost of the purchase]\n" + + "Example: " + COMMAND_WORD + " " + + "root\\TCM\\Herbs\\herb1 50 60\n" + + "Or\n" + + "Parameters: " + + "[Medicine name] " + + "[Quantity of the purchase] " + + "[Cost of the purchase]\n" + + "Example: " + COMMAND_WORD + " " + + "herb1 50 60"; + + public static final String MESSAGE_SUCCESS = "Purchase successful.\n"; +} diff --git a/src/main/java/quickdocs/logic/commands/PurchaseMedicineViaPathCommand.java b/src/main/java/quickdocs/logic/commands/PurchaseMedicineViaPathCommand.java new file mode 100644 index 000000000000..52e79cb9ba9b --- /dev/null +++ b/src/main/java/quickdocs/logic/commands/PurchaseMedicineViaPathCommand.java @@ -0,0 +1,46 @@ +package quickdocs.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.math.BigDecimal; +import java.util.Arrays; + +import quickdocs.logic.CommandHistory; +import quickdocs.logic.commands.exceptions.CommandException; +import quickdocs.model.Model; + +/** + * A command to record purchase of a medicine into the model + */ +public class PurchaseMedicineViaPathCommand extends PurchaseMedicineCommand { + + private final String[] path; + private final int quantity; + private final BigDecimal cost; + + public PurchaseMedicineViaPathCommand(String[] path, int quantity, BigDecimal cost) { + this.path = path; + this.quantity = quantity; + this.cost = cost; + } + + @Override + public CommandResult execute(Model model, CommandHistory commandHistory) throws CommandException { + requireNonNull(model); + try { + model.purchaseMedicine(path, quantity, cost); + } catch (Exception ex) { + throw new CommandException(ex.getMessage()); + } + return new CommandResult(MESSAGE_SUCCESS); + } + + @Override + public boolean equals(Object other) { + return other == this || ( + other instanceof PurchaseMedicineViaPathCommand + && Arrays.equals(path, ((PurchaseMedicineViaPathCommand) other).path) + && quantity == ((PurchaseMedicineViaPathCommand) other).quantity + && cost.equals(((PurchaseMedicineViaPathCommand) other).cost)); + } +} diff --git a/src/main/java/quickdocs/logic/commands/PurchaseMedicineWoPathCommand.java b/src/main/java/quickdocs/logic/commands/PurchaseMedicineWoPathCommand.java new file mode 100644 index 000000000000..92f52236403d --- /dev/null +++ b/src/main/java/quickdocs/logic/commands/PurchaseMedicineWoPathCommand.java @@ -0,0 +1,45 @@ +package quickdocs.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.math.BigDecimal; + +import quickdocs.logic.CommandHistory; +import quickdocs.logic.commands.exceptions.CommandException; +import quickdocs.model.Model; + +/** + * A command to record purchase of a medicine without a path + */ +public class PurchaseMedicineWoPathCommand extends PurchaseMedicineCommand { + + private final String medicineName; + private final int quantity; + private final BigDecimal cost; + + public PurchaseMedicineWoPathCommand(String medicineName, int quantity, BigDecimal cost) { + this.medicineName = medicineName; + this.quantity = quantity; + this.cost = cost; + } + + @Override + public CommandResult execute(Model model, CommandHistory commandHistory) throws CommandException { + requireNonNull(model); + try { + model.purchaseMedicine(medicineName, quantity, cost); + } catch (Exception ex) { + throw new CommandException(ex.getMessage()); + } + return new CommandResult(MESSAGE_SUCCESS); + } + + @Override + public boolean equals(Object other) { + return other == this || ( + other instanceof PurchaseMedicineWoPathCommand + && medicineName.equals(((PurchaseMedicineWoPathCommand) other).medicineName) + && quantity == ((PurchaseMedicineWoPathCommand) other).quantity + && cost.equals(((PurchaseMedicineWoPathCommand) other).cost)); + } +} diff --git a/src/main/java/quickdocs/logic/commands/SetConsultationFeeCommand.java b/src/main/java/quickdocs/logic/commands/SetConsultationFeeCommand.java new file mode 100644 index 000000000000..eecd4e945006 --- /dev/null +++ b/src/main/java/quickdocs/logic/commands/SetConsultationFeeCommand.java @@ -0,0 +1,56 @@ +package quickdocs.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.math.BigDecimal; + +import quickdocs.logic.CommandHistory; +import quickdocs.logic.commands.exceptions.CommandException; +import quickdocs.model.Model; +import quickdocs.model.record.Statistics; + +/** + * Sets the consultation fee of the clinic. + */ +public class SetConsultationFeeCommand extends Command { + + public static final String COMMAND_WORD = "setconsultfee"; + public static final String COMMAND_ALIAS = "setfee"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": sets the consultation fee to the desired value.\n" + + "Parameters: FEE\n" + + "Example: " + COMMAND_WORD + " 30.00"; + + public final BigDecimal fee; + + public SetConsultationFeeCommand(BigDecimal fee) { + requireNonNull(fee); + this.fee = fee; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + try { + model.setConsultationFee(fee); + StringBuilder sb = new StringBuilder(); + sb.append("Consultation fee has been successfully changed to " + Statistics.currencyFormat(fee)); + sb.append("\n\n"); + return new CommandResult(sb.toString()); + } catch (Exception ex) { + throw new CommandException(ex.getMessage()); + } + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof SetConsultationFeeCommand)) { + return false; + } + SetConsultationFeeCommand sc = (SetConsultationFeeCommand) other; + return this.fee.compareTo(sc.fee) == 0; + } +} diff --git a/src/main/java/quickdocs/logic/commands/SetPriceCommand.java b/src/main/java/quickdocs/logic/commands/SetPriceCommand.java new file mode 100644 index 000000000000..2f7c448d7693 --- /dev/null +++ b/src/main/java/quickdocs/logic/commands/SetPriceCommand.java @@ -0,0 +1,26 @@ +package quickdocs.logic.commands; + +/** + * An abstract class, parenting SetPriceViaPathCommand and SetPriceWoPathCommand + */ +public abstract class SetPriceCommand extends Command { + + public static final String COMMAND_WORD = "setprice"; + public static final String COMMAND_ALIAS = "sp"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Set the price for a specific medicine " + + "given by the path/name. " + + "Parameters: " + + "[Medicine path separated by \\] " + + "[Price]\n" + + "Example: " + COMMAND_WORD + " " + + "root\\TCM\\healroot 25.7\n" + + "Or\n" + + "Parameters: " + + "[Name of Medicine] " + + "[Price]\n" + + "Example: " + COMMAND_WORD + " " + + "healroot 25.7"; + + public static final String MESSAGE_SUCCESS = "%1$s's price has been set to %2$s.\n"; +} diff --git a/src/main/java/quickdocs/logic/commands/SetPriceViaPathCommand.java b/src/main/java/quickdocs/logic/commands/SetPriceViaPathCommand.java new file mode 100644 index 000000000000..9facc2591713 --- /dev/null +++ b/src/main/java/quickdocs/logic/commands/SetPriceViaPathCommand.java @@ -0,0 +1,47 @@ +package quickdocs.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.math.BigDecimal; +import java.util.Optional; + +import quickdocs.logic.CommandHistory; +import quickdocs.logic.commands.exceptions.CommandException; +import quickdocs.model.Model; +import quickdocs.model.medicine.Medicine; + +/** + * Set the price of a medicine via path + */ +public class SetPriceViaPathCommand extends SetPriceCommand { + + private String[] path; + private BigDecimal price; + + public SetPriceViaPathCommand(String[] path, BigDecimal price) { + this.path = path; + this.price = price; + } + + /** + * Set the price of a medicine found through path + * @param model {@code Model} which the command should operate on. + * @param commandHistory the commandHistory to record on + * @return CommandResult showing the result + * @throws CommandException + */ + @Override + public CommandResult execute(Model model, CommandHistory commandHistory) throws CommandException { + requireNonNull(model); + try { + Optional medicine = model.findMedicine(path); + if (!medicine.isPresent()) { + throw new CommandException("Invalid path."); + } + model.setPrice(medicine.get(), price); + return new CommandResult(String.format(MESSAGE_SUCCESS, medicine.get().name, price.toString())); + } catch (Exception ex) { + throw new CommandException(ex.getMessage()); + } + } +} diff --git a/src/main/java/quickdocs/logic/commands/SetPriceWoPathCommand.java b/src/main/java/quickdocs/logic/commands/SetPriceWoPathCommand.java new file mode 100644 index 000000000000..9ce67e216cd1 --- /dev/null +++ b/src/main/java/quickdocs/logic/commands/SetPriceWoPathCommand.java @@ -0,0 +1,39 @@ +package quickdocs.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.math.BigDecimal; +import java.util.Optional; + +import quickdocs.logic.CommandHistory; +import quickdocs.logic.commands.exceptions.CommandException; +import quickdocs.model.Model; +import quickdocs.model.medicine.Medicine; + +/** + * Set the price of a medicine by its name + */ +public class SetPriceWoPathCommand extends SetPriceCommand { + + private String medicineName; + private BigDecimal price; + + public SetPriceWoPathCommand(String name, BigDecimal price) { + medicineName = name; + this.price = price; + } + @Override + public CommandResult execute(Model model, CommandHistory commandHistory) throws CommandException { + requireNonNull(model); + try { + Optional medicine = model.findMedicine(medicineName); + if (!medicine.isPresent()) { + throw new CommandException("No medicine with such name found."); + } + model.setPrice(medicine.get(), price); + return new CommandResult(String.format(MESSAGE_SUCCESS, medicineName, price.toString())); + } catch (Exception ex) { + throw new CommandException(ex.getMessage()); + } + } +} diff --git a/src/main/java/quickdocs/logic/commands/StatisticsCommand.java b/src/main/java/quickdocs/logic/commands/StatisticsCommand.java new file mode 100644 index 000000000000..4da7c65a20c8 --- /dev/null +++ b/src/main/java/quickdocs/logic/commands/StatisticsCommand.java @@ -0,0 +1,64 @@ +package quickdocs.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.time.YearMonth; + +import quickdocs.logic.CommandHistory; +import quickdocs.logic.commands.exceptions.CommandException; +import quickdocs.model.Model; +import quickdocs.model.record.Statistics; + +/** + * Gets the Statistics of the clinic. + */ +public class StatisticsCommand extends Command { + + public static final String COMMAND_WORD = "statistics"; + public static final String COMMAND_ALIAS = "stats"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": displays the statistics from the given date range.\n" + + "Dates cannot be before Jan 2019, and the second date must be after the first date, upto current month." + + "Parameters: MMYYYY [MMYYYY]\n" + + "Example: " + COMMAND_WORD + " 012019"; + + private final YearMonth fromYearMonth; + private final YearMonth toYearMonth; + + public StatisticsCommand(YearMonth from, YearMonth to) { + requireNonNull(from); + requireNonNull(to); + this.fromYearMonth = from; + this.toYearMonth = to; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + try { + Statistics stats = model.getStatistics(this.fromYearMonth, this.toYearMonth); + StringBuilder sb = new StringBuilder(); + sb.append("Displaying result from ") + .append(this.fromYearMonth.toString()) + .append(" to ") + .append(this.toYearMonth.toString()) + .append(".\n\n") + .append(stats.toString()); + return new CommandResult(sb.toString()); + } catch (IllegalArgumentException ex) { + throw new CommandException(ex.getMessage()); + } + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof StatisticsCommand)) { + return false; + } + StatisticsCommand sc = (StatisticsCommand) other; + return this.toYearMonth.equals(sc.toYearMonth) && this.fromYearMonth.equals(sc.fromYearMonth); + } +} diff --git a/src/main/java/quickdocs/logic/commands/ViewStorageCommand.java b/src/main/java/quickdocs/logic/commands/ViewStorageCommand.java new file mode 100644 index 000000000000..3bc4637bc3b6 --- /dev/null +++ b/src/main/java/quickdocs/logic/commands/ViewStorageCommand.java @@ -0,0 +1,64 @@ +package quickdocs.logic.commands; + +import static quickdocs.commons.util.StringUtil.fromPathToString; + +import java.util.Arrays; +import java.util.Optional; + +import quickdocs.logic.CommandHistory; +import quickdocs.model.Model; +import quickdocs.model.medicine.Directory; +import quickdocs.model.medicine.Medicine; + +/** + * A Command to view the detail of directory/medicine at the given path + */ +public class ViewStorageCommand extends Command { + + public static final String COMMAND_WORD = "listmed"; + public static final String COMMAND_ALIAS = "lm"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Display the information of the directory/medicine " + + "given by the path " + + "Parameters: " + + "Directory/Medicine separated by \\\n" + + "Example: " + COMMAND_WORD + " " + + "root\\TCM"; + + public static final String MESSAGE_SUCCESS_DIRECTORY = "Directory found at %1$s\n%2$s"; + + public static final String MESSAGE_SUCCESS_MEDICINE = "Medicine found at %1$s\n%2$s"; + + private String[] path; + + public ViewStorageCommand(String[] path) { + this.path = path; + } + + /** + * Execute the command and return a CommandResult showing the detail + * @param model {@code Model} which the command should operate on. + * @param commandHistory the history of commands to record on + * @return The CommandResult including the details of directory/medicine + */ + public CommandResult execute(Model model, CommandHistory commandHistory) { + Optional directory = model.findDirectory(path); + if (!directory.isPresent()) { + Optional medicine = model.findMedicine(path); + if (!medicine.isPresent()) { + return new CommandResult("No directory/medicine found at the given path\n"); + } + return new CommandResult(String.format(MESSAGE_SUCCESS_MEDICINE, fromPathToString(path), + medicine.get().viewDetail() + "\n")); + } + return new CommandResult(String.format(MESSAGE_SUCCESS_DIRECTORY, fromPathToString(path), + directory.get().viewDetail())); + } + + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof ViewStorageCommand + && Arrays.equals(this.path, ((ViewStorageCommand) other).path)); + } +} diff --git a/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java b/src/main/java/quickdocs/logic/commands/exceptions/CommandException.java similarity index 89% rename from src/main/java/seedu/address/logic/commands/exceptions/CommandException.java rename to src/main/java/quickdocs/logic/commands/exceptions/CommandException.java index a16bd14f2cde..96faee5352e4 100644 --- a/src/main/java/seedu/address/logic/commands/exceptions/CommandException.java +++ b/src/main/java/quickdocs/logic/commands/exceptions/CommandException.java @@ -1,4 +1,4 @@ -package seedu.address.logic.commands.exceptions; +package quickdocs.logic.commands.exceptions; /** * Represents an error which occurs during execution of a {@link Command}. diff --git a/src/main/java/quickdocs/logic/parser/AddAppCommandParser.java b/src/main/java/quickdocs/logic/parser/AddAppCommandParser.java new file mode 100644 index 000000000000..f2fed8606d9b --- /dev/null +++ b/src/main/java/quickdocs/logic/parser/AddAppCommandParser.java @@ -0,0 +1,58 @@ +package quickdocs.logic.parser; + +import static quickdocs.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.stream.Stream; + +import quickdocs.logic.commands.AddAppCommand; +import quickdocs.logic.parser.exceptions.ParseException; +import quickdocs.model.patient.Nric; + +/** + * Parses input arguments and creates a new {@code AddAppCommand} object. + */ +public class AddAppCommandParser implements Parser { + public static final Prefix PREFIX_NRIC = new Prefix("r/"); + public static final Prefix PREFIX_DATE = new Prefix("d/"); + public static final Prefix PREFIX_START = new Prefix("s/"); + public static final Prefix PREFIX_END = new Prefix("e/"); + public static final Prefix PREFIX_COMMENT = new Prefix("c/"); + + /** + * Parses the given {@code String} of arguments in the context of the {@code AddAppCommand} + * and returns an {@code AddAppCommand} object for execution. + * + * @throws ParseException if the user input does not conform to the expected format. + */ + public AddAppCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NRIC, PREFIX_DATE, PREFIX_START, PREFIX_END, PREFIX_COMMENT); + + // check if required prefixes are present + boolean prefixesPresent = arePrefixesPresent(argMultimap, PREFIX_NRIC, + PREFIX_DATE, PREFIX_START, PREFIX_END, PREFIX_COMMENT); + boolean preamblePresent = argMultimap.getPreamble().isEmpty(); + if (!prefixesPresent || !preamblePresent) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddAppCommand.MESSAGE_USAGE)); + } + + Nric nric = new Nric(argMultimap.getValue(PREFIX_NRIC).get().trim()); + LocalDate date = ParserUtil.parseDate(argMultimap.getValue(PREFIX_DATE).get().trim()); + LocalTime start = ParserUtil.parseTime(argMultimap.getValue(PREFIX_START).get().trim()); + LocalTime end = ParserUtil.parseTime(argMultimap.getValue(PREFIX_END).get().trim()); + String comment = argMultimap.getValue(PREFIX_COMMENT).get().trim(); + + return new AddAppCommand(nric, date, start, end, comment); + } + + /** + * 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/quickdocs/logic/parser/AddDirectoryCommandParser.java b/src/main/java/quickdocs/logic/parser/AddDirectoryCommandParser.java new file mode 100644 index 000000000000..ff2cfa638984 --- /dev/null +++ b/src/main/java/quickdocs/logic/parser/AddDirectoryCommandParser.java @@ -0,0 +1,41 @@ +package quickdocs.logic.parser; + +import static quickdocs.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import quickdocs.logic.commands.AddDirectoryCommand; +import quickdocs.logic.parser.exceptions.ParseException; + +/** + * A parser to parse User input into AddDirectory Command + */ +public class AddDirectoryCommandParser implements Parser { + + private static final Pattern AddDirectoryCommand_Argument_Format = + Pattern.compile("(?\\S+)(?:\\s+)(?[^\\s^\\\\]+)"); + + /** + * Parse user input into an AddDirectory Command + * @param args user input + * @return A corresponding AddDirectory Command + * @throws ParseException + */ + public AddDirectoryCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddDirectoryCommand.MESSAGE_USAGE)); + } + Matcher matcher = AddDirectoryCommand_Argument_Format.matcher(trimmedArgs); + if (!matcher.matches()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddDirectoryCommand.MESSAGE_USAGE)); + } + String rawPath = matcher.group("rawPath"); + String name = matcher.group("name"); + String[] path = rawPath.split("\\\\"); + return new AddDirectoryCommand(path, name); + } +} diff --git a/src/main/java/quickdocs/logic/parser/AddMedicineCommandParser.java b/src/main/java/quickdocs/logic/parser/AddMedicineCommandParser.java new file mode 100644 index 000000000000..f88ca00f9b9e --- /dev/null +++ b/src/main/java/quickdocs/logic/parser/AddMedicineCommandParser.java @@ -0,0 +1,81 @@ +package quickdocs.logic.parser; + +import static quickdocs.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.math.BigDecimal; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import quickdocs.logic.commands.AddMedicineCommand; +import quickdocs.logic.parser.exceptions.ParseException; + +/** + * Parses input argument and returns an AddMedicineCommand + */ +public class AddMedicineCommandParser implements Parser { + + private static final Pattern AddMedicineCommand_Argument_Format = + Pattern.compile("(?\\S+)(?:\\s+)(?\\S.+)"); + private static final Pattern MedicineInformation_Format = + Pattern.compile("(?\\S+)(?:\\s*)(?\\s.*)?"); + private static final Pattern BigDecimal_Format = Pattern.compile("(?\\d+)(?:\\.?)(?\\d*)"); + private static final Pattern Integer_Format = Pattern.compile("\\d+"); + private static final Prefix PREFIX_PRICE = new Prefix("p/"); + private static final Prefix PREFIX_QUANTITY = new Prefix("q/"); + + /** + * parse the given input to produce a AddMedicineCommand + * @param args the input + * @return An AddMedicineCommand for execution + * @throws ParseException if the user input does not conform the format + */ + public AddMedicineCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddMedicineCommand.MESSAGE_USAGE)); + } + final Matcher pathMedicine = AddMedicineCommand_Argument_Format.matcher(trimmedArgs); + if (!pathMedicine.matches()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddMedicineCommand.MESSAGE_USAGE)); + } + final String rawPath = pathMedicine.group("rawPath"); + String[] path = rawPath.split("\\\\"); + final String medicineInfo = pathMedicine.group("medicineInformation"); + final Matcher namePriceQuantity = MedicineInformation_Format.matcher(medicineInfo); + if (!namePriceQuantity.matches()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddMedicineCommand.MESSAGE_USAGE)); + } + String medicineName = namePriceQuantity.group("name"); + String priceAndQuantity = namePriceQuantity.group("priceAndQuantity"); + if (priceAndQuantity == null) { + return new AddMedicineCommand(path, medicineName, Optional.empty(), Optional.empty()); + } + ArgumentMultimap map = ArgumentTokenizer.tokenize(priceAndQuantity, PREFIX_PRICE, PREFIX_QUANTITY); + if ((!isPrefixesPresent(map, PREFIX_QUANTITY) + || !isPrefixesPresent(map, PREFIX_PRICE) + || !map.getPreamble().isEmpty())) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddMedicineCommand.MESSAGE_USAGE)); + } + String quantityString = map.getValue(PREFIX_QUANTITY).get().trim(); + String priceString = map.getValue(PREFIX_PRICE).get().trim(); + if (!Integer_Format.matcher(quantityString).matches() || !BigDecimal_Format.matcher(priceString).matches()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddMedicineCommand.MESSAGE_USAGE)); + } + return new AddMedicineCommand(path, medicineName, + Optional.of(Integer.parseInt(quantityString)), Optional.of(new BigDecimal(priceString))); + } + + /** + * Returns true if the prefix contains a non-empty {@code Optional} value in the given + * {@code ArgumentMultimap}. + */ + private static boolean isPrefixesPresent(ArgumentMultimap argumentMultimap, Prefix prefix) { + return argumentMultimap.getValue(prefix).isPresent(); + } + +} diff --git a/src/main/java/quickdocs/logic/parser/AddPatientParser.java b/src/main/java/quickdocs/logic/parser/AddPatientParser.java new file mode 100644 index 000000000000..4fbe3e7b67fc --- /dev/null +++ b/src/main/java/quickdocs/logic/parser/AddPatientParser.java @@ -0,0 +1,111 @@ +package quickdocs.logic.parser; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.stream.Stream; + +import quickdocs.logic.commands.AddPatientCommand; +import quickdocs.logic.parser.exceptions.ParseException; +import quickdocs.model.patient.Address; +import quickdocs.model.patient.Contact; +import quickdocs.model.patient.Dob; +import quickdocs.model.patient.Email; +import quickdocs.model.patient.Gender; +import quickdocs.model.patient.Name; +import quickdocs.model.patient.Nric; +import quickdocs.model.patient.Patient; +import quickdocs.model.tag.Tag; + +/** + * parse arguments to create AddPatientCommand for logic to execute + */ +public class AddPatientParser implements Parser { + + public static final Prefix PREFIX_NAME = new Prefix("n/"); + public static final Prefix PREFIX_NRIC = new Prefix("r/"); + public static final Prefix PREFIX_DOB = new Prefix("d/"); + public static final Prefix PREFIX_ADDRESS = new Prefix("a/"); + public static final Prefix PREFIX_EMAIL = new Prefix("e/"); + public static final Prefix PREFIX_CONTACT = new Prefix("c/"); + public static final Prefix PREFIX_GENDER = new Prefix("g/"); + public static final Prefix PREFIX_TAG = new Prefix("t/"); + + public static final String INVALID_ADD_ARGUMENTS = "Invalid or insufficient input parameters entered.\n" + + AddPatientCommand.MESSAGE_USAGE; + + /** + * Parse the arguments entered by users to create an AddPatientCommand for the + * logic manager to execute + * + * @param args the arguments to create the patient record: name, nric and so on + * @return an AddPatientCommand with the patient record to be added + * @throws ParseException when insufficient arguments are entered or any of the arguments are invalid + */ + @Override + public AddPatientCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_NRIC, PREFIX_DOB, PREFIX_ADDRESS, PREFIX_EMAIL, + PREFIX_CONTACT, PREFIX_GENDER, PREFIX_TAG); + + boolean arePrefixesPresent = arePrefixesPresent(argMultimap, PREFIX_NAME, + PREFIX_NRIC, PREFIX_DOB, PREFIX_ADDRESS, PREFIX_EMAIL, + PREFIX_CONTACT, PREFIX_GENDER); + boolean isPreambleMissing = argMultimap.getPreamble().isEmpty(); + + if (!arePrefixesPresent || !isPreambleMissing) { + throw new ParseException(INVALID_ADD_ARGUMENTS); + } + + Patient patient; + + try { + + Name name = new Name(argMultimap.getValue(PREFIX_NAME).get().trim()); + Nric nric = new Nric(argMultimap.getValue(PREFIX_NRIC).get().trim()); + Email email = new Email(argMultimap.getValue(PREFIX_EMAIL).get().trim()); + Address address = new Address(argMultimap.getValue(PREFIX_ADDRESS).get().trim()); + Contact contact = new Contact(argMultimap.getValue(PREFIX_CONTACT).get().trim()); + Gender gender = new Gender(argMultimap.getValue(PREFIX_GENDER).get().trim()); + Dob dob = new Dob(argMultimap.getValue(PREFIX_DOB).get().trim()); + ArrayList tagList = parseTags(argMultimap.getAllValues(PREFIX_TAG)); + patient = new Patient(name, nric, email, address, contact, gender, dob, tagList); + + } catch (IllegalArgumentException Iae) { + throw new ParseException(Iae.getMessage()); + } + + return new AddPatientCommand(patient); + } + + /** + * Returns true if the necessary arguments to create the patient records + * indicated by their prefixes, are supplied + * {@code ArgumentMultimap}. + * + * @param argumentMultimap arguments that are parsed in from the user input + * @param prefixes the prefixes used to separate the different arguments of a patient record + * @return true when all the required arguments are present + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } + + /** + * parse the list of tags entered into a list of tags to create the patient record with + */ + public static ArrayList parseTags(Collection tags) { + final ArrayList tagList = new ArrayList<>(); + for (String tagName : tags) { + tagList.add(parseTag(tagName)); + } + return tagList; + } + + /** + * returns a tag created from the command argument + */ + public static Tag parseTag(String tag) { + String trimmedTag = tag.trim(); + return new Tag(trimmedTag); + } +} diff --git a/src/main/java/quickdocs/logic/parser/AddRemCommandParser.java b/src/main/java/quickdocs/logic/parser/AddRemCommandParser.java new file mode 100644 index 000000000000..0c67b80d7009 --- /dev/null +++ b/src/main/java/quickdocs/logic/parser/AddRemCommandParser.java @@ -0,0 +1,72 @@ +package quickdocs.logic.parser; + +import static quickdocs.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.stream.Stream; + +import quickdocs.logic.commands.AddRemCommand; +import quickdocs.logic.parser.exceptions.ParseException; +import quickdocs.model.reminder.Reminder; + +/** + * Parses input arguments and creates a new {@code AddRemCommand} object. + */ +public class AddRemCommandParser implements Parser { + public static final Prefix PREFIX_TITLE = new Prefix("t/"); + public static final Prefix PREFIX_DATE = new Prefix("d/"); + public static final Prefix PREFIX_START = new Prefix("s/"); + public static final Prefix PREFIX_END = new Prefix("e/"); + public static final Prefix PREFIX_COMMENT = new Prefix("c/"); + + /** + * Parses the given {@code String} of arguments in the context of the {@code AddRemCommand} + * and returns an {@code AddRemCommand} object for execution. + * + * @throws ParseException if the user input does not conform to the expected format. + */ + public AddRemCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_TITLE, PREFIX_DATE, PREFIX_START, PREFIX_END, PREFIX_COMMENT); + + // check if required prefixes are present + boolean prefixesPresent = arePrefixesPresent(argMultimap, PREFIX_TITLE, + PREFIX_DATE, PREFIX_START); + boolean preamblePresent = argMultimap.getPreamble().isEmpty(); + if (!prefixesPresent || !preamblePresent) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddRemCommand.MESSAGE_USAGE)); + } + + String title = argMultimap.getValue(PREFIX_TITLE).get().trim(); + LocalDate date = ParserUtil.parseDate(argMultimap.getValue(PREFIX_DATE).get().trim()); + LocalTime start = ParserUtil.parseTime(argMultimap.getValue(PREFIX_START).get().trim()); + LocalTime end; + String comment; + + // check if end time was provided + if (argMultimap.getValue(PREFIX_END).isPresent()) { + end = ParserUtil.parseTime(argMultimap.getValue(PREFIX_END).get().trim()); + } else { + end = null; + } + + // check if comments were provided + if (argMultimap.getValue(PREFIX_COMMENT).isPresent()) { + comment = argMultimap.getValue(PREFIX_COMMENT).get().trim(); + } else { + comment = null; + } + + return new AddRemCommand(new Reminder(title, comment, date, start, end)); + } + + /** + * 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/quickdocs/logic/parser/AlarmCommandParser.java b/src/main/java/quickdocs/logic/parser/AlarmCommandParser.java new file mode 100644 index 000000000000..baca2859887b --- /dev/null +++ b/src/main/java/quickdocs/logic/parser/AlarmCommandParser.java @@ -0,0 +1,41 @@ +package quickdocs.logic.parser; + +import static quickdocs.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import quickdocs.logic.commands.AlarmCommand; +import quickdocs.logic.parser.exceptions.ParseException; + +/** + * A parser to parse user input to AlarmCommand + */ +public class AlarmCommandParser implements Parser { + + private static final Pattern AlarmCommand_Argument_Format = + Pattern.compile("(?\\S+)(?:\\s+)(?\\d+)"); + + /** + * To parse user input to AlarmCommand + * @param args the user input + * @return A corresponding AlarmCommand + * @throws ParseException + */ + public AlarmCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, AlarmCommand.MESSAGE_USAGE)); + } + final Matcher matcher = AlarmCommand_Argument_Format.matcher(trimmedArgs); + if (!matcher.matches()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, AlarmCommand.MESSAGE_USAGE)); + } + String rawPath = matcher.group("rawPath"); + String threshold = matcher.group("threshold"); + String[] path = rawPath.split("\\\\"); + return new AlarmCommand(path, Integer.parseInt(threshold)); + } +} diff --git a/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java b/src/main/java/quickdocs/logic/parser/ArgumentMultimap.java similarity index 97% rename from src/main/java/seedu/address/logic/parser/ArgumentMultimap.java rename to src/main/java/quickdocs/logic/parser/ArgumentMultimap.java index 954c8e18f8ea..0a61b1ada0ae 100644 --- a/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java +++ b/src/main/java/quickdocs/logic/parser/ArgumentMultimap.java @@ -1,4 +1,4 @@ -package seedu.address.logic.parser; +package quickdocs.logic.parser; import java.util.ArrayList; import java.util.HashMap; @@ -21,7 +21,7 @@ public class ArgumentMultimap { /** * Associates the specified argument value with {@code prefix} key in this map. * If the map previously contained a mapping for the key, the new value is appended to the list of existing values. - * + * * * @param prefix Prefix key with which the specified argument value is to be associated * @param argValue Argument value to be associated with the specified prefix key */ diff --git a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java b/src/main/java/quickdocs/logic/parser/ArgumentTokenizer.java similarity index 99% rename from src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java rename to src/main/java/quickdocs/logic/parser/ArgumentTokenizer.java index 5c9aebfa4888..2c8d33227eaf 100644 --- a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java +++ b/src/main/java/quickdocs/logic/parser/ArgumentTokenizer.java @@ -1,4 +1,4 @@ -package seedu.address.logic.parser; +package quickdocs.logic.parser; import java.util.ArrayList; import java.util.Arrays; diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/quickdocs/logic/parser/CliSyntax.java similarity index 92% rename from src/main/java/seedu/address/logic/parser/CliSyntax.java rename to src/main/java/quickdocs/logic/parser/CliSyntax.java index 75b1a9bf1190..8437ff3eaedf 100644 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ b/src/main/java/quickdocs/logic/parser/CliSyntax.java @@ -1,4 +1,4 @@ -package seedu.address.logic.parser; +package quickdocs.logic.parser; /** * Contains Command Line Interface (CLI) syntax definitions common to multiple commands diff --git a/src/main/java/quickdocs/logic/parser/ConsultationCommandParser.java b/src/main/java/quickdocs/logic/parser/ConsultationCommandParser.java new file mode 100644 index 000000000000..3843a9dd558a --- /dev/null +++ b/src/main/java/quickdocs/logic/parser/ConsultationCommandParser.java @@ -0,0 +1,34 @@ +package quickdocs.logic.parser; + +import static quickdocs.logic.commands.ConsultationCommand.MESSAGE_USAGE; + +import java.util.stream.Stream; + +import quickdocs.logic.commands.ConsultationCommand; +import quickdocs.logic.parser.exceptions.ParseException; + +/** + * Parses the arguments to search for a patient and start the consultation + */ +public class ConsultationCommandParser implements Parser { + + public static final Prefix PREFIX_NRIC = new Prefix("r/"); + public static final String INVALID_CONSULTATION_ARGUMENTS = "Invalid arguments entered for consultation command.\n" + + MESSAGE_USAGE; + + @Override + public ConsultationCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NRIC); + + if (!arePrefixesPresent(argMultimap, PREFIX_NRIC)) { + throw new ParseException(INVALID_CONSULTATION_ARGUMENTS); + } + + return new ConsultationCommand(argMultimap.getValue(PREFIX_NRIC).get()); + } + + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } +} diff --git a/src/main/java/quickdocs/logic/parser/DeleteAppCommandParser.java b/src/main/java/quickdocs/logic/parser/DeleteAppCommandParser.java new file mode 100644 index 000000000000..961aeacc9674 --- /dev/null +++ b/src/main/java/quickdocs/logic/parser/DeleteAppCommandParser.java @@ -0,0 +1,50 @@ +package quickdocs.logic.parser; + +import static quickdocs.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.stream.Stream; + +import quickdocs.logic.commands.DeleteAppCommand; +import quickdocs.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new {@code DeleteAppCommand} object. + */ +public class DeleteAppCommandParser implements Parser { + public static final Prefix PREFIX_DATE = new Prefix("d/"); + public static final Prefix PREFIX_START = new Prefix("s/"); + + /** + * Parses the given {@code String} of arguments in the context of the {@code DeleteAppCommand} + * and returns a {@code DeleteAppCommand} object for execution. + * + * @throws ParseException if the user input does not conform to the expected format. + */ + public DeleteAppCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_DATE, PREFIX_START); + + // check if required prefixes are present + boolean prefixesPresent = arePrefixesPresent(argMultimap, PREFIX_DATE, PREFIX_START); + boolean preamblePresent = argMultimap.getPreamble().isEmpty(); + if (!prefixesPresent || !preamblePresent) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteAppCommand.MESSAGE_USAGE)); + } + + LocalDate date = ParserUtil.parseDate(argMultimap.getValue(PREFIX_DATE).get().trim()); + LocalTime start = ParserUtil.parseTime(argMultimap.getValue(PREFIX_START).get().trim()); + + return new DeleteAppCommand(date, start); + } + + /** + * 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/quickdocs/logic/parser/DeletePatientParser.java b/src/main/java/quickdocs/logic/parser/DeletePatientParser.java new file mode 100644 index 000000000000..7b6d1c70ea1e --- /dev/null +++ b/src/main/java/quickdocs/logic/parser/DeletePatientParser.java @@ -0,0 +1,44 @@ +package quickdocs.logic.parser; + +import java.util.stream.Stream; + +import quickdocs.logic.commands.DeletePatientCommand; +import quickdocs.logic.parser.exceptions.ParseException; +import quickdocs.model.patient.Nric; + +/** + * Parses NRIC and command into a delete patient command to be executed + */ +public class DeletePatientParser implements Parser { + + + public static final Prefix PREFIX_NRIC = new Prefix("r/"); + public static final String INVALID_DELETE_ARGUMENT = "Invalid input parameters entered.\n" + + DeletePatientCommand.MESSAGE_USAGE; + + + @Override + public DeletePatientCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NRIC); + + boolean prefixesPresent = arePrefixesPresent(argMultimap, + PREFIX_NRIC); + + if (!prefixesPresent) { + throw new ParseException(INVALID_DELETE_ARGUMENT); + } + + Nric toDelete = new Nric(argMultimap.getValue(PREFIX_NRIC).get()); + + return new DeletePatientCommand(toDelete); + } + + /** + * 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).anyMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } +} diff --git a/src/main/java/quickdocs/logic/parser/DeleteRemCommandParser.java b/src/main/java/quickdocs/logic/parser/DeleteRemCommandParser.java new file mode 100644 index 000000000000..07434cd59389 --- /dev/null +++ b/src/main/java/quickdocs/logic/parser/DeleteRemCommandParser.java @@ -0,0 +1,29 @@ +package quickdocs.logic.parser; + +import static quickdocs.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import quickdocs.commons.core.index.Index; +import quickdocs.logic.commands.DeleteRemCommand; +import quickdocs.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new {@code DeleteRemCommand} object. + */ +public class DeleteRemCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the {@code DeleteRemCommand} + * and returns a {@code DeleteRemCommand} object for execution. + * + * @throws ParseException if the user input does not conform to the expected format. + */ + public DeleteRemCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new DeleteRemCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteRemCommand.MESSAGE_USAGE), pe); + } + } +} diff --git a/src/main/java/quickdocs/logic/parser/DiagnosePatientCommandParser.java b/src/main/java/quickdocs/logic/parser/DiagnosePatientCommandParser.java new file mode 100644 index 000000000000..0de163f81a10 --- /dev/null +++ b/src/main/java/quickdocs/logic/parser/DiagnosePatientCommandParser.java @@ -0,0 +1,60 @@ +package quickdocs.logic.parser; + +import static quickdocs.logic.commands.DiagnosePatientCommand.MESSAGE_USAGE; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.stream.Stream; + +import quickdocs.logic.commands.DiagnosePatientCommand; +import quickdocs.logic.parser.exceptions.ParseException; +import quickdocs.model.consultation.Assessment; +import quickdocs.model.consultation.Diagnosis; +import quickdocs.model.consultation.Symptom; + +/** + * Parse symptoms and assessment added by user to create a diagnosis for the patient + */ +public class DiagnosePatientCommandParser implements Parser { + + public static final Prefix PREFIX_ASSESSMENT = new Prefix("a/"); + public static final Prefix PREFIX_SYMPTOM = new Prefix("s/"); + public static final String INVALID_ARGUMENTS_DIAGNOSIS = "Invalid arguments added for diagnosis.\n" + + MESSAGE_USAGE; + + @Override + public DiagnosePatientCommand parse(String args) throws ParseException { + + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_ASSESSMENT, PREFIX_SYMPTOM); + + if (!arePrefixesPresent(argMultimap, PREFIX_ASSESSMENT, PREFIX_SYMPTOM)) { + throw new ParseException(INVALID_ARGUMENTS_DIAGNOSIS); + } + + Assessment assessment = new Assessment(argMultimap.getValue(PREFIX_ASSESSMENT).get()); + ArrayList symptoms = parseSymptoms(argMultimap.getAllValues(PREFIX_SYMPTOM)); + + return new DiagnosePatientCommand(new Diagnosis(assessment, symptoms)); + } + + // parsing methods + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } + + /** + * Consolidates the symptom strings from the argMultimap into a list of + * Symptom objects + * + * @param symptoms list of symptoms recorded by doctor as String objects + * @return ArrayList of Symptom objects + */ + public static ArrayList parseSymptoms(Collection symptoms) { + final ArrayList symptomList = new ArrayList<>(); + for (String symptom : symptoms) { + symptomList.add(new Symptom(symptom)); + } + return symptomList; + } +} diff --git a/src/main/java/quickdocs/logic/parser/EditPatientParser.java b/src/main/java/quickdocs/logic/parser/EditPatientParser.java new file mode 100644 index 000000000000..8bb7f85a554c --- /dev/null +++ b/src/main/java/quickdocs/logic/parser/EditPatientParser.java @@ -0,0 +1,126 @@ +package quickdocs.logic.parser; + +import java.util.ArrayList; +import java.util.Collection; + +import quickdocs.logic.commands.EditPatientCommand; +import quickdocs.logic.parser.exceptions.ParseException; +import quickdocs.model.patient.Address; +import quickdocs.model.patient.Contact; +import quickdocs.model.patient.Dob; +import quickdocs.model.patient.Email; +import quickdocs.model.patient.Gender; +import quickdocs.model.patient.Name; +import quickdocs.model.patient.Nric; +import quickdocs.model.patient.PatientEditedFields; +import quickdocs.model.tag.Tag; + +/** + * parse arguments to create EditPatientCommand for logic to execute + */ +public class EditPatientParser implements Parser { + + // use preamble as the original nric to edit + public static final Prefix PREFIX_NAME = new Prefix("n/"); + public static final Prefix PREFIX_NRIC = new Prefix("r/"); + public static final Prefix PREFIX_DOB = new Prefix("d/"); + public static final Prefix PREFIX_ADDRESS = new Prefix("a/"); + public static final Prefix PREFIX_EMAIL = new Prefix("e/"); + public static final Prefix PREFIX_CONTACT = new Prefix("c/"); + public static final Prefix PREFIX_GENDER = new Prefix("g/"); + public static final Prefix PREFIX_TAG = new Prefix("t/"); + + public static final String INVALID_EDIT_ARGUMENTS = "Invalid input parameters entered.\n" + + EditPatientCommand.MESSAGE_USAGE; + public static final String NO_EDIT_PARAMETERS = "Nothing to edit\n"; + + @Override + public EditPatientCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_NRIC, PREFIX_DOB, PREFIX_ADDRESS, PREFIX_EMAIL, + PREFIX_CONTACT, PREFIX_GENDER, PREFIX_TAG); + + if (argMultimap.getPreamble().isEmpty()) { + throw new ParseException(INVALID_EDIT_ARGUMENTS); + } + + Nric nric = new Nric(argMultimap.getPreamble()); + + PatientEditedFields editedFields = createEditedFields(argMultimap); + + if (editedFields.checkEmpty()) { + throw new ParseException(NO_EDIT_PARAMETERS); + } + + return new EditPatientCommand(nric, editedFields); + } + + + /** + * Create a PatientEditedFields object, using the arguments supplied in by the user. + * The PatientEditedFields will only contain the values that are to be changed on the + * original patient object + * + * @param argMultimap all the arguments supplied in by the user during the edit patient parsing + * @return A PatientEditedFields of the details to be edited on the current patient object + */ + public static PatientEditedFields createEditedFields(ArgumentMultimap argMultimap) { + + PatientEditedFields editedFields = new PatientEditedFields(); + + if (argMultimap.getValue(PREFIX_NAME).isPresent()) { + editedFields.setName(new Name(argMultimap.getValue(PREFIX_NAME).get())); + } + + if (argMultimap.getValue(PREFIX_NRIC).isPresent()) { + editedFields.setNric(new Nric(argMultimap.getValue(PREFIX_NRIC).get())); + } + + if (argMultimap.getValue(PREFIX_DOB).isPresent()) { + editedFields.setDob(new Dob(argMultimap.getValue(PREFIX_DOB).get())); + } + + if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { + editedFields.setAddress(new Address(argMultimap.getValue(PREFIX_ADDRESS).get())); + } + + if (argMultimap.getValue(PREFIX_EMAIL).isPresent()) { + editedFields.setEmail(new Email(argMultimap.getValue(PREFIX_EMAIL).get())); + } + + if (argMultimap.getValue(PREFIX_CONTACT).isPresent()) { + editedFields.setContact(new Contact(argMultimap.getValue(PREFIX_CONTACT).get())); + } + + if (argMultimap.getValue(PREFIX_GENDER).isPresent()) { + editedFields.setGender(new Gender(argMultimap.getValue(PREFIX_GENDER).get())); + } + + if (argMultimap.getValue(PREFIX_TAG).isPresent()) { + + ArrayList tagList = parseTags(argMultimap.getAllValues(PREFIX_TAG)); + editedFields.setTagList(tagList); + } + + return editedFields; + } + + /** + * parse the list of tags entered into a list of tags to create the patient record with + */ + public static ArrayList parseTags(Collection tags) { + final ArrayList tagList = new ArrayList<>(); + for (String tagName : tags) { + tagList.add(parseTag(tagName)); + } + return tagList; + } + + /** + * returns a tag created from the command argument + */ + public static Tag parseTag(String tag) { + String trimmedTag = tag.trim(); + return new Tag(trimmedTag); + } +} diff --git a/src/main/java/quickdocs/logic/parser/FreeAppCommandParser.java b/src/main/java/quickdocs/logic/parser/FreeAppCommandParser.java new file mode 100644 index 000000000000..4c3f39dd3959 --- /dev/null +++ b/src/main/java/quickdocs/logic/parser/FreeAppCommandParser.java @@ -0,0 +1,59 @@ +package quickdocs.logic.parser; + +import static quickdocs.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.time.LocalDate; +import java.util.List; +import java.util.stream.Stream; + +import quickdocs.logic.commands.FreeAppCommand; +import quickdocs.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new {@code FreeAppCommand} object. + */ +public class FreeAppCommandParser implements Parser { + public static final Prefix PREFIX_FORMAT = new Prefix("f/"); + public static final Prefix PREFIX_DATE = new Prefix("d/"); + + /** + * Parses the given {@code String} of arguments in the context of the {@code FreeAppCommand} + * and returns a {@code FreeAppCommand} object for execution. + * + * @throws ParseException if the user input does not conform to the expected format. + */ + public FreeAppCommand parse(String args) throws ParseException { + // use dates of next month as default search range if no arguments provided + if (args.isEmpty()) { + LocalDate nextMonthDate = LocalDate.now().plusMonths(1); + List dates = ParserUtil.parseFormatDate(ParserUtil.FORMAT_MONTH, nextMonthDate); + assert dates.size() == 2; + + return new FreeAppCommand(dates.get(0), dates.get(1)); + } + + // check if required prefixes are present + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_FORMAT, PREFIX_DATE); + boolean prefixesPresent = arePrefixesPresent(argMultimap, PREFIX_FORMAT, PREFIX_DATE); + boolean preamblePresent = argMultimap.getPreamble().isEmpty(); + if (!preamblePresent || !prefixesPresent) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, FreeAppCommand.MESSAGE_USAGE)); + } + + LocalDate date = ParserUtil.parseDate(argMultimap.getValue(PREFIX_DATE).get().trim()); + String format = argMultimap.getValue(PREFIX_FORMAT).get().trim(); + List dates = ParserUtil.parseFormatDate(format, date); + assert dates.size() == 2; + + return new FreeAppCommand(dates.get(0), dates.get(1)); + } + + /** + * 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/quickdocs/logic/parser/ListAppCommandParser.java b/src/main/java/quickdocs/logic/parser/ListAppCommandParser.java new file mode 100644 index 000000000000..8e7899744dc3 --- /dev/null +++ b/src/main/java/quickdocs/logic/parser/ListAppCommandParser.java @@ -0,0 +1,76 @@ +package quickdocs.logic.parser; + +import static quickdocs.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.time.LocalDate; +import java.util.List; +import java.util.stream.Stream; + +import quickdocs.logic.commands.ListAppCommand; +import quickdocs.logic.parser.exceptions.ParseException; +import quickdocs.model.patient.Nric; + +/** + * Parses input arguments and creates a new {@code ListAppCommand} object. + */ +public class ListAppCommandParser implements Parser { + public static final Prefix PREFIX_FORMAT = new Prefix("f/"); + public static final Prefix PREFIX_DATE = new Prefix("d/"); + public static final Prefix PREFIX_NRIC = new Prefix("r/"); + + /** + * Parses the given {@code String} of arguments in the context of the {@code ListAppCommand} + * and returns a {@code ListAppCommand} object for execution. + * + * @throws ParseException if the user input does not conform to the expected format. + */ + public ListAppCommand parse(String args) throws ParseException { + // use dates of current week as default search range if no arguments provided + if (args.isEmpty()) { + List dates = ParserUtil.parseFormatDate(ParserUtil.FORMAT_WEEK, LocalDate.now()); + assert dates.size() == 2; + + return new ListAppCommand(dates.get(0), dates.get(1)); + } + + // check if required prefixes are present + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_FORMAT, PREFIX_DATE, PREFIX_NRIC); + boolean listByDate = arePrefixesPresent(argMultimap, PREFIX_FORMAT, PREFIX_DATE); + boolean listByNric = arePrefixesPresent(argMultimap, PREFIX_NRIC); + boolean preamblePresent = argMultimap.getPreamble().isEmpty(); + // Wrong format for the following 3 cases: + // 1. preamble is present + // 2. both formats of listing, by date and by nric, is present + // 3. neither formats of listing, by date nor by nric, is present + if (!preamblePresent + || (listByDate && listByNric) + || (!listByDate && !listByNric)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ListAppCommand.MESSAGE_USAGE)); + } + + // List appointments by date + if (listByDate) { + LocalDate date = ParserUtil.parseDate(argMultimap.getValue(PREFIX_DATE).get().trim()); + String format = argMultimap.getValue(PREFIX_FORMAT).get().trim(); + List dates = ParserUtil.parseFormatDate(format, date); + assert dates.size() == 2; + + return new ListAppCommand(dates.get(0), dates.get(1)); + } + + // List appointments by patient's nric + assert listByNric; + + Nric nric = new Nric(argMultimap.getValue(PREFIX_NRIC).get().trim()); + return new ListAppCommand(nric); + } + + /** + * 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/quickdocs/logic/parser/ListConsultationCommandParser.java b/src/main/java/quickdocs/logic/parser/ListConsultationCommandParser.java new file mode 100644 index 000000000000..49c27b47dc18 --- /dev/null +++ b/src/main/java/quickdocs/logic/parser/ListConsultationCommandParser.java @@ -0,0 +1,41 @@ +package quickdocs.logic.parser; + +//import java.util.stream.Stream; + +import quickdocs.logic.commands.ListConsultationCommand; +import quickdocs.logic.parser.exceptions.ParseException; + +/** + * Parses argument to produce a ListConsultationCommand + */ +public class ListConsultationCommandParser implements Parser { + + public static final String NO_LIST_ARGUMENTS = "Search parameters are missing for the listing of consultation\n" + + ListConsultationCommand.MESSAGE_USAGE; + public static final String INVALID_INDEX = "Index should be numeric"; + public static final Prefix PREFIX_NRIC = new Prefix("r/"); + + @Override + public ListConsultationCommand parse(String args) throws ParseException { + + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NRIC); + + if (argMultimap.getPreamble().isEmpty() && !argMultimap.getValue(PREFIX_NRIC).isPresent()) { + throw new ParseException(NO_LIST_ARGUMENTS); + } + + if (!argMultimap.getPreamble().isEmpty()) { + if (!argMultimap.getPreamble().trim().matches("\\d+")) { + throw new ParseException(INVALID_INDEX); + } + + int index = Integer.valueOf(argMultimap.getPreamble()); + return new ListConsultationCommand(index); + } + + String nric = argMultimap.getValue(PREFIX_NRIC).get(); + return new ListConsultationCommand(nric); + + } +} diff --git a/src/main/java/quickdocs/logic/parser/ListPatientParser.java b/src/main/java/quickdocs/logic/parser/ListPatientParser.java new file mode 100644 index 000000000000..a788c83ffefd --- /dev/null +++ b/src/main/java/quickdocs/logic/parser/ListPatientParser.java @@ -0,0 +1,65 @@ +package quickdocs.logic.parser; + +import quickdocs.logic.commands.ListPatientCommand; +import quickdocs.logic.parser.exceptions.ParseException; +import quickdocs.model.tag.Tag; + +/** + * Parses arguments entered by user into a ListPatientCommand to list + * single or a list of patient records. + * If multiple arguments are entered, only first argument will be used to list + */ +public class ListPatientParser implements Parser { + + public static final String INDEX_INVALID_RANGE = "Index is beyond the valid range"; + public static final String INDEX_NUMERIC = "Index should be numeric"; + public static final Prefix PREFIX_NAME = new Prefix("n/"); + public static final Prefix PREFIX_NRIC = new Prefix("r/"); + public static final Prefix PREFIX_TAG = new Prefix("t/"); + + @Override + public ListPatientCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_NRIC, PREFIX_TAG); + + if (!argMultimap.getPreamble().isEmpty()) { + + String userInput = argMultimap.getPreamble().trim(); + + // prevent non integer input for indexes, also prevent negative values + if (!userInput.matches("\\d+")) { + throw new ParseException(INDEX_NUMERIC); + } + + // prevent integer overflow, can only check up to Long's max value + // any value beyond that will have a NumberFormatException thrown when + // assigning value to a long variable + if (Long.valueOf(userInput) > Integer.MAX_VALUE) { + throw new ParseException(INDEX_INVALID_RANGE); + } + + int index = Integer.valueOf(argMultimap.getPreamble()); + return new ListPatientCommand(index); + } + + if (argMultimap.getValue(PREFIX_NAME).isPresent()) { + // list patient by name, can get multiple records + String name = argMultimap.getValue(PREFIX_NAME).get(); + return new ListPatientCommand(name, true); + } + + if (argMultimap.getValue(PREFIX_NRIC).isPresent()) { + // list patient by name, can get multiple records + String nric = argMultimap.getValue(PREFIX_NRIC).get(); + return new ListPatientCommand(nric, false); + } + + if (argMultimap.getValue(PREFIX_TAG).isPresent()) { + // list all patients with the same tag + return new ListPatientCommand(new Tag(argMultimap.getValue(PREFIX_TAG).get())); + } + + // if nothing is supplied, will try to list all the patients, maximum 50; + return new ListPatientCommand(); + } +} diff --git a/src/main/java/quickdocs/logic/parser/ListRemCommandParser.java b/src/main/java/quickdocs/logic/parser/ListRemCommandParser.java new file mode 100644 index 000000000000..be1257845a9f --- /dev/null +++ b/src/main/java/quickdocs/logic/parser/ListRemCommandParser.java @@ -0,0 +1,80 @@ +package quickdocs.logic.parser; + +import static quickdocs.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.time.LocalDate; +import java.util.List; +import java.util.stream.Stream; + +import quickdocs.commons.core.index.Index; +import quickdocs.logic.commands.ListRemCommand; +import quickdocs.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new {@code ListRemCommand} object. + */ +public class ListRemCommandParser implements Parser { + public static final Prefix PREFIX_FORMAT = new Prefix("f/"); + public static final Prefix PREFIX_DATE = new Prefix("d/"); + public static final Prefix PREFIX_INDEX = new Prefix("i/"); + + /** + * Parses the given {@code String} of arguments in the context of the {@code ListRemCommand} + * and returns a {@code ListRemCommand} object for execution. + * + * @throws ParseException if the user input does not conform to the expected format. + */ + public ListRemCommand parse(String args) throws ParseException { + // use dates of current week as default search range if no arguments provided + if (args.isEmpty()) { + List dates = ParserUtil.parseFormatDate(ParserUtil.FORMAT_WEEK, LocalDate.now()); + assert dates.size() == 2; + + return new ListRemCommand(dates.get(0), dates.get(1)); + } + + // check if required prefixes are present + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_FORMAT, PREFIX_DATE, PREFIX_INDEX); + boolean listRemindersInRange = arePrefixesPresent(argMultimap, PREFIX_FORMAT, PREFIX_DATE); + boolean listSingleReminder = arePrefixesPresent(argMultimap, PREFIX_INDEX); + boolean preamblePresent = argMultimap.getPreamble().isEmpty(); + // Wrong format for the following 3 cases: + // 1. preamble is present + // 2. both formats of listing, by range of dates and by index, is present + // 3. neither formats of listing, by range of dates nor by index, is present + if (!preamblePresent + || (listRemindersInRange && listSingleReminder) + || (!listRemindersInRange && !listSingleReminder)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ListRemCommand.MESSAGE_USAGE)); + } + + // List a single reminder + if (listSingleReminder) { + try { + Index index = ParserUtil.parseIndex(argMultimap.getValue(PREFIX_INDEX).get().trim()); + return new ListRemCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ListRemCommand.MESSAGE_USAGE), pe); + } + } + + // List reminders in a range of dates + assert listRemindersInRange; + + LocalDate date = ParserUtil.parseDate(argMultimap.getValue(PREFIX_DATE).get().trim()); + String format = argMultimap.getValue(PREFIX_FORMAT).get().trim(); + List dates = ParserUtil.parseFormatDate(format, date); + assert dates.size() == 2; + + return new ListRemCommand(dates.get(0), dates.get(1)); + } + + /** + * 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/Parser.java b/src/main/java/quickdocs/logic/parser/Parser.java similarity index 72% rename from src/main/java/seedu/address/logic/parser/Parser.java rename to src/main/java/quickdocs/logic/parser/Parser.java index d6551ad8e3ff..f7368172f91b 100644 --- a/src/main/java/seedu/address/logic/parser/Parser.java +++ b/src/main/java/quickdocs/logic/parser/Parser.java @@ -1,7 +1,7 @@ -package seedu.address.logic.parser; +package quickdocs.logic.parser; -import seedu.address.logic.commands.Command; -import seedu.address.logic.parser.exceptions.ParseException; +import quickdocs.logic.commands.Command; +import quickdocs.logic.parser.exceptions.ParseException; /** * Represents a Parser that is able to parse user input into a {@code Command} of type {@code T}. diff --git a/src/main/java/quickdocs/logic/parser/ParserUtil.java b/src/main/java/quickdocs/logic/parser/ParserUtil.java new file mode 100644 index 000000000000..3885277293a5 --- /dev/null +++ b/src/main/java/quickdocs/logic/parser/ParserUtil.java @@ -0,0 +1,217 @@ +package quickdocs.logic.parser; + +import static java.time.DayOfWeek.MONDAY; +import static java.time.DayOfWeek.SUNDAY; +import static java.time.format.DateTimeFormatter.ISO_LOCAL_DATE; +import static java.time.temporal.TemporalAdjusters.firstDayOfMonth; +import static java.time.temporal.TemporalAdjusters.lastDayOfMonth; +import static java.time.temporal.TemporalAdjusters.nextOrSame; +import static java.time.temporal.TemporalAdjusters.previousOrSame; +import static java.util.Objects.requireNonNull; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import quickdocs.commons.core.index.Index; +import quickdocs.commons.util.StringUtil; +import quickdocs.logic.parser.exceptions.ParseException; +import quickdocs.model.patient.Address; +import quickdocs.model.patient.Contact; +import quickdocs.model.patient.Email; +import quickdocs.model.patient.Name; +import quickdocs.model.tag.Tag; + +/** + * Contains utility methods used for parsing strings in the various *Parser classes. + */ +public class ParserUtil { + + public static final String FORMAT_DAY = "day"; + public static final String FORMAT_WEEK = "week"; + public static final String FORMAT_MONTH = "month"; + public static final String[] FORMATS = {FORMAT_DAY, FORMAT_WEEK, FORMAT_MONTH}; + public static final String MESSAGE_INVALID_INDEX = "Index is not a non-zero unsigned integer."; + public static final String MESSAGE_INVALID_DATE = "Wrong format for date: YYYY-MM-DD"; + public static final String MESSAGE_INVALID_TIME = "Wrong format for time: HH:MM" + + "HH: from 00 to 23\n" + + "MM: from 00 to 59"; + public static final String MESSAGE_INVALID_FORMAT = "Invalid keywords for format.\n" + + "Valid formats are " + + FORMAT_DAY + ", " + + FORMAT_WEEK + ", " + + FORMAT_MONTH; + + + /** + * Parses {@code oneBasedIndex} into an {@code Index} and returns it. Leading and trailing whitespaces will be + * trimmed. + * @throws ParseException if the specified index is invalid (not non-zero unsigned integer). + */ + public static Index parseIndex(String oneBasedIndex) throws ParseException { + String trimmedIndex = oneBasedIndex.trim(); + if (!StringUtil.isNonZeroUnsignedInteger(trimmedIndex)) { + throw new ParseException(MESSAGE_INVALID_INDEX); + } + return Index.fromOneBased(Integer.parseInt(trimmedIndex)); + } + + /** + * Parses a {@code String name} into a {@code Name}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code name} is invalid. + */ + public static Name parseName(String name) throws ParseException { + requireNonNull(name); + String trimmedName = name.trim(); + if (!Name.isValidName(trimmedName)) { + throw new ParseException(Name.NAME_CONSTRAINTS); + } + return new Name(trimmedName); + } + + /** + * Parses a {@code String phone} into a {@code Phone}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code phone} is invalid. + */ + public static Contact parseContact(String phone) throws ParseException { + requireNonNull(phone); + String trimmedPhone = phone.trim(); + if (!Contact.isValidContact(trimmedPhone)) { + throw new ParseException(Contact.CONTACT_CONSTRAINTS); + } + return new Contact(trimmedPhone); + } + + /** + * Parses a {@code String address} into an {@code Address}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code address} is invalid. + */ + public static Address parseAddress(String address) throws ParseException { + requireNonNull(address); + String trimmedAddress = address.trim(); + if (!Address.isValidAddress(trimmedAddress)) { + throw new ParseException(Address.ADDRESS_CONSTRAINTS); + } + return new Address(trimmedAddress); + } + + /** + * Parses a {@code String email} into an {@code Email}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code email} is invalid. + */ + public static Email parseEmail(String email) throws ParseException { + requireNonNull(email); + String trimmedEmail = email.trim(); + if (!Email.isValidEmail(trimmedEmail)) { + throw new ParseException(Email.EMAIL_CONSTRAINTS); + } + return new Email(trimmedEmail); + } + + /** + * Parses a {@code String tag} into a {@code Tag}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code tag} is invalid. + */ + public static Tag parseTag(String tag) throws ParseException { + requireNonNull(tag); + String trimmedTag = tag.trim(); + if (!Tag.isValidTagName(trimmedTag)) { + throw new ParseException(Tag.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)); + } + return tagSet; + } + + /** + * Parses {@code String date} into a {@code LocalDate}. + */ + public static LocalDate parseDate(String date) throws ParseException { + requireNonNull(date); + try { + return LocalDate.parse(date, ISO_LOCAL_DATE); + } catch (DateTimeParseException e) { + throw new ParseException(MESSAGE_INVALID_DATE); + } + } + + /** + * Parses {@code String time} into a {@code LocalTime}. + */ + public static LocalTime parseTime(String time) throws ParseException { + requireNonNull(time); + try { + return LocalTime.parse(time); + } catch (DateTimeParseException e) { + throw new ParseException(MESSAGE_INVALID_TIME); + } + } + + /** + * Parses a {@code LocalDate date} and {@code String format} to the respective start and end dates. + * + * @param format the {@code String format} given to parse the date. + * @param date the {@code LocalDate date} given to be parsed. + * @return {@code List} of {@code LocalDate dates}, with the first element being the start date and + * the second element being the end date. + * @throws ParseException if the given {@code String format} is invalid. + */ + public static List parseFormatDate(String format, LocalDate date) throws ParseException { + // check if the format given is valid + if (Arrays.stream(FORMATS).noneMatch(format::equalsIgnoreCase)) { + throw new ParseException(MESSAGE_INVALID_FORMAT); + } + + LocalDate start; + LocalDate end; + switch (format) { + case FORMAT_DAY: + start = date; + end = date; + break; + case FORMAT_WEEK: + start = date.with(previousOrSame(MONDAY)); + end = date.with(nextOrSame(SUNDAY)); + break; + case FORMAT_MONTH: + start = date.with(firstDayOfMonth()); + end = date.with(lastDayOfMonth()); + break; + default: + start = end = date; + } + + List dates = new ArrayList<>(); + dates.add(start); + dates.add(end); + assert dates.size() == 2; + + return dates; + } +} diff --git a/src/main/java/seedu/address/logic/parser/Prefix.java b/src/main/java/quickdocs/logic/parser/Prefix.java similarity index 95% rename from src/main/java/seedu/address/logic/parser/Prefix.java rename to src/main/java/quickdocs/logic/parser/Prefix.java index c859d5fa5db1..763719246113 100644 --- a/src/main/java/seedu/address/logic/parser/Prefix.java +++ b/src/main/java/quickdocs/logic/parser/Prefix.java @@ -1,4 +1,4 @@ -package seedu.address.logic.parser; +package quickdocs.logic.parser; /** * A prefix that marks the beginning of an argument in an arguments string. diff --git a/src/main/java/quickdocs/logic/parser/PrescriptionCommandParser.java b/src/main/java/quickdocs/logic/parser/PrescriptionCommandParser.java new file mode 100644 index 000000000000..a917f1633e12 --- /dev/null +++ b/src/main/java/quickdocs/logic/parser/PrescriptionCommandParser.java @@ -0,0 +1,55 @@ +package quickdocs.logic.parser; + +import static quickdocs.logic.commands.PrescriptionCommand.MESSAGE_USAGE; + +import java.util.ArrayList; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import quickdocs.logic.commands.PrescriptionCommand; +import quickdocs.logic.parser.exceptions.ParseException; + +/** + * parse arguments from user entered command to prescribe a list of medicine + * to tackle the patient's current symptoms and illness + */ +public class PrescriptionCommandParser implements Parser { + + public static final Prefix PREFIX_MEDICINE = new Prefix("m/"); + public static final Prefix PREFIX_QUANTITY = new Prefix("q/"); + + public static final String INVALID_ARGUMENTS_PRESCRIPTION = + "Invalid arguments entered\n" + MESSAGE_USAGE; + public static final String INSUFFICIENT_QUANTITIES = "Some medicine do not have assigned quantity.\n"; + public static final String EXTRA_QUANTITIES = "Additional quantities specified.\n"; + + @Override + public PrescriptionCommand parse(String args) throws ParseException { + + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_MEDICINE, PREFIX_QUANTITY); + + if (!arePrefixesPresent(argMultimap, PREFIX_MEDICINE, PREFIX_QUANTITY)) { + throw new ParseException(INVALID_ARGUMENTS_PRESCRIPTION); + } + + ArrayList medList = (ArrayList) argMultimap.getAllValues(PREFIX_MEDICINE); + ArrayList qtyList = (ArrayList) argMultimap.getAllValues(PREFIX_QUANTITY).stream() + .map(Integer::valueOf).collect(Collectors.toList()); + + if (medList.size() > qtyList.size()) { + throw new ParseException(INSUFFICIENT_QUANTITIES); + } + + if (medList.size() < qtyList.size()) { + throw new ParseException(EXTRA_QUANTITIES); + } + + return new PrescriptionCommand(medList, qtyList); + } + + // parsing methods + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } +} diff --git a/src/main/java/quickdocs/logic/parser/PurchaseMedicineCommandParser.java b/src/main/java/quickdocs/logic/parser/PurchaseMedicineCommandParser.java new file mode 100644 index 000000000000..680380f3875c --- /dev/null +++ b/src/main/java/quickdocs/logic/parser/PurchaseMedicineCommandParser.java @@ -0,0 +1,68 @@ +package quickdocs.logic.parser; + +import static quickdocs.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.math.BigDecimal; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import quickdocs.logic.commands.PurchaseMedicineCommand; +import quickdocs.logic.commands.PurchaseMedicineViaPathCommand; +import quickdocs.logic.commands.PurchaseMedicineWoPathCommand; +import quickdocs.logic.parser.exceptions.ParseException; + +/** + * Parse input arguments and returns a PurchaseMedicineCommand if input is in correct format + */ +public class PurchaseMedicineCommandParser implements Parser { + + private static final Pattern PurchaseMedicineViaPathCommand_Argument_Format = + Pattern.compile("(?\\S+)(?:\\s+)(?\\d.+)"); + private static final Pattern PurchaseMedicineWOPathCommand_Argument_Format = + Pattern.compile("(?[^(\\s)(\\\\)]+)(?:\\s+)(?\\d.+)"); + private static final Pattern PurchaseInformation_Format = + Pattern.compile("(?\\d+)(?:\\s+)(?\\d+\\.?\\d*)"); + + /** + * Parse the input string to form a PurchaseMedicineCommand + * @param args the input + * @return A PurchaseMedicineCommand corresponding to the input + * @throws ParseException if the user input does not conform to the format + */ + public PurchaseMedicineCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, PurchaseMedicineCommand.MESSAGE_USAGE)); + } + final Matcher medicinePurchaseViaPath = PurchaseMedicineViaPathCommand_Argument_Format.matcher(trimmedArgs); + final Matcher medicinePurchaseWoPath = PurchaseMedicineWOPathCommand_Argument_Format.matcher(trimmedArgs); + if (medicinePurchaseWoPath.matches()) { + final String medicineName = medicinePurchaseWoPath.group("medicineName"); + final String purchaseInfo = medicinePurchaseWoPath.group("purchaseInformation"); + final Matcher quantityCost = PurchaseInformation_Format.matcher(purchaseInfo); + if (!quantityCost.matches()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, PurchaseMedicineCommand.MESSAGE_USAGE)); + } + String quantity = quantityCost.group("quantity"); + String cost = quantityCost.group("cost"); + return new PurchaseMedicineWoPathCommand(medicineName, Integer.parseInt(quantity), new BigDecimal(cost)); + } else if (medicinePurchaseViaPath.matches()) { + final String rawPath = medicinePurchaseViaPath.group("rawPath"); + final String purchaseInfo = medicinePurchaseViaPath.group("purchaseInformation"); + String[] path = rawPath.split("\\\\"); + final Matcher quantityCost = PurchaseInformation_Format.matcher(purchaseInfo); + if (!quantityCost.matches()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, PurchaseMedicineCommand.MESSAGE_USAGE)); + } + String quantity = quantityCost.group("quantity"); + String cost = quantityCost.group("cost"); + return new PurchaseMedicineViaPathCommand(path, Integer.parseInt(quantity), new BigDecimal(cost)); + } else { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, PurchaseMedicineCommand.MESSAGE_USAGE)); + } + } +} diff --git a/src/main/java/quickdocs/logic/parser/QuickDocsParser.java b/src/main/java/quickdocs/logic/parser/QuickDocsParser.java new file mode 100644 index 000000000000..482ceab2337e --- /dev/null +++ b/src/main/java/quickdocs/logic/parser/QuickDocsParser.java @@ -0,0 +1,250 @@ +package quickdocs.logic.parser; + +import static quickdocs.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static quickdocs.commons.core.Messages.MESSAGE_UNKNOWN_COMMAND; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import quickdocs.logic.commands.AbortConsultationCommand; +import quickdocs.logic.commands.AddAppCommand; +import quickdocs.logic.commands.AddDirectoryCommand; +import quickdocs.logic.commands.AddMedicineCommand; +import quickdocs.logic.commands.AddPatientCommand; +import quickdocs.logic.commands.AddRemCommand; +import quickdocs.logic.commands.AlarmCommand; +import quickdocs.logic.commands.Command; +import quickdocs.logic.commands.ConsultationCommand; +import quickdocs.logic.commands.DeleteAppCommand; +import quickdocs.logic.commands.DeletePatientCommand; +import quickdocs.logic.commands.DeleteRemCommand; +import quickdocs.logic.commands.DiagnosePatientCommand; +import quickdocs.logic.commands.EditPatientCommand; +import quickdocs.logic.commands.EndConsultationCommand; +import quickdocs.logic.commands.ExitCommand; +import quickdocs.logic.commands.FreeAppCommand; +import quickdocs.logic.commands.HelpCommand; +import quickdocs.logic.commands.HistoryCommand; +import quickdocs.logic.commands.ListAppCommand; +import quickdocs.logic.commands.ListConsultationCommand; +import quickdocs.logic.commands.ListPatientCommand; +import quickdocs.logic.commands.ListRemCommand; +import quickdocs.logic.commands.PrescriptionCommand; +import quickdocs.logic.commands.PurchaseMedicineCommand; +import quickdocs.logic.commands.SetConsultationFeeCommand; +import quickdocs.logic.commands.SetPriceCommand; +import quickdocs.logic.commands.StatisticsCommand; +import quickdocs.logic.commands.ViewStorageCommand; +import quickdocs.logic.parser.exceptions.ParseException; + +/** + * Parses user input. + */ +public class QuickDocsParser { + + /** + * Used for initial separation of command word and args. + */ + public static final Pattern BASIC_COMMAND_FORMAT = Pattern.compile("(?\\S+)(?.*)"); + + /** + * Parses user input into command for execution. + * + * @param userInput full user input string + * @return the command based on the user input + * @throws ParseException if the user input does not conform the expected format + */ + public Command parseCommand(String userInput) throws ParseException { + final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim()); + if (!matcher.matches()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE)); + } + + final String commandWord = matcher.group("commandWord"); + final String arguments = matcher.group("arguments"); + switch (commandWord) { + + case HistoryCommand.COMMAND_WORD: + return new HistoryCommand(); + + case ExitCommand.COMMAND_WORD: + return new ExitCommand(); + + case HelpCommand.COMMAND_WORD: + return new HelpCommand(); + + case AddPatientCommand.COMMAND_WORD: + case AddPatientCommand.COMMAND_ALIAS: + return new AddPatientParser().parse(arguments); + + case AddAppCommand.COMMAND_WORD: + case AddAppCommand.COMMAND_ALIAS: + return new AddAppCommandParser().parse(arguments); + + case DeleteAppCommand.COMMAND_WORD: + case DeleteAppCommand.COMMAND_ALIAS: + return new DeleteAppCommandParser().parse(arguments); + + case ListAppCommand.COMMAND_WORD: + case ListAppCommand.COMMAND_ALIAS: + return new ListAppCommandParser().parse(arguments); + + case FreeAppCommand.COMMAND_WORD: + case FreeAppCommand.COMMAND_ALIAS: + return new FreeAppCommandParser().parse(arguments); + + case AddRemCommand.COMMAND_WORD: + case AddRemCommand.COMMAND_ALIAS: + return new AddRemCommandParser().parse(arguments); + + case DeleteRemCommand.COMMAND_WORD: + case DeleteRemCommand.COMMAND_ALIAS: + return new DeleteRemCommandParser().parse(arguments); + + case ListRemCommand.COMMAND_WORD: + case ListRemCommand.COMMAND_ALIAS: + return new ListRemCommandParser().parse(arguments); + + case EditPatientCommand.COMMAND_WORD: + case EditPatientCommand.COMMAND_ALIAS: + return new EditPatientParser().parse(arguments); + + case ListPatientCommand.COMMAND_WORD: + case ListPatientCommand.COMMAND_ALIAS: + return new ListPatientParser().parse(arguments); + + case ConsultationCommand.COMMAND_WORD: + case ConsultationCommand.COMMAND_ALIAS: + return new ConsultationCommandParser().parse(arguments); + + case DiagnosePatientCommand.COMMAND_WORD: + case DiagnosePatientCommand.COMMAND_ALIAS: + return new DiagnosePatientCommandParser().parse(arguments); + + case StatisticsCommand.COMMAND_WORD: + case StatisticsCommand.COMMAND_ALIAS: + return new StatisticsCommandParser().parse(arguments); + + case SetConsultationFeeCommand.COMMAND_WORD: + case SetConsultationFeeCommand.COMMAND_ALIAS: + return new SetConsultationFeeCommandParser().parse(arguments); + + case PrescriptionCommand.COMMAND_WORD: + case PrescriptionCommand.COMMAND_ALIAS: + return new PrescriptionCommandParser().parse(arguments); + + case EndConsultationCommand.COMMAND_WORD: + case EndConsultationCommand.COMMAND_ALIAS: + return new EndConsultationCommand(); + + case ListConsultationCommand.COMMAND_WORD: + case ListConsultationCommand.COMMAND_ALIAS: + return new ListConsultationCommandParser().parse(arguments); + + case AddMedicineCommand.COMMAND_WORD: + case AddMedicineCommand.COMMAND_ALIAS: + return new AddMedicineCommandParser().parse(arguments); + + case ViewStorageCommand.COMMAND_WORD: + case ViewStorageCommand.COMMAND_ALIAS: + return new ViewStorageCommandParser().parse(arguments); + + case AlarmCommand.COMMAND_WORD: + return new AlarmCommandParser().parse(arguments); + + case AddDirectoryCommand.COMMAND_WORD: + case AddDirectoryCommand.COMMAND_ALIAS: + return new AddDirectoryCommandParser().parse(arguments); + + case PurchaseMedicineCommand.COMMAND_WORD: + case PurchaseMedicineCommand.COMMAND_ALIAS: + return new PurchaseMedicineCommandParser().parse(arguments); + + case SetPriceCommand.COMMAND_WORD: + case SetPriceCommand.COMMAND_ALIAS: + return new SetPriceCommandParser().parse(arguments); + + case DeletePatientCommand.COMMAND_WORD: + case DeletePatientCommand.COMMAND_ALIAS: + return new DeletePatientParser().parse(arguments); + + case AbortConsultationCommand.COMMAND_WORD: + case AbortConsultationCommand.COMMAND_ALIAS: + return new AbortConsultationCommand(); + + default: + throw new ParseException(MESSAGE_UNKNOWN_COMMAND); + } + } + + /** + * To judge whether suggestion mode should be turned on. + * @param rawArgs The user input + * @return whether suggestion mode should be turned on + */ + public boolean isDirectoryFormat(String rawArgs) { + Matcher matcher = BASIC_COMMAND_FORMAT.matcher(rawArgs); + if (!matcher.matches()) { + return false; + } + String commandWord = matcher.group("commandWord").trim(); + String arguments = matcher.group("arguments").trim(); + if (!arguments.contains("\\") || arguments.contains(" ")) { + return false; + } + switch (commandWord) { + case PurchaseMedicineCommand.COMMAND_WORD: + case PurchaseMedicineCommand.COMMAND_ALIAS: + case SetPriceCommand.COMMAND_WORD: + case SetPriceCommand.COMMAND_ALIAS: + case AlarmCommand.COMMAND_WORD: + case ViewStorageCommand.COMMAND_WORD: + case ViewStorageCommand.COMMAND_ALIAS: + case AddMedicineCommand.COMMAND_WORD: + case AddMedicineCommand.COMMAND_ALIAS: + case AddDirectoryCommand.COMMAND_WORD: + case AddDirectoryCommand.COMMAND_ALIAS: + return true; + default: + return false; + } + } + + /** + * To judge whether medicine is allowed as input for the command entered. + * @param rawArgs The user input + * @return whether suggestion mode should be turned on + */ + public boolean isMedicineAllowed(String rawArgs) { + Matcher matcher = BASIC_COMMAND_FORMAT.matcher(rawArgs); + if (!matcher.matches()) { + return false; + } + String commandWord = matcher.group("commandWord").trim(); + String arguments = matcher.group("arguments").trim(); + if (!arguments.contains("\\") || arguments.contains(" ")) { + return false; + } + switch (commandWord) { + case PurchaseMedicineCommand.COMMAND_WORD: + case PurchaseMedicineCommand.COMMAND_ALIAS: + case SetPriceCommand.COMMAND_WORD: + case SetPriceCommand.COMMAND_ALIAS: + case AlarmCommand.COMMAND_WORD: + case ViewStorageCommand.COMMAND_WORD: + case ViewStorageCommand.COMMAND_ALIAS: + return true; + default: + return false; + } + } + + public String getArgument(String rawArgs) { + rawArgs = rawArgs.trim(); + Matcher matcher = BASIC_COMMAND_FORMAT.matcher(rawArgs); + if (!matcher.matches()) { + throw new IllegalStateException(MESSAGE_INVALID_COMMAND_FORMAT); + } + return matcher.group("arguments").trim(); + } +} diff --git a/src/main/java/quickdocs/logic/parser/SetConsultationFeeCommandParser.java b/src/main/java/quickdocs/logic/parser/SetConsultationFeeCommandParser.java new file mode 100644 index 000000000000..ec6b5337f711 --- /dev/null +++ b/src/main/java/quickdocs/logic/parser/SetConsultationFeeCommandParser.java @@ -0,0 +1,39 @@ +package quickdocs.logic.parser; + +import static quickdocs.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.math.BigDecimal; + +import quickdocs.logic.commands.SetConsultationFeeCommand; +import quickdocs.logic.parser.exceptions.ParseException; + +/** + * Parses input argument and returns a SetConsultationFeeCommand + */ +public class SetConsultationFeeCommandParser implements Parser { + + private static final String REGEX = "^\\$?\\d+(\\.\\d{1,2})?$"; + /** + * Parses the given {@code String} of arguments in the context of the SetConsultationFeeCommand + * and returns a SetConsultationFeeCommand object for execution + * @throws ParseException if the user input does not conform the expected format + */ + public SetConsultationFeeCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + final String[] tokens = trimmedArgs.split("\\s+"); + // check that there is only 1 argument + if (tokens.length != 1) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SetConsultationFeeCommand.MESSAGE_USAGE)); + } + if (!tokens[0].matches(REGEX)) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SetConsultationFeeCommand.MESSAGE_USAGE)); + } + String token = tokens[0]; + if (token.charAt(0) == '$') { + token = token.substring(1); + } + return new SetConsultationFeeCommand(new BigDecimal(token)); + } +} diff --git a/src/main/java/quickdocs/logic/parser/SetPriceCommandParser.java b/src/main/java/quickdocs/logic/parser/SetPriceCommandParser.java new file mode 100644 index 000000000000..59107223bca3 --- /dev/null +++ b/src/main/java/quickdocs/logic/parser/SetPriceCommandParser.java @@ -0,0 +1,47 @@ +package quickdocs.logic.parser; + +import static quickdocs.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.math.BigDecimal; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import quickdocs.logic.commands.SetPriceCommand; +import quickdocs.logic.commands.SetPriceViaPathCommand; +import quickdocs.logic.commands.SetPriceWoPathCommand; +import quickdocs.logic.parser.exceptions.ParseException; + +/** + * A parser to parse user input to SetPriceCommand + */ +public class SetPriceCommandParser implements Parser { + + private static final Pattern SetPriceCommand_WoPathArgument_Format = + Pattern.compile("(?[^(\\s)(\\\\)]+)(?:\\s+)(?\\d+\\.*\\d*)"); + private static final Pattern SetPriceCommand_ViaPathArgument_Format = + Pattern.compile("(?\\S+)(?:\\s+)(?\\d+\\.*\\d*)"); + + @Override + public SetPriceCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SetPriceCommand.MESSAGE_USAGE)); + } + Matcher argumentViaPath = SetPriceCommand_ViaPathArgument_Format.matcher(trimmedArgs); + Matcher argumentWoPath = SetPriceCommand_WoPathArgument_Format.matcher(trimmedArgs); + if (argumentWoPath.matches()) { + String name = argumentWoPath.group("medicineName"); + String price = argumentWoPath.group("price"); + return new SetPriceWoPathCommand(name, new BigDecimal(price)); + } else if (argumentViaPath.matches()) { + String rawPath = argumentViaPath.group("rawPath"); + String price = argumentViaPath.group("price"); + String[] path = rawPath.split("\\\\"); + return new SetPriceViaPathCommand(path, new BigDecimal(price)); + } else { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SetPriceCommand.MESSAGE_USAGE)); + } + } +} diff --git a/src/main/java/quickdocs/logic/parser/StatisticsCommandParser.java b/src/main/java/quickdocs/logic/parser/StatisticsCommandParser.java new file mode 100644 index 000000000000..a29312807cac --- /dev/null +++ b/src/main/java/quickdocs/logic/parser/StatisticsCommandParser.java @@ -0,0 +1,59 @@ +package quickdocs.logic.parser; + +import static quickdocs.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.time.YearMonth; +import java.time.format.DateTimeFormatter; + +import quickdocs.logic.commands.StatisticsCommand; +import quickdocs.logic.parser.exceptions.ParseException; +import quickdocs.model.record.StatisticsManager; + +/** + * Parses input argument and returns a StatisticsCommand + */ +public class StatisticsCommandParser implements Parser { + + private static final String MMYYYY_REGEX = "^(0[1-9]|1[0-2])(\\d{4})$"; + private static final DateTimeFormatter MMYYYY_FORMATTER = DateTimeFormatter.ofPattern("MMyyyy"); + /** + * Parses the given {@code String} of arguments in the context of the StatisticsCommand + * and returns a StatisticsCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public StatisticsCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + final String[] tokens = trimmedArgs.split("\\s+"); + // check if there are at least 1 arguments + if (tokens.length < 1) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, StatisticsCommand.MESSAGE_USAGE)); + } + // check if the FROM MMYYYY is valid + if (!tokens[0].matches(MMYYYY_REGEX)) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, StatisticsCommand.MESSAGE_USAGE)); + } + YearMonth fromYearMonth = YearMonth.parse(tokens[0], MMYYYY_FORMATTER); + // check if the FROM MMYYYY is not before January 2019 + if (fromYearMonth.isBefore(StatisticsManager.START_DATE)) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, StatisticsCommand.MESSAGE_USAGE)); + } + YearMonth toYearMonth = fromYearMonth; + // if there is a TO MMYYYY, check if it is valid + if (tokens.length == 2) { + if (!tokens[1].matches(MMYYYY_REGEX)) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, StatisticsCommand.MESSAGE_USAGE)); + } + toYearMonth = YearMonth.parse(tokens[1], MMYYYY_FORMATTER); + } + // check if TO MMYYYY is not before FROM MMYYYY + if (toYearMonth.isBefore(fromYearMonth)) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, StatisticsCommand.MESSAGE_USAGE)); + } + return new StatisticsCommand(fromYearMonth, toYearMonth); + } +} diff --git a/src/main/java/quickdocs/logic/parser/ViewStorageCommandParser.java b/src/main/java/quickdocs/logic/parser/ViewStorageCommandParser.java new file mode 100644 index 000000000000..842e4303577f --- /dev/null +++ b/src/main/java/quickdocs/logic/parser/ViewStorageCommandParser.java @@ -0,0 +1,36 @@ +package quickdocs.logic.parser; + +import static quickdocs.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import quickdocs.logic.commands.ViewStorageCommand; +import quickdocs.logic.parser.exceptions.ParseException; + +/** + * A parser to return a ViewStorageCommand from user input + */ +public class ViewStorageCommandParser implements Parser { + + private static final Pattern ViewStorageCommand_Argument_Format = Pattern.compile("(?\\S+)"); + + /** + * A parser to return a ViewStorageCommand from user input + */ + public ViewStorageCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ViewStorageCommand.MESSAGE_USAGE)); + } + Matcher matcher = ViewStorageCommand_Argument_Format.matcher(trimmedArgs); + if (!matcher.matches()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ViewStorageCommand.MESSAGE_USAGE) + ); + } + String[] path = matcher.group("rawpath").split("\\\\"); + return new ViewStorageCommand(path); + } +} diff --git a/src/main/java/seedu/address/logic/parser/exceptions/ParseException.java b/src/main/java/quickdocs/logic/parser/exceptions/ParseException.java similarity index 73% rename from src/main/java/seedu/address/logic/parser/exceptions/ParseException.java rename to src/main/java/quickdocs/logic/parser/exceptions/ParseException.java index 158a1a54c1c5..2220d37e0bbe 100644 --- a/src/main/java/seedu/address/logic/parser/exceptions/ParseException.java +++ b/src/main/java/quickdocs/logic/parser/exceptions/ParseException.java @@ -1,6 +1,6 @@ -package seedu.address.logic.parser.exceptions; +package quickdocs.logic.parser.exceptions; -import seedu.address.commons.exceptions.IllegalValueException; +import quickdocs.commons.exceptions.IllegalValueException; /** * Represents a parse error encountered by a parser. diff --git a/src/main/java/quickdocs/model/Model.java b/src/main/java/quickdocs/model/Model.java new file mode 100644 index 000000000000..4ecfe1ee19bd --- /dev/null +++ b/src/main/java/quickdocs/model/Model.java @@ -0,0 +1,179 @@ +package quickdocs.model; + +import java.math.BigDecimal; +import java.time.Clock; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.YearMonth; +import java.util.ArrayList; +import java.util.Optional; +import java.util.function.Predicate; + +import javafx.beans.property.ReadOnlyProperty; +import javafx.collections.ObservableList; +import quickdocs.commons.core.GuiSettings; +import quickdocs.model.appointment.Appointment; +import quickdocs.model.consultation.Consultation; +import quickdocs.model.consultation.Diagnosis; +import quickdocs.model.consultation.Prescription; +import quickdocs.model.medicine.Directory; +import quickdocs.model.medicine.Medicine; +import quickdocs.model.patient.Nric; +import quickdocs.model.patient.Patient; +import quickdocs.model.record.Record; +import quickdocs.model.record.Statistics; +import quickdocs.model.reminder.Reminder; +import quickdocs.model.tag.Tag; + +/** + * The API of the Model component. + */ +public interface Model { + + /** + * Replaces user prefs data with the data in {@code userPrefs}. + */ + void setUserPrefs(ReadOnlyUserPrefs userPrefs); + + /** + * Returns the user prefs. + */ + ReadOnlyUserPrefs getUserPrefs(); + + /** + * Returns the user prefs' GUI settings. + */ + GuiSettings getGuiSettings(); + + /** + * Sets the user prefs' GUI settings. + */ + void setGuiSettings(GuiSettings guiSettings); + + void updateFilteredReminderList(Predicate predicate); + + ObservableList getFilteredReminderList(); + + ReadOnlyProperty selectedReminderProperty(); + + void setSelectedReminder(Reminder reminder); + + //===========Quickdocs methods===================================== + QuickDocs getQuickDocs(); + + //===========Medicine Storage ===================================== + void addMedicine(String medicineName, String[] path, BigDecimal price); + + void addMedicine(String medicineName, int quantity, String[] path, BigDecimal price); + + void addDirectory(String directoryName, String[] path); + + Optional findMedicine(String medicineName); + + Optional findMedicine(String[] path); + + void purchaseMedicine(String[] path, int quantity, BigDecimal cost); + + void purchaseMedicine(String medicineName, int quantity, BigDecimal cost); + + Optional findDirectory(String[] path); + + void setThreshold(Medicine medicine, int threshold); + + void setThreshold(Directory directory, int threshold); + + void addExistingMedicineToDirectory(Medicine medicine, String[] path); + + void setPrice(Medicine medicine, BigDecimal price); + + ArrayList getDirectorySuggestions(String path); + + ArrayList getMedicineSuggestions(String path); + //===========Patient module operations============================ + boolean duplicatePatient(Patient patient); + + void addPatient(Patient patient); + + boolean isPatientListEmpty(); + + boolean checkValidIndex(int index); + + Patient getPatientAtIndex(int index); + + boolean checkDuplicatePatientAfterEdit(int index, Patient editedPatient); + + void replacePatient(int index, Patient editedPatient); + + String findPatientsByName(String searchSequence); + + String listFiftyPatients(); + + String findPatientsByNric(String searchSequence); + + String findPatientsByTag(Tag tag); + + Patient getPatientByNric(String nric); + + Optional getPatientByNric(Nric nric); + + int getIndexByNric(Nric nric); + + void deletePatientByNric(String nric); + + //==========Consultation methods===================== + + void createConsultation(Patient patient); + + void diagnosePatient(Diagnosis diagnosis); + + boolean checkConsultation(); + + void prescribeMedicine(ArrayList prescriptions); + + Consultation getCurrentConsultation(); + + ArrayList getConsultationList(); + + void endConsultation(); + + ArrayList listConsultation(String value); + + Consultation listConsultation(int index); + + void abortConsultation(); + + void executePrescription(Prescription prescription); + //===========Appointment module operations======================== + boolean hasTimeConflicts(Appointment app); + + void addApp(Appointment app); + + String listApp(LocalDate start, LocalDate end); + + String listApp(Patient patient); + + String freeApp(LocalDate start, LocalDate end); + + Optional getAppointment(LocalDate date, LocalTime start); + + void deleteAppointment(Appointment appointment); + + //===========Reminder module operations=========================== + boolean duplicateRem(Reminder rem); + + void addRem(Reminder rem); + + void deleteReminder(Reminder reminder); + + void reminderForMedicine(Medicine medicine); + + Predicate getCurrentWeekRemindersPredicate(); + + void deleteExistingReminderForMedicine(Medicine medicine); + //===========Record module operations============================= + Statistics getStatistics(YearMonth from, YearMonth to); + + void addRecord(Record record, Clock clock); + + void setConsultationFee(BigDecimal fee); +} diff --git a/src/main/java/quickdocs/model/ModelManager.java b/src/main/java/quickdocs/model/ModelManager.java new file mode 100644 index 000000000000..fbe68ade2109 --- /dev/null +++ b/src/main/java/quickdocs/model/ModelManager.java @@ -0,0 +1,510 @@ +package quickdocs.model; + +import static java.time.DayOfWeek.MONDAY; +import static java.time.DayOfWeek.SUNDAY; +import static java.time.temporal.TemporalAdjusters.nextOrSame; +import static java.time.temporal.TemporalAdjusters.previousOrSame; +import static java.util.Objects.requireNonNull; +import static quickdocs.commons.util.CollectionUtil.requireAllNonNull; + +import java.math.BigDecimal; +import java.time.Clock; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.YearMonth; +import java.util.ArrayList; +import java.util.Optional; +import java.util.function.Predicate; +import java.util.logging.Logger; + +import javafx.beans.property.ReadOnlyProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.collections.ObservableList; +import javafx.collections.transformation.FilteredList; +import quickdocs.commons.core.GuiSettings; +import quickdocs.commons.core.LogsCenter; +import quickdocs.model.appointment.Appointment; +import quickdocs.model.appointment.AppointmentManager; +import quickdocs.model.consultation.Consultation; +import quickdocs.model.consultation.ConsultationManager; +import quickdocs.model.consultation.Diagnosis; +import quickdocs.model.consultation.Prescription; +import quickdocs.model.medicine.Directory; +import quickdocs.model.medicine.Medicine; +import quickdocs.model.medicine.MedicineManager; +import quickdocs.model.patient.Nric; +import quickdocs.model.patient.Patient; +import quickdocs.model.patient.PatientManager; +import quickdocs.model.record.MedicinePurchaseRecord; +import quickdocs.model.record.Record; +import quickdocs.model.record.Statistics; +import quickdocs.model.record.StatisticsManager; +import quickdocs.model.reminder.Reminder; +import quickdocs.model.reminder.ReminderManager; +import quickdocs.model.reminder.ReminderWithinDatesPredicate; +import quickdocs.model.reminder.exceptions.ReminderNotFoundException; +import quickdocs.model.tag.Tag; + +/** + * Represents the in-memory model of the address book data. + */ +public class ModelManager implements Model { + private static final Logger logger = LogsCenter.getLogger(ModelManager.class); + + private final UserPrefs userPrefs; + // to handle QuickDocs operations + private final QuickDocs quickDocs; + private final FilteredList filteredReminders; + private final SimpleObjectProperty selectedReminder = new SimpleObjectProperty<>(); + private final MedicineManager medicineManager; + private final PatientManager patientManager; + private final ConsultationManager consultationManager; + private final AppointmentManager appointmentManager; + private final ReminderManager reminderManager; + private final StatisticsManager statisticsManager; + + /** + * Initializes a ModelManager with the given QuickDocs and userPrefs. + */ + public ModelManager(QuickDocs quickDocs, ReadOnlyUserPrefs userPrefs) { + super(); + requireAllNonNull(userPrefs); + + logger.fine("Initializing with QuickDocs: " + quickDocs + " and user prefs " + userPrefs); + + this.quickDocs = quickDocs; + this.userPrefs = new UserPrefs(userPrefs); + this.medicineManager = quickDocs.getMedicineManager(); + this.patientManager = quickDocs.getPatientManager(); + this.consultationManager = quickDocs.getConsultationManager(); + this.appointmentManager = quickDocs.getAppointmentManager(); + this.reminderManager = quickDocs.getReminderManager(); + this.statisticsManager = quickDocs.getStatisticsManager(); + filteredReminders = new FilteredList<>(reminderManager.getObservableReminderList()); + } + + public ModelManager() { + this(new QuickDocs(), new UserPrefs()); + } + + @Override + public QuickDocs getQuickDocs() { + return quickDocs; + } + + //=========== UserPrefs ================================================================================== + + @Override + public void setUserPrefs(ReadOnlyUserPrefs userPrefs) { + requireNonNull(userPrefs); + this.userPrefs.resetData(userPrefs); + } + + @Override + public ReadOnlyUserPrefs getUserPrefs() { + return userPrefs; + } + + @Override + public GuiSettings getGuiSettings() { + return userPrefs.getGuiSettings(); + } + + @Override + public void setGuiSettings(GuiSettings guiSettings) { + requireNonNull(guiSettings); + userPrefs.setGuiSettings(guiSettings); + } + //=========== MedicineManager ============================================================================ + @Override + public void addMedicine(String medicineName, String[] path, BigDecimal price) { + Medicine medicine = medicineManager.addMedicine(medicineName, path, price); + reminderForMedicine(medicine); + quickDocs.indicateModification(true); + } + + @Override + public void addMedicine(String medicineName, int quantity, String[] path, BigDecimal price) { + Medicine medicine = medicineManager.addMedicine(medicineName, quantity, path, price); + reminderForMedicine(medicine); + quickDocs.indicateModification(true); + } + + @Override + public void addExistingMedicineToDirectory(Medicine medicine, String[] path) { + medicineManager.addExistingMedicineToDirectory(medicine, path); + quickDocs.indicateModification(true); + } + + @Override + public void addDirectory(String directoryName, String[] path) { + medicineManager.addDirectory(directoryName, path); + quickDocs.indicateModification(true); + } + + @Override + public Optional findMedicine(String medicineName) { + return medicineManager.findMedicine(medicineName); + } + + @Override + public Optional findMedicine(String[] path) { + return medicineManager.findMedicine(path); + } + + @Override + public void purchaseMedicine(String[] path, int quantity, BigDecimal cost) { + Medicine medicine = medicineManager.purchaseMedicine(path, quantity); + reminderForMedicine(medicine); + addRecord(new MedicinePurchaseRecord(medicine, quantity, cost), Clock.systemDefaultZone()); + quickDocs.indicateModification(true); + } + + @Override + public void purchaseMedicine(String medicineName, int quantity, BigDecimal cost) { + Medicine medicine = medicineManager.purchaseMedicine(medicineName, quantity); + reminderForMedicine(medicine); + addRecord(new MedicinePurchaseRecord(medicine, quantity, cost), Clock.systemDefaultZone()); + quickDocs.indicateModification(true); + } + + @Override + public Optional findDirectory(String[] path) { + return medicineManager.findDirectory(path); + } + + @Override + public void setThreshold(Medicine medicine, int threshold) { + medicine.setThreshold(threshold); + reminderForMedicine(medicine); + quickDocs.indicateModification(true); + } + + @Override + public void setThreshold(Directory directory, int threshold) { + directory.setThreshold(threshold); + for (Medicine medicine : directory.getListOfMedicine()) { + setThreshold(medicine, threshold); + } + for (Directory subDirectory : directory.getListOfDirectory()) { + setThreshold(subDirectory, threshold); + } + quickDocs.indicateModification(true); + } + + @Override + public void setPrice(Medicine medicine, BigDecimal price) { + medicine.setPrice(price); + quickDocs.indicateModification(true); + } + + @Override + public ArrayList getDirectorySuggestions(String path) { + return medicineManager.getDirectorySuggestions(path); + } + + @Override + public ArrayList getMedicineSuggestions(String path) { + return medicineManager.getMedicineSuggestions(path); + } + //=========== Filtered Reminder List Accessors =========================================================== + + @Override + public void updateFilteredReminderList(Predicate predicate) { + requireNonNull(predicate); + filteredReminders.setPredicate(predicate); + } + + @Override + public ObservableList getFilteredReminderList() { + return filteredReminders; + } + + @Override + public Predicate getCurrentWeekRemindersPredicate() { + LocalDate todaysDate = LocalDate.now(); + LocalDate start = todaysDate.with(previousOrSame(MONDAY)); + LocalDate end = todaysDate.with(nextOrSame(SUNDAY)); + return new ReminderWithinDatesPredicate(start, end); + } + + //=========== Selected person =========================================================================== + + @Override + public ReadOnlyProperty selectedReminderProperty() { + return selectedReminder; + } + + @Override + public void setSelectedReminder(Reminder reminder) { + if (reminder != null && !reminderManager.getReminderList().contains(reminder)) { + throw new ReminderNotFoundException(); + } + selectedReminder.setValue(reminder); + } + + @Override + public boolean equals(Object obj) { + // short circuit if same object + if (obj == this) { + return true; + } + + // instanceof handles nulls + if (!(obj instanceof ModelManager)) { + return false; + } + + // state check + ModelManager other = (ModelManager) obj; + return userPrefs.equals(other.userPrefs); + } + + //==========Patient module============================================================================ + + // for adding + public boolean duplicatePatient(Patient patient) { + return this.patientManager.isDuplicatePatient(patient); + } + + /** + * Add a patient to the quickdocs + */ + public void addPatient(Patient patient) { + this.patientManager.addPatient(patient); + quickDocs.indicateModification(true); + } + + // for editing + public boolean isPatientListEmpty() { + return this.patientManager.isPatientListEmpty(); + } + + public boolean checkValidIndex(int index) { + return this.patientManager.checkValidIndex(index); + } + + public Patient getPatientAtIndex(int index) { + return this.patientManager.getPatientAtIndex(index); + } + + public boolean checkDuplicatePatientAfterEdit(int index, Patient editedPatient) { + return this.patientManager.checkDuplicatePatientAfterEdit(index, editedPatient); + } + + /** + * Replace the patient at index with the edited version + */ + public void replacePatient(int index, Patient editedPatient) { + this.patientManager.replacePatient(index, editedPatient); + quickDocs.indicateModification(true); + } + + // for listing + public String findPatientsByName(String searchSequence) { + return this.patientManager.findPatientsByName(searchSequence); + } + + public String listFiftyPatients() { + return this.patientManager.listFiftyPatients(); + } + + public String findPatientsByNric(String searchSequence) { + return this.patientManager.findPatientsByNric(searchSequence); + } + + public String findPatientsByTag(Tag tag) { + return this.patientManager.findPatientsByTag(tag); + } + + public Patient getPatientByNric(String nric) { + return this.patientManager.getPatientByNric(nric); + } + + public Optional getPatientByNric(Nric nric) { + return this.patientManager.getPatientByNric(nric); + } + + public int getIndexByNric(Nric nric) { + return this.patientManager.getIndexByNric(nric); } + + /** + * Delete patient from patient records + * + * @param nric of the patient to be deleted + */ + public void deletePatientByNric(String nric) { + this.patientManager.deletePatientByNric(nric); + quickDocs.indicateModification(true); + } + + //==========Consultation module============================================================================ + + public void createConsultation(Patient patient) { + this.consultationManager.createConsultation(patient); + } + + public void diagnosePatient(Diagnosis diagnosis) { + this.consultationManager.diagnosePatient(diagnosis); + } + + public boolean checkConsultation() { + return this.consultationManager.checkConsultation(); + } + + public void prescribeMedicine(ArrayList prescriptions) { + this.consultationManager.prescribeMedicine(prescriptions); + } + + /** + * end the current consultation session, no further edits can be made + */ + public void endConsultation() { + this.consultationManager.endConsultation(); + quickDocs.indicateModification(true); + } + + public Consultation getCurrentConsultation() { + return this.consultationManager.getCurrentConsultation(); + } + + public ArrayList getConsultationList() { + return this.consultationManager.getConsultationList(); + } + + public ArrayList listConsultation(String value) { + return this.consultationManager.listConsultation(value); + } + + public Consultation listConsultation(int index) { + return this.consultationManager.listConsultation(index); + } + + public void abortConsultation() { + this.consultationManager.abortConsultation(); + } + + /** + * Executes subtraction of medicine quantity according to prescription; checks medicine quantities for reminders; + * @param prescription the prescription to execute + */ + public void executePrescription(Prescription prescription) { + prescription.getMedicine().subtractQuantity(prescription.getQuantity()); + reminderForMedicine(prescription.getMedicine()); + } + //==========Appointment module=========================================================================== + + /** + * Adds an {@code Appointment} and its corresponding {@code Reminder} into {@code AppointmentManager} and + * {@code ReminderManager} respectively. + * + * @param app the {@code Appointment} to add. + */ + public void addApp(Appointment app) { + appointmentManager.addAppointment(app); + Reminder remToAdd = createRemFromApp(app); + addRem((remToAdd)); + quickDocs.indicateModification(true); + } + + /** + * Deletes an {@code Appointment} from {@code AppointmentManager}. + * + * @param appointment the {@code Appointment} to delete + */ + public void deleteAppointment(Appointment appointment) { + Optional reminder = reminderManager.getReminder(appointment); + reminder.ifPresent(r -> reminderManager.delete(reminder.get())); + appointmentManager.delete(appointment); + quickDocs.indicateModification(true); + } + + public Optional getAppointment(LocalDate date, LocalTime start) { + return appointmentManager.getAppointment(date, start); + } + + public boolean hasTimeConflicts(Appointment app) { + return appointmentManager.hasTimeConflicts(app); + } + + public String listApp(LocalDate start, LocalDate end) { + return appointmentManager.listAppointments(start, end); + } + + public String listApp(Patient patient) { + return appointmentManager.listAppointments(patient); + } + + public String freeApp(LocalDate start, LocalDate end) { + return appointmentManager.listFreeSlots(start, end); + } + + //==========Reminder module============================================================================== + + /** + * Adds a {@code Reminder} to {@code ReminderManager}. + * + * @param rem the {@code Reminder} to add. + */ + public void addRem(Reminder rem) { + reminderManager.addReminder(rem); + quickDocs.indicateModification(true); + } + + /** + * Deletes a {@code Reminder} from {@code ReminderManager}. + * + * @param reminder the {@code Reminder} to be deleted. + */ + public void deleteReminder(Reminder reminder) { + reminderManager.delete(reminder); + quickDocs.indicateModification(true); + } + + private Reminder createRemFromApp(Appointment app) { + return new Reminder(app.createTitle(), app.getComment(), app.getDate(), app.getStart(), app.getEnd()); + } + + public boolean duplicateRem(Reminder rem) { + return reminderManager.hasDuplicateReminder(rem); + } + + /** + * Adds a {@code Reminder} into {@code ReminderManager} for a {@code Medicine} with insufficient amount. + * + * @param medicine the {@code Medicine} that has quantity below its threshold. + */ + public void reminderForMedicine(Medicine medicine) { + reminderManager.reminderForMedicine(medicine); + quickDocs.indicateModification(true); + } + + /** + * Deletes outdated low quantity {@code Reminder} previously created for the given {@code Medicine}. + * + * @param medicine {@code Medicine} that is no longer in need of the {@code Reminder} previously created. + */ + public void deleteExistingReminderForMedicine(Medicine medicine) { + if (reminderManager.deleteExistingMedicineReminder(medicine)) { + quickDocs.indicateModification(true); + } + } + + //==========Record module================================================================================ + public Statistics getStatistics(YearMonth from, YearMonth to) { + return statisticsManager.getStatistics(from, to); + } + + /** + * Adds a {@code Record} converted to {@code Statistics} to QuickDocs + */ + public void addRecord(Record record, Clock clock) { + statisticsManager.record(record, clock); + quickDocs.indicateModification(true); + } + + public void setConsultationFee(BigDecimal fee) { + statisticsManager.setConsultationFee(fee); + quickDocs.indicateModification(true); + } +} diff --git a/src/main/java/quickdocs/model/QuickDocs.java b/src/main/java/quickdocs/model/QuickDocs.java new file mode 100644 index 000000000000..bc3470f0d3dd --- /dev/null +++ b/src/main/java/quickdocs/model/QuickDocs.java @@ -0,0 +1,81 @@ +package quickdocs.model; + +import quickdocs.model.appointment.AppointmentManager; +import quickdocs.model.consultation.ConsultationManager; +import quickdocs.model.medicine.MedicineManager; +import quickdocs.model.patient.PatientManager; +import quickdocs.model.record.StatisticsManager; +import quickdocs.model.reminder.ReminderManager; + +/** + * Manages all managers of QuickDocs + */ +public class QuickDocs { + + private PatientManager patientManager = new PatientManager(); + private ConsultationManager consultationManager = new ConsultationManager(); + private AppointmentManager appointmentManager = new AppointmentManager(); + private ReminderManager reminderManager = new ReminderManager(); + private MedicineManager medicineManager = new MedicineManager(); + private StatisticsManager statisticsManager = new StatisticsManager(); + + private boolean isModified = false; + + public MedicineManager getMedicineManager() { + return medicineManager; + } + + public PatientManager getPatientManager() { + return patientManager; + } + + public ConsultationManager getConsultationManager() { + return consultationManager; + } + + public AppointmentManager getAppointmentManager() { + return appointmentManager; + } + + public ReminderManager getReminderManager() { + return reminderManager; + } + + public StatisticsManager getStatisticsManager() { + return statisticsManager; + } + + public void setStatisticsManager(StatisticsManager statisticsManager) { + this.statisticsManager = statisticsManager; + } + + // indicate modification of quickdocs data + public boolean isModified() { + return isModified; + } + + public void indicateModification(boolean state) { + isModified = state; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof QuickDocs)) { + return false; + } + + QuickDocs otherQuickDocs = (QuickDocs) other; + return otherQuickDocs.patientManager.getPatientList() + .equals(this.patientManager.getPatientList()) + && otherQuickDocs.consultationManager.getConsultationList() + .equals(this.consultationManager.getConsultationList()) + && otherQuickDocs.appointmentManager.equals(this.appointmentManager) + && otherQuickDocs.reminderManager.equals(this.reminderManager) + && otherQuickDocs.medicineManager.getListOfMedicine() + .equals(this.medicineManager.getListOfMedicine()); + } +} diff --git a/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java b/src/main/java/quickdocs/model/ReadOnlyUserPrefs.java similarity index 57% rename from src/main/java/seedu/address/model/ReadOnlyUserPrefs.java rename to src/main/java/quickdocs/model/ReadOnlyUserPrefs.java index befd58a4c739..e9bed918fbcd 100644 --- a/src/main/java/seedu/address/model/ReadOnlyUserPrefs.java +++ b/src/main/java/quickdocs/model/ReadOnlyUserPrefs.java @@ -1,8 +1,8 @@ -package seedu.address.model; +package quickdocs.model; import java.nio.file.Path; -import seedu.address.commons.core.GuiSettings; +import quickdocs.commons.core.GuiSettings; /** * Unmodifiable view of user prefs. @@ -11,6 +11,5 @@ public interface ReadOnlyUserPrefs { GuiSettings getGuiSettings(); - Path getAddressBookFilePath(); - + Path getQuickDocsFilePath(); } diff --git a/src/main/java/quickdocs/model/Slot.java b/src/main/java/quickdocs/model/Slot.java new file mode 100644 index 000000000000..75e1b6990772 --- /dev/null +++ b/src/main/java/quickdocs/model/Slot.java @@ -0,0 +1,74 @@ +package quickdocs.model; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.Objects; + +/** + * Represents a time slot in a day, from a start time to an optional end time. + */ +public class Slot implements Comparable { + private LocalDate date; + private LocalTime start; + private LocalTime end; + + public Slot (LocalDate date, LocalTime start, LocalTime end) { + this.date = date; + this.start = start; + this.end = end; + } + + public Slot() { + } + + public LocalDate getDate() { + return date; + } + + public LocalTime getStart() { + return start; + } + + public LocalTime getEnd() { + return end; + } + + @Override + public int compareTo(Slot other) { + if (date.equals(other.getDate())) { + return start.compareTo(other.getStart()); + } else { + return date.compareTo(other.getDate()); + } + } + + @Override + public int hashCode() { + return Objects.hash(date, start, end); + } + + /** + * Returns true if both slots have the same identity and data fields. + * This defines a stronger notion of equality between two slots. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (other == null) { + return false; + } + + if (!(other.getClass() == getClass())) { + return false; + } + + Slot otherSlot = (Slot) other; + // Objects.equals() to handle null fields + return otherSlot.date.equals(date) + && otherSlot.start.equals(start) + && Objects.equals(otherSlot.end, end); + } +} diff --git a/src/main/java/seedu/address/model/UserPrefs.java b/src/main/java/quickdocs/model/UserPrefs.java similarity index 70% rename from src/main/java/seedu/address/model/UserPrefs.java rename to src/main/java/quickdocs/model/UserPrefs.java index 25a5fd6eab9e..33a74f605d59 100644 --- a/src/main/java/seedu/address/model/UserPrefs.java +++ b/src/main/java/quickdocs/model/UserPrefs.java @@ -1,4 +1,4 @@ -package seedu.address.model; +package quickdocs.model; import static java.util.Objects.requireNonNull; @@ -6,7 +6,7 @@ import java.nio.file.Paths; import java.util.Objects; -import seedu.address.commons.core.GuiSettings; +import quickdocs.commons.core.GuiSettings; /** * Represents User's preferences. @@ -14,7 +14,7 @@ public class UserPrefs implements ReadOnlyUserPrefs { private GuiSettings guiSettings = new GuiSettings(); - private Path addressBookFilePath = Paths.get("data" , "addressbook.json"); + private Path quickDocsFilePath = Paths.get("data", "quickdocs.json"); /** * Creates a {@code UserPrefs} with default values. @@ -35,7 +35,7 @@ public UserPrefs(ReadOnlyUserPrefs userPrefs) { public void resetData(ReadOnlyUserPrefs newUserPrefs) { requireNonNull(newUserPrefs); setGuiSettings(newUserPrefs.getGuiSettings()); - setAddressBookFilePath(newUserPrefs.getAddressBookFilePath()); + setQuickDocsFilePath(newUserPrefs.getQuickDocsFilePath()); } public GuiSettings getGuiSettings() { @@ -47,13 +47,13 @@ public void setGuiSettings(GuiSettings guiSettings) { this.guiSettings = guiSettings; } - public Path getAddressBookFilePath() { - return addressBookFilePath; + public Path getQuickDocsFilePath() { + return quickDocsFilePath; } - public void setAddressBookFilePath(Path addressBookFilePath) { - requireNonNull(addressBookFilePath); - this.addressBookFilePath = addressBookFilePath; + public void setQuickDocsFilePath(Path quickDocsFilePath) { + requireNonNull(quickDocsFilePath); + this.quickDocsFilePath = quickDocsFilePath; } @Override @@ -68,19 +68,19 @@ public boolean equals(Object other) { UserPrefs o = (UserPrefs) other; return guiSettings.equals(o.guiSettings) - && addressBookFilePath.equals(o.addressBookFilePath); + && quickDocsFilePath.equals(o.quickDocsFilePath); } @Override public int hashCode() { - return Objects.hash(guiSettings, addressBookFilePath); + return Objects.hash(guiSettings, quickDocsFilePath); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("Gui Settings : " + guiSettings); - sb.append("\nLocal data file location : " + addressBookFilePath); + sb.append("\nLocal data file location : " + quickDocsFilePath); return sb.toString(); } diff --git a/src/main/java/quickdocs/model/appointment/Appointment.java b/src/main/java/quickdocs/model/appointment/Appointment.java new file mode 100644 index 000000000000..5407f4ef742e --- /dev/null +++ b/src/main/java/quickdocs/model/appointment/Appointment.java @@ -0,0 +1,81 @@ +package quickdocs.model.appointment; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.Objects; + +import quickdocs.model.Slot; +import quickdocs.model.patient.Patient; + +/** + * Represents an Appointment created in QuickDocs. + */ +public class Appointment extends Slot { + private Patient patient; + private String comment; + + public Appointment(Patient patient, LocalDate date, LocalTime start, LocalTime end, String comment) { + super(date, start, end); + this.patient = patient; + this.comment = comment; + } + + public Appointment() { + super(); + } + + public Patient getPatient() { + return patient; + } + + public String getComment() { + return comment; + } + + /** + * Generates a {@code String title} to be used in the creation of a {@code Reminder} + * + * @return the {@code String title} of this {@code Appointment} + */ + public String createTitle() { + return "Appointment with " + this.getPatient().getName() + ", " + this.getPatient().getNric(); + } + + /** + * Returns true if both appointments have the same identity and data fields. + * This defines a stronger notion of equality between two appointments. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Appointment)) { + return false; + } + + Appointment otherApp = (Appointment) other; + return super.equals(other) + && otherApp.patient.equals(patient) + && otherApp.comment.equals(comment); + } + + @Override + public int hashCode() { + return Objects.hash(getDate(), getStart(), getEnd(), patient, comment); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append(getPatient().getName()).append(" ") + .append(getPatient().getNric()).append(", ") + .append(getDate()).append(", ") + .append(getStart()) + .append(" to ") + .append(getEnd()).append(", ") + .append(getComment()); + return builder.toString(); + } +} diff --git a/src/main/java/quickdocs/model/appointment/AppointmentManager.java b/src/main/java/quickdocs/model/appointment/AppointmentManager.java new file mode 100644 index 000000000000..e2f38036b45e --- /dev/null +++ b/src/main/java/quickdocs/model/appointment/AppointmentManager.java @@ -0,0 +1,349 @@ +package quickdocs.model.appointment; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import quickdocs.model.Slot; +import quickdocs.model.patient.Patient; + +/** + * Manages the list of {@code Appointments} created. + */ +public class AppointmentManager { + public static final LocalTime OPENING_HOUR = LocalTime.parse("09:00"); + public static final LocalTime CLOSING_HOUR = LocalTime.parse("18:00"); + + private final List appointments; + + public AppointmentManager() { + appointments = new ArrayList<>(); + } + + public List getAppointmentList() { + return appointments; + } + + public void delete(Appointment app) { + appointments.remove(app); + } + + public boolean hasDuplicateAppointment(Appointment app) { + return appointments.contains(app); + } + + + /** + * Finds and returns the {@code Appointment} in the list of appointments with the given date and start time, + * if it exists. + * + * @param date the {@code LocalDate} date of the {@code Appointment} to find. + * @param start the {@code LocalTime} start time of the {@code Appointment} to find. + * @return the {@code Appointment} found, if it exists, else returns {@code Optional.empty()}. + */ + public Optional getAppointment(LocalDate date, LocalTime start) { + List filtered = appointments.stream() + .filter(a -> a.getDate().equals(date)) + .filter(a -> a.getStart().equals(start)) + .collect(Collectors.toList()); + + // filtered cannot contain > 1 appointment as each appointment is uniquely identified by its + // date and start fields. + assert filtered.size() <= 1; + + if (filtered.isEmpty()) { + return Optional.empty(); + } else { + return Optional.of(filtered.get(0)); + } + } + + /** + * Returns a {@code List} of {@code Appointment}s that are within the given search range of dates. + * + * @param start the {@code LocalDate start} date of the search range. + * @param end the {@code LocalDate end} date of the search range. + * @return {@code List} of {@code Appointment}s that has dates from {@code start} to {@code end}. + */ + private List getAppointments(LocalDate start, LocalDate end) { + List validApps = new ArrayList<>(); + LocalDate date; + + for (Appointment app : appointments) { + date = app.getDate(); + // Add appointments to validApps only with dates between the given range (inclusive) + if (date.compareTo(start) >= 0 && date.compareTo(end) <= 0) { + validApps.add(app); + } + + // Stop adding when date of appointment is after given end date, since appointments are already sorted + if (date.compareTo(end) > 0) { + break; + } + } + return validApps; + } + + /** + * Returns a {@code List} of {@code Appointment}s that were created for the given {@code Patient}. + * + * @param patient the {@code Patient} whose {@code Appointment}s to be retrieved. + * @return {@code List} of {@code Appointment}s that were created for {@code Patient patient}. + */ + private List getAppointments(Patient patient) { + List validApps = new ArrayList<>(); + + for (Appointment app : appointments) { + // Add appointments to validApps only if they were created for the given patient + if (app.getPatient().equals(patient)) { + validApps.add(app); + } + } + return validApps; + } + + /** + * Adds an {@code Appointment} to the ordered list of appointments, in the correct position. + * + * @param toAdd the {@code Appointment} to add. + */ + public void addAppointment(Appointment toAdd) { + // checking for time conflicts should have happened in AddAppCommand + assert !this.hasTimeConflicts(toAdd); + if (appointments.isEmpty()) { + appointments.add(toAdd); + return; + } + + // place appointment in correct position + for (Appointment app : appointments) { + if (app.compareTo(toAdd) > 0) { + int index = appointments.indexOf(app); + appointments.add(index, toAdd); + return; + } + } + + // toAdd is to be placed at the end of the list + appointments.add(toAdd); + } + + /** + * Checks if there are any conflicts in appointment timings between the current list of appointments + * and the given {@code Appointment}. + * + * @param otherApp given {@code Appointment} to check timing against the existing list of appointments. + * @return {@code true} if there exists a conflict in timing, else return {@code false}. + */ + public boolean hasTimeConflicts(Appointment otherApp) { + LocalDate date = otherApp.getDate(); + + for (Appointment app : appointments) { + if (app.getDate().compareTo(date) == 0) { + if (hasOverlappingTime(app, otherApp)) { + return true; + } + } else if (app.getDate().isAfter(date)) { + // terminate loop early since appointments are already sorted by date and time + break; + } + } + return false; + } + + /** + * Checks if two {@code Appointment}s have an overlap in timing. + * + * @param appA first {@code Appointment} given. + * @param appB second {@code Appontment} given. + * @return {@code true} if there is an overlap in timing, else return {@code false}. + */ + private boolean hasOverlappingTime(Appointment appA, Appointment appB) { + LocalTime startA = appA.getStart(); + LocalTime endA = appA.getEnd(); + LocalTime startB = appB.getStart(); + LocalTime endB = appB.getEnd(); + + // shortcut to check for overlaps between two intervals + return startA.isBefore(endB) && startB.isBefore(endA); + } + + /** + * Generates a {@code String} of {@code Appointment} details with dates between a search range, inclusive. + * + * @param start the {@code LocalDate start} date of the search range. + * @param end the {@code LocalDate end} date of the search range. + * @return {@code String} of {@code Appointment} details with dates from {@code start} to {@code end}. + */ + public String listAppointments(LocalDate start, LocalDate end) { + List toList = getAppointments(start, end); + return listAppointments(toList); + } + + /** + * Generates a {@code String} of {@code Appointment} details created for a given {@code Patient}. + * + * @param patient the {@code Patient} whose {@code Appointment} details to list. + * @return {@code String} of {@code Appointment} details created for the given {@code Patient}. + */ + public String listAppointments(Patient patient) { + List toList = getAppointments(patient); + return listAppointments(toList); + } + + /** + * Generates a {@code String} of {@code Appointment} details given the appointments. + * + * @param appsToList the {@code List} of {@code Appointment}s to list. + * @return {@code String} of {@code Appointment} details. + */ + public String listAppointments(List appsToList) { + StringBuilder sb = new StringBuilder(); + // index of the appointment to be displayed + int i = 1; + + for (Appointment app : appsToList) { + sb.append(i) + .append(") ") + .append(app.toString()) + .append("\n"); + i++; + } + return sb.toString(); + } + + /** + * Generates a {@code List} of free {@code Slot}s within a given search range of dates. + * A {@code Slot} is free if there are no {@code Appointment}s scheduled during that time slot. + * + * @param start the {@code LocalDate start} date of the search range. + * @param end the {@code LocalDate end} date of the search range. + * @return {@code List} of free {@code Slot}s within the given search range. + */ + private List getFreeSlots(LocalDate start, LocalDate end) { + List freeSlots = new ArrayList<>(); + List toSearch = getAppointments(start, end); + + // all slots are free + if (toSearch.isEmpty()) { + for (LocalDate date = start; date.compareTo(end) <= 0; date = date.plusDays(1)) { + freeSlots.add(new Slot(date, OPENING_HOUR, CLOSING_HOUR)); + } + return freeSlots; + } + + Slot slot; + LocalDate date = start; + int index = 0; + Appointment app = toSearch.get(index); + Appointment prevApp = new Appointment( + app.getPatient(), app.getDate().minusDays(1), app.getStart(), app.getEnd(), app.getComment()); + + // loop through all search dates from the start + while (date.compareTo(end) <= 0) { + + // no appointments for given date + if (!date.isEqual(app.getDate())) { + slot = new Slot(date, OPENING_HOUR, CLOSING_HOUR); + freeSlots.add(slot); + } + + LocalTime startTime; + LocalTime endTime; + // there are appointments in given date + while (date.isEqual(app.getDate())) { + + // start a new slot for the day + if (prevApp.getDate().isBefore(date)) { + startTime = OPENING_HOUR; + } else { + startTime = prevApp.getEnd(); + } + endTime = app.getStart(); + + // do not add a free slot if the start time and end time are the same + if (startTime != endTime) { + slot = new Slot(date, startTime, endTime); + freeSlots.add(slot); + } + prevApp = app; + + // no more next appointment, create last slot for the day + if (toSearch.size() == index + 1 || date.isBefore(toSearch.get(index + 1).getDate())) { + startTime = app.getEnd(); + endTime = CLOSING_HOUR; + + // do not add a free slot if the start time and end time are the same + if (startTime != endTime) { + slot = new Slot(date, startTime, endTime); + freeSlots.add(slot); + } + + // no more appointments in toSearch + if (toSearch.size() == index + 1) { + break; + } + } + index++; + app = toSearch.get(index); + } + + date = date.plusDays(1); + } + + return freeSlots; + } + + /** + * Generates a {@code String} of free {@code Slot}s given a search range of dates. + * A {@code Slot} is free if there are no {@code Appointment}s scheduled during that time slot. + * + * @param start the {@code LocalDate start} date of the search range. + * @param end the {@code LocalDate end} date of the search range. + * @return {@code String} of free {@code Slot}s within the given search range. + */ + public String listFreeSlots(LocalDate start, LocalDate end) { + List freeSlots = getFreeSlots(start, end); + StringBuilder sb = new StringBuilder(); + LocalDate date = start.minusDays(1); + + for (Slot slot : freeSlots) { + // start listing for a new date + if (slot.getDate().isAfter(date)) { + date = slot.getDate(); + sb.append("\n") + .append(date) + .append(": "); + } else { + sb.append(", "); + } + + // Whole day is free + if (slot.getStart().equals(OPENING_HOUR) && slot.getEnd().equals(CLOSING_HOUR)) { + sb.append("All slots are free"); + } else { + sb.append(slot.getStart()) + .append(" to ") + .append(slot.getEnd()); + } + } + return sb.toString(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof AppointmentManager)) { + return false; + } + + AppointmentManager otherManager = (AppointmentManager) other; + return otherManager.appointments.equals(this.appointments); + } +} diff --git a/src/main/java/quickdocs/model/consultation/Assessment.java b/src/main/java/quickdocs/model/consultation/Assessment.java new file mode 100644 index 000000000000..3db68a1e7b96 --- /dev/null +++ b/src/main/java/quickdocs/model/consultation/Assessment.java @@ -0,0 +1,38 @@ +package quickdocs.model.consultation; + +/** + * Represents the assessment of the illness during a consultation session + */ +public class Assessment { + public static final String REGEX_SYMPTOMS = "[\\p{Alnum}][\\p{Alnum} ]*"; + public static final String SYMPTOMS_CONSTRAINTS = + "Assessment should only contain alphanumeric characters and spaces but not blank"; + + private String assessment; + + public Assessment() { + } + + public Assessment(String assessment) { + if (!assessment.matches(REGEX_SYMPTOMS)) { + throw new IllegalArgumentException(SYMPTOMS_CONSTRAINTS); + } + this.assessment = assessment; + } + + public String getAssessment() { + return assessment; + } + + @Override + public String toString() { + return assessment; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Assessment // instanceof handles nulls + && assessment.equals(((Assessment) other).getAssessment())); // state check + } +} diff --git a/src/main/java/quickdocs/model/consultation/Consultation.java b/src/main/java/quickdocs/model/consultation/Consultation.java new file mode 100644 index 000000000000..5610c79371f8 --- /dev/null +++ b/src/main/java/quickdocs/model/consultation/Consultation.java @@ -0,0 +1,99 @@ +package quickdocs.model.consultation; + +import java.time.LocalDateTime; +import java.util.ArrayList; + +import quickdocs.model.patient.Patient; + +/** + * Stores the consultation details of a single session - both diagnosis and prescribed drugs + */ +public class Consultation { + // for listing multiple consultations belonging to a single patient + // default value is -1 until the consultation details are confirmed and the + // consultation session is stored in the consultationManager's list of consultations + private int index; + + private LocalDateTime session; + private Patient patient; + private Diagnosis diagnosis; + private ArrayList prescriptions; + + // empty constructor is used, attributes are assigned later + // when diagnosing a patient, the patient detail is set + // after symptoms and assessments are parsed in, diagnosis is set + // when drugs are prescribed, then prescriptions is set + // once all the fields are not null, only then the consultation can be stored + public Consultation(Patient patient) { + this.index = -1; + this.session = LocalDateTime.now(); + this.patient = patient; + } + + public Consultation() { + } + + //for json records creation of consultation + public Consultation(int index, Patient patient, LocalDateTime session, Diagnosis diagnosis, + ArrayList prescriptions) { + this.index = index; + this.patient = patient; + this.session = session; + this.diagnosis = diagnosis; + this.prescriptions = new ArrayList<>(); + this.prescriptions.addAll(prescriptions); + } + + public int getIndex() { + return index; + } + + public void setIndex(int index) { + this.index = index; + } + + public LocalDateTime getSession() { + return session; + } + + public void setSession(LocalDateTime session) { + this.session = session; + } + + public Patient getPatient() { + return patient; + } + + public Diagnosis getDiagnosis() { + return diagnosis; + } + + public void setDiagnosis(Diagnosis diagnosis) { + this.diagnosis = diagnosis; + } + + public ArrayList getPrescriptions() { + return prescriptions; + } + + public void setPrescriptions(ArrayList prescriptions) { + this.prescriptions = prescriptions; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Consultation for: " + getPatient().getNric().getNric() + + " Name: " + getPatient().getName().getName() + "\n"); + sb.append("====================\n"); + sb.append("\n"); + sb.append(getDiagnosis()); + sb.append("\n"); + sb.append("prescription:\n"); + sb.append("====================\n"); + for (Prescription prescription : prescriptions) { + sb.append(prescription); + } + return sb.toString(); + } +} diff --git a/src/main/java/quickdocs/model/consultation/ConsultationManager.java b/src/main/java/quickdocs/model/consultation/ConsultationManager.java new file mode 100644 index 000000000000..f249f3bed1f9 --- /dev/null +++ b/src/main/java/quickdocs/model/consultation/ConsultationManager.java @@ -0,0 +1,98 @@ +package quickdocs.model.consultation; + +import java.util.ArrayList; + +import quickdocs.model.patient.Patient; + +/** + * Handle all model operations involving the steps in consultation + */ +public class ConsultationManager { + + private ArrayList consultationList; + private Consultation currentConsultation; + + public ConsultationManager() { + this.consultationList = new ArrayList(); + } + + + /** + * Check if there is an ongoing consultation session + */ + public boolean checkConsultation() { + return currentConsultation != null; + } + + /** + * Adds a consultation record into the consultationList + * This method is primarily used in the reading of consultation records from json storage + */ + public void addConsultation(Consultation consultation) { + this.consultationList.add(consultation); + } + + /** + * Starts a consultation session with the specified patient. + * QuickDocs only permit one ongoing consultation session at any one time. + * + * @param patient patient record to start the consultation session with + * @throws IllegalArgumentException when the user attempt to start another session when there is an + * ongoing consultation session + */ + public void createConsultation(Patient patient) { + if (currentConsultation != null) { + throw new IllegalArgumentException("There is already an ongoing consultation session\n"); + } + currentConsultation = new Consultation(patient); + } + + public void diagnosePatient(Diagnosis diagnosis) { + currentConsultation.setDiagnosis(diagnosis); + } + + public void prescribeMedicine(ArrayList prescriptions) { + currentConsultation.setPrescriptions(prescriptions); + } + + public Consultation getCurrentConsultation() { + return currentConsultation; + } + + public ArrayList getConsultationList() { + return consultationList; + } + + /** + * Stores current consultation record into consultationList and ends the current + * session + */ + public void endConsultation() { + currentConsultation.setIndex(consultationList.size()); + consultationList.add(currentConsultation); + currentConsultation = null; + } + + /** + * List past consultation records belonging to a single patient + */ + public ArrayList listConsultation(String value) { + + ArrayList consultationsFound = new ArrayList<>(); + + for (Consultation consult : consultationList) { + if (consult.getPatient().getNric().getNric().equals(value)) { + consultationsFound.add(consult); + } + } + return consultationsFound; + } + + public Consultation listConsultation(int index) { + return consultationList.get(index - 1); + } + + public void abortConsultation() { + currentConsultation = null; + } +} diff --git a/src/main/java/quickdocs/model/consultation/Diagnosis.java b/src/main/java/quickdocs/model/consultation/Diagnosis.java new file mode 100644 index 000000000000..a28a4494df56 --- /dev/null +++ b/src/main/java/quickdocs/model/consultation/Diagnosis.java @@ -0,0 +1,57 @@ +package quickdocs.model.consultation; + +import java.util.ArrayList; +import java.util.Arrays; + +/** + * Represents a diagnosis given to the patient during a consultation session + */ +public class Diagnosis { + + private Assessment assessment; + private ArrayList symptoms; + + public Diagnosis() { + } + + public Diagnosis(Assessment assessment, ArrayList symptoms) { + + if (symptoms.size() < 1) { + throw new IllegalArgumentException("Each diagnosis must have symptoms\n"); + } + + this.assessment = assessment; + this.symptoms = symptoms; + } + + public Assessment getAssessment() { + return assessment; + } + + public ArrayList getSymptoms() { + return symptoms; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Diagnosis\n"); + sb.append("====================\n"); + sb.append("Assessment: " + assessment + "\n"); + sb.append("Symptoms:\n"); + for (int i = 0; i < symptoms.size(); i++) { + sb.append(i + 1 + ") " + symptoms.get(i).getSymptom() + "\n"); + } + return sb.toString(); + } + + // for testing + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Diagnosis // instanceof handles nulls + && assessment.equals(((Diagnosis) other).getAssessment()) + && Arrays.equals( + symptoms.toArray(), ((Diagnosis) other).getSymptoms().toArray())); // state check + } +} diff --git a/src/main/java/quickdocs/model/consultation/Prescription.java b/src/main/java/quickdocs/model/consultation/Prescription.java new file mode 100644 index 000000000000..bff6f8684a64 --- /dev/null +++ b/src/main/java/quickdocs/model/consultation/Prescription.java @@ -0,0 +1,53 @@ +package quickdocs.model.consultation; + +import quickdocs.model.medicine.Medicine; + +/** + * Indicate the drug and the quantity administered to the person + */ +public class Prescription { + + public static final String MEDICINE_CONSTRAINTS = "Medicine name can take any values, and it should not be blank"; + //public static final String MEDICINE_REGEX = "[^\\s].*"; + + private Medicine medicine; + private int quantity; + + public Prescription() { + } + + public Prescription(Medicine medicine, int quantity) { + + //if (!medicine.matches(MEDICINE_REGEX)) { + // throw new IllegalArgumentException(MEDICINE_CONSTRAINTS); + //} + + if (quantity < 1) { + throw new IllegalArgumentException("Amount administered must be a non-zero positive number"); + } + + this.medicine = medicine; + this.quantity = quantity; + } + + public Medicine getMedicine() { + return medicine; + } + + public int getQuantity() { + return quantity; + } + + @Override + public String toString() { + return "Medicine: " + getMedicine().name + " quantity: " + getQuantity() + "\n"; + } + + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof Prescription + && getQuantity() == ((Prescription) other).getQuantity() + && getMedicine().equals(((Prescription) other).getMedicine())); + } +} diff --git a/src/main/java/quickdocs/model/consultation/Symptom.java b/src/main/java/quickdocs/model/consultation/Symptom.java new file mode 100644 index 000000000000..d6c17354fe20 --- /dev/null +++ b/src/main/java/quickdocs/model/consultation/Symptom.java @@ -0,0 +1,38 @@ +package quickdocs.model.consultation; + +/** + * Represents a single symptom of a patient's diagnosis + */ +public class Symptom { + public static final String REGEX_SYMPTOMS = "[\\p{Alnum}][\\p{Alnum} ]*"; + public static final String SYMPTOMS_CONSTRAINTS = + "Symptoms should only contain alphanumeric characters and spaces but not blank"; + + private String symptom; + + public Symptom() { + } + + public Symptom(String symptom) { + if (!symptom.matches(REGEX_SYMPTOMS)) { + throw new IllegalArgumentException(SYMPTOMS_CONSTRAINTS); + } + this.symptom = symptom; + } + + public String getSymptom() { + return symptom; + } + + @Override + public String toString() { + return symptom; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Symptom // instanceof handles nulls + && symptom.equals(((Symptom) other).getSymptom())); // state check + } +} diff --git a/src/main/java/quickdocs/model/medicine/Directory.java b/src/main/java/quickdocs/model/medicine/Directory.java new file mode 100644 index 000000000000..9367bf439201 --- /dev/null +++ b/src/main/java/quickdocs/model/medicine/Directory.java @@ -0,0 +1,222 @@ +package quickdocs.model.medicine; + +import static java.util.Objects.requireNonNull; +import static quickdocs.commons.util.AppUtil.checkArgument; +import static quickdocs.commons.util.CollectionUtil.binarySearch; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Optional; + + +/** + * A model representing a folder-like directory storing subdirectories and medicines + */ +public class Directory { + + public static final String VALIDATION_REGEX = "[^\\s].*"; + public static final String MESSAGE_CONSTRAINTS = "Directory name can take any values, and it should not be blank"; + public static final String ERROR_MESSAGE_MEDICINE_ALREADY_EXISTS_UNDER_SAME_DIRECTORY = + "Medicine or Directory with same name already exist under the same directory"; + + public final String name; + private ArrayList listOfMedicine; + private ArrayList listOfDirectory; + private Optional threshold; + + public Directory(String name) { + requireNonNull(name); + checkArgument(ifFitsDirectoryFormat(name), MESSAGE_CONSTRAINTS); + this.name = name; + this.listOfDirectory = new ArrayList<>(); + this.listOfMedicine = new ArrayList<>(); + this.threshold = Optional.empty(); + } + + private boolean ifFitsDirectoryFormat(String test) { + return test.matches(VALIDATION_REGEX); + } + + /** + * Add a medicine to this directory + * @param medicine the medicine to add + */ + public void addMedicine(Medicine medicine) { + requireNonNull(medicine); + checkArgument(isValidNewNameToBeAdded(medicine.name), + ERROR_MESSAGE_MEDICINE_ALREADY_EXISTS_UNDER_SAME_DIRECTORY); + if (threshold.isPresent()) { + medicine.setThreshold(threshold.get()); + } + listOfMedicine.add(medicine); + listOfMedicine.sort(Comparator.comparing((Medicine med) -> (med.name.toLowerCase()))); + } + + /** + * Add a new sub-directory under this directory + * @param name the name of the new directory + */ + public Directory addDirectory(String name) { + requireNonNull(name); + checkArgument(isValidNewNameToBeAdded(name), ERROR_MESSAGE_MEDICINE_ALREADY_EXISTS_UNDER_SAME_DIRECTORY); + Directory newDirectory = new Directory(name); + if (threshold.isPresent()) { + newDirectory.setThreshold(threshold.get()); + } + listOfDirectory.add(newDirectory); + listOfDirectory.sort(Comparator.comparing((Directory directory) -> (directory.name).toLowerCase())); + return newDirectory; + } + + /** + * Add a directory object under this directory + * @param subDirectory The directory to add + * @return The directory added + */ + public Directory addDirectory(Directory subDirectory) { + requireNonNull(subDirectory); + checkArgument(isValidNewNameToBeAdded(subDirectory.name), + ERROR_MESSAGE_MEDICINE_ALREADY_EXISTS_UNDER_SAME_DIRECTORY); + listOfDirectory.add(subDirectory); + listOfDirectory.sort(Comparator.comparing((Directory directory) -> (directory.name.toLowerCase()))); + return subDirectory; + } + + private boolean isValidNewNameToBeAdded(String newName) { + return isNotExistingDirectoryName(newName) && isNotExistingMedicineName(newName); + } + + /** + * Check whether there is no medicine with identical name in the directory + * @param newName the name that needs checking + * @return if there is no existing medicine with the identical name + */ + private boolean isNotExistingMedicineName(String newName) { + for (Medicine medicine : listOfMedicine) { + if (medicine.name.equalsIgnoreCase(newName)) { + return false; + } + } + return true; + } + + /** + * Checks whether there is no existing directory with identical name + * @param directoryName the directory name that need checking + * @return if there is no existing sub-directory with identical name + */ + private boolean isNotExistingDirectoryName(String directoryName) { + for (Directory directory : listOfDirectory) { + if (directory.name.equalsIgnoreCase(directoryName)) { + return false; + } + } + return true; + } + + /** + * To find a medicine given the path + * @param path the path of the medicine in the form of String[] + * @param pointer the pointer indicating the position of the current directory + * @return if the medicine exists under the given path, return a optional object of that medicine; + * else return Optional.empty() + */ + public Optional findMedicine(String[] path, int pointer) { + if (!path[pointer].equalsIgnoreCase(this.name)) { + return Optional.empty(); + } + if (path.length == pointer + 2) { + return searchAmongMedicine(path[pointer + 1]); + } else { + Optional directory = searchAmongDirectory(path[pointer + 1]); + if (!directory.isPresent()) { + return Optional.empty(); + } + return directory.get().findMedicine(path, pointer + 1); + } + } + /** + * To find a directory given the path + * @param path the path of the medicine in the form of String[] + * @param pointer the pointer indicating the position of the current directory + * @return if the directory exists under the given path, return a optional object of that directory; + * else return Optional.empty() + */ + public Optional findDirectory(String[] path, int pointer) { + if (!path[pointer].equalsIgnoreCase(this.name)) { + return Optional.empty(); + } + if (path.length == pointer + 1) { + return Optional.of(this); + } else { + Optional directory = searchAmongDirectory(path[pointer + 1]); + if (!directory.isPresent()) { + return Optional.empty(); + } else { + return directory.get().findDirectory(path, pointer + 1); + } + } + } + + private Optional searchAmongMedicine(String name) { + Comparator comparator = Comparator.naturalOrder(); + return binarySearch(listOfMedicine, (Medicine med) -> ( + comparator.compare(med.name.toLowerCase(), name.toLowerCase()))); + } + + private Optional searchAmongDirectory(String name) { + Comparator comparator = Comparator.naturalOrder(); + return binarySearch(listOfDirectory, (Directory directory) -> ( + comparator.compare(directory.name.toLowerCase(), name.toLowerCase()))); + } + + @Override + public String toString() { + return "- " + name; + } + + /** + * Return a String representation of the content of the directory + * @return + */ + public String viewDetail() { + StringBuilder sb = new StringBuilder(); + if (!listOfDirectory.isEmpty()) { + sb.append("List of sub-directories: \n"); + for (Directory directory : listOfDirectory) { + sb.append(directory.toString() + "\n"); + } + } + if (!listOfMedicine.isEmpty()) { + sb.append("List of Medicine under this directory: \n"); + for (Medicine medicine : listOfMedicine) { + sb.append(medicine.viewDetail() + "\n"); + } + } + if (listOfMedicine.isEmpty() && listOfDirectory.isEmpty()) { + sb.append("Empty directory\n"); + } + return sb.toString(); + } + + /** + * set a default alarm level for all medicine under this directory and all medicine under its sub-directories + * and so on + * @param thres the alarm level + */ + public void setThreshold(int thres) { + this.threshold = Optional.of(thres); + } + + public Optional getThreshold() { + return threshold; + } + + public ArrayList getListOfMedicine() { + return listOfMedicine; + } + + public ArrayList getListOfDirectory() { + return listOfDirectory; + } +} diff --git a/src/main/java/quickdocs/model/medicine/Medicine.java b/src/main/java/quickdocs/model/medicine/Medicine.java new file mode 100644 index 000000000000..29cf99614620 --- /dev/null +++ b/src/main/java/quickdocs/model/medicine/Medicine.java @@ -0,0 +1,141 @@ +package quickdocs.model.medicine; + +import static java.util.Objects.requireNonNull; +import static quickdocs.commons.util.AppUtil.checkArgument; + +import java.math.BigDecimal; + +/** + * Represents the name and history of quantities of a particular medicine + */ +public class Medicine { + + public static final String MESSAGE_CONSTRAINTS = "Medicine name can take any values, and it should not be blank"; + public static final String VALIDATION_REGEX = "\\S+"; + public static final String TO_STRING = "Medicine: %1$s, Quantity: %2$d, Price: %3$s"; + public static final String REMINDER_TITLE_IF_INSUFFICIENT = "Quantity of %1$s is low."; + public static final String REMINDER_COMMENT_IF_INSUFFICIENT = + "Current quantity is at %1$d.\nThe minimum treshold is %2$d."; + private static final int DEFAULT_THRESHOLD = 0; + + //private static ReminderManager reminderManager = new ReminderManager(); + // + public final String name; + private int quantity; + private int threshold; + private BigDecimal price; + + public Medicine() { + name = ""; + } + + /** + * Constructs a medicine with given name and default quantity 0. + * @param name The name of medicine + */ + public Medicine(String name) { + this(name, 0); + } + + /** + * Constructs a medicine with given name and given quantity; + * @param name The name of medicine + * @param amount The amount of medicine + */ + public Medicine(String name, int amount) { + requireNonNull(name); + checkArgument(isValidMedicine(name), MESSAGE_CONSTRAINTS); + if (amount < 0) { + throw new IllegalArgumentException("Quantity should not be negative."); + } + this.name = name; + setQuantity(amount); + this.threshold = DEFAULT_THRESHOLD; + } + /** + public static void setReminderManager(ReminderManager newReminderManager) { + reminderManager = newReminderManager; + } + */ + public static boolean isValidMedicine(String test) { + return test.matches(VALIDATION_REGEX); + } + + public void setQuantity(int amount) { + if (amount < 0) { + throw new IllegalArgumentException("Quantity must be positive"); + } + quantity = amount; + //generateOrDeleteReminder(); + } + + public int getQuantity() { + return quantity; + } + + /** + * Add a given amount to the existing storage + * @param change the amount to be added; + */ + public void addQuantity(int change) { + if (change <= 0) { + throw new IllegalArgumentException("Change amount must be positive"); + } + int current = this.getQuantity(); + setQuantity(current + change); + } + + /** + * Subtract a given amount to the existing storage; + * Constraint: the subtracted amount must not be larger than existing storage; + * @param change the amount to be subtracted + */ + public void subtractQuantity(int change) { + if (change <= 0) { + throw new IllegalArgumentException("Change amount must be positive"); + } + if (change > this.getQuantity()) { + throw new IllegalArgumentException("Insufficient storage"); + } + int current = this.getQuantity(); + setQuantity(current - change); + } + + /** + * Called after each subtraction to detect if the storage is running low + */ + public boolean isSufficient() { + return quantity >= threshold; + } + + public int getThreshold() { + return threshold; + } + + public void setThreshold(int threshold) { + if (threshold < 0) { + throw new IllegalArgumentException("threshold must be non-negative"); + } + this.threshold = threshold; + } + + @Override + public String toString() { + return String.format(TO_STRING, name, quantity, price.toString()); + } + + public String viewDetail() { + return this.toString(); + } + + public BigDecimal getPrice() { + return price; + } + + public void setPrice(BigDecimal price) { + if (price.compareTo(new BigDecimal("0")) == -1) { + throw new IllegalArgumentException("Price should not be negative."); + } + this.price = price; + } +} diff --git a/src/main/java/quickdocs/model/medicine/MedicineManager.java b/src/main/java/quickdocs/model/medicine/MedicineManager.java new file mode 100644 index 000000000000..de0d5463f4fb --- /dev/null +++ b/src/main/java/quickdocs/model/medicine/MedicineManager.java @@ -0,0 +1,211 @@ +package quickdocs.model.medicine; + +import static quickdocs.commons.util.CollectionUtil.binarySearch; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * An model for overall storage of medicine + */ +public class MedicineManager { + + public static final String ERROR_MESSAGE_MEDICINE_WITH_SAME_NAME_EXISTS_IN_LIST = + "Medicine with the same name already exists in the storage."; + public static final String ERROR_MESSAGE_NO_DIRECTORY_FOUND = + "No directory is found at the given path."; + public static final String ERROR_MESSAGE_NO_MEDICINE_FOUND_BY_PATH = + "No Medicine is found at the given path."; + public static final String ERROR_MESSAGE_NO_MEDICINE_FOUND_BY_NAME = + "No Medicine is found by the given name."; + public static final String ERROR_MESSAGE_NO_EXISTING_MED_FOUND = + "No existing medicine with name %1$s found in the storage."; + + private Directory root; + private ArrayList listOfMedicine; + + public MedicineManager() { + root = new Directory("root"); + listOfMedicine = new ArrayList<>(); + } + + /** + * To add a medicine into a directory. If medicine with same name already exist, add that medicine to the directory. + * If no medicine with same name exists, add a new medicine + * + * @param medicineName name of medicine + * @param path path the madicine to be added to + * @return the new medicine added + */ + public Medicine addMedicine(String medicineName, String[] path, BigDecimal price) { + return (this.addMedicine(medicineName, 0, path, price)); + } + + /** + * To add a medicine to the storage by specifying the name of medicine, quantity of it and the path + * + * @param medicineName name of medicine + * @param quantity quantity of medicine + * @param path the path to store to + * @return the new medicine added + */ + public Medicine addMedicine(String medicineName, int quantity, + String[] path, BigDecimal price) { + Optional findMedicine = findMedicine(medicineName); + if (findMedicine.isPresent()) { + throw new IllegalArgumentException(ERROR_MESSAGE_MEDICINE_WITH_SAME_NAME_EXISTS_IN_LIST); + } + Optional directory = root.findDirectory(path, 0); + if (!directory.isPresent()) { + throw new IllegalArgumentException(ERROR_MESSAGE_NO_DIRECTORY_FOUND); + } + Medicine medicine = new Medicine(medicineName, quantity); + medicine.setPrice(price); + listOfMedicine.add(medicine); + listOfMedicine.sort(Comparator.comparing((Medicine x) -> (x.name))); + directory.get().addMedicine(medicine); + return medicine; + } + + /** + * Add a new directory under a directory specified by the path + * + * @param directoryName the name of the new directory + * @param path the path of the destination directory + * @return the new directory added + */ + public Directory addDirectory(String directoryName, String[] path) { + Optional directory = root.findDirectory(path, 0); + if (!directory.isPresent()) { + throw new IllegalArgumentException(ERROR_MESSAGE_NO_DIRECTORY_FOUND); + } + Directory newDirectory = directory.get().addDirectory(directoryName); + return newDirectory; + } + + /** + * find a medicine by its name + * + * @param medicineName the medicine name to search for + * @return Optional.empty() if there is no medicine with the desired name; + * Optional.of(E) if E's name matches the key + */ + public Optional findMedicine(String medicineName) { + Comparator comparator = Comparator.naturalOrder(); + return binarySearch( + listOfMedicine, (Medicine med) -> ( + comparator.compare(med.name.toLowerCase(), medicineName.toLowerCase()))); + } + + public Optional findMedicine(String[] path) { + return root.findMedicine(path, 0); + } + + public Optional findDirectory(String[] path) { + return root.findDirectory(path, 0); + } + + /** + * record a purchase of medicine in the medicine storage + * + * @param path the path leading to the medicine + * @param quantity quantity of medicine purchased + * @returns the medicine affected + */ + public Medicine purchaseMedicine(String[] path, int quantity) { + Optional medicine = findMedicine(path); + if (!medicine.isPresent()) { + throw new IllegalArgumentException(ERROR_MESSAGE_NO_MEDICINE_FOUND_BY_PATH); + } + medicine.get().addQuantity(quantity); + return medicine.get(); + } + + /** + * record a purchase of medicine in the medicine storage + * + * @param medicineName the name of the medicine + * @param quantity quantity of medicine purchased + * @return the medicine affected + */ + public Medicine purchaseMedicine(String medicineName, int quantity) { + Optional medicine = findMedicine(medicineName); + if (!medicine.isPresent()) { + throw new IllegalArgumentException(ERROR_MESSAGE_NO_MEDICINE_FOUND_BY_NAME); + } + medicine.get().addQuantity(quantity); + return medicine.get(); + } + + public Directory getRoot() { + return root; + } + + public void setRoot(Directory root) { + this.root = root; + } + + public void setListOfMedicine(ArrayList listOfMedicine) { + this.listOfMedicine = listOfMedicine; + } + + public ArrayList getListOfMedicine() { + return listOfMedicine; + } + + /** + * Add an existing medicine to a directory specified by path + * + * @param medicine The medicine to add + * @param path The path specifying the directory + */ + public void addExistingMedicineToDirectory(Medicine medicine, String[] path) { + Optional directory = findDirectory(path); + if (!directory.isPresent()) { + throw new IllegalArgumentException(ERROR_MESSAGE_NO_DIRECTORY_FOUND); + } + if (!listOfMedicine.contains(medicine)) { + throw new IllegalArgumentException(ERROR_MESSAGE_NO_EXISTING_MED_FOUND); + } + directory.get().addMedicine(medicine); + } + + private Directory getDirectoryForSuggestions(String rawPath) { + if (!rawPath.contains("\\")) { + throw new IllegalArgumentException("Not in path format."); + } + rawPath = rawPath.trim(); + rawPath = rawPath.substring(0, rawPath.lastIndexOf("\\")); + String[] path = rawPath.split("\\\\"); + Optional foundDirectory = findDirectory(path); + if (!foundDirectory.isPresent()) { + throw new IllegalArgumentException(ERROR_MESSAGE_NO_DIRECTORY_FOUND); + } + return foundDirectory.get(); + } + + public ArrayList getDirectorySuggestions(String rawPath) { + Directory foundDirectory = getDirectoryForSuggestions(rawPath); + ArrayList suggestions = new ArrayList<>(); + suggestions.addAll(foundDirectory.getListOfDirectory().stream() + .map((Directory directory) -> (directory.name)) + .map((String string) -> (string.substring(0, 1).toUpperCase() + string.substring(1))) + .collect(Collectors.toList())); + suggestions.sort(Comparator.comparing((String::toLowerCase))); + return suggestions; + } + + public ArrayList getMedicineSuggestions(String rawPath) { + Directory foundDirectory = getDirectoryForSuggestions(rawPath); + ArrayList suggestions = new ArrayList<>(); + suggestions.addAll(foundDirectory.getListOfMedicine().stream() + .map((Medicine medicine) -> (medicine.name)) + .map((String string) -> (string.substring(0, 1).toLowerCase() + string.substring(1))) + .collect(Collectors.toList())); + suggestions.sort(Comparator.comparing((String::toLowerCase))); + return suggestions; + } +} diff --git a/src/main/java/quickdocs/model/patient/Address.java b/src/main/java/quickdocs/model/patient/Address.java new file mode 100644 index 000000000000..d4bdf29fd621 --- /dev/null +++ b/src/main/java/quickdocs/model/patient/Address.java @@ -0,0 +1,43 @@ +package quickdocs.model.patient; + +/** + * Represents the address of the patient + */ +public class Address { + public static final String REGEX_ADDRESS = "[^\\s\\W].*"; + public static final String ADDRESS_CONSTRAINTS = + "Addresses should not be blank or only contain only spaces or symbols"; + + private String address; + + // empty constructor for json reconstruction + public Address() { + } + + public Address(String address) { + if (!address.matches(REGEX_ADDRESS)) { + throw new IllegalArgumentException(ADDRESS_CONSTRAINTS); + } + this.address = address; + } + + public String getAddress() { + return address; + } + + @Override + public String toString() { + return address; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Address // instanceof handles nulls + && address.equals(((Address) other).getAddress())); // state check + } + + public static boolean isValidAddress(String string) { + return string.matches(REGEX_ADDRESS); + } +} diff --git a/src/main/java/quickdocs/model/patient/Contact.java b/src/main/java/quickdocs/model/patient/Contact.java new file mode 100644 index 000000000000..6d4e25c7b834 --- /dev/null +++ b/src/main/java/quickdocs/model/patient/Contact.java @@ -0,0 +1,43 @@ +package quickdocs.model.patient; + +/** + * Represents the local phone number of the patient, without country code + */ +public class Contact { + + public static final String REGEX_CONTACT = "[\\d]{8}"; + public static final String CONTACT_CONSTRAINTS = "Local phone number should be 8 digits only"; + + private String contact; + + // empty constructor for json reconstruction + public Contact() { + } + + public Contact(String contact) { + if (!contact.matches(REGEX_CONTACT)) { + throw new IllegalArgumentException(CONTACT_CONSTRAINTS); + } + this.contact = contact; + } + + public String getContact() { + return contact; + } + + @Override + public String toString() { + return contact; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Contact // instanceof handles nulls + && contact.equals(((Contact) other).getContact())); // state check + } + + public static boolean isValidContact(String string) { + return string.matches(REGEX_CONTACT); + } +} diff --git a/src/main/java/quickdocs/model/patient/Dob.java b/src/main/java/quickdocs/model/patient/Dob.java new file mode 100644 index 000000000000..e150547773cf --- /dev/null +++ b/src/main/java/quickdocs/model/patient/Dob.java @@ -0,0 +1,84 @@ +package quickdocs.model.patient; + +import java.time.LocalDate; +import java.util.Arrays; +import java.util.Calendar; +import java.util.List; + +/** + * Represents the patient's date of birth in the YYYY-MM-DD format + */ +public class Dob { + + public static final String REGEX_DOB = "[0-9]{4}-(0?[1-9]|1[012])-(0?[1-9]|[12][0-9]|3[01])"; + public static final String DOB_CONSTRAINTS = "Date of Birth should be in YYYY-MM-DD format"; + public static final String FEBURARY_CONSTRAINT = "Feburary only have 28 or 29 days"; + public static final String LEAPYEAR_CONSTRAINT = "Only leap years have 29 Feb"; + public static final String THIRTYFIRST_CONSTRAINT = "The month entered does not have a 31st day"; + public static final String FUTURE_YEAR = "The entered date of birth is in the future and is invalid"; + public static final String MINIMUM_YEAR = "The entered year is over 100 years ago and is invalid"; + + private LocalDate dob; + + // empty constructor for json reconstruction + public Dob() { + } + + public Dob(String dob) { + if (!dob.matches(REGEX_DOB)) { + throw new IllegalArgumentException(DOB_CONSTRAINTS); + } + + String[] splittedDob = dob.split("-"); + boolean isLeapYear = false; + String month = splittedDob[1]; + String day = splittedDob[2]; + int year = Integer.valueOf(splittedDob[0]); + + if (year > Calendar.getInstance().get(Calendar.YEAR)) { + throw new IllegalArgumentException(FUTURE_YEAR); + } + + if (year < 1900) { + throw new IllegalArgumentException(MINIMUM_YEAR); + } + + if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) { + isLeapYear = true; + } + + // to handle when dob 30/2 + if (Integer.valueOf(day) > 29 && Integer.valueOf(month) == 2) { + throw new IllegalArgumentException(FEBURARY_CONSTRAINT); + } + + // not a leap year but 29/2 + if (!isLeapYear && Integer.valueOf(month) == 2 && Integer.valueOf(day) > 28) { + throw new IllegalArgumentException(LEAPYEAR_CONSTRAINT); + } + + // check if the date entered that is 31st can actually fall on the months with 31 days + List monthsWith31Days = Arrays.asList(1, 3, 5, 7, 8, 10, 12); + if (!monthsWith31Days.contains(Integer.valueOf(month)) && Integer.valueOf(day) == 31) { + throw new IllegalArgumentException(THIRTYFIRST_CONSTRAINT); + } + + this.dob = LocalDate.parse(dob); + } + + public LocalDate getDob() { + return dob; + } + + @Override + public String toString() { + return dob.toString(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Dob // instanceof handles nulls + && dob.toString().equals(((Dob) other).getDob().toString())); // state check + } +} diff --git a/src/main/java/quickdocs/model/patient/Email.java b/src/main/java/quickdocs/model/patient/Email.java new file mode 100644 index 000000000000..4db4a028a137 --- /dev/null +++ b/src/main/java/quickdocs/model/patient/Email.java @@ -0,0 +1,47 @@ +package quickdocs.model.patient; + +import java.util.regex.Pattern; + +/** + * Represents email address of a particular patient + */ +public class Email { + + public static final String REGEX_EMAIL = "^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,6}$"; + public static final String EMAIL_CONSTRAINTS = + "Emails should follow standard email convention: username@domain"; + + private String email; + + // empty constructor for json reconstruction + public Email() { + } + + public Email(String email) { + if (!Pattern.compile(REGEX_EMAIL, Pattern.CASE_INSENSITIVE).matcher(email).find()) { + throw new IllegalArgumentException(EMAIL_CONSTRAINTS); + } + + this.email = email; + } + + public String getEmail() { + return email; + } + + @Override + public String toString() { + return email; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Email // instanceof handles nulls + && email.equals(((Email) other).getEmail())); // state check + } + + public static boolean isValidEmail(String string) { + return Pattern.compile(REGEX_EMAIL, Pattern.CASE_INSENSITIVE).matcher(string).matches(); + } +} diff --git a/src/main/java/quickdocs/model/patient/Gender.java b/src/main/java/quickdocs/model/patient/Gender.java new file mode 100644 index 000000000000..83cd33a5d35e --- /dev/null +++ b/src/main/java/quickdocs/model/patient/Gender.java @@ -0,0 +1,38 @@ +package quickdocs.model.patient; + +/** + * Represent the gender of the patient + */ +public class Gender { + public static final String REGEX_GENDER = "[MF]"; + public static final String GENDER_CONSTRAINTS = "Only 2 gender, M or F allowed"; + + private String gender; + + // empty constructor for json reconstruction + public Gender() { + } + + public Gender (String gender) { + if (!gender.matches(REGEX_GENDER)) { + throw new IllegalArgumentException(GENDER_CONSTRAINTS); + } + this.gender = gender; + } + + public String getGender() { + return gender; + } + + @Override + public String toString() { + return gender; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Gender // instanceof handles nulls + && gender.equals(((Gender) other).getGender())); // state check + } +} diff --git a/src/main/java/quickdocs/model/patient/Name.java b/src/main/java/quickdocs/model/patient/Name.java new file mode 100644 index 000000000000..021ff56e56fb --- /dev/null +++ b/src/main/java/quickdocs/model/patient/Name.java @@ -0,0 +1,44 @@ +package quickdocs.model.patient; + +/** + * Represents the full name of the person in the patient record + */ +public class Name { + + public static final String REGEX_NAME = "[\\p{Alnum}][\\p{Alnum} ]*"; + public static final String NAME_CONSTRAINTS = + "Names should only contain alphanumeric characters and spaces but not blank"; + + private String name; + + // empty constructor for json reconstruction + public Name() { + } + + public Name (String name) { + if (!name.matches(REGEX_NAME)) { + throw new IllegalArgumentException(NAME_CONSTRAINTS); + } + this.name = name; + } + + public String getName() { + return name; + } + + @Override + public String toString() { + return name; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Name // instanceof handles nulls + && name.equals(((Name) other).getName())); // state check + } + + public static boolean isValidName(String string) { + return string.matches(REGEX_NAME); + } +} diff --git a/src/main/java/quickdocs/model/patient/Nric.java b/src/main/java/quickdocs/model/patient/Nric.java new file mode 100644 index 000000000000..2d0d5d955a83 --- /dev/null +++ b/src/main/java/quickdocs/model/patient/Nric.java @@ -0,0 +1,42 @@ +package quickdocs.model.patient; + +/** + * Represents NRIC of the patient record + */ +public class Nric { + public static final String REGEX_NRIC1 = "^[ST]\\d{7}[ABCDEFGHIZJ]"; + public static final String REGEX_NRIC2 = "^[FG]\\d{7}[KLMNPQRTUWX]$"; + public static final String NRIC_CONSTRAINTS = + "NRIC starts with S,T,U or G followed by 7 digits and must end with a valid letter\n" + + "For NRIC starting with S or T, they can only end with these letters: ABCDEFGHIZJ\n" + + "For NRIC starting with F or G, they can only end with these letters: KLMNPQRTUWX\n"; + + private String nric; + + // empty constructor for json reconstruction + public Nric() { + } + + public Nric(String nric) { + if (!nric.matches(REGEX_NRIC1) && !nric.matches(REGEX_NRIC2)) { + throw new IllegalArgumentException(NRIC_CONSTRAINTS); + } + this.nric = nric; + } + + public String getNric() { + return nric; + } + + @Override + public String toString() { + return nric; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Nric // instanceof handles nulls + && nric.equals(((Nric) other).getNric())); // state check + } +} diff --git a/src/main/java/quickdocs/model/patient/Patient.java b/src/main/java/quickdocs/model/patient/Patient.java new file mode 100644 index 000000000000..f750f712499d --- /dev/null +++ b/src/main/java/quickdocs/model/patient/Patient.java @@ -0,0 +1,102 @@ +package quickdocs.model.patient; + +import java.util.ArrayList; + +import quickdocs.model.tag.Tag; + +/** + * Represents a patient record in QuickDocs + */ +public class Patient { + + private Name name; + private Nric nric; + private Email email; + private Address address; + private Contact contact; + private Gender gender; + private Dob dob; + private ArrayList tagList; + + public Patient() { + } + + public Patient(Name name, Nric nric, Email email, Address address, Contact contact, + Gender gender, Dob dob, ArrayList tagList) { + this.name = name; + this.nric = nric; + this.email = email; + this.address = address; + this.contact = contact; + this.gender = gender; + this.dob = dob; + this.tagList = tagList; + } + + public Name getName() { + return name; + } + + public Nric getNric() { + return nric; + } + + public Email getEmail() { + return email; + } + + public Address getAddress() { + return address; + } + + public Contact getContact() { + return contact; + } + + public Gender getGender() { + return gender; + } + + public Dob getDob() { + return dob; + } + + public ArrayList getTagList() { + return tagList; + } + + /** + * Checks another Patient object, and if both have same nric, then its the + * same person + * Every inhabitant of Singapore have a unique NRIC, despite similar names + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Patient)) { + return false; + } + + Patient otherPatient = (Patient) other; + return otherPatient.getNric().equals(nric); + } + + @Override + public String toString() { + + StringBuilder sb = new StringBuilder(); + sb.append("Patient details for: " + nric + "\n"); + sb.append("Name: " + name + "\n"); + sb.append("Date of Birth: " + dob + "\n"); + sb.append("Gender: " + gender + "\n"); + sb.append("Contact: " + contact + "\n"); + sb.append("Email: " + email + "\n"); + sb.append("Address: " + address + "\n"); + sb.append("Tags: " + tagList.toString() + "\n"); + + return sb.toString(); + } +} diff --git a/src/main/java/quickdocs/model/patient/PatientEditedFields.java b/src/main/java/quickdocs/model/patient/PatientEditedFields.java new file mode 100644 index 000000000000..52f80377bae6 --- /dev/null +++ b/src/main/java/quickdocs/model/patient/PatientEditedFields.java @@ -0,0 +1,160 @@ +package quickdocs.model.patient; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Optional; + +import quickdocs.model.tag.Tag; + +/** + * Stores changes to patient records during an EditPatientCommand + * All fields start out as null, and values can be assigned to the attributes using the + * setter methods. + * + * During edit when the PatientEditedFields is compared with, the non-null values will + * represent the changed attributes, and will be used to update the existing Patient Object. + */ +public class PatientEditedFields { + + private Name name; + private Nric nric; + private Email email; + private Address address; + private Contact contact; + private Gender gender; + private Dob dob; + private ArrayList tagList; + + public Optional getName() { + return Optional.ofNullable(name); + } + + public void setName(Name name) { + this.name = name; + } + + public Optional getNric() { + return Optional.ofNullable(nric); + } + + public void setNric(Nric nric) { + this.nric = nric; + } + + public Optional getEmail() { + return Optional.ofNullable(email); + } + + public void setEmail(Email email) { + this.email = email; + } + + public Optional
getAddress() { + return Optional.ofNullable(address); + } + + public void setAddress(Address address) { + this.address = address; + } + + public Optional getContact() { + return Optional.ofNullable(contact); + } + + public void setContact(Contact contact) { + this.contact = contact; + } + + public Optional getGender() { + return Optional.ofNullable(gender); + } + + public void setGender(Gender gender) { + this.gender = gender; + } + + public Optional getDob() { + return Optional.ofNullable(dob); + } + + public void setDob(Dob dob) { + this.dob = dob; + } + + public Optional> getTagList() { + return Optional.ofNullable(tagList); + } + + public void setTagList(ArrayList tagList) { + this.tagList = tagList; + } + + /** + * Checks if PatientEditedFields is empty, which means that no changes were made + */ + public boolean checkEmpty() { + return name == null && nric == null && email == null && address == null + && contact == null && gender == null && dob == null && tagList == null; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof PatientEditedFields)) { + return false; + } + + PatientEditedFields otherPatient = (PatientEditedFields) other; + return checkNonNullFields((PatientEditedFields) other); + } + + /** + * Checks if two PatientEditedFields are the same using non null fields + * + * @param other Another patientEditedFields object to test similarity + * @return boolean value to indicate whether the two PatientEditedFields are the same based on + * non null values + */ + public boolean checkNonNullFields(PatientEditedFields other) { + + if ((name != null && other.getName().isPresent()) && !name.equals(other.getName().get())) { + return false; + } + + if ((nric != null && other.getNric().isPresent()) && !nric.equals(other.getNric().get())) { + return false; + } + + if ((email != null && other.getEmail().isPresent()) && !email.equals(other.getEmail().get())) { + return false; + } + + if ((address != null && other.getAddress().isPresent()) && !address.equals(other.getAddress().get())) { + return false; + } + + if ((contact != null && other.getContact().isPresent()) && !contact.equals(other.getContact().get())) { + return false; + } + + if ((gender != null && other.getGender().isPresent()) && !gender.equals(other.getGender().get())) { + return false; + } + + if ((dob != null && other.getDob().isPresent()) && !dob.equals(other.getDob().get())) { + return false; + } + + if (tagList != null && other.getTagList().isPresent()) { + if (!Arrays.equals(tagList.toArray(), other.getTagList().get().toArray())) { + return false; + } + } + + return true; + } + +} diff --git a/src/main/java/quickdocs/model/patient/PatientManager.java b/src/main/java/quickdocs/model/patient/PatientManager.java new file mode 100644 index 000000000000..9e8f7ae10aee --- /dev/null +++ b/src/main/java/quickdocs/model/patient/PatientManager.java @@ -0,0 +1,309 @@ +package quickdocs.model.patient; + +import java.util.ArrayList; +import java.util.Optional; + +import quickdocs.model.tag.Tag; + +/** + * Handle all operations involving the models of patient module + * including adding, editing, listing and deleting of patient records + */ +public class PatientManager { + + private ArrayList patientList; + + public PatientManager() { + this.patientList = new ArrayList(); + } + + + //==========Patient addition methods================================================= + + /** + * Returns true if patient record to be added have a conflicting NRIC with + * another existing patient record. + * + * @param patient the record to be added + * @return boolean value of the NRIC check + */ + public boolean isDuplicatePatient(Patient patient) { + for (int i = 0; i < patientList.size(); i++) { + if (patientList.get(i).getNric().equals(patient.getNric())) { + return true; + } + } + return false; + } + + public void addPatient(Patient patient) { + patientList.add(patient); + } + + public ArrayList getPatientList() { + return patientList; + } + + //==========Patient edit methods================================================= + + public boolean isPatientListEmpty() { + return patientList.size() < 1; + } + + /** + * Returns true if index entered by user can be used to retrieve patient records from + * the patientList. + * If the index is out of patientList's bounds or is negative, returns false. + * + * @param index index entered by user to view patient record + */ + public boolean checkValidIndex(int index) { + if (index >= patientList.size()) { + return false; + } + + if (index < 0) { + return false; + } + + return true; + } + + public Patient getPatientAtIndex(int index) { + return patientList.get(index); + } + + /** + * Returns true if the edited patient will have the same NRIC as another + * patient record. + * + * @param index index of the patient record to be edited, used to prevent checking the NRIC of + * the current edited patient record + * @param editedPatient patient record that is currently being edited + * @return true if NRIC of the selected patient can be edited without causing a conflict + */ + public boolean checkDuplicatePatientAfterEdit(int index, Patient editedPatient) { + for (int i = 0; i < patientList.size(); i++) { + if (i == index) { + continue; + } + + if (patientList.get(i).getNric().equals(editedPatient.getNric())) { + return true; + } + } + return false; + } + + public void replacePatient(int index, Patient editedPatient) { + patientList.set(index, editedPatient); + } + + + // listing methods + + /** + * Returns the patient record details whose name matches the search sequence the user have + * entered. If there are more than 1 patient whose name matches the search sequence, then + * return the patients' index, name and nric so that the user can filter down the search + * even more + * + * @param searchSequence full name or part of a name to narrow down searching of patient records + * @return either the full patient record of a single patient, or a list of patients + */ + public String findPatientsByName(String searchSequence) { + + // the foundPatients are used to store all + // the patient records whose name contains the searchSequence. + ArrayList foundPatients = new ArrayList<>(); + + // store the index of the patient records that are found + ArrayList foundPatientsIndexes = new ArrayList<>(); + for (int i = 0; i < patientList.size(); i++) { + Patient patient = patientList.get(i); + + if (patient.getName().toString().toLowerCase().contains(searchSequence.toLowerCase())) { + foundPatients.add(patient); + foundPatientsIndexes.add(i + 1); + } + + } + + if (foundPatients.size() == 0) { + return "No patient record found"; + } + + // if there are more than 1 patients, then list all the patients + // whose name matches the searchSequence for the user to narrow down + // the search even more + if (foundPatients.size() > 1) { + return formatMultiplePatients(foundPatients, foundPatientsIndexes); + } + + return foundPatients.get(0).toString(); + + } + + /** + * Returns the details of a single patient record whose NRIC matches the searchSequence + * or a list of patients along with their indexes, name and NRIC if their NRIC matches + * the searchSequence + * + * @param searchSequence A part or the full NRIC sequence + * @return either the full patient record of a single patient, or a list of patients + */ + public String findPatientsByNric(String searchSequence) { + ArrayList foundPatients = new ArrayList<>(); + ArrayList foundPatientsIndexes = new ArrayList<>(); + for (int i = 0; i < patientList.size(); i++) { + Patient patient = patientList.get(i); + + if (patient.getNric().toString().toLowerCase() + .matches("^" + searchSequence.toLowerCase() + ".*")) { + foundPatients.add(patient); + foundPatientsIndexes.add(i + 1); + } + } + + if (foundPatients.size() == 0) { + return "No patient record found"; + } + + if (foundPatients.size() > 1) { + return formatMultiplePatients(foundPatients, foundPatientsIndexes); + } + + return foundPatients.get(0).toString(); + + } + + /** + * Returns the first fifty patient's index, nric and name + * when a patient search have no parameters + */ + public String listFiftyPatients() { + ArrayList foundPatients = new ArrayList<>(); + ArrayList foundPatientsIndexes = new ArrayList<>(); + for (int i = 0; i < 49; i++) { + if (i >= patientList.size()) { + break; + } + Patient patient = patientList.get(i); + foundPatients.add(patient); + foundPatientsIndexes.add(i + 1); + } + return formatMultiplePatients(foundPatients, foundPatientsIndexes); + } + + /** + * For multiple patient records, a list of their names, nric, gender and dob will be displayed instead + * + * @param patients patient objects in modelmanager that fulfills the search criteria + * @param patientIndexes index where object is stored in patientlist + * @return formatted list to be displayed + */ + public static String formatMultiplePatients(ArrayList patients, ArrayList patientIndexes) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < patients.size(); i++) { + Patient patient = patients.get(i); + + sb.append(patientIndexes.get(i) + ") " + patient.getName() + + " " + patient.getNric() + + " " + patient.getGender() + + " " + patient.getDob() + + "\n" + ); + } + sb.append("\n"); + return sb.toString(); + } + + /** + * Returns a list of patients that have the same tag defined by the user + * + * @param tag to filter patient records with + * @return either a list of patients with the specified tag, or a single patient record + * if it is the only one that have the specified tag + */ + public String findPatientsByTag(Tag tag) { + ArrayList foundPatients = new ArrayList<>(); + ArrayList foundPatientsIndexes = new ArrayList<>(); + + for (int i = 0; i < patientList.size(); i++) { + Patient currentPatient = patientList.get(i); + if (currentPatient.getTagList().contains(tag)) { + foundPatients.add(currentPatient); + foundPatientsIndexes.add(i + 1); + } + } + + if (foundPatients.size() == 0) { + return "No patient record found"; + } + + if (foundPatients.size() > 1) { + return formatMultiplePatients(foundPatients, foundPatientsIndexes); + } + + return foundPatients.get(0).toString(); + } + + public Patient getPatientByNric(String nric) { + for (int i = 0; i < patientList.size(); i++) { + if (patientList.get(i).getNric().toString().equals(nric)) { + return patientList.get(i); + } + } + return null; + } + + public Optional getPatientByNric(Nric nric) { + for (Patient patient : patientList) { + if (patient.getNric().equals(nric)) { + return Optional.of(patient); + } + } + return Optional.empty(); + } + + public int getIndexByNric(Nric nric) { + for (int i = 0; i < patientList.size(); i++) { + if (patientList.get(i).getNric().toString().equals(nric.toString())) { + return i; + } + } + return -1; + } + + /** + * Remove patient with nric specified + * + * @param nric of the patient to be deleted + */ + public void deletePatientByNric(String nric) { + for (int i = 0; i < patientList.size(); i++) { + if (patientList.get(i).getNric().toString().equals(nric)) { + patientList.remove(i); + break; + } + } + } + + @Override + public boolean equals(Object obj) { + // short circuit if same object + if (obj == this) { + return true; + } + + // instanceof handles nulls + if (!(obj instanceof PatientManager)) { + return false; + } + + // state check + PatientManager other = (PatientManager) obj; + return this.patientList.equals(other.patientList); + + } +} diff --git a/src/main/java/quickdocs/model/record/ConsultationRecord.java b/src/main/java/quickdocs/model/record/ConsultationRecord.java new file mode 100644 index 000000000000..4e86a9634d24 --- /dev/null +++ b/src/main/java/quickdocs/model/record/ConsultationRecord.java @@ -0,0 +1,47 @@ +package quickdocs.model.record; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.HashMap; + +import quickdocs.model.consultation.Diagnosis; +import quickdocs.model.consultation.Prescription; +import quickdocs.model.consultation.Symptom; + +/** + * Record representation of a consultation + */ +public class ConsultationRecord extends Record { + + private ArrayList prescriptions; + private Diagnosis diagnosis; + + public ConsultationRecord(ArrayList prescription, Diagnosis diagnosis) { + this.prescriptions = prescription; + this.diagnosis = diagnosis; + } + + @Override + public Statistics toStatistics(StatisticsManager statisticsManager) { + BigDecimal consultationFee = statisticsManager.getConsultationFee(); + BigDecimal prescriptionFee = BigDecimal.ZERO; + HashMap medicinesCount = new HashMap<>(); + + for (Prescription prescription : prescriptions) { + int quantity = prescription.getQuantity(); + medicinesCount.put(prescription.getMedicine().name, quantity); + BigDecimal medicinePrice = prescription.getMedicine().getPrice(); + if (medicinePrice != null) { + prescriptionFee = prescriptionFee.add(medicinePrice.multiply(new BigDecimal(quantity))); + } + } + + HashMap symptomsCount = new HashMap<>(); + for (Symptom symptom : diagnosis.getSymptoms()) { + symptomsCount.put(symptom.toString(), 1); + } + + return new Statistics(1, consultationFee.add(prescriptionFee), BigDecimal.ZERO, + medicinesCount, symptomsCount); + } +} diff --git a/src/main/java/quickdocs/model/record/MedicinePurchaseRecord.java b/src/main/java/quickdocs/model/record/MedicinePurchaseRecord.java new file mode 100644 index 000000000000..52f19d0ac926 --- /dev/null +++ b/src/main/java/quickdocs/model/record/MedicinePurchaseRecord.java @@ -0,0 +1,39 @@ +package quickdocs.model.record; + +import java.math.BigDecimal; + +import quickdocs.model.medicine.Medicine; + +/** + * Record representation of a MedicinePurchase + */ +public class MedicinePurchaseRecord extends Record { + private final Medicine medicine; + private final int quantity; + private final BigDecimal cost; + + public MedicinePurchaseRecord(Medicine medicine, int quantity) { + if (medicine.getPrice() == null) { + throw new NullPointerException("Price not set"); + } + if (quantity <= 0 || medicine.getPrice().compareTo(BigDecimal.ZERO) == -1) { + throw new IllegalArgumentException("Invalid arguments"); + } + this.medicine = medicine; + this.quantity = quantity; + this.cost = medicine.getPrice(); + } + public MedicinePurchaseRecord(Medicine medicine, int quantity, BigDecimal cost) { + if (quantity <= 0 || cost.compareTo(BigDecimal.ZERO) == -1) { + throw new IllegalArgumentException("Invalid arguments"); + } + this.medicine = medicine; + this.quantity = quantity; + this.cost = cost; + } + @Override + public Statistics toStatistics(StatisticsManager statisticsManager) { + BigDecimal totalCost = cost.multiply(new BigDecimal(quantity)); + return new Statistics(0, BigDecimal.ZERO, totalCost); + } +} diff --git a/src/main/java/quickdocs/model/record/MonthStatistics.java b/src/main/java/quickdocs/model/record/MonthStatistics.java new file mode 100644 index 000000000000..9aef8344b04b --- /dev/null +++ b/src/main/java/quickdocs/model/record/MonthStatistics.java @@ -0,0 +1,47 @@ +package quickdocs.model.record; + +import java.time.YearMonth; + +/** + * This class holds the Statistics for a specified month + */ +public class MonthStatistics { + private YearMonth yearMonth; + private Statistics stats; + + public MonthStatistics() { + } + public MonthStatistics(YearMonth yearMonth) { + this.yearMonth = yearMonth; + this.stats = new Statistics(); + } + public MonthStatistics(YearMonth yearMonth, Statistics stats) { + this.yearMonth = yearMonth; + this.stats = stats; + } + + /** + * Merges the statistics of a single Record object to the MonthStatistics + */ + public void addRecord(Record record, StatisticsManager statisticsManager) { + Statistics toMerge = record.toStatistics(statisticsManager); + this.stats = this.stats.merge(toMerge); + } + public Statistics getStatistics() { + return this.stats; + } + public YearMonth getYearMonth() { + return this.yearMonth; + } + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof MonthStatistics)) { + return false; + } + return this.yearMonth.equals(((MonthStatistics) other).getYearMonth()) + && this.stats.equals(((MonthStatistics) other).getStatistics()); + } +} diff --git a/src/main/java/quickdocs/model/record/Record.java b/src/main/java/quickdocs/model/record/Record.java new file mode 100644 index 000000000000..a268e275d7ca --- /dev/null +++ b/src/main/java/quickdocs/model/record/Record.java @@ -0,0 +1,9 @@ +package quickdocs.model.record; + +/** + * Abstract class for more specific record class to extend from. + * Ensures that subclasses can be recorded by implementing record(). + */ +public abstract class Record { + public abstract Statistics toStatistics(StatisticsManager statisticsManager); +} diff --git a/src/main/java/quickdocs/model/record/Statistics.java b/src/main/java/quickdocs/model/record/Statistics.java new file mode 100644 index 000000000000..9375070fced1 --- /dev/null +++ b/src/main/java/quickdocs/model/record/Statistics.java @@ -0,0 +1,179 @@ +package quickdocs.model.record; + +import java.math.BigDecimal; +import java.text.NumberFormat; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * This class holds the statistics relevant to the private clinic's needs + * A Statistics object is immutable. + */ +public class Statistics { + private int noOfConsultations; + private BigDecimal revenue; + private BigDecimal expenditure; + private BigDecimal profit; + private HashMap medicinesCount; + private HashMap symptomsCount; + public Statistics() { + this.noOfConsultations = 0; + this.revenue = BigDecimal.ZERO; + this.expenditure = BigDecimal.ZERO; + this.profit = BigDecimal.ZERO; + this.medicinesCount = new HashMap<>(); + this.symptomsCount = new HashMap<>(); + } + public Statistics(int noOfConsultations, BigDecimal revenue, BigDecimal expenditure) { + if (noOfConsultations < 0 || revenue.compareTo(BigDecimal.ZERO) == -1 + || expenditure.compareTo(BigDecimal.ZERO) == -1) { + throw new IllegalArgumentException("Invalid arguments"); + } + this.noOfConsultations = noOfConsultations; + this.revenue = revenue; + this.expenditure = expenditure; + this.profit = revenue.subtract(expenditure); + this.medicinesCount = new HashMap<>(); + this.symptomsCount = new HashMap<>(); + } + public Statistics(int noOfConsultations, BigDecimal revenue, BigDecimal expenditure, + HashMap medicinesCount, HashMap symptomsCount) { + if (noOfConsultations < 0 || revenue.compareTo(BigDecimal.ZERO) == -1 + || expenditure.compareTo(BigDecimal.ZERO) == -1) { + throw new IllegalArgumentException("Invalid arguments"); + } + if (medicinesCount == null || symptomsCount == null) { + throw new NullPointerException(); + } + this.noOfConsultations = noOfConsultations; + this.revenue = revenue; + this.expenditure = expenditure; + this.profit = revenue.subtract(expenditure); + this.medicinesCount = medicinesCount; + this.symptomsCount = symptomsCount; + } + + public Statistics(Statistics stats) { + this.noOfConsultations = stats.getNoOfConsultations(); + this.revenue = stats.getRevenue(); + this.expenditure = stats.getExpenditure(); + this.profit = stats.getProfit(); + this.medicinesCount = stats.getMedicinesCount(); + this.symptomsCount = stats.getSymptomsCount(); + } + + public int getNoOfConsultations() { + return noOfConsultations; + } + + public BigDecimal getRevenue() { + return revenue; + } + + public BigDecimal getExpenditure() { + return expenditure; + } + + public BigDecimal getProfit() { + return profit; + } + + public HashMap getMedicinesCount() { + return this.medicinesCount; + } + + public HashMap getSymptomsCount() { + return this.symptomsCount; + } + + /** + * Merges this Statistics object with another Statistic object + * @param other Statistics to merge with + * @return A new Statistics object + */ + public Statistics merge(Statistics other) { + int newNoOfConsultations = this.getNoOfConsultations() + other.getNoOfConsultations(); + BigDecimal newRevenue = this.getRevenue().add(other.getRevenue()); + BigDecimal newExpenditure = this.getExpenditure().add(other.getExpenditure()); + HashMap newMedicinesCount = this.getMedicinesCount(); + HashMap newSymptomsCount = this.getSymptomsCount(); + other.getMedicinesCount().forEach((k, v) -> newMedicinesCount.merge(k, v, Integer::sum)); + other.getSymptomsCount().forEach((k, v) -> newSymptomsCount.merge(k, v, Integer::sum)); + return new Statistics(newNoOfConsultations, newRevenue, newExpenditure, newMedicinesCount, newSymptomsCount); + } + + public static String getMostCommonKeyFromHashMap(HashMap hashMap) { + if (hashMap == null) { + throw new NullPointerException(); + } + if (hashMap.isEmpty()) { + return "N/A"; + } + StringBuilder sb = new StringBuilder(); + int maxValue = Collections.max(hashMap.values()); + for (Map.Entry entry : hashMap.entrySet()) { + if (entry.getValue() == maxValue) { + sb.append(entry.getKey()) + .append(": ") + .append(entry.getValue()) + .append("\n"); + } + } + + return sb.toString(); + } + + /** + * Function to format BigDecimal objects to the locale's currency format. + * @param money BigDecimal object representing any amount of money. + * @return A String representation of the money formatted to the locale's currency format. + */ + public static String currencyFormat(BigDecimal money) { + /** + * Adapted from https://stackoverflow.com/questions/3395825/how-to-print-formatted-bigdecimal-values/8581941 + */ + return NumberFormat.getCurrencyInstance().format(money); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("Number of consultations: ") + .append(getNoOfConsultations()) + .append("\n\n") + .append("Most common medicine prescribed: \n") + .append(Statistics.getMostCommonKeyFromHashMap(medicinesCount)) + .append("\n") + .append("Most common symptom diagnosed: \n") + .append(Statistics.getMostCommonKeyFromHashMap(symptomsCount)) + .append("\n") + .append("Revenue: ") + .append(Statistics.currencyFormat(getRevenue())) + .append("\n") + .append("Expenditure: ") + .append(Statistics.currencyFormat(getExpenditure())) + .append("\n") + .append("Profit: ") + .append(Statistics.currencyFormat(getProfit())) + .append("\n\n"); + return sb.toString(); + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof Statistics)) { + return false; + } + Statistics stats = (Statistics) other; + return this.getNoOfConsultations() == stats.getNoOfConsultations() + && (this.getRevenue().compareTo(stats.getRevenue()) == 0) + && (this.getExpenditure().compareTo(stats.getExpenditure()) == 0) + && (this.getProfit().compareTo(stats.getProfit()) == 0) + && this.getMedicinesCount().equals(stats.getMedicinesCount()) + && this.getSymptomsCount().equals(stats.getSymptomsCount()); + } +} diff --git a/src/main/java/quickdocs/model/record/StatisticsManager.java b/src/main/java/quickdocs/model/record/StatisticsManager.java new file mode 100644 index 000000000000..b931e47a5abf --- /dev/null +++ b/src/main/java/quickdocs/model/record/StatisticsManager.java @@ -0,0 +1,121 @@ +package quickdocs.model.record; + +import java.math.BigDecimal; +import java.time.Clock; +import java.time.YearMonth; +import java.util.ArrayList; +import java.util.List; + +/** + * Manager for the Statistics objects, segmented in months + */ +public class StatisticsManager { + + public static final BigDecimal DEFAULT_CONSULTATION_FEE = BigDecimal.valueOf(30.00); + public static final YearMonth START_DATE = YearMonth.of(2019, 1); + + private static final int NUMBER_OF_MONTHS_IN_A_YEAR = 12; + + private BigDecimal consultationFee; + private List monthStatistics; + + public StatisticsManager() { + consultationFee = DEFAULT_CONSULTATION_FEE; + monthStatistics = new ArrayList<>(); + } + + public BigDecimal getConsultationFee() { + return this.consultationFee; + } + + /** + * Sets the consultation fee of the clinic. Must be a non-negative BigDecimal value. + * @param cost A non-negative BigDecimal value + */ + public void setConsultationFee(BigDecimal cost) { + if (cost.compareTo(BigDecimal.ZERO) == -1) { + throw new IllegalArgumentException("Consultation Fee cannot be a negative number"); + } + consultationFee = cost; + } + + /** + * Adds the statistics of the record to the storage by merging, sorted to months. + * @param record Record object for merging of statistics + * @param clock Clock object to obtain the month and year of when the record is recorded. + */ + public void record(Record record, Clock clock) { + int idx = getYearMonthIndex(YearMonth.now(clock)); + if (idx < 0) { + throw new IllegalArgumentException("System clock is before January 2019"); + } + this.updateListSize(clock); + this.monthStatistics.get(idx).addRecord(record, this); + } + private int getYearMonthIndex(YearMonth now) { + return ((now.getYear() - START_DATE.getYear()) * NUMBER_OF_MONTHS_IN_A_YEAR) + + (now.getMonthValue() - START_DATE.getMonthValue()); + } + /** + * Updates the ArrayList of Statistics to the proper size according to the current time. + * @param clock Clock used to get the month and year from + */ + private void updateListSize(Clock clock) { + YearMonth now = YearMonth.now(clock); + if (now.isBefore(START_DATE)) { + throw new IllegalArgumentException("System clock is before January 2019"); + } + updateListSize(now); + } + + /** + * Updates the ArrayList of Statistics to the proper size according to the yearMonth. + */ + private void updateListSize(YearMonth yearMonth) { + int expectedSize = getYearMonthIndex(yearMonth) + 1; + int sizeDifference = expectedSize - monthStatistics.size(); + if (sizeDifference > 0) { + for (int i = sizeDifference - 1; i >= 0; i--) { + YearMonth toAdd = yearMonth.minusMonths((long) i); + this.monthStatistics.add(new MonthStatistics(toAdd)); + } + } + } + + public Statistics getStatistics(YearMonth from, YearMonth to) { + Statistics stats = new Statistics(); + int fromIdx = getYearMonthIndex(from); + int toIdx = getYearMonthIndex(to); + // check if the queried indexes are in range + if (fromIdx < 0 || toIdx < 0 || fromIdx > monthStatistics.size() - 1 || toIdx > monthStatistics.size() - 1 + || toIdx < fromIdx) { + throw new IllegalArgumentException("Invalid MMYY range"); + } + for (int idx = fromIdx; idx <= toIdx; idx++) { + stats = stats.merge(monthStatistics.get(idx).getStatistics()); + } + return stats; + } + + public List getMonthStatisticsList() { + return this.monthStatistics; + } + + public int getMonthStatisticsListSize() { + return this.monthStatistics.size(); + } + + /** + * Adds a MonthStatistics object to the StatisticsManager if it does not exist + */ + public void addMonthStatistics(MonthStatistics monthStatistics) { + YearMonth yearMonth = monthStatistics.getYearMonth(); + YearMonth currentMonth = YearMonth.now(Clock.systemDefaultZone()); + if (yearMonth.isBefore(START_DATE) || yearMonth.isAfter(currentMonth)) { + throw new IllegalArgumentException("Invalid stored statistics"); + } + updateListSize(yearMonth); + int idx = getYearMonthIndex(yearMonth); + this.monthStatistics.set(idx, monthStatistics); + } +} diff --git a/src/main/java/quickdocs/model/reminder/Reminder.java b/src/main/java/quickdocs/model/reminder/Reminder.java new file mode 100644 index 000000000000..60147075a886 --- /dev/null +++ b/src/main/java/quickdocs/model/reminder/Reminder.java @@ -0,0 +1,98 @@ +package quickdocs.model.reminder; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.Objects; + +import quickdocs.model.Slot; + +/** + * Represents a Reminder created in QuickDocs. + */ +public class Reminder extends Slot { + private String title; + private String comment; + + public Reminder() { + super(); + } + + public Reminder(String title, LocalDate date, LocalTime start) { + super(date, start, null); + this.title = title; + this.comment = ""; + } + + public Reminder(String title, String comment, LocalDate date, LocalTime start) { + super(date, start, null); + this.title = title; + this.comment = comment; + } + + public Reminder(String title, LocalDate date, LocalTime start, LocalTime end) { + super(date, start, end); + this.title = title; + this.comment = ""; + } + + public Reminder(String title, String comment, LocalDate date, LocalTime start, LocalTime end) { + super(date, start, end); + this.title = title; + this.comment = comment; + + } + + public String getTitle() { + return title; + } + + public String getComment() { + return comment; + } + + /** + * Returns true if both reminders have the same identity and data fields. + * This defines a stronger notion of equality between two reminders. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Reminder)) { + return false; + } + + Reminder otherReminder = (Reminder) other; + // Objects.equals() to handle null fields + return super.equals(other) + && otherReminder.title.equals(title) + && Objects.equals(otherReminder.comment, comment); + } + + @Override + public int hashCode() { + return Objects.hash(getDate(), getStart(), getEnd(), title, comment); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append(getTitle()).append(":\n") + .append("Date: ") + .append(getDate()).append("\n") + .append("Time: ") + .append(getStart()); + + if (getEnd() != null) { + builder.append(" to ").append(getEnd()); + } + builder.append("\n"); + + if (comment != null) { + builder.append("Comments: ").append(getComment()).append("\n"); + } + return builder.toString(); + } +} diff --git a/src/main/java/quickdocs/model/reminder/ReminderManager.java b/src/main/java/quickdocs/model/reminder/ReminderManager.java new file mode 100644 index 000000000000..537719f34052 --- /dev/null +++ b/src/main/java/quickdocs/model/reminder/ReminderManager.java @@ -0,0 +1,166 @@ +package quickdocs.model.reminder; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import quickdocs.model.appointment.Appointment; +import quickdocs.model.medicine.Medicine; + +/** + * Manages the list of {@code Reminders} created. + */ +public class ReminderManager { + private final List reminders; + private final ObservableList internalList; + private final ObservableList internalUnmodifiableList; + + public ReminderManager() { + reminders = new ArrayList<>(); + internalList = FXCollections.observableArrayList(); + internalUnmodifiableList = FXCollections.unmodifiableObservableList(internalList); + } + + public List getReminderList() { + return reminders; + } + + public ObservableList getObservableReminderList() { + return internalUnmodifiableList; + } + + /** + * Finds and returns the {@code Reminder} created for the given {@code Appointment} in the list of reminders, + * if it exists. + * + * @param appointment the {@code Appointment}'s {@code Reminder} to find. + * @return the {@code Reminder} found, if it exists, else returns {@code Optional.empty()}. + */ + public Optional getReminder(Appointment appointment) { + String title = appointment.createTitle(); + LocalDate date = appointment.getDate(); + LocalTime start = appointment.getStart(); + + List filtered = reminders.stream() + .filter(r -> r.getTitle().equals(title)) + .filter(r -> r.getDate().equals(date)) + .filter(a -> a.getStart().equals(start)) + .collect(Collectors.toList()); + + // filtered cannot contain > 1 reminder as each reminder for an appointment is uniquely identified by its + // title, date, and start fields. + assert filtered.size() <= 1; + + if (filtered.isEmpty()) { + return Optional.empty(); + } else { + return Optional.of(filtered.get(0)); + } + } + + /** + * Adds a {@code Reminder} to the ordered list of reminders, in the correct position. + * + * @param toAdd the {@code Reminder} to be added. + */ + public void addReminder(Reminder toAdd) { + assert !hasDuplicateReminder(toAdd); + + if (reminders.isEmpty()) { + reminders.add(toAdd); + internalList.add(toAdd); + return; + } + + // place reminder in correct position + for (Reminder rem : reminders) { + if (rem.compareTo(toAdd) > 0) { + int index = reminders.indexOf(rem); + reminders.add(index, toAdd); + internalList.add(index, toAdd); + return; + } + } + + // toAdd is to be placed at the end of the list + reminders.add(toAdd); + internalList.add(toAdd); + } + + public boolean hasDuplicateReminder(Reminder rem) { + return reminders.contains(rem); + } + + /** + * Deletes a {@code Reminder} in the list of reminders. + * + * @param reminder the {@code Reminder} to be deleted. + */ + public void delete(Reminder reminder) { + reminders.remove(reminder); + internalList.remove(reminder); + } + + /** + * Creates and adds a new {@code Reminder} when the given {@code Medicine}'s quantity falls below its threshold. + * + * @param medicine The {@code Medicine} to create a {@code Reminder} for. + */ + public void reminderForMedicine(Medicine medicine) { + if (medicine.isSufficient()) { + deleteExistingMedicineReminder(medicine); + } else { + String title = String.format(Medicine.REMINDER_TITLE_IF_INSUFFICIENT, medicine.name); + String comment = String.format(Medicine.REMINDER_COMMENT_IF_INSUFFICIENT, + medicine.getQuantity(), medicine.getThreshold()); + LocalDate date = LocalDate.now(); + LocalTime startTime = LocalTime.of(LocalTime.now().getHour(), LocalTime.now().getMinute(), + LocalTime.now().getSecond()); + Reminder reminder = new Reminder(title, comment, date, startTime, null); + deleteExistingMedicineReminder(medicine); + addReminder(reminder); + } + } + + /** + * Deletes any existing {@code Reminder} corresponding to the given {@code Medicine}, which was created + * when the {@code Medicine}'s quantity fell below its threshold. + * + * @param medicine the {@code Medicine} whose {@code Reminder} to search for. + * @return {@code true} if the {@code Reminder} was found and deleted, else returns {@code false}. + */ + public boolean deleteExistingMedicineReminder(Medicine medicine) { + String title = String.format(Medicine.REMINDER_TITLE_IF_INSUFFICIENT, medicine.name); + boolean changed = false; + int i = 0; + while (i < reminders.size()) { + Reminder exReminder = reminders.get(i); + if (exReminder.getTitle().equals(title)) { + delete(exReminder); + changed = true; + i--; + } + i++; + } + return changed; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof ReminderManager)) { + return false; + } + + ReminderManager otherManager = (ReminderManager) other; + return otherManager.reminders.equals(this.reminders); + } +} diff --git a/src/main/java/quickdocs/model/reminder/ReminderWithinDatesPredicate.java b/src/main/java/quickdocs/model/reminder/ReminderWithinDatesPredicate.java new file mode 100644 index 000000000000..8a0635432670 --- /dev/null +++ b/src/main/java/quickdocs/model/reminder/ReminderWithinDatesPredicate.java @@ -0,0 +1,33 @@ +package quickdocs.model.reminder; + +import java.time.LocalDate; +import java.util.function.Predicate; + +/** + * A {@code Predicate} to test that a {@code Reminder}'s date is within the given start and end date. + */ +public class ReminderWithinDatesPredicate implements Predicate { + private final LocalDate start; + private final LocalDate end; + + public ReminderWithinDatesPredicate(LocalDate start, LocalDate end) { + // extend the range by one day so we can use the isAfter() and isBefore() methods + // on the dates directly in the test() method + this.start = start.minusDays(1); + this.end = end.plusDays(1); + } + + @Override + public boolean test(Reminder reminder) { + LocalDate remDate = reminder.getDate(); + return remDate.isAfter(start) && remDate.isBefore(end); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ReminderWithinDatesPredicate // instanceof handles nulls + && start.equals(((ReminderWithinDatesPredicate) other).start) + && end.equals(((ReminderWithinDatesPredicate) other).end)); // state check + } +} diff --git a/src/main/java/quickdocs/model/reminder/exceptions/ReminderNotFoundException.java b/src/main/java/quickdocs/model/reminder/exceptions/ReminderNotFoundException.java new file mode 100644 index 000000000000..e54592cf8052 --- /dev/null +++ b/src/main/java/quickdocs/model/reminder/exceptions/ReminderNotFoundException.java @@ -0,0 +1,6 @@ +package quickdocs.model.reminder.exceptions; + +/** + * Signals that the operation is unable to find the specified reminder. + */ +public class ReminderNotFoundException extends RuntimeException {} diff --git a/src/main/java/seedu/address/model/tag/Tag.java b/src/main/java/quickdocs/model/tag/Tag.java similarity index 86% rename from src/main/java/seedu/address/model/tag/Tag.java rename to src/main/java/quickdocs/model/tag/Tag.java index b0ea7e7dad7f..b910390db288 100644 --- a/src/main/java/seedu/address/model/tag/Tag.java +++ b/src/main/java/quickdocs/model/tag/Tag.java @@ -1,7 +1,8 @@ -package seedu.address.model.tag; +package quickdocs.model.tag; import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; + +import quickdocs.commons.util.AppUtil; /** * Represents a Tag in the address book. @@ -21,10 +22,14 @@ public class Tag { */ public Tag(String tagName) { requireNonNull(tagName); - checkArgument(isValidTagName(tagName), MESSAGE_CONSTRAINTS); + AppUtil.checkArgument(isValidTagName(tagName), MESSAGE_CONSTRAINTS); this.tagName = tagName; } + public Tag() { + this.tagName = null; + } + /** * Returns true if a given string is a valid tag name. */ diff --git a/src/main/java/quickdocs/storage/JsonAdaptedAppointment.java b/src/main/java/quickdocs/storage/JsonAdaptedAppointment.java new file mode 100644 index 000000000000..d50f00f34157 --- /dev/null +++ b/src/main/java/quickdocs/storage/JsonAdaptedAppointment.java @@ -0,0 +1,96 @@ +package quickdocs.storage; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeParseException; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import quickdocs.model.appointment.Appointment; +import quickdocs.model.patient.Patient; + +/** + * Jackson-friendly version of {@link Appointment}. + */ +public class JsonAdaptedAppointment { + public static final String APP_MISSING_FIELD_MESSAGE_FORMAT = "Appointment's %s field is missing!"; + + private Patient patient; + private String date; + private String start; + private String end; + private String comment; + + /** + * Constructs a {@code JsonAdaptedAppointment} with the given {@code Appointment} details. + */ + @JsonCreator + public JsonAdaptedAppointment(@JsonProperty("patient") Patient patient, + @JsonProperty("date") String date, + @JsonProperty("start") String start, + @JsonProperty("end") String end, + @JsonProperty("comment") String comment) { + this.patient = patient; + this.date = date; + this.start = start; + this.end = end; + this.comment = comment; + } + + /** + * Converts a given {@code Appointment} into this class for Jackson use. + */ + public JsonAdaptedAppointment(Appointment source) { + patient = source.getPatient(); + date = source.getDate().toString(); + start = source.getStart().toString(); + end = source.getEnd().toString(); + comment = source.getComment(); + } + + /** + * Converts this Jackson-friendly adapted {@code Appointment} object into the model's {@code Appointment} object. + * + * @throws IllegalArgumentException if there were any data constraints violated for {@code Appointment} fields. + */ + public Appointment toModelType() throws IllegalArgumentException { + // check if any fields are missing + if (patient == null) { + throw new IllegalArgumentException(String.format(APP_MISSING_FIELD_MESSAGE_FORMAT, "Patient")); + } + if (date == null) { + throw new IllegalArgumentException(String.format(APP_MISSING_FIELD_MESSAGE_FORMAT, "Date")); + } + if (start == null) { + throw new IllegalArgumentException(String.format(APP_MISSING_FIELD_MESSAGE_FORMAT, "Start")); + } + if (end == null) { + throw new IllegalArgumentException(String.format(APP_MISSING_FIELD_MESSAGE_FORMAT, "End")); + } + if (comment == null) { + throw new IllegalArgumentException(String.format(APP_MISSING_FIELD_MESSAGE_FORMAT, "Comment")); + } + + Patient modelPatient = patient; + String modelComment = comment; + LocalDate modelDate; + LocalTime modelStart; + LocalTime modelEnd; + + try { + modelDate = LocalDate.parse(date); + } catch (DateTimeParseException e) { + throw new IllegalArgumentException("Date format: YYYY-MM-DD"); + } + + try { + modelStart = LocalTime.parse(start); + modelEnd = LocalTime.parse(end); + } catch (DateTimeParseException e) { + throw new IllegalArgumentException("Time format: HH:MM"); + } + + return new Appointment(modelPatient, modelDate, modelStart, modelEnd, modelComment); + } +} diff --git a/src/main/java/quickdocs/storage/JsonAdaptedConsultation.java b/src/main/java/quickdocs/storage/JsonAdaptedConsultation.java new file mode 100644 index 000000000000..20e3aa454ded --- /dev/null +++ b/src/main/java/quickdocs/storage/JsonAdaptedConsultation.java @@ -0,0 +1,86 @@ +package quickdocs.storage; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import quickdocs.commons.exceptions.IllegalValueException; +import quickdocs.model.consultation.Consultation; +import quickdocs.model.consultation.Diagnosis; +import quickdocs.model.consultation.Prescription; +import quickdocs.model.patient.Patient; + +/** + * Jackson-friendly version of {@link Consultation}. + */ +public class JsonAdaptedConsultation { + + private int index; + private LocalDateTime session; + + private Patient patient; + private Diagnosis diagnosis; + + private ArrayList prescriptions; + + /** + * Constructs a {@code JsonAdaptedPerson} with the given person details. + */ + @JsonCreator + public JsonAdaptedConsultation(@JsonProperty("index") int index, + @JsonProperty("session") LocalDateTime session, + @JsonProperty("patient") Patient patient, + @JsonProperty("diagnosis") Diagnosis diagnosis, + @JsonProperty("prescriptions") List prescriptions) { + this.index = index; + this.session = session; + this.patient = patient; + this.diagnosis = diagnosis; + + this.prescriptions = new ArrayList<>(); + if (prescriptions != null) { + this.prescriptions.addAll(prescriptions); + } + } + + /** + * Converts a given {@code Consultation} into this class for Jackson use. + */ + public JsonAdaptedConsultation(Consultation source) { + + this.index = source.getIndex(); + this.session = source.getSession(); + this.patient = source.getPatient(); + this.diagnosis = source.getDiagnosis(); + this.prescriptions = new ArrayList<>(); + this.prescriptions.addAll(source.getPrescriptions()); + } + + /** + * Converts this Jackson-friendly adapted person object into the model's {@code Person} object. + * + * @return The Consultation model object + * @throws IllegalValueException if there were any data constraints violated in the tags. + * @throws IllegalArgumentException if there were any data constraints violated for patient fields. + */ + public Consultation toModelType() throws IllegalValueException, IllegalArgumentException { + int modelIndex = this.index; + LocalDateTime modelSession = this.session; + Patient modelPatient = this.patient; + Diagnosis modelDiagnosis = this.diagnosis; + ArrayList prescriptions = new ArrayList<>(); + prescriptions.addAll(this.prescriptions); + + Consultation consultation = new Consultation(modelPatient); + consultation.setIndex(modelIndex); + consultation.setSession(modelSession); + consultation.setDiagnosis(modelDiagnosis); + consultation.setPrescriptions(prescriptions); + return consultation; + } + + +} diff --git a/src/main/java/quickdocs/storage/JsonAdaptedDirectory.java b/src/main/java/quickdocs/storage/JsonAdaptedDirectory.java new file mode 100644 index 000000000000..9dcd61c650f3 --- /dev/null +++ b/src/main/java/quickdocs/storage/JsonAdaptedDirectory.java @@ -0,0 +1,71 @@ +package quickdocs.storage; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import quickdocs.model.medicine.Directory; +import quickdocs.model.medicine.Medicine; + +/** + * A Jackson-friendly version of {@link Directory}. + */ +public class JsonAdaptedDirectory { + + private ArrayList listOfMedicineNames; + private ArrayList listOfDirectories; + private String name; + private Optional threshold; + + @JsonCreator + public JsonAdaptedDirectory(@JsonProperty("name") String name, + @JsonProperty("threshold") Optional threshold, + @JsonProperty("listOfMedicineNames") List listOfMedicineNames, + @JsonProperty("listOfDirectories") List listOfDirectories) { + this.name = name; + this.threshold = threshold; + this.listOfMedicineNames = new ArrayList<>(); + this.listOfDirectories = new ArrayList<>(); + this.listOfDirectories.addAll(listOfDirectories); + this.listOfMedicineNames.addAll(listOfMedicineNames); + } + + public JsonAdaptedDirectory(Directory directory) { + this.name = directory.name; + this.threshold = directory.getThreshold(); + listOfMedicineNames = new ArrayList<>(); + if (!directory.getListOfMedicine().isEmpty()) { + listOfMedicineNames.addAll(directory.getListOfMedicine() + .stream() + .map((Medicine medicine) -> medicine.name) + .collect(Collectors.toList())); + } + listOfDirectories = new ArrayList<>(); + if (!directory.getListOfDirectory().isEmpty()) { + listOfDirectories.addAll(directory.getListOfDirectory() + .stream() + .map((Directory subDirectory) -> new JsonAdaptedDirectory(subDirectory)) + .collect(Collectors.toList())); + } + } + + public ArrayList getListOfMedicineNames() { + return listOfMedicineNames; + } + + public ArrayList getListOfDirectories() { + return listOfDirectories; + } + + public String getName() { + return name; + } + + public Optional getThreshold() { + return threshold; + } +} diff --git a/src/main/java/quickdocs/storage/JsonAdaptedMedicine.java b/src/main/java/quickdocs/storage/JsonAdaptedMedicine.java new file mode 100644 index 000000000000..34c65954a79b --- /dev/null +++ b/src/main/java/quickdocs/storage/JsonAdaptedMedicine.java @@ -0,0 +1,52 @@ +package quickdocs.storage; + +import java.math.BigDecimal; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import quickdocs.commons.exceptions.IllegalValueException; +import quickdocs.model.medicine.Medicine; + +/** + * Jackson-friendly version of {@link Medicine}. + */ +public class JsonAdaptedMedicine { + + private String name; + private int quantity; + private int threshold; + private BigDecimal price; + + @JsonCreator + public JsonAdaptedMedicine (@JsonProperty("name") String name, @JsonProperty("quantity") int quantity, + @JsonProperty("threshold") int threshold, @JsonProperty("price") BigDecimal price) { + this.name = name; + this.quantity = quantity; + this.threshold = threshold; + this.price = price; + } + + public JsonAdaptedMedicine (Medicine medicine) { + this.name = medicine.name; + this.quantity = medicine.getQuantity(); + this.price = medicine.getPrice(); + this.threshold = medicine.getThreshold(); + } + + /** + * Converts this Jackson-friendly adapted medicine object into the model's {@link Medicine} object. + * @return A model-type medicine object. + * @throws IllegalValueException If the quantity/price values violates preconditions. + */ + public Medicine toModelType() { + Medicine medicine = new Medicine(name, quantity); + medicine.setPrice(price); + medicine.setThreshold(threshold); + return medicine; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/quickdocs/storage/JsonAdaptedMonthStatistics.java b/src/main/java/quickdocs/storage/JsonAdaptedMonthStatistics.java new file mode 100644 index 000000000000..4836fbc8b689 --- /dev/null +++ b/src/main/java/quickdocs/storage/JsonAdaptedMonthStatistics.java @@ -0,0 +1,46 @@ +package quickdocs.storage; + +import java.time.YearMonth; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import quickdocs.model.record.MonthStatistics; +import quickdocs.model.record.Statistics; + +/** + * Jackson-friendly version of {@Link MonthStatistics}. + */ +public class JsonAdaptedMonthStatistics { + + private YearMonth yearMonth; + private JsonAdaptedStatistics statistics; + + /** + * Consutrcts a {@code MonthStatistics} with the given YearMonth and Statistics + */ + @JsonCreator + public JsonAdaptedMonthStatistics(@JsonProperty("yearMonth") YearMonth yearMonth, + @JsonProperty("statistics") JsonAdaptedStatistics stats) { + this.yearMonth = yearMonth; + this.statistics = stats; + } + + /** + * Converts a given {@code MonthStatistics} into this class for Jackson use. + */ + public JsonAdaptedMonthStatistics(MonthStatistics source) { + this.yearMonth = source.getYearMonth(); + this.statistics = new JsonAdaptedStatistics(source.getStatistics()); + } + + /** + * Converts this Jackson-friendly adapted MonthStatistics object into the model's {@code MonthStatistics} object. + * + * @throws IllegalArgumentException if there were any data constraints violated for MonthStatistics fields. + */ + public MonthStatistics toModelType() throws IllegalArgumentException { + Statistics stats = this.statistics.toModelType(); + return new MonthStatistics(this.yearMonth, stats); + } +} diff --git a/src/main/java/quickdocs/storage/JsonAdaptedPatient.java b/src/main/java/quickdocs/storage/JsonAdaptedPatient.java new file mode 100644 index 000000000000..0341b12426bd --- /dev/null +++ b/src/main/java/quickdocs/storage/JsonAdaptedPatient.java @@ -0,0 +1,107 @@ +package quickdocs.storage; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import quickdocs.commons.exceptions.IllegalValueException; +import quickdocs.model.patient.Address; +import quickdocs.model.patient.Contact; +import quickdocs.model.patient.Dob; +import quickdocs.model.patient.Email; +import quickdocs.model.patient.Gender; +import quickdocs.model.patient.Name; +import quickdocs.model.patient.Nric; +import quickdocs.model.patient.Patient; +import quickdocs.model.tag.Tag; + +/** + * Jackson-friendly version of {@link Patient}. + */ +public class JsonAdaptedPatient { + + private String name; + private String nric; + private String email; + private String address; + private String contact; + private String gender; + private String dob; + private List tagList = new ArrayList<>(); + + /** + * Constructs a {@code JsonAdaptedPerson} with the given person details. + */ + @JsonCreator + public JsonAdaptedPatient(@JsonProperty("name") String name, @JsonProperty("nric") String nric, + @JsonProperty("email") String email, @JsonProperty("address") String address, + @JsonProperty("contact") String contact, @JsonProperty("gender") String gender, + @JsonProperty("dob") String dob, + @JsonProperty("tagged") List tagged) { + this.name = name; + this.nric = nric; + this.email = email; + this.address = address; + this.contact = contact; + this.gender = gender; + this.dob = dob; + if (tagged != null) { + this.tagList.addAll(tagged); + } + } + + /** + * Converts a given {@code Person} into this class for Jackson use. + */ + public JsonAdaptedPatient(Patient source) { + + this.name = source.getName().getName(); + this.nric = source.getNric().getNric(); + this.email = source.getEmail().getEmail(); + this.address = source.getAddress().getAddress(); + this.contact = source.getContact().getContact(); + this.gender = source.getGender().getGender(); + this.dob = source.getDob().toString(); + tagList.addAll(source.getTagList().stream().map(JsonAdaptedTag::new).collect(Collectors.toList())); + } + + /** + * Converts this Jackson-friendly adapted person object into the model's {@code Person} object. + * + * @return a Patient model object + * @throws IllegalValueException if there were any data constraints violated in the tags. + */ + public Patient toModelType() throws IllegalValueException { + final List personTags = new ArrayList<>(); + for (JsonAdaptedTag tag : tagList) { + personTags.add(tag.toModelType()); + } + + final Name modelName = new Name(name); + + final Nric modelNric = new Nric(nric); + + final Email modelEmail = new Email(email); + + final Address modelAddress = new Address(address); + + final Contact modelContact = new Contact(contact); + + final Gender modelGender = new Gender(gender); + + final Dob modelDob = new Dob(dob); + + final ArrayList modelTags = new ArrayList<>(); + + for (JsonAdaptedTag tag : tagList) { + modelTags.add(tag.toModelType()); + } + + return new Patient(modelName, modelNric, modelEmail, modelAddress, modelContact, modelGender, + modelDob, modelTags); + } + +} diff --git a/src/main/java/quickdocs/storage/JsonAdaptedReminder.java b/src/main/java/quickdocs/storage/JsonAdaptedReminder.java new file mode 100644 index 000000000000..99977bfddbd8 --- /dev/null +++ b/src/main/java/quickdocs/storage/JsonAdaptedReminder.java @@ -0,0 +1,96 @@ +package quickdocs.storage; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeParseException; +import java.util.Optional; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import quickdocs.model.reminder.Reminder; + +/** + * Jackson-friendly version of {@link Reminder}. + */ +public class JsonAdaptedReminder { + public static final String REM_MISSING_FIELD_MESSAGE_FORMAT = "Reminder's %s field is missing!"; + + private String title; + private String date; + private String start; + private String comment = null; + private String end = null; + + /** + * Constructs a {@code JsonAdaptedAppointment} with the given {@code Reminder} details. + */ + @JsonCreator + public JsonAdaptedReminder(@JsonProperty("title") String title, + @JsonProperty("comment") String comment, + @JsonProperty("date") String date, + @JsonProperty("start") String start, + @JsonProperty("end") String end) { + this.title = title; + this.comment = comment; + this.date = date; + this.start = start; + this.end = end; + } + + /** + * Converts a given {@code Reminder} into this class for Jackson use. + */ + public JsonAdaptedReminder(Reminder source) { + title = source.getTitle(); + date = source.getDate().toString(); + start = source.getStart().toString(); + Optional commentString = Optional.ofNullable(source.getComment()); + commentString.ifPresent(s -> comment = commentString.get()); + Optional endTime = Optional.ofNullable(source.getEnd()); + endTime.ifPresent(e -> end = endTime.get().toString()); + } + + /** + * Converts this Jackson-friendly adapted {@code Reminder} object into the model's {@code Reminder} object. + * + * @throws IllegalArgumentException if there were any data constraints violated for {@code Reminder} fields. + */ + public Reminder toModelType() throws IllegalArgumentException { + // check if any required fields are missing + if (title == null) { + throw new IllegalArgumentException(String.format(REM_MISSING_FIELD_MESSAGE_FORMAT, "Title")); + } + if (date == null) { + throw new IllegalArgumentException(String.format(REM_MISSING_FIELD_MESSAGE_FORMAT, "Date")); + } + if (start == null) { + throw new IllegalArgumentException(String.format(REM_MISSING_FIELD_MESSAGE_FORMAT, "Start")); + } + + String modelTitle = title; + String modelComment = comment; + LocalDate modelDate; + LocalTime modelStart; + LocalTime modelEnd; + + try { + modelDate = LocalDate.parse(date); + } catch (DateTimeParseException e) { + throw new IllegalArgumentException("Date format: YYYY-MM-DD"); + } + + try { + modelStart = LocalTime.parse(start); + if (Optional.ofNullable(end).isPresent()) { + modelEnd = LocalTime.parse(end); + } else { + modelEnd = null; + } + } catch (DateTimeParseException e) { + throw new IllegalArgumentException("Time format: HH:MM"); + } + + return new Reminder(modelTitle, modelComment, modelDate, modelStart, modelEnd); + } +} diff --git a/src/main/java/quickdocs/storage/JsonAdaptedStatistics.java b/src/main/java/quickdocs/storage/JsonAdaptedStatistics.java new file mode 100644 index 000000000000..71b1d34f2305 --- /dev/null +++ b/src/main/java/quickdocs/storage/JsonAdaptedStatistics.java @@ -0,0 +1,57 @@ +package quickdocs.storage; + +import java.math.BigDecimal; +import java.util.HashMap; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import quickdocs.model.record.Statistics; + +/** + * Jackson-friendly version of {@Link Statistics}. + */ +public class JsonAdaptedStatistics { + + private int consultations; + private BigDecimal revenue; + private BigDecimal expenditure; + private HashMap medicines; + private HashMap symptoms; + + /** + * Constructs a {@code JsonAdaptedStatistics} with the given Statistics. + */ + @JsonCreator + public JsonAdaptedStatistics(@JsonProperty("consultations") int consultations, + @JsonProperty("revenue") BigDecimal revenue, + @JsonProperty("expenditure") BigDecimal expenditure, + @JsonProperty("medicines") HashMap medicines, + @JsonProperty("symptoms") HashMap symptoms) { + this.consultations = consultations; + this.revenue = revenue; + this.expenditure = expenditure; + this.medicines = medicines; + this.symptoms = symptoms; + } + + /** + * Converts a given {@code Statistics} into this class for Jackson use. + */ + public JsonAdaptedStatistics(Statistics source) { + this.consultations = source.getNoOfConsultations(); + this.revenue = source.getRevenue(); + this.expenditure = source.getExpenditure(); + this.medicines = source.getMedicinesCount(); + this.symptoms = source.getSymptomsCount(); + } + + /** + * Converts this Jackson-friendly adapted Statistics object into the model's {@code Statistics} object. + * + * @throws IllegalArgumentException if there were any data constraints violated for Statistics fields. + */ + public Statistics toModelType() throws IllegalArgumentException { + return new Statistics(this.consultations, this.revenue, this.expenditure, this.medicines, this.symptoms); + } +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedTag.java b/src/main/java/quickdocs/storage/JsonAdaptedTag.java similarity index 89% rename from src/main/java/seedu/address/storage/JsonAdaptedTag.java rename to src/main/java/quickdocs/storage/JsonAdaptedTag.java index 0df22bdb7546..fa37b756370f 100644 --- a/src/main/java/seedu/address/storage/JsonAdaptedTag.java +++ b/src/main/java/quickdocs/storage/JsonAdaptedTag.java @@ -1,10 +1,10 @@ -package seedu.address.storage; +package quickdocs.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; +import quickdocs.commons.exceptions.IllegalValueException; +import quickdocs.model.tag.Tag; /** * Jackson-friendly version of {@link Tag}. diff --git a/src/main/java/quickdocs/storage/JsonQuickDocsStorage.java b/src/main/java/quickdocs/storage/JsonQuickDocsStorage.java new file mode 100644 index 000000000000..8b5492bf6ab6 --- /dev/null +++ b/src/main/java/quickdocs/storage/JsonQuickDocsStorage.java @@ -0,0 +1,78 @@ +package quickdocs.storage; + +import static java.util.Objects.requireNonNull; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; +import java.util.logging.Logger; + +import quickdocs.commons.core.LogsCenter; +import quickdocs.commons.exceptions.DataConversionException; +import quickdocs.commons.exceptions.IllegalValueException; +import quickdocs.commons.util.FileUtil; +import quickdocs.commons.util.JsonUtil; +import quickdocs.model.QuickDocs; + +/** + * A class to access QuickDocs data stored as a json file on the hard disk. + */ +public class JsonQuickDocsStorage implements QuickDocsStorage { + private static final Logger logger = LogsCenter.getLogger(JsonQuickDocsStorage.class); + private Path filePath; + + public JsonQuickDocsStorage(Path filePath) { + this.filePath = filePath; + } + + public Path getFilePath() { + return filePath; + } + + @Override + public Optional readQuickDocs() throws DataConversionException { + return readQuickDocs(filePath); + } + + /** + * Similar to {@link #readQuickDocs()}. + * + * @param filePath location of the data. Cannot be null. + * @return an {@code Optional} object that contains all objects read from the json file. + * @throws DataConversionException if the file is not in the correct format. + */ + public Optional readQuickDocs(Path filePath) throws DataConversionException { + requireNonNull(filePath); + + Optional jsonQuickDocs = JsonUtil.readJsonFile( + filePath, JsonSerializableQuickDocs.class); + if (!jsonQuickDocs.isPresent()) { + return Optional.empty(); + } + + try { + return Optional.of(jsonQuickDocs.get().toModelType()); + } catch (IllegalValueException | IllegalArgumentException ie) { + logger.info("Illegal values found in " + filePath + ": " + ie.getMessage()); + throw new DataConversionException(ie); + } + } + + @Override + public void saveQuickDocs(QuickDocs quickDocs) throws IOException { + saveQuickDocs(quickDocs, filePath); + } + + /** + * Similar to {@link #saveQuickDocs(QuickDocs)}. + * + * @param filePath location of the data. Cannot be null. + */ + public void saveQuickDocs(QuickDocs quickDocs, Path filePath) throws IOException { + requireNonNull(quickDocs); + requireNonNull(filePath); + + FileUtil.createIfMissing(filePath); + JsonUtil.saveJsonFile(new JsonSerializableQuickDocs(quickDocs), filePath); + } +} diff --git a/src/main/java/quickdocs/storage/JsonSerializableQuickDocs.java b/src/main/java/quickdocs/storage/JsonSerializableQuickDocs.java new file mode 100644 index 000000000000..e77c786fe19a --- /dev/null +++ b/src/main/java/quickdocs/storage/JsonSerializableQuickDocs.java @@ -0,0 +1,205 @@ +package quickdocs.storage; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import quickdocs.commons.exceptions.IllegalValueException; +import quickdocs.model.QuickDocs; +import quickdocs.model.appointment.Appointment; +import quickdocs.model.appointment.AppointmentManager; +import quickdocs.model.consultation.Consultation; +import quickdocs.model.consultation.ConsultationManager; +import quickdocs.model.medicine.Directory; +import quickdocs.model.medicine.Medicine; +import quickdocs.model.medicine.MedicineManager; +import quickdocs.model.patient.Patient; +import quickdocs.model.patient.PatientManager; +import quickdocs.model.record.MonthStatistics; +import quickdocs.model.record.StatisticsManager; +import quickdocs.model.reminder.Reminder; +import quickdocs.model.reminder.ReminderManager; + +/** + * This class allows QuickDocs to be saved into the external storage file in the json format. + * Using json data from the external storage file, the QuickDocs object can be reconstructed + * for use when QuickDocs is started + */ +public class JsonSerializableQuickDocs { + + public static final String MESSAGE_DUPLICATE_PATIENT = "Patients list contains duplicate patient(s)."; + public static final String MESSAGE_DUPLICATE_APPOINTMENT = "Appointment list contains duplicate appointment(s)."; + public static final String MESSAGE_DUPLICATE_REMINDER = "Reminder list contains duplicate reminder(s)."; + public static final String MESSAGE_DUPLICATE_MEDICINE = "Medicine list contains medicines with same name."; + public static final String MESSAGE_NONEXISTING_MEDICINE = + "A Directory contains a medicine not found in the list of medicines."; + public static final String MESSAGE_INVALID_CONSULTATION_FEE = "Consultation Fee is not a non-negative number."; + + private final List patientList = new ArrayList<>(); + private final List consultationList = new ArrayList<>(); + private final List appointmentList = new ArrayList<>(); + private final List reminderList = new ArrayList<>(); + private final List medicineList = new ArrayList<>(); + private JsonAdaptedDirectory rootDirectory; + private final List monthStatisticsList = new ArrayList<>(); + private BigDecimal consultationFee = StatisticsManager.DEFAULT_CONSULTATION_FEE; + + @JsonCreator + public JsonSerializableQuickDocs(@JsonProperty("patientList") List patients, + @JsonProperty("consultationList") List consultations, + @JsonProperty("appointmentList") List appointments, + @JsonProperty("reminderList") List reminders, + @JsonProperty("medicineList") List medicines, + @JsonProperty("rootDirectory") JsonAdaptedDirectory rootDirectory, + @JsonProperty("monthStatisticsList") + List monthStatisticsList, + @JsonProperty("consultationFee") BigDecimal consultationFee) { + this.patientList.addAll(patients); + this.consultationList.addAll(consultations); + this.appointmentList.addAll(appointments); + this.reminderList.addAll(reminders); + this.medicineList.addAll(medicines); + this.rootDirectory = rootDirectory; + this.monthStatisticsList.addAll(monthStatisticsList); + this.consultationFee = consultationFee; + } + + /** + * Converts a given {@code QuickDocs} into a JsonSerializableQuickDocs to save into + * an external Json file using Jackson + * + * @param source The QuickDocs model object in memory that is currently holding the data from + * all five modules + */ + public JsonSerializableQuickDocs(QuickDocs source) { + patientList.addAll(source.getPatientManager().getPatientList() + .stream().map(JsonAdaptedPatient::new).collect(Collectors.toList())); + consultationList.addAll(source.getConsultationManager().getConsultationList() + .stream().map(JsonAdaptedConsultation::new).collect(Collectors.toList())); + appointmentList.addAll(source.getAppointmentManager().getAppointmentList() + .stream().map(JsonAdaptedAppointment::new).collect(Collectors.toList())); + reminderList.addAll(source.getReminderManager().getReminderList() + .stream().map(JsonAdaptedReminder::new).collect(Collectors.toList())); + medicineList.addAll(source.getMedicineManager().getListOfMedicine() + .stream().map(JsonAdaptedMedicine::new).collect(Collectors.toList())); + rootDirectory = new JsonAdaptedDirectory(source.getMedicineManager().getRoot()); + monthStatisticsList.addAll(source.getStatisticsManager().getMonthStatisticsList() + .stream().map(JsonAdaptedMonthStatistics::new).collect(Collectors.toList())); + consultationFee = source.getStatisticsManager().getConsultationFee(); + } + + /** + * Converts the JsonSerializableQuickDocs into the model's {@code QuickDocs} object. + * + * @throws IllegalValueException if there were any data constraints violated. + * @throws IllegalArgumentException if there were any data constraints violated for any class fields + */ + public QuickDocs toModelType() throws IllegalValueException, IllegalArgumentException { + QuickDocs quickDocs = new QuickDocs(); + + PatientManager patientManager = quickDocs.getPatientManager(); + for (JsonAdaptedPatient jsonAdaptedPatient : patientList) { + Patient patient = jsonAdaptedPatient.toModelType(); + + // handle duplicates + if (patientManager.isDuplicatePatient(patient)) { + throw new IllegalValueException(MESSAGE_DUPLICATE_PATIENT); + } + patientManager.addPatient(patient); + } + + ConsultationManager consultationManager = quickDocs.getConsultationManager(); + for (JsonAdaptedConsultation jsonAdaptedConsultation : consultationList) { + Consultation consultation = jsonAdaptedConsultation.toModelType(); + consultationManager.addConsultation(consultation); + } + + AppointmentManager appointmentManager = quickDocs.getAppointmentManager(); + for (JsonAdaptedAppointment jsonAdaptedAppointment : appointmentList) { + Appointment appointment = jsonAdaptedAppointment.toModelType(); + + // handle duplicates + if (appointmentManager.hasDuplicateAppointment(appointment)) { + throw new IllegalValueException(MESSAGE_DUPLICATE_APPOINTMENT); + } + appointmentManager.addAppointment(appointment); + } + + ReminderManager reminderManager = quickDocs.getReminderManager(); + for (JsonAdaptedReminder jsonAdaptedReminder : reminderList) { + Reminder reminder = jsonAdaptedReminder.toModelType(); + + // handle duplicates + if (reminderManager.hasDuplicateReminder(reminder)) { + throw new IllegalValueException(MESSAGE_DUPLICATE_REMINDER); + } + reminderManager.addReminder(reminder); + } + + MedicineManager medicineManager = quickDocs.getMedicineManager(); + ArrayList listOfMedicine = new ArrayList<>(); + listOfMedicine.addAll(medicineList + .stream() + .map((JsonAdaptedMedicine::toModelType)) + .collect(Collectors.toList())); + listOfMedicine.sort(Comparator.comparing((Medicine medicine) -> (medicine.name.toLowerCase()))); + HashMap medicineHashMap = new HashMap<>(); + for (Medicine medicine : listOfMedicine) { + String medicineName = medicine.name; + if (medicineHashMap.containsKey(medicineName)) { + throw new IllegalValueException(MESSAGE_DUPLICATE_MEDICINE); + } else { + medicineHashMap.put(medicineName, medicine); + } + } + Directory modelTypeRoot = toModelTypeDirectory(medicineHashMap, rootDirectory); + medicineManager.setRoot(modelTypeRoot); + medicineManager.setListOfMedicine(listOfMedicine); + + StatisticsManager statisticsManager = quickDocs.getStatisticsManager(); + for (JsonAdaptedMonthStatistics jsonAdaptedMonthStatistics : monthStatisticsList) { + MonthStatistics monthStatistics = jsonAdaptedMonthStatistics.toModelType(); + statisticsManager.addMonthStatistics(monthStatistics); + } + if (this.consultationFee.compareTo(BigDecimal.ZERO) == -1) { + throw new IllegalValueException(MESSAGE_INVALID_CONSULTATION_FEE); + } + statisticsManager.setConsultationFee(this.consultationFee); + + return quickDocs; + } + + /** + * Convert a {@link JsonAdaptedDirectory} to a Directory using information from medicineHashMap + * + * @param map A hashmap of medicine name mapping to medicine + * @param jsonDirectory the JsonAdaptedDirectory to convert from + * @return The converted directory + * @throws IllegalValueException if a directory contains medicine not from map + */ + private Directory toModelTypeDirectory(HashMap map, JsonAdaptedDirectory jsonDirectory) + throws IllegalValueException { + Directory directory = new Directory(jsonDirectory.getName()); + if (jsonDirectory.getThreshold().isPresent()) { + directory.setThreshold(jsonDirectory.getThreshold().get()); + } + ArrayList medicineNames = jsonDirectory.getListOfMedicineNames(); + for (String medicineName : medicineNames) { + if (!map.containsKey(medicineName)) { + throw new IllegalValueException(MESSAGE_NONEXISTING_MEDICINE); + } + directory.addMedicine(map.get(medicineName)); + } + ArrayList jsonAdaptedDirectories = jsonDirectory.getListOfDirectories(); + for (JsonAdaptedDirectory jsonAdaptedDirectory : jsonAdaptedDirectories) { + directory.addDirectory(toModelTypeDirectory(map, jsonAdaptedDirectory)); + } + return directory; + } +} diff --git a/src/main/java/seedu/address/storage/JsonUserPrefsStorage.java b/src/main/java/quickdocs/storage/JsonUserPrefsStorage.java similarity index 79% rename from src/main/java/seedu/address/storage/JsonUserPrefsStorage.java rename to src/main/java/quickdocs/storage/JsonUserPrefsStorage.java index bc2bbad84aa3..3f2bf1fd655c 100644 --- a/src/main/java/seedu/address/storage/JsonUserPrefsStorage.java +++ b/src/main/java/quickdocs/storage/JsonUserPrefsStorage.java @@ -1,16 +1,16 @@ -package seedu.address.storage; +package quickdocs.storage; import java.io.IOException; import java.nio.file.Path; import java.util.Optional; -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.commons.util.JsonUtil; -import seedu.address.model.ReadOnlyUserPrefs; -import seedu.address.model.UserPrefs; +import quickdocs.commons.exceptions.DataConversionException; +import quickdocs.commons.util.JsonUtil; +import quickdocs.model.ReadOnlyUserPrefs; +import quickdocs.model.UserPrefs; /** - * A class to access UserPrefs stored in the hard disk as a json file + * A class to access UserPrefs stored in the hard disk as a json file. */ public class JsonUserPrefsStorage implements UserPrefsStorage { @@ -31,7 +31,8 @@ public Optional readUserPrefs() throws DataConversionException { } /** - * Similar to {@link #readUserPrefs()} + * Similar to {@link #readUserPrefs()}. + * * @param prefsFilePath location of the data. Cannot be null. * @throws DataConversionException if the file format is not as expected. */ diff --git a/src/main/java/quickdocs/storage/QuickDocsStorage.java b/src/main/java/quickdocs/storage/QuickDocsStorage.java new file mode 100644 index 000000000000..7f10d24523c2 --- /dev/null +++ b/src/main/java/quickdocs/storage/QuickDocsStorage.java @@ -0,0 +1,35 @@ +package quickdocs.storage; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; + +import quickdocs.commons.exceptions.DataConversionException; +import quickdocs.model.QuickDocs; + +/** + * Represents a storage for {@link QuickDocs}. + */ +public interface QuickDocsStorage { + + /** + * Returns the file path of the data file. + */ + Path getFilePath(); + + /** + * Returns {@code QuickDocs} data as a {@link QuickDocs}. + * + * Returns {@code Optional.empty()} if storage file is not found. + * @throws DataConversionException if the data in storage is not in the expected format. + * @throws IOException if there was any problem when reading from the storage. + */ + Optional readQuickDocs() throws DataConversionException, IOException; + + /** + * Saves the given {@link QuickDocs} to the storage. + * @param quickDocs cannot be null. + * @throws IOException if there was any problem writing to the file. + */ + void saveQuickDocs(QuickDocs quickDocs) throws IOException; +} diff --git a/src/main/java/quickdocs/storage/Storage.java b/src/main/java/quickdocs/storage/Storage.java new file mode 100644 index 000000000000..4f80c745111b --- /dev/null +++ b/src/main/java/quickdocs/storage/Storage.java @@ -0,0 +1,25 @@ +package quickdocs.storage; + +import java.io.IOException; +import java.util.Optional; + +import quickdocs.commons.exceptions.DataConversionException; +import quickdocs.model.QuickDocs; +import quickdocs.model.ReadOnlyUserPrefs; +import quickdocs.model.UserPrefs; + +/** + * API of the Storage component + */ +public interface Storage extends UserPrefsStorage { + + @Override + Optional readUserPrefs() throws DataConversionException, IOException; + + @Override + void saveUserPrefs(ReadOnlyUserPrefs userPrefs) throws IOException; + + Optional readQuickDocs() throws DataConversionException, IOException; + + void saveQuickDocs(QuickDocs quickDocs) throws IOException; +} diff --git a/src/main/java/quickdocs/storage/StorageManager.java b/src/main/java/quickdocs/storage/StorageManager.java new file mode 100644 index 000000000000..1599d273ea1d --- /dev/null +++ b/src/main/java/quickdocs/storage/StorageManager.java @@ -0,0 +1,76 @@ +package quickdocs.storage; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; +import java.util.logging.Logger; + +import quickdocs.commons.core.LogsCenter; +import quickdocs.commons.exceptions.DataConversionException; +import quickdocs.model.QuickDocs; +import quickdocs.model.ReadOnlyUserPrefs; +import quickdocs.model.UserPrefs; + +/** + * Manages storage of AddressBook data in local storage. + */ +public class StorageManager implements Storage { + + private static final Logger logger = LogsCenter.getLogger(StorageManager.class); + private UserPrefsStorage userPrefsStorage; + private QuickDocsStorage quickDocsStorage; + + public StorageManager(UserPrefsStorage userPrefsStorage, + QuickDocsStorage quickDocsStorage) { + super(); + this.userPrefsStorage = userPrefsStorage; + + this.quickDocsStorage = quickDocsStorage; + } + + // ================ UserPrefs methods ============================== + + @Override + public Path getUserPrefsFilePath() { + return userPrefsStorage.getUserPrefsFilePath(); + } + + @Override + public Optional readUserPrefs() throws DataConversionException, IOException { + logger.fine("Attempting to read user prefs data from file: " + getUserPrefsFilePath()); + return userPrefsStorage.readUserPrefs(); + } + + @Override + public void saveUserPrefs(ReadOnlyUserPrefs userPrefs) throws IOException { + logger.fine("Attempting to write user prefs data to file: " + getUserPrefsFilePath()); + userPrefsStorage.saveUserPrefs(userPrefs); + } + + // ================ QuickDocs methods ============================== + + /** + * Read {@code QuickDocs} data file and retrieve data from json format. + * + * @return an {@code Optional} object that contains all objects read from the json file. + * @throws DataConversionException if the file is not in the correct format. + * @throws IOException if there was an error reading the json file. + */ + @Override + public Optional readQuickDocs() throws DataConversionException, IOException { + logger.fine("Attempting to read data from file: " + quickDocsStorage.getFilePath()); + return quickDocsStorage.readQuickDocs(); + } + + /** + * Saves {@code QuickDocs} data into a data file in json format. + * + * @param quickDocs the {@code QuickDocs} object to save. + * @throws IOException if there was an error writing to the json file. + */ + @Override + public void saveQuickDocs(QuickDocs quickDocs) throws IOException { + logger.fine("Attempting to write to data file: " + quickDocsStorage.getFilePath()); + quickDocsStorage.saveQuickDocs(quickDocs); + } +} diff --git a/src/main/java/seedu/address/storage/UserPrefsStorage.java b/src/main/java/quickdocs/storage/UserPrefsStorage.java similarity index 65% rename from src/main/java/seedu/address/storage/UserPrefsStorage.java rename to src/main/java/quickdocs/storage/UserPrefsStorage.java index 29eef178dbc4..aa8bede30026 100644 --- a/src/main/java/seedu/address/storage/UserPrefsStorage.java +++ b/src/main/java/quickdocs/storage/UserPrefsStorage.java @@ -1,15 +1,15 @@ -package seedu.address.storage; +package quickdocs.storage; import java.io.IOException; import java.nio.file.Path; import java.util.Optional; -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.model.ReadOnlyUserPrefs; -import seedu.address.model.UserPrefs; +import quickdocs.commons.exceptions.DataConversionException; +import quickdocs.model.ReadOnlyUserPrefs; +import quickdocs.model.UserPrefs; /** - * Represents a storage for {@link seedu.address.model.UserPrefs}. + * Represents a storage for {@link UserPrefs}. */ public interface UserPrefsStorage { @@ -20,14 +20,16 @@ public interface UserPrefsStorage { /** * Returns UserPrefs data from storage. - * Returns {@code Optional.empty()} if storage file is not found. + * Returns {@code Optional.empty()} if storage file is not found. + * * @throws DataConversionException if the data in storage is not in the expected format. * @throws IOException if there was any problem when reading from the storage. */ Optional readUserPrefs() throws DataConversionException, IOException; /** - * Saves the given {@link seedu.address.model.ReadOnlyUserPrefs} to the storage. + * Saves the given {@link ReadOnlyUserPrefs} to the storage. + * * @param userPrefs cannot be null. * @throws IOException if there was any problem writing to the file. */ diff --git a/src/main/java/seedu/address/ui/CommandBox.java b/src/main/java/quickdocs/ui/CommandBox.java similarity index 94% rename from src/main/java/seedu/address/ui/CommandBox.java rename to src/main/java/quickdocs/ui/CommandBox.java index bf09f3dcbea6..9b2461806f05 100644 --- a/src/main/java/seedu/address/ui/CommandBox.java +++ b/src/main/java/quickdocs/ui/CommandBox.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package quickdocs.ui; import java.util.List; @@ -7,9 +7,10 @@ import javafx.scene.control.TextField; import javafx.scene.input.KeyEvent; import javafx.scene.layout.Region; -import seedu.address.logic.commands.CommandResult; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.logic.parser.exceptions.ParseException; +import quickdocs.logic.Logic; +import quickdocs.logic.commands.CommandResult; +import quickdocs.logic.commands.exceptions.CommandException; +import quickdocs.logic.parser.exceptions.ParseException; /** * The UI component that is responsible for receiving user command inputs. @@ -146,7 +147,7 @@ public interface CommandExecutor { /** * Executes the command and returns the result. * - * @see seedu.address.logic.Logic#execute(String) + * @see Logic#execute(String) */ CommandResult execute(String commandText) throws CommandException, ParseException; } diff --git a/src/main/java/seedu/address/ui/HelpWindow.java b/src/main/java/quickdocs/ui/HelpWindow.java similarity index 96% rename from src/main/java/seedu/address/ui/HelpWindow.java rename to src/main/java/quickdocs/ui/HelpWindow.java index 22606d102f83..c86c3c0bca46 100644 --- a/src/main/java/seedu/address/ui/HelpWindow.java +++ b/src/main/java/quickdocs/ui/HelpWindow.java @@ -1,11 +1,11 @@ -package seedu.address.ui; +package quickdocs.ui; import java.util.logging.Logger; import javafx.fxml.FXML; import javafx.scene.web.WebView; import javafx.stage.Stage; -import seedu.address.commons.core.LogsCenter; +import quickdocs.commons.core.LogsCenter; /** * Controller for a help page diff --git a/src/main/java/seedu/address/ui/ListElementPointer.java b/src/main/java/quickdocs/ui/ListElementPointer.java similarity index 99% rename from src/main/java/seedu/address/ui/ListElementPointer.java rename to src/main/java/quickdocs/ui/ListElementPointer.java index 54db5ae7efee..b5a86fcf8ad7 100644 --- a/src/main/java/seedu/address/ui/ListElementPointer.java +++ b/src/main/java/quickdocs/ui/ListElementPointer.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package quickdocs.ui; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/quickdocs/ui/MainWindow.java similarity index 76% rename from src/main/java/seedu/address/ui/MainWindow.java rename to src/main/java/quickdocs/ui/MainWindow.java index ac165736001d..8a3561d2862a 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/quickdocs/ui/MainWindow.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package quickdocs.ui; import java.util.logging.Logger; @@ -10,12 +10,12 @@ import javafx.scene.input.KeyEvent; import javafx.scene.layout.StackPane; import javafx.stage.Stage; -import seedu.address.commons.core.GuiSettings; -import seedu.address.commons.core.LogsCenter; -import seedu.address.logic.Logic; -import seedu.address.logic.commands.CommandResult; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.logic.parser.exceptions.ParseException; +import quickdocs.commons.core.GuiSettings; +import quickdocs.commons.core.LogsCenter; +import quickdocs.logic.Logic; +import quickdocs.logic.commands.CommandResult; +import quickdocs.logic.commands.exceptions.CommandException; +import quickdocs.logic.parser.exceptions.ParseException; /** * The Main Window. Provides the basic application layout containing @@ -31,8 +31,6 @@ public class MainWindow extends UiPart { private Logic logic; // Independent Ui parts residing in this Ui container - private BrowserPanel browserPanel; - private PersonListPanel personListPanel; private ResultDisplay resultDisplay; private HelpWindow helpWindow; @@ -107,27 +105,6 @@ private void setAccelerator(MenuItem menuItem, KeyCombination keyCombination) { }); } - /** - * Fills up all the placeholders of this window. - */ - void fillInnerParts() { - browserPanel = new BrowserPanel(logic.selectedPersonProperty()); - browserPlaceholder.getChildren().add(browserPanel.getRoot()); - - personListPanel = new PersonListPanel(logic.getFilteredPersonList(), logic.selectedPersonProperty(), - logic::setSelectedPerson); - personListPanelPlaceholder.getChildren().add(personListPanel.getRoot()); - - resultDisplay = new ResultDisplay(); - resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot()); - - StatusBarFooter statusBarFooter = new StatusBarFooter(logic.getAddressBookFilePath(), logic.getAddressBook()); - statusbarPlaceholder.getChildren().add(statusBarFooter.getRoot()); - - CommandBox commandBox = new CommandBox(this::executeCommand, logic.getHistory()); - commandBoxPlaceholder.getChildren().add(commandBox.getRoot()); - } - /** * Sets the default size based on {@code guiSettings}. */ @@ -168,14 +145,11 @@ private void handleExit() { primaryStage.hide(); } - public PersonListPanel getPersonListPanel() { - return personListPanel; - } /** * Executes the command and returns the result. * - * @see seedu.address.logic.Logic#execute(String) + * @see Logic#execute(String) */ private CommandResult executeCommand(String commandText) throws CommandException, ParseException { try { diff --git a/src/main/java/quickdocs/ui/ReminderCard.java b/src/main/java/quickdocs/ui/ReminderCard.java new file mode 100644 index 000000000000..cacdfcee1717 --- /dev/null +++ b/src/main/java/quickdocs/ui/ReminderCard.java @@ -0,0 +1,103 @@ +package quickdocs.ui; + +import java.time.LocalTime; +import java.util.Optional; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.Region; +import quickdocs.model.reminder.Reminder; + +/** + * An UI component that displays information of a {@code Reminder}. + */ +public class ReminderCard extends UiPart { + + private static final String FXML = "ReminderListCard.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 + */ + + public final Reminder reminder; + + @FXML + private Label title; + @FXML + private Label id; + @FXML + private Label date; + @FXML + private Label time; + @FXML + private Label comment; + + public ReminderCard(Reminder reminder, int displayedIndex) { + super(FXML); + this.reminder = reminder; + id.setText(displayedIndex + ". "); + setTitle(reminder); + title.setStyle("-fx-font-weight: bold;" + "-fx-font-size: 14px"); + date.setText(reminder.getDate().toString()); + setTime(reminder); + comment.setText(reminder.getComment()); + } + + /** + * Initialises the text of {@code Label time} given the {@code Reminder}. + * + * @param reminder the {@code Reminder} to initialise the time + */ + private void setTime(Reminder reminder) { + LocalTime start = reminder.getStart(); + String time = start.toString(); + Optional end = Optional.ofNullable(reminder.getEnd()); + if (end.isPresent()) { + time += " to " + + end.get().toString(); + } + this.time.setText(time); + } + + /** + * Initialises the text of {@code Label title} given the {@code Reminder}. + * + * @param reminder the {@code Reminder} to initialise the title + */ + private void setTitle(Reminder reminder) { + String title = reminder.getTitle(); + + // Shorten title for appointment reminders + if (title.startsWith("Appointment with ")) { + int commaIndex = title.indexOf(","); + if (commaIndex > 0) { + title = title.substring(0, commaIndex); + } + title = title.replace("Appointment", "Apt"); + } + + this.title.setText(title); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ReminderCard)) { + return false; + } + + // state check + ReminderCard card = (ReminderCard) other; + return id.getText().equals(card.id.getText()) + && reminder.equals(card.reminder); + } +} diff --git a/src/main/java/quickdocs/ui/ReminderListPanel.java b/src/main/java/quickdocs/ui/ReminderListPanel.java new file mode 100644 index 000000000000..0dde97f0f558 --- /dev/null +++ b/src/main/java/quickdocs/ui/ReminderListPanel.java @@ -0,0 +1,114 @@ +package quickdocs.ui; + +import java.time.LocalTime; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.logging.Logger; + +import javafx.beans.value.ObservableValue; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.control.TextArea; +import javafx.scene.layout.Region; +import quickdocs.commons.core.LogsCenter; +import quickdocs.model.reminder.Reminder; + +/** + * Panel containing the list of reminders. + */ +public class ReminderListPanel extends UiPart { + private static final String FXML = "ReminderListPanel.fxml"; + private static final String APPOINTMENT_BACKGROUND = "derive(lightskyblue, 50%)"; + private static final String MEDICINE_BACKGROUND = "derive(firebrick, 93%)"; + private static final String OTHER_BACKGROUND = "derive(beige, 35%)"; + private final Logger logger = LogsCenter.getLogger(ReminderListPanel.class); + private final TextArea display; + + @FXML + private ListView reminderListView; + + public ReminderListPanel(List reminderList, ObservableValue selectedReminder, + Consumer onSelectedReminderChange, TextArea display) { + super(FXML); + this.display = display; + reminderListView.setItems((ObservableList) reminderList); + reminderListView.setCellFactory(listView -> new ReminderListViewCell()); + reminderListView.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { + logger.fine("Selection in reminder list panel changed to : '" + newValue + "'"); + onSelectedReminderChange.accept(newValue); + }); + selectedReminder.addListener((observable, oldValue, newValue) -> { + logger.fine("Selected reminder changed to: " + newValue); + + // Don't modify selection if we are already selecting the selected reminder, + // otherwise we would have an infinite loop. + if (Objects.equals(reminderListView.getSelectionModel().getSelectedItem(), newValue)) { + return; + } + + if (newValue == null) { + reminderListView.getSelectionModel().clearSelection(); + } else { + int index = reminderListView.getItems().indexOf(newValue); + reminderListView.scrollTo(index); + reminderListView.getSelectionModel().clearAndSelect(index); + } + }); + } + + /** + * Displays the selected {@code Reminder} information on the main display. + * + * @param display the main display on the UI + * @param reminder the selected {@code Reminder} by mouse click + */ + private void listReminder(TextArea display, Reminder reminder) { + String reminderString = "---------------------------------------------------------------------------\n" + + "Displaying selected reminder:\n" + + "============================================\n" + + reminder.toString() + + "\n"; + display.appendText(reminderString); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Reminder} using a {@code ReminderCard}. + */ + class ReminderListViewCell extends ListCell { + @Override + protected void updateItem(Reminder reminder, boolean empty) { + super.updateItem(reminder, empty); + + if (empty || reminder == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new ReminderCard(reminder, getIndex() + 1).getRoot()); + String title = reminder.getTitle(); + Optional end = Optional.ofNullable(reminder.getEnd()); + String comment = reminder.getComment(); + + if (title.startsWith("Appointment with ") + && end.isPresent() + && !comment.isEmpty()) { + // Reminder is for an appointment + setStyle("-fx-control-inner-background: " + APPOINTMENT_BACKGROUND + ";"); + } else if (title.startsWith("Quantity of ") + && title.endsWith(" is low.") + && !end.isPresent() + && !comment.isEmpty()) { + // Reminder is for low medicine + setStyle("-fx-control-inner-background: " + MEDICINE_BACKGROUND + ";"); + } else { + setStyle("-fx-control-inner-background: " + OTHER_BACKGROUND + ";"); + } + + setOnMouseClicked(event -> listReminder(display, reminder)); + } + } + } +} diff --git a/src/main/java/seedu/address/ui/ResultDisplay.java b/src/main/java/quickdocs/ui/ResultDisplay.java similarity index 95% rename from src/main/java/seedu/address/ui/ResultDisplay.java rename to src/main/java/quickdocs/ui/ResultDisplay.java index 7d98e84eedf0..8b549918418e 100644 --- a/src/main/java/seedu/address/ui/ResultDisplay.java +++ b/src/main/java/quickdocs/ui/ResultDisplay.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package quickdocs.ui; import static java.util.Objects.requireNonNull; diff --git a/src/main/java/quickdocs/ui/RootLayoutController.java b/src/main/java/quickdocs/ui/RootLayoutController.java new file mode 100644 index 000000000000..55671766c866 --- /dev/null +++ b/src/main/java/quickdocs/ui/RootLayoutController.java @@ -0,0 +1,341 @@ +package quickdocs.ui; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.control.TextArea; +import javafx.scene.control.TextField; +import javafx.scene.input.KeyCode; +import javafx.scene.input.KeyEvent; +import javafx.scene.layout.StackPane; +import javafx.stage.Stage; +import quickdocs.logic.Logic; +import quickdocs.logic.commands.CommandResult; + +/** + * This class handles user interaction with the root layout + */ +public class RootLayoutController { + + private boolean suggestionOn = false; + private boolean isMedicineAllowed = false; + private ArrayList suggestions; + private Logic logicManager; + private ReminderListPanel reminderListPanel; + private List history; + private ListElementPointer historySnapshot; + + @FXML + private Stage primaryStage; + + @FXML + private TextArea resultDisplay; + + @FXML + private TextField userInputField; + + @FXML + private TextArea inputFeedbackArea; + + @FXML + private StackPane reminderList; + + @FXML + private Label currentSessionLabel; + + private HelpWindow helpWindow; + + public void setPrimaryStage(Stage primaryStage) { + this.primaryStage = primaryStage; + } + + /** + * Initialize the root layout controller with the logic manager, history and help window + * so they can be used when UI is interacted with + * + * @param logicManager initialised at MainApp + */ + public void initialiseRootLayout(Logic logicManager) { + this.logicManager = logicManager; + this.history = this.logicManager.getHistory(); + this.historySnapshot = new ListElementPointer(history); + this.helpWindow = new HelpWindow(); + } + + public TextArea getDisplay() { + return resultDisplay; + } + + /** + * Captures the user's entered command (when the user presses enter) and passes it to the logic + * logic will handle the parsing and execution and returns the result + * which will then be displayed on the resultDisplay control in the ui + * + * @param event Event associated with the user pressing enter to confirm a command + */ + @FXML + public void enterInput(KeyEvent event) { + if (event.getCode().equals(KeyCode.ENTER)) { + try { + inputFeedbackArea.setText(""); + CommandResult result = logicManager.execute(userInputField.getText()); + + // handling exit + if (result.isExit()) { + //primaryStage.close(); + handleExit(); + } + + // handling help + if (result.isShowHelp()) { + handleHelp(); + } + + // consultation session handling + indicateConsultation(result.getFeedbackToUser()); + endConsultation(result.getFeedbackToUser()); + + resultDisplay.appendText(">>> " + userInputField.getText() + "\n"); + resultDisplay.appendText( + "---------------------------------------------------------------------------\n"); + resultDisplay.appendText(result.getFeedbackToUser()); + resultDisplay.appendText("\n"); + + // move resultDisplay to the end to show result of last entered command + resultDisplay.selectPositionCaret(resultDisplay.getText().length()); + + // history handling + initHistory(); + historySnapshot.next(); + userInputField.setText(""); + + fillReminderList(); + } catch (NumberFormatException nfe) { + inputFeedbackArea.setText("Index entered is beyond valid range"); + } catch (Exception e) { + inputFeedbackArea.setText(e.getMessage()); + } + return; + } + + /** + * The following code handles both the command history and the suggestions of medicine filepath + * + * previously entered commands are navigable using the up and down directional keys + * suggestions are navigable using the Page-Up and Page-Down keys + */ + suggestionOn = logicManager.isDirectoryFormat(userInputField.getText()); + if (suggestionOn) { + isMedicineAllowed = logicManager.isMedicineAllowed(userInputField.getText()); + } + + switch (event.getCode()) { + case PAGE_UP: + event.consume(); + if (!suggestionOn) { + break; + } + + suggestions = logicManager.getDirectorySuggestions(userInputField.getText()); + if (isMedicineAllowed) { + suggestions.addAll(logicManager.getMedicineSuggestions(userInputField.getText())); + suggestions.sort(Comparator.comparing(String::toLowerCase)); + } + int pointer = getIndex(userInputField.getText(), suggestions); + if (pointer > 0) { + if (pointer % 2 == 0) { + userInputField.setText(processPath(userInputField.getText(), suggestions.get(pointer / 2 - 1))); + } else { + userInputField.setText(processPath(userInputField.getText(), suggestions.get(pointer / 2))); + } + } + userInputField.positionCaret(userInputField.getText().length()); + break; + case PAGE_DOWN: + event.consume(); + if (!suggestionOn) { + break; + } + suggestions = logicManager.getDirectorySuggestions(userInputField.getText()); + if (isMedicineAllowed) { + suggestions.addAll(logicManager.getMedicineSuggestions(userInputField.getText())); + suggestions.sort(Comparator.comparing(String::toLowerCase)); + } + pointer = getIndex(userInputField.getText(), suggestions); + if (pointer < 2 * (suggestions.size() - 1)) { + if (pointer % 2 == 0) { + userInputField.setText(processPath(userInputField.getText(), suggestions.get(pointer / 2 + 1))); + } else { + userInputField.setText(processPath(userInputField.getText(), suggestions.get((pointer + 1) / 2))); + } + } + userInputField.positionCaret(userInputField.getText().length()); + break; + case UP: + event.consume(); + navigateToPreviousInput(); + break; + case DOWN: + event.consume(); + navigateToNextInput(); + break; + default: + break; + } + } + + /** + * Get the position of current user input in the list of suggestions + * @param input current user input, starting from the next character from the last \ character + * @param suggestions list of suggestions + * @return A even number x if input equals suggestions[x/2]; + * an odd number, if input is larger than suggestions[(x-1)/2] but smaller than suggestions[(x+1)/2] + */ + private int getIndex(String input, ArrayList suggestions) { + String currentName = input.substring(input.lastIndexOf("\\") + 1).trim(); + if (currentName.equals("")) { + return -1; + } + int current = -1; + Comparator comparator = Comparator.comparing(String::toLowerCase); + while (true) { + if (current == (suggestions.size() - 1) * 2 + 1) { + break; + } + if (comparator.compare(suggestions.get((current + 1) / 2), currentName) > 0) { + break; + } + if (comparator.compare(suggestions.get((current + 1) / 2), currentName) == 0) { + current++; + break; + } + current += 2; + } + return current; + } + + private String processPath(String input, String idealName) { + String remain = input.substring(0, input.lastIndexOf("\\") + 1); + return remain + idealName; + } + + /** + * This method will check if the parameters entered into the command text field is valid + * by calling the various checkers across the module + * + * @param event Event associated with the user pressing space bar between parameters + */ + @FXML + public void checkInput(KeyEvent event) { + if (event.getCode() == KeyCode.SPACE) { + //inputFeedbackArea.setText("space entered"); + } + } + + /** + * Fills up all the placeholders of this window. + */ + public void fillReminderList() { + reminderListPanel = new ReminderListPanel(logicManager.getFilteredReminderList(), + logicManager.selectedReminderProperty(), logicManager::setSelectedReminder, this.getDisplay()); + reminderList.getChildren().add(reminderListPanel.getRoot()); + } + + public TextArea getResultDisplay() { + return this.resultDisplay; + } + + // history handling + + /** + * Initializes the history snapshot. + */ + private void initHistory() { + historySnapshot = new ListElementPointer(history); + // add an empty string to represent the most-recent end of historySnapshot, to be shown to + // the user if she tries to navigate past the most-recent end of the historySnapshot. + historySnapshot.add(""); + } + + + /** + * Updates the text field with the previous input in {@code historySnapshot}, + * if there exists a previous input in {@code historySnapshot} + */ + private void navigateToPreviousInput() { + assert historySnapshot != null; + if (!historySnapshot.hasPrevious()) { + return; + } + + replaceText(historySnapshot.previous()); + } + + /** + * Updates the text field with the next input in {@code historySnapshot}, + * if there exists a next input in {@code historySnapshot} + */ + private void navigateToNextInput() { + assert historySnapshot != null; + if (!historySnapshot.hasNext()) { + return; + } + + replaceText(historySnapshot.next()); + } + + /** + * Sets {@code CommandBox}'s text field with {@code text} and + * positions the caret to the end of the {@code text}. + */ + private void replaceText(String text) { + userInputField.setText(text); + userInputField.positionCaret(userInputField.getText().length()); + } + + /** + * First, check whether the command result is from the consultation command + * if it is, make label resultDisplay the ongoing session + * + * @param checkConsultation can be any command result from the various commands + */ + private void indicateConsultation(String checkConsultation) { + if (checkConsultation.contains("Consultation") + && checkConsultation.contains("started")) { + int colonPos = checkConsultation.indexOf(":"); + String nric = checkConsultation.substring(colonPos + 2, colonPos + 11); + currentSessionLabel.setText("Consultation ongoing for: " + nric); + } + } + + /** + * If command result indicates that consultation has ended + * make label disappear + * + * @param checkConsultation string result of command executed + */ + private void endConsultation(String checkConsultation) { + if ((checkConsultation.contains("Consultation") + && (checkConsultation.contains("ended") || checkConsultation.contains("aborted")))) { + currentSessionLabel.setText(""); + } + } + + public void handleExit() { + primaryStage.close(); + } + + /** + * Opens the help window or focuses on it if it's already opened. + */ + public void handleHelp() { + if (!helpWindow.isShowing()) { + helpWindow.show(); + } else { + helpWindow.focus(); + } + } +} diff --git a/src/main/java/seedu/address/ui/Ui.java b/src/main/java/quickdocs/ui/Ui.java similarity index 86% rename from src/main/java/seedu/address/ui/Ui.java rename to src/main/java/quickdocs/ui/Ui.java index 17aa0b494fe3..71543d580f35 100644 --- a/src/main/java/seedu/address/ui/Ui.java +++ b/src/main/java/quickdocs/ui/Ui.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package quickdocs.ui; import javafx.stage.Stage; diff --git a/src/main/java/seedu/address/ui/UiManager.java b/src/main/java/quickdocs/ui/UiManager.java similarity index 92% rename from src/main/java/seedu/address/ui/UiManager.java rename to src/main/java/quickdocs/ui/UiManager.java index 876621d79b94..2a64fc3f61f0 100644 --- a/src/main/java/seedu/address/ui/UiManager.java +++ b/src/main/java/quickdocs/ui/UiManager.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package quickdocs.ui; import java.util.logging.Logger; @@ -7,10 +7,10 @@ import javafx.scene.control.Alert.AlertType; import javafx.scene.image.Image; import javafx.stage.Stage; -import seedu.address.MainApp; -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.util.StringUtil; -import seedu.address.logic.Logic; +import quickdocs.MainApp; +import quickdocs.commons.core.LogsCenter; +import quickdocs.commons.util.StringUtil; +import quickdocs.logic.Logic; /** * The manager of the UI component. @@ -40,7 +40,6 @@ public void start(Stage primaryStage) { try { mainWindow = new MainWindow(primaryStage, logic); mainWindow.show(); //This should be called before creating other UI parts - mainWindow.fillInnerParts(); } catch (Throwable e) { logger.severe(StringUtil.getDetails(e)); diff --git a/src/main/java/seedu/address/ui/UiPart.java b/src/main/java/quickdocs/ui/UiPart.java similarity index 97% rename from src/main/java/seedu/address/ui/UiPart.java rename to src/main/java/quickdocs/ui/UiPart.java index fc820e01a9c3..c36930a86530 100644 --- a/src/main/java/seedu/address/ui/UiPart.java +++ b/src/main/java/quickdocs/ui/UiPart.java @@ -1,4 +1,4 @@ -package seedu.address.ui; +package quickdocs.ui; import static java.util.Objects.requireNonNull; @@ -6,7 +6,7 @@ import java.net.URL; import javafx.fxml.FXMLLoader; -import seedu.address.MainApp; +import quickdocs.MainApp; /** * Represents a distinct part of the UI. e.g. Windows, dialogs, panels, status bars, etc. diff --git a/src/main/java/seedu/address/commons/util/CollectionUtil.java b/src/main/java/seedu/address/commons/util/CollectionUtil.java deleted file mode 100644 index eafe4dfd6818..000000000000 --- a/src/main/java/seedu/address/commons/util/CollectionUtil.java +++ /dev/null @@ -1,35 +0,0 @@ -package seedu.address.commons.util; - -import static java.util.Objects.requireNonNull; - -import java.util.Arrays; -import java.util.Collection; -import java.util.Objects; -import java.util.stream.Stream; - -/** - * Utility methods related to Collections - */ -public class CollectionUtil { - - /** @see #requireAllNonNull(Collection) */ - public static void requireAllNonNull(Object... items) { - requireNonNull(items); - Stream.of(items).forEach(Objects::requireNonNull); - } - - /** - * Throws NullPointerException if {@code items} or any element of {@code items} is null. - */ - public static void requireAllNonNull(Collection items) { - requireNonNull(items); - items.forEach(Objects::requireNonNull); - } - - /** - * Returns true if {@code items} contain any elements that are non-null. - */ - public static boolean isAnyNonNull(Object... items) { - return items != null && Arrays.stream(items).anyMatch(Objects::nonNull); - } -} diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java deleted file mode 100644 index 60369e2074e4..000000000000 --- a/src/main/java/seedu/address/logic/Logic.java +++ /dev/null @@ -1,72 +0,0 @@ -package seedu.address.logic; - -import java.nio.file.Path; - -import javafx.beans.property.ReadOnlyProperty; -import javafx.collections.ObservableList; -import seedu.address.commons.core.GuiSettings; -import seedu.address.logic.commands.CommandResult; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Person; - -/** - * API of the Logic component - */ -public interface Logic { - /** - * Executes the command and returns the result. - * @param commandText The command as entered by the user. - * @return the result of the command execution. - * @throws CommandException If an error occurs during command execution. - * @throws ParseException If an error occurs during parsing. - */ - CommandResult execute(String commandText) throws CommandException, ParseException; - - /** - * Returns the AddressBook. - * - * @see seedu.address.model.Model#getAddressBook() - */ - ReadOnlyAddressBook getAddressBook(); - - /** Returns an unmodifiable view of the filtered list of persons */ - ObservableList getFilteredPersonList(); - - /** - * Returns an unmodifiable view of the list of commands entered by the user. - * The list is ordered from the least recent command to the most recent command. - */ - ObservableList getHistory(); - - /** - * Returns the user prefs' address book file path. - */ - Path getAddressBookFilePath(); - - /** - * Returns the user prefs' GUI settings. - */ - GuiSettings getGuiSettings(); - - /** - * Set the user prefs' GUI settings. - */ - void setGuiSettings(GuiSettings guiSettings); - - /** - * Selected person in the filtered person list. - * null if no person is selected. - * - * @see seedu.address.model.Model#selectedPersonProperty() - */ - ReadOnlyProperty selectedPersonProperty(); - - /** - * Sets the selected person in the filtered person list. - * - * @see seedu.address.model.Model#setSelectedPerson(Person) - */ - void setSelectedPerson(Person person); -} diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java deleted file mode 100644 index 5cb24a617beb..000000000000 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ /dev/null @@ -1,108 +0,0 @@ -package seedu.address.logic; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.logging.Logger; - -import javafx.beans.property.ReadOnlyProperty; -import javafx.collections.ObservableList; -import seedu.address.commons.core.GuiSettings; -import seedu.address.commons.core.LogsCenter; -import seedu.address.logic.commands.Command; -import seedu.address.logic.commands.CommandResult; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.logic.parser.AddressBookParser; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.Model; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Person; -import seedu.address.storage.Storage; - -/** - * The main LogicManager of the app. - */ -public class LogicManager implements Logic { - public static final String FILE_OPS_ERROR_MESSAGE = "Could not save data to file: "; - private final Logger logger = LogsCenter.getLogger(LogicManager.class); - - private final Model model; - private final Storage storage; - private final CommandHistory history; - private final AddressBookParser addressBookParser; - private boolean addressBookModified; - - public LogicManager(Model model, Storage storage) { - this.model = model; - this.storage = storage; - history = new CommandHistory(); - addressBookParser = new AddressBookParser(); - - // Set addressBookModified to true whenever the models' address book is modified. - model.getAddressBook().addListener(observable -> addressBookModified = true); - } - - @Override - public CommandResult execute(String commandText) throws CommandException, ParseException { - logger.info("----------------[USER COMMAND][" + commandText + "]"); - addressBookModified = false; - - CommandResult commandResult; - try { - Command command = addressBookParser.parseCommand(commandText); - commandResult = command.execute(model, history); - } finally { - history.add(commandText); - } - - if (addressBookModified) { - logger.info("Address book modified, saving to file."); - try { - storage.saveAddressBook(model.getAddressBook()); - } catch (IOException ioe) { - throw new CommandException(FILE_OPS_ERROR_MESSAGE + ioe, ioe); - } - } - - return commandResult; - } - - @Override - public ReadOnlyAddressBook getAddressBook() { - return model.getAddressBook(); - } - - @Override - public ObservableList getFilteredPersonList() { - return model.getFilteredPersonList(); - } - - @Override - public ObservableList getHistory() { - return history.getHistory(); - } - - @Override - public Path getAddressBookFilePath() { - return model.getAddressBookFilePath(); - } - - @Override - public GuiSettings getGuiSettings() { - return model.getGuiSettings(); - } - - @Override - public void setGuiSettings(GuiSettings guiSettings) { - model.setGuiSettings(guiSettings); - } - - @Override - public ReadOnlyProperty selectedPersonProperty() { - return model.selectedPersonProperty(); - } - - @Override - public void setSelectedPerson(Person person) { - model.setSelectedPerson(person); - } -} diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddCommand.java deleted file mode 100644 index d88e831ff1ce..000000000000 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ /dev/null @@ -1,69 +0,0 @@ -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_NAME; -import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; -import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; - -import seedu.address.logic.CommandHistory; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.Model; -import seedu.address.model.person.Person; - -/** - * Adds a person to the address book. - */ -public class AddCommand extends Command { - - public static final String COMMAND_WORD = "add"; - - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a person to the address book. " - + "Parameters: " - + PREFIX_NAME + "NAME " - + PREFIX_PHONE + "PHONE " - + PREFIX_EMAIL + "EMAIL " - + PREFIX_ADDRESS + "ADDRESS " - + "[" + PREFIX_TAG + "TAG]...\n" - + "Example: " + COMMAND_WORD + " " - + PREFIX_NAME + "John Doe " - + PREFIX_PHONE + "98765432 " - + PREFIX_EMAIL + "johnd@example.com " - + PREFIX_ADDRESS + "311, Clementi Ave 2, #02-25 " - + PREFIX_TAG + "friends " - + PREFIX_TAG + "owesMoney"; - - public static final String MESSAGE_SUCCESS = "New person added: %1$s"; - public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book"; - - private final Person toAdd; - - /** - * Creates an AddCommand to add the specified {@code Person} - */ - public AddCommand(Person person) { - requireNonNull(person); - toAdd = person; - } - - @Override - public CommandResult execute(Model model, CommandHistory history) throws CommandException { - requireNonNull(model); - - if (model.hasPerson(toAdd)) { - throw new CommandException(MESSAGE_DUPLICATE_PERSON); - } - - model.addPerson(toAdd); - model.commitAddressBook(); - return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd)); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof AddCommand // instanceof handles nulls - && toAdd.equals(((AddCommand) other).toAdd)); - } -} diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/address/logic/commands/ClearCommand.java deleted file mode 100644 index a22219ad76ad..000000000000 --- a/src/main/java/seedu/address/logic/commands/ClearCommand.java +++ /dev/null @@ -1,25 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; - -import seedu.address.logic.CommandHistory; -import seedu.address.model.AddressBook; -import seedu.address.model.Model; - -/** - * Clears the address book. - */ -public class ClearCommand extends Command { - - public static final String COMMAND_WORD = "clear"; - public static final String MESSAGE_SUCCESS = "Address book has been cleared!"; - - - @Override - public CommandResult execute(Model model, CommandHistory history) { - requireNonNull(model); - model.setAddressBook(new AddressBook()); - model.commitAddressBook(); - return new CommandResult(MESSAGE_SUCCESS); - } -} diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java deleted file mode 100644 index a20e9d49eac7..000000000000 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ /dev/null @@ -1,55 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; - -import java.util.List; - -import seedu.address.commons.core.Messages; -import seedu.address.commons.core.index.Index; -import seedu.address.logic.CommandHistory; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.Model; -import seedu.address.model.person.Person; - -/** - * Deletes a person identified using it's displayed index from the address book. - */ -public class DeleteCommand extends Command { - - public static final String COMMAND_WORD = "delete"; - - public static final String MESSAGE_USAGE = COMMAND_WORD - + ": Deletes the person identified by the index number used in the displayed person list.\n" - + "Parameters: INDEX (must be a positive integer)\n" - + "Example: " + COMMAND_WORD + " 1"; - - public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted Person: %1$s"; - - private final Index targetIndex; - - public DeleteCommand(Index targetIndex) { - this.targetIndex = targetIndex; - } - - @Override - public CommandResult execute(Model model, CommandHistory history) throws CommandException { - requireNonNull(model); - List lastShownList = model.getFilteredPersonList(); - - if (targetIndex.getZeroBased() >= lastShownList.size()) { - throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); - } - - Person personToDelete = lastShownList.get(targetIndex.getZeroBased()); - model.deletePerson(personToDelete); - model.commitAddressBook(); - return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, personToDelete)); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof DeleteCommand // instanceof handles nulls - && targetIndex.equals(((DeleteCommand) other).targetIndex)); // state check - } -} diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java deleted file mode 100644 index 952a9e7e7f2b..000000000000 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ /dev/null @@ -1,228 +0,0 @@ -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_NAME; -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_PERSONS; - -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; - -import seedu.address.commons.core.Messages; -import seedu.address.commons.core.index.Index; -import seedu.address.commons.util.CollectionUtil; -import seedu.address.logic.CommandHistory; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.Model; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Person; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; - -/** - * Edits the details of an existing person in the address book. - */ -public class EditCommand extends Command { - - public static final String COMMAND_WORD = "edit"; - - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the details of the person identified " - + "by the index number used in the displayed person list. " - + "Existing values will be overwritten by the input values.\n" - + "Parameters: INDEX (must be a positive integer) " - + "[" + PREFIX_NAME + "NAME] " - + "[" + PREFIX_PHONE + "PHONE] " - + "[" + PREFIX_EMAIL + "EMAIL] " - + "[" + PREFIX_ADDRESS + "ADDRESS] " - + "[" + PREFIX_TAG + "TAG]...\n" - + "Example: " + COMMAND_WORD + " 1 " - + PREFIX_PHONE + "91234567 " - + PREFIX_EMAIL + "johndoe@example.com"; - - 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 final Index index; - private final EditPersonDescriptor editPersonDescriptor; - - /** - * @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) { - requireNonNull(index); - requireNonNull(editPersonDescriptor); - - this.index = index; - this.editPersonDescriptor = new EditPersonDescriptor(editPersonDescriptor); - } - - @Override - public CommandResult execute(Model model, CommandHistory history) throws CommandException { - requireNonNull(model); - List lastShownList = model.getFilteredPersonList(); - - if (index.getZeroBased() >= lastShownList.size()) { - throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); - } - - Person personToEdit = lastShownList.get(index.getZeroBased()); - Person editedPerson = createEditedPerson(personToEdit, editPersonDescriptor); - - if (!personToEdit.isSamePerson(editedPerson) && model.hasPerson(editedPerson)) { - throw new CommandException(MESSAGE_DUPLICATE_PERSON); - } - - model.setPerson(personToEdit, editedPerson); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - model.commitAddressBook(); - return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, editedPerson)); - } - - /** - * Creates and returns a {@code Person} with the details of {@code personToEdit} - * edited with {@code editPersonDescriptor}. - */ - 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); - } - - @Override - public boolean equals(Object other) { - // short circuit if same object - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof EditCommand)) { - return false; - } - - // state check - EditCommand e = (EditCommand) other; - return index.equals(e.index) - && editPersonDescriptor.equals(e.editPersonDescriptor); - } - - /** - * Stores the details to edit the person with. Each non-empty field value will replace the - * corresponding field value of the person. - */ - public static class EditPersonDescriptor { - private Name name; - private Phone phone; - private Email email; - private Address address; - private Set tags; - - public EditPersonDescriptor() {} - - /** - * Copy constructor. - * A defensive copy of {@code tags} is used internally. - */ - public EditPersonDescriptor(EditPersonDescriptor toCopy) { - setName(toCopy.name); - setPhone(toCopy.phone); - setEmail(toCopy.email); - setAddress(toCopy.address); - setTags(toCopy.tags); - } - - /** - * Returns true if at least one field is edited. - */ - public boolean isAnyFieldEdited() { - return CollectionUtil.isAnyNonNull(name, phone, email, address, tags); - } - - public void setName(Name name) { - this.name = name; - } - - public Optional getName() { - return Optional.ofNullable(name); - } - - public void setPhone(Phone phone) { - this.phone = phone; - } - - public Optional getPhone() { - return Optional.ofNullable(phone); - } - - public void setEmail(Email email) { - this.email = email; - } - - public Optional getEmail() { - return Optional.ofNullable(email); - } - - public void setAddress(Address address) { - this.address = address; - } - - public Optional
getAddress() { - return Optional.ofNullable(address); - } - - /** - * Sets {@code tags} to this object's {@code tags}. - * A defensive copy of {@code tags} is used internally. - */ - public void setTags(Set tags) { - this.tags = (tags != null) ? new HashSet<>(tags) : null; - } - - /** - * Returns an unmodifiable tag set, which throws {@code UnsupportedOperationException} - * if modification is attempted. - * Returns {@code Optional#empty()} if {@code tags} is null. - */ - public Optional> getTags() { - return (tags != null) ? Optional.of(Collections.unmodifiableSet(tags)) : Optional.empty(); - } - - @Override - public boolean equals(Object other) { - // short circuit if same object - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof EditPersonDescriptor)) { - return false; - } - - // state check - EditPersonDescriptor e = (EditPersonDescriptor) other; - - return getName().equals(e.getName()) - && getPhone().equals(e.getPhone()) - && getEmail().equals(e.getEmail()) - && getAddress().equals(e.getAddress()) - && getTags().equals(e.getTags()); - } - } -} diff --git a/src/main/java/seedu/address/logic/commands/FindCommand.java b/src/main/java/seedu/address/logic/commands/FindCommand.java deleted file mode 100644 index beb178e3a3f5..000000000000 --- a/src/main/java/seedu/address/logic/commands/FindCommand.java +++ /dev/null @@ -1,43 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; - -import seedu.address.commons.core.Messages; -import seedu.address.logic.CommandHistory; -import seedu.address.model.Model; -import seedu.address.model.person.NameContainsKeywordsPredicate; - -/** - * Finds and lists all persons in address book whose name contains any of the argument keywords. - * Keyword matching is case insensitive. - */ -public class FindCommand extends Command { - - public static final String COMMAND_WORD = "find"; - - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose names contain 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 final NameContainsKeywordsPredicate predicate; - - public FindCommand(NameContainsKeywordsPredicate predicate) { - this.predicate = predicate; - } - - @Override - public CommandResult execute(Model model, CommandHistory history) { - requireNonNull(model); - model.updateFilteredPersonList(predicate); - return new CommandResult( - String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size())); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof FindCommand // instanceof handles nulls - && predicate.equals(((FindCommand) other).predicate)); // state check - } -} diff --git a/src/main/java/seedu/address/logic/commands/ListCommand.java b/src/main/java/seedu/address/logic/commands/ListCommand.java deleted file mode 100644 index 6d44824c7d1b..000000000000 --- a/src/main/java/seedu/address/logic/commands/ListCommand.java +++ /dev/null @@ -1,25 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; -import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; - -import seedu.address.logic.CommandHistory; -import seedu.address.model.Model; - -/** - * Lists all persons in the address book to the user. - */ -public class ListCommand extends Command { - - public static final String COMMAND_WORD = "list"; - - public static final String MESSAGE_SUCCESS = "Listed all persons"; - - - @Override - public CommandResult execute(Model model, CommandHistory history) { - requireNonNull(model); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - return new CommandResult(MESSAGE_SUCCESS); - } -} diff --git a/src/main/java/seedu/address/logic/commands/RedoCommand.java b/src/main/java/seedu/address/logic/commands/RedoCommand.java deleted file mode 100644 index 227771a4eef6..000000000000 --- a/src/main/java/seedu/address/logic/commands/RedoCommand.java +++ /dev/null @@ -1,31 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; -import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; - -import seedu.address.logic.CommandHistory; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.Model; - -/** - * Reverts the {@code model}'s address book to its previously undone state. - */ -public class RedoCommand extends Command { - - public static final String COMMAND_WORD = "redo"; - public static final String MESSAGE_SUCCESS = "Redo success!"; - public static final String MESSAGE_FAILURE = "No more commands to redo!"; - - @Override - public CommandResult execute(Model model, CommandHistory history) throws CommandException { - requireNonNull(model); - - if (!model.canRedoAddressBook()) { - throw new CommandException(MESSAGE_FAILURE); - } - - model.redoAddressBook(); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - return new CommandResult(MESSAGE_SUCCESS); - } -} diff --git a/src/main/java/seedu/address/logic/commands/SelectCommand.java b/src/main/java/seedu/address/logic/commands/SelectCommand.java deleted file mode 100644 index baa3c1f30bb4..000000000000 --- a/src/main/java/seedu/address/logic/commands/SelectCommand.java +++ /dev/null @@ -1,55 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; - -import java.util.List; - -import seedu.address.commons.core.Messages; -import seedu.address.commons.core.index.Index; -import seedu.address.logic.CommandHistory; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.Model; -import seedu.address.model.person.Person; - -/** - * Selects a person identified using it's displayed index from the address book. - */ -public class SelectCommand extends Command { - - public static final String COMMAND_WORD = "select"; - - public static final String MESSAGE_USAGE = COMMAND_WORD - + ": Selects the person identified by the index number used in the displayed person list.\n" - + "Parameters: INDEX (must be a positive integer)\n" - + "Example: " + COMMAND_WORD + " 1"; - - public static final String MESSAGE_SELECT_PERSON_SUCCESS = "Selected Person: %1$s"; - - private final Index targetIndex; - - public SelectCommand(Index targetIndex) { - this.targetIndex = targetIndex; - } - - @Override - public CommandResult execute(Model model, CommandHistory history) throws CommandException { - requireNonNull(model); - - List filteredPersonList = model.getFilteredPersonList(); - - if (targetIndex.getZeroBased() >= filteredPersonList.size()) { - throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); - } - - model.setSelectedPerson(filteredPersonList.get(targetIndex.getZeroBased())); - return new CommandResult(String.format(MESSAGE_SELECT_PERSON_SUCCESS, targetIndex.getOneBased())); - - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof SelectCommand // instanceof handles nulls - && targetIndex.equals(((SelectCommand) other).targetIndex)); // state check - } -} diff --git a/src/main/java/seedu/address/logic/commands/UndoCommand.java b/src/main/java/seedu/address/logic/commands/UndoCommand.java deleted file mode 100644 index 40441264f346..000000000000 --- a/src/main/java/seedu/address/logic/commands/UndoCommand.java +++ /dev/null @@ -1,31 +0,0 @@ -package seedu.address.logic.commands; - -import static java.util.Objects.requireNonNull; -import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; - -import seedu.address.logic.CommandHistory; -import seedu.address.logic.commands.exceptions.CommandException; -import seedu.address.model.Model; - -/** - * Reverts the {@code model}'s address book to its previous state. - */ -public class UndoCommand extends Command { - - public static final String COMMAND_WORD = "undo"; - public static final String MESSAGE_SUCCESS = "Undo success!"; - public static final String MESSAGE_FAILURE = "No more commands to undo!"; - - @Override - public CommandResult execute(Model model, CommandHistory history) throws CommandException { - requireNonNull(model); - - if (!model.canUndoAddressBook()) { - throw new CommandException(MESSAGE_FAILURE); - } - - model.undoAddressBook(); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - return new CommandResult(MESSAGE_SUCCESS); - } -} diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddCommandParser.java deleted file mode 100644 index 3b8bfa035e83..000000000000 --- a/src/main/java/seedu/address/logic/parser/AddCommandParser.java +++ /dev/null @@ -1,60 +0,0 @@ -package seedu.address.logic.parser; - -import static seedu.address.commons.core.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_NAME; -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.parser.exceptions.ParseException; -import seedu.address.model.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Person; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; - -/** - * Parses input arguments and creates a new AddCommand object - */ -public class AddCommandParser 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 { - 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) - || !argMultimap.getPreamble().isEmpty()) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); - } - - Name name = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()); - Phone phone = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get()); - Email email = ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get()); - Address address = ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get()); - Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); - - Person person = new Person(name, phone, email, address, tagList); - - return new AddCommand(person); - } - - /** - * 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 deleted file mode 100644 index b7d57f5db86a..000000000000 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ /dev/null @@ -1,92 +0,0 @@ -package seedu.address.logic.parser; - -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.commons.core.Messages.MESSAGE_UNKNOWN_COMMAND; - -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import seedu.address.logic.commands.AddCommand; -import seedu.address.logic.commands.ClearCommand; -import seedu.address.logic.commands.Command; -import seedu.address.logic.commands.DeleteCommand; -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.HistoryCommand; -import seedu.address.logic.commands.ListCommand; -import seedu.address.logic.commands.RedoCommand; -import seedu.address.logic.commands.SelectCommand; -import seedu.address.logic.commands.UndoCommand; -import seedu.address.logic.parser.exceptions.ParseException; - -/** - * Parses user input. - */ -public class AddressBookParser { - - /** - * Used for initial separation of command word and args. - */ - private static final Pattern BASIC_COMMAND_FORMAT = Pattern.compile("(?\\S+)(?.*)"); - - /** - * Parses user input into command for execution. - * - * @param userInput full user input string - * @return the command based on the user input - * @throws ParseException if the user input does not conform the expected format - */ - public Command parseCommand(String userInput) throws ParseException { - final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim()); - if (!matcher.matches()) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE)); - } - - final String commandWord = matcher.group("commandWord"); - final String arguments = matcher.group("arguments"); - switch (commandWord) { - - case AddCommand.COMMAND_WORD: - return new AddCommandParser().parse(arguments); - - case EditCommand.COMMAND_WORD: - return new EditCommandParser().parse(arguments); - - case SelectCommand.COMMAND_WORD: - return new SelectCommandParser().parse(arguments); - - case DeleteCommand.COMMAND_WORD: - return new DeleteCommandParser().parse(arguments); - - case ClearCommand.COMMAND_WORD: - return new ClearCommand(); - - case FindCommand.COMMAND_WORD: - return new FindCommandParser().parse(arguments); - - case ListCommand.COMMAND_WORD: - return new ListCommand(); - - case HistoryCommand.COMMAND_WORD: - return new HistoryCommand(); - - case ExitCommand.COMMAND_WORD: - return new ExitCommand(); - - case HelpCommand.COMMAND_WORD: - return new HelpCommand(); - - case UndoCommand.COMMAND_WORD: - return new UndoCommand(); - - case RedoCommand.COMMAND_WORD: - return new RedoCommand(); - - default: - throw new ParseException(MESSAGE_UNKNOWN_COMMAND); - } - } - -} diff --git a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java deleted file mode 100644 index 4d1f4bb0e4ec..000000000000 --- a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java +++ /dev/null @@ -1,29 +0,0 @@ -package seedu.address.logic.parser; - -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; - -import seedu.address.commons.core.index.Index; -import seedu.address.logic.commands.DeleteCommand; -import seedu.address.logic.parser.exceptions.ParseException; - -/** - * Parses input arguments and creates a new DeleteCommand object - */ -public class DeleteCommandParser implements Parser { - - /** - * Parses the given {@code String} of arguments in the context of the DeleteCommand - * and returns an DeleteCommand object for execution. - * @throws ParseException if the user input does not conform the expected format - */ - public DeleteCommand parse(String args) throws ParseException { - try { - Index index = ParserUtil.parseIndex(args); - return new DeleteCommand(index); - } catch (ParseException pe) { - throw new ParseException( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE), pe); - } - } - -} diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/seedu/address/logic/parser/EditCommandParser.java deleted file mode 100644 index 845644b7dea1..000000000000 --- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java +++ /dev/null @@ -1,82 +0,0 @@ -package seedu.address.logic.parser; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.core.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_NAME; -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 - */ -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); - - Index index; - - try { - index = ParserUtil.parseIndex(argMultimap.getPreamble()); - } catch (ParseException pe) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE), pe); - } - - EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor(); - if (argMultimap.getValue(PREFIX_NAME).isPresent()) { - editPersonDescriptor.setName(ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get())); - } - 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_ADDRESS).isPresent()) { - editPersonDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); - } - parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); - - if (!editPersonDescriptor.isAnyFieldEdited()) { - throw new ParseException(EditCommand.MESSAGE_NOT_EDITED); - } - - 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/FindCommandParser.java b/src/main/java/seedu/address/logic/parser/FindCommandParser.java deleted file mode 100644 index b186a967cb94..000000000000 --- a/src/main/java/seedu/address/logic/parser/FindCommandParser.java +++ /dev/null @@ -1,33 +0,0 @@ -package seedu.address.logic.parser; - -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; - -import java.util.Arrays; - -import seedu.address.logic.commands.FindCommand; -import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.NameContainsKeywordsPredicate; - -/** - * Parses input arguments and creates a new FindCommand object - */ -public class FindCommandParser implements Parser { - - /** - * Parses the given {@code String} of arguments in the context of the FindCommand - * and returns an FindCommand object for execution. - * @throws ParseException if the user input does not conform the expected format - */ - public FindCommand parse(String args) throws ParseException { - String trimmedArgs = args.trim(); - if (trimmedArgs.isEmpty()) { - throw new ParseException( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); - } - - String[] nameKeywords = trimmedArgs.split("\\s+"); - - return new FindCommand(new NameContainsKeywordsPredicate(Arrays.asList(nameKeywords))); - } - -} diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java deleted file mode 100644 index b117acb9c55b..000000000000 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ /dev/null @@ -1,124 +0,0 @@ -package seedu.address.logic.parser; - -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.person.Address; -import seedu.address.model.person.Email; -import seedu.address.model.person.Name; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; - -/** - * Contains utility methods used for parsing strings in the various *Parser classes. - */ -public class ParserUtil { - - public static final String MESSAGE_INVALID_INDEX = "Index is not a non-zero unsigned integer."; - - /** - * Parses {@code oneBasedIndex} into an {@code Index} and returns it. Leading and trailing whitespaces will be - * trimmed. - * @throws ParseException if the specified index is invalid (not non-zero unsigned integer). - */ - public static Index parseIndex(String oneBasedIndex) throws ParseException { - String trimmedIndex = oneBasedIndex.trim(); - if (!StringUtil.isNonZeroUnsignedInteger(trimmedIndex)) { - throw new ParseException(MESSAGE_INVALID_INDEX); - } - return Index.fromOneBased(Integer.parseInt(trimmedIndex)); - } - - /** - * Parses a {@code String name} into a {@code Name}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code name} is invalid. - */ - public static Name parseName(String name) throws ParseException { - requireNonNull(name); - String trimmedName = name.trim(); - if (!Name.isValidName(trimmedName)) { - throw new ParseException(Name.MESSAGE_CONSTRAINTS); - } - return new Name(trimmedName); - } - - /** - * Parses a {@code String phone} into a {@code Phone}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code phone} is invalid. - */ - public static Phone parsePhone(String phone) throws ParseException { - requireNonNull(phone); - String trimmedPhone = phone.trim(); - if (!Phone.isValidPhone(trimmedPhone)) { - throw new ParseException(Phone.MESSAGE_CONSTRAINTS); - } - return new Phone(trimmedPhone); - } - - /** - * Parses a {@code String address} into an {@code Address}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code address} is invalid. - */ - public static Address parseAddress(String address) throws ParseException { - requireNonNull(address); - String trimmedAddress = address.trim(); - if (!Address.isValidAddress(trimmedAddress)) { - throw new ParseException(Address.MESSAGE_CONSTRAINTS); - } - return new Address(trimmedAddress); - } - - /** - * Parses a {@code String email} into an {@code Email}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code email} is invalid. - */ - public static Email parseEmail(String email) throws ParseException { - requireNonNull(email); - String trimmedEmail = email.trim(); - if (!Email.isValidEmail(trimmedEmail)) { - throw new ParseException(Email.MESSAGE_CONSTRAINTS); - } - return new Email(trimmedEmail); - } - - /** - * Parses a {@code String tag} into a {@code Tag}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code tag} is invalid. - */ - public static Tag parseTag(String tag) throws ParseException { - requireNonNull(tag); - String trimmedTag = tag.trim(); - if (!Tag.isValidTagName(trimmedTag)) { - throw new ParseException(Tag.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)); - } - return tagSet; - } -} diff --git a/src/main/java/seedu/address/logic/parser/SelectCommandParser.java b/src/main/java/seedu/address/logic/parser/SelectCommandParser.java deleted file mode 100644 index 565b7f04bfe1..000000000000 --- a/src/main/java/seedu/address/logic/parser/SelectCommandParser.java +++ /dev/null @@ -1,28 +0,0 @@ -package seedu.address.logic.parser; - -import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; - -import seedu.address.commons.core.index.Index; -import seedu.address.logic.commands.SelectCommand; -import seedu.address.logic.parser.exceptions.ParseException; - -/** - * Parses input arguments and creates a new SelectCommand object - */ -public class SelectCommandParser implements Parser { - - /** - * Parses the given {@code String} of arguments in the context of the SelectCommand - * and returns an SelectCommand object for execution. - * @throws ParseException if the user input does not conform the expected format - */ - public SelectCommand parse(String args) throws ParseException { - try { - Index index = ParserUtil.parseIndex(args); - return new SelectCommand(index); - } catch (ParseException pe) { - throw new ParseException( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, SelectCommand.MESSAGE_USAGE), pe); - } - } -} diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java deleted file mode 100644 index 30557cf81ee7..000000000000 --- a/src/main/java/seedu/address/model/AddressBook.java +++ /dev/null @@ -1,144 +0,0 @@ -package seedu.address.model; - -import static java.util.Objects.requireNonNull; - -import java.util.List; - -import javafx.beans.InvalidationListener; -import javafx.collections.ObservableList; -import seedu.address.commons.util.InvalidationListenerManager; -import seedu.address.model.person.Person; -import seedu.address.model.person.UniquePersonList; - -/** - * Wraps all data at the address-book level - * Duplicates are not allowed (by .isSamePerson comparison) - */ -public class AddressBook implements ReadOnlyAddressBook { - - private final UniquePersonList persons; - private final InvalidationListenerManager invalidationListenerManager = new InvalidationListenerManager(); - - /* - * The 'unusual' code block below is an non-static initialization block, sometimes used to avoid duplication - * between constructors. See https://docs.oracle.com/javase/tutorial/java/javaOO/initial.html - * - * Note that non-static init blocks are not recommended to use. There are other ways to avoid duplication - * among constructors. - */ - { - persons = new UniquePersonList(); - } - - public AddressBook() {} - - /** - * Creates an AddressBook using the Persons in the {@code toBeCopied} - */ - public AddressBook(ReadOnlyAddressBook toBeCopied) { - this(); - resetData(toBeCopied); - } - - //// list overwrite operations - - /** - * Replaces the contents of the person list with {@code persons}. - * {@code persons} must not contain duplicate persons. - */ - public void setPersons(List persons) { - this.persons.setPersons(persons); - indicateModified(); - } - - /** - * Resets the existing data of this {@code AddressBook} with {@code newData}. - */ - public void resetData(ReadOnlyAddressBook newData) { - requireNonNull(newData); - - setPersons(newData.getPersonList()); - } - - //// person-level operations - - /** - * Returns true if a person with the same identity as {@code person} exists in the address book. - */ - public boolean hasPerson(Person person) { - requireNonNull(person); - return persons.contains(person); - } - - /** - * Adds a person to the address book. - * The person must not already exist in the address book. - */ - public void addPerson(Person p) { - persons.add(p); - indicateModified(); - } - - /** - * Replaces the given person {@code target} in the list with {@code editedPerson}. - * {@code target} must exist in the address book. - * The person identity of {@code editedPerson} must not be the same as another existing person in the address book. - */ - public void setPerson(Person target, Person editedPerson) { - requireNonNull(editedPerson); - - persons.setPerson(target, editedPerson); - indicateModified(); - } - - /** - * Removes {@code key} from this {@code AddressBook}. - * {@code key} must exist in the address book. - */ - public void removePerson(Person key) { - persons.remove(key); - indicateModified(); - } - - @Override - public void addListener(InvalidationListener listener) { - invalidationListenerManager.addListener(listener); - } - - @Override - public void removeListener(InvalidationListener listener) { - invalidationListenerManager.removeListener(listener); - } - - /** - * Notifies listeners that the address book has been modified. - */ - protected void indicateModified() { - invalidationListenerManager.callListeners(this); - } - - //// util methods - - @Override - public String toString() { - return persons.asUnmodifiableObservableList().size() + " persons"; - // TODO: refine later - } - - @Override - public ObservableList getPersonList() { - return persons.asUnmodifiableObservableList(); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof AddressBook // instanceof handles nulls - && persons.equals(((AddressBook) other).persons)); - } - - @Override - public int hashCode() { - return persons.hashCode(); - } -} diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java deleted file mode 100644 index e857533821b6..000000000000 --- a/src/main/java/seedu/address/model/Model.java +++ /dev/null @@ -1,130 +0,0 @@ -package seedu.address.model; - -import java.nio.file.Path; -import java.util.function.Predicate; - -import javafx.beans.property.ReadOnlyProperty; -import javafx.collections.ObservableList; -import seedu.address.commons.core.GuiSettings; -import seedu.address.model.person.Person; - -/** - * The API of the Model component. - */ -public interface Model { - /** {@code Predicate} that always evaluate to true */ - Predicate PREDICATE_SHOW_ALL_PERSONS = unused -> true; - - /** - * Replaces user prefs data with the data in {@code userPrefs}. - */ - void setUserPrefs(ReadOnlyUserPrefs userPrefs); - - /** - * Returns the user prefs. - */ - ReadOnlyUserPrefs getUserPrefs(); - - /** - * Returns the user prefs' GUI settings. - */ - GuiSettings getGuiSettings(); - - /** - * Sets the user prefs' GUI settings. - */ - void setGuiSettings(GuiSettings guiSettings); - - /** - * Returns the user prefs' address book file path. - */ - Path getAddressBookFilePath(); - - /** - * Sets the user prefs' address book file path. - */ - void setAddressBookFilePath(Path addressBookFilePath); - - /** - * Replaces address book data with the data in {@code addressBook}. - */ - void setAddressBook(ReadOnlyAddressBook addressBook); - - /** Returns the AddressBook */ - ReadOnlyAddressBook getAddressBook(); - - /** - * Returns true if a person with the same identity as {@code person} exists in the address book. - */ - boolean hasPerson(Person person); - - /** - * Deletes the given person. - * The person must exist in the address book. - */ - void deletePerson(Person target); - - /** - * Adds the given person. - * {@code person} must not already exist in the address book. - */ - void addPerson(Person person); - - /** - * Replaces the given person {@code target} with {@code editedPerson}. - * {@code target} must exist in the address book. - * The person identity of {@code editedPerson} must not be the same as another existing person in the address book. - */ - void setPerson(Person target, Person editedPerson); - - /** Returns an unmodifiable view of the filtered person list */ - ObservableList getFilteredPersonList(); - - /** - * Updates the filter of the filtered person list to filter by the given {@code predicate}. - * @throws NullPointerException if {@code predicate} is null. - */ - void updateFilteredPersonList(Predicate predicate); - - /** - * Returns true if the model has previous address book states to restore. - */ - boolean canUndoAddressBook(); - - /** - * Returns true if the model has undone address book states to restore. - */ - boolean canRedoAddressBook(); - - /** - * Restores the model's address book to its previous state. - */ - void undoAddressBook(); - - /** - * Restores the model's address book to its previously undone state. - */ - void redoAddressBook(); - - /** - * Saves the current address book state for undo/redo. - */ - void commitAddressBook(); - - /** - * Selected person in the filtered person list. - * null if no person is selected. - */ - ReadOnlyProperty selectedPersonProperty(); - - /** - * Returns the selected person in the filtered person list. - * null if no person is selected. - */ - Person getSelectedPerson(); - - /** - * Sets the selected person in the filtered person list. - */ - void setSelectedPerson(Person person); -} diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java deleted file mode 100644 index b56806232814..000000000000 --- a/src/main/java/seedu/address/model/ModelManager.java +++ /dev/null @@ -1,235 +0,0 @@ -package seedu.address.model; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; - -import java.nio.file.Path; -import java.util.Objects; -import java.util.function.Predicate; -import java.util.logging.Logger; - -import javafx.beans.property.ReadOnlyProperty; -import javafx.beans.property.SimpleObjectProperty; -import javafx.collections.ListChangeListener; -import javafx.collections.ObservableList; -import javafx.collections.transformation.FilteredList; -import seedu.address.commons.core.GuiSettings; -import seedu.address.commons.core.LogsCenter; -import seedu.address.model.person.Person; -import seedu.address.model.person.exceptions.PersonNotFoundException; - -/** - * Represents the in-memory model of the address book data. - */ -public class ModelManager implements Model { - private static final Logger logger = LogsCenter.getLogger(ModelManager.class); - - private final VersionedAddressBook versionedAddressBook; - private final UserPrefs userPrefs; - private final FilteredList filteredPersons; - private final SimpleObjectProperty selectedPerson = new SimpleObjectProperty<>(); - - /** - * Initializes a ModelManager with the given addressBook and userPrefs. - */ - public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyUserPrefs userPrefs) { - super(); - requireAllNonNull(addressBook, userPrefs); - - logger.fine("Initializing with address book: " + addressBook + " and user prefs " + userPrefs); - - versionedAddressBook = new VersionedAddressBook(addressBook); - this.userPrefs = new UserPrefs(userPrefs); - filteredPersons = new FilteredList<>(versionedAddressBook.getPersonList()); - filteredPersons.addListener(this::ensureSelectedPersonIsValid); - } - - public ModelManager() { - this(new AddressBook(), new UserPrefs()); - } - - //=========== UserPrefs ================================================================================== - - @Override - public void setUserPrefs(ReadOnlyUserPrefs userPrefs) { - requireNonNull(userPrefs); - this.userPrefs.resetData(userPrefs); - } - - @Override - public ReadOnlyUserPrefs getUserPrefs() { - return userPrefs; - } - - @Override - public GuiSettings getGuiSettings() { - return userPrefs.getGuiSettings(); - } - - @Override - public void setGuiSettings(GuiSettings guiSettings) { - requireNonNull(guiSettings); - userPrefs.setGuiSettings(guiSettings); - } - - @Override - public Path getAddressBookFilePath() { - return userPrefs.getAddressBookFilePath(); - } - - @Override - public void setAddressBookFilePath(Path addressBookFilePath) { - requireNonNull(addressBookFilePath); - userPrefs.setAddressBookFilePath(addressBookFilePath); - } - - //=========== AddressBook ================================================================================ - - @Override - public void setAddressBook(ReadOnlyAddressBook addressBook) { - versionedAddressBook.resetData(addressBook); - } - - @Override - public ReadOnlyAddressBook getAddressBook() { - return versionedAddressBook; - } - - @Override - public boolean hasPerson(Person person) { - requireNonNull(person); - return versionedAddressBook.hasPerson(person); - } - - @Override - public void deletePerson(Person target) { - versionedAddressBook.removePerson(target); - } - - @Override - public void addPerson(Person person) { - versionedAddressBook.addPerson(person); - updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - } - - @Override - public void setPerson(Person target, Person editedPerson) { - requireAllNonNull(target, editedPerson); - - versionedAddressBook.setPerson(target, editedPerson); - } - - //=========== Filtered Person List Accessors ============================================================= - - /** - * Returns an unmodifiable view of the list of {@code Person} backed by the internal list of - * {@code versionedAddressBook} - */ - @Override - public ObservableList getFilteredPersonList() { - return filteredPersons; - } - - @Override - public void updateFilteredPersonList(Predicate predicate) { - requireNonNull(predicate); - filteredPersons.setPredicate(predicate); - } - - //=========== Undo/Redo ================================================================================= - - @Override - public boolean canUndoAddressBook() { - return versionedAddressBook.canUndo(); - } - - @Override - public boolean canRedoAddressBook() { - return versionedAddressBook.canRedo(); - } - - @Override - public void undoAddressBook() { - versionedAddressBook.undo(); - } - - @Override - public void redoAddressBook() { - versionedAddressBook.redo(); - } - - @Override - public void commitAddressBook() { - versionedAddressBook.commit(); - } - - //=========== Selected person =========================================================================== - - @Override - public ReadOnlyProperty selectedPersonProperty() { - return selectedPerson; - } - - @Override - public Person getSelectedPerson() { - return selectedPerson.getValue(); - } - - @Override - public void setSelectedPerson(Person person) { - if (person != null && !filteredPersons.contains(person)) { - throw new PersonNotFoundException(); - } - selectedPerson.setValue(person); - } - - /** - * Ensures {@code selectedPerson} is a valid person in {@code filteredPersons}. - */ - private void ensureSelectedPersonIsValid(ListChangeListener.Change change) { - while (change.next()) { - if (selectedPerson.getValue() == null) { - // null is always a valid selected person, so we do not need to check that it is valid anymore. - return; - } - - boolean wasSelectedPersonReplaced = change.wasReplaced() && change.getAddedSize() == change.getRemovedSize() - && change.getRemoved().contains(selectedPerson.getValue()); - if (wasSelectedPersonReplaced) { - // Update selectedPerson to its new value. - int index = change.getRemoved().indexOf(selectedPerson.getValue()); - selectedPerson.setValue(change.getAddedSubList().get(index)); - continue; - } - - boolean wasSelectedPersonRemoved = change.getRemoved().stream() - .anyMatch(removedPerson -> selectedPerson.getValue().isSamePerson(removedPerson)); - if (wasSelectedPersonRemoved) { - // Select the person that came before it in the list, - // or clear the selection if there is no such person. - selectedPerson.setValue(change.getFrom() > 0 ? change.getList().get(change.getFrom() - 1) : null); - } - } - } - - @Override - public boolean equals(Object obj) { - // short circuit if same object - if (obj == this) { - return true; - } - - // instanceof handles nulls - if (!(obj instanceof ModelManager)) { - return false; - } - - // state check - ModelManager other = (ModelManager) obj; - return versionedAddressBook.equals(other.versionedAddressBook) - && userPrefs.equals(other.userPrefs) - && filteredPersons.equals(other.filteredPersons) - && Objects.equals(selectedPerson.get(), other.selectedPerson.get()); - } - -} diff --git a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java deleted file mode 100644 index 6a301434b33b..000000000000 --- a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java +++ /dev/null @@ -1,18 +0,0 @@ -package seedu.address.model; - -import javafx.beans.Observable; -import javafx.collections.ObservableList; -import seedu.address.model.person.Person; - -/** - * Unmodifiable view of an address book - */ -public interface ReadOnlyAddressBook extends Observable { - - /** - * Returns an unmodifiable view of the persons list. - * This list will not contain any duplicate persons. - */ - ObservableList getPersonList(); - -} diff --git a/src/main/java/seedu/address/model/VersionedAddressBook.java b/src/main/java/seedu/address/model/VersionedAddressBook.java deleted file mode 100644 index e17a9e3ba4ab..000000000000 --- a/src/main/java/seedu/address/model/VersionedAddressBook.java +++ /dev/null @@ -1,110 +0,0 @@ -package seedu.address.model; - -import java.util.ArrayList; -import java.util.List; - -/** - * {@code AddressBook} that keeps track of its own history. - */ -public class VersionedAddressBook extends AddressBook { - - private final List addressBookStateList; - private int currentStatePointer; - - public VersionedAddressBook(ReadOnlyAddressBook initialState) { - super(initialState); - - addressBookStateList = new ArrayList<>(); - addressBookStateList.add(new AddressBook(initialState)); - currentStatePointer = 0; - } - - /** - * Saves a copy of the current {@code AddressBook} state at the end of the state list. - * Undone states are removed from the state list. - */ - public void commit() { - removeStatesAfterCurrentPointer(); - addressBookStateList.add(new AddressBook(this)); - currentStatePointer++; - indicateModified(); - } - - private void removeStatesAfterCurrentPointer() { - addressBookStateList.subList(currentStatePointer + 1, addressBookStateList.size()).clear(); - } - - /** - * Restores the address book to its previous state. - */ - public void undo() { - if (!canUndo()) { - throw new NoUndoableStateException(); - } - currentStatePointer--; - resetData(addressBookStateList.get(currentStatePointer)); - } - - /** - * Restores the address book to its previously undone state. - */ - public void redo() { - if (!canRedo()) { - throw new NoRedoableStateException(); - } - currentStatePointer++; - resetData(addressBookStateList.get(currentStatePointer)); - } - - /** - * Returns true if {@code undo()} has address book states to undo. - */ - public boolean canUndo() { - return currentStatePointer > 0; - } - - /** - * Returns true if {@code redo()} has address book states to redo. - */ - public boolean canRedo() { - return currentStatePointer < addressBookStateList.size() - 1; - } - - @Override - public boolean equals(Object other) { - // short circuit if same object - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof VersionedAddressBook)) { - return false; - } - - VersionedAddressBook otherVersionedAddressBook = (VersionedAddressBook) other; - - // state check - return super.equals(otherVersionedAddressBook) - && addressBookStateList.equals(otherVersionedAddressBook.addressBookStateList) - && currentStatePointer == otherVersionedAddressBook.currentStatePointer; - } - - /** - * Thrown when trying to {@code undo()} but can't. - */ - public static class NoUndoableStateException extends RuntimeException { - private NoUndoableStateException() { - super("Current state pointer at start of addressBookState list, unable to undo."); - } - } - - /** - * Thrown when trying to {@code redo()} but can't. - */ - public static class NoRedoableStateException extends RuntimeException { - private NoRedoableStateException() { - super("Current state pointer at end of addressBookState list, unable to redo."); - } - } -} diff --git a/src/main/java/seedu/address/model/person/Address.java b/src/main/java/seedu/address/model/person/Address.java deleted file mode 100644 index 60472ca22a09..000000000000 --- a/src/main/java/seedu/address/model/person/Address.java +++ /dev/null @@ -1,57 +0,0 @@ -package seedu.address.model.person; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; - -/** - * Represents a Person's address in the address book. - * Guarantees: immutable; is valid as declared in {@link #isValidAddress(String)} - */ -public class Address { - - public static final String MESSAGE_CONSTRAINTS = "Addresses can take any values, 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 = "[^\\s].*"; - - public final String value; - - /** - * Constructs an {@code Address}. - * - * @param address A valid address. - */ - public Address(String address) { - requireNonNull(address); - checkArgument(isValidAddress(address), MESSAGE_CONSTRAINTS); - value = address; - } - - /** - * Returns true if a given string is a valid email. - */ - public static boolean isValidAddress(String test) { - return test.matches(VALIDATION_REGEX); - } - - @Override - public String toString() { - return value; - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof Address // instanceof handles nulls - && value.equals(((Address) other).value)); // state check - } - - @Override - public int hashCode() { - return value.hashCode(); - } - -} diff --git a/src/main/java/seedu/address/model/person/Email.java b/src/main/java/seedu/address/model/person/Email.java deleted file mode 100644 index a5bbe0b6a5fc..000000000000 --- a/src/main/java/seedu/address/model/person/Email.java +++ /dev/null @@ -1,67 +0,0 @@ -package seedu.address.model.person; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; - -/** - * Represents a Person's email in the address book. - * Guarantees: immutable; is valid as declared in {@link #isValidEmail(String)} - */ -public class Email { - - private static final String SPECIAL_CHARACTERS = "!#$%&'*+/=?`{|}~^.-"; - public static final String MESSAGE_CONSTRAINTS = "Emails should be of the format local-part@domain " - + "and adhere to the following constraints:\n" - + "1. The local-part should only contain alphanumeric characters and these special characters, excluding " - + "the parentheses, (" + SPECIAL_CHARACTERS + ") .\n" - + "2. This is followed by a '@' and then a domain name. " - + "The domain name must:\n" - + " - be at least 2 characters long\n" - + " - start and end with alphanumeric characters\n" - + " - consist of alphanumeric characters, a period or a hyphen for the characters in between, if any."; - // alphanumeric and special characters - private static final String LOCAL_PART_REGEX = "^[\\w" + SPECIAL_CHARACTERS + "]+"; - private static final String DOMAIN_FIRST_CHARACTER_REGEX = "[^\\W_]"; // alphanumeric characters except underscore - private static final String DOMAIN_MIDDLE_REGEX = "[a-zA-Z0-9.-]*"; // alphanumeric, period and hyphen - private static final String DOMAIN_LAST_CHARACTER_REGEX = "[^\\W_]$"; - public static final String VALIDATION_REGEX = LOCAL_PART_REGEX + "@" - + DOMAIN_FIRST_CHARACTER_REGEX + DOMAIN_MIDDLE_REGEX + DOMAIN_LAST_CHARACTER_REGEX; - - public final String value; - - /** - * Constructs an {@code Email}. - * - * @param email A valid email address. - */ - public Email(String email) { - requireNonNull(email); - checkArgument(isValidEmail(email), MESSAGE_CONSTRAINTS); - value = email; - } - - /** - * Returns if a given string is a valid email. - */ - public static boolean isValidEmail(String test) { - return test.matches(VALIDATION_REGEX); - } - - @Override - public String toString() { - return value; - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof Email // instanceof handles nulls - && value.equals(((Email) other).value)); // state check - } - - @Override - public int hashCode() { - return value.hashCode(); - } - -} diff --git a/src/main/java/seedu/address/model/person/Name.java b/src/main/java/seedu/address/model/person/Name.java deleted file mode 100644 index 79244d71cf73..000000000000 --- a/src/main/java/seedu/address/model/person/Name.java +++ /dev/null @@ -1,59 +0,0 @@ -package seedu.address.model.person; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; - -/** - * Represents a Person's name in the address book. - * Guarantees: immutable; is valid as declared in {@link #isValidName(String)} - */ -public class Name { - - public static final String MESSAGE_CONSTRAINTS = - "Names should only contain alphanumeric 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 final String fullName; - - /** - * Constructs a {@code Name}. - * - * @param name A valid name. - */ - public Name(String name) { - requireNonNull(name); - checkArgument(isValidName(name), MESSAGE_CONSTRAINTS); - fullName = name; - } - - /** - * Returns true if a given string is a valid name. - */ - public static boolean isValidName(String test) { - return test.matches(VALIDATION_REGEX); - } - - - @Override - public String toString() { - return fullName; - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof Name // instanceof handles nulls - && fullName.equals(((Name) other).fullName)); // state check - } - - @Override - public int hashCode() { - return fullName.hashCode(); - } - -} diff --git a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java deleted file mode 100644 index c9b5868427ca..000000000000 --- a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java +++ /dev/null @@ -1,31 +0,0 @@ -package seedu.address.model.person; - -import java.util.List; -import java.util.function.Predicate; - -import seedu.address.commons.util.StringUtil; - -/** - * Tests that a {@code Person}'s {@code Name} matches any of the keywords given. - */ -public class NameContainsKeywordsPredicate implements Predicate { - private final List keywords; - - public NameContainsKeywordsPredicate(List keywords) { - this.keywords = keywords; - } - - @Override - public boolean test(Person person) { - return keywords.stream() - .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(person.getName().fullName, keyword)); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof NameContainsKeywordsPredicate // instanceof handles nulls - && keywords.equals(((NameContainsKeywordsPredicate) other).keywords)); // state check - } - -} diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java deleted file mode 100644 index 557a7a60cd51..000000000000 --- a/src/main/java/seedu/address/model/person/Person.java +++ /dev/null @@ -1,120 +0,0 @@ -package seedu.address.model.person; - -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.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 { - - // Identity fields - private final Name name; - 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); - this.name = name; - this.phone = phone; - this.email = email; - this.address = address; - this.tags.addAll(tags); - } - - public Name getName() { - return name; - } - - 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 of the same name have at least one other identity field that is the same. - * This defines a weaker notion of equality between two persons. - */ - public boolean isSamePerson(Person otherPerson) { - if (otherPerson == this) { - return true; - } - - return otherPerson != null - && otherPerson.getName().equals(getName()) - && (otherPerson.getPhone().equals(getPhone()) || otherPerson.getEmail().equals(getEmail())); - } - - /** - * 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; - } - - if (!(other instanceof Person)) { - return false; - } - - Person otherPerson = (Person) other; - return otherPerson.getName().equals(getName()) - && otherPerson.getPhone().equals(getPhone()) - && otherPerson.getEmail().equals(getEmail()) - && otherPerson.getAddress().equals(getAddress()) - && otherPerson.getTags().equals(getTags()); - } - - @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() { - final StringBuilder builder = new StringBuilder(); - builder.append(getName()) - .append(" Phone: ") - .append(getPhone()) - .append(" Email: ") - .append(getEmail()) - .append(" Address: ") - .append(getAddress()) - .append(" Tags: "); - getTags().forEach(builder::append); - return builder.toString(); - } - -} diff --git a/src/main/java/seedu/address/model/person/Phone.java b/src/main/java/seedu/address/model/person/Phone.java deleted file mode 100644 index 872c76b382fd..000000000000 --- a/src/main/java/seedu/address/model/person/Phone.java +++ /dev/null @@ -1,53 +0,0 @@ -package seedu.address.model.person; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; - -/** - * Represents a Person's phone number in the address book. - * Guarantees: immutable; is valid as declared in {@link #isValidPhone(String)} - */ -public class Phone { - - - public static final String MESSAGE_CONSTRAINTS = - "Phone numbers should only contain numbers, and it should be at least 3 digits long"; - public static final String VALIDATION_REGEX = "\\d{3,}"; - public final String value; - - /** - * Constructs a {@code Phone}. - * - * @param phone A valid phone number. - */ - public Phone(String phone) { - requireNonNull(phone); - checkArgument(isValidPhone(phone), MESSAGE_CONSTRAINTS); - value = phone; - } - - /** - * Returns true if a given string is a valid phone number. - */ - public static boolean isValidPhone(String test) { - return test.matches(VALIDATION_REGEX); - } - - @Override - public String toString() { - return value; - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof Phone // instanceof handles nulls - && value.equals(((Phone) other).value)); // state check - } - - @Override - public int hashCode() { - return value.hashCode(); - } - -} diff --git a/src/main/java/seedu/address/model/person/UniquePersonList.java b/src/main/java/seedu/address/model/person/UniquePersonList.java deleted file mode 100644 index 0fee4fe57e6b..000000000000 --- a/src/main/java/seedu/address/model/person/UniquePersonList.java +++ /dev/null @@ -1,137 +0,0 @@ -package seedu.address.model.person; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; - -import java.util.Iterator; -import java.util.List; - -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; -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 - * persons uses Person#isSamePerson(Person) for equality so as to ensure that the person being added or updated is - * unique in terms of identity in the UniquePersonList. However, the removal of a person uses Person#equals(Object) so - * as to ensure that the person with exactly the same fields will be removed. - * - * Supports a minimal set of list operations. - * - * @see Person#isSamePerson(Person) - */ -public class UniquePersonList implements Iterable { - - private final ObservableList internalList = FXCollections.observableArrayList(); - private final ObservableList internalUnmodifiableList = - FXCollections.unmodifiableObservableList(internalList); - - /** - * Returns true if the list contains an equivalent person as the given argument. - */ - public boolean contains(Person toCheck) { - requireNonNull(toCheck); - return internalList.stream().anyMatch(toCheck::isSamePerson); - } - - /** - * Adds a person to the list. - * The person must not already exist in the list. - */ - public void add(Person toAdd) { - requireNonNull(toAdd); - if (contains(toAdd)) { - throw new DuplicatePersonException(); - } - internalList.add(toAdd); - } - - /** - * Replaces the person {@code target} in the list with {@code editedPerson}. - * {@code target} must exist in the list. - * The person identity of {@code editedPerson} must not be the same as another existing person in the list. - */ - public void setPerson(Person target, Person editedPerson) { - requireAllNonNull(target, editedPerson); - - int index = internalList.indexOf(target); - if (index == -1) { - throw new PersonNotFoundException(); - } - - if (!target.isSamePerson(editedPerson) && contains(editedPerson)) { - throw new DuplicatePersonException(); - } - - internalList.set(index, editedPerson); - } - - /** - * Removes the equivalent person from the list. - * The person must exist in the list. - */ - public void remove(Person toRemove) { - requireNonNull(toRemove); - if (!internalList.remove(toRemove)) { - throw new PersonNotFoundException(); - } - } - - public void setPersons(UniquePersonList replacement) { - requireNonNull(replacement); - internalList.setAll(replacement.internalList); - } - - /** - * Replaces the contents of this list with {@code persons}. - * {@code persons} must not contain duplicate persons. - */ - public void setPersons(List persons) { - requireAllNonNull(persons); - if (!personsAreUnique(persons)) { - throw new DuplicatePersonException(); - } - - internalList.setAll(persons); - } - - /** - * Returns the backing list as an unmodifiable {@code ObservableList}. - */ - public ObservableList asUnmodifiableObservableList() { - return internalUnmodifiableList; - } - - @Override - public Iterator iterator() { - return internalList.iterator(); - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof UniquePersonList // instanceof handles nulls - && internalList.equals(((UniquePersonList) other).internalList)); - } - - @Override - public int hashCode() { - return internalList.hashCode(); - } - - /** - * Returns true if {@code persons} contains only unique persons. - */ - private boolean personsAreUnique(List persons) { - for (int i = 0; i < persons.size() - 1; i++) { - for (int j = i + 1; j < persons.size(); j++) { - if (persons.get(i).isSamePerson(persons.get(j))) { - return false; - } - } - } - return true; - } -} diff --git a/src/main/java/seedu/address/model/person/exceptions/DuplicatePersonException.java b/src/main/java/seedu/address/model/person/exceptions/DuplicatePersonException.java deleted file mode 100644 index d7290f594423..000000000000 --- a/src/main/java/seedu/address/model/person/exceptions/DuplicatePersonException.java +++ /dev/null @@ -1,11 +0,0 @@ -package seedu.address.model.person.exceptions; - -/** - * Signals that the operation will result in duplicate Persons (Persons are considered duplicates if they have the same - * identity). - */ -public class DuplicatePersonException extends RuntimeException { - public DuplicatePersonException() { - super("Operation would result in duplicate persons"); - } -} diff --git a/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java b/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java deleted file mode 100644 index fa764426ca73..000000000000 --- a/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java +++ /dev/null @@ -1,6 +0,0 @@ -package seedu.address.model.person.exceptions; - -/** - * Signals that the operation is unable to find the specified person. - */ -public class PersonNotFoundException extends RuntimeException {} diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java deleted file mode 100644 index 1806da4facfa..000000000000 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ /dev/null @@ -1,60 +0,0 @@ -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.Name; -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. - */ -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")) - }; - } - - public static ReadOnlyAddressBook getSampleAddressBook() { - AddressBook sampleAb = new AddressBook(); - for (Person samplePerson : getSamplePersons()) { - sampleAb.addPerson(samplePerson); - } - 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/AddressBookStorage.java b/src/main/java/seedu/address/storage/AddressBookStorage.java deleted file mode 100644 index 4599182b3f92..000000000000 --- a/src/main/java/seedu/address/storage/AddressBookStorage.java +++ /dev/null @@ -1,45 +0,0 @@ -package seedu.address.storage; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.Optional; - -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.model.ReadOnlyAddressBook; - -/** - * Represents a storage for {@link seedu.address.model.AddressBook}. - */ -public interface AddressBookStorage { - - /** - * Returns the file path of the data file. - */ - Path getAddressBookFilePath(); - - /** - * Returns AddressBook data as a {@link ReadOnlyAddressBook}. - * Returns {@code Optional.empty()} if storage file is not found. - * @throws DataConversionException if the data in storage is not in the expected format. - * @throws IOException if there was any problem when reading from the storage. - */ - Optional readAddressBook() throws DataConversionException, IOException; - - /** - * @see #getAddressBookFilePath() - */ - Optional readAddressBook(Path filePath) throws DataConversionException, IOException; - - /** - * Saves the given {@link ReadOnlyAddressBook} to the storage. - * @param addressBook cannot be null. - * @throws IOException if there was any problem writing to the file. - */ - void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException; - - /** - * @see #saveAddressBook(ReadOnlyAddressBook) - */ - void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) throws IOException; - -} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java deleted file mode 100644 index a6321cec2eac..000000000000 --- a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java +++ /dev/null @@ -1,109 +0,0 @@ -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.Name; -import seedu.address.model.person.Person; -import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; - -/** - * Jackson-friendly version of {@link Person}. - */ -class JsonAdaptedPerson { - - public static final String MISSING_FIELD_MESSAGE_FORMAT = "Person's %s field is missing!"; - - private final String name; - private final String phone; - private final String email; - private final String address; - private final List tagged = 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("tagged") List tagged) { - this.name = name; - this.phone = phone; - this.email = email; - this.address = address; - if (tagged != null) { - this.tagged.addAll(tagged); - } - } - - /** - * Converts a given {@code Person} into this class for Jackson use. - */ - public JsonAdaptedPerson(Person source) { - name = source.getName().fullName; - phone = source.getPhone().value; - email = source.getEmail().value; - address = source.getAddress().value; - tagged.addAll(source.getTags().stream() - .map(JsonAdaptedTag::new) - .collect(Collectors.toList())); - } - - /** - * Converts this Jackson-friendly adapted person object into the model's {@code Person} object. - * - * @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 : tagged) { - personTags.add(tag.toModelType()); - } - - if (name == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName())); - } - if (!Name.isValidName(name)) { - throw new IllegalValueException(Name.MESSAGE_CONSTRAINTS); - } - final Name modelName = new Name(name); - - if (phone == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Phone.class.getSimpleName())); - } - if (!Phone.isValidPhone(phone)) { - throw new IllegalValueException(Phone.MESSAGE_CONSTRAINTS); - } - 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); - } - final Address modelAddress = new Address(address); - - final Set modelTags = new HashSet<>(personTags); - return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags); - } - -} diff --git a/src/main/java/seedu/address/storage/JsonAddressBookStorage.java b/src/main/java/seedu/address/storage/JsonAddressBookStorage.java deleted file mode 100644 index dfab9daaa0d3..000000000000 --- a/src/main/java/seedu/address/storage/JsonAddressBookStorage.java +++ /dev/null @@ -1,80 +0,0 @@ -package seedu.address.storage; - -import static java.util.Objects.requireNonNull; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.Optional; -import java.util.logging.Logger; - -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.commons.util.FileUtil; -import seedu.address.commons.util.JsonUtil; -import seedu.address.model.ReadOnlyAddressBook; - -/** - * A class to access AddressBook data stored as a json file on the hard disk. - */ -public class JsonAddressBookStorage implements AddressBookStorage { - - private static final Logger logger = LogsCenter.getLogger(JsonAddressBookStorage.class); - - private Path filePath; - - public JsonAddressBookStorage(Path filePath) { - this.filePath = filePath; - } - - public Path getAddressBookFilePath() { - return filePath; - } - - @Override - public Optional readAddressBook() throws DataConversionException { - return readAddressBook(filePath); - } - - /** - * Similar to {@link #readAddressBook()}. - * - * @param filePath location of the data. Cannot be null. - * @throws DataConversionException if the file is not in the correct format. - */ - public Optional readAddressBook(Path filePath) throws DataConversionException { - requireNonNull(filePath); - - Optional jsonAddressBook = JsonUtil.readJsonFile( - filePath, JsonSerializableAddressBook.class); - if (!jsonAddressBook.isPresent()) { - return Optional.empty(); - } - - try { - return Optional.of(jsonAddressBook.get().toModelType()); - } catch (IllegalValueException ive) { - logger.info("Illegal values found in " + filePath + ": " + ive.getMessage()); - throw new DataConversionException(ive); - } - } - - @Override - public void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException { - saveAddressBook(addressBook, filePath); - } - - /** - * Similar to {@link #saveAddressBook(ReadOnlyAddressBook)}. - * - * @param filePath location of the data. Cannot be null. - */ - public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) throws IOException { - requireNonNull(addressBook); - requireNonNull(filePath); - - FileUtil.createIfMissing(filePath); - JsonUtil.saveJsonFile(new JsonSerializableAddressBook(addressBook), filePath); - } - -} diff --git a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java b/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java deleted file mode 100644 index 5efd834091d4..000000000000 --- a/src/main/java/seedu/address/storage/JsonSerializableAddressBook.java +++ /dev/null @@ -1,60 +0,0 @@ -package seedu.address.storage; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -import com.fasterxml.jackson.annotation.JsonCreator; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonRootName; - -import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.AddressBook; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Person; - -/** - * An Immutable AddressBook that is serializable to JSON format. - */ -@JsonRootName(value = "addressbook") -class JsonSerializableAddressBook { - - public static final String MESSAGE_DUPLICATE_PERSON = "Persons list contains duplicate person(s)."; - - private final List persons = new ArrayList<>(); - - /** - * Constructs a {@code JsonSerializableAddressBook} with the given persons. - */ - @JsonCreator - public JsonSerializableAddressBook(@JsonProperty("persons") List persons) { - this.persons.addAll(persons); - } - - /** - * Converts a given {@code ReadOnlyAddressBook} into this class for Jackson use. - * - * @param source future changes to this will not affect the created {@code JsonSerializableAddressBook}. - */ - public JsonSerializableAddressBook(ReadOnlyAddressBook source) { - persons.addAll(source.getPersonList().stream().map(JsonAdaptedPerson::new).collect(Collectors.toList())); - } - - /** - * Converts this address book into the model's {@code AddressBook} object. - * - * @throws IllegalValueException if there were any data constraints violated. - */ - public AddressBook toModelType() throws IllegalValueException { - AddressBook addressBook = new AddressBook(); - for (JsonAdaptedPerson jsonAdaptedPerson : persons) { - Person person = jsonAdaptedPerson.toModelType(); - if (addressBook.hasPerson(person)) { - throw new IllegalValueException(MESSAGE_DUPLICATE_PERSON); - } - addressBook.addPerson(person); - } - return addressBook; - } - -} diff --git a/src/main/java/seedu/address/storage/Storage.java b/src/main/java/seedu/address/storage/Storage.java deleted file mode 100644 index beda8bd9f11b..000000000000 --- a/src/main/java/seedu/address/storage/Storage.java +++ /dev/null @@ -1,32 +0,0 @@ -package seedu.address.storage; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.Optional; - -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.ReadOnlyUserPrefs; -import seedu.address.model.UserPrefs; - -/** - * API of the Storage component - */ -public interface Storage extends AddressBookStorage, UserPrefsStorage { - - @Override - Optional readUserPrefs() throws DataConversionException, IOException; - - @Override - void saveUserPrefs(ReadOnlyUserPrefs userPrefs) throws IOException; - - @Override - Path getAddressBookFilePath(); - - @Override - Optional readAddressBook() throws DataConversionException, IOException; - - @Override - void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException; - -} diff --git a/src/main/java/seedu/address/storage/StorageManager.java b/src/main/java/seedu/address/storage/StorageManager.java deleted file mode 100644 index e4f452b6cbf4..000000000000 --- a/src/main/java/seedu/address/storage/StorageManager.java +++ /dev/null @@ -1,77 +0,0 @@ -package seedu.address.storage; - -import java.io.IOException; -import java.nio.file.Path; -import java.util.Optional; -import java.util.logging.Logger; - -import seedu.address.commons.core.LogsCenter; -import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.ReadOnlyUserPrefs; -import seedu.address.model.UserPrefs; - -/** - * Manages storage of AddressBook data in local storage. - */ -public class StorageManager implements Storage { - - private static final Logger logger = LogsCenter.getLogger(StorageManager.class); - private AddressBookStorage addressBookStorage; - private UserPrefsStorage userPrefsStorage; - - - public StorageManager(AddressBookStorage addressBookStorage, UserPrefsStorage userPrefsStorage) { - super(); - this.addressBookStorage = addressBookStorage; - this.userPrefsStorage = userPrefsStorage; - } - - // ================ UserPrefs methods ============================== - - @Override - public Path getUserPrefsFilePath() { - return userPrefsStorage.getUserPrefsFilePath(); - } - - @Override - public Optional readUserPrefs() throws DataConversionException, IOException { - return userPrefsStorage.readUserPrefs(); - } - - @Override - public void saveUserPrefs(ReadOnlyUserPrefs userPrefs) throws IOException { - userPrefsStorage.saveUserPrefs(userPrefs); - } - - - // ================ AddressBook methods ============================== - - @Override - public Path getAddressBookFilePath() { - return addressBookStorage.getAddressBookFilePath(); - } - - @Override - public Optional readAddressBook() throws DataConversionException, IOException { - return readAddressBook(addressBookStorage.getAddressBookFilePath()); - } - - @Override - public Optional readAddressBook(Path filePath) throws DataConversionException, IOException { - logger.fine("Attempting to read data from file: " + filePath); - return addressBookStorage.readAddressBook(filePath); - } - - @Override - public void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException { - saveAddressBook(addressBook, addressBookStorage.getAddressBookFilePath()); - } - - @Override - public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) throws IOException { - logger.fine("Attempting to write to data file: " + filePath); - addressBookStorage.saveAddressBook(addressBook, filePath); - } - -} diff --git a/src/main/java/seedu/address/ui/BrowserPanel.java b/src/main/java/seedu/address/ui/BrowserPanel.java deleted file mode 100644 index 53876e01c8d1..000000000000 --- a/src/main/java/seedu/address/ui/BrowserPanel.java +++ /dev/null @@ -1,67 +0,0 @@ -package seedu.address.ui; - -import static java.util.Objects.requireNonNull; - -import java.net.URL; -import java.util.logging.Logger; - -import javafx.application.Platform; -import javafx.beans.value.ObservableValue; -import javafx.event.Event; -import javafx.fxml.FXML; -import javafx.scene.layout.Region; -import javafx.scene.web.WebView; -import seedu.address.MainApp; -import seedu.address.commons.core.LogsCenter; -import seedu.address.model.person.Person; - -/** - * The Browser Panel of the App. - */ -public class BrowserPanel extends UiPart { - - public static final URL DEFAULT_PAGE = - requireNonNull(MainApp.class.getResource(FXML_FILE_FOLDER + "default.html")); - public static final String SEARCH_PAGE_URL = "https://se-education.org/dummy-search-page/?name="; - - private static final String FXML = "BrowserPanel.fxml"; - - private final Logger logger = LogsCenter.getLogger(getClass()); - - @FXML - private WebView browser; - - public BrowserPanel(ObservableValue selectedPerson) { - super(FXML); - - // To prevent triggering events for typing inside the loaded Web page. - getRoot().setOnKeyPressed(Event::consume); - - // Load person page when selected person changes. - selectedPerson.addListener((observable, oldValue, newValue) -> { - if (newValue == null) { - loadDefaultPage(); - return; - } - loadPersonPage(newValue); - }); - - loadDefaultPage(); - } - - private void loadPersonPage(Person person) { - loadPage(SEARCH_PAGE_URL + person.getName().fullName); - } - - public void loadPage(String url) { - Platform.runLater(() -> browser.getEngine().load(url)); - } - - /** - * Loads a default HTML file with a background that matches the general theme. - */ - private void loadDefaultPage() { - loadPage(DEFAULT_PAGE.toExternalForm()); - } - -} diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java deleted file mode 100644 index f6727ea83abd..000000000000 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ /dev/null @@ -1,70 +0,0 @@ -package seedu.address.ui; - -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}. - */ -public class PersonCard extends UiPart { - - private static final String FXML = "PersonListCard.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 - */ - - public final Person person; - - @FXML - private HBox cardPane; - @FXML - private Label name; - @FXML - private Label id; - @FXML - private Label phone; - @FXML - private Label address; - @FXML - private Label email; - @FXML - private FlowPane tags; - - public PersonCard(Person person, int displayedIndex) { - super(FXML); - this.person = person; - id.setText(displayedIndex + ". "); - name.setText(person.getName().fullName); - phone.setText(person.getPhone().value); - address.setText(person.getAddress().value); - email.setText(person.getEmail().value); - person.getTags().forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); - } - - @Override - public boolean equals(Object other) { - // short circuit if same object - if (other == this) { - return true; - } - - // instanceof handles nulls - if (!(other instanceof PersonCard)) { - return false; - } - - // state check - PersonCard card = (PersonCard) other; - return id.getText().equals(card.id.getText()) - && person.equals(card.person); - } -} diff --git a/src/main/java/seedu/address/ui/PersonListPanel.java b/src/main/java/seedu/address/ui/PersonListPanel.java deleted file mode 100644 index 5ca3fa4fc671..000000000000 --- a/src/main/java/seedu/address/ui/PersonListPanel.java +++ /dev/null @@ -1,71 +0,0 @@ -package seedu.address.ui; - -import java.util.Objects; -import java.util.function.Consumer; -import java.util.logging.Logger; - -import javafx.beans.value.ObservableValue; -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.person.Person; - -/** - * Panel containing the list of persons. - */ -public class PersonListPanel extends UiPart { - private static final String FXML = "PersonListPanel.fxml"; - private final Logger logger = LogsCenter.getLogger(PersonListPanel.class); - - @FXML - private ListView personListView; - - public PersonListPanel(ObservableList personList, ObservableValue selectedPerson, - Consumer onSelectedPersonChange) { - super(FXML); - personListView.setItems(personList); - personListView.setCellFactory(listView -> new PersonListViewCell()); - personListView.getSelectionModel().selectedItemProperty().addListener((observable, oldValue, newValue) -> { - logger.fine("Selection in person list panel changed to : '" + newValue + "'"); - onSelectedPersonChange.accept(newValue); - }); - selectedPerson.addListener((observable, oldValue, newValue) -> { - logger.fine("Selected person changed to: " + newValue); - - // Don't modify selection if we are already selecting the selected person, - // otherwise we would have an infinite loop. - if (Objects.equals(personListView.getSelectionModel().getSelectedItem(), newValue)) { - return; - } - - if (newValue == null) { - personListView.getSelectionModel().clearSelection(); - } else { - int index = personListView.getItems().indexOf(newValue); - personListView.scrollTo(index); - personListView.getSelectionModel().clearAndSelect(index); - } - }); - } - - /** - * Custom {@code ListCell} that displays the graphics of a {@code Person} using a {@code PersonCard}. - */ - class PersonListViewCell extends ListCell { - @Override - protected void updateItem(Person person, boolean empty) { - super.updateItem(person, empty); - - if (empty || person == null) { - setGraphic(null); - setText(null); - } else { - setGraphic(new PersonCard(person, getIndex() + 1).getRoot()); - } - } - } - -} diff --git a/src/main/java/seedu/address/ui/StatusBarFooter.java b/src/main/java/seedu/address/ui/StatusBarFooter.java deleted file mode 100644 index b22e1f525256..000000000000 --- a/src/main/java/seedu/address/ui/StatusBarFooter.java +++ /dev/null @@ -1,69 +0,0 @@ -package seedu.address.ui; - -import java.nio.file.Path; -import java.nio.file.Paths; -import java.time.Clock; -import java.util.Date; - -import javafx.fxml.FXML; -import javafx.scene.control.Label; -import javafx.scene.layout.Region; -import seedu.address.model.ReadOnlyAddressBook; - -/** - * A ui for the status bar that is displayed at the footer of the application. - */ -public class StatusBarFooter extends UiPart { - - public static final String SYNC_STATUS_INITIAL = "Not updated yet in this session"; - public static final String SYNC_STATUS_UPDATED = "Last Updated: %s"; - - /** - * Used to generate time stamps. - * - * TODO: change clock to an instance variable. - * We leave it as a static variable because manual dependency injection - * will require passing down the clock reference all the way from MainApp, - * but it should be easier once we have factories/DI frameworks. - */ - private static Clock clock = Clock.systemDefaultZone(); - - private static final String FXML = "StatusBarFooter.fxml"; - - @FXML - private Label syncStatus; - @FXML - private Label saveLocationStatus; - - - public StatusBarFooter(Path saveLocation, ReadOnlyAddressBook addressBook) { - super(FXML); - addressBook.addListener(observable -> updateSyncStatus()); - syncStatus.setText(SYNC_STATUS_INITIAL); - saveLocationStatus.setText(Paths.get(".").resolve(saveLocation).toString()); - } - - /** - * Sets the clock used to determine the current time. - */ - public static void setClock(Clock clock) { - StatusBarFooter.clock = clock; - } - - /** - * Returns the clock currently in use. - */ - public static Clock getClock() { - return clock; - } - - /** - * Updates "last updated" status to the current time. - */ - private void updateSyncStatus() { - long now = clock.millis(); - String lastUpdated = new Date(now).toString(); - syncStatus.setText(String.format(SYNC_STATUS_UPDATED, lastUpdated)); - } - -} diff --git a/src/main/resources/images/quickdocsicon_512.png b/src/main/resources/images/quickdocsicon_512.png new file mode 100644 index 000000000000..f53903d28899 Binary files /dev/null and b/src/main/resources/images/quickdocsicon_512.png differ diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index 36e6b001cd8d..27a9bb06b8e1 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -343,10 +343,58 @@ } #tags .label { - -fx-text-fill: white; - -fx-background-color: #3e7b91; -fx-padding: 1 3 1 3; -fx-border-radius: 2; -fx-background-radius: 2; -fx-font-size: 11; } + +#tags .teal { + -fx-text-fill: white; + -fx-background-color: #3e7b91; +} + +#tags .red { + -fx-text-fill: black; + -fx-background-color: red; +} + +#tags .yellow { + -fx-background-color: yellow; + -fx-text-fill: black; +} + +#tags .blue { + -fx-text-fill: white; + -fx-background-color: blue; +} + +#tags .orange { + -fx-text-fill: black; + -fx-background-color: orange; +} + +#tags .brown { + -fx-text-fill: white; + -fx-background-color: brown; +} + +#tags .green { + -fx-text-fill: black; + -fx-background-color: green; +} + +#tags .pink { + -fx-text-fill: black; + -fx-background-color: pink; +} + +#tags .black { + -fx-text-fill: white; + -fx-background-color: black; +} + +#tags .grey { + -fx-text-fill: black; + -fx-background-color: grey; +} diff --git a/src/main/resources/view/ReminderListCard.fxml b/src/main/resources/view/ReminderListCard.fxml new file mode 100644 index 000000000000..1f5f8a375e51 --- /dev/null +++ b/src/main/resources/view/ReminderListCard.fxml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/ReminderListPanel.fxml b/src/main/resources/view/ReminderListPanel.fxml new file mode 100644 index 000000000000..facb94e5482c --- /dev/null +++ b/src/main/resources/view/ReminderListPanel.fxml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/main/resources/view/RootLayout.fxml b/src/main/resources/view/RootLayout.fxml new file mode 100644 index 000000000000..fae473feedf1 --- /dev/null +++ b/src/main/resources/view/RootLayout.fxml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +