diff --git a/.travis.yml b/.travis.yml index 1ffe1f2a5c77..603353277546 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,7 +5,7 @@ matrix: script: >- ./config/travis/run-checks.sh && - travis_retry ./gradlew clean checkstyleMain checkstyleTest headless allTests coverage coveralls asciidoctor copyDummySearchPage + travis_retry ./gradlew clean checkstyleMain checkstyleTest headless allTests coverage coveralls asciidoctor deploy: skip_cleanup: true diff --git a/README.adoc b/README.adoc index 450054624f48..e050e9dab380 100644 --- a/README.adoc +++ b/README.adoc @@ -1,11 +1,9 @@ -= Address Book (Level 4) += InsuRen 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://gitter.im/se-edu/Lobby[image:https://badges.gitter.im/se-edu/Lobby.svg[Gitter chat]] +https://travis-ci.org/CS2103-AY1819S1-W13-1/main[image:https://travis-ci.org/CS2103-AY1819S1-W13-1/main.svg?branch=master[Build Status]] +https://ci.appveyor.com/project/denzelchung/main-1gn9v/branch/master[image:https://ci.appveyor.com/api/projects/status/0cw1hdcgcqu31k9l/branch/master?svg=true[Build status]] +https://coveralls.io/github/CS2103-AY1819S1-W13-1/main?branch=master[image:https://coveralls.io/repos/github/CS2103-AY1819S1-W13-1/main/badge.svg?branch=master[Coverage Status]] ifdef::env-github[] image::docs/images/Ui.png[width="600"] @@ -15,19 +13,25 @@ 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. +'Ren' in Mandarin translates to 'person' or 'people', and true to our name, this app is all about managing one's +network of clients in an organized, efficient and intuitive manner. InsuRen is geared to the needs of the modern +Insurance salesman, but anyone whose business is their strong rapport with their clients will find this +to be an indispensable tool. + +Managing one's clientele has never been easier! InsuRen is designed to be used +without the hassle of a mouse, as users interact with their database purely through intuitive text commands. +Some key features of our product include: + +* Adding tags to contacts and sorting contacts by tags. +* Scheduling meetings for each contact and consolidating all meetings into a weekly timetable. +* Keeping track of clients' payment dues. +* Importing contacts from external databases and exporting the contacts in InsuRen's database. +* And many more! == Site Map * <> * <> -* <> * <> * <> @@ -35,6 +39,7 @@ endif::[] * 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_. +* This project was evolved from the "AddressBook-Level4" project created by SE-EDU initiative at https://github.com/se-edu/ * Libraries used: https://github.com/TestFX/TestFX[TextFX], https://bitbucket.org/controlsfx/controlsfx/[ControlsFX], 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..5884040009b4 --- /dev/null +++ b/_reposense/config.json @@ -0,0 +1,30 @@ +{ + "authors": [ + { + "githubId": "A19Sean", + "displayName": "AUYOK... SEAN", + "authorNames": ["A19Sean", "Auyok Sean"] + }, + { + "githubId": "chantca95", + "displayName": "CHAN ... ALEX", + "authorNames": ["chantca95"] + }, + { + "githubId": "AyushChatto", + "displayName": "CHATT...AYUSH", + "authorNames": ["AyushChatto"] + }, + { + "githubId": "denzelchung", + "displayName": "CHUNG...ENZEL", + "authorNames": ["denzelchung"] + }, + { + "githubId": "zioul123", + "displayName": "KIM-C...LOUIZ", + "authorNames": ["zioul123"] + } + ] +} + diff --git a/build.gradle b/build.gradle index f8e614f8b49b..90c6c266f0da 100644 --- a/build.gradle +++ b/build.gradle @@ -82,7 +82,7 @@ dependencies { } shadowJar { - archiveName = 'addressbook.jar' + archiveName = 'insuren.jar' destinationDir = file("${buildDir}/jar/") } @@ -134,6 +134,7 @@ test { testLogging { events TestLogEvent.FAILED, TestLogEvent.SKIPPED + exceptionFormat "full" // Prints the currently running test's name in the CI's build log, // so that we can check if tests are being silently skipped or @@ -207,9 +208,8 @@ 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-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) + 'site-name': 'InsuRen', + 'site-githuburl': 'https://github.com/CS2103-AY1819S1-W13-1/main', ] options['template_dirs'].each { @@ -236,11 +236,6 @@ task deployOfflineDocs(type: Copy) { } } -task copyDummySearchPage(type: Copy) { - from 'docs/DummySearchPage.html' - into "${buildDir}/docs/html5" -} - deployOfflineDocs.dependsOn asciidoctor processResources.dependsOn deployOfflineDocs diff --git a/docs/AboutUs.adoc b/docs/AboutUs.adoc index e647ed1e715a..36466b2a5d0a 100644 --- a/docs/AboutUs.adoc +++ b/docs/AboutUs.adoc @@ -4,53 +4,53 @@ :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} + +InsuRen was developed by the https://github.com/CS2103-AY1819S1-W13-1[W13-1] 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]] [<>] +=== Auyok Sean +image::a19sean.png[width="150", align="left"] +{empty}[https://github.com/A19Sean[github]] [<>] -Role: Project Advisor +Role: Developer + +Responsibilities: Documentation and Code Quality, UI Component ''' -=== John Roe -image::lejolly.jpg[width="150", align="left"] -{empty}[http://github.com/lejolly[github]] [<>] +=== Chattoraj Ayush +image::ayushchatto.png[width="150", align="left"] +{empty}[https://github.com/AyushChatto[github]] [<>] -Role: Team Lead + -Responsibilities: UI +Role: Developer + +Responsibilities: Workflow, Git, and Github, Logic Component ''' -=== Johnny Doe -image::yijinl.jpg[width="150", align="left"] -{empty}[http://github.com/yijinl[github]] [<>] +=== Alex Chan +image::chantca95.png[width="150", align="left"] +{empty}[https://github.com/chantca95[github]] [<>] Role: Developer + -Responsibilities: Data +Responsibilities: Testing, UI Component, Documentation ''' -=== Johnny Roe -image::m133225.jpg[width="150", align="left"] -{empty}[http://github.com/m133225[github]] [<>] +=== Denzel Chung +image::denzelchung.png[width="150", align="left"] +{empty}[https://github.com/denzelchung[github]] [<>] Role: Developer + -Responsibilities: Dev Ops + Threading +Responsibilities: Integration, Storage Component ''' -=== Benson Meier -image::yl_coder.jpg[width="150", align="left"] -{empty}[http://github.com/yl-coder[github]] [<>] +=== Louiz Kim-Chan +image::zioul123.png[width="150", align="left"] +{empty}[https://github.com/zioul123[github]] [<>] -Role: Developer + -Responsibilities: UI +Role: Team lead + +Responsibilities: Deliverables and deadlines, Model Component ''' diff --git a/docs/ContactUs.adoc b/docs/ContactUs.adoc index 5de5363abffd..c2adf46e7b1b 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-AY1819S1-W13-1/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 `louiz.kc [at] u.nus.edu`, `sean.auyok [at] u.nus.edu`, `ayush.chattoraj [at] u.nus.edu`, `alexchan [at] u.nus.edu`, `e0177160 [at] u.nus.edu`. diff --git a/docs/DeveloperGuide.adoc b/docs/DeveloperGuide.adoc index 817ec81d7832..4a8b9dee016b 100644 --- a/docs/DeveloperGuide.adoc +++ b/docs/DeveloperGuide.adoc @@ -1,4 +1,4 @@ -= AddressBook Level 4 - Developer Guide += InsuRen - Developer Guide :site-section: DeveloperGuide :toc: :toc-title: @@ -13,9 +13,9 @@ ifdef::env-github[] :warning-caption: :warning: :experimental: endif::[] -:repoURL: https://github.com/se-edu/addressbook-level4/tree/master +:repoURL: https://github.com/CS2103-AY1819S1-W13-1/main/tree/master -By: `Team SE-EDU`      Since: `Jun 2016`      Licence: `MIT` +By: `Team W13-1`      Since: `Sep 2018`      Licence: `MIT` == Setting up @@ -74,9 +74,9 @@ Optionally, you can follow the <> docume ==== Updating documentation to match your fork -After forking the repo, the documentation will still have the SE-EDU branding and refer to the `se-edu/addressbook-level4` repo. +After forking the repo, the documentation will still have the CS2103-AY1819S1-W13-1 branding and refer to the `CS2103-AY1819S1-W13-1/main` repo. -If you plan to develop this fork as a separate product (i.e. instead of contributing to `se-edu/addressbook-level4`), you should do the following: +If you plan to develop this fork as a separate product (i.e. instead of contributing to `CS2103-AY1819S1-W13-1/main`), you should do the following: . Configure the <> in link:{repoURL}/build.gradle[`build.gradle`], such as the `site-name`, to suit your own project. @@ -98,10 +98,7 @@ Having both Travis and AppVeyor ensures your App works on both Unix-based platfo ==== Getting started with coding -When you are ready to start coding, - -1. Get some sense of the overall design by reading <>. -2. Take a look at <>. +When you are ready to start coding, get some sense of the overall design by reading <>. == Design @@ -213,15 +210,10 @@ image::ModelClassDiagram.png[width="800"] The `Model`, * stores a `UserPref` object that represents the user's preferences. -* stores the Address Book data. +* stores the contact information 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. * 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"] - [[Design-Storage]] === Storage component @@ -244,6 +236,188 @@ Classes used by multiple components are in the `seedu.addressbook.commons` packa This section describes some noteworthy details on how certain features are implemented. +// tag::byNameCommands[] +=== By-Name-Commands Feature +==== General Current Implementation + +The "by-name-commands" are extensions to the regular Commands, facilitated by classes that extend the regular `Command` classes. Currently implemented are the `EditByNameCommand` and the `DeleteByNameCommand`. +They make use of `String` identifiers and the `PersonFinderUtil` to find the `Person` that the `Command` refers to, rather than an `Index`. + +This allows time to be saved when trying to run a command, because instead of having to run a `find` or `list` command to display a `Person`, then type the command based on the `Index` of the list that the `Person` appears under, commands can be targeted swiftly and precisely. +The "by-name-commands" depend on the following operation/classes: + +* `PersonFinderUtil#findPerson(Model model, String personIdentifier)` -- Finds and returns the `Person` that is uniquely identified by the `personIdentifier` in the `Model` provided. +* `NameContainsAllKeywordsPredicate` -- Tests as true when the name of a `Person` matches all the keywords in the command's arguments. +** The `PersonFinderUtil#findPerson` method makes use of the `NameContainsAllKeywordsPredicate`, which is in contrast to the `NameContainsKeywordsPredicate` used in the `FindCommand`. +** When editing/deleting by name, we need a more specific filter, rather than a general one. Instead of finding a `Person` that contains at least one identifier term in their name, the `Person` found must contain *all* identifier terms in their name. + +The following sequence diagram shows how a command is generated by the `AddressBookParser`. When a `` (`edit`/`delete`) is provided, the `CommandParser` will either generate a `Command` or a `ByNameCommand`. + +image::ByNameCommandSequenceDiagram.png[width="600"] + +[NOTE] +In the following section, the shorthand format `\_ByNameCommand` and `_Command` when used in the same context will refer to a similar type of command, e.g. `DeleteByNameCommand` and `DeleteCommand`, but `_` is general to refer to either `Delete` or `Edit`. + +==== Design Considerations +===== Aspect: Whether a `_ByNameCommand` should Extend the Regular `_Command` +* **Alternative 1 (current choice):** It extends the `Command` as shown: + +image::ByNameCommandClassDiagram.png[width="150"] + +** Pros: Due to polymorphism, a `_ByNameCommand` can replace instances of `_Command` seamlessly in the code without having to +change many parts to add this additional feature. It also makes sense, because a `_ByNameCommand` "is a" `_Command` (e.g. an `EditByNameCommand` is an `EditCommand`) +** Cons: There is an unused field in `EditCommand` (`index`). +* **Alternative 2:** Create a new `_ByNameCommand`, standalone from the `_Command` +** Pros: Can save a bit of memory space on execution, since parts of the `_Command` that are not used do not provide extra baggage to the `_ByNameCommand` (e.g. no extra `Index` in the `EditByNameCommand`) +** Cons: There is a need to modify more parts of the Logic component in the code base to accommodate a new command. + +===== Aspect: When the Person is Searched/Matched +* **Alternative 1 (current choice):** During the execution of `execute` +** Additional details: A `String personIdentifier` will be stored in the command, and upon `execute`, a person is first matched, then the edit is carried out. +** Pros: Execute takes in the model as an argument, making searching for a `Person` convenient. +** Cons: The same `_ByNameCommand` executed at a different time can have a different result since it does not have a unique `Person`, but an identifier to find a name. +* **Alternative 2:** Before creation of the command +** Additional details: The command will have a `Person` +** Pros: The command is deterministic, since it targets a unique `Person`. +** Cons: Need to gain access to the model before the person can be found, which is not usually done by `AddressBookParser`; high level changes are necessary. + +// tag::editByName[] +==== Edit By Name feature +===== Current Implementation + +The edit by name mechanism is facilitated by the new `Command`, `EditByNameCommand`. +It extends `EditCommand` with a "Person Identifier" String that is used in place of the Index (of a displayed list) used in the normal `EditCommand`. +Additionally, it implements/depends on the following operations: + +* `EditByNameCommand#execute()` -- Executes the command encapsulated by this `EditByNameCommand`. + +Given below is an example usage scenario and how the Edit-By-Name mechanism behaves at each step. + +Step 1. The user launches the application and already has at least one client's contact in InsuRen. + +image::EditByNameCommand1StateDiagram.png[width="800"] + +Step 2. The user executes `edit Alice p/91232233` to edit Alice's phone number. However, there are more than two people with a name that matches Alice, so InsuRen notifies the user. + +image::EditByNameCommand2StateDiagram.png[width="800"] + +[NOTE] +If a command fails its execution due to multiple or no people matching the identifier, it will not edit any contact details. + +Step 3. The user uses a much more specific name identifier, `edit Alice Tay Ren Ying p/91232233`, but this does not match any contact, so InsuRen notifies the user. + +image::EditByNameCommand3StateDiagram.png[width="800"] + +Step 4. The user uses a name identifier that uniquely identifies one person, `edit Alice Tay p/91232233`. The edit command is carried out, and the contact details of the identified person are changed accordingly. + +image::EditByNameCommand4StateDiagram.png[width="800"] + +The following activity diagram summarizes what happens when a user executes the `EditByNameCommand`: + +image::EditByNameCommandActivityDiagram.png[width="500"] +// end::editByName[] +// tag::deleteByName[] +==== Delete By Name feature +===== Current Implementation + +The delete by name mechanism is facilitated by the new `Command`, `DeleteByNameCommand`. +It extends `DeleteCommand` with a "Person Identifier" String that is used in place of the Index (of a displayed list) used in the normal `DeleteCommand`. +Additionally, it implements the following operations: + +* `DeleteByNameCommand#execute()` -- Executes the command encapsulated by this `DeleteByNameCommand`. + +Given below is an example usage scenario and how the Delete-By-Name mechanism behaves at each step. + +Step 1. The user launches the application and already has at least one client's contact in InsuRen. + +image::DeleteByNameCommand1StateDiagram.png[width="800"] + +Step 2. The user executes `delete Alice` to delete Alice from InsuRen. However, there are more than two people with a name that matches Alice, so InsuRen notifies the user. + +image::DeleteByNameCommand2StateDiagram.png[width="800"] + +[NOTE] +If a command fails its execution due to multiple or no people matching the identifier, it will not delete any contact details. + +Step 3. The user uses a much more specific name identifier, `delete Alice Tay Ren Ying`, but this does not match any contact, so InsuRen notifies the user. + +image::DeleteByNameCommand3StateDiagram.png[width="800"] + +Step 4. The user uses a name identifier that uniquely identifies one person, `delete Alice Tay`. The delete command is carried out, Alice Tay is removed from InsuRen's contact list. + +image::DeleteByNameCommand4StateDiagram.png[width="800"] + +The following activity diagram summarizes what happens when a user executes the `DeleteByNameCommand`: + +image::DeleteByNameCommandActivityDiagram.png[width="350"] +// end::deleteByName[] +// end::byNameCommands[] + +// tag::schedule[] +=== Schedule feature +==== Current Implementation +The schedule mechanism is facilitated by the new `Command`, `Schedule`. It extends `AddressBook` with a list of meetings, stored internally as a `UniqueMeetingList`. It also allows meetings to be associated to InsuRen entries, since each `Person` can have up to one `Meeting`. +The complete list of meetings, as well as the meetings scheduled on a single day, can subsequently be accessed using the `Meetings` command. +Additionally, the `Schedule` Command implements the following operations: + +* `ScheduleCommand#createScheduledPerson(Person personToSchedule, Meeting meeting)` - Returns a `Person` object that has a meeting scheduled according to `meeting`. +* `ScheduleCommand#execute()` - Executes the command encapsulated by `ScheduleCommand`. + +Given below is an example usage scenario and how the Schedule mechanism behaves at each step. + +Step 1. The user launches the application and already has at least one client's contact in InsuRen. + +image::ScheduleCommand1StateDiagram.png[width="300"] + +Step 2. The user executes `schedule 1 m/16/10/18 1800` to schedule a meeting with the person in the first index at 1800 hours on 16th October, 2018. However, there is already a meeting scheduled at this time, so it is flagged out to the user. +[NOTE] +No meetings are scheduled if there is a clash + +image::ScheduleCommand2StateDiagram.png[width="300"] + +Step 3. The user executes `schedule 1 m/32/10/18 1830` but since this is not a valid date, InsuRen flags it out to the user. + +image::ScheduleCommand3StateDiagram.png[width="700"] + +Step 4. The user executes `schedule 1 m/16/10/18 1830`. The meeting is schedule and the person card is changed to reflect the same accordingly. + +image::ScheduleCommand4StateDiagram.png[width="300"] + +The following activity diagram summarises what happens when a user executes the `ScheduleCommand`: + +image::ScheduleCommandActivityDiagram.png[width="350"] + +The following sequence diagram shows how the operation itself works. + +image::ScheduleSequenceDiagram.png[width="700"] + +==== Design Considerations + +===== Aspect: Where meetings are stored + +* **Alternative 1 (Current choice):** The meetings are stored in both the `Person` model and in the global meeting list `UniqueMeetingList`. +** Pros: Easy to ensure no clashes occur between meetings. +** Cons: Significant changes need to be made to the model to accomodate this. + +* **Alternative 2: ** The meetings are stored in only the `Person` model. +** Pros: Minimal changes to the model required; prevents duplication of data. +** Cons: Difficult to ensure uniqueness of meeting times. + +* **Alternative 3: ** The meetings are stored in only the `UniqueMeetingList`. +** Pros: Prevents the duplication of data; easy to ensure no clashes. +** Cons: Would need additional data structures to pair the meeting to the entry. + +===== Aspect: Date storage format + +* **Alternative 1 (Current choice):** The date and time is stored as a 10-character string. +** Pros: Allows the setting of a `none` value, and offers flexibility. +** Cons: Does not utilize the Java API libraries for dates and times. + +* **Alternative 2:** The date and time is stored as a `DateAndTime` object. +** Pros: Ability to use Java API functions for dates. +** Cons: Less flexible as all dates entered must be valid. +// end::schedule[] + + // tag::undoredo[] === Undo/Redo feature ==== Current Implementation @@ -323,14 +497,249 @@ image::UndoRedoActivityDiagram.png[width="650"] ** 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[] +// tag::undoredo[] + +// tag::import[] +=== Import feature +==== Current Implementation +The import contacts feature is facilitated by the new `Command`, `import`. It adds a list of contacts from a properly formatted csv file +to `AddressBook`. The rules pertaining to accepted formatting of csv files can be found in the user guide. Additionally, it +implements the following operations: + +* `getFileFromUserInput(String)` -- gets a File from the path indicated by a user's text input. +* `getFileFromFileBrowser()` -- gets a File via a file browser. +* `parseFile(File) and parseLinesFromFile(BufferedReader)` -- parses the file from either of the above two methods. Prepares an +arrayList of Persons to add to the contact list. + +Given below is an example usage scenario and how the import mechanism behaves at each step. + +Step 1. The user launches an application and there is either a list of existing contacts or the list is empty. + +Step 2. The user executes import command (i for shorthand). If the user ONLY types import, a file browser will pop up. +If the user includes a file path, InsuRen will attempt to retrieve the file from the given path. + +image::import.png[width="400"] + +image::import_user_input.png[width="400"] + +Step 3. If no such file exists, InsuRen will report an error. + +image::import_user_input_fail.png[width="400"] + +Step 4. If the file is successfully loaded (regardless of method), InsuRen checks for duplicates and incomplete contacts. +Insuren compiles a list of contacts and runs the `add` Command on all of them, adding them to the list of existing contacts. + +Step 5. A relevant message will be displayed, depending on whether there were successful imports, duplicate contacts etc. + +image::import_success.png[width="400"] + +image::import_duplicates.png[width="400"] + +The following activity diagram summarizes what happens when a user executes the `Import` Command: + +image::importActivityDiagram.png[width="400"] + +The following sequence diagram shows what happens when a user executes the `Import` Command (user input mode only, +file browser mode omitted): + +image::ImportSequenceDiagram2.png[width="400"] + +==== Design Considerations + +===== Aspect: How import executes + +* **Alternative 1 (current choice):** Build from `Add` command: +Import makes use of the `hasPerson` method of `Model` to check for duplicate contacts in the csv file being imported. +It also manually checks if any entry in the csv file is incomplete in that it has no name value. +Lastly, the import command also utilizes the format checking methods in `Name`, `Email`, `Address` etc. to catch any +entries with invalid formats +** Pros: Easy to implement, any future modifications to Add or any changes to the validity of `Name`, `Email` etc will +not cause import to crash. +** Cons: Higher coupling. +// end::import[] + +// tag::export[] +=== Export feature +==== Current Implementation +The export contacts feature is facilitated by the new `Command`, `export`. It takes the current list of contacts in InsuRen +and exports it as a csv file, whose file name is given by the user and MUST end with .csv. The exported contact list will be +saved in the root directory of the project. `export` implements the following operations: + +* `parse(String)` - parses the user's given file name String and checks if it is valid. +* `populateFile(PrintWriter, Model)` - populates the (already initialized) file with data from the current Model. +* `insertPersonIntoCsv(Person, PrintWriter) and cleanEntry(String)` - these two methods add contacts to the csv in the same +order as they are displayed in InsuRen. Fields are cleaned by removing commas and brackets before being inserted in to the csv. + +Given below is an example usage scenario and how the export mechanism behaves at each step. + +Step 1. The user launches an application and there is either a list of existing contacts or the list is empty. + +Step 2. The user executes export command (x for shorthand), followed by FILE_NAME. If no file name is given or the file name +does not end with .csv, InsuRen throws an error message. + +Step 3. InsuRen fetches the current contact list, creates a new .csv file and copies all contacts into it. + +The following activity diagram summarizes what happens when a user executes the `Export` Command: + +image::ExportActivityDiagram.png[width="400"] + +The following sequence diagram shows what happens when a user executes the `Export` Command: + +image::ExportSequenceDiagram2.png[width="400"] + +==== Design Considerations + +===== Aspect: How export executes + +* **Alternative 1 (current choice):** Read contacts from a ReadOnlyAddressBook: +Export makes use of model.getAddressBook() and the getPersonList method within. +** Pros: Easy to implement. Since we are only dealing with a ReadOnlyAddressBook, the state of InsuRen will not be altered. +** Cons: Only able to capture snapshots of the contact list. Not dynamically updated. +// end::export[] + +// tag::addpicture[] +=== Add Picture feature +==== Current Implementation + +The picture mechanism is facilitated by the new `PictureCommand`. +It extends `Command` with an execution to set a picture, stored internally in `Person` as `picture`. + +Given below is an example usage scenario and how the picture mechanism behaves at each step. + +Step 1. The user launches the application and already has at least one client's contact in InsuRen. + +image::PictureCommand1StateDiagram.png[width="800"] + +Step 2. The user executes `pic 4 l/images/invalidpath.jpg` to add a picture for David. However, the file `invalidpath.jpg` does not exist. +`Picture#isValidPicture()` validates the given file path and InsuRen informs the user that the path given is invalid. + +image::PictureCommand2StateDiagram.png[width="800"] + +[NOTE] +If a command fails its execution, it will not pass the validation check, `Picture#isValidPicture()`, so InsuRen will not update the user's picture and instead return an error message. + +Step 3. The user now decides to execute `pic 4 l/images/david.jpg`, a valid image located in his drive, to add a picture for David. +The `pic` command calls `Model#getFilteredPersonList()` to retrieve the list of contacts and filters `index` 4. +The `PictureCommandParser` retrieves the input from the user and validates it. +`ParserUtil#parseFileLocation()` is called and the picture path is checked. If the path is valid, it then calls `Picture#setPicture()` to update the picture for the contact. Finally, `Model#commitAddressBook()` is called, causing the modified state of the address book after the `pic 4 l/images/david.jpg` command executes to be saved. + +image::PictureCommand3StateDiagram.png[width="800"] + +The following activity diagram summarizes what happens when a user executes the `PictureCommand`: + +image::PictureCommandActivityDiagram.png[width="400"] + +The following sequence diagram shows what happens when a user executes the `PictureCommand`: + +image::PictureCommandSequenceDiagram.png[width="800"] + +==== Design Considerations + +===== Aspect: How picture is stored + +* **Alternative 1 (current choice):** `Person` has a picture field. +** Pros: `Picture` can have it's own `Picture#isValidPicture()` method to validate the input. It is consistent with the other fields within `Person`. +** Cons: More memory is used as there is a need to store an object. A new `Picture` class has to be made and implemented. +* **Alternative 2:** `Person` will store a `Path` or `String` instead. +** Pros: Will use less memory (do not have to implement a new class and store an object). +** Cons: All checks have to be done within the `execute` method. Might overlook certain details and cause bugs. + +===== Aspect: Type of picture + +* **Alternative 1 (current choice):** Picture can be a `.jpg` or `.png` file. +** Pros: `.jpg` and `.png` are common file formats that the user is used to. +** Cons: Not flexible in what image files are accepted. +* **Alternative 2:** In addition to alternative 1, the picture can also be a valid `URL` containing an image. +** Pros: More flexible. User does not have to download the image file onto his local disk in order to use it. Can retrieve pictures of his contacts online and use it directly. +** Cons: Additional checks have to be done (i.e. check if the `URL` is valid, check if the `URL` is an image file, what happens if the `URL` or server is broken?) + +===== Aspect: Path validation + +* **Alternative 1 (current choice):** File location input from user is checked against `Files#exists()` and whether it ends with a `.png` or `.jpg`. +** Pros: More secure. `Files#exists()` checks whether the file is on the disk while the other checks for the file extension. +** Cons: Will have to check twice. +* **Alternative 2:** Just do `Files#exists()`. +** Pros: Straightforward and simple. +** Cons: Less secure, might result in an error if the file is not checked properly. + +// end::addpicture[] + +// tag::tagcommand[] +=== Tag feature +==== Current Implementation + +Each contact in Insuren can have any number of tags. The `tag` command allows the user to easily find contacts by tags. +The user can also easily edit or delete tags using the tag command, allowing for better management of tags in Insuren. + +Given below is an example usage scenario and how the tag command behaves at each step. + +Step 1. The user launches the application and already has a few tagged contacts in InsuRen. + +image::TagCommand1StateDiagram.png[width="300"] + +Step 2. The user executes `tag Important` to retrieve all contacts tagged with `Important`. Tags are **case-sensitive**. + +image::TagCommand2StateDiagram.png[width="300"] + +Step 3. The user executes `tag Family Colleague` to retrieve all contacts tagged with `Family` or `Colleague`. + +image::TagCommand3StateDiagram.png[width="300"] + +Step 4. If the user wants to change all instances of the `Colleague` tag to `Work`, the user can input `tag edit +Colleague Work`. `edit` is **not** case-sensitive. + +image::TagCommand4StateDiagram.png[width="300"] + +Step 5. If the user would like to delete the `close` tag, the user simply executes `tag close delete`. +`delete` is **not** case-sensitive. + +image::TagCommand5StateDiagram.png[width="300"] + +Step 6. If the user would like to delete the `Family` and `Colleague` tags together, the user simply executes `tag +Family Colleague delete`. Both tags will be deleted. + +image::TagCommand6StateDiagram.png[width="300"] + +All tag commands can be undone or redone with `undo` or `redo` respectively. + +The following activity diagram summarizes what happens when a user executes the `PictureCommand`: + +image::TagCommandActivityDiagram.png[width="500"] + +The following sequence diagram shows what happens when a user executes the `TagCommand`: + +image::TagCommandSequenceDiagram.png[width="800"] + +==== Design Considerations + +===== Aspect: How tag command works + +* **Alternative 1 (current choice):** Search through the address book's list of persons to find all persons with any matching tag. +** Pros: Consistent with `find` command, easy to implement. +** Cons: Performance can be slow especially if InsuRen has many contacts as InsuRen will look through every person. +* **Alternative 2:** A hashmap is used with the key values being each unique tag and the values being a list of persons associated with each tag. +** Pros: Will have faster lookup, O(1) access time to get the list of persons associated with a tag. +** Cons: Will use more memory storing a separate data structure. This separate data structure also has to be updated with the right list of persons +every time a person's details are edited or a person is deleted. Programming such a data structure would require significantly more effort. + +// end::tagcommand[] // tag::dataencryption[] === [Proposed] Data Encryption -_{Explain here how the data encryption feature will be implemented}_ - +Due to the Singapore Personal Data Protection Act (PDPA), any disclosure of the user's personal information is considered to have severe implications. +Thus, all data that are being stored in `Storage` should be encrypted using a secure encryption scheme with a secret key. +When the user opens InsuRen, he should be prompted to login before he is able to access the secure data. // end::dataencryption[] +// tag::addpictureurl[] +=== [Proposed] Add Picture from URL + +The current implementation of the `pic` command in v1.4 only allows users to upload images that are available on their local drives. +Giving users the option to upload images that is available on the internet would be much more convenient to the user. +Users can simply go to their client's Facebook or other social media accounts to retrieve the image URL. + +// end::addpictureurl[] + === Logging We are using `java.util.logging` package for logging. The `LogsCenter` class is used to manage the logging levels and logging destinations. @@ -533,365 +942,433 @@ Here are the steps to create a new release. === Managing Dependencies -A project often depends on third-party libraries. For example, Address Book depends on the http://wiki.fasterxml.com/JacksonHome[Jackson library] for XML parsing. Managing these _dependencies_ can be automated using Gradle. For example, Gradle can download the dependencies automatically, which is better than these alternatives. + +A project often depends on third-party libraries. For example, InsuRen depends on the http://wiki.fasterxml.com/JacksonHome[Jackson library] for XML parsing. Managing these _dependencies_ can be automated using Gradle. For example, Gradle can download the dependencies automatically, which is better than these alternatives. + a. Include those libraries in the repo (this bloats the repo size) + b. Require developers to download those libraries manually (this creates extra work for developers) -[[GetStartedProgramming]] [appendix] -== Suggested Programming Tasks to Get Started +== Product Scope -Suggested path for new programmers: +*Target user profile*: -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 <>. +* Insurance Agents +* needs to manage many meetings with clients +* 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 -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. -[[GetStartedProgramming-EachComponent]] -=== Improving each component +*Value proposition*: +* Specific to Insurance Agents +* Manage contacts faster than a typical mouse/GUI driven app -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). +[appendix] +== User Stories -[discrete] -==== `Logic` component +Priorities: High (must have) - `* * \*`, Medium (nice to have) - `* \*`, Low (unlikely to have) - `*` -*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. +[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 -[TIP] -Do take a look at <> before attempting to modify the `Logic` component. +|`* * *` |Insurance Agent getting new customers |Add clients (including incomplete ones) |Be able to add clients who did not fill their forms completely -. 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. -**** +|`* * *` |Insurance Agent |delete a client's details |remove clients that I no longer need -[discrete] -==== `Model` component +|`* * *` |Insurance Agent |find a client by name |locate details of clients without having to go through the entire list -*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. +|`* *` |Insurance Agent |hide <> by default |minimize chance of someone else seeing them by accident -[TIP] -Do take a look at <> before attempting to modify the `Model` component. - -. 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. -**** +|`*` |Insurance Agent with many clients |sort clients by name |locate a client easily -[discrete] -==== `Ui` component +|`* * *` |Insurance Agent |Maintain updated contacts to my clients |Maintain my network -*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. +|`* *` |Insurance Agent with many meetings |See when my meetings with clients are |Set aside time to meet them -[TIP] -Do take a look at <> before attempting to modify the `UI` component. +|`* *` |Insurance Agent, concerned about customer’s plan being cancelled |Be notified when customer's deadlines for payments are near |Notify my clients of impending payments on time -. 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. -**** +|`* *` |Experienced Insurance Agent |Mass import contact details (via excel) |Load my existing contacts without keying them manually -. 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. -**** +|`* *` |Insurance Agent |Export email addresses |Email the contacts -. 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. -**** +|`* * *` |Insurance Agent who needs to maintain contact |Add a picture for my contacts |To identify them by picture and name -[discrete] -==== `Storage` component +|`* *` |Insurance Agent who needs to maintain contact |Display frequently contacted people |Contact them fast -*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. +|`* *` |Insurance Agent |Remove accidental duplicates |Keep my contact book neat -[TIP] -Do take a look at <> before attempting to modify the `Storage` component. +|`* *` |Insurance Agent who has different networks |View tagged contacts |Quickly view related contacts -. 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/XmlAddressBookStorage.java[`XmlAddressBookStorage`] class. -* Solution -** See this https://github.com/se-edu/addressbook-level4/pull/594[PR] for the full solution. -**** +|`* *` |Insurance Agent who has tagged contacts |Edit tags |Keep my contact book updated -[[GetStartedProgramming-RemarkCommand]] -=== Creating a new command: `remark` +|`* *` |Insurance Agent who has tagged contacts |Delete tags |Keep my contact book updated -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. +|======================================================================= -*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. -==== Description -Edits the remark for a person specified in the `INDEX`. + -Format: `remark INDEX r/[REMARK]` +[appendix] +== Use Cases -Examples: +(For all use cases below, the *System* is the `InsuRen` and the *Actor* is the `user`, unless specified otherwise) -* `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. +// tag::addUseCase[] +[discrete] +=== Use Case: Add Clients -==== Step-by-step Instructions +*MSS* -===== [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. +1. User requests to add client, specifying the compulsory field (name) and non-compulsory fields (address, email, phone number, and tags). +2. InsuRen stores the new client, and displays a confirmation message. ++ +Use case ends. -**Main:** +*Extensions* -. 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`. +[none] +* 1a. The user does not include the person’s name. ++ +[none] +** 1a1. InsuRen shows an error message. +Use case resumes at step 1. +// end::addUseCase[] -**Tests:** +[discrete] +=== Use case: Delete person -. 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`. +*MSS* -===== [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.` +1. User requests to list persons +2. InsuRen shows a list of persons +3. User requests to delete a specific person in the list +4. InsuRen deletes the person ++ +Use case ends. -**Main:** +*Extensions* -. 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`. +[none] +* 2a. The list is empty. ++ +Use case ends. -**Tests:** +* 3a. The given index is invalid. ++ +[none] +** 3a1. InsuRen shows an error message. ++ +Use case resumes at step 2. -. 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. +[discrete] +=== Use Case: See Meeting Timings with Clients -===== [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. +*MSS* -**Main:** +1. User inputs the customers’ meeting times in their address book entries. +2. InsuRen stores the meeting times, and displays them in the person card of the client. +3. User searches for meetings with clients by time, and InsuRen returns the client details if there is a meeting scheduled at that time. ++ +Use case ends. -. 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. +*Extensions* -**Tests:** +[none] +* 2a. There are no meetings scheduled for the time searched. ++ +[none] +** 2a1. InsuRen returns the next meeting after the specified time. ++ +Use case ends. -. Modify link:{repoURL}/src/test/java/guitests/guihandles/PersonCardHandle.java[`PersonCardHandle`] so that future tests can read the contents of the remark label. +* 2b. There are no meetings scheduled for any time after the searched time. ++ +[none] +** 2b1. InsuRen states that there are no meetings scheduled ++ +Use case ends. -===== [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. +[discrete] +=== Use Case: Add meeting field to an entry -**Main:** +*MSS* -. 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`. +1. User adds meeting time with specific client. +2. InsuRen will add meeting field to specified contact. Meeting will be displayed the next time user executes ‘list’. ++ +Use case ends. -**Tests:** +*Extensions* -. Add test for `Remark`, to test the `Remark#equals()` method. +[none] +* 1a. User inputs invalid contact/meeting time. ++ +[none] +** 1a1. InsuRen prints error message, prompting user to re-enter a valid ‘schedule’ command. ++ +Use case resumes at step 1. -===== [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`]. +[discrete] +=== Use Case: Be notified of expiring plans -**Main:** +*MSS* -. 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 `addressBook.xml` so that the application will load the sample data when you launch it.) +1. User inputs an expiry date field for each client’s insurance plan. +2. InsuRen alerts the user of clients with expiring insurance plans every time it is initialized. ++ +Use case ends. -===== [Step 6] Storage: Add `Remark` field to `XmlAdaptedPerson` 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/XmlAdaptedPerson.java[`XmlAdaptedPerson`] to include a `Remark` field so that it will be saved. +*Extensions* -**Main:** +* 2a. There are no plans expiring soon. ++ +[none] +** 2a1. InsuRen notifies the user that there are no imminent expiries. ++ +Use case ends. -. Add a new Xml field for `Remark`. +[discrete] -**Tests:** +// tag::import[] +=== Use Case: Mass import contacts into InsuRen -. Fix `invalidAndValidPersonAddressBook.xml`, `typicalPersonsAddressBook.xml`, `validAddressBook.xml` etc., such that the XML tests will not fail due to a missing `` element. +*MSS* -===== [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`]. +1. User requests to add import contacts from a file in a user-given directory. +2. InsuRen loads new contacts from import file, appending the new contacts to the end of the existing contact list. ++ +Use case ends. -**Tests:** +*Extensions* -. 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`]. +* 1a. File does not exist at directory path or invalid file type (must be .csv or .txt) ++ +[none] +** 1a1. InsuRen shows an error message. ++ +Use case resumes at step 1. +// end::import[] -===== [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. +[discrete] +// tag::export[] +=== Use Case: Export contact list from InsuRen -**Main:** +*MSS* -. Modify link:{repoURL}/src/main/java/seedu/address/ui/PersonCard.java[`PersonCard`]'s constructor to bind the `Remark` field to the `Person` 's remark. +1. User requests to export current state of InsuRen to a csv file whose name is given by the user. +2. InsuRen compiles all contacts into a csv (with the given name), saves it in the root project/application directory. ++ +Use case ends. -**Tests:** +*Extensions* -. Modify link:{repoURL}/src/test/java/seedu/address/ui/testutil/GuiTestAssert.java[`GuiTestAssert#assertCardDisplaysPerson(...)`] so that it will compare the now-functioning remark label. +* 1a. No file name given, or given file name does not contain .csv suffix ++ +[none] +** 1a1. InsuRen shows an error message. ++ +Use case resumes at step 1. +// end::export[] -===== [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. +// tag::editUseCase[] +[discrete] +=== Use Case: Edit Clients by Name -**Main:** +*MSS* -. Replace the logic in `RemarkCommand#execute()` (that currently just throws an `Exception`), with the actual logic to modify the remarks of a person. +1. User requests to edit client, specifying the name of the client and any fields to be modified. +2. InsuRen edits the client’s respective fields, and displays a confirmation message. ++ +Use case ends. -**Tests:** +*Extensions* -. Update `RemarkCommandTest` to test that the `execute()` logic works. +* 1a. The user does not include the person’s name. ++ +[none] +** 1a1. InsuRen shows an error message. ++ +Use case resumes at step 1. -==== Full Solution +* 1b. The user does not include any field to edit. ++ +[none] +** 1b1. InsuRen shows an error message. ++ +Use case resumes at step 1. -See this https://github.com/se-edu/addressbook-level4/pull/599[PR] for the step-by-step solution. +* 1c. There are multiple clients with the same name. ++ +[none] +** 1c1. InsuRen shows an error message, prompting the user to either use a more specific name, or edit by index. ++ +Use case resumes at step 1. +// end::editUseCase[] -[appendix] -== Product Scope +// tag::deleteUseCase[] +[discrete] +=== Use Case: Delete Clients by Name -*Target user profile*: +*MSS* -* 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 +1. User requests to delete a client, specifying the name of the client. +2. InsuRen deletes the specified client from storage, and displays a confirmation message. ++ +Use case ends. -*Value proposition*: manage contacts faster than a typical mouse/GUI driven app +*Extensions* -[appendix] -== User Stories +* 1a. The user does not include the person’s name. ++ +[none] +** 1a1. InsuRen shows an error message. ++ +Use case resumes at step 1. -Priorities: High (must have) - `* * \*`, Medium (nice to have) - `* \*`, Low (unlikely to have) - `*` +* 1b. There are multiple clients with the same name. ++ +[none] +** 1b1. InsuRen shows an error message, prompting the user to either use a more specific name, or delete by index. ++ +Use case resumes at step 1. +// end::deleteUseCase[] -[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 +// tag::picUseCase[] +[discrete] +=== Use Case: Upload Picture of Client -|`* * *` |user |add a new person | +*MSS* + +1. User requests upload picture of client. +2. InsuRen requests for the client’s ID. +3. User specifies the client’s ID. +4. InsuRen requests for the file location. +5. User specifies the file location. +6. InsuRen uploads the file and tags it to the client’s profile. ++ +Use case ends. -|`* * *` |user |delete a person |remove entries that I no longer need +*Extensions* -|`* * *` |user |find a person by name |locate details of persons without having to go through the entire list +* 3a. InsuRen detects an error in the entered data. ++ +[none] +** 3a1. InsuRen requests for the correct data. ++ +Use case resumes from step 3. +* 5a. InsuRen detects an error in the entered data. ++ +[none] +** 5a1. InsuRen requests for the correct data. ++ +Use case resumes from step 5. +// end::picUseCase[] -|`* *` |user |hide <> by default |minimize chance of someone else seeing them by accident +// tag::tagUseCase[] +[discrete] +=== Use Case: View tagged contacts -|`*` |user with many persons in the address book |sort persons by name |locate a person easily -|======================================================================= +*MSS* -_{More to be added}_ +1. User requests all contacts with any number of user-specified tags. +2. InsuRen lists all contacts that contain any one of the user-specified tags. ++ +Use case ends. -[appendix] -== Use Cases +*Extensions* -(For all use cases below, the *System* is the `AddressBook` and the *Actor* is the `user`, unless specified otherwise) +* 1a. User enters tags that are not present in any contacts in Insuren. ++ +[none] +** 1a1. InsuRen shows an empty contact list. ++ +Use case resumes at step 1. [discrete] -=== Use case: Delete person +=== Use Case: Edit tags *MSS* -1. User requests to list persons -2. AddressBook shows a list of persons -3. User requests to delete a specific person in the list -4. AddressBook deletes the person +1. User requests to edit a tag, specifying an existing tag and a new tag name. +2. InsuRen updates all contacts with the existing tag, changing the tag name to the new user-specified tag name. +3. InsuRen lists all contacts whose tags have been updated. + Use case ends. *Extensions* +* 1a. User enters tags that are not present in any contacts in Insuren. ++ [none] -* 2a. The list is empty. +** 1a1. InsuRen shows an empty contact list, stating that 0 contacts have their tags changed. ++ +Use case resumes at step 1. + +* 1b. User does not enter any tag to edit. ++ +[none] +** 1b1. InsuRen shows an error message. ++ +Use case resumes at step 1. + +* 1c. User does not enter a new tag name. ++ +[none] +** 1c1. InsuRen shows an error message. ++ +Use case resumes at step 1. + +[discrete] +=== Use Case: Delete tags + +*MSS* + +1. User requests to delete a tag, specifying any number of tags he or she wants to delete. +2. InsuRen finds all instances of any of the user-specified tags and deletes them from each contact. +3. InsuRen lists all contacts whose tags have been deleted. + Use case ends. -* 3a. The given index is invalid. +*Extensions* + +* 1a. User enters tags that are not present in any contacts in Insuren. + [none] -** 3a1. AddressBook shows an error message. +** 1a1. InsuRen shows an empty contact list, stating that 0 contacts have their tags deleted. + -Use case resumes at step 2. +Use case resumes at step 1. -_{More to be added}_ +* 1b. User does not enter any tag to delete. ++ +[none] +** 1b1. InsuRen shows an empty contact list, stating that 0 contacts have their tags deleted. ++ +Use case resumes at step 1. +// end::tagUseCase[] +// tag::nfr[] [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 persons without a noticeable sluggishness in performance for typical usage. +. InsuRen should work on any <> as long as it has Java `9` or higher installed. +. InsuRen should be able to hold up to 1000 clients' contact without a noticeable sluggishness in performance for typical usage. +. InsuRen should process a user command in 1 second or less, without any noticeable delay. +. InsuRen should display a clear and concise error message to provide feedback to the user when an invalid input is received. +. InsuRen should be backward compatible with data produced by earlier versions of Insuren. +. InsuRen should be open-source. +. InsuRen is offered as a free product. +. All data entries are backed-up regularly. +. All data entries are stored in a xml file. +. A user should be able to learn and use the product without any form of training. . A user with above average typing speed for regular English text (i.e. not code, not system admin commands) should be able to accomplish most of the tasks faster using commands than using the mouse. - -_{More to be added}_ +. The UI should be responsive to changes. +. The product should be self-explanatory and intuitive such that an insurance agent is able to adapt to it within the first 10 minutes of using the product for the first time. +. When the program crashes, all data up till the point of crash will still be available upon relaunch of the program. +. The system should work by running on the JAR file without any installation. +. The system should work even if the user does not have any internet connection. +. The JAR file should be small in size (< 50 MB). +// end::nfr[] [appendix] == Glossary @@ -902,23 +1379,6 @@ Windows, Linux, Unix, OS-X [[private-contact-detail]] Private contact detail:: A contact detail that is not meant to be shared with others -[appendix] -== Product Survey - -*Product Name* - -Author: ... - -Pros: - -* ... -* ... - -Cons: - -* ... -* ... - [appendix] == Instructions for Manual Testing @@ -933,7 +1393,7 @@ These instructions only provide a starting point for testers to work on; testers .. 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. + Expected: Shows the GUI with a set of sample clients. The window size may not be optimum. . Saving window preferences @@ -941,26 +1401,258 @@ These instructions only provide a starting point for testers to work on; testers .. Re-launch the app by double-clicking the jar file. + Expected: The most recent window size and location is retained. -_{ more test cases ... }_ - +// tag::testingAdd[] +=== Adding a person + +. Adding a person to InsuRen + +.. Prerequisites: No `Person` in the list is identifiable as identical to those that are to be added during this test. +... Identifiable as identical means that: +.... `name` is the same *and* +.... `phone` *or* `email` is the same +.. Test case: `add n/Anne Loh p/11114444 e/abc@email.com a/44th Street t/Friend` + + Expected: Anne Loh is added. Details of the client are shown in the status message. Timestamp in the status bar is updated. +.. Test case: `add n/Ben Chua t/Friend` + + Expected: Ben Chua is added. Details of the client are shown in the status message. Timestamp in the status bar is updated. +.. Test case: `add n/Anne Loh p/22223333 e/def@email.com` + + Expected: Anne Loh is added. Details of the client are shown in the status message. Timestamp in the status bar is updated. +.. Test case: `add n/Anne Loh p/22223333` + + Expected: Nothing is updated. Error details shown in the status message. Status bar remains the same. +.. Test case: `add n/Anne Loh p/22223333 a/abc street` + + Expected: Nothing is updated. Error details shown in the status message. Status bar remains the same. +.. Test case: `add n/Anne Loh e/abc@email.com a/abc street` + + Expected: Nothing is updated. Error details shown in the status message. Status bar remains the same. +.. Test case: `add n/Anne Loh` + + Expected: Anne Loh is added. Details of the client are shown in the status message. Timestamp in the status bar is updated. +// end::testingAdd[] + +// tag::testingEdit[] +=== Editing a person + +. Editing a person + +.. Editing by Index +... Prerequisites: List all persons using the `list` command. Multiple persons in the list. +... Test case: `edit 1 n/Abcde t/` + + Expected: First client is renamed "Abcde" and tags are deleted. Details of the edited client shown in the status message. Timestamp in the status bar is updated. +... Test case: `edit 0 p/18854835 t/friend t/jailed` + + Expected: No person is edited. Error details shown in the status message. Status bar remains the same. +... Other incorrect edit commands to try: `edit` (no arguments), `edit x t/friend` (where x is larger than the list size or negative), `edit 3` (where no fields are provided) + + Expected: Similar to previous. +.. Editing by Name +... Prerequisites: Make sure that nobody in InsuRen has the name/part of their name as Alice, Lee, Lim, Chua or Bob, then add people to the list with the names `Alice Lee`, `Alice Chua`, `Alice Lim` and `Bob`. +... Test case: `edit Bob e/abc@email.com` + Expected: Bob's email is changed to abc@email.com. Details of the edited client shown in the status message. Timestamp in the status bar is updated. +... Test case: `edit Alice Chua n/Bobby Chua` + Expected: Alice Chua is renamed to Bobby Chua. Details of the edited client shown in the status message. Timestamp in the status bar is updated. +... Test case: `edit Alice p/883838333` + + Expected: No person is edited. Error details shown in the status message. Status bar remains the same. +... Other incorrect edit commands to try: `edit x n/abc` (where x matches nobody in the list), `edit Alice Lim` (where no fields are provided) + + Expected: Similar to previous. +// end::testingEdit[] + +// tag::testingDelete[] === Deleting a person -. Deleting a person while all persons are listed +. Deleting a person + +.. Deleting by Index +... Prerequisites: List all persons using the `list` command. Multiple persons in the list. +... Test case: `delete 1` + + Expected: First client is deleted from the list. Details of the deleted client 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 or negative) + + Expected: Similar to previous. +.. Deleting by Name +... Prerequisites: Make sure that nobody in InsuRen has the name/part of their name as Alice, Lee, Lim, Chua or Bob, then add people to the list with the names `Alice Lee`, `Alice Chua`, `Alice Lim` and `Bob`. +... Test case: `delete Bob` + Expected: Bob is deleted. Details of the deleted client shown in the status message. Timestamp in the status bar is updated. +... Test case: `delete Alice Chua` + Expected: Alice Chua is deleted. Details of the deleted client shown in the status message. Timestamp in the status bar is updated. +... Test case: `delete Alice` + + Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. +... Other incorrect delete commands to try: `delete x` (where x matches nobody in the list) + + Expected: Similar to previous. +// end::testingDelete[] -.. 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. +=== Saving data -_{ more test cases ... }_ +. Dealing with missing image files -=== Saving data +.. Prerequisites: A person has an updated picture aside from the default picture. +.. Assumption: `/Users/John/Downloads/Insuren/images/petertan.jpg` is the client's picture. +.. Test case: rename, delete, or move the image file `/Users/John/Downloads/Insuren/images/petertan.jpg` such that there is no `petertan.jpg` file in the `images` folder. + + Expected: The default placeholder picture will be used. + +// tag::manualTestingImport[] +=== Importing contacts + +. Importing a fresh list of contacts into an empty InsuRen. + +.. Prerequisites: clear all persons using the `clear` command. No persons in the list. +Also ensure that there is a populated csv file within the application's ROOT directory. An example of an acceptable csv is shown in the user guide. Let's call this file asdf.csv +.. Test case: `import l/asdf.csv` + + Expected: InsuRen will be populated with the contacts in asdf.csv. -. Dealing with missing/corrupted data files +. Importing a contact into InsuRen when such contacts are already in InsuRen. -.. _{explain how to simulate a missing/corrupted file and the expected behavior}_ +.. Prerequisites: Take the starting point of this test to be the end of the previous - ie. after you have +successfully imported contacts from asdf.csv. +.. Test case: `import` + + Expected: A file browser will pop up. Navigate to and select asdf.csv one more time. Error messages should be + displayed, stating that no contacts have been imported as InsuRen has found duplicate contacts. -_{ more test cases ... }_ +. Importing invalid contacts into InsuRen. + +.. Prerequisites: Clear InsuRen like in (1) above, then deliberately corrupt some of the entries in asdf.csv by removing names or giving invalid phone numbers, meetings etc. +.. Test case: `import l/asdf.csv` + + Expected: Valid contacts will be imported into InsuRen. Additionally, an error will be displayed, stating that InsuRen has found invalid contacts. +// end::manualTestingImport[] + +// tag::manualTestingExport[] +=== Exporting contacts + +. Exporting InsuRen's current contacts into a new csv file. + +.. Prerequisites: Ensure that InsuRen has at least 1 contact. +.. Test case: `export contacts.csv` + + Expected: A new contacts.csv file will appear in the ROOT directory, populated with InsuRen's current contacts. + +. Exporting into an invalid file. + +.. Prerequisites: Ensure that InsuRen has at least 1 contact. +.. Test case: `export asdf` + + Expected: Error message will be thrown by InsuRen, stating that an incorrect export file name has been provided. +// end::manualTestingExport[] + +// tag::testingpicture[] +=== Adding picture + +. Adding a picture to a person while all persons are listed + +.. Prerequisites: List all persons using the `list` command. Multiple persons in the list. `/Users/John/Downloads/Insuren/images/petertan.jpg` is a valid file path. +.. Test case: `pic 1 l//Users/John/Downloads/Insuren/images/petertan.jpg` + + Expected: First client's picture is updated. Details of the updated client shown in the status message. Timestamp in the status bar is updated. +.. Test case: `pic 0 l//Users/John/Downloads/Insuren/images/petertan.jpg` + + Expected: Nothing is updated. Error details shown in the status message. Status bar remains the same. +.. Test case: `pic 1 l//Users/John/Downloads/Insuren/images/invalid_image_path.jpg` + + Expected: Nothing is updated. Error details shown in the status message. Status bar remains the same. +.. Test case: `pic 1 l//Users/John/Downloads/Insuren/images/invalid_image_type.mp3` + + Expected: Nothing is updated. Error details shown in the status message. Status bar remains the same. +.. Other incorrect picture commands to try: `pic`, `pic x` (where x is larger than the list size) + + Expected: Similar to previous. +// end::testingpicture[] + +// tag::testingTag[] +=== Viewing tags + +. Viewing all user-specified tags + +.. Prerequisites: InsuRen should have these contacts initially: +* `n/Anne Loh t/Friend` +* `n/Ben Chua t/Friend` +* `n/Charlie Dong t/Friend t/Buddy` +* `n/David Ee t/Buddy` +* `n/Euler Foo t/Buddy t/Close` +* `n/Fiona Goh` + +.. Test case: `tag Friend` + + Expected: `Anne Loh`, `Ben Chua` and `Charlie Dong` contacts are displayed. +.. Test case: `tag Friend Buddy` + + Expected: `Anne Loh`, `Ben Chua`, `Charlie Dong`, `David Ee` and `Euler Foo` contacts are displayed. +.. Test case: `tag Friend Close` + + Expected: `Anne Loh`, `Ben Chua`, `Charlie Dong` and `Euler Foo` contacts are displayed. +.. Test case: `tag friend Close` + + Expected: `Euler Foo` contact is displayed. +.. Test case: `tag Friend buddy close` + + Expected: `Anne Loh`, `Ben Chua` and `Charlie Dong` contacts are displayed. +.. Test case: `tag friend buddy close` + + Expected: No contacts are displayed. +.. Test case: `tag` + + Expected: Error details shown in the status message. + +=== Editing tags + +. Editing a user-specified tag + +.. Prerequisites: InsuRen should have these contacts initially: +* `n/Anne Loh t/Friend` +* `n/Ben Chua t/Friend` +* `n/Charlie Dong t/Friend t/Buddy` +* `n/David Ee t/Buddy` +* `n/Euler Foo t/Buddy t/Close` +* `n/Fiona Goh` + +.. Test case: `tag edit Friend friend` + + Expected: `Anne Loh`, `Ben Chua` and `Charlie Dong` contacts are displayed. Their tags are updated to `friend`. +.. Test case: `tag edit Close bestie` + + Expected: `Euler Foo` contact is displayed. His tags are now `t/Buddy t/bestie`. +.. Test case: `tag edit test testing` + + Expected: No contacts are displayed, no tags are edited. +.. Test case: `tag edit test` + + Expected: Nothing is updated. Error details shown in the status message. Status bar remains the same. +.. Test case: `tag edit` + + Expected: Nothing is updated. Error details shown in the status message. Status bar remains the same. + +=== Deleting tags + +. Deleting all user-specified tags + +.. Prerequisites: InsuRen should have these contacts initially: +* `n/Anne Loh t/Friend` +* `n/Ben Chua t/Friend` +* `n/Charlie Dong t/Friend t/Buddy` +* `n/David Ee t/Buddy` +* `n/Euler Foo t/Buddy t/Close t/Family` +* `n/Fiona Goh t/Family` +* `n/George Ho t/Family t/Dad` + +.. Test case: `tag delete friend` + + Expected: Nothing is updated. Error details shown in the status message. Status bar remains the same. +.. Test case: `tag delete friend buddy close` + + Expected: Nothing is updated. Error details shown in the status message. Status bar remains the same. +.. Test case: `tag delete Close` + + Expected: `Euler Foo` is displayed. His tags are updated to `t/Buddy`. `Close` tag is deleted. +.. Test case: `tag delete Friend` + + Expected: `Anne Loh`, `Ben Chua` and `Charlie Dong` contacts are displayed. They no longer have the `Friend` tag. +.. Test case: `tag delete Buddy Family` + + Expected: `Charlie Dong`, `David Ee`, `Euler Foo`, `Fiona Goh` and `George Ho` contacts are displayed. + They all do no have the `Buddy` or `Family` tags. +.. Test case: `tag delete` = + Expected: Nothing is updated. Error details shown in the status message. Status bar remains the same. +// end::testingTag[] +======= +// tag::testingschedule[] +=== Adding a Meeting + +. Adding a meeting to a person while all persons all listed. + +.. Prerequisites: List all persons using the `list` command. Multiple persons in the list. +.. Test case: `schedule 1 m/21/02/18 1430` + + Expected: First client is scheduled for a meeting on 21st February 2018, at 1430. Timestamp in the status bar is updated. +.. Test case: `schedule 0 m/21/02/18 1430` + + Expected: Nothing is updated. Error details shown in the status message. Status bar remains the same. +.. Test case: `schedule 1 m/31/02/18 1430` + + Expected: Nothing is updated. Error details shown in the status message. Status bar remains the same. +.. Test case: `schedule 1 m/21/02/18 2630` + + Expected: Nothing is updated. Error details shown in the status message. Status bar remains the same. +// end::testingschedule[] + +// tag::testingmeetings[] +=== Searching by Meetings + +. Searching for people by meetings. + +.. Prerequisites: Two persons in the list. One must have a meeting scheduled on 21st February 2018, and the other must have a meeting scheduled on 23rd February 2018. +.. Test case: `meetings 21/02/18` + + Expected: The client with the meeting on 21st February 2018 is listed. +.. Test case: `meetings 23/02/18` + + Expected: The client with the meeting on 23rd February 2018 is listed. +.. Test case: `meetings` + + Expected: Both clients are listed. +.. Test case: `meetings 24/02/18` + + Expected: No clients are listed. +.. Test case: `meetings 31/02/18` + + Expected: Nothing is updated. Error details shown in the status message. Status bar remains the same. +// end::testingmeetings[] diff --git a/docs/DummySearchPage.html b/docs/DummySearchPage.html deleted file mode 100644 index 1607d4c57291..000000000000 --- a/docs/DummySearchPage.html +++ /dev/null @@ -1,32 +0,0 @@ - - - - - Dummy Search Page - - - - Hi : This is a placeholder page for se-edu/addressbook-level4.
- You may update the code to load a page from a real service (e.g., Google search).
- This dummy page is used here because, given the high number of forks of this repo, loading a page from a real third-party service by default can result in that service taking counter-measures (e.g., redirecting to captcha pages) due to the high number of rapid requests received from a single IP.
- When you have made the change, please remove: -
    -
  1. This file (docs/DummySearchPage.html).
  2. -
  3. Task copyDummySearchPage in both build.gradle and .travis.yml.
  4. -
- - diff --git a/docs/UserGuide.adoc b/docs/UserGuide.adoc index 7e0070e12f49..6e52a0d3a508 100644 --- a/docs/UserGuide.adoc +++ b/docs/UserGuide.adoc @@ -1,4 +1,4 @@ -= AddressBook Level 4 - User Guide += InsuRen - User Guide :site-section: UserGuide :toc: :toc-title: @@ -12,19 +12,19 @@ ifdef::env-github[] :tip-caption: :bulb: :note-caption: :information_source: endif::[] -:repoURL: https://github.com/se-edu/addressbook-level4 +:repoURL: https://github.com/CS2103-AY1819S1-W13-1/main -By: `Team SE-EDU` Since: `Jun 2016` Licence: `MIT` +By: `Team W13-1` Since: `Sep 2018` 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! +InsuRen is for insurance salesmen who *prefer to use a desktop app for managing contacts*. It can help to *manage one’s network of clients in an organized, efficient and intuitive manner.* More importantly, InsuRen 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, InsuRen can get your contact management tasks done faster than traditional GUI apps. Interested? Jump to the <> to get started. Enjoy! == 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. +. Download the latest `insuren.jar` link:{repoURL}/releases[here]. +. Copy the file to the folder you want to use as the home folder for InsuRen. . Double-click the file to start the app. The GUI should appear in a few seconds. + image::Ui.png[width="790"] @@ -34,7 +34,7 @@ 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. +* **`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 InsuRen. * **`delete`**`3` : deletes the 3rd contact shown in the current list * *`exit`* : exits the app @@ -50,37 +50,60 @@ e.g. typing *`help`* and pressing kbd:[Enter] will open the help window. * 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. +* All commands have a shorthand version for easy access. Simply replace the command word with the shorthand. All other syntax is identical. ==== === Viewing help : `help` Format: `help` +Shorthand: `h` + === Adding a person: `add` -Adds a person to the address book + -Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]...` +Adds a person to InsuRen. + +Format: `add n/NAME [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG]...` +[NOTE] +We recommend against (but do not forbid) using `edit` or `delete` (case-insensitive) as tags, as you may run into complications when using the `tag edit` and `tag delete` functionality. + +Shorthand: `a` [TIP] -A person can have any number of tags (including 0) +Only the Name field is compulsory, all other fields need not be included. +A person can have one name, phone, email and address, and any number of tags (including 0) Examples: -* `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` -* `add n/Betsy Crowe t/friend e/betsycrowe@example.com a/Newgate Prison p/1234567 t/criminal` +* `add n/John Doe p/98765432 e/johnd@example.com a/John street block 123 #01-01` + +Adds a `Person` with name John Doe, phone number 98765432, email johnd@example.com, and address John street block 123 #01-01, to InsuRen, provided that he does not already exist in InsuRen. +* `a n/Betsy Crowe t/friend e/betsycrowe@example.com a/Newgate Prison p/1234567 t/criminal` + +Adds a `Person` with name Betsy Crowe, phone number 1234567, email betsycrowe@example.com, address Newgate Prison, and tags friend and criminal to InsuRen, provided that she does not already exist in InsuRen. +* `add n/Abigail` + +Adds a `Person` with name Abigail to InsuRen. === Listing all persons : `list` -Shows a list of all persons in the address book. + +Shows a list of all persons in InsuRen. + Format: `list` +Shorthand: `l` + +// tag::editByName[] === Editing a person : `edit` -Edits an existing person in the address book. + -Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]...` +Edits an existing person in InsuRen by the displayed list's index or by an existing name. + +Format: `edit INDEX/EXISTING_NAME [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]...` +[NOTE] +We recommend against (but do not forbid) using `edit` or `delete` (case-insensitive) as tags, as you may run into complications when using the `tag edit` and `tag delete` functionality. +Shorthand: `e` **** -* 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, ... +* You can edit a person by *index* or by *name*. +** For editing 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, ... +** For editing the person with a name matching the EXISTING_NAME, the name *​must uniquely identify a person.* +*** If nobody matches the EXISTING_NAME, or there are multiple contacts matching it, InsuRen will notify you and not carry out changes. +*** You can be less specific or more specific in the existing name to identify a person, but in the case that two people have exactly the same name, you have to use the edit by index command. * 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. @@ -91,14 +114,21 @@ Examples: * `edit 1 p/91234567 e/johndoe@example.com` + Edits the phone number and email address of the 1st person to be `91234567` and `johndoe@example.com` respectively. -* `edit 2 n/Betsy Crower t/` + +* `e 2 n/Betsy Crower t/` + Edits the name of the 2nd person to be `Betsy Crower` and clears all existing tags. +* `edit John Doe p/91234567 e/johndoe@example.com` + +Edits the phone number and email address of John Doe to be ​91234567​ and johndoe@example.com​ respectively, if John Doe can be uniquely identified by name in InsuRen. +* `e Johnny n/Betsy Crower` + +Edits the name of Johnny to be `Betsy Crower` if Johnny can be uniquely identified by name in InsuRen. +// end::editByName[] === Locating persons by name: `find` Finds persons whose names contain any of the given keywords. + Format: `find KEYWORD [MORE_KEYWORDS]` +Shorthand: `f` + **** * 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` @@ -114,31 +144,44 @@ Returns `john` and `John Doe` * `find Betsy Tim John` + Returns any person having names `Betsy`, `Tim`, or `John` +// tag::deleteByName[] === Deleting a person : `delete` -Deletes the specified person from the address book. + -Format: `delete INDEX` +Deletes the specified person from InsuRen by the displayed list's index or by an existing name. + +Format: `delete INDEX/EXISTING_NAME` +Shorthand: `d` **** -* 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, ... +* Deletes the person by *index* or by *name*. +** For deleting by `INDEX`, +*** The index refers to the index number shown in the displayed person list. +*** The index *must be a positive integer* 1, 2, 3, ... +** For deleting by `EXISTING_NAME`, +*** The name *​must uniquely identify a person.* +*** If nobody matches the EXISTING_NAME, or there are multiple contacts matching it, InsuRen will notify you and not carry out changes. +*** You can be less specific or more specific in the existing name to identify a person, but in the case that two people have exactly the same name, you have to delete by `INDEX`. **** Examples: * `list` + `delete 2` + -Deletes the 2nd person in the address book. +Deletes the 2nd person in InsuRen. * `find Betsy` + -`delete 1` + +`d 1` + Deletes the 1st person in the results of the `find` command. +* `delete John` + +Deletes John if John can be uniquely identified by name in InsuRen. +* `d Abel Lee` + +Deletes Abel Lee if Abel Lee can be uniquely identified by name in InsuRen. +// end::deleteByName[] === Selecting a person : `select` Selects the person identified by the index number used in the displayed person list. + Format: `select INDEX` +Shorthand: `s` **** * 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. @@ -149,7 +192,7 @@ Examples: * `list` + `select 2` + -Selects the 2nd person in the address book. +Selects the 2nd person in InsuRen. * `find Betsy` + `select 1` + Selects the 1st person in the results of the `find` command. @@ -159,20 +202,24 @@ Selects the 1st person in the results of the `find` command. Lists all the commands that you have entered in reverse chronological order. + Format: `history` +Shorthand: `hs` [NOTE] ==== -Pressing the kbd:[↑] and kbd:[↓] arrows will display the previous and next input respectively in the command box. +Pressing the kbd:[↑] and kbd:[↓] arrows will display the previous and next input respectively in the command +box. ==== // tag::undoredo[] === Undoing previous command : `undo` -Restores the address book to the state before the previous _undoable_ command was executed. + +Restores InsuRen to the state before the previous _undoable_ command was executed. + Format: `undo` +Shorthand: `u` [NOTE] ==== -Undoable commands: those commands that modify the address book's content (`add`, `delete`, `edit` and `clear`). +Undoable commands: those commands that modify InsuRen's content (`add`, `delete`, `edit`, `import`, `schedule`, + `pic` and `clear`). ==== Examples: @@ -196,6 +243,8 @@ The `undo` command fails as there are no undoable commands executed previously. Reverses the most recent `undo` command. + Format: `redo` +Shorthand: `r` + Examples: * `delete 1` + @@ -216,39 +265,278 @@ The `redo` command fails as there are no `undo` commands executed previously. === Clearing all entries : `clear` -Clears all entries from the address book. + +Clears all entries from InsuRen. + Format: `clear` +Shorthand: `c` + === Exiting the program : `exit` Exits the program. + Format: `exit` +Shorthand: `q` + +// tag::import[] +=== Mass import contacts: `import` + +Import contacts from a csv file. +If no file path is given, a file browser will open for users to navigate to their desired file (.txt and .csv only) +If a file path is given, InsuRen will attempt to obtain and read the file specified by the given file path. +Format: `import` + +Shorthand: `i` +**** +* InsuRen will fetch the file from the given path. +* InsuRen will throw an error message if the file cannot be found from the given (typed) file path +* InsuRen will NOT throw an error message if the formatting of the file is incorrect. +* Improperly formatted contacts and/or duplicate contacts will be ignored. +**** + +Example (user does not provide a file path): + +* `import` +* A file browser will pop up as shown below: ++ +image::import.png[width="400"] + +Example (user provides a file path): + +* FOR WINDOWS: +** `import l/D:/AddressbookCorrect.csv` (absolute pathing) or +** `import l/AddressbookCorrect.csv` (relative pathing - if you save the .csv file in the same directory as the .jar file). +* FOR MAC: +** `import l//FILEPATH` (absolute pathing - note the double slashes) or +** `import l/AddressbookCorrect.csv` (relative pathing). +* Regardless which method is used, InsuRen will load contacts from the given csv file. +* Each contact's Name, Phone, Email, Address, Meeting and Tag(s) fields must be keyed in the csv in that order. +* All fields are optional, except for Name. Contacts with no Address, Phone etc. must have those fields left BLANK. Ie. the +corresponding Excel cell MUST have nothing in it. +* Any invalid entries (contacts with no name) will be ignored. +* Examples of properly formatted csv files are shown below. (Can be both csv or txt) + ++ +image::import-acceptable-csv3.png[width="600"] + +// end::import[] + +// tag::export[] +=== Export contact list: `export` + +Exports the current contact list into a csv file whose name is given as the second argument. The export file + can be found in the root directory of the project/application. + +Format: `export DESTINATION_FILE_NAME.csv` + +Shorthand: `x` + +**** +* InsuRen will copy all contacts and format them into a csv file, with each row representing a unique contact. +* InsuRen will throw an error message if given file name is invalid (has no .csv suffix). +**** + +Example: + +* `export 28Nov.csv` + +InsuRen contacts are exported to `28Nov.csv` ++ +image::exportedCsv.png[width="600"] +// end::export[] + +// tag::schedule[] +=== Add meeting time: `schedule` + +Add a meeting at the input date and time with a specified person. + +Format: `schedule INDEX m/DD/MM/YY HHMM` + +Shorthand: `sch` + +**** +* InsuRen will add the meeting to your list of meetings, as well as mark the person with the meeting. +* You may use any special characters such as '/' and '-', or whitespace to seperate the entries. As long as the relative order +of the numbers is DDMMYYHHMM, InsuRen will save it. +* Meetings can be scheduled on any valid date in the past (to keep a record) or in the future (as a reminder). +* If the date is invalid (i.e. it is not a real date), an error will be thrown. +* If there are any meetings scheduled at the same time, it will be flagged out. +**** + +Example: + +* `schedule 1 m/12/03/19 0930` + +InsuRen will record that a meeting is scheduled on 12 March 2019, 0930 with the first entry. +// end::schedule[] + +// tag::meetings[] +=== View meeting timings: `meetings` + +Displays the details of the meeting at the input date and time. + +Format: `meetings [DD/MM/YY]` + +shorthand: `m` +**** +* If there are meetings scheduled on the queried date, the details of the clients the meetings are scheduled with +are displayed. +* If the query has no date, then all meetings scheduled in InsuRen are displayed. +* If there is no meeting scheduled on the day, an empty list is displayed. +**** + +Example: + +* `meetings 23/02/18` + +InsuRen displays meetings scheduled on 23rd February, 2018. +// end::meetings[] + +// tag::notifs[] +=== Notifications for expiring plans `[coming in v2.0]` + +InsuRen entries have an optional field for date of plan expiry. You will automatically be notified of clients +with plans expiring within a month from the day when InsuRen is initialized. + +No additional search queries are needed. +// end::notifs[] + +// tag::picture[] +=== Adding a picture to contact: `pic` + +Adds a picture to a person in InsuRen. + +Format: `​pic INDEX l/FILE_LOCATION​` + +Shorthand: `p` + +**** +* InsuRen will add the image to the contact. +* Only `.jpg` and `.png` files are accepted. +* Supports file location for any <>. +** Windows: `C:/Users/John/Downloads/InsuRen/images/petertan.jpg` +** Mac: `/Users/John/Downloads/Insuren/images/petertan.jpg` +* File location can be an absolute path (`/Users/John/Downloads/InsuRen/images/petertan.jpg`), or a relative path (`images/petertan.jpg`). +* If the picture gets deleted or renamed, InsuRen will show the default user picture. +**** + +Examples: + +* `pic 2 l/john.jpg` + +The second person in the list will now have image `john.jpg` in his contact. +* `pic 1 l//Users/John/Downloads/InsuRen/images/petertan.jpg` + +The first person in the list will now have image `petertan.jpg` from the given path in his contact. +* `p 3 l/C:/Users/John/Downloads/InsuRen/images/davidlee.png` + +The third person in the list will now have image `davidlee.png` from the given path in his contact. + +// end::picture[] + +// tag::tag[] +=== Using tags: `tag` + +==== View contacts by tag +View all contacts in any existing tag. + +Format: `tag TAG_NAME [MORE_TAG_NAMES]` + +Shorthand: `t` + +**** +* View all contacts that belong to the same tag. +**** + +Example: + +* `tag Work` + +Returns all contacts with the `Work` tag. +* `tag Work Important` + +Returns all contacts with the `Work` or `Important` tags. + +==== Edit a tag +Edit a tag, replacing all its occurrences with a new user-specified tag. + +Format: `tag edit EXISTING_TAG_NAME NEW_TAG_NAME` +[NOTE] +We recommend against (but do not forbid) using `edit` or `delete` (case-insensitive) as tags, as you may run into complications when using the `tag edit` and `tag delete` functionality. + +**** +* All contacts tagged with `EXISTING_TAG_NAME` will have the tag replaced by `NEW_TAG_NAME`. +**** + +Example: + +* `tag edit Work Business` + +All contacts that had the `Work` tag have the tag changed to `Business`. + +* `t EDIT Friends Buddies` + +Usage of "edit" is **not** case-sensitive. + +==== Delete a tag +Delete a tag, removing it from all contacts. + +Format: `tag TAG_NAME [MORE_TAG_NAMES...] delete` + +**** +* All contacts in `TAG_NAME` will be removed from the tag. Contacts that were previously tagged are not deleted. +**** + +Example: + +* `tag Work delete` + +All contacts that were previously tagged with `Work` have the `Work` tag removed. `Work` tag is deleted. + +* `t Work Important delete` + +All contacts that were previously tagged with `Work` and `Important` have the aforementioned tags removed. `Work` and +`Important` tags are deleted. +// end::tag[] + === Saving the data -Address book data are saved in the hard disk automatically after any command that changes the data. + +InsuRen data are saved in the hard disk automatically after any command that changes the data. + There is no need to save manually. // tag::dataencryption[] === Encrypting data files `[coming in v2.0]` +Encrypt all data in InsuRen behind a password. + +Format: `encrypt PASSWORD` + +**** +* The next time a user opens InsuRen, he will have to enter a password before the contact list populates. +**** + +Example: -_{explain how the user can enable/disable data encryption}_ +* `encrypt Pa$$w0rd` + +All data will be encrypted. The next time the user opens InsuRen, InsuRen will prompt her for a password. // end::dataencryption[] +// tag::picture-url[] +=== Adding a picture from URL to contact `[coming in v2.0]` + +Adds a picture from a URL to a person in InsuRen. + +Format: `​pic INDEX l/FILE_URL​` + +Shorthand: `p` + +**** +* InsuRen will add the image to the contact. +* If the URL is broken after it is successfully added, InsuRen will show a picture indicating that the URL is broken. +**** + +Examples: + +* `pic 1 l/https://cs2103-ay1819s1-w13-1.github.io/main/images/denzelchung.png` + +The first person in the list will now have image `denzelchung.png` from the `Github` webpage in his contact. + +// end::picture-url[] + == FAQ *Q*: How do I transfer my data to another Computer? + -*A*: Install the app in the other computer and overwrite the empty data file it creates with the file that contains the data of your previous Address Book folder. +*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 InsuRen folder. + +Alternatively, use the export command to get your current contact list and save it in portable storage (email, thumbdrive). Install InsuRen on the other computer +and import the abovementioned csv file into your new computer's copy of InsuRen. == Command Summary * *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` +* *Delete* : `delete INDEX/EXISTING_NAME` + +e.g. `delete 3` + +or `delete John Doe Kah Wai` +* *Edit* : `edit INDEX/EXISTING_NAME [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG]...` + +e.g. `edit 2 n/James Lee e/jameslee@example.com` + +or `edit John Doe n/John Chan a/525 East 80th Street, New York` * *Find* : `find KEYWORD [MORE_KEYWORDS]` + e.g. `find James Jake` * *List* : `list` @@ -258,3 +546,10 @@ e.g.`select 2` * *History* : `history` * *Undo* : `undo` * *Redo* : `redo` +* *Import contacts* : `import` or `import l/FILE_PATH` +* *Export contacts* : `export DESTINATION_DIRECTORY` +* *Schedule* : `schedule INDEX m/DD/MM/YY HHMM` +* *Meetings* : `meetings [DD/MM/YY]` +* *Add picture* : `​pic INDEX l/FILE_LOCATION` +* *View all contacts with a specified tag* : `tag TAG_NAME` +* *Remove all contacts from a specified tag* : `tag TAG_NAME remove` diff --git a/docs/diagrams/ByNameCommandClassDiagram.pptx b/docs/diagrams/ByNameCommandClassDiagram.pptx new file mode 100644 index 000000000000..c572152ab631 Binary files /dev/null and b/docs/diagrams/ByNameCommandClassDiagram.pptx differ diff --git a/docs/diagrams/ByNameCommandSequenceDiagram.pptx b/docs/diagrams/ByNameCommandSequenceDiagram.pptx new file mode 100644 index 000000000000..2b0d6c21ff20 Binary files /dev/null and b/docs/diagrams/ByNameCommandSequenceDiagram.pptx differ diff --git a/docs/diagrams/ExportSequenceDiagram.pptx b/docs/diagrams/ExportSequenceDiagram.pptx new file mode 100644 index 000000000000..199f80deaf91 Binary files /dev/null and b/docs/diagrams/ExportSequenceDiagram.pptx differ diff --git a/docs/diagrams/ImportSequenceDiagram.pptx b/docs/diagrams/ImportSequenceDiagram.pptx new file mode 100644 index 000000000000..09dbda173f2e Binary files /dev/null and b/docs/diagrams/ImportSequenceDiagram.pptx differ diff --git a/docs/diagrams/ModelComponentClassBetterOopDiagram.pptx b/docs/diagrams/ModelComponentClassBetterOopDiagram.pptx index d0561dfd305a..6a1fda492323 100644 Binary files a/docs/diagrams/ModelComponentClassBetterOopDiagram.pptx and b/docs/diagrams/ModelComponentClassBetterOopDiagram.pptx differ diff --git a/docs/diagrams/ModelComponentClassDiagram.pptx b/docs/diagrams/ModelComponentClassDiagram.pptx index 3c976908eaa7..6236e58315bf 100644 Binary files a/docs/diagrams/ModelComponentClassDiagram.pptx and b/docs/diagrams/ModelComponentClassDiagram.pptx differ diff --git a/docs/diagrams/TagCommandSequenceDiagram.pptx b/docs/diagrams/TagCommandSequenceDiagram.pptx new file mode 100644 index 000000000000..60e747789f7c Binary files /dev/null and b/docs/diagrams/TagCommandSequenceDiagram.pptx differ diff --git a/docs/images/ByNameCommandClassDiagram.png b/docs/images/ByNameCommandClassDiagram.png new file mode 100644 index 000000000000..ef30961dafa0 Binary files /dev/null and b/docs/images/ByNameCommandClassDiagram.png differ diff --git a/docs/images/ByNameCommandSequenceDiagram.png b/docs/images/ByNameCommandSequenceDiagram.png new file mode 100644 index 000000000000..25500006c598 Binary files /dev/null and b/docs/images/ByNameCommandSequenceDiagram.png differ diff --git a/docs/images/DeleteByNameCommand1StateDiagram.png b/docs/images/DeleteByNameCommand1StateDiagram.png new file mode 100644 index 000000000000..41d25b5d87f8 Binary files /dev/null and b/docs/images/DeleteByNameCommand1StateDiagram.png differ diff --git a/docs/images/DeleteByNameCommand2StateDiagram.png b/docs/images/DeleteByNameCommand2StateDiagram.png new file mode 100644 index 000000000000..58965c8ccf38 Binary files /dev/null and b/docs/images/DeleteByNameCommand2StateDiagram.png differ diff --git a/docs/images/DeleteByNameCommand3StateDiagram.png b/docs/images/DeleteByNameCommand3StateDiagram.png new file mode 100644 index 000000000000..5e8bdb52ecbb Binary files /dev/null and b/docs/images/DeleteByNameCommand3StateDiagram.png differ diff --git a/docs/images/DeleteByNameCommand4StateDiagram.png b/docs/images/DeleteByNameCommand4StateDiagram.png new file mode 100644 index 000000000000..f8282cd7ab15 Binary files /dev/null and b/docs/images/DeleteByNameCommand4StateDiagram.png differ diff --git a/docs/images/DeleteByNameCommandActivityDiagram.png b/docs/images/DeleteByNameCommandActivityDiagram.png new file mode 100644 index 000000000000..233e4c38f64e Binary files /dev/null and b/docs/images/DeleteByNameCommandActivityDiagram.png differ diff --git a/docs/images/EditByNameCommand1StateDiagram.png b/docs/images/EditByNameCommand1StateDiagram.png new file mode 100644 index 000000000000..41d25b5d87f8 Binary files /dev/null and b/docs/images/EditByNameCommand1StateDiagram.png differ diff --git a/docs/images/EditByNameCommand2StateDiagram.png b/docs/images/EditByNameCommand2StateDiagram.png new file mode 100644 index 000000000000..20da7ad66507 Binary files /dev/null and b/docs/images/EditByNameCommand2StateDiagram.png differ diff --git a/docs/images/EditByNameCommand3StateDiagram.png b/docs/images/EditByNameCommand3StateDiagram.png new file mode 100644 index 000000000000..92901235c99d Binary files /dev/null and b/docs/images/EditByNameCommand3StateDiagram.png differ diff --git a/docs/images/EditByNameCommand4StateDiagram.png b/docs/images/EditByNameCommand4StateDiagram.png new file mode 100644 index 000000000000..9f158c96d731 Binary files /dev/null and b/docs/images/EditByNameCommand4StateDiagram.png differ diff --git a/docs/images/EditByNameCommandActivityDiagram.png b/docs/images/EditByNameCommandActivityDiagram.png new file mode 100644 index 000000000000..05a30a2e876f Binary files /dev/null and b/docs/images/EditByNameCommandActivityDiagram.png differ diff --git a/docs/images/ExportActivityDiagram.png b/docs/images/ExportActivityDiagram.png new file mode 100644 index 000000000000..9a456fa4942d Binary files /dev/null and b/docs/images/ExportActivityDiagram.png differ diff --git a/docs/images/ExportSequenceDiagram.png b/docs/images/ExportSequenceDiagram.png new file mode 100644 index 000000000000..01835c48042f Binary files /dev/null and b/docs/images/ExportSequenceDiagram.png differ diff --git a/docs/images/ExportSequenceDiagram2.png b/docs/images/ExportSequenceDiagram2.png new file mode 100644 index 000000000000..58b6a11c603e Binary files /dev/null and b/docs/images/ExportSequenceDiagram2.png differ diff --git a/docs/images/ImportSequenceDiagram.png b/docs/images/ImportSequenceDiagram.png new file mode 100644 index 000000000000..e702fb036853 Binary files /dev/null and b/docs/images/ImportSequenceDiagram.png differ diff --git a/docs/images/ImportSequenceDiagram2.png b/docs/images/ImportSequenceDiagram2.png new file mode 100644 index 000000000000..e7407ec21a3a Binary files /dev/null and b/docs/images/ImportSequenceDiagram2.png differ diff --git a/docs/images/ModelClassBetterOopDiagram.png b/docs/images/ModelClassBetterOopDiagram.png index 9ba8eb5e31d0..2db9335ddf0d 100644 Binary files a/docs/images/ModelClassBetterOopDiagram.png and b/docs/images/ModelClassBetterOopDiagram.png differ diff --git a/docs/images/ModelClassDiagram.png b/docs/images/ModelClassDiagram.png index 9fb19078b859..5dc823c15118 100644 Binary files a/docs/images/ModelClassDiagram.png and b/docs/images/ModelClassDiagram.png differ diff --git a/docs/images/PictureCommand1StateDiagram.png b/docs/images/PictureCommand1StateDiagram.png new file mode 100644 index 000000000000..1465539ef364 Binary files /dev/null and b/docs/images/PictureCommand1StateDiagram.png differ diff --git a/docs/images/PictureCommand2StateDiagram.png b/docs/images/PictureCommand2StateDiagram.png new file mode 100644 index 000000000000..1becf124d4db Binary files /dev/null and b/docs/images/PictureCommand2StateDiagram.png differ diff --git a/docs/images/PictureCommand3StateDiagram.png b/docs/images/PictureCommand3StateDiagram.png new file mode 100644 index 000000000000..2e4f356329f2 Binary files /dev/null and b/docs/images/PictureCommand3StateDiagram.png differ diff --git a/docs/images/PictureCommandActivityDiagram.png b/docs/images/PictureCommandActivityDiagram.png new file mode 100644 index 000000000000..77820b91d346 Binary files /dev/null and b/docs/images/PictureCommandActivityDiagram.png differ diff --git a/docs/images/PictureCommandSequenceDiagram.png b/docs/images/PictureCommandSequenceDiagram.png new file mode 100644 index 000000000000..9726831ac59f Binary files /dev/null and b/docs/images/PictureCommandSequenceDiagram.png differ diff --git a/docs/images/ScheduleCommand1StateDiagram.png b/docs/images/ScheduleCommand1StateDiagram.png new file mode 100644 index 000000000000..6deda1d5a6de Binary files /dev/null and b/docs/images/ScheduleCommand1StateDiagram.png differ diff --git a/docs/images/ScheduleCommand2StateDiagram.png b/docs/images/ScheduleCommand2StateDiagram.png new file mode 100644 index 000000000000..e084c2a30fd8 Binary files /dev/null and b/docs/images/ScheduleCommand2StateDiagram.png differ diff --git a/docs/images/ScheduleCommand3StateDiagram.png b/docs/images/ScheduleCommand3StateDiagram.png new file mode 100644 index 000000000000..d5c5dbe1bc91 Binary files /dev/null and b/docs/images/ScheduleCommand3StateDiagram.png differ diff --git a/docs/images/ScheduleCommand4StateDiagram.png b/docs/images/ScheduleCommand4StateDiagram.png new file mode 100644 index 000000000000..b8a259d7c71c Binary files /dev/null and b/docs/images/ScheduleCommand4StateDiagram.png differ diff --git a/docs/images/ScheduleCommandActivityDiagram.png b/docs/images/ScheduleCommandActivityDiagram.png new file mode 100644 index 000000000000..c110e9ef0abb Binary files /dev/null and b/docs/images/ScheduleCommandActivityDiagram.png differ diff --git a/docs/images/ScheduleSequenceDiagram.png b/docs/images/ScheduleSequenceDiagram.png new file mode 100644 index 000000000000..25af6f93dc08 Binary files /dev/null and b/docs/images/ScheduleSequenceDiagram.png differ diff --git a/docs/images/TagCommand1StateDiagram.png b/docs/images/TagCommand1StateDiagram.png new file mode 100644 index 000000000000..c6f0db642125 Binary files /dev/null and b/docs/images/TagCommand1StateDiagram.png differ diff --git a/docs/images/TagCommand2StateDiagram.png b/docs/images/TagCommand2StateDiagram.png new file mode 100644 index 000000000000..72f6e8773038 Binary files /dev/null and b/docs/images/TagCommand2StateDiagram.png differ diff --git a/docs/images/TagCommand3StateDiagram.png b/docs/images/TagCommand3StateDiagram.png new file mode 100644 index 000000000000..84342cd830df Binary files /dev/null and b/docs/images/TagCommand3StateDiagram.png differ diff --git a/docs/images/TagCommand4StateDiagram.png b/docs/images/TagCommand4StateDiagram.png new file mode 100644 index 000000000000..582a2930fa72 Binary files /dev/null and b/docs/images/TagCommand4StateDiagram.png differ diff --git a/docs/images/TagCommand5StateDiagram.png b/docs/images/TagCommand5StateDiagram.png new file mode 100644 index 000000000000..a76dcafad844 Binary files /dev/null and b/docs/images/TagCommand5StateDiagram.png differ diff --git a/docs/images/TagCommand6StateDiagram.png b/docs/images/TagCommand6StateDiagram.png new file mode 100644 index 000000000000..7afd4a7823b4 Binary files /dev/null and b/docs/images/TagCommand6StateDiagram.png differ diff --git a/docs/images/TagCommandActivityDiagram.png b/docs/images/TagCommandActivityDiagram.png new file mode 100644 index 000000000000..329cb5e8fd81 Binary files /dev/null and b/docs/images/TagCommandActivityDiagram.png differ diff --git a/docs/images/TagCommandSequenceDiagram.png b/docs/images/TagCommandSequenceDiagram.png new file mode 100644 index 000000000000..b98b4f458ef3 Binary files /dev/null and b/docs/images/TagCommandSequenceDiagram.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 5ec9c527b49c..5d965dce8a59 100644 Binary files a/docs/images/Ui.png and b/docs/images/Ui.png differ diff --git a/docs/images/a19sean.png b/docs/images/a19sean.png new file mode 100644 index 000000000000..dcc4cb7bf28d Binary files /dev/null and b/docs/images/a19sean.png differ diff --git a/docs/images/ayushchatto.png b/docs/images/ayushchatto.png new file mode 100644 index 000000000000..f8b049381764 Binary files /dev/null and b/docs/images/ayushchatto.png differ diff --git a/docs/images/chantca95.png b/docs/images/chantca95.png new file mode 100644 index 000000000000..101056d015b1 Binary files /dev/null and b/docs/images/chantca95.png differ diff --git a/docs/images/damithc.jpg b/docs/images/damithc.jpg deleted file mode 100644 index 127543883893..000000000000 Binary files a/docs/images/damithc.jpg and /dev/null differ diff --git a/docs/images/denzelchung.png b/docs/images/denzelchung.png new file mode 100644 index 000000000000..342596b0880b Binary files /dev/null and b/docs/images/denzelchung.png differ diff --git a/docs/images/exportedCsv.png b/docs/images/exportedCsv.png new file mode 100644 index 000000000000..0c10a2f7a7b8 Binary files /dev/null and b/docs/images/exportedCsv.png differ diff --git a/docs/images/import-acceptable-csv.png b/docs/images/import-acceptable-csv.png new file mode 100644 index 000000000000..5f81bbb637ed Binary files /dev/null and b/docs/images/import-acceptable-csv.png differ diff --git a/docs/images/import-acceptable-csv2.png b/docs/images/import-acceptable-csv2.png new file mode 100644 index 000000000000..8f0d8867e63d Binary files /dev/null and b/docs/images/import-acceptable-csv2.png differ diff --git a/docs/images/import-acceptable-csv3.png b/docs/images/import-acceptable-csv3.png new file mode 100644 index 000000000000..db061a8465f8 Binary files /dev/null and b/docs/images/import-acceptable-csv3.png differ diff --git a/docs/images/import.png b/docs/images/import.png new file mode 100644 index 000000000000..d5e478c7220d Binary files /dev/null and b/docs/images/import.png differ diff --git a/docs/images/importActivityDiagram.png b/docs/images/importActivityDiagram.png new file mode 100644 index 000000000000..25d5235a3d12 Binary files /dev/null and b/docs/images/importActivityDiagram.png differ diff --git a/docs/images/import_duplicates.png b/docs/images/import_duplicates.png new file mode 100644 index 000000000000..6e5e02406829 Binary files /dev/null and b/docs/images/import_duplicates.png differ diff --git a/docs/images/import_success.png b/docs/images/import_success.png new file mode 100644 index 000000000000..459923222e87 Binary files /dev/null and b/docs/images/import_success.png differ diff --git a/docs/images/import_user_input.png b/docs/images/import_user_input.png new file mode 100644 index 000000000000..2a2acfc8566c Binary files /dev/null and b/docs/images/import_user_input.png differ diff --git a/docs/images/import_user_input_fail.png b/docs/images/import_user_input_fail.png new file mode 100644 index 000000000000..af40ee5964ee Binary files /dev/null and b/docs/images/import_user_input_fail.png differ diff --git a/docs/images/lejolly.jpg b/docs/images/lejolly.jpg deleted file mode 100644 index 2d1d94e0cf5d..000000000000 Binary files a/docs/images/lejolly.jpg and /dev/null differ diff --git a/docs/images/m133225.jpg b/docs/images/m133225.jpg deleted file mode 100644 index fd14fb94593a..000000000000 Binary files a/docs/images/m133225.jpg and /dev/null differ diff --git a/docs/images/yijinl.jpg b/docs/images/yijinl.jpg deleted file mode 100644 index adbf62ad9406..000000000000 Binary files a/docs/images/yijinl.jpg and /dev/null differ diff --git a/docs/images/yl_coder.jpg b/docs/images/yl_coder.jpg deleted file mode 100644 index 17b48a732272..000000000000 Binary files a/docs/images/yl_coder.jpg and /dev/null differ diff --git a/docs/images/zioul123.png b/docs/images/zioul123.png new file mode 100644 index 000000000000..150f4a3d9b6f Binary files /dev/null and b/docs/images/zioul123.png differ diff --git a/docs/team/A19Sean.adoc b/docs/team/A19Sean.adoc new file mode 100644 index 000000000000..ab7e4c1b5db9 --- /dev/null +++ b/docs/team/A19Sean.adoc @@ -0,0 +1,58 @@ += Auyok Sean - Project Portfolio +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets + +== PROJECT: InsuRen + +--- + +== Overview + +'Ren' in Mandarin translates to 'person' or 'people', and true to our name, this app is all about managing one’s network of clients in an organized, efficient and intuitive manner. InsuRen is geared to the needs of the modern Insurance salesman, but anyone whose business is their strong rapport with their clients will find this to be an indispensable tool. + +== Summary of contributions + +* *Major enhancement*: added *the ability to view tagged contacts and to edit and delete tags* +** What it does: allows the user view all contacts with user-specified tags, as well as edit and delete tags across all contacts. +** Justification: This feature improves the product significantly because a user can easily view relevant contacts and organise their contact book. +** Highlights: This enhancement makes use of a new `PersonContainsTagPredicate` to find the relevant tagged contacts. The enhancement also has three different functionality within one command word by recognising other keywords present in the user's command. +** Credits: Nil + +* *Code contributed*: [https://nus-cs2103-ay1819s1.github.io/cs2103-dashboard/#=undefined&search=a19sean&sort=displayName&since=2018-09-12&until=2018-10-08&timeframe=day&reverse=false&repoSort=true[RepoSense]] + +* *Other contributions*: + +** Project management: +*** Managed release `v1.3` on GitHub +** Enhancements to existing features: +*** Edited ResultDisplay such that upon an invalid command, the result display text would be in a red font. (Pull request https://github.com/CS2103-AY1819S1-W13-1/main/pull/13[#13]) +** Documentation: +*** Regularly updated User Guide and Developer Guide: https://github.com/CS2103-AY1819S1-W13-1/main/pull/9[#9], https://github.com/CS2103-AY1819S1-W13-1/main/pull/102[#102], https://github.com/CS2103-AY1819S1-W13-1/main/pull/108[#108], https://github.com/CS2103-AY1819S1-W13-1/main/pull/172[#172], https://github.com/CS2103-AY1819S1-W13-1/main/pull/183[#183] +** Community: +*** PRs reviewed (with non-trivial review comments): https://github.com/CS2103-AY1819S1-W13-1/main/pull/39[#39], https://github.com/CS2103-AY1819S1-W13-1/main/pull/73[#73], https://github.com/CS2103-AY1819S1-W13-1/main/pull/79[#79] +*** Reported bugs and suggestions for other teams in the class (examples: https://github.com/CS2103-AY1819S1-W12-3/main/issues/127[1], +https://github.com/CS2103-AY1819S1-W12-3/main/issues/142[2], https://github.com/CS2103-AY1819S1-W12-3/main/issues/145[3]) + +== 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=tag] + +== 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=tagcommand] + +include::../DeveloperGuide.adoc[tag=tagUseCase] + +include::../DeveloperGuide.adoc[tag=testingTag] + +include::../DeveloperGuide.adoc[tag=nfr] diff --git a/docs/team/AyushChatto.adoc b/docs/team/AyushChatto.adoc new file mode 100644 index 000000000000..155e6d137a27 --- /dev/null +++ b/docs/team/AyushChatto.adoc @@ -0,0 +1,58 @@ += Chattoraj Ayush - Project Portfolio +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets + +== PROJECT: InsuRen + +--- + +== Overview + +'Ren' in Mandarin translates to 'person' or 'people', and true to our name, this app is all about managing one’s network of clients in an organized, efficient and intuitive manner. InsuRen is geared to the needs of the modern Insurance salesman, but anyone whose business is their strong rapport with their clients will find this to be an indispensable tool. + +== Summary of contributions + +* *Major enhancement*: added *the ability to schedule meetings with clients* +** What it does: allows the user to schedule a meeting with a client. Schedule meetings can be searched using the meetings command. +** Justification: This feature allows insurance agents to keep track of their schedules by enabling them to have a record of all their meetings. It also allows them to quickly search through all their meetings, and prevents them from accidentally scheduling clashing meetings. +** Highlights: This enhancement added a new command. It required the use of multiple Java API classes for the validation of dates and times, as well the implementation of a new global data structure to store all meeting timings to prevent meetings from being scheduled at the same time. + +* *Minor enhancement*: added shorthand commands for all existing commands for faster access. + +* *Code contributed*: [https://nus-cs2103-ay1819s1.github.io/cs2103-dashboard/#=undefined&search=ayushchatto[RepoSense]] + +* *Other contributions*: + +** Project management: +*** Managed releases `v1.1` - `v1.2` (2 releases) on GitHub +** Documentation: +*** Did general changes to existing contents of the User Guide and Developer Guide: https://github.com/CS2103-AY1819S1-W13-1/main/pull/57[#57], https://github.com/CS2103-AY1819S1-W13-1/main/pull/91[#91], https://github.com/CS2103-AY1819S1-W13-1/main/pull/100[#100], https://github.com/CS2103-AY1819S1-W13-1/main/pull/116[#116], +** Community: +*** PRs reviewed (with non-trivial review comments): https://github.com/CS2103-AY1819S1-W13-1/main/pull/102[#102], https://github.com/CS2103-AY1819S1-W13-1/main/pull/60[#60] +*** Reported bugs and suggestions for other teams in the class (examples: https://github.com/CS2103-AY1819S1-T09-4/main/issues/107[1], https://github.com/CS2103-AY1819S1-T09-4/main/issues/109[2]) + +== 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=schedule] + +include::../UserGuide.adoc[tag=meetings] + +include::../UserGuide.adoc[tag=notifs] + +== 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=schedule] + +include::../DeveloperGuide.adoc[tag=testingschedule] + +include::../DeveloperGuide.adoc[tag=testingmeetings] diff --git a/docs/team/chantca95.adoc b/docs/team/chantca95.adoc new file mode 100644 index 000000000000..9bdeb6e819d5 --- /dev/null +++ b/docs/team/chantca95.adoc @@ -0,0 +1,70 @@ += Alex Chan - Project Portfolio +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets + +== PROJECT: InsuRen + +--- + +== Overview + +'Ren' in Mandarin translates to 'person' or 'people', and true to our name, this app is all about managing one’s network of clients in an organized, efficient and intuitive manner. InsuRen is geared to the needs of the modern Insurance salesman, but anyone whose business is their strong rapport with their clients will find this to be an indispensable tool. + +== Summary of contributions +* *Code contributed*: https://nus-cs2103-ay1819s1.github.io/cs2103-dashboard/#=undefined&search=chantca95[Project Code Dashboard] +* *Major enhancement*: added *the ability to import contacts from a CSV file* +** What it does: allows the user to import contacts into InsuRen from a previously appropriately-formatted Excel CSV file. +** Justification: This feature makes it convenient for users who want to populate their own InsuRen apps with existing lists of contacts. Rather than manually copying and adding contacts one by one into InsuRen, users can instead simply format their contact lists into a CSV as shown in the user guide, and import them into InsuRen in one go. +** Highlights: This enhancement uses a new *Command*, *ImportCommand*. Users can choose CSV files using a mouse and file browser, or if they are familiar with pathing formats on their respective Operating Systems, they can also enter the path (either relative and absolute) of the required CSV. +** Credits: none + +* *Minor enhancement*: added *the ability to export InsuRen data into a CSV file* +** What it does: allows the user to export the current contacts in InsuRen into a new CSV file, whose name is given by the user. +** Justification: This feature allows for convenient movement of a user's contact list. Some usages include keeping a back up of one's contact list, allowing one to view his InsuRen data on mobile platforms, etc. +** Highlights: This enhancement uses a new *Command*, *ExportCommand*. +** Credits: http://javaonlineguide.net/2014/10/compare-two-files-in-java-example-code.html - code referenced for testing ExportCommand + +* *Other contributions*: + +** Project management: +*** Ensured high test coverage, thorough tests +** Enhancements to existing features: +*** Added Coloured Tags +*** Import works based on Add Command - adding multiple Contacts based on csv files +*** Export utilizes the current state of the Model to create an csv file. +** Documentation: +*** Edited our README to reflect our product's purpose, vision and branding: https://github.com/CS2103-AY1819S1-W13-1/main/pull/6/files[#6] +*** Did general changes to existing contents of the User Guide and Developer Guide to reflect InsuRen branding: https://github.com/CS2103-AY1819S1-W13-1/main/pull/73/files[#73] +*** Updated `ImportCommand` and `ExportCommand` documentation on the User Guide and Developer Guide: https://github.com/CS2103-AY1819S1-W13-1/main/pull/83/files[#83] https://github.com/CS2103-AY1819S1-W13-1/main/pull/113/files[#113] +** Community: +*** Reported bugs and suggestions for other teams in the class (examples: https://github.com/CS2103-AY1819S1-W17-2/main/issues/263[1], https://github.com/CS2103-AY1819S1-W17-2/main/issues/262[2], https://github.com/CS2103-AY1819S1-W17-2/main/issues/261[3], +https://github.com/CS2103-AY1819S1-W17-2/main/issues/258[4], https://github.com/CS2103-AY1819S1-W17-2/main/issues/257[5], https://github.com/CS2103-AY1819S1-W17-2/main/issues/259[6], +https://github.com/CS2103-AY1819S1-W17-2/main/issues/254[7]) + + +== 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=import] + +include::../UserGuide.adoc[tag=export] + +== 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=import] + +include::../DeveloperGuide.adoc[tag=export] + +include::../DeveloperGuide.adoc[tag=manualTestingImport] + +include::../DeveloperGuide.adoc[tag=manualTestingExport] + + diff --git a/docs/team/denzelchung.adoc b/docs/team/denzelchung.adoc new file mode 100644 index 000000000000..e0466528e596 --- /dev/null +++ b/docs/team/denzelchung.adoc @@ -0,0 +1,66 @@ += Denzel Chung - Project Portfolio +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets + +== PROJECT: InsuRen + +--- + +== Overview + +'Ren' in Mandarin translates to 'person' or 'people', and true to our name, this app is all about managing one’s network of clients in an organized, efficient and intuitive manner. InsuRen is geared to the needs of the modern Insurance salesman, but anyone whose business is their strong rapport with their clients will find this to be an indispensable tool. + +== Summary of contributions + +* *Code contributed*: https://nus-cs2103-ay1819s1.github.io/cs2103-dashboard/#=undefined&search=denzelchung[Project Code Dashboard] +* *Major enhancement*: added *the ability to upload picture* +** What it does: allows the user to add a picture for his clients. +** Justification: This feature improves the usability and convenience of the product as the user can now identify his clients by their picture. In the event where the user has two clients of similar names, this feature can help the user identify their identities by looking at their picture and not make the mistake of calling the wrong person. +** Highlights: This enhancement added a new command. It required knowledge on path validation and handling the different platform's path format to make the program run on any mainstream OS. +InsuRen is programmed to do the following: +*** Loads a default picture if no picture is specified. +*** Loads the default picture if the set picture is somehow deleted and could not be found. +*** Accepts both absolute and relative path from the program's running directory. +** Credits: _rongjiecomputer_ for his suggestion on using `Guava` on the https://github.com/nus-cs2103-AY1819S1/forum/issues/115[forum]. + +* *Other contributions*: + +** Project management: +*** Setting up of project tools such as AppVeyor, Travis, and Coveralls. +** Enhancements to existing features: +*** Updated `Storage` to backup data file (Pull request https://github.com/CS2103-AY1819S1-W13-1/main/pull/12[#12]) +** Documentation: +*** Did general changes to existing contents of the User Guide and Developer Guide: https://github.com/CS2103-AY1819S1-W13-1/main/pull/34[#34] +*** Updated `PictureCommand` documentation on the User Guide and Developer Guide: https://github.com/CS2103-AY1819S1-W13-1/main/pull/93[#93], https://github.com/CS2103-AY1819S1-W13-1/main/pull/152[#152], https://github.com/CS2103-AY1819S1-W13-1/main/pull/157[#157] +** Community: +*** PRs reviewed (with non-trivial review comments): https://github.com/CS2103-AY1819S1-W13-1/main/pull/85[#85], https://github.com/CS2103-AY1819S1-W13-1/main/pull/160[#160] +*** Reported bugs and suggestions for other teams in the class (examples: https://github.com/CS2103-AY1819S1-T12-4/main/issues/147[1], https://github.com/CS2103-AY1819S1-T12-4/main/issues/148[2], https://github.com/CS2103-AY1819S1-T12-4/main/issues/151[3]) + +== 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=picture] + +include::../UserGuide.adoc[tag=picture-url] + +== 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=addpicture] + +include::../DeveloperGuide.adoc[tag=dataencryption] + +include::../DeveloperGuide.adoc[tag=addpictureurl] + +include::../DeveloperGuide.adoc[tag=picUseCase] + +include::../DeveloperGuide.adoc[tag=nfr] + +include::../DeveloperGuide.adoc[tag=testingpicture] diff --git a/docs/team/zioul123.adoc b/docs/team/zioul123.adoc new file mode 100644 index 000000000000..d3b5cb109219 --- /dev/null +++ b/docs/team/zioul123.adoc @@ -0,0 +1,67 @@ += Louiz Kim-Chan - Project Portfolio +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets + +== PROJECT: InsuRen + +--- + +== Overview + +'Ren' in Mandarin translates to 'person' or 'people', and true to our name, this app is all about managing one’s network of clients in an organized, efficient and intuitive manner. InsuRen is geared to the needs of the modern Insurance salesman, but anyone whose business is their strong rapport with their clients will find this to be an indispensable tool. + +== Summary of contributions + +* *Major enhancement*: added the ability to *edit client contact information and delete client contacts by name*. +** What it does: Instead of having to edit or delete contact info by using an index, you can edit and delete contacts directly using their name, provided their name is distinguishable from all other names present in InsuRen. +** Justification: This feature improves the product significantly because normally, editing requires scrolling or using `find` to filter the list first. Since we don't want the user to have to use the mouse, and we want to speed up processes, editing by name will help achieve the product's goals. +** Highlights: This enhancement extends the `edit` and `delete` commands and makes use of a new `Predicate` and `PersonFinderUtil` to find a `Person` when provided a unique identifier. +** Credits: Nil. + +* *Minor enhancement*: Make the address, email and phone number fields `Optional` (Pull requests https://github.com/CS2103-AY1819S1-W13-1/main/pull/16[#16], https://github.com/CS2103-AY1819S1-W13-1/main/pull/33[#33]). +** What it does: Instead of requiring all fields to be filled when adding a person, which may require an Insurance Agent to fill placeholder information, she can leave several fields blank instead. +** Justification: This feature improves the product significantly, because insurance agents do not always have all the information about their clients they need, but they may still need to keep track of them and contact them with whatever incomplete information they have. This feature thus adds a degree of freedom and ease of use to insurance agents. + +* *Code contributed*: [https://nus-cs2103-ay1819s1.github.io/cs2103-dashboard/#=undefined&search=zioul123[RepoSense]] + +* *Other contributions*: + +** Project management: +*** Managed release `v1.2` and `v1.3.1` on GitHub +** Enhancements to existing features: +*** Add a display for the number of people in InsuRen on the status bar (Pull request https://github.com/CS2103-AY1819S1-W13-1/main/pull/8[#8]) +*** Change aesthetics and add text wrapping to Person Cards (Pull request https://github.com/CS2103-AY1819S1-W13-1/main/pull/4[#4]) +** Documentation: +*** Update the About Us page to reflect our team (Pull requests https://github.com/CS2103-AY1819S1-W13-1/main/pull/2[#2], https://github.com/CS2103-AY1819S1-W13-1/main/pull/15[#15]) +*** Update the images shown on the project page (Pull requests https://github.com/CS2103-AY1819S1-W13-1/main/pull/2[#2], https://github.com/CS2103-AY1819S1-W13-1/main/pull/15[#15], https://github.com/CS2103-AY1819S1-W13-1/main/pull/28[#28], https://github.com/CS2103-AY1819S1-W13-1/main/pull/56[#56]) +** Community: +*** PRs reviewed (with non-trivial review comments): https://github.com/CS2103-AY1819S1-W13-1/main/pull/22[#22], https://github.com/CS2103-AY1819S1-W13-1/main/pull/23[#23], https://github.com/CS2103-AY1819S1-W13-1/main/pull/40[#40], https://github.com/CS2103-AY1819S1-W13-1/main/pull/57[#57], https://github.com/CS2103-AY1819S1-W13-1/main/pull/58[#58], https://github.com/CS2103-AY1819S1-W13-1/main/pull/60[#60], https://github.com/CS2103-AY1819S1-W13-1/main/pull/71[#71] +*** Reported bugs and suggestions for other teams in the class (examples: https://github.com/CS2103-AY1819S1-F11-4/main/issues/169[1], https://github.com/CS2103-AY1819S1-F11-4/main/issues/171[2], https://github.com/CS2103-AY1819S1-F11-4/main/issues/184[3], https://github.com/CS2103-AY1819S1-F11-4/main/issues/186[4], https://github.com/CS2103-AY1819S1-F11-4/main/issues/190[5]) +** Tools: +*** Get CI tools to print stack traces on failed tests (Pull request https://github.com/CS2103-AY1819S1-W13-1/main/pull/77[#77]) + +== 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=editByName] + +include::../UserGuide.adoc[tag=deleteByName] + +== 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=byNameCommands] + +include::../DeveloperGuide.adoc[tag=editUseCase] + +include::../DeveloperGuide.adoc[tag=deleteUseCase] + +include::../DeveloperGuide.adoc[tag=testingDelete] + diff --git a/docs/templates/_header.html.slim b/docs/templates/_header.html.slim index 1995d26a1615..24c6247f7ef2 100644 --- a/docs/templates/_header.html.slim +++ b/docs/templates/_header.html.slim @@ -6,14 +6,6 @@ a.navbar-brand href='https://se-edu.github.io/' img src=(site_url 'images/SeEduLogo.png') alt='SE-EDU' ul.navbar-nav - li.nav-item - a.nav-link href='https://se-edu.github.io/addressbook-level1' AB-1 - li.nav-item - a.nav-link href='https://se-edu.github.io/addressbook-level2' AB-2 - li.nav-item - a.nav-link href='https://se-edu.github.io/addressbook-level3' AB-3 - li.nav-item - a.nav-link.active href=(site_url 'index.html') AB-4 li.nav-item a.nav-link href='https://se-edu.github.io/collate' Collate li.nav-item diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java index ecdd043a4f81..96218210a715 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/seedu/address/MainApp.java @@ -40,7 +40,7 @@ */ 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, false); private static final Logger logger = LogsCenter.getLogger(MainApp.class); diff --git a/src/main/java/seedu/address/commons/core/Config.java b/src/main/java/seedu/address/commons/core/Config.java index e978d621e086..6695794339e9 100644 --- a/src/main/java/seedu/address/commons/core/Config.java +++ b/src/main/java/seedu/address/commons/core/Config.java @@ -13,7 +13,7 @@ public class Config { public static final Path DEFAULT_CONFIG_FILE = Paths.get("config.json"); // Config values customizable through config file - private String appTitle = "Address App"; + private String appTitle = "InsuRen"; private Level logLevel = Level.INFO; private Path userPrefsFilePath = Paths.get("preferences.json"); diff --git a/src/main/java/seedu/address/commons/core/Messages.java b/src/main/java/seedu/address/commons/core/Messages.java index 1deb3a1e4695..abdf8bf63232 100644 --- a/src/main/java/seedu/address/commons/core/Messages.java +++ b/src/main/java/seedu/address/commons/core/Messages.java @@ -9,5 +9,14 @@ public class Messages { 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!"; + public static final String MESSAGE_MEETINGS_LISTED_OVERVIEW = "%1$d meetings scheduled!"; + public static final String MESSAGE_PERSON_NOT_FOUND = "The identifier provided does not match anyone in the " + + "person list!"; + public static final String MESSAGE_MULTIPLE_PERSONS_FOUND = "The identifier provided matches multiple people in" + + " the person list! Please re-use the command with either a specified index or a more specific name!"; + public static final String MESSAGE_TAGGED_PERSONS_LISTED_OVERVIEW = "%1$d tagged persons listed!"; + public static final String MESSAGE_TAG_DELETED_OVERVIEW = "%1$d persons untagged!"; + public static final String MESSAGE_TAG_EDITED_OVERVIEW = "%1$d persons changed tag from \"%2$s\" to \"%3$s\"!"; + public static final String MESSAGE_CLASHING_MEETINGS = "There is already a meeting scheduled at the given time"; } diff --git a/src/main/java/seedu/address/commons/events/ui/NewResultAvailableEvent.java b/src/main/java/seedu/address/commons/events/ui/NewResultAvailableEvent.java index a5e8b2e13883..117f5db4effa 100644 --- a/src/main/java/seedu/address/commons/events/ui/NewResultAvailableEvent.java +++ b/src/main/java/seedu/address/commons/events/ui/NewResultAvailableEvent.java @@ -8,9 +8,11 @@ public class NewResultAvailableEvent extends BaseEvent { public final String message; + public final Boolean isValid; - public NewResultAvailableEvent(String message) { + public NewResultAvailableEvent(String message, boolean isValid) { this.message = message; + this.isValid = isValid; } @Override diff --git a/src/main/java/seedu/address/commons/util/StringUtil.java b/src/main/java/seedu/address/commons/util/StringUtil.java index 61cc8c9a1cb8..579377dfdf1c 100644 --- a/src/main/java/seedu/address/commons/util/StringUtil.java +++ b/src/main/java/seedu/address/commons/util/StringUtil.java @@ -65,4 +65,20 @@ public static boolean isNonZeroUnsignedInteger(String s) { return false; } } + + //@@author zioul123 + /** + * Returns true if {@code s} represents an integer. Will return false for any other non-null string input. + * @throws NullPointerException if {@code s} is null. + */ + public static boolean isInteger(String s) { + requireNonNull(s); + + try { + int value = Integer.parseInt(s); + return true; + } catch (NumberFormatException nfe) { + return false; + } + } } diff --git a/src/main/java/seedu/address/logic/IndexParserUtil.java b/src/main/java/seedu/address/logic/IndexParserUtil.java new file mode 100644 index 000000000000..67de485f7a7a --- /dev/null +++ b/src/main/java/seedu/address/logic/IndexParserUtil.java @@ -0,0 +1,29 @@ +package seedu.address.logic; + +import java.util.Optional; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; + +//@@author zioul123 +/** + * Contains a utility method used for parsing a {@code String} for an Index, without throwing ParseExceptions. + */ +public class IndexParserUtil { + + /** + * Attempts to parseFileFromArgs an index from the provided string. + * @param string The preamble of the command typed. + * @return An Optional of the index if the specified index is valid, an empty optional otherwise + */ + public static Optional getIndex(String string) { + Index index; + try { + index = ParserUtil.parseIndex(string); + } catch (ParseException pe) { + return Optional.empty(); + } + return Optional.of(index); + } +} diff --git a/src/main/java/seedu/address/logic/PersonFinderUtil.java b/src/main/java/seedu/address/logic/PersonFinderUtil.java new file mode 100644 index 000000000000..3290d51a35e7 --- /dev/null +++ b/src/main/java/seedu/address/logic/PersonFinderUtil.java @@ -0,0 +1,49 @@ +package seedu.address.logic; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.util.Arrays; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import seedu.address.commons.core.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.Model; +import seedu.address.model.person.NameContainsAllKeywordsPredicate; +import seedu.address.model.person.Person; + +//@@author zioul123 +/** + * Contains a utility method used for finding a person by their name in a model provided. + */ +public class PersonFinderUtil { + + public static final String GENERAL_MESSAGE_USAGE = "Press/type help to see the command usage."; + + /** + * Find a single person from the specified {@Code Model} using the {@Code String personIdentifier}. + */ + public static Person findPerson(Model model, String personIdentifier) throws ParseException, CommandException { + String trimmedArgs = personIdentifier.trim(); + if (trimmedArgs.isEmpty()) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, GENERAL_MESSAGE_USAGE)); + } + String[] nameKeywords = trimmedArgs.split("\\s+"); + NameContainsAllKeywordsPredicate predicate = new NameContainsAllKeywordsPredicate(Arrays.asList(nameKeywords)); + + // Supplier is used because the stream is acted on more than once. + Supplier> filteredPersons = () -> + model.getAddressBook().getPersonList().stream().filter(predicate); + + long numOfPeopleMatching = filteredPersons.get().count(); + if (numOfPeopleMatching == 0) { + throw new CommandException(Messages.MESSAGE_PERSON_NOT_FOUND); + } else if (numOfPeopleMatching != 1) { + throw new CommandException(Messages.MESSAGE_MULTIPLE_PERSONS_FOUND); + } + + return filteredPersons.get().iterator().next(); + } +} diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddCommand.java index d88e831ff1ce..2fad12694406 100644 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ b/src/main/java/seedu/address/logic/commands/AddCommand.java @@ -18,6 +18,7 @@ public class AddCommand extends Command { public static final String COMMAND_WORD = "add"; + public static final String COMMAND_ALIAS = "a"; public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a person to the address book. " + "Parameters: " diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/address/logic/commands/ClearCommand.java index 1f85bcfe85a8..08b8172d2974 100644 --- a/src/main/java/seedu/address/logic/commands/ClearCommand.java +++ b/src/main/java/seedu/address/logic/commands/ClearCommand.java @@ -12,6 +12,7 @@ public class ClearCommand extends Command { public static final String COMMAND_WORD = "clear"; + public static final String COMMAND_ALIAS = "c"; public static final String MESSAGE_SUCCESS = "Address book has been cleared!"; diff --git a/src/main/java/seedu/address/logic/commands/DeleteByNameCommand.java b/src/main/java/seedu/address/logic/commands/DeleteByNameCommand.java new file mode 100644 index 000000000000..dcac577ccfc6 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/DeleteByNameCommand.java @@ -0,0 +1,51 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.PersonFinderUtil; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.Model; +import seedu.address.model.person.Person; + +//@@author zioul123 +/** + * Deletes a person identified using their name from the address book. + */ +public class DeleteByNameCommand extends DeleteCommand { + + protected final String personIdentifier; + + public DeleteByNameCommand(String personIdentifier) { + super(Index.fromZeroBased(NO_INDEX)); + requireNonNull(personIdentifier); + + this.personIdentifier = personIdentifier; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + + Person personToDelete; + try { + personToDelete = PersonFinderUtil.findPerson(model, personIdentifier); + } catch (ParseException pe) { + throw new CommandException(String.format(Messages.MESSAGE_INVALID_COMMAND_FORMAT, MESSAGE_USAGE)); + } + + 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 DeleteByNameCommand // instanceof handles nulls + && personIdentifier.equals(((DeleteByNameCommand) other).personIdentifier)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java index a20e9d49eac7..12720906dc67 100644 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ b/src/main/java/seedu/address/logic/commands/DeleteCommand.java @@ -17,7 +17,7 @@ public class DeleteCommand extends Command { public static final String COMMAND_WORD = "delete"; - + public static final String COMMAND_ALIAS = "d"; 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" @@ -25,6 +25,8 @@ public class DeleteCommand extends Command { public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted Person: %1$s"; + public static final int NO_INDEX = 1000000000; + private final Index targetIndex; public DeleteCommand(Index targetIndex) { diff --git a/src/main/java/seedu/address/logic/commands/EditByNameCommand.java b/src/main/java/seedu/address/logic/commands/EditByNameCommand.java new file mode 100644 index 000000000000..80f43ccc7ae2 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/EditByNameCommand.java @@ -0,0 +1,68 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.PersonFinderUtil; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.Model; +import seedu.address.model.person.Person; + +//@@author zioul123 +/** + * Edits the details of an existing person in the address book using their name. + */ +public class EditByNameCommand extends EditCommand { + + protected final String personIdentifier; + + public EditByNameCommand(String personIdentifier, EditPersonDescriptor editPersonDescriptor) { + super(Index.fromZeroBased(NO_INDEX), editPersonDescriptor); + requireNonNull(personIdentifier); + + this.personIdentifier = personIdentifier; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + + Person person; + try { + person = PersonFinderUtil.findPerson(model, personIdentifier); + } catch (ParseException pe) { + throw new CommandException(MESSAGE_NO_EDIT_IDENTIFIER); + } + Person editedPerson = EditCommand.createEditedPerson(person, editPersonDescriptor); + + if (!person.isSamePerson(editedPerson) && model.hasPerson(editedPerson)) { + throw new CommandException(EditCommand.MESSAGE_DUPLICATE_PERSON); + } + + model.updatePerson(person, editedPerson); + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + model.commitAddressBook(); + return new CommandResult(String.format(EditCommand.MESSAGE_EDIT_PERSON_SUCCESS, editedPerson)); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditByNameCommand)) { + return false; + } + + // state check + EditByNameCommand e = (EditByNameCommand) other; + return personIdentifier.equals(e.personIdentifier) + && editPersonDescriptor.equals(e.editPersonDescriptor); + } +} diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java index dc782d8e230f..7c50505669b9 100644 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ b/src/main/java/seedu/address/logic/commands/EditCommand.java @@ -20,11 +20,13 @@ import seedu.address.logic.CommandHistory; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; +import seedu.address.model.meeting.Meeting; 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.person.Picture; import seedu.address.model.tag.Tag; /** @@ -33,7 +35,7 @@ public class EditCommand extends Command { public static final String COMMAND_WORD = "edit"; - + public static final String COMMAND_ALIAS = "e"; 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" @@ -48,11 +50,15 @@ public class EditCommand extends Command { + PREFIX_EMAIL + "johndoe@example.com"; public static final String MESSAGE_EDIT_PERSON_SUCCESS = "Edited Person: %1$s"; + public static final String MESSAGE_NO_EDIT_IDENTIFIER = "No identifier specified! Please provide either an " + + "INDEX parameter or a name parameter.\n" + MESSAGE_USAGE; 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."; + public static final int NO_INDEX = 1000000000; + + protected final EditPersonDescriptor editPersonDescriptor; private final Index index; - private final EditPersonDescriptor editPersonDescriptor; /** * @param index of the person in the filtered person list to edit @@ -92,16 +98,27 @@ public CommandResult execute(Model model, CommandHistory history) throws Command * Creates and returns a {@code Person} with the details of {@code personToEdit} * edited with {@code editPersonDescriptor}. */ - private static Person createEditedPerson(Person personToEdit, EditPersonDescriptor editPersonDescriptor) { + protected 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()); + //@@author zioul123 + // Check if the editPersonDescriptor has these fields. + boolean hasPhone = editPersonDescriptor.getPhone().isPresent(); + boolean hasEmail = editPersonDescriptor.getEmail().isPresent(); + boolean hasAddress = editPersonDescriptor.getAddress().isPresent(); + + // Take the value of the editPersonDescriptor's field if it exists, otherwise take the original value. + Optional updatedPhone = hasPhone ? editPersonDescriptor.getPhone() : personToEdit.getPhone(); + Optional updatedEmail = hasEmail ? editPersonDescriptor.getEmail() : personToEdit.getEmail(); + Optional
updatedAddress = hasAddress ? editPersonDescriptor.getAddress() : personToEdit.getAddress(); + //@@author Set updatedTags = editPersonDescriptor.getTags().orElse(personToEdit.getTags()); - - return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags); + // Edit command does not change meeting. Use schedule to change meeting. + Meeting updatedMeeting = personToEdit.getMeeting(); + Picture updatedPicture = personToEdit.getPicture(); + return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, + updatedTags, updatedMeeting, updatedPicture); } @Override diff --git a/src/main/java/seedu/address/logic/commands/ExitCommand.java b/src/main/java/seedu/address/logic/commands/ExitCommand.java index e848fa918964..f5e2a05aa6d0 100644 --- a/src/main/java/seedu/address/logic/commands/ExitCommand.java +++ b/src/main/java/seedu/address/logic/commands/ExitCommand.java @@ -11,7 +11,7 @@ public class ExitCommand extends Command { public static final String COMMAND_WORD = "exit"; - + public static final String COMMAND_ALIAS = "q"; public static final String MESSAGE_EXIT_ACKNOWLEDGEMENT = "Exiting Address Book as requested ..."; @Override 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..81c1ceaa12b1 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ExportCommand.java @@ -0,0 +1,132 @@ +//@@author chantca95 +package seedu.address.logic.commands; + +import static seedu.address.model.meeting.Meeting.NO_MEETING_MSG; + +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Set; + +import javafx.collections.ObservableList; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Person; +import seedu.address.model.tag.Tag; +/** + * Exports current state of InsuRen as a .csv file whose name is specified by the user. + */ +public class ExportCommand extends Command { + public static final String COMMAND_WORD = "export"; + public static final String COMMAND_ALIAS = "x"; + public static final String MESSAGE_USAGE = COMMAND_WORD + " destinationFilename.csv"; + public static final String MESSAGE_SUCCESS = "Contacts successfully exported."; + public static final String MESSAGE_DUPLICATE_FILE = "A file already exists with that name. Choose a new name."; + + private String fileName; + + public ExportCommand(String fileName) { + this.fileName = fileName; + } + /** + * Writes current InsuRen data into .csv file. + */ + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + FileOutputStream fos = null; + try { + fos = new FileOutputStream(fileName, true); + } catch (FileNotFoundException e) { + e.printStackTrace(); + } + PrintWriter pw = new PrintWriter(fos); + populateFile(pw, model); + pw.close(); + + return new CommandResult(MESSAGE_SUCCESS); + } + /** + * Gets list of persons from a ReadOnlyAddressBook and populates the .csv file + */ + private void populateFile(PrintWriter pw, Model model) { + ObservableList bufferList = model.getAddressBook().getPersonList(); + pw.println("Name, Phone, Email, Address, Meeting, Tags"); + for (Person current : bufferList) { + insertPersonIntoCsv(current, pw); + } + } + /** + * Insert Persons one by one, cleaning up their entries in the process. + */ + private void insertPersonIntoCsv(Person current, PrintWriter pw) { + String name; + String phone; + String email; + String address; + String meeting; + Set tags; + ArrayList stringTags = new ArrayList<>(); + + name = cleanEntry(current.getName().toString()); + if (current.getPhone().isPresent()) { + phone = cleanEntry(current.getPhone().toString()); + } else { + phone = ""; + } + if (current.getEmail().isPresent()) { + email = cleanEntry(current.getEmail().toString()); + } else { + email = ""; + } + if (current.getAddress().isPresent()) { + address = cleanEntry(current.getAddress().toString()); + } else { + address = ""; + } + meeting = current.getMeeting().toString(); + if (meeting.equals(NO_MEETING_MSG)) { + meeting = ""; + } + tags = current.getTags(); + for (Tag currentTag : tags) { + String currString = currentTag.toString(); + currString = cleanEntry(currString); + stringTags.add(currString); + } + + //write this person to the printwriter + pw.print(name + "," + phone + "," + email + "," + address + "," + meeting + ","); + for (String currentTag : stringTags) { + pw.print(currentTag); + pw.print(","); + } + pw.println(); + } + /** + * Removes commas and Optional[] bracketing from a Person's contact fields. + */ + private String cleanEntry(String oldStr) { + String newStr = oldStr.replaceAll(",", ""); + newStr = newStr.replaceAll("Optional", ""); + newStr = newStr.replaceAll("\\[", ""); + newStr = newStr.replaceAll("]", ""); + return newStr; + } + + public String getFileName() { + return fileName; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if ((other instanceof ExportCommand)) { + ExportCommand otherEc = (ExportCommand) other; + return (this.fileName.equals(otherEc.getFileName())); + } + return false; + } +} diff --git a/src/main/java/seedu/address/logic/commands/FindCommand.java b/src/main/java/seedu/address/logic/commands/FindCommand.java index beb178e3a3f5..540172843d16 100644 --- a/src/main/java/seedu/address/logic/commands/FindCommand.java +++ b/src/main/java/seedu/address/logic/commands/FindCommand.java @@ -14,7 +14,7 @@ public class FindCommand extends Command { public static final String COMMAND_WORD = "find"; - + public static final String COMMAND_ALIAS = "f"; 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" diff --git a/src/main/java/seedu/address/logic/commands/HelpCommand.java b/src/main/java/seedu/address/logic/commands/HelpCommand.java index 66305e95d8f2..6f80a1fa4dc4 100644 --- a/src/main/java/seedu/address/logic/commands/HelpCommand.java +++ b/src/main/java/seedu/address/logic/commands/HelpCommand.java @@ -11,7 +11,7 @@ public class HelpCommand extends Command { public static final String COMMAND_WORD = "help"; - + public static final String COMMAND_ALIAS = "h"; public static final String MESSAGE_USAGE = COMMAND_WORD + ": Shows program usage instructions.\n" + "Example: " + COMMAND_WORD; diff --git a/src/main/java/seedu/address/logic/commands/HistoryCommand.java b/src/main/java/seedu/address/logic/commands/HistoryCommand.java index f1541fb57f20..251a3bb3d2f8 100644 --- a/src/main/java/seedu/address/logic/commands/HistoryCommand.java +++ b/src/main/java/seedu/address/logic/commands/HistoryCommand.java @@ -14,6 +14,7 @@ public class HistoryCommand extends Command { public static final String COMMAND_WORD = "history"; + public static final String COMMAND_ALIAS = "hs"; public static final String MESSAGE_SUCCESS = "Entered commands (from most recent to earliest):\n%1$s"; public static final String MESSAGE_NO_HISTORY = "You have not yet entered any commands."; 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..ed7ef9417312 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ImportCommand.java @@ -0,0 +1,92 @@ +package seedu.address.logic.commands; +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_FILE_LOCATION; + +import java.util.ArrayList; + +import seedu.address.logic.CommandHistory; +import seedu.address.model.Model; +import seedu.address.model.person.Person; + +//@@author chantca95 +/** + * Adds a list of pre-established Valid persons to the address book. + * Duplicate contacts will not be added. + */ +public class ImportCommand extends Command { + public static final String COMMAND_WORD = "import"; + public static final String COMMAND_ALIAS = "i"; + + public static final String MESSAGE_SUCCESS = "Import successful!"; + public static final String MESSAGE_DUPLICATE = " Duplicate contacts not imported."; + public static final String MESSAGE_FAIL = "No contacts imported."; + public static final String MESSAGE_USAGE = "Imports contacts from a CSV file. \n" + + "Parameters: " + PREFIX_FILE_LOCATION + "FILE_LOCATION " + + "OR leave the path empty to open up a file browser (ie. type import ONLY)"; + public static final String MESSAGE_INVALID_FIELD = " Contacts with invalid fields not imported."; + public static final String MESSAGE_NAMELESS_CONTACT = " Contacts without at least a name field not imported."; + + private final ArrayList personsToAdd; + private final boolean hasContactWithInvalidField; + private final boolean hasContactWithoutName; + + public ImportCommand(ArrayList imports, boolean hasContactWithInvalidField, boolean hasContactWithoutName) { + requireNonNull(imports); + personsToAdd = imports; + this.hasContactWithInvalidField = hasContactWithInvalidField; + this.hasContactWithoutName = hasContactWithoutName; + } + /** + * Adds Persons in list to AddressBook. + */ + @Override + public CommandResult execute(Model model, CommandHistory history) { + boolean hasAddedNew = false; + boolean hasDuplicate = false; + + for (int i = 0; i < personsToAdd.size(); i++) { + if (model.hasPerson(personsToAdd.get(i))) { + hasDuplicate = true; + continue; + } + model.addPerson(personsToAdd.get(i)); + hasAddedNew = true; + } + if (hasAddedNew) { + model.commitAddressBook(); + } + + //Format the final message acccordingly + StringBuilder finalReportMessage = new StringBuilder(); + if (hasAddedNew) { + finalReportMessage.append(MESSAGE_SUCCESS); + } else { + finalReportMessage.append(MESSAGE_FAIL); + } + if (hasDuplicate) { + finalReportMessage.append(MESSAGE_DUPLICATE); + } + if (hasContactWithInvalidField) { + finalReportMessage.append(MESSAGE_INVALID_FIELD); + } + if (hasContactWithoutName) { + finalReportMessage.append(MESSAGE_NAMELESS_CONTACT); + } + return new CommandResult(finalReportMessage.toString()); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if ((other instanceof ImportCommand)) { + ImportCommand otherIc = (ImportCommand) other; + + return (personsToAdd.equals(otherIc.personsToAdd) + && hasContactWithoutName == otherIc.hasContactWithoutName + && hasContactWithInvalidField == otherIc.hasContactWithInvalidField); + } + return false; + } +} diff --git a/src/main/java/seedu/address/logic/commands/ListCommand.java b/src/main/java/seedu/address/logic/commands/ListCommand.java index 6d44824c7d1b..316927e8a83b 100644 --- a/src/main/java/seedu/address/logic/commands/ListCommand.java +++ b/src/main/java/seedu/address/logic/commands/ListCommand.java @@ -12,7 +12,7 @@ public class ListCommand extends Command { public static final String COMMAND_WORD = "list"; - + public static final String COMMAND_ALIAS = "l"; public static final String MESSAGE_SUCCESS = "Listed all persons"; diff --git a/src/main/java/seedu/address/logic/commands/MeetingsCommand.java b/src/main/java/seedu/address/logic/commands/MeetingsCommand.java new file mode 100644 index 000000000000..f6973403f657 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/MeetingsCommand.java @@ -0,0 +1,43 @@ +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.meeting.SameMeetingDayPredicate; + +//@@author AyushChatto +/** + * Returns details of meetings scheduled at a particular time. + */ +public class MeetingsCommand extends Command { + public static final String COMMAND_WORD = "meetings"; + public static final String COMMAND_ALIAS = "m"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Displays all persons who have meetings " + + "scheduled on the same day, if it is provided. Lists all meetings scheduled otherwise. \n" + + "Parameters: [DD/MM/YY] \n" + + "Example: " + COMMAND_WORD + " 23/02/18"; + + private final SameMeetingDayPredicate predicate; + + public MeetingsCommand(SameMeetingDayPredicate predicate) { + requireNonNull(predicate); + this.predicate = predicate; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) { + requireNonNull(model); + model.updateFilteredPersonList(predicate); + return new CommandResult( + String.format(Messages.MESSAGE_MEETINGS_LISTED_OVERVIEW, model.getFilteredPersonList().size())); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof MeetingsCommand // instanceof handles nulls + && predicate.equals(((MeetingsCommand) other).predicate)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/PictureCommand.java b/src/main/java/seedu/address/logic/commands/PictureCommand.java new file mode 100644 index 000000000000..c9ebd09ad23c --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/PictureCommand.java @@ -0,0 +1,70 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_FILE_LOCATION; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +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; +import seedu.address.model.person.Picture; + +//@@author denzelchung +/** + * Adds a picture to a contact in the address book. + */ +public class PictureCommand extends Command { + + public static final String COMMAND_WORD = "pic"; + public static final String COMMAND_ALIAS = "p"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a picture to a contact in the address book " + + "by the index number used in the displayed person list.\n" + + "Parameters: INDEX (must be a positive integer) " + + PREFIX_FILE_LOCATION + "FILE_LOCATION\n" + + "Example: " + COMMAND_WORD + " 1 " + + PREFIX_FILE_LOCATION + "johndoe.jpg"; + + public static final String MESSAGE_SUCCESS = "Added picture for Person: %1$s"; + + private final Index index; + private final Picture picture; + + /** + * @param index of the person in the filtered person list to edit + * @param picture of the profile picture + */ + public PictureCommand(Index index, Picture picture) { + requireNonNull(index); + requireNonNull(picture); + + this.index = index; + this.picture = picture; + } + + @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 = new Person(personToEdit.getName(), personToEdit.getPhone(), personToEdit.getEmail(), + personToEdit.getAddress(), personToEdit.getTags(), personToEdit.getMeeting()); + editedPerson.setPicture(picture); + + model.updatePerson(personToEdit, editedPerson); + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + model.commitAddressBook(); + + return new CommandResult(String.format(MESSAGE_SUCCESS, editedPerson)); + } +} diff --git a/src/main/java/seedu/address/logic/commands/RedoCommand.java b/src/main/java/seedu/address/logic/commands/RedoCommand.java index 227771a4eef6..28694420dbcc 100644 --- a/src/main/java/seedu/address/logic/commands/RedoCommand.java +++ b/src/main/java/seedu/address/logic/commands/RedoCommand.java @@ -13,6 +13,7 @@ public class RedoCommand extends Command { public static final String COMMAND_WORD = "redo"; + public static final String COMMAND_ALIAS = "r"; public static final String MESSAGE_SUCCESS = "Redo success!"; public static final String MESSAGE_FAILURE = "No more commands to redo!"; diff --git a/src/main/java/seedu/address/logic/commands/ScheduleCommand.java b/src/main/java/seedu/address/logic/commands/ScheduleCommand.java new file mode 100644 index 000000000000..36054eeb72ff --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ScheduleCommand.java @@ -0,0 +1,94 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MEETING; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +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.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.meeting.Meeting; +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.person.Picture; +import seedu.address.model.tag.Tag; + +//@@author AyushChatto +/** + * Schedules a meeting with a person. + */ +public class ScheduleCommand extends Command { + + public static final String COMMAND_WORD = "schedule"; + public static final String COMMAND_ALIAS = "sch"; + + public static final String MESSAGE_SCHEDULING_SUCCESS = "Meeting added"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Schedules a value with the person at the given " + + "index number. " + + "Existing values will be overwritten by new values. \n" + + "Parameters: INDEX (must be a positive integer)" + + "[" + PREFIX_MEETING + "MEETING TIME]\n" + + "Example: " + COMMAND_WORD + " 1 " + + PREFIX_MEETING + "31/12/18 1630"; + public static final String MESSAGE_NO_PREFIX = "No prefix " + PREFIX_MEETING + " detected."; + private final Index index; + private final Meeting meeting; + + public ScheduleCommand(Index index, Meeting meeting) { + requireNonNull(index); + requireNonNull(meeting); + this.index = index; + this.meeting = meeting; + } + + @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); + } + if (model.hasMeeting(meeting)) { + throw new CommandException(Messages.MESSAGE_CLASHING_MEETINGS); + } + + Person personToSchedule = lastShownList.get(index.getZeroBased()); + + if (!personToSchedule.getMeeting().value.equals(Meeting.NO_MEETING)) { + model.deleteMeeting(personToSchedule.getMeeting()); + } + if (!meeting.value.equals(Meeting.NO_MEETING)) { + model.addMeeting(meeting); + } + Person scheduledPerson = createScheduledPerson(personToSchedule, meeting); + model.updatePerson(personToSchedule, scheduledPerson); + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + model.commitAddressBook(); + return new CommandResult(String.format(MESSAGE_SCHEDULING_SUCCESS)); + } + + /** + * Creates and returns a {@code Person} with the details of {@code personToSchedule} + * updated with the value {@code value}. + */ + private static Person createScheduledPerson(Person personToSchedule, Meeting meeting) { + assert personToSchedule != null; + Name name = personToSchedule.getName(); + Optional phone = personToSchedule.getPhone(); + Optional email = personToSchedule.getEmail(); + Optional
address = personToSchedule.getAddress(); + Set tags = personToSchedule.getTags(); + Picture picture = personToSchedule.getPicture(); + return new Person(name, phone, email, address, tags, meeting, picture); + } +} diff --git a/src/main/java/seedu/address/logic/commands/SelectCommand.java b/src/main/java/seedu/address/logic/commands/SelectCommand.java index f5e8c1a8722e..159818a7b1b9 100644 --- a/src/main/java/seedu/address/logic/commands/SelectCommand.java +++ b/src/main/java/seedu/address/logic/commands/SelectCommand.java @@ -19,7 +19,7 @@ public class SelectCommand extends Command { public static final String COMMAND_WORD = "select"; - + public static final String COMMAND_ALIAS = "s"; 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" diff --git a/src/main/java/seedu/address/logic/commands/TagCommand.java b/src/main/java/seedu/address/logic/commands/TagCommand.java new file mode 100644 index 000000000000..3aabf32c8087 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/TagCommand.java @@ -0,0 +1,146 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Predicate; + +import seedu.address.commons.core.Messages; +import seedu.address.logic.CommandHistory; +import seedu.address.model.Model; +import seedu.address.model.person.Person; +import seedu.address.model.tag.PersonContainsTagPredicate; +import seedu.address.model.tag.Tag; + +//@@author A19Sean +/** + * Finds and lists all persons in address book whose tag matches the argument keyword. + * Keyword matching is case insensitive. + */ +public class TagCommand extends Command { + + public static final String COMMAND_WORD = "tag"; + public static final String COMMAND_ALIAS = "t"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": View, edit or delete all contacts with a specified (case-sensitive) tag.\n" + + "Parameters for view or delete functionality: TAG [MORE TAGS]... [delete]\n" + + "Parameters for edit functionality: edit EXISTING_TAG NEW_TAG\n" + + "Example: " + COMMAND_WORD + " Work Friends Important\n" + + "will list out all users with \"Work\", \"Friends\" or \"Important\" tags.\n" + + "Example: " + COMMAND_WORD + " Work Friends Important delete\n" + + "will delete all \"Work\", \"Friends\" and \"Important\" tags from contacts.\n" + + "Example: " + COMMAND_WORD + " edit Work Colleague\n" + + "will change the \"Work\" tag to \"Colleage\" for all tagged contacts."; + + /** + * Enums for the possible actions that can be performed from a tag command. + */ + public enum Action { + FIND, + DELETE, + EDIT + } + + private final PersonContainsTagPredicate predicate; + + private final Action action; + + private final List tags; + + public TagCommand(PersonContainsTagPredicate predicate, Action action, List tags) { + this.predicate = predicate; + this.action = action; + this.tags = tags; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) { + requireNonNull(model); + String message; + if (this.action == Action.DELETE) { + message = Messages.MESSAGE_TAG_DELETED_OVERVIEW; + model.updateFilteredPersonList(predicate); + List currentList = new ArrayList<>(model.getFilteredPersonList()); + List updatedList = new ArrayList<>(); + currentList.forEach(person -> { + Set personTags = new HashSet<>(person.getTags()); + for (String tag: tags) { + Tag tagToBeDeleted = new Tag(tag); + if (personTags.contains(tagToBeDeleted)) { + personTags.remove(tagToBeDeleted); + } + } + Person editedPerson = new Person(person.getName(), person.getPhone(), person.getEmail(), + person.getAddress(), personTags, person.getMeeting(), person.getPicture()); + updatedList.add(editedPerson); + model.updatePerson(person, editedPerson); + }); + model.updateFilteredPersonList(new PersonIsUntaggedPredicate(updatedList)); + model.commitAddressBook(); + } else if (this.action == Action.EDIT) { + message = Messages.MESSAGE_TAG_EDITED_OVERVIEW; + model.updateFilteredPersonList(predicate); + List currentList = new ArrayList<>(model.getFilteredPersonList()); + List updatedList = new ArrayList<>(); + currentList.forEach(person -> { + Set personTags = new HashSet<>(person.getTags()); + Tag tagToBeDeleted = new Tag(tags.get(0)); + Tag tagToBeAdded = new Tag(tags.get(1)); + if (personTags.contains(tagToBeDeleted)) { + personTags.remove(tagToBeDeleted); + personTags.add(tagToBeAdded); + } + Person editedPerson = new Person(person.getName(), person.getPhone(), person.getEmail(), + person.getAddress(), personTags, person.getMeeting(), person.getPicture()); + updatedList.add(editedPerson); + model.updatePerson(person, editedPerson); + }); + model.updateFilteredPersonList(new PersonIsUntaggedPredicate(updatedList)); + model.commitAddressBook(); + } else { + message = Messages.MESSAGE_TAGGED_PERSONS_LISTED_OVERVIEW; + model.updateFilteredPersonList(predicate); + } + if (this.action == Action.DELETE || this.action == Action.FIND) { + return new CommandResult(String.format(message, model.getFilteredPersonList().size())); + } else { + return new CommandResult(String.format(message, model.getFilteredPersonList().size(), tags.get(0), + tags.get(1))); + } + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof TagCommand // instanceof handles nulls + && predicate.equals(((TagCommand) other).predicate)); // state check + } + + /** + * Tests that a {@code Person} has just been untagged. + */ + private class PersonIsUntaggedPredicate implements Predicate { + private final List persons; + + private PersonIsUntaggedPredicate(List persons) { + this.persons = persons; + } + + @Override + public boolean test(Person person) { + return persons.stream() + .anyMatch(p -> p.equals(person)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof PersonIsUntaggedPredicate // instanceof handles nulls + && persons.equals(((PersonIsUntaggedPredicate) other).persons)); // state check + } + } +} diff --git a/src/main/java/seedu/address/logic/commands/UndoCommand.java b/src/main/java/seedu/address/logic/commands/UndoCommand.java index 40441264f346..e4c6b715b064 100644 --- a/src/main/java/seedu/address/logic/commands/UndoCommand.java +++ b/src/main/java/seedu/address/logic/commands/UndoCommand.java @@ -13,6 +13,7 @@ public class UndoCommand extends Command { public static final String COMMAND_WORD = "undo"; + public static final String COMMAND_ALIAS = "u"; public static final String MESSAGE_SUCCESS = "Undo success!"; public static final String MESSAGE_FAILURE = "No more commands to undo!"; diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddCommandParser.java index 3b8bfa035e83..594a888c3be9 100644 --- a/src/main/java/seedu/address/logic/parser/AddCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/AddCommandParser.java @@ -7,6 +7,7 @@ import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import java.util.Optional; import java.util.Set; import java.util.stream.Stream; @@ -33,15 +34,30 @@ public AddCommand parse(String args) throws ParseException { ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); - if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ADDRESS, PREFIX_PHONE, PREFIX_EMAIL) + if (!arePrefixesPresent(argMultimap, PREFIX_NAME) || !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()); + + //@@author zioul123 + Optional inputPhoneString = argMultimap.getValue(PREFIX_PHONE); + Optional phone = inputPhoneString.isPresent() + ? Optional.of(ParserUtil.parsePhone(inputPhoneString.get())) + : Optional.empty(); + + Optional inputEmailString = argMultimap.getValue(PREFIX_EMAIL); + Optional email = inputEmailString.isPresent() + ? Optional.of(ParserUtil.parseEmail(inputEmailString.get())) + : Optional.empty(); + + Optional inputAddressString = argMultimap.getValue(PREFIX_ADDRESS); + Optional
address = inputAddressString.isPresent() + ? Optional.of(ParserUtil.parseAddress(inputAddressString.get())) + : Optional.empty(); + //@@author + Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); Person person = new Person(name, phone, email, address, tagList); diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java index b7d57f5db86a..c666fa77ceba 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -12,12 +12,18 @@ 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.MeetingsCommand; +import seedu.address.logic.commands.PictureCommand; import seedu.address.logic.commands.RedoCommand; +import seedu.address.logic.commands.ScheduleCommand; import seedu.address.logic.commands.SelectCommand; +import seedu.address.logic.commands.TagCommand; import seedu.address.logic.commands.UndoCommand; import seedu.address.logic.parser.exceptions.ParseException; @@ -49,41 +55,81 @@ public Command parseCommand(String userInput) throws ParseException { switch (commandWord) { case AddCommand.COMMAND_WORD: + case AddCommand.COMMAND_ALIAS: return new AddCommandParser().parse(arguments); case EditCommand.COMMAND_WORD: + case EditCommand.COMMAND_ALIAS: return new EditCommandParser().parse(arguments); case SelectCommand.COMMAND_WORD: + case SelectCommand.COMMAND_ALIAS: return new SelectCommandParser().parse(arguments); case DeleteCommand.COMMAND_WORD: + case DeleteCommand.COMMAND_ALIAS: return new DeleteCommandParser().parse(arguments); case ClearCommand.COMMAND_WORD: + case ClearCommand.COMMAND_ALIAS: return new ClearCommand(); case FindCommand.COMMAND_WORD: + case FindCommand.COMMAND_ALIAS: return new FindCommandParser().parse(arguments); case ListCommand.COMMAND_WORD: + case ListCommand.COMMAND_ALIAS: return new ListCommand(); case HistoryCommand.COMMAND_WORD: + case HistoryCommand.COMMAND_ALIAS: return new HistoryCommand(); case ExitCommand.COMMAND_WORD: + case ExitCommand.COMMAND_ALIAS: return new ExitCommand(); + case ExportCommand.COMMAND_WORD: + case ExportCommand.COMMAND_ALIAS: + return new ExportCommandParser().parse(arguments); + case HelpCommand.COMMAND_WORD: + case HelpCommand.COMMAND_ALIAS: return new HelpCommand(); case UndoCommand.COMMAND_WORD: + case UndoCommand.COMMAND_ALIAS: return new UndoCommand(); case RedoCommand.COMMAND_WORD: + case RedoCommand.COMMAND_ALIAS: return new RedoCommand(); + case PictureCommand.COMMAND_WORD: + case PictureCommand.COMMAND_ALIAS: + return new PictureCommandParser().parse(arguments); + + //@@author chantca95 + case ImportCommand.COMMAND_WORD: + case ImportCommand.COMMAND_ALIAS: + return new ImportCommandParser().parse(arguments); + + //@@author + case ScheduleCommand.COMMAND_WORD: + case ScheduleCommand.COMMAND_ALIAS: + return new ScheduleCommandParser().parse(arguments); + + case MeetingsCommand.COMMAND_WORD: + case MeetingsCommand.COMMAND_ALIAS: + return new MeetingsCommandParser().parse(arguments); + + //@@author A19Sean + case TagCommand.COMMAND_WORD: + case TagCommand.COMMAND_ALIAS: + return new TagCommandParser().parse(arguments); + //@@author + default: throw new ParseException(MESSAGE_UNKNOWN_COMMAND); } diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java index 75b1a9bf1190..4f08dbe9d4df 100644 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ b/src/main/java/seedu/address/logic/parser/CliSyntax.java @@ -11,5 +11,7 @@ public class CliSyntax { public static final Prefix PREFIX_EMAIL = new Prefix("e/"); public static final Prefix PREFIX_ADDRESS = new Prefix("a/"); public static final Prefix PREFIX_TAG = new Prefix("t/"); + public static final Prefix PREFIX_FILE_LOCATION = new Prefix("l/"); + public static final Prefix PREFIX_MEETING = new Prefix("m/"); } diff --git a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java index 4d1f4bb0e4ec..9257407db71c 100644 --- a/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/DeleteCommandParser.java @@ -2,7 +2,12 @@ import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import java.util.Optional; + import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.StringUtil; +import seedu.address.logic.IndexParserUtil; +import seedu.address.logic.commands.DeleteByNameCommand; import seedu.address.logic.commands.DeleteCommand; import seedu.address.logic.parser.exceptions.ParseException; @@ -17,13 +22,23 @@ public class DeleteCommandParser implements Parser { * @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) { + //@@author zioul123 + // There must be an index or String provided + if (args.trim().isEmpty()) { throw new ParseException( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE), pe); + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE)); + } + + Optional index = IndexParserUtil.getIndex(args); // This method will trim args on its own + // Command provided an index but index was rejected + if (!index.isPresent() && StringUtil.isInteger(args.trim())) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE)); } - } + // Decide which command to return based on whether the user input was an index or a string + return index.map(DeleteCommand::new) + .orElseGet(() -> new DeleteByNameCommand(args)); + //@@author + } } diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/seedu/address/logic/parser/EditCommandParser.java index 845644b7dea1..55ca0b591708 100644 --- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/EditCommandParser.java @@ -2,6 +2,7 @@ import static java.util.Objects.requireNonNull; import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.commands.EditCommand.MESSAGE_USAGE; 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; @@ -14,6 +15,9 @@ import java.util.Set; import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.StringUtil; +import seedu.address.logic.IndexParserUtil; +import seedu.address.logic.commands.EditByNameCommand; import seedu.address.logic.commands.EditCommand; import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; import seedu.address.logic.parser.exceptions.ParseException; @@ -34,14 +38,6 @@ public EditCommand parse(String args) throws ParseException { 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())); @@ -61,7 +57,24 @@ public EditCommand parse(String args) throws ParseException { throw new ParseException(EditCommand.MESSAGE_NOT_EDITED); } - return new EditCommand(index, editPersonDescriptor); + String preamble = argMultimap.getPreamble(); + + //@@author zioul123 + // There must be either an index or string in the preamble + if (preamble.isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, MESSAGE_USAGE)); + } + + Optional index = IndexParserUtil.getIndex(preamble); + // The command used an index and not a string, but the index was rejected. + if (!index.isPresent() && StringUtil.isInteger(preamble)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, MESSAGE_USAGE)); + } + + // Decide which command to use based on whether the user input was an index or a string + return index.map(idx -> new EditCommand(idx, editPersonDescriptor)) + .orElseGet(() -> new EditByNameCommand(preamble, editPersonDescriptor)); + //@@author } /** @@ -78,5 +91,4 @@ private Optional> parseTagsForEdit(Collection tags) throws Pars 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/ExportCommandParser.java b/src/main/java/seedu/address/logic/parser/ExportCommandParser.java new file mode 100644 index 000000000000..f58c10e9a636 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ExportCommandParser.java @@ -0,0 +1,34 @@ +//@@author chantca95 +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import java.io.File; + +import seedu.address.logic.commands.ExportCommand; +import seedu.address.logic.parser.exceptions.ParseException; +/** + * Parses the user's given file name, then creates a new ExportCommand with the given file name + */ +public class ExportCommandParser implements Parser { + /** + * Checks if the user's given file name contains the .csv suffix, and displays an error message if it does not. + */ + public ExportCommand parse(String fileName) throws ParseException { + + int length = fileName.length(); + //checks whether the user's input ends with .csv + if (length < 5) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ExportCommand.MESSAGE_USAGE)); + } + if (!fileName.substring(length - 4, length).equals(".csv")) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ExportCommand.MESSAGE_USAGE)); + } + String trimmedFileName = fileName.trim(); + File temp = new File(trimmedFileName); + if (temp.exists()) { + throw new ParseException(ExportCommand.MESSAGE_DUPLICATE_FILE); + } + return new ExportCommand(trimmedFileName); + } +} diff --git a/src/main/java/seedu/address/logic/parser/FindCommandParser.java b/src/main/java/seedu/address/logic/parser/FindCommandParser.java index b186a967cb94..66622381a6b1 100644 --- a/src/main/java/seedu/address/logic/parser/FindCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/FindCommandParser.java @@ -29,5 +29,4 @@ public FindCommand parse(String args) throws ParseException { return new FindCommand(new NameContainsKeywordsPredicate(Arrays.asList(nameKeywords))); } - } 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..7be1c11ab6c2 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ImportCommandParser.java @@ -0,0 +1,190 @@ +//@@author chantca95 +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_FILE_LOCATION; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Optional; +import java.util.Set; + +import javafx.stage.FileChooser; +import javafx.stage.Stage; +import seedu.address.logic.commands.ImportCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.meeting.Meeting; +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; +/** + * Directs user to choose a file, then reads from the file. + * Prepares a list of Persons to add to the address book. + */ +public class ImportCommandParser implements Parser { + + private static final int NAME_FIELD = 0; + private static final int PHONE_FIELD = 1; + private static final int EMAIL_FIELD = 2; + private static final int ADDRESS_FIELD = 3; + private static final int MEETING_FIELD = 4; + private static final int TAG_FIELD_START = 5; + + private ArrayList persons; + + /** + * Creates a new ImportCommandParser with an empty ArrayList of Persons to be added. + */ + public ImportCommandParser() { + persons = new ArrayList<>(); + } + + /** + * Starts the import process by directing users to choose a file. + */ + @Override + public ImportCommand parse(String args) throws ParseException { + if (args.isEmpty()) { + return parseFile(getFileFromFileBrowser()); + } else { + return parseFile(getFileFromArgs(args)); + } + } + /** + * Parses the selected csv file pointed to by the user's input. + */ + private File getFileFromArgs(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_FILE_LOCATION); + if (!argMultimap.getValue(PREFIX_FILE_LOCATION).isPresent()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ImportCommand.MESSAGE_USAGE)); + } + Path fileLocation = ParserUtil.parseCsv(argMultimap.getValue(PREFIX_FILE_LOCATION).get()); + return fileLocation.toFile(); + } + + private File getFileFromFileBrowser() { + FileChooser fileChooser = new FileChooser(); + fileChooser.setTitle("Select .csv file"); + fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("CSV", "*.csv")); + fileChooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("TEXT", "*.txt")); + File file = fileChooser.showOpenDialog(new Stage()); + return file; + } + + /** + * Parses the selected csv file. + */ + public ImportCommand parseFile(File file) throws ParseException { + + FileReader fr; + + try { + fr = new FileReader(file); + } catch (FileNotFoundException fnfe) { + throw new ParseException("File not found"); + } + + BufferedReader br = new BufferedReader(fr); + return parseLinesFromFile(br); + } + /** + * Reads every row of the chosen csv file. + * Contacts with wrongly formatted fields and/or without Name fields are ignored. + */ + private ImportCommand parseLinesFromFile(BufferedReader br) { + boolean hasContactWithInvalidField = false; + boolean hasContactWithoutName = false; + try { + String line = br.readLine(); + while (line != null) { + String[] attributes = line.split(",", -1); + int numAttributes = attributes.length; + + if (attributes[NAME_FIELD].equalsIgnoreCase("Name") + || attributes[NAME_FIELD].equalsIgnoreCase("Name:")) { // ignore headers + line = br.readLine(); + continue; + } + + if (contactHasNoName(attributes, numAttributes)) { + hasContactWithoutName = true; + line = br.readLine(); + continue; + } + + Name name = null; + Optional phone = Optional.empty(); + Optional email = Optional.empty(); + Optional
address = Optional.empty(); + Meeting meeting = null; + + try { + name = ParserUtil.parseName(attributes[NAME_FIELD]); + if (!attributes[PHONE_FIELD].matches("")) { + phone = Optional.of(ParserUtil.parsePhone(attributes[PHONE_FIELD])); + } + if (!attributes[EMAIL_FIELD].matches("")) { + email = Optional.of(ParserUtil.parseEmail(attributes[EMAIL_FIELD])); + } + if (!attributes[ADDRESS_FIELD].matches("")) { + address = Optional.of(ParserUtil.parseAddress(attributes[ADDRESS_FIELD])); + } + if (!attributes[MEETING_FIELD].matches("")) { + meeting = ParserUtil.parseMeeting(attributes[MEETING_FIELD]); + } + } catch (ParseException pe) { + hasContactWithInvalidField = true; + line = br.readLine(); + continue; + } + + ArrayList tags = new ArrayList<>(); + //Check for tags + if (numAttributes > TAG_FIELD_START) { + for (int i = TAG_FIELD_START; i < numAttributes; i++) { + if (!attributes[i].matches("")) { + tags.add(attributes[i]); + } + } + } + + Set tagList = null; + try { + tagList = ParserUtil.parseTags(tags); + } catch (ParseException e) { + line = br.readLine(); + continue; + } + if (meeting == null) { + persons.add(new Person(name, phone, email, address, tagList)); + } else { + persons.add(new Person(name, phone, email, address, tagList, meeting)); + } + line = br.readLine(); + } + br.close(); + } catch (IOException ioe) { + ioe.printStackTrace(); + } + return new ImportCommand(persons, hasContactWithInvalidField, hasContactWithoutName); + } + + /** + this method checks if the compulsory name field is filled up. + */ + private boolean contactHasNoName(String[] attributes, int numAttributes) { + if (attributes[0].matches("")) { + return true; + } + return false; + } +} diff --git a/src/main/java/seedu/address/logic/parser/MeetingsCommandParser.java b/src/main/java/seedu/address/logic/parser/MeetingsCommandParser.java new file mode 100644 index 000000000000..1a890a002dea --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/MeetingsCommandParser.java @@ -0,0 +1,38 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.logic.commands.MeetingsCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.meeting.Meeting; +import seedu.address.model.meeting.SameMeetingDayPredicate; + +//@@author AyushChatto +/** + * Parses input arguments and creates a new MeetingsCommand object. + */ +public class MeetingsCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the MeetingsCommand + * and returns an MeetingsCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public MeetingsCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + Meeting meeting; + + if (trimmedArgs.isEmpty()) { + return new MeetingsCommand(new SameMeetingDayPredicate(new Meeting(Meeting.NO_MEETING))); + } + + String paddedArgs = args + "0000"; + + try { + meeting = ParserUtil.parseMeeting(paddedArgs); + return new MeetingsCommand(new SameMeetingDayPredicate(meeting)); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, MeetingsCommand.MESSAGE_USAGE), pe); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/Parser.java b/src/main/java/seedu/address/logic/parser/Parser.java index d6551ad8e3ff..344704d70e80 100644 --- a/src/main/java/seedu/address/logic/parser/Parser.java +++ b/src/main/java/seedu/address/logic/parser/Parser.java @@ -4,7 +4,7 @@ import seedu.address.logic.parser.exceptions.ParseException; /** - * Represents a Parser that is able to parse user input into a {@code Command} of type {@code T}. + * Represents a Parser that is able to parseFileFromArgs user input into a {@code Command} of type {@code T}. */ public interface Parser { diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java index 76daf40807e2..3a475f0aec85 100644 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java @@ -2,6 +2,8 @@ import static java.util.Objects.requireNonNull; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Collection; import java.util.HashSet; import java.util.Set; @@ -9,10 +11,12 @@ import seedu.address.commons.core.index.Index; import seedu.address.commons.util.StringUtil; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.meeting.Meeting; 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.person.Picture; import seedu.address.model.tag.Tag; /** @@ -95,6 +99,23 @@ public static Email parseEmail(String email) throws ParseException { return new Email(trimmedEmail); } + //@@author AyushChatto + /** + * Parses a {@code String meeting} into a {@code Meeting}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code value} is invalid. + */ + public static Meeting parseMeeting(String meeting) throws ParseException { + requireNonNull(meeting); + String trimmedMeeting = meeting.trim(); + trimmedMeeting = Meeting.formatMeeting(trimmedMeeting); + if (!Meeting.isValidMeeting(trimmedMeeting)) { + throw new ParseException(Meeting.MESSAGE_MEETING_CONSTRAINTS); + } + return new Meeting(trimmedMeeting); + } + /** * Parses a {@code String tag} into a {@code Tag}. * Leading and trailing whitespaces will be trimmed. @@ -121,4 +142,41 @@ public static Set parseTags(Collection tags) throws ParseException } return tagSet; } + + /** + * Parses a {@code String fileLocation} into a {@code Path}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code fileLocation} is invalid. + */ + public static Picture parseFileLocation(String fileLocation) throws ParseException { + requireNonNull(fileLocation); + String trimmedFileLocation = fileLocation.trim(); + + if (trimmedFileLocation.equals(Picture.DEFAULT_PICTURE_URL.getPath())) { + return new Picture(trimmedFileLocation); + } + + if (!Picture.isValidPicture(trimmedFileLocation)) { + if (Picture.isValidPictureInDirectory(trimmedFileLocation)) { + return new Picture(Picture.getDirectoryPath(trimmedFileLocation)); + } + // not valid and not in directory + throw new ParseException(Picture.MESSAGE_PICTURE_CONSTRAINTS); + } + return new Picture(Picture.getPath(trimmedFileLocation)); + } + + /** + * Parses a {@code String csvFileLocation} into a {@code Path}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code fileLocation} is invalid. + */ + public static Path parseCsv(String fileLocation) throws ParseException { + requireNonNull(fileLocation); + String trimmedFileLocation = fileLocation.trim(); + Path path = Paths.get(trimmedFileLocation); + return path; + } } diff --git a/src/main/java/seedu/address/logic/parser/PictureCommandParser.java b/src/main/java/seedu/address/logic/parser/PictureCommandParser.java new file mode 100644 index 000000000000..af7cc0fae74f --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/PictureCommandParser.java @@ -0,0 +1,43 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_FILE_LOCATION; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.PictureCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Picture; + +//@@author denzelchung +/** + * Parses input arguments and creates a new PictureCommand object + */ +public class PictureCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the PictureCommand + * and returns an PictureCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + @Override + public PictureCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_FILE_LOCATION); + + Index index; + + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, PictureCommand.MESSAGE_USAGE), pe); + } + + if (!argMultimap.getValue(PREFIX_FILE_LOCATION).isPresent()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, PictureCommand.MESSAGE_USAGE)); + } + + Picture picture = ParserUtil.parseFileLocation(argMultimap.getValue(PREFIX_FILE_LOCATION).get()); + + return new PictureCommand(index, picture); + } +} diff --git a/src/main/java/seedu/address/logic/parser/ScheduleCommandParser.java b/src/main/java/seedu/address/logic/parser/ScheduleCommandParser.java new file mode 100644 index 000000000000..4a32f34f6bdc --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ScheduleCommandParser.java @@ -0,0 +1,47 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MEETING; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.ScheduleCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.meeting.Meeting; + +//@@author AyushChatto +/** + * Parses input arguments and creates a new ScheduleCommand object + */ +public class ScheduleCommandParser 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 ScheduleCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_MEETING); + + Index index; + Meeting meeting; + + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ScheduleCommand.MESSAGE_USAGE), pe); + } + + try { + if (argMultimap.getValue(PREFIX_MEETING).isPresent()) { + meeting = ParserUtil.parseMeeting(argMultimap.getValue(PREFIX_MEETING).get()); + return new ScheduleCommand(index, meeting); + } else { + throw new ParseException(ScheduleCommand.MESSAGE_NO_PREFIX); + } + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ScheduleCommand.MESSAGE_USAGE), pe); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/TagCommandParser.java b/src/main/java/seedu/address/logic/parser/TagCommandParser.java new file mode 100644 index 000000000000..910680e1aaff --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/TagCommandParser.java @@ -0,0 +1,52 @@ +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.TagCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.tag.PersonContainsTagPredicate; + +//@@author A19Sean +/** + * Parses input arguments and creates a new TagCommand object + */ +public class TagCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the TagCommand + * and returns a TagCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public TagCommand parse(String args) throws ParseException { + String trimmedArgs = args.trim(); + String[] splitArgs = trimmedArgs.split("\\s+"); + if (trimmedArgs.isEmpty() || (splitArgs[0].equalsIgnoreCase("edit") && splitArgs.length != 3)) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, TagCommand.MESSAGE_USAGE)); + } + + TagCommand.Action action; + String[] tagKeywords; + if (splitArgs[splitArgs.length - 1].equalsIgnoreCase("delete")) { + action = TagCommand.Action.DELETE; + tagKeywords = Arrays.copyOfRange(splitArgs, 0, splitArgs.length - 1); + } else if (splitArgs[0].equalsIgnoreCase("edit")) { + action = TagCommand.Action.EDIT; + tagKeywords = Arrays.copyOfRange(splitArgs, 1, splitArgs.length); + } else { + action = TagCommand.Action.FIND; + tagKeywords = splitArgs; + } + + if (action == TagCommand.Action.DELETE || action == TagCommand.Action.FIND) { + return new TagCommand(new PersonContainsTagPredicate(Arrays.asList(tagKeywords)), action, + Arrays.asList(tagKeywords)); + } else { + return new TagCommand(new PersonContainsTagPredicate(Arrays.asList(tagKeywords[0])), action, + Arrays.asList(tagKeywords)); + } + } + +} diff --git a/src/main/java/seedu/address/logic/parser/exceptions/ParseException.java b/src/main/java/seedu/address/logic/parser/exceptions/ParseException.java index 158a1a54c1c5..20c15bd2e407 100644 --- a/src/main/java/seedu/address/logic/parser/exceptions/ParseException.java +++ b/src/main/java/seedu/address/logic/parser/exceptions/ParseException.java @@ -3,7 +3,7 @@ import seedu.address.commons.exceptions.IllegalValueException; /** - * Represents a parse error encountered by a parser. + * Represents a parseFileFromArgs error encountered by a parser. */ public class ParseException extends IllegalValueException { diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java index 7f85c8b9258b..883d96ba6cf7 100644 --- a/src/main/java/seedu/address/model/AddressBook.java +++ b/src/main/java/seedu/address/model/AddressBook.java @@ -5,6 +5,8 @@ import java.util.List; import javafx.collections.ObservableList; +import seedu.address.model.meeting.Meeting; +import seedu.address.model.meeting.UniqueMeetingList; import seedu.address.model.person.Person; import seedu.address.model.person.UniquePersonList; @@ -15,16 +17,18 @@ public class AddressBook implements ReadOnlyAddressBook { private final UniquePersonList persons; + private final UniqueMeetingList meetings; /* * 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 + * Note that non-static parse blocks are not recommended to use. There are other ways to avoid duplication * among constructors. */ { persons = new UniquePersonList(); + meetings = new UniqueMeetingList(); } public AddressBook() {} @@ -47,6 +51,14 @@ public void setPersons(List persons) { this.persons.setPersons(persons); } + /** + * Replaces the contents of the meetings list with {@code meetings}. + * {@code meetings} must not contain clashing meetings. + */ + public void setMeetings(List meetings) { + this.meetings.setMeetings(meetings); + } + /** * Resets the existing data of this {@code AddressBook} with {@code newData}. */ @@ -54,6 +66,10 @@ public void resetData(ReadOnlyAddressBook newData) { requireNonNull(newData); setPersons(newData.getPersonList()); + setMeetings(newData.getMeetingList()); + + + } //// person-level operations @@ -106,6 +122,11 @@ public ObservableList getPersonList() { return persons.asUnmodifiableObservableList(); } + @Override + public ObservableList getMeetingList() { + return meetings.asUnmodifiableObservableList(); + } + @Override public boolean equals(Object other) { return other == this // short circuit if same object @@ -117,4 +138,29 @@ public boolean equals(Object other) { public int hashCode() { return persons.hashCode(); } + + /** + * Returns true if a meeting with the same timing as {@code meeting} exists in the address book. + */ + public boolean hasMeeting(Meeting meeting) { + requireNonNull(meeting); + return meetings.contains(meeting); + } + + /** + * Removes {@code target} from this {@code AddressBook}. + * {@code target} must exist in the address book. + */ + public void removeMeeting(Meeting target) { + meetings.remove(target); + } + + + /** + * Adds a meeting to the address book. + * The meeting must not already exist in the address book. + */ + public void addMeeting(Meeting meeting) { + meetings.add(meeting); + } } diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index ac4521f33199..c1cc13b34d9b 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -3,6 +3,7 @@ import java.util.function.Predicate; import javafx.collections.ObservableList; +import seedu.address.model.meeting.Meeting; import seedu.address.model.person.Person; /** @@ -75,4 +76,21 @@ public interface Model { * Saves the current address book state for undo/redo. */ void commitAddressBook(); + + /** + * Returns true if a meeting with the same timing as {@code meeting} exists in the address book. + */ + boolean hasMeeting(Meeting meeting); + + /** + * Deletes the given meeting. + * The meeting must exist in the address book. + */ + void deleteMeeting(Meeting target); + + /** + * Adds the given meeting. + * {@code meeting} must not already exist in the address book. + */ + void addMeeting(Meeting meeting); } diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index a664602ef5b1..ec2e4cade5f6 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/seedu/address/model/ModelManager.java @@ -12,6 +12,7 @@ import seedu.address.commons.core.ComponentManager; import seedu.address.commons.core.LogsCenter; import seedu.address.commons.events.model.AddressBookChangedEvent; +import seedu.address.model.meeting.Meeting; import seedu.address.model.person.Person; /** @@ -100,6 +101,35 @@ public void updateFilteredPersonList(Predicate predicate) { filteredPersons.setPredicate(predicate); } + //=========== Meetings ================================================================================= + + /** + * Returns true if a meeting with the same timing as {@code meeting} exists in the address book. + */ + public boolean hasMeeting(Meeting meeting) { + requireNonNull(meeting); + return versionedAddressBook.hasMeeting(meeting); + } + + /** + * Deletes the given meeting. + * The meeting must exist in the address book. + */ + public void deleteMeeting(Meeting target) { + versionedAddressBook.removeMeeting(target); + indicateAddressBookChanged(); + } + + /** + * Adds the given meeting. + * {@code meeting} must not already exist in the address book. + */ + public void addMeeting(Meeting meeting) { + versionedAddressBook.addMeeting(meeting); + updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + indicateAddressBookChanged(); + } + //=========== Undo/Redo ================================================================================= @Override diff --git a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java index 6ddc2cd9a290..fddb47a2570d 100644 --- a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java +++ b/src/main/java/seedu/address/model/ReadOnlyAddressBook.java @@ -1,6 +1,7 @@ package seedu.address.model; import javafx.collections.ObservableList; +import seedu.address.model.meeting.Meeting; import seedu.address.model.person.Person; /** @@ -14,4 +15,10 @@ public interface ReadOnlyAddressBook { */ ObservableList getPersonList(); + /** + * Returns an unmodifiable view of the meetings list. + * This list will not contain any clashing meetings. + */ + ObservableList getMeetingList(); + } diff --git a/src/main/java/seedu/address/model/meeting/Meeting.java b/src/main/java/seedu/address/model/meeting/Meeting.java new file mode 100644 index 000000000000..05c5d506bfa6 --- /dev/null +++ b/src/main/java/seedu/address/model/meeting/Meeting.java @@ -0,0 +1,115 @@ +package seedu.address.model.meeting; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +import java.time.DateTimeException; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.time.format.ResolverStyle; + +//@@author AyushChatto +/** + * Represents a value that the user has scheduled with the client. + * Guarantees: immutable. + */ +public class Meeting { + + public static final String MESSAGE_MEETING_CONSTRAINTS = + "Meeting should only contain a date and time in DD/MM/YY HHMM format"; + + public static final String MEETING_VALIDATION_REGEX = "\\d{10}"; + public static final String NO_MEETING = "0000000000"; + public static final String NO_MEETING_MSG = "No meeting scheduled"; + + public final String value; + + /** + * Constructs a {@code Meeting}. + * + * @param meeting A valid value time. + */ + public Meeting(String meeting) { + requireNonNull(meeting); + checkArgument(isValidMeeting(meeting), MESSAGE_MEETING_CONSTRAINTS); + value = meeting; + } + + /** + * Formats meetings to meet the standard entry format by removing special characters + */ + public static String formatMeeting(String uneditedMeeting) { + String editedMeeting = ""; + for (int i = 0; i < uneditedMeeting.length(); i++) { + if (Character.isDigit(uneditedMeeting.charAt(i))) { + editedMeeting += uneditedMeeting.charAt(i); + } + } + return editedMeeting; + } + + /** + * Returns true if a given string is a valid value. + */ + public static boolean isValidMeeting(String test) { + if (test.equals(NO_MEETING)) { + return true; + } + String formattedTest = formatMeeting(test); + if (formattedTest.matches(MEETING_VALIDATION_REGEX)) { + try { + DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("ddMMuu") + .withResolverStyle(ResolverStyle.STRICT); + LocalDate.parse(formattedTest.substring(0, 6), dateFormatter); + DateTimeFormatter timeFormatter = DateTimeFormatter.ofPattern("HHmm") + .withResolverStyle(ResolverStyle.STRICT); + LocalTime.parse(formattedTest.substring(6, 10), timeFormatter); + return true; + } catch (DateTimeException e) { + return false; + } + } else { + return false; + } + } + + /** + * Returns a string representing the date of the meeting + */ + public String getDay() { + return value.substring(0, 6); + } + /** + * Returns true if {@code meeting} is on the same day. + */ + public boolean isSameDay(Meeting meeting) { + return getDay().equals(meeting.getDay()); + } + + @Override + public String toString() { + if (value.equals(NO_MEETING)) { + return NO_MEETING_MSG; + } + StringBuilder sb = new StringBuilder(); + sb.append(" " + value.substring(0, 2) + "/" + + value.substring(2, 4) + "/" + + value.substring(4, 6) + " at " + + value.substring(6, 8) + + value.substring(8, 10)); + return sb.toString(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Meeting // instanceof handles nulls + && value.equals(((Meeting) other).value)); // state check + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/meeting/SameMeetingDayPredicate.java b/src/main/java/seedu/address/model/meeting/SameMeetingDayPredicate.java new file mode 100644 index 000000000000..4d1c497133e2 --- /dev/null +++ b/src/main/java/seedu/address/model/meeting/SameMeetingDayPredicate.java @@ -0,0 +1,27 @@ +package seedu.address.model.meeting; + +import java.util.function.Predicate; + +import seedu.address.model.person.Person; + +//@@author AyushChatto +/** + * Tests that a {@code Person}'s {@code Meeting} is on the same day as the meeting given. + */ +public class SameMeetingDayPredicate implements Predicate { + + private final Meeting meeting; + + public SameMeetingDayPredicate(Meeting meeting) { + this.meeting = meeting; + } + + @Override + public boolean test(Person person) { + if (meeting.value.equals(Meeting.NO_MEETING)) { + return !person.getMeeting().value.equals(Meeting.NO_MEETING); + } else { + return meeting.isSameDay(person.getMeeting()); + } + } +} diff --git a/src/main/java/seedu/address/model/meeting/UniqueMeetingList.java b/src/main/java/seedu/address/model/meeting/UniqueMeetingList.java new file mode 100644 index 000000000000..b0a52eabe400 --- /dev/null +++ b/src/main/java/seedu/address/model/meeting/UniqueMeetingList.java @@ -0,0 +1,107 @@ +package seedu.address.model.meeting; + +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.meeting.exceptions.DuplicateMeetingException; +import seedu.address.model.meeting.exceptions.MeetingNotFoundException; + +//@@author AyushChatto +/** + * A list of meetings that enforces uniqueness between its elements and does not allow nulls. + */ +public class UniqueMeetingList implements Iterable { + private final ObservableList allMeetings = FXCollections.observableArrayList(); + + /** + * Returns true if the list contains an equivalent value as the given argument. + */ + public boolean contains(Meeting toCheck) { + requireNonNull(toCheck); + return allMeetings.stream().anyMatch(toCheck::equals); + } + + /** + * Adds a value to the list. + * No value at the same time can exist in the list. + */ + public void add(Meeting toAdd) { + requireNonNull(toAdd); + if (contains(toAdd)) { + throw new DuplicateMeetingException(); + } + allMeetings.add(toAdd); + } + + /** + * Replaces the value {@code target} in the list with {@code editedMeeting}. + * {@code target} must exist in the list. + */ + public void setMeeting(Meeting target, Meeting editedMeeting) { + requireAllNonNull(target, editedMeeting); + + int index = allMeetings.indexOf(target); + if (index == -1) { + throw new MeetingNotFoundException(); + } + + allMeetings.set(index, editedMeeting); + } + + /** + * Replaces the contents of this list with {@code meetings}. + * {@code meetings} must not contain duplicate meetings. + */ + public void setMeetings(List meetings) { + requireAllNonNull(meetings); + if (!meetingsAreUnique(meetings)) { + throw new DuplicateMeetingException(); + } + + allMeetings.setAll(meetings); + } + + + /** + * Removes the equivalent value from the list. + * The value must exist in the list. + */ + public void remove(Meeting toRemove) { + requireNonNull(toRemove); + if (!allMeetings.remove(toRemove)) { + throw new MeetingNotFoundException(); + } + } + + @Override + public Iterator iterator() { + return allMeetings.iterator(); + } + + /** + * Returns the backing list as an unmodifiable {@code ObservableList}. + */ + public ObservableList asUnmodifiableObservableList() { + return FXCollections.unmodifiableObservableList(allMeetings); + } + + /** + * Returns true if {@code meetings} contains only unique meetings. + */ + private boolean meetingsAreUnique(List meetings) { + for (int i = 0; i < meetings.size() - 1; i++) { + for (int j = i + 1; j < meetings.size(); j++) { + if (meetings.get(i).equals(meetings.get(j))) { + return false; + } + } + } + return true; + } +} diff --git a/src/main/java/seedu/address/model/meeting/exceptions/DuplicateMeetingException.java b/src/main/java/seedu/address/model/meeting/exceptions/DuplicateMeetingException.java new file mode 100644 index 000000000000..eb221b5a726b --- /dev/null +++ b/src/main/java/seedu/address/model/meeting/exceptions/DuplicateMeetingException.java @@ -0,0 +1,12 @@ +package seedu.address.model.meeting.exceptions; + +//@@author AyushChatto +/** + * Signals that the operation will result in duplicate Meetings (Meetings are considered duplicte if they are at the + * same time). + */ +public class DuplicateMeetingException extends RuntimeException { + public DuplicateMeetingException() { + super("There is already a value at the given time"); + } +} diff --git a/src/main/java/seedu/address/model/meeting/exceptions/MeetingNotFoundException.java b/src/main/java/seedu/address/model/meeting/exceptions/MeetingNotFoundException.java new file mode 100644 index 000000000000..05c6fb50e350 --- /dev/null +++ b/src/main/java/seedu/address/model/meeting/exceptions/MeetingNotFoundException.java @@ -0,0 +1,8 @@ +package seedu.address.model.meeting.exceptions; + +//@@author AyushChatto +/** + * Signals that the operation is unable to find the specified value. + */ +public class MeetingNotFoundException extends RuntimeException { +} diff --git a/src/main/java/seedu/address/model/person/Address.java b/src/main/java/seedu/address/model/person/Address.java index a1409233ceb9..791094da524a 100644 --- a/src/main/java/seedu/address/model/person/Address.java +++ b/src/main/java/seedu/address/model/person/Address.java @@ -9,6 +9,7 @@ */ public class Address { + public static final String NO_ADDRESS = "NO_ADDRESS"; public static final String MESSAGE_ADDRESS_CONSTRAINTS = "Addresses can take any values, and it should not be blank"; diff --git a/src/main/java/seedu/address/model/person/Email.java b/src/main/java/seedu/address/model/person/Email.java index 38a7629e9a2d..9cd1a860d168 100644 --- a/src/main/java/seedu/address/model/person/Email.java +++ b/src/main/java/seedu/address/model/person/Email.java @@ -9,6 +9,7 @@ */ public class Email { + public static final String NO_EMAIL = "NOEMAIL@NOEMAIL.NOEMAIL"; private static final String SPECIAL_CHARACTERS = "!#$%&'*+/=?`{|}~^.-"; public static final String MESSAGE_EMAIL_CONSTRAINTS = "Emails should be of the format local-part@domain " + "and adhere to the following constraints:\n" diff --git a/src/main/java/seedu/address/model/person/NameContainsAllKeywordsPredicate.java b/src/main/java/seedu/address/model/person/NameContainsAllKeywordsPredicate.java new file mode 100644 index 000000000000..3d0d5d852c59 --- /dev/null +++ b/src/main/java/seedu/address/model/person/NameContainsAllKeywordsPredicate.java @@ -0,0 +1,31 @@ +package seedu.address.model.person; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.StringUtil; + +//@@author zioul123 +/** + * Tests that a {@code Person}'s {@code Name} matches all of the keywords given. + */ +public class NameContainsAllKeywordsPredicate implements Predicate { + private final List keywords; + + public NameContainsAllKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Person person) { + return keywords.stream() + .allMatch(keyword -> StringUtil.containsWordIgnoreCase(person.getName().fullName, keyword)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof NameContainsAllKeywordsPredicate // instanceof handles nulls + && keywords.equals(((NameContainsAllKeywordsPredicate) 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 index 557a7a60cd51..295a394d88e2 100644 --- a/src/main/java/seedu/address/model/person/Person.java +++ b/src/main/java/seedu/address/model/person/Person.java @@ -5,53 +5,112 @@ import java.util.Collections; import java.util.HashSet; import java.util.Objects; +import java.util.Optional; import java.util.Set; +import seedu.address.model.meeting.Meeting; 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. + * Guarantees: details are present and not null, but details other than name are optional, field values are + * validated, immutable. Only the picture field is mutable as users are able to change the profile picture. */ public class Person { // Identity fields private final Name name; - private final Phone phone; - private final Email email; + private final Optional phone; + private final Optional email; // Data fields - private final Address address; + private final Optional
address; private final Set tags = new HashSet<>(); + private Picture picture; + + private final Meeting meeting; /** * Every field must be present and not null. */ - public Person(Name name, Phone phone, Email email, Address address, Set tags) { + public Person(Name name, Optional phone, Optional email, Optional
address, Set tags) { requireAllNonNull(name, phone, email, address, tags); this.name = name; this.phone = phone; this.email = email; this.address = address; + this.picture = new Picture(Picture.DEFAULT_PICTURE_URL.getPath()); + this.tags.addAll(tags); + this.meeting = new Meeting(Meeting.NO_MEETING); + } + + //@@author AyushChatto + /** + * Constructor for scheduling a value. Not to be used for creating a new entry in the + * address book. + */ + public Person(Name name, Optional phone, Optional email, Optional
address, + Set tags, Meeting meeting) { + requireAllNonNull(name, phone, email, address, tags, meeting); + this.name = name; + this.phone = phone; + this.email = email; + this.address = address; + this.picture = new Picture(Picture.DEFAULT_PICTURE_URL.getPath()); + this.tags.addAll(tags); + this.meeting = meeting; + } + + //@@author AyushChatto + /** + * Constructor for retaining pictures. + */ + public Person(Name name, Optional phone, Optional email, Optional
address, + Set tags, Meeting meeting, Picture picture) { + requireAllNonNull(name, phone, email, address, tags, meeting); + this.name = name; + this.phone = phone; + this.email = email; + this.address = address; + this.picture = picture; this.tags.addAll(tags); + this.meeting = meeting; } public Name getName() { + + + return name; } - public Phone getPhone() { + public Optional getPhone() { return phone; } - public Email getEmail() { + public Optional getEmail() { return email; } - public Address getAddress() { + public Optional
getAddress() { return address; } + //@@author AyushChatto + public Meeting getMeeting() { + return meeting; + } + + //@@author denzelchung + public Picture getPicture() { + return picture; + } + + public void setPicture(Picture picture) { + this.picture = picture; + } + + //@@author /** * Returns an immutable tag set, which throws {@code UnsupportedOperationException} * if modification is attempted. @@ -69,9 +128,19 @@ public boolean isSamePerson(Person otherPerson) { return true; } - return otherPerson != null - && otherPerson.getName().equals(getName()) - && (otherPerson.getPhone().equals(getPhone()) || otherPerson.getEmail().equals(getEmail())); + //@@author zioul123 + // The other person must exist and have the same name to be the same person + if (otherPerson == null || !otherPerson.getName().equals(getName())) { + return false; + } + + boolean bothHavePhone = getPhone().isPresent() && otherPerson.getPhone().isPresent(); + boolean bothHaveEmail = getEmail().isPresent() && otherPerson.getEmail().isPresent(); + + // Do not compare fields unless they are present + return ((bothHavePhone && otherPerson.getPhone().equals(getPhone())) + || (bothHaveEmail && otherPerson.getEmail().equals(getEmail()))); + //@@author } /** @@ -93,26 +162,37 @@ public boolean equals(Object other) { && otherPerson.getPhone().equals(getPhone()) && otherPerson.getEmail().equals(getEmail()) && otherPerson.getAddress().equals(getAddress()) - && otherPerson.getTags().equals(getTags()); + && otherPerson.getTags().equals(getTags()) + && otherPerson.getMeeting().equals(getMeeting()); } @Override public int hashCode() { // use this method for custom fields hashing instead of implementing your own - return Objects.hash(name, phone, email, address, tags); + return Objects.hash(name, phone, email, address, tags, picture); } @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: "); + builder.append(getName()); + + builder.append(" Phone: "); + getPhone().ifPresentOrElse(builder::append, () -> builder.append("None")); + + builder.append(" Email: "); + getEmail().ifPresentOrElse(builder::append, () -> builder.append("None")); + + builder.append(" Address: "); + getAddress().ifPresentOrElse(builder::append, () -> builder.append("None")); + + builder.append(" Meeting: ") + .append(getMeeting().toString()); + + builder.append(" Picture: ") + .append(getPicture()); + + builder.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 index a22e51653835..bc660e5cc00f 100644 --- a/src/main/java/seedu/address/model/person/Phone.java +++ b/src/main/java/seedu/address/model/person/Phone.java @@ -9,7 +9,7 @@ */ public class Phone { - + public static final String NO_PHONE = "000000000000"; public static final String MESSAGE_PHONE_CONSTRAINTS = "Phone numbers should only contain numbers, and it should be at least 3 digits long"; public static final String PHONE_VALIDATION_REGEX = "\\d{3,}"; diff --git a/src/main/java/seedu/address/model/person/Picture.java b/src/main/java/seedu/address/model/person/Picture.java new file mode 100644 index 000000000000..8164ea5eb3b7 --- /dev/null +++ b/src/main/java/seedu/address/model/person/Picture.java @@ -0,0 +1,119 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; + +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.InvalidPathException; +import java.nio.file.Path; +import java.nio.file.Paths; + +import seedu.address.logic.parser.exceptions.ParseException; + +//@@author denzelchung +/** + * Represents a Person's picture in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidPicture(String)} + */ +public class Picture { + + // https://www.stubbornjava.com/posts/reading-file-resources-with-guava + public static final URL DEFAULT_PICTURE_URL = com.google.common.io.Resources + .getResource("images/placeholder_image.jpg"); + public static final String MESSAGE_PICTURE_CONSTRAINTS = + "Picture should be a valid file path"; + + /* + * Regular expression validation for path. + */ + public static final String PICTURE_PATH_WIN_VALIDATION_REGEX = + "([a-zA-Z]:)?((\\\\|/)[a-zA-Z0-9_.-]+)+(\\\\|/)?"; + public static final String DEFAULT_PATH_WIN_VALIDATION_REGEX = + "/" + PICTURE_PATH_WIN_VALIDATION_REGEX; + public static final String PICTURE_PATH_MAC_VALIDATION_REGEX = + "^((?!.*//.*)(?!.*/ .*)/{1}([^\\\\(){}:\\*\\?<>\\|\\\"\\'])+\\.(jpg|png))$"; + + public final String picture; + + /** + * Constructs an {@code Picture}. + * + * @param picture A valid picture. + */ + public Picture(String picture) { + requireNonNull(picture); + + if (!isValidPicture(picture)) { + this.picture = DEFAULT_PICTURE_URL.getPath(); + } else { + this.picture = picture; + } + } + + /** + * Returns the current directory concatenated with the given path. + */ + public static String getDirectoryPath(String path) { + return Paths.get(System.getProperty("user.dir") + "/" + path).toString(); + } + + /** + * Returns the path generated by the Path class. + * Removes additional slashes '/' in the given unformattedPath. + */ + public static String getPath(String unformattedPath) throws ParseException { + try { + Path path = Paths.get(unformattedPath); + return path.toString(); + } catch (InvalidPathException ipe) { + throw new ParseException(Picture.MESSAGE_PICTURE_CONSTRAINTS); + } + } + + /** + * Returns true if a given string is a valid picture. + */ + public static boolean isValidPicture(String test) { + if (test.equals(DEFAULT_PICTURE_URL.getPath())) { + return true; + } + + if (test.matches(DEFAULT_PATH_WIN_VALIDATION_REGEX)) { + test = test.substring(1, test.length()); + } + + return ((test.endsWith(".jpg") || test.endsWith(".png")) + && (Files.exists(Paths.get(test)))); + } + + /** + * Returns true if a given string is a valid picture in the current directory. + */ + public static boolean isValidPictureInDirectory(String test) throws ParseException { + try { + String directoryPath = getDirectoryPath(test); + Path path = Paths.get(directoryPath); + return Files.exists(path); + } catch (InvalidPathException ipe) { + throw new ParseException(Picture.MESSAGE_PICTURE_CONSTRAINTS); + } + } + + @Override + public String toString() { + return picture; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Picture // instanceof handles nulls + && picture.equals(((Picture) other).picture)); // state check + } + + @Override + public int hashCode() { + return picture.hashCode(); + } + +} diff --git a/src/main/java/seedu/address/model/tag/PersonContainsTagPredicate.java b/src/main/java/seedu/address/model/tag/PersonContainsTagPredicate.java new file mode 100644 index 000000000000..8027700df7a7 --- /dev/null +++ b/src/main/java/seedu/address/model/tag/PersonContainsTagPredicate.java @@ -0,0 +1,35 @@ +package seedu.address.model.tag; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.model.person.Person; + +//@@author A19Sean +/** + * Tests that a {@code Person} contains any of the tags given. + */ +public class PersonContainsTagPredicate implements Predicate { + private final List tags; + + public PersonContainsTagPredicate(List tags) { + this.tags = tags; + } + + @Override + public boolean test(Person person) { + return tags.stream() + .anyMatch(tagName -> { + Tag tag = new Tag(tagName); + return person.getTags().contains(tag); + }); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof PersonContainsTagPredicate // instanceof handles nulls + && tags.equals(((PersonContainsTagPredicate) other).tags)); // state check + } + +} diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java index 1806da4facfa..87d12a604be8 100644 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ b/src/main/java/seedu/address/model/util/SampleDataUtil.java @@ -1,6 +1,7 @@ package seedu.address.model.util; import java.util.Arrays; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -19,23 +20,28 @@ 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"), + new Person(new Name("Alex Yeoh"), Optional.of(new Phone("87438807")), + Optional.of(new Email("alexyeoh@example.com")), + Optional.of(new Address("Blk 30 Geylang Street 29, #06-40")), getTagSet("friends")), + new Person(new Name("Bernice Yu"), Optional.of(new Phone("99272758")), + Optional.of(new Email("berniceyu@example.com")), + Optional.of(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"), + new Person(new Name("Charlotte Oliveiro"), Optional.of(new Phone("93210283")), + Optional.of(new Email("charlotte@example.com")), + Optional.of(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"), + new Person(new Name("David Li"), Optional.of(new Phone("91031282")), + Optional.of(new Email("lidavid@example.com")), + Optional.of(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"), + new Person(new Name("Irfan Ibrahim"), Optional.of(new Phone("92492021")), + Optional.of(new Email("irfan@example.com")), + Optional.of(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"), + new Person(new Name("Roy Balakrishnan"), Optional.of(new Phone("92624417")), + Optional.of(new Email("royb@example.com")), + Optional.of(new Address("Blk 45 Aljunied Street 85, #11-31")), getTagSet("colleagues")) }; } diff --git a/src/main/java/seedu/address/storage/AddressBookStorage.java b/src/main/java/seedu/address/storage/AddressBookStorage.java index 4599182b3f92..ec7157e44210 100644 --- a/src/main/java/seedu/address/storage/AddressBookStorage.java +++ b/src/main/java/seedu/address/storage/AddressBookStorage.java @@ -42,4 +42,10 @@ public interface AddressBookStorage { */ void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) throws IOException; + /** + * Saves the address book in a fixed temporary location. + * @param addressBook cannot be null. + * @throws IOException if there was any problem writing to the file. + */ + void backupAddressBook(ReadOnlyAddressBook addressBook) throws IOException; } diff --git a/src/main/java/seedu/address/storage/StorageManager.java b/src/main/java/seedu/address/storage/StorageManager.java index b0df908a76a7..165d5029c8f9 100644 --- a/src/main/java/seedu/address/storage/StorageManager.java +++ b/src/main/java/seedu/address/storage/StorageManager.java @@ -90,4 +90,8 @@ public void handleAddressBookChangedEvent(AddressBookChangedEvent event) { } } + @Override + public void backupAddressBook(ReadOnlyAddressBook addressBook) throws IOException { + addressBookStorage.backupAddressBook(addressBook); + } } diff --git a/src/main/java/seedu/address/storage/XmlAdaptedMeeting.java b/src/main/java/seedu/address/storage/XmlAdaptedMeeting.java new file mode 100644 index 000000000000..bbc534ba407f --- /dev/null +++ b/src/main/java/seedu/address/storage/XmlAdaptedMeeting.java @@ -0,0 +1,50 @@ +package seedu.address.storage; + +import javax.xml.bind.annotation.XmlElement; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.meeting.Meeting; + +/** + * JAXB-friendly version of the Meeting. + */ +public class XmlAdaptedMeeting { + @XmlElement(required = true) + private String meeting; + + /** + * Constructs an XmlAdaptedMeeting. + * This is the no-arg constructor that is required by JAXB. + */ + public XmlAdaptedMeeting() {} + + /** + * Constructs a {@code XmlAdaptedMeeting} with the given {@code meeting}. + */ + public XmlAdaptedMeeting(String meeting) { + this.meeting = meeting; + } + + /** + * Converts a given Meeting into this class for JAXB use. + * + * @param source future changes to this will not affect the created + */ + public XmlAdaptedMeeting(Meeting source) { + meeting = source.value; + } + + /** + * Converts this jaxb-friendly adapted meeting object into the model's Meeting object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted meeting + */ + public Meeting toModelType() throws IllegalValueException { + if (!Meeting.isValidMeeting(meeting)) { + throw new IllegalValueException(Meeting.MESSAGE_MEETING_CONSTRAINTS); + } + return new Meeting(meeting); + } + + +} diff --git a/src/main/java/seedu/address/storage/XmlAdaptedPerson.java b/src/main/java/seedu/address/storage/XmlAdaptedPerson.java index c03785e5700f..c9629d46c83b 100644 --- a/src/main/java/seedu/address/storage/XmlAdaptedPerson.java +++ b/src/main/java/seedu/address/storage/XmlAdaptedPerson.java @@ -4,17 +4,20 @@ import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; import javax.xml.bind.annotation.XmlElement; import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.meeting.Meeting; 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.person.Picture; import seedu.address.model.tag.Tag; /** @@ -32,6 +35,10 @@ public class XmlAdaptedPerson { private String email; @XmlElement(required = true) private String address; + @XmlElement(required = true) + private String meeting; + @XmlElement(required = true) + private String picture; @XmlElement private List tagged = new ArrayList<>(); @@ -62,12 +69,14 @@ public XmlAdaptedPerson(String name, String phone, String email, String address, */ public XmlAdaptedPerson(Person source) { name = source.getName().fullName; - phone = source.getPhone().value; - email = source.getEmail().value; - address = source.getAddress().value; + phone = source.getPhone().isPresent() ? source.getPhone().get().value : Phone.NO_PHONE; + email = source.getEmail().isPresent() ? source.getEmail().get().value : Email.NO_EMAIL; + address = source.getAddress().isPresent() ? source.getAddress().get().value : Address.NO_ADDRESS; tagged = source.getTags().stream() .map(XmlAdaptedTag::new) .collect(Collectors.toList()); + meeting = source.getMeeting().value; + picture = source.getPicture().picture; } /** @@ -95,7 +104,8 @@ public Person toModelType() throws IllegalValueException { if (!Phone.isValidPhone(phone)) { throw new IllegalValueException(Phone.MESSAGE_PHONE_CONSTRAINTS); } - final Phone modelPhone = new Phone(phone); + final Optional modelPhone = Optional.ofNullable( + phone.equals(Phone.NO_PHONE) ? null : new Phone(phone)); if (email == null) { throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Email.class.getSimpleName())); @@ -103,7 +113,8 @@ public Person toModelType() throws IllegalValueException { if (!Email.isValidEmail(email)) { throw new IllegalValueException(Email.MESSAGE_EMAIL_CONSTRAINTS); } - final Email modelEmail = new Email(email); + final Optional modelEmail = Optional.ofNullable( + email.equals(Email.NO_EMAIL) ? null : new Email(email)); if (address == null) { throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Address.class.getSimpleName())); @@ -111,10 +122,27 @@ public Person toModelType() throws IllegalValueException { if (!Address.isValidAddress(address)) { throw new IllegalValueException(Address.MESSAGE_ADDRESS_CONSTRAINTS); } - final Address modelAddress = new Address(address); + final Optional
modelAddress = Optional.ofNullable( + address.equals(Address.NO_ADDRESS) ? null : new Address(address)); + + if (meeting == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Meeting.class.getSimpleName())); + } + if (!Meeting.isValidMeeting(meeting)) { + throw new IllegalValueException(Meeting.MESSAGE_MEETING_CONSTRAINTS); + } + final Meeting modelMeeting = new Meeting(meeting); + + if (picture == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Picture.class.getSimpleName())); + } + if (!Picture.isValidPicture(picture)) { + picture = Picture.DEFAULT_PICTURE_URL.getPath(); + } + final Picture modelPicture = new Picture(picture); final Set modelTags = new HashSet<>(personTags); - return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags); + return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags, modelMeeting, modelPicture); } @Override @@ -132,6 +160,7 @@ public boolean equals(Object other) { && Objects.equals(phone, otherPerson.phone) && Objects.equals(email, otherPerson.email) && Objects.equals(address, otherPerson.address) + && Objects.equals(picture, otherPerson.picture) && tagged.equals(otherPerson.tagged); } } diff --git a/src/main/java/seedu/address/storage/XmlAddressBookStorage.java b/src/main/java/seedu/address/storage/XmlAddressBookStorage.java index ecf0e7ec23a8..f140015955f9 100644 --- a/src/main/java/seedu/address/storage/XmlAddressBookStorage.java +++ b/src/main/java/seedu/address/storage/XmlAddressBookStorage.java @@ -6,6 +6,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Optional; import java.util.logging.Logger; @@ -23,9 +24,11 @@ public class XmlAddressBookStorage implements AddressBookStorage { private static final Logger logger = LogsCenter.getLogger(XmlAddressBookStorage.class); private Path filePath; + private Path backupFilePath; public XmlAddressBookStorage(Path filePath) { this.filePath = filePath; + backupFilePath = Paths.get(filePath.toString(), ".backup"); } public Path getAddressBookFilePath() { @@ -77,4 +80,8 @@ public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) thro XmlFileStorage.saveDataToFile(filePath, new XmlSerializableAddressBook(addressBook)); } + @Override + public void backupAddressBook(ReadOnlyAddressBook addressBook) throws IOException { + saveAddressBook(addressBook, backupFilePath); + } } diff --git a/src/main/java/seedu/address/storage/XmlSerializableAddressBook.java b/src/main/java/seedu/address/storage/XmlSerializableAddressBook.java index b85fa4a8f07e..ed07d9337320 100644 --- a/src/main/java/seedu/address/storage/XmlSerializableAddressBook.java +++ b/src/main/java/seedu/address/storage/XmlSerializableAddressBook.java @@ -10,6 +10,7 @@ import seedu.address.commons.exceptions.IllegalValueException; import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.meeting.Meeting; import seedu.address.model.person.Person; /** @@ -19,16 +20,21 @@ public class XmlSerializableAddressBook { public static final String MESSAGE_DUPLICATE_PERSON = "Persons list contains duplicate person(s)."; + public static final String MESSAGE_CLASHING_MEETINGS = "Meeting list contains clashing meeting(s)"; @XmlElement private List persons; + @XmlElement + private List meetings; + /** * Creates an empty XmlSerializableAddressBook. * This empty constructor is required for marshalling. */ public XmlSerializableAddressBook() { persons = new ArrayList<>(); + meetings = new ArrayList<>(); } /** @@ -37,6 +43,7 @@ public XmlSerializableAddressBook() { public XmlSerializableAddressBook(ReadOnlyAddressBook src) { this(); persons.addAll(src.getPersonList().stream().map(XmlAdaptedPerson::new).collect(Collectors.toList())); + meetings.addAll(src.getMeetingList().stream().map(XmlAdaptedMeeting::new).collect(Collectors.toList())); } /** @@ -54,6 +61,13 @@ public AddressBook toModelType() throws IllegalValueException { } addressBook.addPerson(person); } + for (XmlAdaptedMeeting m : meetings) { + Meeting meeting = m.toModelType(); + if (addressBook.hasMeeting(meeting)) { + throw new IllegalValueException(MESSAGE_CLASHING_MEETINGS); + } + addressBook.addMeeting(meeting); + } return addressBook; } diff --git a/src/main/java/seedu/address/ui/BrowserPanel.java b/src/main/java/seedu/address/ui/BrowserPanel.java index b43de90a2b9f..6ebbe92e5e4d 100644 --- a/src/main/java/seedu/address/ui/BrowserPanel.java +++ b/src/main/java/seedu/address/ui/BrowserPanel.java @@ -21,8 +21,6 @@ public class BrowserPanel extends UiPart { public static final String DEFAULT_PAGE = "default.html"; - public static final String SEARCH_PAGE_URL = - "https://se-edu.github.io/addressbook-level4/DummySearchPage.html?name="; private static final String FXML = "BrowserPanel.fxml"; @@ -42,7 +40,8 @@ public BrowserPanel() { } private void loadPersonPage(Person person) { - loadPage(SEARCH_PAGE_URL + person.getName().fullName); + // Load the default page, because PersonPages are not implemented yet. + loadDefaultPage(); } public void loadPage(String url) { diff --git a/src/main/java/seedu/address/ui/CommandBox.java b/src/main/java/seedu/address/ui/CommandBox.java index 3d7aaded5640..eb931ecaa22f 100644 --- a/src/main/java/seedu/address/ui/CommandBox.java +++ b/src/main/java/seedu/address/ui/CommandBox.java @@ -107,14 +107,14 @@ private void handleCommandEntered() { // process result of the command commandTextField.setText(""); logger.info("Result: " + commandResult.feedbackToUser); - raise(new NewResultAvailableEvent(commandResult.feedbackToUser)); + raise(new NewResultAvailableEvent(commandResult.feedbackToUser, true)); } catch (CommandException | ParseException e) { initHistory(); // handle command failure setStyleToIndicateCommandFailure(); logger.info("Invalid command: " + commandTextField.getText()); - raise(new NewResultAvailableEvent(e.getMessage())); + raise(new NewResultAvailableEvent(e.getMessage(), false)); } } diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java index 0e361a4d7baf..75d5b3cbabb5 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/address/ui/MainWindow.java @@ -128,7 +128,8 @@ void fillInnerParts() { ResultDisplay resultDisplay = new ResultDisplay(); resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot()); - StatusBarFooter statusBarFooter = new StatusBarFooter(prefs.getAddressBookFilePath()); + StatusBarFooter statusBarFooter = new StatusBarFooter(prefs.getAddressBookFilePath(), + logic.getFilteredPersonList().size()); statusbarPlaceholder.getChildren().add(statusBarFooter.getRoot()); CommandBox commandBox = new CommandBox(logic); diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java index f6727ea83abd..a7d72f0226f8 100644 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ b/src/main/java/seedu/address/ui/PersonCard.java @@ -1,18 +1,33 @@ package seedu.address.ui; +import java.io.IOException; +import java.util.logging.Logger; + import javafx.fxml.FXML; import javafx.scene.control.Label; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Region; +import seedu.address.commons.core.LogsCenter; +import seedu.address.model.meeting.Meeting; import seedu.address.model.person.Person; +import seedu.address.model.person.Picture; /** * An UI component that displays information of a {@code Person}. */ public class PersonCard extends UiPart { + public static final String NO_PHONE = "No Phone Number"; + public static final String NO_EMAIL = "No Email Address"; + public static final String NO_ADDRESS = "No Address"; private static final String FXML = "PersonListCard.fxml"; + private static final String[] TAG_COLOR_STYLES = { "teal", "red", "yellow", "blue", + "orange", "brown", "green", "pink", "black", "grey"}; + + private static final Logger logger = LogsCenter.getLogger(PersonCard.class); /** * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. @@ -27,6 +42,8 @@ public class PersonCard extends UiPart { @FXML private HBox cardPane; @FXML + private ImageView picture; + @FXML private Label name; @FXML private Label id; @@ -37,6 +54,8 @@ public class PersonCard extends UiPart { @FXML private Label email; @FXML + private Label meeting; + @FXML private FlowPane tags; public PersonCard(Person person, int displayedIndex) { @@ -44,10 +63,68 @@ public PersonCard(Person person, int displayedIndex) { 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))); + + //@@author zioul123 + person.getPhone() + .ifPresentOrElse(p -> { + phone.setText(p.value); + }, () -> phone.setText(NO_PHONE)); + person.getAddress() + .ifPresentOrElse(a -> { + address.setText(a.value); + }, () -> address.setText(NO_ADDRESS)); + person.getEmail() + .ifPresentOrElse(e -> { + email.setText(e.value); + }, () -> email.setText(NO_EMAIL)); + meeting.setText(person.getMeeting().value.equals(Meeting.NO_MEETING) ? Meeting.NO_MEETING_MSG : "Meeting on" + + person.getMeeting().toString() + "hrs"); + + //@@author denzelchung + // set profile picture + Image image; + + if (!Picture.isValidPicture(person.getPicture().picture) + || person.getPicture().picture.equals(Picture.DEFAULT_PICTURE_URL.getPath())) { + try { + image = new Image(Picture.DEFAULT_PICTURE_URL.openStream()); + } catch (IOException io) { + logger.warning("Unable to load default picture"); + image = new Image(Picture.DEFAULT_PICTURE_URL.getPath()); + } + } else { + image = new Image("file:" + person.getPicture().picture); + } + + picture.setImage(image); + picture.setCache(true); + + //@@author + initTags(person); + + name.setWrapText(true); + address.setWrapText(true); + email.setWrapText(true); + meeting.setWrapText(true); + } + + /** + * Returns the color style for {@code tagName}'s label. + */ + private String getTagColorStyleFor(String tagName) { + // we use the hash code of the tag name to generate a random color, so that the color remain consistent + // between different runs of the program while still making it random enough between tags. + return TAG_COLOR_STYLES[Math.abs(tagName.hashCode()) % TAG_COLOR_STYLES.length]; + } + /** + * Creates the tag labels for {@code person}. + */ + private void initTags(Person person) { + person.getTags().forEach(tag -> { + Label tagLabel = new Label(tag.tagName); + tagLabel.getStyleClass().add(getTagColorStyleFor(tag.tagName)); + tags.getChildren().add(tagLabel); + }); } @Override diff --git a/src/main/java/seedu/address/ui/ResultDisplay.java b/src/main/java/seedu/address/ui/ResultDisplay.java index d05536bbee96..326eab0af7f3 100644 --- a/src/main/java/seedu/address/ui/ResultDisplay.java +++ b/src/main/java/seedu/address/ui/ResultDisplay.java @@ -7,6 +7,7 @@ import javafx.application.Platform; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; +import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.control.TextArea; import javafx.scene.layout.Region; @@ -18,6 +19,8 @@ */ public class ResultDisplay extends UiPart { + public static final String ERROR_STYLE_CLASS = "error"; + private static final Logger logger = LogsCenter.getLogger(ResultDisplay.class); private static final String FXML = "ResultDisplay.fxml"; @@ -35,7 +38,34 @@ public ResultDisplay() { @Subscribe private void handleNewResultAvailableEvent(NewResultAvailableEvent event) { logger.info(LogsCenter.getEventHandlingLogMessage(event)); - Platform.runLater(() -> displayed.setValue(event.message)); + Platform.runLater(() -> { + if (event.isValid) { + setStyleToDefault(); + } else { + setStyleToIndicateCommandFailure(); + } + displayed.setValue(event.message); + }); + } + + /** + * Sets the result text to use the default style. + */ + private void setStyleToDefault() { + resultDisplay.getStyleClass().remove(ERROR_STYLE_CLASS); + } + + /** + * Sets the result text style to indicate a failed command. + */ + private void setStyleToIndicateCommandFailure() { + ObservableList styleClass = resultDisplay.getStyleClass(); + + if (styleClass.contains(ERROR_STYLE_CLASS)) { + return; + } + + styleClass.add(ERROR_STYLE_CLASS); } } diff --git a/src/main/java/seedu/address/ui/StatusBarFooter.java b/src/main/java/seedu/address/ui/StatusBarFooter.java index f6ba29502422..ad9447e4bd0d 100644 --- a/src/main/java/seedu/address/ui/StatusBarFooter.java +++ b/src/main/java/seedu/address/ui/StatusBarFooter.java @@ -23,6 +23,7 @@ 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"; + public static final String TOTAL_NUMBER_STATUS = "%d person(s) total"; /** * Used to generate time stamps. @@ -42,12 +43,16 @@ public class StatusBarFooter extends UiPart { private StatusBar syncStatus; @FXML private StatusBar saveLocationStatus; + @FXML + private StatusBar totalNumberStatus; - public StatusBarFooter(Path saveLocation) { + public StatusBarFooter(Path saveLocation, int totalPeople) { super(FXML); setSyncStatus(SYNC_STATUS_INITIAL); setSaveLocation(Paths.get(".").resolve(saveLocation).toString()); + setTotalNumberStatus(0); + setTotalNumberStatus(totalPeople); registerAsAnEventHandler(this); } @@ -65,6 +70,10 @@ public static Clock getClock() { return clock; } + public void setTotalNumberStatus(int totalNumber) { + Platform.runLater(() -> totalNumberStatus.setText(String.format(TOTAL_NUMBER_STATUS, totalNumber))); + } + private void setSaveLocation(String location) { Platform.runLater(() -> saveLocationStatus.setText(location)); } @@ -79,5 +88,6 @@ public void handleAddressBookChangedEvent(AddressBookChangedEvent abce) { String lastUpdated = new Date(now).toString(); logger.info(LogsCenter.getEventHandlingLogMessage(abce, "Setting last updated status to " + lastUpdated)); setSyncStatus(String.format(SYNC_STATUS_UPDATED, lastUpdated)); + setTotalNumberStatus(abce.data.getPersonList().size()); } } diff --git a/src/main/resources/images/placeholder_image.jpg b/src/main/resources/images/placeholder_image.jpg new file mode 100644 index 000000000000..c23c56f64b7e Binary files /dev/null and b/src/main/resources/images/placeholder_image.jpg differ diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index c8941ea18263..25fbb36ee675 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -342,10 +342,49 @@ } #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: teal; +} +#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/PersonListCard.fxml b/src/main/resources/view/PersonListCard.fxml index f08ea32ad558..3c3d3687546d 100644 --- a/src/main/resources/view/PersonListCard.fxml +++ b/src/main/resources/view/PersonListCard.fxml @@ -8,15 +8,20 @@ + + - - - + + + + + + - + diff --git a/src/main/resources/view/StatusBarFooter.fxml b/src/main/resources/view/StatusBarFooter.fxml index 041e1ff9004f..1bb053196792 100644 --- a/src/main/resources/view/StatusBarFooter.fxml +++ b/src/main/resources/view/StatusBarFooter.fxml @@ -1,14 +1,20 @@ - + + - + - - + + + - - + + + + + + diff --git a/src/test/data/CsvTest/AddressbookContactOnlyName.csv b/src/test/data/CsvTest/AddressbookContactOnlyName.csv new file mode 100644 index 000000000000..94a7d6be9656 --- /dev/null +++ b/src/test/data/CsvTest/AddressbookContactOnlyName.csv @@ -0,0 +1,3 @@ +Name,Number,Email,Address,Meetings,Tags +Ernie,,,,, +Arjun,,,,, diff --git a/src/test/data/CsvTest/AddressbookCorrect.csv b/src/test/data/CsvTest/AddressbookCorrect.csv new file mode 100644 index 000000000000..9f18dd903482 --- /dev/null +++ b/src/test/data/CsvTest/AddressbookCorrect.csv @@ -0,0 +1,4 @@ +Name:,Phone Number,Email,Address,Meetings,Tags (As many as you wish), +Alex Chan,97412033,chantca95@gmail.com,Bedok North Street 2 Block 120,24/01/18 1230,Loanshark, +Louiz,98573747,louizkc@gmail.com,Cinammon College Level 19,,, +Auyok Sean,85737463,seanA@gmail.com,IDKWhere he stays Road,,Transferee,Student diff --git a/src/test/data/CsvTest/AddressbookDuplicateClash.csv b/src/test/data/CsvTest/AddressbookDuplicateClash.csv new file mode 100644 index 000000000000..6beeba6f9db0 --- /dev/null +++ b/src/test/data/CsvTest/AddressbookDuplicateClash.csv @@ -0,0 +1,3 @@ +Name,Phone Number,Email,Address,Meetings,Tags (As many as you wish), +Alex Chan,97412033,chantca95@gmail.com,Bedok North Street 2 Block 120,,Loanshark, +Alistair,95812341,princeali@gmail.com,Cinammon College Also,,Transferee,Floorball diff --git a/src/test/data/CsvTest/AddressbookDuplicateClashNegative.csv b/src/test/data/CsvTest/AddressbookDuplicateClashNegative.csv new file mode 100644 index 000000000000..b2ea626db6d5 --- /dev/null +++ b/src/test/data/CsvTest/AddressbookDuplicateClashNegative.csv @@ -0,0 +1,2 @@ +Name:,Phone Number,Email,Address,Meeting,Tags (As many as you wish) +Alex Chan,43678243,javalover@gmail.com,Bedok North Street 2 Block 120,,Loanshark diff --git a/src/test/data/CsvTest/AddressbookIncompleteContacts.csv b/src/test/data/CsvTest/AddressbookIncompleteContacts.csv new file mode 100644 index 000000000000..ca318f64d22b --- /dev/null +++ b/src/test/data/CsvTest/AddressbookIncompleteContacts.csv @@ -0,0 +1,4 @@ +Name,Phone Number,Email,Address,Meeting,Tags (As many as you wish), +,85791283,sooyj@u.nus.edu.sg,,,CS2106, +Winnie ,91847372,honeypot@disney.com,Walt Disney Studios,,Pooh,Bear +Martin Henz,84736316,henz@comp.nus.edu.sg,COM 1,,1101S, diff --git a/src/test/data/CsvTest/AddressbookInvalidContactField.csv b/src/test/data/CsvTest/AddressbookInvalidContactField.csv new file mode 100644 index 000000000000..16a76e743a84 --- /dev/null +++ b/src/test/data/CsvTest/AddressbookInvalidContactField.csv @@ -0,0 +1,3 @@ +Name,Number,Email,Address,Meeting,Tags,,"***Ernie will be ignored, Bert will be successfully added. Error message shows up because of Ernie" +Ernie,89473624,test(invalid field),Sesame Street,,puppet,, +Bert,94837243,valid@gmail.com,Sesame Street,,puppet,grouchy, diff --git a/src/test/data/CsvTest/AddressbookText.txt b/src/test/data/CsvTest/AddressbookText.txt new file mode 100644 index 000000000000..2555e8d4937c --- /dev/null +++ b/src/test/data/CsvTest/AddressbookText.txt @@ -0,0 +1,2 @@ +Chee Cheong Fun,98987423,horjiak@gmail.com,Bedok 88,,hawker,businessman +Amos Yee,98743423,amosyee@gmail.com,America,,refugee,activist diff --git a/src/test/data/CsvTest/expectedExport.csv b/src/test/data/CsvTest/expectedExport.csv new file mode 100644 index 000000000000..8eac4fe28a04 --- /dev/null +++ b/src/test/data/CsvTest/expectedExport.csv @@ -0,0 +1,11 @@ +Name, Phone, Email, Address, Meeting, Tags +Alice Pauline,94351253,alice@example.com,123 Jurong West Ave 6 #08-111,,friends, +Benson Meier,98765432,johnd@example.com,311 Clementi Ave 2 #02-25,,owesMoney,friends, +Carl Kurz,95352563,heinz@example.com,wall street,, +Daniel Meier,87652533,cornelia@example.com,10th street,,friends, +Elle Meyer,9482224,werner@example.com,michegan ave,, +Fiona Kunz,9482427,lydia@example.com,little tokyo,,owesMoney,DBS, +George Best,9482442,anna@example.com,4th street,, +Henry Golding,,henry@example.com,Crazy Rich Street,, +Ianna Cluse,83848586,,24 Recluse Avenue,, +Jenny Khiu,91234523,jenny@example.com,,, diff --git a/src/test/data/XmlAddressBookStorageTest/invalidAndValidPersonAddressBook.xml b/src/test/data/XmlAddressBookStorageTest/invalidAndValidPersonAddressBook.xml index 41e411568a5f..005c449fe2b7 100644 --- a/src/test/data/XmlAddressBookStorageTest/invalidAndValidPersonAddressBook.xml +++ b/src/test/data/XmlAddressBookStorageTest/invalidAndValidPersonAddressBook.xml @@ -6,6 +6,8 @@ 9482424 hans@example.com
4th street
+ 0000000000 + /images/hans.jpg @@ -13,5 +15,7 @@ 948asdf2424 hans@example.com
4th street
+ 0000000000 + /images/hans.jpg
diff --git a/src/test/data/XmlAddressBookStorageTest/invalidPersonAddressBook.xml b/src/test/data/XmlAddressBookStorageTest/invalidPersonAddressBook.xml index cfa128e72828..281f1eeec346 100644 --- a/src/test/data/XmlAddressBookStorageTest/invalidPersonAddressBook.xml +++ b/src/test/data/XmlAddressBookStorageTest/invalidPersonAddressBook.xml @@ -6,5 +6,6 @@ 9482424 hans@example.com
4th street
+ 0000000000 diff --git a/src/test/data/XmlSerializableAddressBookTest/duplicatePersonAddressBook.xml b/src/test/data/XmlSerializableAddressBookTest/duplicatePersonAddressBook.xml index ac02230263d3..a2418603fc98 100644 --- a/src/test/data/XmlSerializableAddressBookTest/duplicatePersonAddressBook.xml +++ b/src/test/data/XmlSerializableAddressBookTest/duplicatePersonAddressBook.xml @@ -6,6 +6,8 @@ 94351253 alice@example.com
123, Jurong West Ave 6, #08-111
+ 0000000000 + /images/alice.jpg friends @@ -15,6 +17,8 @@ 94351253 pauline@example.com
4th street
+ 0000000000 + /images/alice.jpg diff --git a/src/test/data/XmlSerializableAddressBookTest/invalidPersonAddressBook.xml b/src/test/data/XmlSerializableAddressBookTest/invalidPersonAddressBook.xml index 13d5b1cb1c8a..ed1c28e27efc 100644 --- a/src/test/data/XmlSerializableAddressBookTest/invalidPersonAddressBook.xml +++ b/src/test/data/XmlSerializableAddressBookTest/invalidPersonAddressBook.xml @@ -6,5 +6,6 @@ 9482424 hans@exam!32ple
4th street
+ 0000000000 diff --git a/src/test/data/XmlSerializableAddressBookTest/typicalPersonsAddressBook.xml b/src/test/data/XmlSerializableAddressBookTest/typicalPersonsAddressBook.xml index d812b05e32bb..d3839dbe7f9e 100644 --- a/src/test/data/XmlSerializableAddressBookTest/typicalPersonsAddressBook.xml +++ b/src/test/data/XmlSerializableAddressBookTest/typicalPersonsAddressBook.xml @@ -6,45 +6,85 @@ 94351253 alice@example.com
123, Jurong West Ave 6, #08-111
+ 0000000000 friends + /images/placeholder_image.jpg Benson Meier 98765432 johnd@example.com
311, Clementi Ave 2, #02-25
+ 0000000000 owesMoney friends + /images/placeholder_image.jpg
Carl Kurz 95352563 heinz@example.com
wall street
+ 0000000000 + /images/placeholder_image.jpg
Daniel Meier 87652533 cornelia@example.com
10th street
+ 0000000000 friends + /images/placeholder_image.jpg
Elle Meyer 9482224 werner@example.com
michegan ave
+ 0000000000 + /images/placeholder_image.jpg
Fiona Kunz 9482427 lydia@example.com
little tokyo
+ 0000000000 + owesMoney + DBS + /images/placeholder_image.jpg
George Best 9482442 anna@example.com
4th street
+ 0000000000 + /images/placeholder_image.jpg +
+ + Henry Golding + 000000000000 + henry@example.com +
Crazy Rich Street
+ 0000000000 + /images/placeholder_image.jpg +
+ + Ianna Cluse + 83848586 + NOEMAIL@NOEMAIL.NOEMAIL +
24 Recluse Avenue
+ 0000000000 + /images/placeholder_image.jpg +
+ + Jenny Khiu + 91234523 + jenny@example.com +
NO_ADDRESS
+ 0000000000 + /images/placeholder_image.jpg
diff --git a/src/test/data/XmlUtilTest/invalidPersonField.xml b/src/test/data/XmlUtilTest/invalidPersonField.xml index ba49c971e884..f9aaca049eb3 100644 --- a/src/test/data/XmlUtilTest/invalidPersonField.xml +++ b/src/test/data/XmlUtilTest/invalidPersonField.xml @@ -6,4 +6,5 @@ hans@example
4th street
friends + 0000000000 diff --git a/src/test/data/XmlUtilTest/missingPersonField.xml b/src/test/data/XmlUtilTest/missingPersonField.xml index c0da5c86d080..aefb65d716a2 100644 --- a/src/test/data/XmlUtilTest/missingPersonField.xml +++ b/src/test/data/XmlUtilTest/missingPersonField.xml @@ -5,4 +5,5 @@ hans@example
4th street
friends + 0000000000 diff --git a/src/test/data/XmlUtilTest/validAddressBook.xml b/src/test/data/XmlUtilTest/validAddressBook.xml index 6265778674d3..afdefa16b12f 100644 --- a/src/test/data/XmlUtilTest/validAddressBook.xml +++ b/src/test/data/XmlUtilTest/validAddressBook.xml @@ -5,53 +5,71 @@ 9482424 hans@example.com
4th street
+ 0000000000 + /images/hans.jpg Ruth Mueller 87249245 ruth@example.com
81th street
+ 0000000000 + /images/ruth.jpg
Heinz Kurz 95352563 heinz@example.com
wall street
+ 0000000000 + /images/heinz.jpg
Cornelia Meier 87652533 cornelia@example.com
10th street
+ 0000000000 + /images/cornelia.jpg
Werner Meyer 9482224 werner@example.com
michegan ave
+ 0000000000 + /images/werner.jpg
Lydia Kunz 9482427 lydia@example.com
little tokyo
+ 0000000000 + /images/lydia.jpg
Anna Best 9482442 anna@example.com
4th street
+ 0000000000 + /images/anna.jpg
Stefan Meier 8482424 stefan@example.com
little india
+ 0000000000 + /images/stefan.jpg
Martin Mueller 8482131 hans@example.com
chicago ave
+ 0000000000 + /images/martin.jpg
diff --git a/src/test/data/XmlUtilTest/validPerson.xml b/src/test/data/XmlUtilTest/validPerson.xml index c029008d54f4..b0bd64832afd 100644 --- a/src/test/data/XmlUtilTest/validPerson.xml +++ b/src/test/data/XmlUtilTest/validPerson.xml @@ -5,4 +5,5 @@ hans@example
4th street
friends + 0000000000 diff --git a/src/test/java/guitests/guihandles/PersonCardHandle.java b/src/test/java/guitests/guihandles/PersonCardHandle.java index 1789735e49a8..3ebac32dd00c 100644 --- a/src/test/java/guitests/guihandles/PersonCardHandle.java +++ b/src/test/java/guitests/guihandles/PersonCardHandle.java @@ -7,8 +7,11 @@ import javafx.scene.Node; import javafx.scene.control.Label; +import javafx.scene.image.Image; +import javafx.scene.image.ImageView; import javafx.scene.layout.Region; import seedu.address.model.person.Person; +import seedu.address.ui.PersonCard; /** * Provides a handle to a person card in the person list panel. @@ -20,6 +23,7 @@ public class PersonCardHandle extends NodeHandle { private static final String PHONE_FIELD_ID = "#phone"; private static final String EMAIL_FIELD_ID = "#email"; private static final String TAGS_FIELD_ID = "#tags"; + private static final String PICTURE_FIELD_ID = "#picture"; private final Label idLabel; private final Label nameLabel; @@ -27,6 +31,7 @@ public class PersonCardHandle extends NodeHandle { private final Label phoneLabel; private final Label emailLabel; private final List