From 49e7a334104d15cc62a7982f94a04aeec7b23772 Mon Sep 17 00:00:00 2001 From: cchj1995 Date: Thu, 14 Mar 2019 21:44:31 +0800 Subject: [PATCH 1/3] Added new user stories for task functionality in DeveloperGuide.adoc --- docs/DeveloperGuide.adoc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/DeveloperGuide.adoc b/docs/DeveloperGuide.adoc index 945c8e823978..2a62e9c750e6 100644 --- a/docs/DeveloperGuide.adoc +++ b/docs/DeveloperGuide.adoc @@ -835,6 +835,12 @@ Priorities: High (must have) - `* * \*`, Medium (nice to have) - `* \*`, Low (un |`* * *` |user |find a person by name |locate details of persons without having to go through the entire list +|`* * *` |user |add a new task | + +|`* * *` |user |delete a task |remove tasks that I have already completed or no longer need to do + +|`* * *` |user |edit a task |change details of certains tasks that I have already added + |`* *` |user |hide <> by default |minimize chance of someone else seeing them by accident |`* *` |user |copy a person |reduce the time needed to create a new person who has similar records to an existing person in the list From 1321687d422d149020b5aed53fdc347e397214a1 Mon Sep 17 00:00:00 2001 From: cchj1995 Date: Thu, 14 Mar 2019 21:52:27 +0800 Subject: [PATCH 2/3] Added cchj1995.adoc to team folder using johndoe's portfolio as base --- docs/team/cchj1995.adoc | 72 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 docs/team/cchj1995.adoc diff --git a/docs/team/cchj1995.adoc b/docs/team/cchj1995.adoc new file mode 100644 index 000000000000..c2a5ab41c51f --- /dev/null +++ b/docs/team/cchj1995.adoc @@ -0,0 +1,72 @@ += Jonathan Choo - Project Portfolio +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets + +== PROJECT: AddressBook - Level 4 + +--- + +== Overview + +AddressBook - Level 4 is a desktop address book application used for teaching Software Engineering principles. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. + +== Summary of contributions + +* *Major enhancement*: added *the ability to undo/redo previous commands* +** What it does: allows the user to undo all previous commands one at a time. Preceding undo commands can be reversed by using the redo command. +** Justification: This feature improves the product significantly because a user can make mistakes in commands and the app should provide a convenient way to rectify them. +** Highlights: This enhancement affects existing commands and commands to be added in future. It required an in-depth analysis of design alternatives. The implementation too was challenging as it required changes to existing commands. +** Credits: _{mention here if you reused any code/ideas from elsewhere or if a third-party library is heavily used in the feature so that a reader can make a more accurate judgement of how much effort went into the feature}_ + +* *Minor enhancement*: added a history command that allows the user to navigate to previous commands using up/down keys. + +* *Code contributed*: [https://github.com[Functional code]] [https://github.com[Test code]] _{give links to collated code files}_ + +* *Other contributions*: + +** Project management: +*** Managed releases `v1.3` - `v1.5rc` (3 releases) on GitHub +** Enhancements to existing features: +*** Updated the GUI color scheme (Pull requests https://github.com[#33], https://github.com[#34]) +*** Wrote additional tests for existing features to increase coverage from 88% to 92% (Pull requests https://github.com[#36], https://github.com[#38]) +** Documentation: +*** Did cosmetic tweaks to existing contents of the User Guide: https://github.com[#14] +** Community: +*** PRs reviewed (with non-trivial review comments): https://github.com[#12], https://github.com[#32], https://github.com[#19], https://github.com[#42] +*** Contributed to forum discussions (examples: https://github.com[1], https://github.com[2], https://github.com[3], https://github.com[4]) +*** Reported bugs and suggestions for other teams in the class (examples: https://github.com[1], https://github.com[2], https://github.com[3]) +*** Some parts of the history feature I added was adopted by several other class mates (https://github.com[1], https://github.com[2]) +** Tools: +*** Integrated a third party library (Natty) to the project (https://github.com[#42]) +*** Integrated a new Github plugin (CircleCI) to the team repo + +_{you can add/remove categories in the list above}_ + +== Contributions to the User Guide + + +|=== +|_Given below are sections I contributed to the User Guide. They showcase my ability to write documentation targeting end-users._ +|=== + +include::../UserGuide.adoc[tag=undoredo] + +include::../UserGuide.adoc[tag=dataencryption] + +== Contributions to the Developer Guide + +|=== +|_Given below are sections I contributed to the Developer Guide. They showcase my ability to write technical documentation and the technical depth of my contributions to the project._ +|=== + +include::../DeveloperGuide.adoc[tag=undoredo] + +include::../DeveloperGuide.adoc[tag=dataencryption] + + +== PROJECT: PowerPointLabs + +--- + +_{Optionally, you may include other projects in your portfolio.}_ From e6948e7b1f1e412423f57bdf76d2ded3f4c4f6c4 Mon Sep 17 00:00:00 2001 From: kthSim Date: Thu, 14 Mar 2019 23:19:46 +0800 Subject: [PATCH 3/3] Docs (#65) * Added Tooth and Teeth skeleton codes * Added Teeth as an attribute to Person * Added Tooth isPresent attribute, with getter method * Added hasStatus getter method * Modified and added constructors to initialise all attributes * Added new Status class to represent Tooth status * Added crucial attributes to Status class * Added relevant getter methods to uphold OOP information hiding * Implemented Status variable, and its getter and setter methods * Assigned default values to Tooth attributes * Added skeleton code for Tooth getter method * Added relevant static teeth counts * Minor fixes for readability * Minor fixes * Minor changes to code style * Added ImportCommand to AddressBookParser.java * Added ImportCommand.java * Added ImportCommandParser.java * Added parseFile() to ParserUtil.java * Added ImportCommand to parseCommand() of AddressBookParser.java * Updated regex for parseFile() in ParserUtil.java * Updated comments for ImportCommandParser.java * Updated parseFile() for file validation in ParserUtil.java * Updated ImportCommandParser.java * Added data folder filepath to concat with input file in ParserUtil.java * Added readFile() to ImportCommand.java * Added ExportCommand.java * Removed unused import statements from ImportCommand.java * Added ExportCommand to parseCommand() of AddressBookParser.java * Updated code for ExportCommand.java * Added ExportCommandParser.java * Added parseFile() to ParserUtil.java * Changed command word to 'export' in ExportCommand.java * Updated ParserUtil.java to work with ExportCommand * Updated ExportCommandParser.java to receive exception message from ParserUtil.java * Updated ExportCommand.java * Updated ParserUtil.java to work with ExportCommand.java * Updated ImportCommandParser.java to receive exception message from ParserUtil.java * Updated readFile() to be private in ImportCommand.java * Updated ImportCommandParser.java to handle import validation * Added comments to ImportCommand.java * Updated writeFile() to be private in ExportCommand.java * Updated writeFile() to be private in ExportCommand.java * Created empty attribute classes for Date,Time,Description, Procedure, and Record * Impletemented code for the attribute classes Description & Procedure * Minor code comment fix * Implemented code for attribute classes Date & Time * Implemented code for Record class * Updated code to match codeStyle * Added import and export commands to UserGuide.adoc * Removed unused imports in ExportCommand.java * Renamed variable to match camelCase in ParserUtil.java * Renamed variable to match camelCase in ParserUtil.java * Fixed codestyle in ImportCommand.java * Fixed codestyle in ImportCommand.java * Fixed codestyle in ImportCommandParser.java * Fixed codestyle in ExportCommandParser.java * Fixed ImportCommand.java codestyle * Fixed ExportCommand.java codestyle * Add CopyCommand class * Add CopyCommand class * Add CopyTag class * Add CopyTag class for copy information * Modified Person class for temporary duplicates * Modified EditCommand for removal of copy information * Main logic for CopyCommand * Command info for CopyCommand * Add CopyCommandParser class * Copy command can be called * Cleared a bug by typo * = Add methods to help check copies * = Add Copy tag for copies * = Add checkNoCopy method into Model interface * = Add logger info for copy status * = Change isSamePerson logic for loading saved data * = Change error in INFO display * = Style check change * = Style check change * = Style check change * Added LoginWindow class and LoginWindow.fxml * Removed unnecessary segments from skeleton of LoginWindow class * Removed GUI portions for skeleton base of LoginWindow.fxml * Added textbox to plain LoginWindow.fxml and created handleStringEntered function in LoginWindow class * Added hide function to LoginWindow class * Changed hide function to close function * Fixed coding style * Fixed Import Style, removed unused Imports in LoginWindow.fxml * Created blank personal detail attribute classes (Age, DrugAllergy, Nric, Sex) * Implemented personal detail attribute classes (Age, DrugAllergy, Nric, Sex) * Minor checkstyle change * Renamed photos * Updated UserGuide.adoc * Updated image file * Testing UserGuide.adoc * Test * Test * Minor update * Test * Rename Date & Time to DateCustom & TimeCustom so it doesn't interfere with the existing inbuilt Java classes * Updated date & time occurances to DateCustom & TimeCustom * Minor checkStyle changes * Created empty command & parser classes for Sort and Stats * updated badges on README * Updated devGuide with all discussed userStories --- README.adoc | 6 +- docs/AboutUs.adoc | 4 +- docs/DeveloperGuide.adoc | 34 ++++-- docs/UserGuide.adoc | 44 ++++---- docs/images/{cchj1995.jpg => cchj1995.png} | Bin docs/images/{wsemis.JPG => wsemis.png} | Bin gradle/wrapper/gradle-wrapper.jar | Bin 54329 -> 54417 bytes src/main/java/seedu/address/MainApp.java | 5 + .../address/logic/commands/CopyCommand.java | 62 ++++++++++ .../address/logic/commands/EditCommand.java | 5 + .../address/logic/commands/ExportCommand.java | 63 +++++++++++ .../address/logic/commands/ImportCommand.java | 82 ++++++++++++++ .../address/logic/commands/SortCommand.java | 7 ++ .../address/logic/commands/StatsCommand.java | 7 ++ .../logic/parser/AddressBookParser.java | 12 ++ .../logic/parser/CopyCommandParser.java | 35 ++++++ .../logic/parser/ExportCommandParser.java | 28 +++++ .../logic/parser/ImportCommandParser.java | 34 ++++++ .../address/logic/parser/ParserUtil.java | 22 ++++ .../logic/parser/SortCommandParser.java | 7 ++ .../logic/parser/StatsCommandParser.java | 7 ++ .../java/seedu/address/model/AddressBook.java | 12 ++ src/main/java/seedu/address/model/Model.java | 2 + .../seedu/address/model/ModelManager.java | 5 + .../address/model/datetime/DateCustom.java | 50 +++++++++ .../address/model/datetime/TimeCustom.java | 50 +++++++++ .../model/description/Description.java | 58 ++++++++++ .../seedu/address/model/person/Address.java | 2 +- .../java/seedu/address/model/person/Age.java | 53 +++++++++ .../address/model/person/DrugAllergy.java | 60 ++++++++++ .../java/seedu/address/model/person/Nric.java | 52 +++++++++ .../seedu/address/model/person/Person.java | 69 +++++++++++- .../java/seedu/address/model/person/Sex.java | 53 +++++++++ .../seedu/address/model/person/Status.java | 25 +++++ .../seedu/address/model/person/Teeth.java | 26 +++++ .../seedu/address/model/person/Tooth.java | 38 +++++++ .../seedu/address/model/record/Procedure.java | 57 ++++++++++ .../seedu/address/model/record/Record.java | 106 ++++++++++++++++++ .../java/seedu/address/model/tag/CopyTag.java | 63 +++++++++++ .../java/seedu/address/ui/LoginWindow.java | 86 ++++++++++++++ src/main/resources/view/LoginWindow.fxml | 29 +++++ .../logic/commands/AddCommandTest.java | 3 + 42 files changed, 1321 insertions(+), 42 deletions(-) rename docs/images/{cchj1995.jpg => cchj1995.png} (100%) rename docs/images/{wsemis.JPG => wsemis.png} (100%) create mode 100644 src/main/java/seedu/address/logic/commands/CopyCommand.java create mode 100644 src/main/java/seedu/address/logic/commands/ExportCommand.java create mode 100644 src/main/java/seedu/address/logic/commands/ImportCommand.java create mode 100644 src/main/java/seedu/address/logic/commands/SortCommand.java create mode 100644 src/main/java/seedu/address/logic/commands/StatsCommand.java create mode 100644 src/main/java/seedu/address/logic/parser/CopyCommandParser.java create mode 100644 src/main/java/seedu/address/logic/parser/ExportCommandParser.java create mode 100644 src/main/java/seedu/address/logic/parser/ImportCommandParser.java create mode 100644 src/main/java/seedu/address/logic/parser/SortCommandParser.java create mode 100644 src/main/java/seedu/address/logic/parser/StatsCommandParser.java create mode 100644 src/main/java/seedu/address/model/datetime/DateCustom.java create mode 100644 src/main/java/seedu/address/model/datetime/TimeCustom.java create mode 100644 src/main/java/seedu/address/model/description/Description.java create mode 100644 src/main/java/seedu/address/model/person/Age.java create mode 100644 src/main/java/seedu/address/model/person/DrugAllergy.java create mode 100644 src/main/java/seedu/address/model/person/Nric.java create mode 100644 src/main/java/seedu/address/model/person/Sex.java create mode 100644 src/main/java/seedu/address/model/person/Status.java create mode 100644 src/main/java/seedu/address/model/person/Teeth.java create mode 100644 src/main/java/seedu/address/model/person/Tooth.java create mode 100644 src/main/java/seedu/address/model/record/Procedure.java create mode 100644 src/main/java/seedu/address/model/record/Record.java create mode 100644 src/main/java/seedu/address/model/tag/CopyTag.java create mode 100644 src/main/java/seedu/address/ui/LoginWindow.java create mode 100644 src/main/resources/view/LoginWindow.fxml diff --git a/README.adoc b/README.adoc index a542afe48ba4..fbc84980d144 100644 --- a/README.adoc +++ b/README.adoc @@ -1,10 +1,8 @@ = OurTeeth (Morphed from Address Book Level 4) 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://travis-ci.org/CS2103-AY1819S2-W17-2/main[image:https://travis-ci.org/CS2103-AY1819S2-W17-2/main.svg?branch=master["Build Status", link="https://travis-ci.org/CS2103-AY1819S2-W17-2/main"]] +https://app.netlify.com/sites/cs2103-w17-2/deploys[image:https://api.netlify.com/api/v1/badges/0ed4dd99-3443-483f-94e5-64bb727aa43c/deploy-status[Netlify Status]] ifdef::env-github[] image::docs/images/Ui.png[width="600"] diff --git a/docs/AboutUs.adoc b/docs/AboutUs.adoc index ba9c9857dd97..76a75fc2d64c 100644 --- a/docs/AboutUs.adoc +++ b/docs/AboutUs.adoc @@ -47,7 +47,7 @@ Responsibilities: Records IO ''' === Jonathan Choo -image::cchj1995.jpg[width="150", align="left"] +image::cchj1995.png[width="150", align="left"] {empty}[http://github.com/cchj1995[github]] [<>] Role: Developer + @@ -56,7 +56,7 @@ Responsibilities: Login ''' === Wang Debang -image::wsemis.JPG[width="150", aligh="left"] +image::wsemis.png[width="150", aligh="left"] {empty}[https://github.com/wSemis[github]] [<>] Role: Developer + diff --git a/docs/DeveloperGuide.adoc b/docs/DeveloperGuide.adoc index 2a62e9c750e6..cf6671816d6c 100644 --- a/docs/DeveloperGuide.adoc +++ b/docs/DeveloperGuide.adoc @@ -1,4 +1,4 @@ -= AddressBook Level 4 - Developer Guide += OurTeeth - Developer Guide :site-section: DeveloperGuide :toc: :toc-title: @@ -15,7 +15,7 @@ ifdef::env-github[] endif::[] :repoURL: https://github.com/se-edu/addressbook-level4/tree/master -By: `Team SE-EDU`      Since: `Jun 2016`      Licence: `MIT` +By: `Team W17-2`      Since: `14 Mar 2019`      Licence: `MIT` == Setting up @@ -827,27 +827,43 @@ Priorities: High (must have) - `* * \*`, Medium (nice to have) - `* \*`, Low (un [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 -|`* * *` |user |add a new person | +|`* * *` |user |add a new patient's particulars | know about their situation -|`* * *` |user |delete a person |remove entries that I no longer need +|`* * *` |user |edit my patient's particulars when the situation changes | -|`* * *` |user |find a person by name |locate details of persons without having to go through the entire list +|`* * *` |user |my patients' medical records and teeth data tied to them |know how their situation evolved -|`* * *` |user |add a new task | +|`* * *` |user |store, edit, and view my dental patients' teeth condition | understand and serve my dental patients' +teeth condition better + +|`* * *` |user |add a new task | keep track of what I need to do |`* * *` |user |delete a task |remove tasks that I have already completed or no longer need to do |`* * *` |user |edit a task |change details of certains tasks that I have already added -|`* *` |user |hide <> by default |minimize chance of someone else seeing them by accident +|`* * *` |user | encrypt the imported/exported data | ensure patient information remains confidential + +|`* * *` |user | append imported/exported data | work more flexibly with patient information + +|`* * *` |user | my imported/exported data supported by multiple file types | work more flexibly with patient information + +|`* * *` |user | see a statistics report on each patient's dental history | have an easier time understanding their potential problems + +|`* * *` |user | warning to come up when I'm exiting the program if there exists duplicate entries | be reminded to +edit them before exiting. + +|`* *` |user | see an overall statistics report on my patients | analyze potential trends |`* *` |user |copy a person |reduce the time needed to create a new person who has similar records to an existing person in the list |`* *` |user |export entries into an external file |view certain entries even when I am not using the application -|`* *` |user |import entries from an external file |reduce the time needed to create a person whose information can be already be obtained from an external source +|`* *` |user |import entries from an external file |reduce the time needed to create a person whose information can +be already be obtained from an external source + +|`* *` |user |see relevant dentistry tags on my patients' entries | have an overview of my patients' condition |`*` |user with confidential patient information|log into the application with a password |prevent unauthorized access to the application when I am not around diff --git a/docs/UserGuide.adoc b/docs/UserGuide.adoc index 28d55cb7c848..4341183a7238 100644 --- a/docs/UserGuide.adoc +++ b/docs/UserGuide.adoc @@ -296,7 +296,13 @@ Universal Numbering System. ==== The Universal Numbering System -image::images/image1.png[image,width=200,height=294] +ifdef::env-github[] +image::image1.png[width="200"] +endif::[] + +ifndef::env-github[] +image::image1.png[width="200"] +endif::[] The uppercase letters A through T are used for primary teeth and the numbers 1 - 32 are used for permanent teeth. The tooth designated "1" is @@ -340,38 +346,26 @@ newly created teeth layout. indicate personalised note describing more detail on the patient’s teeth health. -=== Export -Exports the entire records into the specified file path with the -specified file name. - -Format: `export FILE_PATH FILE_NAME` - -Example: +=== Importing records: `import` - -* `export /mnt/ext_drive/ clinicRecords.txt` + -Creates a file named clinicRecords.txt in the /mnt/ext_drive/ folder - - -=== Import -Imports data into the program from the specified file path and -overwrites current data. - -Format: `import FILE_PATH [append]` - -Note: The optional “append” adds the file data onto the current records -rather than overwriting. +Reads all entries from a specified text file and overwrites the address book. + +Format: `import FILENAME` Examples: +* `import records1.json` + +Reads all entries from a records1.json and overwrites the address book. + -* `import /mnt/sample/newData` + -Replaces the current records with the ones in the newData file +=== Exporting records: `export` +Writes all entries from the address book to a specified text file. + +If no file exists, a new one will be created. +Format: `export FILENAME` -* `import /mnt/sample/extraData append` + -Appends the records within extraData onto the current program’s records. +Examples: +* `Export records2.json` + +Writes all entries from the address book to records2.json. + === List Shows a list of all personnel or medical stored in the application diff --git a/docs/images/cchj1995.jpg b/docs/images/cchj1995.png similarity index 100% rename from docs/images/cchj1995.jpg rename to docs/images/cchj1995.png diff --git a/docs/images/wsemis.JPG b/docs/images/wsemis.png similarity index 100% rename from docs/images/wsemis.JPG rename to docs/images/wsemis.png diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index f6b961fd5a86aa5fbfe90f707c3138408be7c718..758de960ec7947253b058ff79c88ce51f3abe08a 100644 GIT binary patch delta 7592 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 delta 7527 zcmZ9R1x#F9_qSnicXulc?yf}(6dk0vyZc~+d$GaY-3n72TD-Wng<{36#a;jXnwxvy zd?z`{^Q_;>$vQhHImzBB)d(F`2+2ZdEN_QzrQqP;Kq=DNDA{KoXCCly$>bke%{kEW zYe{bxkm2A2p|qUrkN`qGvS9UV-f^1{Tmv^lyIF-rb}Woy4YW{nG-ugNX^Pi~mfp=` zPROtLj()Lc)?7ukwK~-5mOJ!-;(e=AnFyVa>VMqFzl40c*SoDc5o*a@b;>~91z+ch ztOsV^1g?v%i+~^28+(z>D4ts}4Nu!KY0@@ic}aOyN0Zg*A@O0ze6fgX4lJ)y8u7V^f&}%mO4uz`^vA6Lw-Qo5uGH1}vU$a+^|3w-WK=eF_MX>O$GLV)wcAcp zh*T6(L7XBKYUMaiM49U?hWO;vi-q?5hn!~l&|9-5j>yVW53HH{!By|ludBatTh1%I zB<5SDlSLkLXE_TG{URmGqsQ8OhUpwH)igC2r{Pupf>5+__=jg$-USeGmSt)gWkvYZ z-kZ4kYsYKeP;19vcOcmWSx| z+-U`!P#CBplekzlnu)n_x@Iuiyc^K#h`e_RucIjtwccf#Yv!rlCk-Ad{T>ugl(U)K zcNe#pomfa^@rF;Nj;3tB|G|i){vJ0%uGh6cD z2FMo4ZF!>UMst*&*CY9-S$Cus(VF%ebF~qH?wXCx#Pzts_0RRM+ zYv5Sj6g+ByZ#7_5Adc?4B8=U)D=~kH5C>uV<*=;qLoh_%)P#|P$_zYvnM4X;V@w{t zs^YP1ookyt9BdnCtmj&-sAR|gdMh&!aS}xz88G+T-@wuH5H>b&HHYMO2Og>Si1B1{ zKh9cyP>h=J)WnrLcY9Tl!|2?1o*i|FZvjWf|hn zJA~1%+!E@OTjXpyEc&CBS#OGiI2^{wB3mELt`oI@mCrNMI)1pa*3dLUp8SAx{&tqRVFF1ZJ{z$z>2gi} zTXE63p5N1T$3t2kHa}RBa`yJ=4-p~SRJ%cdmJ?kwkEC3eW{imSw-;xNUMxcTHA0OP zGd`#=MOT0UpQp>NN+Zf55Z>oyU9Z$e?`GYDyqqkm$9qAXjo#^Jkihuzlu08}ES|vm^XWsf4)4S3EMyPmjw~ano-X zU9?8uhiY_7Z9;jciUT!{C0Ku zM)M%VYP7tV-{aX%1+kEQPPE;irmt&z0_?tCgmy-iiVx!3o&u(fL2|eB zT%0gk7fXpV#$KfR8bs+Ra?S8o?Kgd)ht(V-7(^RgF

>O|5`!Y@WVeHnjeM=H)Z) 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 lastShownList = model.getFilteredPersonList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Person personToCopy = lastShownList.get(index.getZeroBased()); + Person copyPerson = personToCopy.copy(); + + model.addPerson(copyPerson); + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + model.commitAddressBook(); + return new CommandResult(String.format(MESSAGE_SUCCESS, copyPerson)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof CopyCommand // instanceof handles nulls + && index.equals(((CopyCommand) other).index)); + } +} diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java index 952a9e7e7f2b..6bcb7b6fbe6b 100644 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ b/src/main/java/seedu/address/logic/commands/EditCommand.java @@ -83,6 +83,11 @@ public CommandResult execute(Model model, CommandHistory history) throws Command } model.setPerson(personToEdit, editedPerson); + + if (personToEdit.isCopy()) { + personToEdit.editCopy(); + } + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); model.commitAddressBook(); return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, editedPerson)); diff --git a/src/main/java/seedu/address/logic/commands/ExportCommand.java b/src/main/java/seedu/address/logic/commands/ExportCommand.java new file mode 100644 index 000000000000..dd962994e5c9 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ExportCommand.java @@ -0,0 +1,63 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +import java.io.File; +import java.io.IOException; +import java.util.logging.Logger; + +import seedu.address.MainApp; +import seedu.address.commons.core.LogsCenter; +import seedu.address.logic.CommandHistory; +import seedu.address.model.Model; +import seedu.address.storage.AddressBookStorage; +import seedu.address.storage.JsonAddressBookStorage; +import seedu.address.storage.StorageManager; + +/** + * Exports records to a text file. + */ +public class ExportCommand extends Command { + + public static final String COMMAND_WORD = "export"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Exports to text file in the \"data\" folder \n" + + "Parameters: FILENAME\n" + + "Example: " + COMMAND_WORD + " records1.json"; + + public static final String MESSAGE_SUCCESS = "Exported the records!"; + private static final String MESSAGE_FAILURE = "Problem while writing to the file."; + + private final File file; + + public ExportCommand(File file) { + this.file = file; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) { + requireNonNull(model); + writeFile(model); + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + return new CommandResult(MESSAGE_SUCCESS); + } + + /** + * writeFile() writes or overwrites a file with the contents of the current address book. + */ + private void writeFile(Model model) { + AddressBookStorage addressBookStorage = new JsonAddressBookStorage(file.toPath()); + + StorageManager storage = new StorageManager(addressBookStorage, null); + + final Logger logger = LogsCenter.getLogger(MainApp.class); + + try { + storage.saveAddressBook(model.getAddressBook(), file.toPath()); + } catch (IOException e) { + logger.warning(MESSAGE_FAILURE); + } + } +} diff --git a/src/main/java/seedu/address/logic/commands/ImportCommand.java b/src/main/java/seedu/address/logic/commands/ImportCommand.java new file mode 100644 index 000000000000..5e475f0d3d6b --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ImportCommand.java @@ -0,0 +1,82 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +import java.io.File; +import java.io.IOException; +import java.util.Optional; +import java.util.logging.Logger; + +import seedu.address.MainApp; +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.exceptions.DataConversionException; +import seedu.address.logic.CommandHistory; +import seedu.address.model.AddressBook; +import seedu.address.model.Model; +import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.util.SampleDataUtil; +import seedu.address.storage.AddressBookStorage; +import seedu.address.storage.JsonAddressBookStorage; +import seedu.address.storage.StorageManager; + +/** + * Imports records from a text file. + */ +public class ImportCommand extends Command { + + public static final String COMMAND_WORD = "import"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Imports a text file in the \"data\" folder \n" + + "Parameters: FILENAME\n" + + "Example: " + COMMAND_WORD + " records1.json"; + + public static final String MESSAGE_SUCCESS = "Imported the records!"; + + private final File file; + + public ImportCommand(File file) { + this.file = file; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) { + requireNonNull(model); + readFile(model); + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + return new CommandResult(MESSAGE_SUCCESS); + } + + /** + * readFile() overwrites the current address book with the contents of the file. + */ + private void readFile(Model model) { + AddressBookStorage addressBookStorage = new JsonAddressBookStorage(file.toPath()); + + StorageManager storage = new StorageManager(addressBookStorage, null); + + final Logger logger = LogsCenter.getLogger(MainApp.class); + + Optional addressBookOptional; + ReadOnlyAddressBook initialData; + + try { + addressBookOptional = storage.readAddressBook(); + if (!addressBookOptional.isPresent()) { + logger.info("Data file not found. Will be starting with a sample AddressBook"); + } + initialData = addressBookOptional.orElseGet(SampleDataUtil::getSampleAddressBook); + } catch (DataConversionException e) { + logger.warning("Data file not in the correct format. " + + "Will be starting with an empty AddressBook"); + initialData = new AddressBook(); + } catch (IOException e) { + logger.warning("Problem while reading from the file. " + + "Will be starting with an empty AddressBook"); + initialData = new AddressBook(); + } + + model.setAddressBook(initialData); + } +} diff --git a/src/main/java/seedu/address/logic/commands/SortCommand.java b/src/main/java/seedu/address/logic/commands/SortCommand.java new file mode 100644 index 000000000000..3de459d48ee9 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/SortCommand.java @@ -0,0 +1,7 @@ +package seedu.address.logic.commands; + +/** + * Sorts data in accordance to the given parameter + */ +public class SortCommand { +} diff --git a/src/main/java/seedu/address/logic/commands/StatsCommand.java b/src/main/java/seedu/address/logic/commands/StatsCommand.java new file mode 100644 index 000000000000..da210749f48e --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/StatsCommand.java @@ -0,0 +1,7 @@ +package seedu.address.logic.commands; + +/** + * Shows statistics for the given patient + */ +public class StatsCommand { +} diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java index b7d57f5db86a..229605ab8808 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -9,12 +9,15 @@ import seedu.address.logic.commands.AddCommand; import seedu.address.logic.commands.ClearCommand; import seedu.address.logic.commands.Command; +import seedu.address.logic.commands.CopyCommand; import seedu.address.logic.commands.DeleteCommand; import seedu.address.logic.commands.EditCommand; import seedu.address.logic.commands.ExitCommand; +import seedu.address.logic.commands.ExportCommand; import seedu.address.logic.commands.FindCommand; import seedu.address.logic.commands.HelpCommand; import seedu.address.logic.commands.HistoryCommand; +import seedu.address.logic.commands.ImportCommand; import seedu.address.logic.commands.ListCommand; import seedu.address.logic.commands.RedoCommand; import seedu.address.logic.commands.SelectCommand; @@ -84,6 +87,15 @@ public Command parseCommand(String userInput) throws ParseException { case RedoCommand.COMMAND_WORD: return new RedoCommand(); + case CopyCommand.COMMAND_WORD: + return new CopyCommandParser().parse(arguments); + + case ImportCommand.COMMAND_WORD: + return new ImportCommandParser().parse(arguments); + + case ExportCommand.COMMAND_WORD: + return new ExportCommandParser().parse(arguments); + default: throw new ParseException(MESSAGE_UNKNOWN_COMMAND); } diff --git a/src/main/java/seedu/address/logic/parser/CopyCommandParser.java b/src/main/java/seedu/address/logic/parser/CopyCommandParser.java new file mode 100644 index 000000000000..7ac5ee3b7585 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/CopyCommandParser.java @@ -0,0 +1,35 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.CopyCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parse user input + */ +public class CopyCommandParser implements Parser { + + /** + * + * @param args user's input for index + * @return corresponding copy command + * @throws ParseException if user input is in a wrong format + */ + public CopyCommand parse(String args) throws ParseException { + requireNonNull(args); + + Index index; + + try { + index = ParserUtil.parseIndex(args); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, CopyCommand.MESSAGE_USAGE), pe); + } + + + return new CopyCommand(index); + } +} diff --git a/src/main/java/seedu/address/logic/parser/ExportCommandParser.java b/src/main/java/seedu/address/logic/parser/ExportCommandParser.java new file mode 100644 index 000000000000..53351f7bb982 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ExportCommandParser.java @@ -0,0 +1,28 @@ +package seedu.address.logic.parser; + +import java.io.File; + +import seedu.address.logic.commands.ExportCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new ExportCommand object. + */ +public class ExportCommandParser implements Parser { + + /** + * Parses the given argument {@code String} in the context of the ExportCommand + * and returns an ExportCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public ExportCommand parse(String args) throws ParseException { + try { + File file = ParserUtil.parseFile(args); + return new ExportCommand(file); + } catch (ParseException pe) { + throw new ParseException(pe.getMessage()); + } + } + // TODO: Combine ImportCommandParser and ExportCommandParser + +} diff --git a/src/main/java/seedu/address/logic/parser/ImportCommandParser.java b/src/main/java/seedu/address/logic/parser/ImportCommandParser.java new file mode 100644 index 000000000000..dffd3c61d8a2 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ImportCommandParser.java @@ -0,0 +1,34 @@ +package seedu.address.logic.parser; + +import java.io.File; + +import seedu.address.logic.commands.ImportCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new ImportCommand object. + */ +public class ImportCommandParser implements Parser { + + /** + * Parses the given argument {@code String} in the context of the ImportCommand + * and returns an ImportCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public ImportCommand parse(String args) throws ParseException { + try { + File file = ParserUtil.parseFile(args); + importValidation(file); + return new ImportCommand(file); + } catch (ParseException pe) { + throw new ParseException(pe.getMessage()); + } + } + + private void importValidation(File file) throws ParseException { + if (!file.exists() || !file.isFile() || !file.canRead()) { + throw new ParseException("File is invalid"); + } + } + +} diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java index b117acb9c55b..579d4a5e29c3 100644 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java @@ -2,6 +2,7 @@ import static java.util.Objects.requireNonNull; +import java.io.File; import java.util.Collection; import java.util.HashSet; import java.util.Set; @@ -121,4 +122,25 @@ public static Set parseTags(Collection tags) throws ParseException } return tagSet; } + + /** + * Parses a {@code String file}. + * + * @throws ParseException if the given {@code file} is invalid. + */ + public static File parseFile(String filePath) throws ParseException { + requireNonNull(filePath); + filePath = filePath.trim(); + final String validationRegex = "\\p{Alnum}+.(txt|xml|json)$"; + + if (!filePath.matches(validationRegex)) { + throw new ParseException("File name is invalid"); + } + + String newPath = "data\\"; + + File file = new File(newPath.concat(filePath)); + + return file; + } } diff --git a/src/main/java/seedu/address/logic/parser/SortCommandParser.java b/src/main/java/seedu/address/logic/parser/SortCommandParser.java new file mode 100644 index 000000000000..5485d667b97e --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/SortCommandParser.java @@ -0,0 +1,7 @@ +package seedu.address.logic.parser; + +/** + * Parses input and creates a new SortCommand object + */ +public class SortCommandParser { +} diff --git a/src/main/java/seedu/address/logic/parser/StatsCommandParser.java b/src/main/java/seedu/address/logic/parser/StatsCommandParser.java new file mode 100644 index 000000000000..1eea1b7c8485 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/StatsCommandParser.java @@ -0,0 +1,7 @@ +package seedu.address.logic.parser; + +/** + * Parses input and creates a new StatsCommand object + */ +public class StatsCommandParser { +} diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java index 30557cf81ee7..cc811add73e9 100644 --- a/src/main/java/seedu/address/model/AddressBook.java +++ b/src/main/java/seedu/address/model/AddressBook.java @@ -100,6 +100,18 @@ public void removePerson(Person key) { indicateModified(); } + /** + * @return {@code true} if no {@code person} is a copy + */ + public boolean checkNoCopy() { + for (Person p : persons) { + if (p.isCopy()) { + return false; + } + } + return true; + } + @Override public void addListener(InvalidationListener listener) { invalidationListenerManager.addListener(listener); diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index e857533821b6..0514f76d2859 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -127,4 +127,6 @@ public interface Model { * Sets the selected person in the filtered person list. */ void setSelectedPerson(Person person); + + boolean checkNoCopy(); } diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index b56806232814..309acbe3aa53 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/seedu/address/model/ModelManager.java @@ -212,6 +212,11 @@ private void ensureSelectedPersonIsValid(ListChangeListener.Change tags = new HashSet<>(); + private CopyTag copyInfo; + private int copyCount; /** * Every field must be present and not null. @@ -34,6 +40,21 @@ public Person(Name name, Phone phone, Email email, Address address, Set tag this.email = email; this.address = address; this.tags.addAll(tags); + copyInfo = null; + copyCount = 0; + } + + public Person(Name name, Phone phone, Email email, Address address, Set tags, + Person personToCopy, int copyCount) { + requireAllNonNull(name, phone, email, address, tags); + this.name = name; + this.phone = phone; + this.email = email; + this.address = address; + this.tags.addAll(tags); + this.tags.add(new Tag("Copy")); + this.copyCount = copyCount; + copyInfo = new CopyTag(personToCopy, "$Copy" + copyCount); } public Name getName() { @@ -52,6 +73,51 @@ public Address getAddress() { return address; } + public int getCopyCount() { + return copyCount; + } + + public boolean hasCopy() { + return copyCount > 0; + } + + /** + * + * @return true if a person has {@code Tag} copy + */ + private boolean copyInTag() { + for (Tag t : getTags()) { + if (t.tagName.equals("Copy")) { + return true; + } + } + return false; + } + + public boolean isCopy() { + return copyInfo != null || copyInTag(); + } + + public void editCopy() { + copyInfo.getOriginalPerson().edittedCopy(); + } + + private void edittedCopy() { + copyCount -= 1; + } + + /** + * @return another instance of the same person + * {@code Tag} Copy is added + */ + public Person copy() { + if (isCopy()) { + return copyInfo.getOriginalPerson().copy(); + } + copyCount++; + return new Person(name, phone, email, address, tags, this, copyCount); + } + /** * Returns an immutable tag set, which throws {@code UnsupportedOperationException} * if modification is attempted. @@ -71,7 +137,8 @@ public boolean isSamePerson(Person otherPerson) { return otherPerson != null && otherPerson.getName().equals(getName()) - && (otherPerson.getPhone().equals(getPhone()) || otherPerson.getEmail().equals(getEmail())); + && (otherPerson.getPhone().equals(getPhone()) || otherPerson.getEmail().equals(getEmail())) + && !(isCopy() || otherPerson.isCopy()); } /** diff --git a/src/main/java/seedu/address/model/person/Sex.java b/src/main/java/seedu/address/model/person/Sex.java new file mode 100644 index 000000000000..4066df2b9bd5 --- /dev/null +++ b/src/main/java/seedu/address/model/person/Sex.java @@ -0,0 +1,53 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Person's Sex in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidSex(String)} + */ +public class Sex { + + + public static final String MESSAGE_CONSTRAINTS = + "Sex should only be either M or F"; + public static final String VALIDATION_REGEX = "^[M,F]$"; + public final String value; + + /** + * Constructs a {@code Sex}. + * + * @param sex A valid Sex value. + */ + public Sex(String sex) { + requireNonNull(sex); + checkArgument(isValidSex(sex), MESSAGE_CONSTRAINTS); + value = sex; + } + + /** + * Returns true if a given string is a valid Sex number. + */ + public static boolean isValidSex(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 Sex // instanceof handles nulls + && value.equals(((Sex) other).value)); // state check + } + + @Override + public int hashCode() { + return value.hashCode(); + } + +} diff --git a/src/main/java/seedu/address/model/person/Status.java b/src/main/java/seedu/address/model/person/Status.java new file mode 100644 index 000000000000..17eb89b9de89 --- /dev/null +++ b/src/main/java/seedu/address/model/person/Status.java @@ -0,0 +1,25 @@ +package seedu.address.model.person; + +import java.util.Date; + +/** + * Represents the status of a teeth. + * Attributes of class is immutable to uphold information integrity. + */ +public class Status { + private String description; + private Date dateCreated; + + public Status(String description) { + this.description = description; + this.dateCreated = new Date(); + } + + public String getDescription() { + return description; + } + + public Date getDateCreated() { + return dateCreated; + } +} diff --git a/src/main/java/seedu/address/model/person/Teeth.java b/src/main/java/seedu/address/model/person/Teeth.java new file mode 100644 index 000000000000..9e939f5ddfeb --- /dev/null +++ b/src/main/java/seedu/address/model/person/Teeth.java @@ -0,0 +1,26 @@ +package seedu.address.model.person; + +import java.util.ArrayList; + +/** + * Represents a set of teeth a Person has. + */ +public class Teeth { + private final int childTeethCount = 20; + private final int adultTeethCount = 32; + + private ArrayList teeth; + + public Teeth() { + teeth = new ArrayList<>(); + } + + /** + * Returns a Tooth representing a patient's tooth using a tooth number. + * @param toothNum the tooth number of tooth to be retrieved. + * @return the tooth represented by provided tooth number. + */ + public Tooth getTooth(int toothNum) { + return null; + } +} diff --git a/src/main/java/seedu/address/model/person/Tooth.java b/src/main/java/seedu/address/model/person/Tooth.java new file mode 100644 index 000000000000..b7b7190cded8 --- /dev/null +++ b/src/main/java/seedu/address/model/person/Tooth.java @@ -0,0 +1,38 @@ +package seedu.address.model.person; + +/** + * Represents a tooth of a Person. + */ +public class Tooth { + private boolean isPresent = true; + private boolean isOnStatus = false; + private Status status; + + public Tooth() { } + + public Tooth(Status status) { + this.isOnStatus = true; + this.status = status; + } + + public Tooth(boolean isPresent, boolean hasStatus) { + this.isPresent = isPresent; + this.isOnStatus = hasStatus; + } + + public void setStatus(Status status) { + this.status = status; + } + + public Status getStatus() { + return status; + } + + public boolean isPresent() { + return isPresent; + } + + public boolean isStatus() { + return isOnStatus; + } +} diff --git a/src/main/java/seedu/address/model/record/Procedure.java b/src/main/java/seedu/address/model/record/Procedure.java new file mode 100644 index 000000000000..b6f001204208 --- /dev/null +++ b/src/main/java/seedu/address/model/record/Procedure.java @@ -0,0 +1,57 @@ +package seedu.address.model.record; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a procedure of a record. + * Guarantees: immutable; is valid as declared in {@link #isValidProcedure(String)} + */ +public class Procedure { + + public static final String MESSAGE_CONSTRAINTS = "Procedures should only contain alphanumeric characters, and it " + + "should not be blank"; + + /* + * The first character of the description 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 procedureType; + + /** + * Constructs a {@code Procedure} + * + * @param procedure A valid procedure + */ + public Procedure(String procedure) { + requireNonNull(procedure); + checkArgument(isValidProcedure(procedure), MESSAGE_CONSTRAINTS); + procedureType = procedure; + } + + /** + * Returns true if a given string is a valid procedure. + */ + public static boolean isValidProcedure (String test) { + return test.matches(VALIDATION_REGEX); + } + + @Override + public String toString() { + return procedureType; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Procedure // instanceof handles nulls + && procedureType.equals(((Procedure) other).procedureType)); // state check + } + + @Override + public int hashCode() { + return procedureType.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/record/Record.java b/src/main/java/seedu/address/model/record/Record.java new file mode 100644 index 000000000000..620a49b86e4c --- /dev/null +++ b/src/main/java/seedu/address/model/record/Record.java @@ -0,0 +1,106 @@ +package seedu.address.model.record; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Objects; + +import seedu.address.model.datetime.DateCustom; +import seedu.address.model.datetime.TimeCustom; +import seedu.address.model.description.Description; +import seedu.address.model.person.Name; +import seedu.address.model.person.Person; + + +/** + * Represents a Record + * Guarantees: details are present and not null, field values are validated, immutable. + */ +public class Record { + + // Identity field + private final Person person; + + // Data field + private final Procedure procedure; + private final DateCustom date; + private final TimeCustom time; + private final Name doctorName; + private final Description description; + + public Record(Person person, Procedure procedure, DateCustom date, TimeCustom time, Name docName, + Description desc) { + requireAllNonNull(person, procedure, date, time, docName, desc); + this.person = person; + this.procedure = procedure; + this.date = date; + this.time = time; + this.doctorName = docName; + this.description = desc; + } + + //getter methods + public Person getPerson() { + return person; + } + + public Procedure getProcedure() { + return procedure; + } + + public DateCustom getDate() { + return date; + } + + public TimeCustom getTime() { + return time; + } + + public Name getDoctorName() { + return doctorName; + } + + public Description getDescription() { + return description; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } else if (!(other instanceof Record)) { + return false; + } + + Record otherRecord = (Record) other; + + return otherRecord.getPerson().equals(getPerson()) + && otherRecord.getProcedure().equals(getProcedure()) + && otherRecord.getDate().equals(getDate()) + && otherRecord.getTime().equals(getTime()) + && otherRecord.getDoctorName().equals(getDoctorName()) + && otherRecord.getDescription().equals(getDescription()); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append(person.getName()) + .append(" Procedure: ") + .append(getProcedure()) + .append(" DateCustom: ") + .append(getDate()) + .append(" TimeCustom: ") + .append(getTime()) + .append(" Doctor: ") + .append(getDoctorName()) + .append(" Description: ") + .append(getDescription()); + + return builder.toString(); + } + + @Override + public int hashCode() { + return Objects.hash(person, procedure, date, time, doctorName, description); + } +} diff --git a/src/main/java/seedu/address/model/tag/CopyTag.java b/src/main/java/seedu/address/model/tag/CopyTag.java new file mode 100644 index 000000000000..9aa0f5eeb0e5 --- /dev/null +++ b/src/main/java/seedu/address/model/tag/CopyTag.java @@ -0,0 +1,63 @@ +package seedu.address.model.tag; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +import seedu.address.model.person.Person; + +/** + * Represents a Tag in the address book. + * Guarantees: immutable; name is valid as declared in {@link #isValidTagName(String)} + */ +public class CopyTag { + + public static final String MESSAGE_CONSTRAINTS = "Tags names should be like $Copy1"; + public static final String VALIDATION_REGEX = "(\\$)(Copy)([0-9]+)"; + + public final String tagName; + private final Person originalPerson; + + /** + * Constructs a {@code CopyTag}. + * + * @param personToCopy A pointer to the original person + * @param tagName A valid tag name. + */ + public CopyTag(Person personToCopy, String tagName) { + requireNonNull(personToCopy); + originalPerson = personToCopy; + checkArgument(isValidTagName(tagName), MESSAGE_CONSTRAINTS); + this.tagName = tagName; + } + + /** + * Returns true if a given string is a valid tag name. + */ + public static boolean isValidTagName(String test) { + return test.matches(VALIDATION_REGEX); + } + + public Person getOriginalPerson() { + return originalPerson; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Tag // instanceof handles nulls + && tagName.equals(((Tag) other).tagName)); // state check + } + + @Override + public int hashCode() { + return tagName.hashCode(); + } + + /** + * Format state as text for viewing. + */ + public String toString() { + return '[' + tagName + ']'; + } + +} diff --git a/src/main/java/seedu/address/ui/LoginWindow.java b/src/main/java/seedu/address/ui/LoginWindow.java new file mode 100644 index 000000000000..493586bea9f6 --- /dev/null +++ b/src/main/java/seedu/address/ui/LoginWindow.java @@ -0,0 +1,86 @@ +package seedu.address.ui; + +import java.util.logging.Logger; + +import javafx.fxml.FXML; +import javafx.stage.Stage; +import seedu.address.commons.core.GuiSettings; +import seedu.address.commons.core.LogsCenter; +import seedu.address.logic.Logic; + + +/** + * The Main Window. Provides the basic application layout containing + * a menu bar and space where other JavaFX elements can be placed. + */ +public class LoginWindow extends UiPart { + + private static final String FXML = "LoginWindow.fxml"; + + private final Logger logger = LogsCenter.getLogger(getClass()); + + private Stage primaryStage; + private Logic logic; + private HelpWindow helpWindow; + + + public LoginWindow(Stage primaryStage, Logic logic) { + super(FXML, primaryStage); + + // Set dependencies + this.primaryStage = primaryStage; + this.logic = logic; + + // Configure the UI + setWindowDefaultSize(logic.getGuiSettings()); + + helpWindow = new HelpWindow(); + } + + public Stage getPrimaryStage() { + return primaryStage; + } + + + /** + * Sets the default size based on {@code guiSettings}. + */ + private void setWindowDefaultSize(GuiSettings guiSettings) { + primaryStage.setHeight(guiSettings.getWindowHeight()); + primaryStage.setWidth(guiSettings.getWindowWidth()); + if (guiSettings.getWindowCoordinates() != null) { + primaryStage.setX(guiSettings.getWindowCoordinates().getX()); + primaryStage.setY(guiSettings.getWindowCoordinates().getY()); + } + } + + + void show() { + primaryStage.show(); + } + + void close() { + primaryStage.close(); + } + + /** + * Closes the application. + */ + @FXML + private void handleExit() { + GuiSettings guiSettings = new GuiSettings(primaryStage.getWidth(), primaryStage.getHeight(), + (int) primaryStage.getX(), (int) primaryStage.getY()); + logic.setGuiSettings(guiSettings); + helpWindow.hide(); + primaryStage.hide(); + } + + /** + * Handles the Enter button pressed event. + */ + @FXML + private void handleStringEntered() { + primaryStage.close(); + } + +} diff --git a/src/main/resources/view/LoginWindow.fxml b/src/main/resources/view/LoginWindow.fxml new file mode 100644 index 000000000000..1b3b3dfb9762 --- /dev/null +++ b/src/main/resources/view/LoginWindow.fxml @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/test/java/seedu/address/logic/commands/AddCommandTest.java b/src/test/java/seedu/address/logic/commands/AddCommandTest.java index f120a897779b..e3830b35c3d6 100644 --- a/src/test/java/seedu/address/logic/commands/AddCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/AddCommandTest.java @@ -201,6 +201,9 @@ public Person getSelectedPerson() { public void setSelectedPerson(Person person) { throw new AssertionError("This method should not be called."); } + + @Override + public boolean checkNoCopy() { throw new AssertionError("This method should not be called."); } } /**