diff --git a/.gitignore b/.gitignore index 823d175eb670..c8e31b0cdb8a 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ lib/* *.log *.log.* *.csv -config.json +/config.json src/test/data/sandbox/ preferences.json .DS_Store diff --git a/README.adoc b/README.adoc index 0ad72da1a5ba..ae3fb743fdda 100644 --- a/README.adoc +++ b/README.adoc @@ -1,11 +1,10 @@ -= Address Book (Level 4) += Centralised Human Resource System (CHRS) ifdef::env-github,env-browser[:relfileprefix: docs/] -https://travis-ci.org/nusCS2113-AY1819S1/addressbook-level4[image:https://travis-ci.org/nusCS2113-AY1819S1/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]] +image:https://api.codacy.com/project/badge/Grade/b9c9b25a8f144d94a8ac84b66110915c[link="https://app.codacy.com/app/zhihong8888/main?utm_source=github.com&utm_medium=referral&utm_content=CS2113-AY1819S1-T16-4/main&utm_campaign=Badge_Grade_Dashboard"] +https://travis-ci.org/CS2113-AY1819S1-T16-4/main[image:https://travis-ci.org/nusCS2113-AY1819S1/addressbook-level4.svg?branch=master[Build Status]] +https://ci.appveyor.com/project/LimYiSheng/main-mq9f4/branch/master[image:https://ci.appveyor.com/api/projects/status/qyjcn8xybhessr25/branch/master?svg=true[Build status]] +https://coveralls.io/github/CS2113-AY1819S1-T16-4/main?branch=master[image:https://coveralls.io/repos/github/CS2113-AY1819S1-T16-4/main/badge.svg?branch=master[Coverage Status]] ifdef::env-github[] image::docs/images/Ui.png[width="600"] @@ -15,19 +14,16 @@ 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. +* *Centralised Human Resource System (CHRS)* is a desktop application. It has a GUI but most of the user interactions happen using a CLI (Command Line Interface). +* CHRS is targeted at the *Human Resource (HR) department*. +* As the HR department requires access to various information of each employee, it may be a hassle for them to obtain the required information if the information are stored in various different systems and/or databases. +* Therefore, CHRS aims to help the HR department store all relevant information to each employee within one system. +* CHRS is developed by group T16-4 from the module CS2113 in National University of Singapore (NUS). == Site Map * <> * <> -* <> * <> * <> @@ -36,6 +32,6 @@ 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_. * 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] +* This application was built using the source code found in https://github.com/se-edu/addressbook-level4[AddressBook-level4] by SE-EDU. == Licence : link:LICENSE[MIT] - diff --git a/_reposense/config.json b/_reposense/config.json new file mode 100644 index 000000000000..21de3003d85d --- /dev/null +++ b/_reposense/config.json @@ -0,0 +1,30 @@ +{ + "authors": + [ + { + "githubId": "ryanchen2018", + "displayName": "CHE...ING", + "authorNames": ["ryanchen2018"] + }, + { + "githubId": "ChuaZhenWei", + "displayName": "CHU...WEI", + "authorNames": ["ChuaZhenWei"] + }, + { + "githubId": "LimYiSheng", + "displayName": "LIM...ENG", + "authorNames": ["LimYiSheng"] + }, + { + "githubId": "XiiaoPanda", + "displayName": "VER...ONG", + "authorNames": ["XiiaoPanda"] + }, + { + "githubId": "zhihong8888", + "displayName": "WIL...ONG", + "authorNames": ["zhihong8888"] + } + ] +} diff --git a/build.gradle b/build.gradle index f8e614f8b49b..2a966dc52024 100644 --- a/build.gradle +++ b/build.gradle @@ -207,9 +207,9 @@ 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': 'Centralised Human Resource System (CHRS)', + 'site-githuburl': 'https://github.com/CS2113-AY1819S1-T16-4/main', + //'site-seedu': true, // delete this line if your project is not a fork (not a SE-EDU project) ] options['template_dirs'].each { diff --git a/docs/AboutUs.adoc b/docs/AboutUs.adoc index e647ed1e715a..158723b0bd46 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} + +CHRS - developed by CS2113 T16-4 team. + We are a team based in the http://www.comp.nus.edu.sg[School of Computing, National University of Singapore]. == Project Team -=== John Doe -image::damithc.jpg[width="150", align="left"] -{empty}[http://www.comp.nus.edu.sg/~damithch[homepage]] [https://github.com/damithc[github]] [<>] +=== Lim Yi Sheng +image::limyisheng.png[width="150", align="left"] +{empty} [https://github.com/LimYiSheng[github]][<>] -Role: Project Advisor +Role: Team Lead + Developer + Integrator + +Responsibilities: Ensures deadlines and deliverables are met. Also in charge of code integration. ''' -=== John Roe -image::lejolly.jpg[width="150", align="left"] -{empty}[http://github.com/lejolly[github]] [<>] +=== Chua Zhen Wei +image::chuazhenwei.png[width="150", align="left"] +{empty}[https://github.com/ChuaZhenWei[github]][<>] -Role: Team Lead + -Responsibilities: UI +Role: Developer + Tester + +Responsibilities: Ensures usability and functionality alongside discovering potential bugs. ''' -=== Johnny Doe -image::yijinl.jpg[width="150", align="left"] -{empty}[http://github.com/yijinl[github]] [<>] +=== William Ng Zhi Hong +image::zhihong8888.png[width="150", align="left"] +{empty}[https://github.com/zhihong8888[github]][<>] -Role: Developer + -Responsibilities: Data +Role: Developer + Code Quality Checker + +Responsibilities: Ensures that codes are up to java coding standards. ''' -=== Johnny Roe -image::m133225.jpg[width="150", align="left"] -{empty}[http://github.com/m133225[github]] [<>] +=== Vernon Cher Chu Xiong +image::xiiaopanda.png[width="150", align="left"] +{empty}[https://github.com/XiiaoPanda[github]][<>] -Role: Developer + -Responsibilities: Dev Ops + Threading +Role: Developer + Tester + +Responsibilities: Ensures usability and functionality alongside discovering potential bugs. ''' -=== Benson Meier -image::yl_coder.jpg[width="150", align="left"] -{empty}[http://github.com/yl-coder[github]] [<>] +=== Chen Qunming +image::ryanchen2018.png[width="150", align="left"] +{empty}[https://github.com/ryanchen2018[github]][<>] -Role: Developer + -Responsibilities: UI +Role: Developer + Documentation Writer + +Responsibilities: Ensures changes to CHRS are documented in good quality. ''' diff --git a/docs/ContactUs.adoc b/docs/ContactUs.adoc index 5de5363abffd..fda8fa947201 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/CS2113-AY1819S1-T16-4/main/issues[issue tracker] if you noticed bugs or have suggestions on how to improve. * *Contributing* : We welcome pull requests. Follow the process described https://github.com/oss-generic/process[here] -* *Email us* : You can also reach us at `damith [at] comp.nus.edu.sg` +* *Email us* : You can also reach us at `limyisheng [at] u.nus.edu` diff --git a/docs/DeveloperGuide.adoc b/docs/DeveloperGuide.adoc index ea58481e4740..87b2128d4a30 100644 --- a/docs/DeveloperGuide.adoc +++ b/docs/DeveloperGuide.adoc @@ -1,4 +1,4 @@ -= AddressBook Level 4 - Developer Guide += Centralised Human Resource System - Developer Guide :site-section: DeveloperGuide :toc: :toc-title: @@ -12,9 +12,9 @@ ifdef::env-github[] :note-caption: :information_source: :warning-caption: :warning: endif::[] -:repoURL: https://github.com/se-edu/addressbook-level4/tree/master +:repoURL: https://github.com/CS2113-AY1819S1-T16-4/main/tree/master -By: `Team SE-EDU`      Since: `Jun 2016`      Licence: `MIT` +By: `CS2113-T16-4`      Since: `Aug 2018`      Licence: `MIT` == Setting up @@ -208,8 +208,8 @@ image::ModelClassDiagram.png[width="800"] The `Model`, * stores a `UserPref` object that represents the user's preferences. -* stores the Address Book data. -* exposes an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change. +* stores the Address Book, Schedule List, Expenses List and Recruitment List data. +* exposes unmodifiable `ObservableList`, `ObservableList`, `ObservableList` and `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change. * does not depend on any of the other three components. [NOTE] @@ -229,6 +229,9 @@ The `Storage` component, * can save `UserPref` objects in json format and read it back. * can save the Address Book data in xml format and read it back. +* can save the Expenses List data in xml format and read it back. +* can save the Schedule List data in xml format and read it back. +* can save the Recruitment List data in xml format and read it back. [[Design-Commons]] === Common classes @@ -243,8 +246,24 @@ This section describes some noteworthy details on how certain features are imple === Undo/Redo feature ==== Current Implementation -The undo/redo mechanism is facilitated by `VersionedAddressBook`. -It extends `AddressBook` with an undo/redo history, stored internally as an `addressBookStateList` and `currentStatePointer`. +The undo/redo mechanism is facilitated by `VersionedModelList`, which oversees all the undo and redo for other +storage, namely, `VersionedAddressBook`, `VersionedScheduleList`, `VersionedExpensesList` +and `VersionedRecruitmentList`. + +`VersionedModelList` stored internally as an `modelTypesStateList` and `currentStatePointer`. +Additionally, it implements the following operations: + +* `VersionedAddressBook#add()` -- Save the storage model type that has committed in its history. +* `VersionedAddressBook#addMultiple()` -- Saves the set of storage model types that has committed in its history. +* `VersionedAddressBook#undo()` -- Delegate `VersionedAddressBook`, `VersionedScheduleList`, `VersionedExpensesList` +and `VersionedRecruitmentList` to restore the previous storage model state from its history. +* `VersionedAddressBook#redo()` -- Delegate `VersionedAddressBook`, `VersionedScheduleList`, `VersionedExpensesList` +and `VersionedRecruitmentList` to restore a previously undone address book state from its history. + +The following is an example of `VersionedAddressBook` implementation. The implementation will +be similar to `VersionedScheduleList`, `VersionedExpensesList` and `VersionedRecruitmentList`. + +`VersionedAddressBook` extends `AddressBook` with an undo/redo history, stored internally as an `addressBookStateList` and `currentStatePointer`. Additionally, it implements the following operations: * `VersionedAddressBook#commit()` -- Saves the current address book state in its history. @@ -319,12 +338,987 @@ image::UndoRedoActivityDiagram.png[width="650"] ** 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::dataencryption[] -=== [Proposed] Data Encryption +// tag::modifyPay[] +=== Modify Pay Feature +The command `modifyPay` allows the user to modify the salary and bonus of the employee based on the input. + +==== Current Implementation + +The implementation of this command consist of two phases. + +===== Phase 1 + +The `modifyPay` function is first supported by `ModifyPayCommandParser` which implements the `Parser` interface. The interface parse the arguments parameter, which was inputted by the users to form the `ModifyPayCommand` object. The arguments will also have their validity checked by the method, `isValidSalary` and `isValidBonus`, within respective Classes before being parse into the salary and bonus column which will return an error message when the arguments did not meet the requirements. + +Code Snippet of `ModifyPayCommandParser` that show the parsing of input and the checking validity of the inputs: + +[source, java] +---- + public class ModifyPayCommandParser implements Parser { + private static final double BONUS_UPPER_LIMIT = 24.0; + + public ModifyPayCommand parse(String args) throws ParseException { + requireNonNull(args); + + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_SALARY, PREFIX_BONUS); + + Index index; + + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ModifyPayCommand.MESSAGE_USAGE), pe); + } + + if (!didPrefixAppearOnlyOnce(args)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ModifyPayCommand.MESSAGE_USAGE)); + } + + ModSalaryDescriptor modSalaryDescriptor = new ModSalaryDescriptor(); + + if (argMultimap.getValue(PREFIX_SALARY).isPresent()) { + modSalaryDescriptor.setSalary(ParserUtil.parseSalary(argMultimap.getValue(PREFIX_SALARY).get())); + } + + if (argMultimap.getValue(PREFIX_BONUS).isPresent()) { + Bonus bonusInput = ParserUtil.parseBonus(argMultimap.getValue(PREFIX_BONUS).get()); + + double bonus = Double.parseDouble(argMultimap.getValue(PREFIX_BONUS).get()); + + if (bonus > BONUS_UPPER_LIMIT) { + throw new ParseException(Bonus.MESSAGE_BONUS_CONSTRAINTS); + } + + modSalaryDescriptor.setBonus(bonusInput); + } + + if (!modSalaryDescriptor.isAnyFieldEdited()) { + throw new ParseException(ModifyPayCommand.MESSAGE_NOT_MODIFIED); + } + + return new ModifyPayCommand(index, modSalaryDescriptor); + } + private boolean didPrefixAppearOnlyOnce(String argument) { + String salaryPrefix = " " + PREFIX_SALARY.toString(); + String bonusPrefix = " " + PREFIX_BONUS.toString(); + + return argument.indexOf(salaryPrefix) == argument.lastIndexOf(salaryPrefix) + && argument.indexOf(bonusPrefix) == argument.lastIndexOf(bonusPrefix); + } + } +---- + +Code snippet of `Salary` + +[source, java] +---- + public static final String MESSAGE_SALARY_CONSTRAINTS = + "Salary should only contain numbers, and it should not be blank. Only a maximum of 6 whole numbers and " + + "2 decimal place are allowed. (Max Salary store value is 999999.99)\n"; + public static final String SALARY_VALIDATION_REGEX = "[%]?[-]?[0-9]{1,6}([.][0-9]{1,2})?"; + public final String value; + + /** + * Constructs a {@code salary}. + * + * @param salary A valid salary. + */ + + public Salary(String salary) { + requireNonNull(salary); + checkArgument(isValidSalary(salary), MESSAGE_SALARY_CONSTRAINTS); + value = salary; + } + + /** + * Returns true if a given string is a valid salary. + */ + public static boolean isValidSalary(String test) { + return test.matches(SALARY_VALIDATION_REGEX); + } +---- + +Code snippet of `Bonus` + +[source, java] +---- + public static final String MESSAGE_BONUS_CONSTRAINTS = + "Bonus should only contain positive numbers and maximum of 2 decimal places from 0 to 24," + + " and it should not be blank"; + public static final String BONUS_VALIDATION_REGEX = "(([0-9]{1,7}([.][0-9]{1,2})?)|(1[0-9]{7}([.][0-9]{1,2})?)" + + "|(2[0-3]([0-9]{1,6})([.][0-9]{1,2})?))"; + public final String value; + + public Bonus(String bonus) { + requireNonNull(bonus); + checkArgument(isValidBonus(bonus), MESSAGE_BONUS_CONSTRAINTS); + value = bonus; + } + + /** + * Returns true if a given string is a valid bonus. + */ + public static boolean isValidBonus(String test) { + return test.matches(BONUS_VALIDATION_REGEX); + } +---- + +===== Phase 2 + +The `ModifyPayCommand` is being executed in this phase. `createModifiedPerson` method calls for the edited value from `ModifyPayCommandParser` to check if there are values being inputted by the users. When it is found to be a null values, `createModifiedPerson` will take back the original value. + +Code snippet of `ModifyPayCommand` which execute the `createModifiedPerson` method: + +[source, java] +---- + private static Person createModifiedPerson(Person personToEdit, + ModSalaryDescriptor modSalaryDescriptor) throws ParseException { + assert personToEdit != null; + + EmployeeId updatedEmployeeId = personToEdit.getEmployeeId(); + Name updatedName = personToEdit.getName(); + DateOfBirth updatedDateOfBirth = personToEdit.getDateOfBirth(); + Phone updatedPhone = personToEdit.getPhone(); + Email updatedEmail = personToEdit.getEmail(); + Department updatedDepartment = personToEdit.getDepartment(); + Position updatedPosition = personToEdit.getPosition(); + Address updatedAddress = personToEdit.getAddress(); + Salary updatedSalary = ParserUtil.parseSalary(typeOfSalaryMod(personToEdit, modSalaryDescriptor)); + Bonus updatedBonus = ParserUtil.parseBonus(modifyBonusMonth(personToEdit, modSalaryDescriptor, updatedSalary)); + + Set updatedTags = personToEdit.getTags(); + + return new Person(updatedEmployeeId, updatedName, updatedDateOfBirth, updatedPhone, updatedEmail, + updatedDepartment, updatedPosition, updatedAddress, updatedSalary, updatedBonus, updatedTags); + } +---- + +The result of `ModifyPayCommand` is encapsulated as a `CommandResult` object which is +passed back into the UI to reflect the newly modified Salary/Bonus. +The sequence diagram below illustrates the operation of `modifyPay` command: + +image::ModifyPayCommandSequenceDiagram.png[width="850"] + +==== Employee's Pay Report: `payReport` _[Upcoming in 2.0]_ +Generate a report to show the break down of employee's pay such as base Salary, Bonus, Overtime Pay and CPF Contribution. [Upcoming in 2.0] + +==== Design Considerations +Aspect implementation of `modifyPay` command: + +* Alternative 1(current choice): A separate command to handle modification of payroll. + +** Pros: Reduce the risk of accidental editing of sensitive information +** Cons: Additional method and prefixes are required to execute the function + +* Alternative 2: One command to handle all the modifications +** Pros: Lesser Class to handle different fields +** Cons: User may accidentally touch on sensitive data and ended up editing them + +// end::modifyPay[] + +// tag::RecruitmentPost[] +=== Recruitment Post Feature +`Recruitment post` features allows HR staffs to manage internal recruitment posts easily before publishing to the public +. It allows users to do add, delete, clear, select and edit recruitment post based on 3 fields including job position, +working experience, and job description. + +All logic commands come with a dedicated parser. These parsers ensure that the input conforms to the following +field restrictions in phase 1 before parsing to the actual command for execution in phase 2: +[width="90%",cols="25%,<15%,50%",options="header",] +|======================================================================= +|Field Name |Prefix |Limitations + +|JOB_POSITION |jp/ | Job position accepts only characters. It must not be blank and should not include numbers and +punctuation mark. And users are not allowed to exceed the character limit which is from 1 to 20 +|MINIMAL_YEARS_OF_WORKING_EXPERIENCE |me/ | Minimal years of working experience must be integers and should not be blank +. And It is limited from 0 to 30 +|JOB_DESCRIPTION |jd/ | Job description accepts only characters and Punctuation mark including only comma, full stop, +and single right quote. It must not be blank and should not include numbers. And users are not allowed to exceed the +character limit which is from 1 to 200 +|======================================================================= + +==== Current Implementation for `addRecruitmentPost` +`addRecruitmentPost` command allows a new recruitment post to be added. The sequence diagram below illustrates the +operation of `addRecruitmentPost` command: + +image::Recruitment/addRecruitmentPost.png[width="650"] + +In addition, Command Exception will be thrown due to the following: + +.. Invalid command format is used. +.. A duplicated recruitment field exists. Minimal 1 field should be different. +.. Fields' restrictions are not followed. + +==== Current Implementation for `editRecruitmentPost` +`editRecruitmentPost` command allows an existing recruitment post to be edited. The sequence diagram below illustrates +the operation of `editRecruitmentPost` command: + +image::Recruitment/editRecruitmentPost.png[width="650"] + +In addition, Command Exception will be thrown due to the following: + +.. Invalid command format is used. +.. A duplicated recruitment field exists. Minimal 1 field should be different from existing recruitment post that is +about to be edited. +.. Fields' restrictions are not followed. + +==== Current Implementation for `selectRecruitmentPost` +`selectRecruitmentPost` command allows users to select an existing recruitment post based on recruit post index number +displayed in the recruitment post list panel. The sequence diagram below illustrates the operation +of `selectRecruitmentPost` command: + +image::Recruitment/selectRecruitmentPost.png[width="650"] + +In addition, Command Exception will be thrown due to the following: + +.. Invalid command format is used. +.. Recruitment post list is empty. +.. Invalid index number is used. + +==== Current Implementation for `deleteRecruitmentPost` +`deleteRecruitmentPost` command allows users to delete an existing recruitment post based on recruit post index number +displayed in the recruitment post list panel. The sequence diagram below illustrates the operation +of `deleteRecruitmentPost` command: + +image::Recruitment/deleteRecruitmentPost.png[width="650"] + +In addition, Command Exception will be thrown due to the following: + +.. Invalid command format is used. +.. Recruitment post list is empty. +.. Invalid index number is used. + +==== Future Implementation for `publishRecruitmentPost` _[Upcoming in 2.0]_ +`publishRecruitmentPost` command allows users to publish an existing recruitment post to job portal such as job street +website. _[Upcoming in 2.0]_ + +==== Design Considerations for Recruitment Post Feature +Aspect implementation of all Recruitment Post Commands + +* **Alternative 1 (current choice):** Have separate commands +to manage recruitment posts. +** Pros: Individual command is easier for user to use and easier for developer to debug and test. +** Cons: More methods and implementation is required. +* **Alternative 2:** Have a single command to manage recruitment posts. +** Pros: Lesser methods and implementation is required. +** Cons: The command could be too complicated for users to use as more fields would be needed. + +// end::RecruitmentPost[] + +// tag::schedule1[] +=== Schedule Feature + +`Schedule` command helps the HR admins to schedule employee's work and leave schedules in the company. +A person can have multiple schedules. Type field describes whether the schedule is a work or a leave. +Below is the schedule class diagram. + +image::Schedule/schedule_class.png[width="400"] + +For each schedule commands, it comes with a dedicated schedule parser. +First, the schedule parsers ensure that the prefix required are all present with `!arePrefixesPresent`. +[source, java] +---- + ... + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } + ... +---- +The parsers validate field inputs by creating a new `Type`, `Date`, `EmployeeId` and `Schedule` object. + +[source, java] +---- + ... + Type type = ParserUtil.parseStatus(argMultimap.getValue(PREFIX_SCHEDULE_TYPE).get()); + Date date = ParserUtil.parseDate(argMultimap.getValue(PREFIX_SCHEDULE_DATE).get()); + EmployeeId id = ParserUtil.parseEmployeeId(argMultimap.getValue(PREFIX_EMPLOYEEID).get()); + Schedule schedule = new Schedule(id, type, date); + ... +---- + +The objects will `throw exception` if it does not conform to the following field restrictions. + +[width="90%",cols="25%,<15%,50%",options="header",] +|======================================================================= +|Field Name +|Prefix +|Limitations + +|EMPLOYEEID +|id/ +|Employee Id should only contain exactly 6 numbers. + +|DATE +|d/ +|Date must be a valid date in the calendar DD/MM/YYYY]. Year must also fall into the range + of 2000-2099. Leading 0s can be omitted in day and month field. + You are not allowed to schedule for dates that have past today's date. + +|TYPE +|t/ +|Type can be either WORK or LEAVE only, case not sensitive. + +|======================================================================= + + +==== Add Schedule +`addSchedule` command allows a new work or leave schedule to be added. +Shown below is the normal flow of the sequence diagram for adding a new schedule. + +image::Schedule/addSchedule_seq.png[width="650"] + +In addition, Command Exception will be thrown due to the following: + +.. Employee Id of the schedule to be added is not found in address book. +.. A duplicated schedule exists. +.. User wants to add a work schedule, but a leave schedule exists on same date. +.. User wants to add leave schedule, but a work schedule exists on same date. + +The `AddScheduleParser` parses a `Schedule` object to the `AddWorks` command. +`AddWorks` command looks at the `Type` inside the `Schedule`. + +[source, java] +---- +Type type = toAddSchedule.getType(); +---- + +If the `Type` is equals to leave and work is not scheduled on the same date `model.hasSchedule(toCheckWork)`, +no exception will be thrown. + +[source, java] +---- +... +if (type.equals(leave)) { + Schedule toCheckWork = new Schedule(toAddSchedule.getEmployeeId(), work, + toAddSchedule.getScheduleDate()); + if (model.hasSchedule(toCheckWork)) { + throw new CommandException(MESSAGE_HAS_WORK); +} +... +---- +And it adds the schedule and commit to the storage. +[source, java] +---- +... +model.addSchedule(toAddSchedule); +model.commitScheduleList(); +... +---- +==== Delete Schedule +`deleteSchedule` command allows a schedule to be deleted. +Shown below is the normal flow of the sequence diagram for adding a new schedule. + +image::Schedule/deleteSchedule_seq.png[width="650"] + +In addition, Command Exception will be thrown due to the following: + +.. Index parsed is an invalid index in the ScheduleList panel. + +==== Add Works +`addWorks` command allows multiple work schedules to be added based on +the employees shown in the employee's panel. +Shown below is the normal flow of the sequence diagram for adding a working schedule. + +image::Schedule/addWorks_seq.png[width="650"] + +In addition, Command Exception will be thrown due to the following: + +.. Size of the filtered person list is 0. This happens when there are no +employees shown. +.. Commit is false. This happens either when all employees have been +added work, or some had leave on the same day. + +The `AddWorksParser` parses a set of dates `Set` to be scheduled work to the `AddWorks` command. +For each date specified by the user in the set of dates `for (Date date :setOfDates)`, +it will be checked by for each observable employee in the employee list `for (Person person : model.getFilteredPersonList())` +for the possibility of scheduling work. +No schedules will be created if a duplicate schedule `model.hasSchedule(toAddSchedule)` +or existing work schedule `model.hasSchedule(hasLeaveSchedule)` is found on that date. + +[source, java] +---- +... +for (Date date :setOfDates) { + for (Person person : model.getFilteredPersonList()) { + Schedule toAddSchedule = new Schedule(person.getEmployeeId(), work , date); + Schedule hasLeaveSchedule = new Schedule(person.getEmployeeId(), leave , date); + if (model.hasSchedule(hasLeaveSchedule)) { + employeeIdMapToLeaves.put(person.getEmployeeId(), date); + } else if (!model.hasSchedule(toAddSchedule)) { + commit = true; + model.addSchedule(toAddSchedule); + } + } +} +... +---- + +==== Delete Works +`deleteWorks` command allows multiple work schedules to be deleted based on +the employees shown in the employee's panel. +Shown below is the normal flow of the sequence diagram for deleting multiple work schedules. + +image::Schedule/deleteWorks_seq.png[width="650"] + +In addition, Command Exception will be thrown due to the following: + +.. Size of the filtered person list is 0. This happens when there are no +employees shown. + +The `deleteWorksParser` parses a set of dates `Set` to be deleted work to the `DeleteWorks` command. + +For each date specified by the user in the set of dates `for (Date date :setOfDates)`, +it will be checked by for each observable employee in the employee list `for (Person person : model.getFilteredPersonList())` +for the possibility of deleting work. Work schedule will be deleted `model.deleteSchedule(toDeleteSchedule)` +if found on that date `model.hasSchedule(toDeleteSchedule)`. + +[source, java] +---- +... +for (Date date : setOfDates) { + for (Person person : model.getFilteredPersonList()) { + Schedule toDeleteSchedule = new Schedule(person.getEmployeeId(), work , date); + if (model.hasSchedule(toDeleteSchedule)) { + commit = true; + model.deleteSchedule(toDeleteSchedule); + } + } +} +... +---- + +// end::schedule1[] + +==== Add Leaves +`addLeaves` command allows multiple leave schedules to be added based on +the employees shown in the employee's panel. +Shown below is the normal flow of the sequence diagram for adding a leave schedule. + +image::Schedule/addLeaves_seq.png[width="650"] + +In addition, Command Exception will be thrown due to the following: + +.. Size of the filtered person list is 0. This happens when there are no +employees shown. +.. Commit is false. This happens either when all employees have been +added work, or some had leave on the same day. + +==== Delete Leaves +`deleteLeaves` command allows multiple leave schedules to be deleted based on +the employees shown in the employee's panel. +Shown below is the normal flow of the sequence diagram for deleting multiple leave schedules. + +image::Schedule/deleteLeaves_seq.png[width="650"] + +In addition, Command Exception will be thrown due to the following: + +.. Size of the filtered person list is 0. This happens when there is no +employees shown. + +// tag::schedule2[] + +==== Calculate Leaves +`calculateLeaves` command allows user to calculate leaves taken for the specified employee id and year. +Shown below is the normal flow of the sequence diagram for calculating leaves. + +image::Schedule/calculateLeaves_seq.png[width="650"] + +In addition, Command Exception will be thrown due to the following: + +.. Employee Id to be calculated is not found in address book. +.. Employee Id has not taken any leaves in the specified year. + +The `CalculateLeavesCommandParser` parses a `EmployeeId` and `Year` object +to the `CalculateLeaves` command. +`CalculateLeaves` filters the schedule list to show only schedules that +contains the `employeeId` to be calculated. + +[source, java] +---- +List employeeIdList = new ArrayList<>(); +employeeIdList.add(employeeId.value); +EmployeeIdScheduleContainsKeywordsPredicate employeeIdPredicate = + new EmployeeIdScheduleContainsKeywordsPredicate(employeeIdList); +model.updateFilteredScheduleList(employeeIdPredicate); +---- + +For each schedule in the filtered schedule list `for (Schedule schedule : model.getFilteredScheduleList()` +if the leave and year matches the one to be calculated, it +increments the number of leaves `numLeaves`. + +[source, java] +---- +... +for (Schedule schedule : model.getFilteredScheduleList()) { + if (schedule.getScheduleYear().equals(year.toString()) + && schedule.getType().toString().equals(Type.LEAVE)) { + numLeaves++; + } +} +... +---- + +Finally it returns the calculated leave. + +[source, java] +---- +CommandResult(String.format(MESSAGE_SUCCESS, employeeId, year, numLeaves)) +---- + +==== Send Schedule: `sendSchedule` _[Upcoming in 2.0]_ + +Send schedules to the employee for calendar import using the Employee's email address. _[Upcoming in 2.0]_ + +==== Design considerations for Schedule Features +Aspect: Command + +* **Alternative 1 (current choice):** Have separate commands +to add work/leave schedules. +** Pros: Easier for the user to use, without the need to specify +the type of schedule. +** Cons: More codes needed, but easier for user to schedule +for multiple employees with multiple dates. +* **Alternative 2:** Have a single command to add work/leave schedule. +** Pros: Easier to code. +** Cons: Requires user to specify type of schedule. Can only +schedule 1 at a time. + +Aspect: Storage + +* **Alternative 1 (current choice):** Have schedules stored in a separate +XML file. +** Pros: Easier to manage scalability issues as schedules grow in numbers. +Possibility of having a database for schedule in future. +** Cons: More codes needed. Extra time is needed to delete schedules when +employee is deleted. +* **Alternative 2:** Have schedules stored in the same XML file as addressbook.xml +** Pros: Pros: Easier to implement, as only an additional field is required by modeling it as a set of schedules. +** Cons: Very hard to manage large number of schedules, or add additional +fields to describe the schedule. + +// end::schedule2[] + +// tag::Expenses[] +=== Add Expenses +The expenses feature contains to two commands. + +* The command `addExpenses` allows the user to add expenses for each employee. + +==== Current Implementation for `addExpenses` command + +===== Phase 1 +The `addExpenses` command is handled by `AddExpensesCommandParser`. +The `AddExpensesCommandParser` implements the `Parser` interface, takes in input from the user as a form of argument, +The `AddExpensesCommandParser` will then check if user input contains too many prefixes or check for multiple entries of +same prefix. + +Next, the `AddExpensesCommandParser` will compute the value of `expensesAmount` to be added by summing the +the values of `travelExpenses`, `medicalExpenses` and `miscellaneousExpenses`. The `AddExpensesCommandParser` will then +create the `Expenses` and `EditExpensesDescriptor` objects. Other classes will handle validation of the input, check if +it has the correct input format. + +The five methods that helps to valid the input are: + +* `isValidEmployeeId` from `EmployeeId` class +* `isValidExpensesAmount` from `ExpensesAmount` class +* `isValidTravelExpenses` from `TravelExpenses` class +* `isValidMedicalExpenses` from `MedicalExpenses` class +* `isValidMiscellaneousExpenses` from `MiscellaneousExpenses` class + +[NOTE] +If the input does not comply with the required format, an error message will be shown to the user. + +Code snippet of `isValidEmployeeId` that shows the checking of the input compliance: + +[source, java] +---- +public static final String EMPLOYE_EXPENSES_ID_VALIDATION_REGEX = "\\d{3,}"; + +/** + * Returns true if a given string is a valid Employee Expenses Id. + */ + public static boolean isValidEmployeeId(String test) { + return test.matches(EMPLOYE_EXPENSES_ID_VALIDATION_REGEX); + } +---- + +Code snippet of `isValidExpensesAmount` that shows the checking of the input compliance: + +[source, java] +---- +public static final String EMPLOYE_EXPENSES_AMOUNT_VALIDATION_REGEX = "[-]?[0-9]+(.[0-9]{0,2})?"; + +/** + * Returns true if a given string is a valid Expenses Amount. + */ + public static boolean isValidExpensesAmount(String test) { + return test.matches(EMPLOYE_EXPENSES_AMOUNT_VALIDATION_REGEX); + } +---- + +===== Phase 2 +The `AddExpensesCommand` will take in the `Expenses` and `EditExpensesDescriptor` objects. + +The `AddExpensesCommand` will first check if there exists a person in the person list that has the same employeeId as +`Expenses` object using the condition `(!model.hasEmployeeId(toCheckEmployeeId)`. +The `AddExpensesCommand` will next check if there exists an expenses in the expenses list that has the same employeeId +as `Expenses` object using the condition `(!model.hasExpenses(toAddExpenses)`. + +Code snippet of condition that checks the person and expenses list for person and expenses with same employeeId: + +[source, java] +---- +if (!model.hasEmployeeId(toCheckEmployeeId)) { + throw new CommandException(MESSAGE_EMPLOYEE_ID_NOT_FOUND); + } else if (!model.hasExpenses(toAddExpenses)) { +---- + +** If Expenses List don't have expenses with same EmployeeId, +*** The `AddExpensesCommand` will then check if any of the fields will contains negative value or exceed the limit. An exception will be +thrown if at least one of the fields contains negative value or exceed the limit. +*** The `AddExpensesCommand` will then add the expenses into CHRS. + +Code snippet of `If Expenses List don't have expenses with same EmployeeId` +[source, java] +---- +else if (!model.hasExpenses(toAddExpenses)) { + if (Double.parseDouble(toAddExpenses.getExpensesAmount().toString()) < 0 + || Double.parseDouble(toAddExpenses.getTravelExpenses().toString()) < 0 + || Double.parseDouble(toAddExpenses.getMedicalExpenses().toString()) < 0 + || Double.parseDouble(toAddExpenses.getMiscellaneousExpenses().toString()) < 0) { + throw new CommandException(MESSAGE_NEGATIVE_LEFTOVER); + } else if (Double.parseDouble(toAddExpenses.getExpensesAmount().toString()) > MAX_TOTAL_EXPENSES + || Double.parseDouble(toAddExpenses.getTravelExpenses().toString()) > MAX_EXPENSES_AMOUNT + || Double.parseDouble(toAddExpenses.getMedicalExpenses().toString()) > MAX_EXPENSES_AMOUNT + || Double.parseDouble(toAddExpenses.getMiscellaneousExpenses().toString()) > MAX_EXPENSES_AMOUNT + ) { + throw new CommandException(MESSAGE_VALUE_OVER_LIMIT); + } else if (Double.parseDouble(toAddExpenses.getExpensesAmount().toString()) >= 0 + && Double.parseDouble(toAddExpenses.getTravelExpenses().toString()) >= 0 + && Double.parseDouble(toAddExpenses.getMedicalExpenses().toString()) >= 0 + && Double.parseDouble(toAddExpenses.getMiscellaneousExpenses().toString()) >= 0 + && Double.parseDouble(toAddExpenses.getExpensesAmount().toString()) <= MAX_TOTAL_EXPENSES + && Double.parseDouble(toAddExpenses.getTravelExpenses().toString()) <= MAX_EXPENSES_AMOUNT + && Double.parseDouble(toAddExpenses.getMedicalExpenses().toString()) <= MAX_EXPENSES_AMOUNT + && Double.parseDouble(toAddExpenses.getMiscellaneousExpenses().toString()) <= MAX_EXPENSES_AMOUNT + ) { + model.addExpenses(toAddExpenses); + model.commitExpensesList(); + messageToShow = MESSAGE_SUCCESS; + } +---- + +** If Expenses List have expenses with same EmployeeId, +*** The `AddExpensesCommand` will create an `expensesToEdit` object The `AddExpensesCommand` will then call +`createEditedExpenses` method with `expensesToEdit` and `EditExpensesDescriptor` object as parameters. + +Code snippet creating `expensesToEdit` object and calling `createdEditedExpenses` method +[source, java] +---- +else if (model.hasExpenses(toAddExpenses)) { + EmployeeIdExpensesContainsKeywordsPredicate predicatEmployeeId; + List employeeIdList = new ArrayList<>(); + List lastShownListExpenses; + + employeeIdList.add(toCheckEmployeeId.getEmployeeId().value); + predicatEmployeeId = new EmployeeIdExpensesContainsKeywordsPredicate(employeeIdList); + + model.updateFilteredExpensesList(predicatEmployeeId); + lastShownListExpenses = model.getFilteredExpensesList(); + + Expenses expensesToEdit = lastShownListExpenses.get(0); + Expenses editedExpenses = createEditedExpenses(expensesToEdit, editExpensesDescriptor); +---- + +*** The `createEditedExpenses` method will call `modifyExpensesAmount`, `modifyTravelExpenses`, `modifyMedicalExpenses`, +`modifyMiscellaneousExpenses`, methods with `expensesToEdit` and `EditExpensesDescriptor` object as parameter. + +Code snippet `createdEditedExpenses` method +[source, java] +---- + private Expenses createEditedExpenses(Expenses expensesToEdit, EditExpensesDescriptor + editExpensesDescriptor) { + assert expensesToEdit != null; + ExpensesAmount updatedExpensesAmount = null; + TravelExpenses updatedTravelExpenses = null; + MedicalExpenses updatedMedicalExpenses = null; + MiscellaneousExpenses updatedMiscellaneousExpenses = null; + + EmployeeId updatedEmployeeId = expensesToEdit.getEmployeeId(); + try { + updatedExpensesAmount = ParserUtil.parseExpensesAmount(modifyExpensesAmount(expensesToEdit, + editExpensesDescriptor)); + updatedTravelExpenses = ParserUtil.parseTravelExpenses(modifyTravelExpenses(expensesToEdit, + editExpensesDescriptor)); + updatedMedicalExpenses = ParserUtil.parseMedicalExpenses(modifyMedicalExpenses(expensesToEdit, + editExpensesDescriptor)); + updatedMiscellaneousExpenses = ParserUtil.parseMiscellaneousExpenses(modifyMiscellaneousExpenses( + expensesToEdit, editExpensesDescriptor)); + } catch (ParseException pe) { + pe.printStackTrace(); + } + + return new Expenses(updatedEmployeeId, updatedExpensesAmount, updatedTravelExpenses, updatedMedicalExpenses, + updatedMiscellaneousExpenses); + } +---- + +*** Each method will edit their respective field from `expensesToEdit` with the same field from `EditExpensesDescriptor`. +The method will set isNegativeLeftover to be true and set isOverLimit to be true if the values is below 0 or exceed max +value. + +Code snippet `modifyExpensesAmount` method, one of the methods being called +[source, java] +---- +private String modifyExpensesAmount (Expenses expensesToEdit, EditExpensesDescriptor + editExpensesDescriptor) { + NumberFormat formatter = new DecimalFormat("#0.00"); + String newExpensesAmount = expensesToEdit.getExpensesAmount().toString(); + double updateExpensesAmount = Double.parseDouble(newExpensesAmount); + String change = editExpensesDescriptor.getExpensesAmount().toString().replaceAll("[^0-9.-]", + ""); + updateExpensesAmount += Double.parseDouble(change); + if (updateExpensesAmount < 0) { + setIsNegativeLeftover(true); + } else if (updateExpensesAmount > MAX_TOTAL_EXPENSES) { + setIsOverLimit(true); + } else if (updateExpensesAmount >= 0) { + newExpensesAmount = String.valueOf(formatter.format(updateExpensesAmount)); + } + return newExpensesAmount; +} +---- +*** The `AddExpensesCommand` will check for isNegativeLeftover and isOverLimit. If both are false, the +`AddExpensesCommand` will then update the updated expenses into CHRS. + +Code snippet of isNegativeLeftover and isOverLimit check +[source, java] +---- +if (getIsNegativeLeftover()) { + throw new CommandException(MESSAGE_NEGATIVE_LEFTOVER); + } else if (getIsOverLimit()) { + throw new CommandException(MESSAGE_VALUE_OVER_LIMIT); + } else if (!getIsNegativeLeftover() && !getIsOverLimit()) { + messageToShow = MESSAGE_SUCCESS; + model.updateExpenses(expensesToEdit, editedExpenses); + model.commitExpensesList(); + } + model.updateFilteredExpensesList(PREDICATE_SHOW_ALL_EXPENSES); +} +---- + +The sequence diagram below illustrates the operation of the `AddExpensesCommand`: + +image::Add_ExpensesCommandSequenceDiagram.png[width="900"] + +==== Design Considerations +Aspect implementation of `AddExpenses` command: + +* **Alternative 1 (current choice):** Have a single command to add and modify expenses. +** Pros: User isn't require to ensure that there exist an expenses for the employee before modifying the expenses +** Cons: Have to handle more conditions and possibly more prone to bugs. +* **Alternative 2:** Have separate commands to add or modify expenses. +** Pros: User won't have the possibly of mixing up if they want to add new expenses or modify old expenses. +** Cons: Requires user to know more commands. + +Aspect: `Expenses List` Storage + +* **Alternative 1 (current choice):** Have expenses stored in XML format. +** Pros: Easier to read, code and test XML files. +** Cons: Can't compute or tabulate data from file. +* **Alternative 2:** Have schedules stored in CSV format. +** Pros: More efficient way of storing tabular data like expenses +** Cons: Harder to read as compared to XML file. + +==== Expenses Report: `expensesReport` _[Upcoming in 2.0]_ + +Generates an expenses report with the total expenses amount claimed and have not been claimed for a year _[Upcoming in 2.0]_ + +// end::Expenses[] + +// tag::filter[] +=== Filter Feature +The command `filter` allows the user to filter employees based on their respective department and/or position within the company. + +==== Current Implementation +The implementation of this command is separated into two phases. + +===== Phase 1 +The `filter` command's mechanism is facilitated by `FilterCommandParser`. +The `FilterCommandParser` implements the `Parser` interface, parses the arguments parameter, which is the input from the user, and creates a new `FilterCommand` object. +Apart from parsing the input, the `FilterCommandParser` also checks whether the input complies with the required format. +The checking of input compliance is assisted by a helper method, `didPrefixesAppearOnlyOnce`, which checks whether the department prefix and/or position prefix appears only once. + +[NOTE] +If the input does not comply with the required format, an error message will be shown to the user. + +Code snippet of the `parse` method within `FilterCommandParser` showing the checks for input compliance: +[source, java] +---- +public FilterCommand parse(String args) throws ParseException { + ... + ... + + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_DEPARTMENT, PREFIX_POSITION); + + if (trimmedArgs.isEmpty() || (!argMultimap.getValue(PREFIX_DEPARTMENT).isPresent() + && !argMultimap.getValue(PREFIX_POSITION).isPresent()) || !didPrefixesAppearOnlyOnce(trimmedArgs) + || !ACCEPTED_ORDERS.contains(sortOrder)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, FilterCommand.MESSAGE_USAGE)); + } + + ... + ... +} +---- + +Code snippet of the helper method `didPrefixesAppearOnlyOnce`: +[source, java] +---- +public boolean didPrefixesAppearOnlyOnce(String argument) { + String departmentPrefix = " " + PREFIX_DEPARTMENT.toString(); + String positionPrefix = " " + PREFIX_POSITION.toString(); + + return argument.indexOf(departmentPrefix) == argument.lastIndexOf(departmentPrefix) + && argument.indexOf(positionPrefix) == argument.lastIndexOf(positionPrefix); +} +---- + +If the input complies with the required format, further checks will be performed on the keywords to ensure that the keywords are valid. +This keywords checking is completed by the `processDepartmentKeywords` and `processPositionKeywords` methods that are both within the `FilterCommandParser` class. +This guide will focus on explaining how `processDepartmentKeywords` work instead of both as the methods `processDepartmentKeywords` and `processPositionKeywords` are similar. + +Code snippet of the `parse` method in `FilterCommandParser` calling `processDepartmentKeywords` method: + +[source, java] +---- +public FilterCommand parse(String args) throws ParseException { + ... + ... + + if (!argMultimap.getValue(PREFIX_DEPARTMENT).isPresent()) { + filterCommand.setIsDepartmentPrefixPresent(false); + } else if (argMultimap.getValue(PREFIX_DEPARTMENT).isPresent() + && !processDepartmentKeywords(argMultimap, filterCommand)) { + throw new ParseException(Department.MESSAGE_DEPARTMENT_KEYWORD_CONSTRAINTS); + } + + ... + ... +} +---- + +Code snippet of `processDepartmentKeywords` helper method: + +[source, java] +---- +public boolean processDepartmentKeywords(ArgumentMultimap argMultimap, FilterCommand command) { + String trimmedDepartment = (argMultimap.getValue(PREFIX_DEPARTMENT).get().trim()); + String[] departmentKeywords = trimmedDepartment.split("\\s+"); + + return validityCheckForDepartments(command, departmentKeywords); +} +---- + +The `processDepartmentKeywords` is further assisted by the `validityCheckForDepartments` method which calls another method `areDepartmentKeywordsValid` to check whether the given keywords comply with the `DEPARTMENT_KEYWORD_VALIDATION_REGEX`. +If the keywords from the user are all valid, the departmentPredicate will be created and passed to the `FilterCommand` object through a setter. + +[NOTE] +The `DEPARTMENT_KEYWORD_VALIDATION_REGEX` is a regular expression that accepts only spaces and case-insensitive alphabets. +It only allows 1 to 30 characters per keyword. + +Code snippet of `validityCheckForDepartments` and `areDepartmentKeywordsValid` methods in `FilterCommandParser` class: + +[source, java] +---- +public boolean validityCheckForDepartments(FilterCommand command, String[] keywords) { + if (!areDepartmentKeywordsValid(keywords)) { + return false; + } + + command.setIsDepartmentPrefixPresent(true); + command.setDepartmentPredicate(new DepartmentContainsKeywordsPredicate(Arrays.asList(keywords))); + return true; +} + +public boolean areDepartmentKeywordsValid(String[] keywords) { + for (String keyword: keywords) { + if (!keyword.matches(DEPARTMENT_KEYWORD_VALIDATION_REGEX)) { + return false; + } + } + return true; +} +---- + +===== Phase 2 +The `FilterCommand` is being executed in this phase. +`FilterCommand` first checks which prefix(es) is(are) present prior to calling the `updateFilteredPersonList` method, which updates `filteredPersonList` + with the employees of the relevant department(s) and/or position(s) that matches the keyword(s) input from the user. + Additionally, the methods `updateFilteredExpensesList` and `updateFilteredScheduleList` will also be called to update the `filteredExpensesList` and `filteredScheduleList` respectively to show only the matched employees' expenses and schedules. + +[NOTE] +If the keywords input from the user results in 0 person found. The CLI will show the user a list of currently available department(s) and/or position(s) in the PersonList. +The listing of available department(s) and/or position(s) depends on the presence of the prefix. + +Example: If `filter asc d/hello` results in 0 person found, the CLI will list all the currently available department(s) in CHRS. + +Code snippet of `FilterCommand` that checks which prefix is present prior to updating `filteredPersonList`, `filteredExpensesList` and `filteredScheduleList`: + +[source, java] +---- +public class FilterCommand extends Command { + ... + ... + + @Override + public CommandResult execute(Model model, CommandHistory history) { + requireNonNull(model); + String allAvailableDepartments = listAvailableDepartments(model); + String allAvailablePositions = listAvailablePositions(model); + + if (isDepartmentPrefixPresent && !isPositionPrefixPresent) { + model.updateFilteredPersonList(departmentPredicate, sortOrder); + } else if (isPositionPrefixPresent && !isDepartmentPrefixPresent) { + model.updateFilteredPersonList(positionPredicate, sortOrder); + } else if (isDepartmentPrefixPresent && isPositionPrefixPresent) { + model.updateFilteredPersonList(departmentPredicate.and(positionPredicate), sortOrder); + } -_{Explain here how the data encryption feature will be implemented}_ + EmployeeIdExpensesContainsKeywordsPredicate expensesPredicate = generateEmployeeIdExpensesPredicate(model); + EmployeeIdScheduleContainsKeywordsPredicate schedulePredicate = generateEmployeeIdSchedulePredicate(model); + model.updateFilteredExpensesList(expensesPredicate); + model.updateFilteredScheduleList(schedulePredicate); -// end::dataencryption[] + return new CommandResult(feedbackToUser(model, allAvailableDepartments, allAvailablePositions)); + } + + ... + ... +} +---- + +The result of the execution of `FilterCommand` is then encapsulated as a `CommandResult` object which is returned to the `LogicManager`. +During the execution of `FilterCommand`, the `filteredPersonList`, `filteredExpensesList` and `filteredScheduleList` will be updated accordingly to display only employees, that belongs to the department(s) + and/or hold the specified position(s) as specified by the user input. + +The sequence diagram below illustrates the operation of `filter` command: + +image::filterCommandSequenceDiagram.png[width="900"] + +==== Design Considerations +Aspect: Implementation of `filter` command + +* **Alternative 1 (current choice)**: One command to handle filtering by department(s) and/or position(s) +** Pros: Does not require several command and parser files to handle filtering of different fields +** Cons: Additional methods required to check for presence of prefix and input validity +* **Alternative 2**: Separate commands for filtering department and position, one for department and one for position +** Pros: Easy to implement as it follows the existing `find` command +** Cons: More commands to create and implementation can get quite complicated when filter requires both fields + +==== Future improvements: Keyword suggestion and auto complete feature _[Upcoming in 2.0]_ +This feature will suggest keywords when filtering or finding employee and the keyword will be auto-completed by pressing `TAB` key. _[Upcoming in 2.0]_ + +// end::filter[] === Logging @@ -813,15 +1807,32 @@ See this https://github.com/se-edu/addressbook-level4/pull/599[PR] for the step- [appendix] == Product Scope -*Target user profile*: +*Target user profile*: Human Resource (HR) Department + +*Product Scope*: Resolve accessibility problems and provide a centralised database for end users’ convenience + +*Value Proposition*: + +* A centralised system with specific fields required by HR. Some fields are as follow: -* 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 + Employees' salary and bonus -*Value proposition*: manage contacts faster than a typical mouse/GUI driven app + Employees' expenses claims, such as medical and transport + + Employees' leave balance + +* Working schedule of employees (in a timetable format, GUI) +* Recruitment post(s) to advertise the positions available within the company +* Generation of reports such as: + + Employees' claim history + + Employees' salary slip + + Employees' past training(s) + +* An internal messenger to enable communication between the HR department and employees +* Function to schedule training for employees [appendix] == User Stories @@ -831,35 +1842,274 @@ Priorities: High (must have) - `* * \*`, Medium (nice to have) - `* \*`, Low (un [width="59%",cols="22%,<23%,<25%,<30%",options="header",] |======================================================================= |Priority |As a ... |I want to ... |So that I can... -|`* * *` |new user |see usage instructions |refer to instructions when I forget how to use the App +|`* * *` |HR Staff |have filter functions|search for more specific data -|`* * *` |user |add a new person | +|`* * *` |HR Staff|edit current employee’s data |edit the employee’s data accordingly -|`* * *` |user |delete a person |remove entries that I no longer need +|`* * *` |HR Staff|add new employee’s data|add new employee’s data into the system -|`* * *` |user |find a person by name |locate details of persons without having to go through the entire list +|`* * *` |HR Staff|delete old employee’s data|remove employee’s details that are no longer required -|`* *` |user |hide <> by default |minimize chance of someone else seeing them by accident +|`* * *` |HR Staff (handling payroll)|have auto calculation of pay and bonus increments|avoid doing the calculations manually -|`*` |user with many persons in the address book |sort persons by name |locate a person easily -|======================================================================= +|`* * *` |New HR Staff|view all the available functions|know the available functions for use in the application + +|`* * *` |HR Staff (handling financial matters)|view all the expenses claim by each employee that has yet to be claimed|avoid keeping track of them manually + +|`* * *` |HR Staff|have a search function|search for employee’s data easily + +|`* * *` |HR Staff (handling financial matters)|add expenses claim by employee at individual level|keep track of employee’s expenses claim easily + +|`* * *` |HR Manager|have a function to add or remove leave/off|keep track employee’s leave/off easily. + +|`* * *` |HR Staff (handling financial matters)|remove expenses claim by employee after the claim is completed|keep track of employee’s expenses claim easily -_{More to be added}_ +|`* * *` |HR Staff (handling recruitment)|list all the past recruitment post(s) created|keep track of what position has already been posted for recruiting + +|`* * *` |HR Staff (handling recruitment)|add recruitment post(s)|put up new recruitment post(s) for newly available positions + +|`* * *` |HR Staff (handling recruitment)|remove recruitment post(s)|take down old recruitment post(s) for positions that have already been filled + +|`* * *` |HR Staff (handling recruitment)|edit recruitment post(s)|edit currently available recruitment post(s) at any time + +|`* *` |HR Director|have different user access|control the information access within the system + +|`* *` |HR Manager|generate a report of my employees' data|have an overview of the employees + +|`* *` |HR Staff|have departments tagged to each personnel|search for staffs in a specific department easily + +|`* *` |HR Staff|have shorter commands|reduce the amount of typing required + +|`* *` |HR Staff|have an autocomplete function |type my commands partially and have lower risks of typing wrong commands + +|`* *` |HR Manager|track who last made changes to the work schedule|find out who to clarify doubts with (if any) + +|`* *` |HR Manager|view the work schedule for each day |find out which employees are working on which day + +|`* *` |HR Manager|have a function to handle leave related issues |approve/disprove employee’s leave + +|`* *` |HR Staff|undo the last change|correct my mistake easily + +|`* *` |HR Staff|redo the last change|correct my undo easily + +|`* *` |Paranoid HR Staff|have the option to hide personally identifiable information|lower the chances of my information being leaked or seen +|======================================================================= [appendix] == Use Cases -(For all use cases below, the *System* is the `AddressBook` and the *Actor* is the `user`, unless specified otherwise) +(For all use cases below, the *System* is the `CHRS` and the *Actor* is the `HR Staff`, unless specified otherwise) + +[discrete] +=== Use case: UC01 - Employee data filter function +*MSS* + +1. User requests to filter employees based on department(s) and/or rank/position(s) +2. CHRS output the date of the employees of the specified department(s) and/or rank(s)/position(s) ++ +Use case ends. + +*Extensions* + +[none] +* 1a. CHRS detects an invalid department or position keyword(s). +[none] +** 1a1. CHRS informs the user that the keyword is invalid. ++ +Use case resumes at step 1. +* 1b. CHRS detects that the system holds no data of any employee. +[none] +** 1b1. CHRS informs user that the system has no records of any employee data and shows the currently available department(s) and/or rank(s)/position(s) to filter by. ++ +Use case ends. +* 1c. CHRS detects an invalid input format or missing details. +[none] +** 1c1. CHRS shows a message containing details of the command usage. ++ +Use case resumes at step 1. + +[discrete] +=== Use case: UC02 - Edit employee’s data +*MSS* + +1. User requests to list employees +2. CHRS shows a list of employees +3. User requests to edit a specific employee’s data in the list +4. CHRS updates relevant data of the employee ++ +Use case ends. + +*Extensions* + +[none] +* 2a. The list is empty. ++ +Use case ends. +* 3a. The given index is invalid. +[none] +** 3a1. CHRS shows an error message. ++ +Use case resumes at step 2. +* 3b. CHRS detects invalid input format or missing details. +[none] +** 3b1. CHRS shows an error message containing either the details of the command usage or parameters constraints. ++ +Use case resumes at step 2. + +[discrete] +=== Use case: UC03 - Add employee’s data +*MSS* + +1. User requests to add new employee’s data +2. CHRS stores the data of the employee ++ +Use case ends. + +*Extensions* + +[none] +* 1a. CHRS detects that the employee’s data already exists. +[none] +** 1a1. CHRS informs user that the data already exists. ++ +Use case resumes at step 1. +* 1b. CHRS detects invalid input format or missing details. +[none] +** 1b1. CHRS shows an error message containing either the details of the command usage or parameters constraints. ++ +Use case resumes at step 1. + +[discrete] +=== Use case: UC04 - Delete employee’s data +*MSS* + +1. User requests to list employees +2. CHRS shows a list of employees +3. User requests to delete employee’s data +4. CHRS deletes the all data associated to the employee ++ +Use case ends. + +*Extensions* + +[none] +* 2a. The list is empty. ++ +Use case ends. +* 3a. The given index is invalid. +[none] +** 3a1. CHRS shows an error message. ++ +Use case resumes at step 2. + +[discrete] +=== Actor: HR Staff (Handling payroll) +[discrete] +=== Use case: UC05 - Pay/Bonus increment or decrement +*MSS* + +1. User requests to list employees +2. CHRS shows a list of employees +3. User requests to modify employee(s) pay/bonus +4. CHRS updates the pay and/or bonus of the specified employee(s) ++ +Use case ends. + +*Extensions* + +[none] +* 2a. The list is empty. ++ +Use case ends. +* 3a. The given index is invalid. +[none] +** 3a1. CHRS shows an error message. ++ +Use case resumes at step 2. +* 3b. CHRS detects invalid input format or missing details. +[none] +** 3b1. CHRS shows an error message containing either the details of the command usage or parameters constraints. ++ +Use case resumes at step 2. + +[discrete] +=== Use case: UC06 - View available functions or commands +*MSS* + +1. User requests to view the available functions or commands in CHRS +2. CHRS shows the list of available functions or commands ++ +Use case ends. + +[discrete] +=== Actor: HR Staff +[discrete] +=== Use case: UC07 - List all CHRS data +*MSS* + +1. User requests to list all CHRS data +2. CHRS shows all CHRS data ++ +Use case ends. + +[discrete] +=== Use case: UC08 - Search function +*MSS* + +1. User requests to search for a certain employee’s data by either the name or employee ID +2. CHRS shows the data associated with the employee’s name or employee ID ++ +Use case ends. + +*Extensions* + +[none] +* 1a. CHRS detects that requested employee’s data does not exist. +[none] +** 1a1. CHRS informs user that the data does not exist. ++ +Use case ends. +* 1b. CHRS detects invalid input format or missing details. +[none] +** 1b1. CHRS shows an error message containing either the details of the command usage or parameters constraints. ++ +Use case resumes at step 1. [discrete] -=== Use case: Delete person +=== Actor: HR Staff (Handling financial matters) +[discrete] +=== Use case: UC09 – Add expenses +*MSS* + +1. User requests to add expenses claim by a certain employee +2. CHRS updates the expenses claim by that employee in the system ++ +Use case ends. + +*Extensions* + +[none] +* 1a. CHRS detects that requested employee does not exist. +[none] +** 1a1. CHRS informs user that the employee does not exist. ++ +Use case resumes at step 1. +* 1b. CHRS detects invalid input format or expenses exceed limit or result in negative value. +[none] +** 1b1. CHRS shows an error message containing either the details of the command usage or parameters constraints. ++ +Use case resumes at step 1. +[discrete] +=== Actor: HR Staff (Handling financial matters) +[discrete] +=== Use case: UC10 - Delete expenses *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 list expenses claims +2. CHRS shows a list of expenses claims +3. User requests to delete expenses claim by a certain employee +4. CHRS deletes the expenses claim by that employee in the system + Use case ends. @@ -869,52 +2119,185 @@ Use case ends. * 2a. The list is empty. + Use case ends. +* 3a. The given index is invalid. +[none] +** 3a1. CHRS shows an error message. ++ +Use case resumes at step 2. + +[discrete] +=== Actor: HR Staff (Handling recruitment) +[discrete] +=== Use case: UC11 – Add recruitment post +*MSS* + +1. User requests to add a new recruitment post +2. CHRS stores the details of the new recruitment post ++ +Use case ends. + +*Extensions* + +[none] +* 1a. CHRS detects that the recruitment post already exists. +[none] +** 1a1. CHRS informs user that the recruitment post already exists. ++ +Use case resumes at step 1. +* 1b. CHRS detects invalid input format or missing details. +[none] +** 1b1. CHRS shows an error message containing either the details of the command usage or parameters constraints. ++ +Use case resumes at step 1. +[discrete] +=== Actor: HR Staff (Handling recruitment) +[discrete] +=== Use case: UC12 – Delete recruitment post +*MSS* + +1. User requests to list recruitment posts +2. CHRS shows a list of recruitment posts +3. User requests to delete a recruitment post +4. CHRS deletes all details of the recruitment post ++ +Use case ends. + +*Extensions* + +[none] +* 2a. The list is empty. ++ +Use case ends. +* 3a. The given index is invalid. +[none] +** 3a1. CHRS shows an error message. ++ +Use case resumes at step 2. + +[discrete] +=== Actor: HR Staff (Handling recruitment) +[discrete] +=== Use case: UC13 - Edit recruitment post +*MSS* + +1. User requests to list recruitment posts +2. CHRS shows a list of recruitment posts +3. User requests to edit a specific recruitment post in the list +4. CHRS updates relevant data of the recruitment post ++ +Use case ends. + +*Extensions* + +[none] +* 2a. The list is empty. ++ +Use case ends. * 3a. The given index is invalid. +[none] +** 3a1. CHRS shows an error message. + +Use case resumes at step 2. +* 3b. CHRS detects invalid input format or missing details. [none] -** 3a1. AddressBook shows an error message. +** 3b1. CHRS shows an error message containing either the details of the command usage or parameters constraints. + Use case resumes at step 2. -_{More to be added}_ +[discrete] +=== Use case: UC14 - Add schedules +*MSS* -[appendix] -== Non Functional Requirements +1. User requests to add schedule for a certain employee +2. CHRS stores the schedule for that employee ++ +Use case ends. -. Should work on any <> as long as it has Java `9` or higher installed. -. Should be able to hold up to 1000 persons without a noticeable sluggishness in performance for typical usage. -. A user with above average typing speed for regular English text (i.e. not code, not system admin commands) should be able to accomplish most of the tasks faster using commands than using the mouse. +*Extensions* -_{More to be added}_ +[none] +* 1a. CHRS detects that requested employee does not exist. +[none] +** 1a1. CHRS informs user that the employee does not exist. ++ +Use case resumes at step 1. +* 1b. CHRS detects invalid input format or missing details. +[none] +** 1b1. CHRS shows an error message containing either the details of the command usage or parameters constraints. ++ +Use case resumes at step 1. +[none] +* 1c. CHRS detects that the schedule already exists. +[none] +** 1c1. CHRS informs user that the schedule already exists. ++ +Use case resumes at step 1. -[appendix] -== Glossary +[discrete] +=== Use case: UC15 - Delete schedules +*MSS* -[[mainstream-os]] Mainstream OS:: -Windows, Linux, Unix, OS-X +1. User requests to list schedules +2. CHRS shows a list of schedules +3. User requests to delete schedule of a certain employee +4. CHRS deletes the schedule of that employee in the system ++ +Use case ends. -[[private-contact-detail]] Private contact detail:: -A contact detail that is not meant to be shared with others +*Extensions* -[appendix] -== Product Survey +[none] +* 2a. The list is empty. ++ +Use case ends. +* 3a. The given index is invalid. +[none] +** 3a1. CHRS shows an error message. ++ +Use case resumes at step 2. -*Product Name* +[discrete] +=== Use case; UC16 - Calculate leaves taken by employee +*MSS* -Author: ... +1. User requests to calculate the amount of leaves taken by a specific employee in a certain year +2. CHRS shows the amount of leaves taken by that employee within that year ++ +Use case ends. -Pros: +*Extensions* -* ... -* ... +[none] +* 1a. CHRS detects that requested employee does not exist. +[none] +** 1a1. CHRS informs user that the employee does not exist. ++ +Use case resumes at step 1. -Cons: +[appendix] +== Non Functional Requirements -* ... -* ... +. Should have different user access rights +. Should be able to run on both 32-bit and 64-bit OS +. Should work on any <> as long as it has Java `9` or higher installed. +. Should be able to hold up to 1000 persons and still have a response time of 2 seconds for typical usage. +. A user with above average typing speed, for instance 60 words per minute, 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. [appendix] +== Glossary + +[[mainstream-os]] Mainstream OS:: +Windows, Linux, Unix, OS-X + +Personally identifiable information (PII):: +Any personal data, such as contact number, email and address, that could potentially identify an individual. + +Index:: +An increasing integer used to label employees’ ID. + +[appendix] + == Instructions for Manual Testing Given below are instructions to test the app manually. @@ -936,26 +2319,323 @@ 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 ... }_ - -=== Deleting a person - -. Deleting a person while all persons are listed - -.. Prerequisites: List all persons using the `list` command. Multiple persons in the list. +// tag::yisheng[] +=== Adding an employee + +. Adding an employee while the storage is empty + +.. Prerequisites: There must be no existing data in the system and the following `add` command must be executed first prior to all test cases. +... `clear` +... `add id/999999 n/Amanda Chan dob/16/06/1995 d/Finance r/Intern p/92346666 a/21 Lower Kent Ridge Rd e/amandachan@gmail.com s/1000.00` +.. Test case: `add id/999999 n/Amanda dob/31/07/1995 d/Finance r/Intern p/92346668 a/21 Lower Kent Ridge Rd e/amanda@gmail.com s/1000.00` + +Expected: No employee is added. Error details shown in the status message. The details of the employee with name "Amanda Chan" added during the prerequisite shown on Employees column. Status bar remains the same. +.. Test case: `add id/999998 n/Amanda dob/31/07/1995 d/Finance r/Intern p/92346668 a/21 Lower Kent Ridge Rd e/amandachan@gmail.com s/1000.00` + +Expected: No employee is added. Error details shown in the status message. The details of the employee with name "Amanda Chan" added during the prerequisite shown on Employees column. Status bar remains the same. +.. Test case: `add id/888888 n/Belinda Chan dob/30/06/1995 d/Finance r/Intern p/91234555 a/21 Lower Kent Ridge Rd e/belinda@gmail.com s/1000.00` + +Expected: Employee is added to the list. Details of the added employee shown in the status message. The details of all employees shown on Employees column. Timestamp in the status bar is updated. + +=== Editing an employee + +. Editing an employee while the storage is empty + +.. Prerequisites: There must be no existing data in the system and the following commands must be executed first prior to all test cases. +... `clear` +... `add id/999999 n/Amanda Chan dob/16/06/1995 d/Finance r/Intern p/92346666 a/21 Lower Kent Ridge Rd e/amandachan@gmail.com s/1000.00` +... `add id/888888 n/Belinda Chan dob/30/06/1995 d/Finance r/Intern p/91234555 a/21 Lower Kent Ridge Rd e/belinda@gmail.com s/1000.00` + +... `list` +.. Test case: `edit 1 d/Human Resource r/Manager` + +Expected: First employee's details is updated on the list. Details of the edited employee shown in the status message. The details of all employees shown on Employees column. Timestamp in the status bar is updated. +.. Test case: `list` followed by `edit 1 e/belinda@gmail.com` + +Expected: No employee is edited. Error details shown in the status message. The details of the employee with name "Belinda Chan" added during the prerequisite shown on Employees column. Status bar remains the same. +.. Test case: `edit 0` + +Expected: No changes on Employees column. No employee is edited. Error details shown in status message. Status bar remains the same. + +=== Filtering employees of a certain department and position + +. Filtering employees while storage is empty + +.. Prerequisites: There must be no existing data in the system and the following commands must be executed first prior to all test cases. +... `clear` +... `add id/999999 n/Amanda Chan dob/16/06/1995 d/Finance r/Intern p/92346666 a/21 Lower Kent Ridge Rd e/amandachan@gmail.com s/1000.00` +... `add id/888888 n/Belinda Chan dob/30/06/1995 d/Finance r/Manager p/91234555 a/21 Lower Kent Ridge Rd e/belinda@gmail.com s/1000.00` + +... `list` +.. Test case: `filter asc d/finance r/intern` + +Expected: The details of the employee with name "Amanda Chan" added during the prerequisite shown on Employees column. Number of employees listed shown in the status message. Status bar remains the same. +.. Test case: `filter asc d/Human Resource` + +Expected: No employees will be listed under Employees column. Number of employees listed and available departments in CHRS shown in the status message. Status bar remains the same. +.. Test case: `filter dsc d/finance d/human` + +Expected: No changes on Employees column. Error details shown in the status message. Status bar remains the same. + +=== Finding a specific employee + +. Filtering employees while storage is empty + +.. Prerequisites: There must be no existing data in the system and the following commands must be executed first prior to all test cases. +... `clear` +... `add id/999999 n/Amanda Chan dob/16/06/1995 d/Finance r/Intern p/92346666 a/21 Lower Kent Ridge Rd e/amandachan@gmail.com s/1000.00` +... `add id/888888 n/Belinda Chan dob/30/06/1995 d/Finance r/Manager p/91234555 a/21 Lower Kent Ridge Rd e/belinda@gmail.com s/1000.00` + +... `list` +.. Test case: `find amanda chan` + +Expected: The details of the employee with name "Amanda Chan" added during the prerequisite shown in Employees column. Number of employees listed shown in the status message. Status bar remains the same. +.. Test case: `find 888888` + +Expected: The details of the employee with name "Belinda Chan" added during the prerequisite shown in Employees column. Number of employees listed shown in the status message. Status bar remains the same. +.. Test case: `find 12345678` + +Expected: No changes on Employees column. Error details shown in the status message. Status bar remains the same. + +=== Selecting an employee + +. Selecting an employee while all employees are listed +.. Prerequisites: Ensure the storage is not empty and list all employees using the `list` command. Multiple employees should be in the list. +.. Test case: selectPerson 1 + +Expected: First employee is selected from the list. Selected employee index shown in the status message. Status bar remains the same. +.. Test case: selectPerson 0 + +Expected: If there are no employees selected previously, no employees will be selected. Else, the previously selected employee will remain selected. Error details shown in the status message. Status bar remains the same. +.. Other incorrect `selectPerson` commands to try: `selectPerson`, `selectPerson x` (where x is larger than the list size) + +Expected: Similar to previous. +// end::yisheng[] + +// tag::casper[] +=== Modifying an employee's pay + +. Modifying an employee's pay +.. Prerequisites: There must be at least 1 employee in the list. Salary are not allowed to exceed 999999.99(max value) or goes below 0.01(min value). + +Allocated Bonus should not be more than 24 months of Salary. + +.. Test case: modifyPay 1 s/1000 + +Expected: First employee's salary will be increased by 1000 if max salary value are not exceeded after modification. Timestamp in the status bar is updated. +Else, error details shown in the status message. Status bar remains the same. + +.. Test case: modifyPay 1 s/-1000 + +Expected: First employee's salary will be decreased by 1000 if salary does not goes below min value after modification. Timestamp in the status bar is updated. +Else, error details shown in the status message. Status bar remains the same. + +.. Test case: modifyPay 1 s/%100 + +Expected: First employee's salary will be doubled if the salary does not exceed the max value after modification. Timestamp in the status bar is updated. +Else, error details shown in the status message. Status bar remains the same. + +.. Test case: modifyPay 1 s/%-5 + +Expected: First employee's salary will be decreased by 5%. Timestamp in the status bar is updated. + +.. Test case: modifyPay 1 b/2 + +Expected: First employee's bonus will be changed to 2 months of current bonus. Timestamp in the status bar is updated. + +.. Test case: modifyPay 1 b/25 + +Expected: Error details shown in the status message. Status bar remains the same. + +.. Test case: modifyPay 1 s/1000 b/2 + +Expected: If max salary value are not exceeded after modification, +first employee's salary increased by 1000 and bonus value changed to 2 months of new salary. Timestamp in the status bar is updated. +Else, error details shown in the status message. Status bar remains the same. + +=== Modifying the pay of all the employees in the list + +. Modifying an employee's pay +.. Prerequisites: There must be at least 1 employee in the list. Salary are not allowed to exceed 999999.99(max value) or goes below 0.01(min value). + +Allocated Bonus should not be more than 24 months of Salary. + +.. Test case: modifyAllPay s/1000 + +Expected: All the employees' salary shown in the list will be increased by 1000 if none of +the employee salary in the list exceed max salary value after modification. Timestamp in the status bar is updated. +Else, error details shown in the status message. Status bar remains the same. + +.. Test case: modifyAllPay s/-1000 + +Expected: All the employees' salary shown in the list will be decreased by 1000 +if none of the employee salary goes below min value after modification. Timestamp in the status bar is updated. +Else, error details shown in the status message. Status bar remains the same. + +.. Test case: modifyAllPay s/%100 + +Expected: All the employees' salary shown in the list will be doubled +if none of the employee salary exceed the max value after modification. Timestamp in the status bar is updated. +Else, error details shown in the status message. Status bar remains the same. + +.. Test case: modifyAllPay s/%-5 + +Expected: All the employees' salary shown in the list will be decreased by 5%. Timestamp in the status bar is updated. + +.. Test case: modifyAllPay b/2 + +Expected: All the employees' bonus shown in the list will be changed to 2 months of current bonus. Timestamp in the status bar is updated. + +.. Test case: modifyAllPay b/25 + +Expected: Error details shown in the status message. Status bar remains the same. + +.. Test case: modifyAllPay s/1000 b/2 + +Expected: If max salary value are not exceeded for any employee shown in the list, +All the employees' salary shown in the list will be increased by 1000 and bonus value changed to 2 months of new salary. Timestamp in the status bar is updated. +Else, error details shown in the status message. Status bar remains the same. +// end::casper[] + +// tag::william[] +=== Add Schedule +. Add a single leave schedule +.. Prerequisites: There must be at least 3 valid employees in the observable employee list panel, + employee id 000001 among one of the employees and a empty schedule list. +.. Test case: addSchedule id/000001 d/02/02/2019 t/LEAVE + +Expected: New schedule added: Employee Id:000001 Date: 02/02/2019 Type: LEAVE + +.. Test case: addSchedule id/000001 d/04/02/2019 t/WORK + +Expected: New schedule added: Employee Id:000001 Date: 04/02/2019 Type: WORK + + +=== Add Works +. Add multiple work schedules for multiple employees. +.. Prerequisites: Completed `addSchedule` manual test above. +... Test case: addWorks d/02/02/2019 d/03/03/2019 + +Expected: New working schedules added for SOME of the observable employees whom are not yet added date: [02/02/2019, 03/03/2019] + Unable to schedule work for the following employees below whom are on leave: + Employee Id: 000001 Has leave on: [02/02/2019] + +=== Add Leaves +. Add multiple leave schedules for multiple employees. +.. Prerequisites: Completed `addWorks` manual test above. + +.. Test case: addLeaves d/04/02/2019 d/05/02/2019 + +Expected: New leave schedules added for SOME of the observable employees whom are not yet added date: [04/02/2019, 05/02/2019] + Unable to schedule work for the following employees below whom are on work: + Employee Id: 000001 Has work on: [04/02/2019] + +=== Calculate Leaves +. Calculate leaves taken by an employee in a year. +.. Prerequisites: Completed `addLeaves` manual test above. + +.. Test case: calculateLeaves id/000001 y/2019 + +Expected: Number of leaves scheduled for Employee 000001 year 2019 is: 2. + +=== Select Schedule +. Select a schedule +.. Prerequisites: Completed `calculateLeaves` manual test above. +.. Test case: selectSchedule 1 + +Expected: Selected Schedule: 1 + +=== Delete Schedule +. Delete a schedule +.. Prerequisites: Completed `selectSchedule` manual test above. +... Test Case: deleteSchedule 1 + +Expected: Deleted Schedule: Date: 02/02/2019 Type: LEAVE + +=== Delete Works +. Delete multiple work schedules +.. Prerequisites: Completed `deleteSchedule` manual test above. +.. Test Case: deleteWorks d/02/02/2019 d/03/03/2019 + +Expected: Working schedule deleted for all observable employees that contain date : [02/02/2019, 03/03/2019] + +=== Delete Leaves +. Delete multiple leave schedules +.. Prerequisites: Completed `deleteWorks` manual test above. +.. Test Case: deleteLeaves d/04/02/2019 d/05/02/2019 + +Expected: Leaves deleted for all observable employees that contain date : [04/02/2019, 05/02/2019] + + +// end::william[] + +// tag::vernon[] + +=== Add Expenses +. Add an expenses or modify existing expenses +.. Prerequisites: Employee with employeeId "000001" must exist in person list and doesn't have an expenses in the expenses list + +.. Test Case: addExpenses id/000001 tra/50 med/100 misc/150 + +Expected: Travel Expenses has value of 50.00. Medical Expenses has value of 100.00. Miscellaneous Expenses has value of 150.00. Total Expenses has value of 300.00. Timestamp in the status bar is updated. + ++ +Test Case: addExpenses id/000001 tra/100 med/50 misc/25 + +Expected: Travel expenses is increased to 150.00, medical expenses is increased to 150.00 and miscellaneous expenses is increased to 175.00, total expenses is increased to 475.00. Timestamp in the status bar is updated. + + +=== Delete Expenses +. Delete an expenses +.. There must be at least 1 employee in the observable expenses list panel +.. Test Case: deleteExpenses 1 + +Expected: Deletes expenses claim from expenses list with Index '1' in the expenses list. + +=== Select Expenses +. Select an expenses +.. There must be at least 1 employee in the observable expenses list panel +.. Test Case: selectExpenses 1 + +Expected: Select an expenses claim from expenses list with Index '1' in the expenses list. + + +// end::vernon[] + +// tag::ryan[] +=== Add A Recruitment Post +. Add a single recruitment post +.. Prerequisites: none of the fields should be blank. As for job position field, it takes in only characters with +maximum 20 character limits. As for working experience field, it takes in only integers from 0 to 30. As for +job description field, it takes in only characters with character limits from 1 to 200 and punctuation marks +including only comma, full stop and single right quote. At least one field should be different if there are more +than 1 post added. + + +.. Test case: addRecruitmentPost jp/IT Manager me/5 jd/To maintain the network server, and company's IT framework. + +Expected: New recruitment post is added: + Job Position: IT Manager + Minimal years of working experience: 5 + Job Description: To maintain the network server, and company's IT framework. + +.. Test case: addRecruitmentPost jp/IT Manager1 me/5 jd/To maintain the network server in company + +Expected: Job Position accepts only characters. It should not include numbers or should not be blank. +And the maximum length of the job position is 20 characters + +.. Test case: addRecruitmentPost jp/IT Manager me/31 jd/To maintain the network server in company + +Expected: Working Experience should only contain integers with length of at least 1 digit longAnd the range of the +working experience is from 0 to 30 + +.. Test case: addRecruitmentPost jp/IT Manager me/-1 jd/To maintain the network server in company + +Expected: Working Experience should only contain integers with length of at least 1 digit longAnd the range of the +working experience is from 0 to 30 + +.. Test case: addRecruitmentPost jp/IT Manager me/5 jd/To maintain the network server in company + +Expected: Working Experience should only contain integers with length of at least 1 digit longAnd the range of the +working experience is from 0 to 30 + +.. Test case: addRecruitmentPost jp/IT Manager me/5 jd/To maintain the network server in company! + +Expected: Job description accepts only characters. It should not include numbers or should not be blank. +For the purpose of using punctuation marks, it only allows comma, full stop and single right quote. +The length of job description is from 1 to 200 characters. + +=== Edit A Recruitment Post +. Edit a single recruitment post +.. Prerequisites: at least one recruitment post has been added to the recruitmentPost list panel. Minimal one field +should be different from the existing recruitment post that is about to be edited. + +.. Test case: editRecruitmentPost 1 jp/Accountants me/5 jd/To maintain the network server, and company's IT framework. + +Expected: Edited Recruitment Post: + Job Position: Accountants + Minimal years of working experience: 5 + Job Description: To maintain the network server, and company's IT framework. + +.. Test case: editRecruitmentPost 1 jp/Accountants me/5 jd/To maintain the network server, and company's IT framework. + + Expected: This job position already exists in the address book + +.. Test case: editRecruitmentPost 0 jp/Accountants me/5 jd/To maintain the network server, and company's IT framework. + + Expected: Invalid command format! + editRecruitmentPost: Edits the details of the recruitment post identified by the index number used in the + displayed recruitment list. Existing values will be overwritten by the input values. Parameters: + INDEX (must be a positive integer) [jp/Job Position] [me/Minimal Working experience] [jd/Job description] + Example: editRecruitmentPost 1 jp/IT Manager me/3 jd/To maintain the company server + +=== Select A Recruitment Post +. Select a recruitment post +.. Prerequisites: at least one recruitment post should be added before performing selectRecruitPostCommand. +.. Test case: selectRecruitmentPost 1 + +Expected: Selected Recruitment Post: 1 + +=== Delete A Recruitment Post +. Delete a recruitment post +.. Prerequisites: at least one recruitment post should be added before performing deleteRecruitmentPostCommand. +... Test Case: deleteRecruitmentPost 1 + +Expected: Deleted Recruitment Post: + Job Position: Accountants + Minimal years of working experience: 5 + Job Description: To maintain the network server, and company's IT framework. + +// end::ryan[] + +=== Deleting an employee + +. Deleting an employee while all employees are listed + +.. Prerequisites: List all employees using the `list` command. Multiple employees 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. + Expected: First employee is deleted from the list. Details of the deleted contact shown in the status message. Expenses and schedule related to the employee are also deleted from the ExpensesList and ScheduleList respectively. 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: No employee 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) + Expected: Similar to previous. - -_{ more test cases ... }_ - -=== Saving data - -. Dealing with missing/corrupted data files - -.. _{explain how to simulate a missing/corrupted file and the expected behavior}_ - -_{ more test cases ... }_ diff --git a/docs/UserGuide.adoc b/docs/UserGuide.adoc index 7e0070e12f49..b071ea85dcfd 100644 --- a/docs/UserGuide.adoc +++ b/docs/UserGuide.adoc @@ -1,4 +1,4 @@ -= AddressBook Level 4 - User Guide += Centralised Human Resource System - 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/CS2113-AY1819S1-T16-4/main -By: `Team SE-EDU` Since: `Jun 2016` Licence: `MIT` +By: `CS2113-T16-4` == 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! +Centralised Human Resource System (CHRS) is for the human resource personnel who use a desktop to manage the employee’s information. More importantly, CHRS is optimized for personnel with the preference of working with a Command Line Interface while still having the benefits of a Graphical User Interface (GUI). After familiarizing with the CLI functions, CHRS 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. +. Ensure you have Java version `9` or newer installed in your Computer. +. Download the latest `CHRS.jar` link:{repoURL}/releases[here]. +. Copy the file to the folder you want to use as the home folder for your Centralised Human Resource System. . Double-click the file to start the app. The GUI should appear in a few seconds. + image::Ui.png[width="790"] @@ -33,147 +33,661 @@ image::Ui.png[width="790"] e.g. typing *`help`* and pressing kbd:[Enter] will open the help window. . Some example commands you can try: -* *`list`* : lists all contacts -* **`add`**`n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` : adds a contact named `John Doe` to the Address Book. -* **`delete`**`3` : deletes the 3rd contact shown in the current list +* *`list`* : lists all employee details +* **`add`**`id/000001 n/John Doe dob/21-01-1993 p/98765432 e/johnd@example.com d/Finance r/Intern a/John street, block 123, #01-01 s/600`: adds an employee named `John Doe` to CHRS. +* **`delete`**`3` : deletes the employee with the index '3' in CHRS * *`exit`* : exits the app . Refer to <> for details of each command. +[[Fields]] +== Fields + +*An employee can have the following fields associated:* + +[width="90%",cols="25%,<15%,50%",options="header",] +|======================================================================= +|Field Name |Prefix |Limitations +|EMPLOYEEID |id/ |Employee Id should only contain exactly 6 numbers +|NAME |n/ |Name should only contain alphabets and spaces and it should be at least 3 characters long +|DATE_OF_BIRTH |dob/ |Date Of Birth should be in the format of DD/MM/YYYY and it only allows dates from 01/01/1900 to 31/12/2002 +|PHONE_NUMBER |p/ |Phone numbers should only contain numbers and it should be at least 3 digits long +|EMAIL |e/ |Email should be in the format of local-part@domain. + +The local-part should only contain alphanumeric characters and these special characters except the quotations("): "!#$%&'*+/=?`{\|}~^.-". + +The domain name part must start and end with alphanumeric characters, be at least 2 characters long and it should only contain alphanumeric characters and these special characters except the quotations("): ".-" +|DEPARTMENT |d/ |Department should only contain alphabets and spaces and it should be within 2 to 30 characters long +|RANK_POSITION |r/ |Rank/Position should only contain alphabets and spaces and it should be within 2 to 30 characters long +|ADDRESS |a/ |Address can take in any values without limitations +|SALARY |s/ |Takes in a maximum of 6 whole numbers and 2 decimal place. (Max. value in total = 999999.99) + +A "%" is allowed to be place before value for modification of Salary using percentage. + +Input value can be in negative to deduct salary, but the Salary should not be 0 or below + +Input value should not be blank +|BONUS |b/ |Only take in positive numbers with maximum of 2 decimal places from 0 to 24 +|======================================================================= + +*A schedule post should include things below* +// tag::addScheduleCommandField[] +[width="90%",cols="25%,<15%,50%",options="header",] +|======================================================================= +|Field Name +|Prefix +|Limitations + +|EMPLOYEEID +|id/ +|Employee Id should only contain exactly 6 numbers. + +|DATE +|d/ +|Date must be a valid date in the calendar DD/MM/YYYY]. Year must also fall into the range + of 2000-2099. Leading 0s can be omitted in day and month field. + You are not allowed to schedule for dates that have past today's date. + +|TYPE +|t/ +|Type can be either WORK or LEAVE only, case not sensitive. + +|======================================================================= +// end::addScheduleCommandField[] + +*An employee can incur the following expenses* + +[width="90%",cols="25%,<15%,50%",options="header",] +|======================================================================= +|Field Name +|Prefix +|Limitations + +|EMPLOYEEID +|id/ +|Employee Id should only contain exactly 6 numbers + +|TRAVELEXPENSES +|tra/ +|Maximum of 6 whole numbers and 2 decimal points + +Allow negative values + +Minimum 1 digit + + +|MEDICALEXPENSES +|med/ +|Maximum of 6 whole numbers and 2 decimal points + +Allow negative values + +Minimum 1 digit + +|MISCELLANEOUS +|misc/ +|Maximum of 6 whole numbers and 2 decimal points + +Allow negative values + +Minimum 1 digit + +|Total Expenses +|- +|Maximum of 7 whole numbers and 2 decimal points +|======================================================================= + +*A recruitment post should include things below* + +[width="90%",cols="25%,<15%,50%",options="header",] +|======================================================================= +|Field Name |Prefix |Limitations +|JOB_POSITION |jp/ | Job position accepts only characters. It must not be blank and should not include numbers and +punctuation mark. And users are not allowed to exceed the character limit which is from 1 to 20 +|MINIMAL_YEARS_OF_WORKING_EXPERIENCE |me/ | Minimal years of working experience must be integers and should not be blank +. And It is limited from 0 to 30 +|JOB_DESCRIPTION |jd/ | Job description accepts only characters and punctuation marks including only comma, +full stop and single right quote. It must not be blank and should not include numbers. And users are not +allowed to exceed the character limit which is from 1 to 200 +|======================================================================= + [[Features]] == Features ==== *Command Format* -* Words in `UPPER_CASE` are the parameters to be supplied by the user e.g. in `add n/NAME`, `NAME` is a parameter which can be used as `add n/John Doe`. +* Words in `UPPER_CASE` are the parameters field Name to be supplied by the user e.g. in `add n/NAME`, `NAME` is a parameter which can be used as `add n/John Doe`. Refer to <> for details of each field constraints. * 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. +* Commands which specified INDEX as part of the input requires a positive integer within the range of INT_MAX. +* All prefixes to each field such as `d/`, `r/`, `n/`, etc, are to be preceded with a space. ==== -=== Viewing help : `help` +// tag::addCommand[] +=== Add employee's data : `add` + +Adds employee's data to the database + +Format: `add id/EMPLOYEEID n/NAME dob/DATE_OF_BIRTH p/PHONE_NUMBER e/EMAIL d/DEPARTMENT r/RANK_POSITION a/ADDRESS s/SALARY t/[TAGS]...` + +Examples: + +* `add id/000001 n/John Doe dob/13/12/2000 p/98765432 e/johnd@example.com d/IT r/Assistant a/John street, block 123, #01-01 s/3000.00 t/FlyKite` + +Adds an employee with the fields listed above +* `add id/888888 n/Betsy dob/23/05/1987 p/95544332 e/betsy@example.com d/Account r/Manager a/Betsy street, block 3, #11-01 s/5000.00` + +Adds an employee with the fields listed above + +[NOTE] +Any usage of `add` command that will result in duplicated employeeId or phone number or email will be rejected. Additionally, duplicated name alongside date of birth will also be rejected. +// end::addCommand[] -Format: `help` +// tag::editCommand[] +=== Edit an existing employee’s data : `edit` -=== Adding a person: `add` +Edit an existing employee’s data in CHRS. -Adds a person to the address book + -Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]...` +Format: `edit INDEX [n/NAME] [p/PHONE_NUMBER] [a/ADDRESS] [e/EMAIL] [d/DEPARTMENT] [r/RANK_POSITION]` -[TIP] -A person can have any number of tags (including 0) +[NOTE] +Include at least one field alongside the INDEX. The existing values of the employee will be updated to the input values. 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` +* `edit 1 p/98765432 d/HR r/Manager` + +Edits employee at index 1 to have the new input of phone, department and rank/position -=== Listing all persons : `list` +[NOTE] +Any usage of `edit` command that will result in duplicated phone number or email will be rejected. Additionally, usage of this command to edit an employee's name to be the same as another employee who has the same date of birth will be rejected. +// end::editCommand[] -Shows a list of all persons in the address book. + -Format: `list` +// tag::findCommand[] +=== Locating persons by name or employee ID : `find` -=== Editing a person : `edit` +Find the employee's name that contains the input or find the employee id that matches the input. +The expenses and schedule list will also be updated accordingly to show only the matched employee(s)' expenses and schedule. -Edits an existing person in the address book. + -Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]...` +Format: `find [NAME] [EMPLOYEEID]` -**** -* Edits the person at the specified `INDEX`. The index refers to the index number shown in the displayed person list. The index *must be a positive integer* 1, 2, 3, ... -* At least one of the optional fields must be provided. -* Existing values will be updated to the input values. -* When editing tags, the existing tags of the person will be removed i.e adding of tags is not cumulative. -* You can remove all the person's tags by typing `t/` without specifying any tags after it. -**** +[NOTE] +Include only one of the fields, either Name or Employee Id. + +The NAME parameter is case-insensitive, i.e. The command `find john` will find the instances of JoHn, joHN, etc. Examples: -* `edit 1 p/91234567 e/johndoe@example.com` + -Edits the phone number and email address of the 1st person to be `91234567` and `johndoe@example.com` respectively. -* `edit 2 n/Betsy Crower t/` + -Edits the name of the 2nd person to be `Betsy Crower` and clears all existing tags. +* `find John` + +Find all instances of John, and his associated expenses and schedule +* `find 000001` + +Find the employee with employee ID `000001`, and his/her associated expenses and schedule + +[NOTE] +Any usage of `find` command will be rejected if it contains special characters or alphanumeric input. It accepts only either alphabets or numbers in a single input, not both. +// end::findCommand[] -=== Locating persons by name: `find` +// tag::filterCommand[] +=== Filter and list of specific employee's data : `filter` +Filters out the employees whose department and/or rank/position contains the keyword(s) input from the user and list them in ascending or descending name order. +The expenses and schedule list will also be updated accordingly to show only the matched employees' expenses and schedule. -Finds persons whose names contain any of the given keywords. + -Format: `find KEYWORD [MORE_KEYWORDS]` +Format: `filter SORT_ORDER [d/DEPARTMENT] [r/POSITION]` -**** -* The search is case insensitive. e.g `hans` will match `Hans` -* The order of the keywords does not matter. e.g. `Hans Bo` will match `Bo Hans` -* Only the name is searched. -* Only full words will be matched e.g. `Han` will not match `Hans` -* Persons matching at least one keyword will be returned (i.e. `OR` search). e.g. `Hans Bo` will return `Hans Gruber`, `Bo Yang` -**** +[NOTE] +The SORT_ORDER parameter should either be asc for ascending or dsc for descending. The SORT_ORDER parameter is case-insensitive. + +Include either department or rank/position or both, at least one of the field must be included alongside the sort order. The keywords are delimited by a space, i.e filter asc d/human resource would mean the keywords are "human" and "resource". The keywords matching is case-insensitive. Examples: -* `find John` + -Returns `john` and `John Doe` -* `find Betsy Tim John` + -Returns any person having names `Betsy`, `Tim`, or `John` +* `filter asc d/Human Resource r/Manager` + +List all employees whose department contains the keyword of either human or resource and rank/position contains the keyword of manager in ascending name order. +Expenses and schedule list will also be updated to show only matched employee(s)' expenses and schedule(s). +* `filter dsc d/Finance` + +List all employees whose department contains the keyword of finance in descending name order. +Expenses and schedule list will also be updated to show only matched employee(s)' expenses and schedule(s). -=== Deleting a person : `delete` +[NOTE] +Any usage of `filter` command that results in the same prefix appearing more than once will be rejected. Example: filter asc d/Human d/Finance will be rejected. +// end::filterCommand[] + +// tag::deleteCommand[] +=== Delete an employee’s data : `delete` + +Deletes the specified employee from the CHRS. +All existing schedules or expenses with the same employee id will be +deleted as well. -Deletes the specified person from the address book. + Format: `delete INDEX` -**** -* 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, ... -**** +Examples: + +* `delete 4` + +Deletes the employee with the index of '4' in the list. +// end::deleteCommand[] + +// tag::selectPersonCommand[] +=== Select an employee : `selectPerson` +Select an employee based on employee list's index ID. + +Format: `selectPerson INDEX` or `sp INDEX` + +Examples: + +* `selectPerson 1` + +Select the employee with the index of '1' in the list. +// end::selectPersonCommand[] + +// tag::modifyPayCommand[] +=== Modify the Salary and/or Bonus of an employee : `modifyPay` + +Modify the Salary and/or Bonus of the employee identified by the index. + +Format: `modifyPay INDEX [s/SALARY] [b/BONUS]` or `mp INDEX [s/SALARY] [b/BONUS]` + +[NOTE] +At least one of either Salary or Bonus must be included + +Bonus will be replaced by new values with every modification + +Examples: + +* `modifyPay 1 s/300` + +Modify the Salary of employee with index '1' with 300 increment +* `modifyPay 2 b/2` + +Modify the Bonus of employee with index '2' to 2 months of the salary +* `modifyPay 3 s/%5 b/1` + +Modify the Salary of employee with index '3' by 5% increment and Bonus to 1 month of salary + +[NOTE] +Modification to Salary and/or Bonus that causes negative values will be rejected + +Modification to Salary that goes higher than 999999.99 will be rejected since Salary can only hold a maximum +of 6 whole numbers and 2 decimal places. +// end::modifyPayCommand[] + +// tag::modifyAllPayCommand[] +=== Modify the Salary and/or Bonus all listed employee(s) : `modifyAllPay` + +Modify the Salary and/or Bonus of all the employee(s) shown on the display list. + +Format: `modifyAllPay [s/SALARY] [b/BONUS]` or `map [s/SALARY] [b/BONUS]` + +[NOTE] +At least one of either Salary or Bonus must be included + +Bonus will be replaced by new values with every modification + +Examples: + +* `modifyAllPay s/300` + +Modify the Salary of all the listed employee(s) by increment of 300 +* `modifyAllPay b/2` + +Modify the Bonus of all the listed employee(s) to 1 month of their salary +* `modifyAllPay s/%5 b/1` + +Modify the Salary of all the listed employee(s) by 5% increment and Bonus to 1 month of salary + +[NOTE] +Modification to Salary and/or Bonus that causes negative values to any employee(s) on the list will be rejected + +Modification to Salary that goes higher than 999999.99 will be rejected since Salary can only hold a maximum +of 6 whole numbers and 2 decimal places. +// end::modifyAllPayCommand[] + +// tag::addExpensesCommand[] +=== Add expenses claims under a specific employee : `addExpenses` +Add new expenses for employee or modify expenses if there already exists an expenses + +Format: `addExpenses id/EMPLOYEEID [tra/TRAVELEXPENSES] [med/MEDICALEXPENSES] [misc/MISCELLANEOUS]` + +Or `ae id/EMPLOYEEID [tra/TRAVELEXPENSES] [med/MEDICALEXPENSES] [misc/MISCELLANEOUS]` + +At least one of the fields, Travel Expenses, Medical Expenses, Miscellaneous Expenses must be included. + +Examples: + +* `ae id/000001 tra/111 med/222 misc/333` + +Creates a new expenses that contain 111.00, 222.00 and 333.00 for the above fields for employee with employee id +'000001'. + +Total Expenses will reflect 666.00. + +* `addExpenses id/000002 med/111 misc/222` + +Creates a new expenses that contain 111.00, 222.00 for the above fields for employee with employee id +'000002'. Expenses will contain 0.00 for fields not included in command. + +Total Expenses will reflect 333.00. ++ +`addExpenses id/000002 tra/111 med/222 misc/-111` + +Add 111.00, 222.00 and minus 111.00 for the above fields for employee with +employee id '000002'. + +Total Expenses will reflect 555.00. + +[NOTE] +Any usage of `addExpenses` Command that will result in negative value for any fields will be rejected. +// end::addExpensesCommand[] + +// tag::deleteExpensesCommand[] +=== Delete expenses claims of a specific employee : `deleteExpenses` + +Deletes expenses claim from an employee. + +Format: `deleteExpenses INDEX` or `de INDEX` + +Examples: + +* `deleteExpenses 1` + +Deletes expenses claim from employee with Index '1' in the list. +// end::deleteExpensesCommand[] + +// tag::selectExpensesCommand[] +=== Select an expenses claim : `selectExpenses` +Select an expenses based on expenses list index ID. User could use command_alias: 'se'. + +Format: `selectExpenses INDEX` or `se INDEX` + +Examples: + +* `selectExpenses 1` + +Select the expenses with the index of '1' in the list +// end::selectExpensesCommand[] + +// tag::clearExpensesCommand[] +=== Clear expenses list : `clearExpenses` +Clear all expenses at one go. User could use command_alias: 'ce'. + +Format: `clearExpenses` or `ce` +// end::clearExpensesCommand[] + +// tag::addScheduleCommand[] +=== Add a new schedule : `addSchedule` + +Add a new schedule for an employee. + +Format: `addSchedule id/EMPLOYEEID d/DATE t/TYPE` or `as id/EMPLOYEEID d/DATE t/TYPE` + +Examples: + +* `addSchedule id/000001 d/02/02/2019 t/WORK` + +Adds a new schedule for employee id 000001, date 02/02/2019, to work on that day. +* `as id/000001 d/03/03/2019 t/LEAVE` +Adds a new schedule for employee id 000001, date 03/03/2019, to be on leave for that day. + +[NOTE] +Scheduling with an employee id that doesn't exist in the address book will be rejected. + +You are not allowed to schedule for dates that have past today's date. + +Exact duplicate schedules will be rejected. +// end::addScheduleCommand[] + +// tag::deleteScheduleCommand[] +=== Delete an existing schedule : `deleteSchedule` +Deletes the specified schedule from the schedule list panel. + +Format: `deleteSchedule INDEX` or `ds INDEX` + +Examples: + +* `deleteSchedule 1` + +Deletes the schedule with the index of '1' in the list. +* `ds 2` + +Deletes the schedule with the index of '2' in the list. +// end::deleteScheduleCommand[] + +// tag::addWorksCommand[] +=== Add multiple work schedules : `addWorks` + +Add work schedules for all the observable employees in the employees list pane. + +Use `find` / `filter` / `list` to get the desired employees you wish to schedule. + +All observable employees in the employees list pane will be scheduled work schedule +with the date specified by the user. + +Format: `addWorks d/DATE [d/DATE]...` or `aw d/DATE [d/DATE]...` + +Examples: + +* `addWorks d/02/02/2019` + +Adds a new schedule for all observable employees in the employees list panel with +date 02/02/2019, to work on that day. +* `aw d/02/02/2019 d/03/03/2019` + +Adds new schedules for all observable employees in the employees list panel with +date 02/02/2019 and 03/03/2019, to work on that day. + +[NOTE] +For those employees whom are not scheduled with the date, the command will +create a new schedule. + +When all employees are scheduled with the date, +the command will tell the user that every observable employee in the list +have been scheduled with the specified date. + +You are not allowed to schedule for dates that have past today's date. +// end::addWorksCommand[] + +// tag::deleteWorksCommand[] +=== Delete multiple work schedules : `deleteWorks` +Delete work schedules for all the observable employees in the employees list pane. + +Use `find` / `filter` / `list` to get the desired employees you wish to schedule. + +All observable employees in the employees list pane will be deleted work schedules +with date specified by the user. + +Format: `deleteWorks d/DATE [d/DATE]...` or `dw d/DATE [d/DATE]...` + +Examples: + +* `deleteWorks d/02/02/2019` + +Deletes a schedule for all observable employees in the employees list panel with +date 02/02/2019, with work on that day. +* `dw d/02/02/2019 d/03/03/2019` + +Deletes schedules for all observable employees in the employees list panel with +date 02/02/2019 and 03/03/2019, with work on that day. + +[NOTE] +For those employees whom are scheduled with the date, the command will +delete the work schedule. + +When all employees are deleted with the scheduled date, +the command will tell the user every observable employees in the list does not have work schedule +on the specified date. +// end::deleteWorksCommand[] + +// tag::addLeavesCommand[] +=== Add multiple leave schedules : `addLeaves` +Add leave schedules for all the observable employees in the employees list pane. + +Use `find` / `filter` / `list` to get the desired employees you wish to schedule. + +All observable employees in the employees list pane will be scheduled +with leave and date specified by the user. + +Format: `addLeaves d/DATE [d/DATE]...` or `al d/DATE [d/DATE]...` + +Examples: + +* `addLeaves d/02/02/2019` + +Adds a new schedule for all observable employees in the employees list panel with +date 02/02/2019, to be on leave on that day. +* `al d/02/02/2019 d/03/03/2019` + +Adds new schedules for all observable employees in the employees list panel with +date 02/02/2019 and 03/03/2019, to be on leave on that day. + +[NOTE] +For those employees whom are not scheduled with the date, the command will +create a new leave schedule. + +When all employees are scheduled with the date, +the command will tell the user that every observable employees in the list +have been scheduled with the specified date. + +You are not allowed to schedule for dates that have past today's date. +// end::addLeavesCommand[] + +// tag::deleteLeavesCommand[] +=== Delete multiple leave schedules : `deleteLeaves` +Delete leave schedules for all the observable employees in the employees list pane. +Use `find` / `filter` / `list` to get the desired employees you wish to schedule. +All observable employees in the employees list pane will be deleted leave schedules +with date specified by the user. + +Format: `deleteLeaves d/DATE [d/DATE]...` or `dl d/DATE [d/DATE]...` + +Examples: + +* `deleteLeaves d/02/02/2019` + +Deletes a schedule for all observable employees in the employees list panel with. +date 02/02/2019, with leave on that day. +* `dl d/02/02/2019 d/03/03/2019` + +Deletes schedules for all observable employees in the employees list panel with. +date 02/02/2019 and 03/03/2019, with leave on that day. + +[NOTE] +For those employees whom are scheduled with the date, the command will +delete the schedule. + +When all employees are deleted with the scheduled date, +the command will tell the user every observable employee in the list does not have leave schedule +on the specified date. +// end::deleteLeavesCommand[] + +// tag::calculateLeavesCommand[] +=== Calculate total leaves in a year : `calculateLeaves` +Calculates total number of leaves scheduled for an employee for the entire specified year +in the schedule list. + +Format: `calculateLeaves id/EMPLOYEEID y/YYYY` or `cl id/EMPLOYEEID y/YYYY` + +Examples: + +* `calculateLeaves id/000001 date/2019` + +Calculates total number of leave scheduled for an employee id 000001 in whole of year 2019. +* `cl id/000002 date/2020` + +Calculates total number of leave scheduled for an employee id 000002 in whole of year 2020. +// end::calculateLeavesCommand[] + +// tag::selectScheduleCommand[] +=== Select schedule in the schedule list : `selectSchedule` +Select a schedule based on schedule index ID. + +Format: `selectSchedule INDEX` or `ss INDEX`. + +Examples: + +* `selectSchedule 1` + +Select the schedule with the index of '1' +* `ss 1` + +Select the schedule with the index of '1' +// end::selectScheduleCommand[] + +// tag::clearSchedulesCommand[] +=== Clear the entire schedule list : `clearSchedules` + +Clear the entire schedule list. + +Format: `clearSchedules`. or `cs`. + +Examples: + +* `clearSchedules` + +Clear the entire Schedule List. +// end::clearSchedulesCommand[] + +// tag::addRecruitmentPostCommand[] +=== Add new recruitment post : `addRecruitmentPost` +Add a recruitment post based on job position, minimal years of working experience and job description. + +Format: `addRecruitmentPost jp/JOB_POSITION me/MINIMAL_YEARS_OF_WORKING_EXPERIENCE jd/JOB_DESCRIPTION` or +`arp jp/JOB_POSITION me/MINIMAL_YEARS_OF_WORKING EXPERIENCE jd/JOB_DESCRIPTION` + +Examples: + +* `addRecruitmentPost jp/IT Manager me/3 jd/maintain the functionality of company server` + +Add an recruitment post with the available position called IT Manager, and the job requires minimal 3 years of +working experience in similar field. The job position requires the candidates' ability to maintain the +functionality of company server + + +* `arp jp/IT Manager me/3 jd/maintain the functionality of company server` + +Add an recruitment post with the available position called IT Manager, and the job requires minimal 3 years of +working experience in similar field. The job position requires the candidates' ability to maintain the +functionality of company server + +[NOTE] +Duplicate recruitment posts are not allowed. Minimal one field should be different. +// end::addRecruitmentPostCommand[] + +// tag::editRecruitmentPostCommand[] +=== Edit an existing recruitment post : `editRecruitmentPost` +Edit a recruitment post based on its index no. + +Format: `editRecruitmentPost [Index] jp/JOB POSITION me/MINIMAL YEARS OF WORKING EXPERIENCE jd/JOB DESCRIPTION` +or `erp [Index] jp/JOB POSITION me/MINIMAL YEARS OF WORKING EXPERIENCE jd/JOB DESCRIPTION` Examples: -* `list` + -`delete 2` + -Deletes the 2nd person in the address book. -* `find Betsy` + -`delete 1` + -Deletes the 1st person in the results of the `find` command. +* `editRecruitmentPost 1 jp/IT Manager me/3 jd/To maintain the company server` + +Edit the recruitment post with index 1. And the post information from job position, minimal +working experience to job description respectively changes to IT manager, minimal working +experience of 3 years in relevant field and the job description is to maintain the company +server. -=== Selecting a person : `select` +* `erp 1 jp/IT Manager me/3 jd/To maintain the company server` + +Edit the recruitment post with index 1. And the post information from job position, minimal +working experience to job description respectively changes to IT manager, minimal working +experience of 3 years in relevant field and the job description is to maintain the company +server. -Selects the person identified by the index number used in the displayed person list. + -Format: `select INDEX` +[NOTE] +Minimal one field should be changed in order to make the editing recruitment post function work. +// end::editRecruitmentPostCommand[] -**** -* Selects the person and loads the Google search page the person at the specified `INDEX`. -* The index refers to the index number shown in the displayed person list. -* The index *must be a positive integer* `1, 2, 3, ...` -**** +// tag::selectRecruitmentPostCommand[] +=== Select an existing recruitment post : `selectRecruitmentPost` +Select a recruitment post based on post index ID. + +Format: `selectRecruitmentPost INDEX` or `srp INDEX` Examples: -* `list` + -`select 2` + -Selects the 2nd person in the address book. -* `find Betsy` + -`select 1` + -Selects the 1st person in the results of the `find` command. +* `selectRecruitmentPost 1` + +Select the recruitment post with the index of '1' + +[NOTE] +At least one recruitment post should be added in the recruitment post list panel before performing this function. +// end::selectRecruitmentPostCommand[] +// tag::deleteRecruitmentPostCommand[] +=== Delete an existing recruitment post : `deleteRecruitmentPost` +Delete a recruitment post based on post index ID. + +Format: `deleteRecruitmentPost INDEX` or `drp INDEX` + +Examples: + +* `deleteRecruitmentPost 1` + +Deletes the recruitment post with the index of '1' + +[NOTE] +At least one recruitment post should be added in the recruitment post list panel before performing this function. +// end::deleteRecruitmentPostCommand[] + +// tag::clearRecruitmentPostCommand[] +=== Clear recruitment posts : `clearRecruitmentPost` +Clear all recruitment posts at one go. + +Format: `clearRecruitmentPost` or `crp` +// end::clearRecruitmentPostCommand[] + +// tag::listCommand[] +=== Listing all employees : `list` + +Shows a list of all employees, schedules, recruitment posts and expenses claims in CHRS. + +Format: `list` +// end::listCommand[] + +// tag::helpCommand[] +=== View all available functions or commands : `help` + +Views all the functions and commands that the CHRS have. + +Format: 'help' + +[NOTE] +A UserGuide popup window will appear for the user. +// end::helpCommand[] + +// tag::historyCommand[] === Listing entered commands : `history` -Lists all the commands that you have entered in reverse chronological order. + +Lists all the commands that you have entered in reverse chronological order. + Format: `history` [NOTE] ==== Pressing the kbd:[↑] and kbd:[↓] arrows will display the previous and next input respectively in the command box. ==== +// end::historyCommand[] // tag::undoredo[] === Undoing previous command : `undo` -Restores the address book to the state before the previous _undoable_ command was executed. + -Format: `undo` +Restores CHRS to the state before the previous _undoable_ command was executed. +If a _undoable_ command is executed after undo, then the command that was undone +will not be undoable anymore as it is already undone. -[NOTE] -==== -Undoable commands: those commands that modify the address book's content (`add`, `delete`, `edit` and `clear`). -==== +Format: `undo` Examples: @@ -181,19 +695,34 @@ Examples: `list` + `undo` (reverses the `delete 1` command) + -* `select 1` + +* `selectEmployee 1` + `list` + `undo` + -The `undo` command fails as there are no undoable commands executed previously. +The `undo` command fails as there are no undoable commands executed previously * `delete 1` + `clear` + `undo` (reverses the `clear` command) + `undo` (reverses the `delete 1` command) + +* `delete 1` + +`undo` (reverses the `delete 1` command) + +`clear`(`delete 1` commmand will no longer be undoable as it is already undone) + +`undo` (reverses the `clear` command) + +`undo` (no more commands to undo) + + +[NOTE] +==== +Undoable commands: those commands that modify CHRS content. +For commands similar functions to + +(list*, find*, filter*, select*, calculate*) +the command will not be able to undo or redo). +==== + === Redoing the previously undone command : `redo` -Reverses the most recent `undo` command. + +Reverses the most recent `undo` command. + Format: `redo` Examples: @@ -204,7 +733,7 @@ Examples: * `delete 1` + `redo` + -The `redo` command fails as there are no `undo` commands executed previously. +The `redo` command fails as there are no `undo` commands executed previously * `delete 1` + `clear` + @@ -212,16 +741,26 @@ The `redo` command fails as there are no `undo` commands executed previously. `undo` (reverses the `delete 1` command) + `redo` (reapplies the `delete 1` command) + `redo` (reapplies the `clear` command) + + +[NOTE] +==== +Redoable commands: those commands that modify CHRS content. +For commands similar functions to + +(list*, find*, filter*, select*, calculate*) +the command will not be able to undo or redo. +==== // end::undoredo[] === Clearing all entries : `clear` -Clears all entries from the address book. + +Clears all entries from CHRS. + Format: `clear` === Exiting the program : `exit` -Exits the program. + +Exits the program. + Format: `exit` === Saving the data @@ -229,32 +768,189 @@ Format: `exit` Address book data are saved in the hard disk automatically after any command that changes the data. + There is no need to save manually. -// tag::dataencryption[] +// tag::yisheng2.0Feature[] +=== Keyword suggestion and auto complete feature _[Upcoming in 2.0]_ +This feature will suggest keywords when filtering or finding employee and the keyword will be auto-completed by pressing `TAB` key. _[Upcoming in 2.0]_ +// end::yisheng2.0Feature[] + +// tag::casper2.0Feature[] +=== Employees' income report generation feature _[Upcoming in 2.0]_ +This feature will generate an employee payout report to show the break down of the Salary and Bonus in term of Base Salary, Bonus, Overtime Pay and CPF Contribution. _[Upcoming in 2.0]_ +// end::casper2.0Feature[] + +// tag::william2.0Feature[] +=== Send Calendar: `sendCalendar` _[Upcoming in 2.0]_ +Send calendar to the employee for employee to import schedule, sent using the Employee's email address. _[Upcoming in 2.0]_ +// end::william2.0Feature[] + +// tag::recruitment2.0Feature[] +=== Publish recruitment posts to job portal: `publishRecruitmentPost` _[Upcoming in 2.0]_ +`publishRecruitmentPost` command allows users to publish an existing recruitment post to job portal such as job street +website. _[Upcoming in 2.0]_ +// end::recruitment2.0Feature[] + +// tag::expenses2.0Feature[] +=== Expenses Report: `expensesReport` _[Upcoming in 2.0]_ +Generates an expenses report with the total expenses amount claimed and have not been claimed for a year _[Upcoming in 2.0]_ +// end::expenses2.0Feature[] + === Encrypting data files `[coming in v2.0]` -_{explain how the user can enable/disable data encryption}_ -// end::dataencryption[] +Upcoming in 2.0! Stay Tune! == 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. +*Q1*: How do I transfer my data to another Computer? + +*A1*: 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. + +*Q2*: What is the purpose of this app? + +*A2*: To provide a centralized and low-budget platform for personnel working in the Human Resource field to complete their work in a faster and more dynamic ways. + +*Q3*: Can I run it with Java version 8 and below? + +*A3*: It will be best to run it with Java version 9 and above. This is to prevent errors in running the application and ensure the functions are running as intended. + +*Q4*: How do I know what functions are there in the application? + +*A4*: The list of functions can be viewed by typing “help” in the command. This will direct the user to User Guide which have further elaborations and guide of how the functions are being used. == 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` -* *Find* : `find KEYWORD [MORE_KEYWORDS]` + -e.g. `find James Jake` -* *List* : `list` -* *Help* : `help` -* *Select* : `select INDEX` + -e.g.`select 2` -* *History* : `history` -* *Undo* : `undo` -* *Redo* : `redo` +[%header,cols=3*] +|=== +|Command Features +|Command Word +|Command Alias + +|Add employee’s data +|add +|- + +|Edit an existing employee's data +|edit +|- + +|Locating employee(s) by name or employee ID +|find +|- + +|Filter and list of specific employee's data +|filter +|- + +|Delete an employee's data +|delete +|- + +|Select an employee +|selectPerson +|sp + +|Modify the Salary and/or Bonus of an employee +|modifyPay +|mp + +|Modify the Salary and/or Bonus all listed employee(s) +|modifyAllPay +|map + +|Add expenses claims under a specific employee +|addExpenses +|ae + +|Delete expenses claims of a specific employee +|deleteExpenses +|de + +|Select an expenses claim +|selectExpenses +|se + +|Clear expenses list +|clearExpenses +|ce + +|Add a new schedule +|addSchedule +|as + +|Delete an existing schedule +|deleteSchedule +|ds + +|Add multiple work schedules +|addWorks +|aw + +|Delete multiple work schedules +|deleteWorks +|dw + +|Add multiple leave schedules +|addLeaves +|al + +|Delete multiple leave schedules +|deleteLeaves +|dl + +|Calculate total leaves in a year +|calculateLeaves +|cl + +|Select schedule in the schedule list +|selectSchedule +|ss + +|Clear the entire schedule list +|clearSchedules +|cs + +|Add new recruitment post +|addRecruitmentPost +|arp + +|Edit an existing recruitment post +|editRecruitmentPost +|erp + +|Select recruitment post +|selectRecruitmentPost +|srp + +|Delete recruitment post +|deleteRecruitmentPost +|drp + +|Clear recruitment posts +|clearRecruitmentPost +|crp + +|Listing all employees' +|list +|- + +|View all available functions or commands +|help +|- + +|Listing entered commands +|history +|- + +|Undoing previous command +|undo +|- + +|Redoing the previously undone command +|redo +|- + +|Clearing all entries +|clear +|- + +|Exiting the program +|exit +|- + +|=== +======= diff --git a/docs/diagrams/ModelComponentClassBetterOopDiagram.pptx b/docs/diagrams/ModelComponentClassBetterOopDiagram.pptx index d0561dfd305a..143bfdebc3b1 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..0e9bb602e19b 100644 Binary files a/docs/diagrams/ModelComponentClassDiagram.pptx and b/docs/diagrams/ModelComponentClassDiagram.pptx differ diff --git a/docs/diagrams/StorageComponentClassDiagram.pptx b/docs/diagrams/StorageComponentClassDiagram.pptx index be29a9de7ca6..1b7acc19e869 100644 Binary files a/docs/diagrams/StorageComponentClassDiagram.pptx and b/docs/diagrams/StorageComponentClassDiagram.pptx differ diff --git a/docs/images/Add_ExpensesCommandSequenceDiagram.png b/docs/images/Add_ExpensesCommandSequenceDiagram.png new file mode 100644 index 000000000000..0ceb788f5741 Binary files /dev/null and b/docs/images/Add_ExpensesCommandSequenceDiagram.png differ diff --git a/docs/images/ModelClassBetterOopDiagram.png b/docs/images/ModelClassBetterOopDiagram.png index 9ba8eb5e31d0..f113df56e3bd 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..03a79f68303d 100644 Binary files a/docs/images/ModelClassDiagram.png and b/docs/images/ModelClassDiagram.png differ diff --git a/docs/images/ModifyPayCommandSequenceDiagram.png b/docs/images/ModifyPayCommandSequenceDiagram.png new file mode 100644 index 000000000000..19f8e1c754c6 Binary files /dev/null and b/docs/images/ModifyPayCommandSequenceDiagram.png differ diff --git a/docs/images/Recruitment/addRecruitmentPost.png b/docs/images/Recruitment/addRecruitmentPost.png new file mode 100644 index 000000000000..e4ba3d982247 Binary files /dev/null and b/docs/images/Recruitment/addRecruitmentPost.png differ diff --git a/docs/images/Recruitment/deleteRecruitmentPost.png b/docs/images/Recruitment/deleteRecruitmentPost.png new file mode 100644 index 000000000000..932f274fb0f7 Binary files /dev/null and b/docs/images/Recruitment/deleteRecruitmentPost.png differ diff --git a/docs/images/Recruitment/editRecruitmentPost.png b/docs/images/Recruitment/editRecruitmentPost.png new file mode 100644 index 000000000000..39aa8f527c09 Binary files /dev/null and b/docs/images/Recruitment/editRecruitmentPost.png differ diff --git a/docs/images/Recruitment/selectRecruitmentPost.png b/docs/images/Recruitment/selectRecruitmentPost.png new file mode 100644 index 000000000000..590575e33be2 Binary files /dev/null and b/docs/images/Recruitment/selectRecruitmentPost.png differ diff --git a/docs/images/Schedule/addLeaves_seq.png b/docs/images/Schedule/addLeaves_seq.png new file mode 100644 index 000000000000..623eaf1b11f6 Binary files /dev/null and b/docs/images/Schedule/addLeaves_seq.png differ diff --git a/docs/images/Schedule/addSchedule_seq.png b/docs/images/Schedule/addSchedule_seq.png new file mode 100644 index 000000000000..f4d89a3abeed Binary files /dev/null and b/docs/images/Schedule/addSchedule_seq.png differ diff --git a/docs/images/Schedule/addWorks_seq.png b/docs/images/Schedule/addWorks_seq.png new file mode 100644 index 000000000000..fbcf193ec441 Binary files /dev/null and b/docs/images/Schedule/addWorks_seq.png differ diff --git a/docs/images/Schedule/calculateLeaves_seq.png b/docs/images/Schedule/calculateLeaves_seq.png new file mode 100644 index 000000000000..01fa62a94a62 Binary files /dev/null and b/docs/images/Schedule/calculateLeaves_seq.png differ diff --git a/docs/images/Schedule/deleteLeaves_seq.png b/docs/images/Schedule/deleteLeaves_seq.png new file mode 100644 index 000000000000..adbd5573f251 Binary files /dev/null and b/docs/images/Schedule/deleteLeaves_seq.png differ diff --git a/docs/images/Schedule/deleteSchedule_seq.png b/docs/images/Schedule/deleteSchedule_seq.png new file mode 100644 index 000000000000..7f86d056d015 Binary files /dev/null and b/docs/images/Schedule/deleteSchedule_seq.png differ diff --git a/docs/images/Schedule/deleteWorks_seq.png b/docs/images/Schedule/deleteWorks_seq.png new file mode 100644 index 000000000000..a7fc7bae3ca8 Binary files /dev/null and b/docs/images/Schedule/deleteWorks_seq.png differ diff --git a/docs/images/Schedule/schedule_class.png b/docs/images/Schedule/schedule_class.png new file mode 100644 index 000000000000..a0055c862b6d Binary files /dev/null and b/docs/images/Schedule/schedule_class.png differ diff --git a/docs/images/StorageClassDiagram.png b/docs/images/StorageClassDiagram.png index 7a4cd2700cbf..5aac92d0c699 100644 Binary files a/docs/images/StorageClassDiagram.png and b/docs/images/StorageClassDiagram.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 5ec9c527b49c..b9870fab2534 100644 Binary files a/docs/images/Ui.png and b/docs/images/Ui.png differ diff --git a/docs/images/UiClassDiagram.png b/docs/images/UiClassDiagram.png index 369469ef176e..858234331114 100644 Binary files a/docs/images/UiClassDiagram.png and b/docs/images/UiClassDiagram.png differ diff --git a/docs/images/chuazhenwei.png b/docs/images/chuazhenwei.png new file mode 100644 index 000000000000..12136fe9b401 Binary files /dev/null and b/docs/images/chuazhenwei.png differ diff --git a/docs/images/filterCommandSequenceDiagram.png b/docs/images/filterCommandSequenceDiagram.png new file mode 100644 index 000000000000..ac08fba488fc Binary files /dev/null and b/docs/images/filterCommandSequenceDiagram.png differ diff --git a/docs/images/limyisheng.png b/docs/images/limyisheng.png new file mode 100644 index 000000000000..22f6526a1d9f Binary files /dev/null and b/docs/images/limyisheng.png differ diff --git a/docs/images/ryanchen2018.png b/docs/images/ryanchen2018.png new file mode 100644 index 000000000000..c661e6ea32a5 Binary files /dev/null and b/docs/images/ryanchen2018.png differ diff --git a/docs/images/xiiaopanda.png b/docs/images/xiiaopanda.png new file mode 100644 index 000000000000..8475262d5291 Binary files /dev/null and b/docs/images/xiiaopanda.png differ diff --git a/docs/images/zhihong8888.png b/docs/images/zhihong8888.png new file mode 100644 index 000000000000..220e4eb5835e Binary files /dev/null and b/docs/images/zhihong8888.png differ diff --git a/docs/team/chuazhenwei.adoc b/docs/team/chuazhenwei.adoc new file mode 100644 index 000000000000..e42dddb737f1 --- /dev/null +++ b/docs/team/chuazhenwei.adoc @@ -0,0 +1,63 @@ += Chua Zhen Wei - Project Portfolio +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets + +== PROJECT: Centralised Human Resource System (CHRS) + +--- + +== Overview + +Centralised Human Resource System (CHRS) is an application for managing employees within the company. +The application is created to assist the Human Resource Department of the company to better manage the employees' information. +CHRS is capable of checking work schedule, creating recruitment posts, checking of expenses claims and storage of various information of each employee such as salary, department, position, etc. + +== Summary of contributions + +* *Project Management*: +** Assists in approving, reviewing and merging pull requests. + +* *Major enhancement*: added `modifyPay` and `modifyAllPay` command +** What it does: allows the user to modify the salary and bonus of the employees. Additionally, the `modifyAllPay` command enhance the functionality of `modifyPay` command by enabling the user to modify the salary and bonus of every employees shown in the list. +** Justification: This feature improves the product significantly because the user can indicate the changes to be made to the employee(s) salary based on values or percentage, and to the bonus based on months of salary. +** Highlights: These commands enhance the users experience in dealing with payroll due to the functionality to modify the salary and bonus just by indicating the value or percentage for salary, and number of months for bonus without having to calculate themselves which reduce chances for human error. + +* *Code contributed*: [https://nuscs2113-ay1819s1.github.io/dashboard/#=undefined&search=chuazhenwei&sort=displayName&since=2018-09-12&until=2018-11-04&timeframe=day&reverse=false&repoSort=true[Reposense Dashboard]] + +* *Other contributions*: + +** Test Case: +*** Wrote additional tests for existing features to increase coverage from 61.3% to 63.2% (https://github.com/CS2113-AY1819S1-T16-4/main/pull/224[#224]), 63.2% to 65.0% (https://github.com/CS2113-AY1819S1-T16-4/main/pull/226[#226]), 81.4% to 82.2% (https://github.com/CS2113-AY1819S1-T16-4/main/pull/237[#237]), 82.2% to 82.7% (https://github.com/CS2113-AY1819S1-T16-4/main/pull/238[#238]), 84.9% to 87.9% (https://github.com/CS2113-AY1819S1-T16-4/main/pull/246[#246]) + +** Documentation: +*** Updated Developer Guide on `modifyPay` feature, UI component diagram and instruction for manual testing. (Pull requests: https://github.com/CS2113-AY1819S1-T16-4/main/pull/105[#105], https://github.com/CS2113-AY1819S1-T16-4/main/pull/231/files[#231], https://github.com/CS2113-AY1819S1-T16-4/main/pull/241[#241]) +*** Updated User Guide to ensure information are up to date. (Pull requests: https://github.com/CS2113-AY1819S1-T16-4/main/pull/2[#2], https://github.com/CS2113-AY1819S1-T16-4/main/pull/4/files[#4], https://github.com/CS2113-AY1819S1-T16-4/main/pull/17/files[#17], https://github.com/CS2113-AY1819S1-T16-4/main/pull/19[#19], + https://github.com/CS2113-AY1819S1-T16-4/main/pull/112[#112], https://github.com/CS2113-AY1819S1-T16-4/main/pull/170[#170], https://github.com/CS2113-AY1819S1-T16-4/main/pull/173[#173], https://github.com/CS2113-AY1819S1-T16-4/main/pull/187[#187]) + +** Community: +*** PRs reviewed (With non-trivial comments) (Pull requests: https://github.com/CS2113-AY1819S1-T16-4/main/pull/73[#73], https://github.com/CS2113-AY1819S1-T16-4/main/pull/74[#74], https://github.com/CS2113-AY1819S1-T16-4/main/pull/76[#76], https://github.com/CS2113-AY1819S1-T16-4/main/pull/120[#120], https://github.com/CS2113-AY1819S1-T16-4/main/pull/133[#133], https://github.com/CS2113-AY1819S1-T16-4/main/pull/136[#136], +https://github.com/CS2113-AY1819S1-T16-4/main/pull/137[#137], https://github.com/CS2113-AY1819S1-T16-4/main/pull/139[#139], https://github.com/CS2113-AY1819S1-T16-4/main/pull/140[#140], https://github.com/CS2113-AY1819S1-T16-4/main/pull/240[#240], https://github.com/CS2113-AY1819S1-T16-4/main/pull/248[#248]) +*** Reported bugs and suggestions for other teams in the class (examples: https://github.com/CS2113-AY1819S1-W12-2/main/issues/100[1], https://github.com/CS2113-AY1819S1-W12-2/main/issues/105[2], https://github.com/CS2113-AY1819S1-W12-2/main/issues/110[3], https://github.com/CS2113-AY1819S1-W12-2/main/issues/114[4], +https://github.com/CS2113-AY1819S1-W12-2/main/issues/119[5], https://github.com/CS2113-AY1819S1-W12-2/main/issues/123[6], https://github.com/CS2113-AY1819S1-W12-2/main/issues/135[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=modifyPayCommand] + +include::../UserGuide.adoc[tag=modifyAllPayCommand] + +include::../UserGuide.adoc[tag=casper2.0Feature] + + +== 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=modifyPay] diff --git a/docs/team/johndoe.adoc b/docs/team/johndoe.adoc deleted file mode 100644 index 453c2152ab9d..000000000000 --- a/docs/team/johndoe.adoc +++ /dev/null @@ -1,72 +0,0 @@ -= John Doe - Project Portfolio -:site-section: AboutUs -:imagesDir: ../images -:stylesDir: ../stylesheets - -== PROJECT: AddressBook - Level 4 - ---- - -== Overview - -AddressBook - Level 4 is a desktop address book application used for teaching Software Engineering principles. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10 kLoC. - -== Summary of contributions - -* *Major enhancement*: added *the ability to undo/redo previous commands* -** What it does: allows the user to undo all previous commands one at a time. Preceding undo commands can be reversed by using the redo command. -** Justification: This feature improves the product significantly because a user can make mistakes in commands and the app should provide a convenient way to rectify them. -** Highlights: This enhancement affects existing commands and commands to be added in future. It required an in-depth analysis of design alternatives. The implementation too was challenging as it required changes to existing commands. -** Credits: _{mention here if you reused any code/ideas from elsewhere or if a third-party library is heavily used in the feature so that a reader can make a more accurate judgement of how much effort went into the feature}_ - -* *Minor enhancement*: added a history command that allows the user to navigate to previous commands using up/down keys. - -* *Code contributed*: [https://github.com[Functional code]] [https://github.com[Test code]] _{give links to collated code files}_ - -* *Other contributions*: - -** Project management: -*** Managed releases `v1.3` - `v1.5rc` (3 releases) on GitHub -** Enhancements to existing features: -*** Updated the GUI color scheme (Pull requests https://github.com[#33], https://github.com[#34]) -*** Wrote additional tests for existing features to increase coverage from 88% to 92% (Pull requests https://github.com[#36], https://github.com[#38]) -** Documentation: -*** Did cosmetic tweaks to existing contents of the User Guide: https://github.com[#14] -** Community: -*** PRs reviewed (with non-trivial review comments): https://github.com[#12], https://github.com[#32], https://github.com[#19], https://github.com[#42] -*** Contributed to forum discussions (examples: https://github.com[1], https://github.com[2], https://github.com[3], https://github.com[4]) -*** Reported bugs and suggestions for other teams in the class (examples: https://github.com[1], https://github.com[2], https://github.com[3]) -*** Some parts of the history feature I added was adopted by several other class mates (https://github.com[1], https://github.com[2]) -** Tools: -*** Integrated a third party library (Natty) to the project (https://github.com[#42]) -*** Integrated a new Github plugin (CircleCI) to the team repo - -_{you can add/remove categories in the list above}_ - -== Contributions to the User Guide - - -|=== -|_Given below are sections I contributed to the User Guide. They showcase my ability to write documentation targeting end-users._ -|=== - -include::../UserGuide.adoc[tag=undoredo] - -include::../UserGuide.adoc[tag=dataencryption] - -== Contributions to the Developer Guide - -|=== -|_Given below are sections I contributed to the Developer Guide. They showcase my ability to write technical documentation and the technical depth of my contributions to the project._ -|=== - -include::../DeveloperGuide.adoc[tag=undoredo] - -include::../DeveloperGuide.adoc[tag=dataencryption] - - -== PROJECT: PowerPointLabs - ---- - -_{Optionally, you may include other projects in your portfolio.}_ diff --git a/docs/team/limyisheng.adoc b/docs/team/limyisheng.adoc new file mode 100644 index 000000000000..fc2b58e514ed --- /dev/null +++ b/docs/team/limyisheng.adoc @@ -0,0 +1,77 @@ += Lim Yi Sheng - Project Portfolio +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets + +== PROJECT: Centralised Human Resource System (CHRS) + +--- + +== Overview + +Centralised Human Resource System (CHRS) is an application for managing employees within the company. +The application is created to assist the Human Resource Department of the company to better manage the employees' information. +CHRS is capable of checking work schedule, creating recruitment posts, checking of expenses claims and storage of various information of each employee such as salary, department, position, etc. + +== Summary of contributions + +* *Major enhancement*: Added `filter` and enhanced `find` command +** What it does: The `filter` command allows user to filter out employees of a certain department, rank/position or both and the `find` command allows user to search for a specific employee via their name or employee ID. +** Justification: This feature improves the product significantly as the user is able to search for the required employee's details quickly. +** Highlights: This enhancement enhances the user experience as the user can now search for an employee's details without having the need to go through the whole list of employees. This feature required in-depth analysis of possible design alternatives and in-depth understanding of the HR needs when searching. The implementation of this feature was challenging as it required enhancement to existing commands and understanding of predicates. + +* *Minor enhancements*: +*** Added sorting to PersonList, ExpensesList and ScheduleList to ensure that the lists stay sorted in ascending order. (Pull request: https://github.com/CS2113-AY1819S1-T16-4/main/pull/219[#219]) +*** Enhanced `add` and `edit` commands to reject having employees with duplication in these fields: `EmployeeId` OR `Email` OR `Phone` OR `Name` and `DateOfBirth`. Should any attempt of duplication occur, the application will display the employee being duplicated and reject the command. (Pull requests: https://github.com/CS2113-AY1819S1-T16-4/main/pull/124[#124], https://github.com/CS2113-AY1819S1-T16-4/main/pull/134[#134], https://github.com/CS2113-AY1819S1-T16-4/main/pull/136[#136], https://github.com/CS2113-AY1819S1-T16-4/main/pull/219[#219]) + +* *Code contributed*: [https://nuscs2113-ay1819s1.github.io/dashboard/#=undefined&search=limyisheng&sort=displayName&since=2018-09-12&until=2018-11-04&timeframe=day&reverse=false&repoSort=true[Reposense Dashboard]] + +* *Other contributions*: + +** Project management: +*** Managed releases `v1.1` - `v1.4` (5 releases) on GitHub +**** Managed .jar file release and description of each release +*** Managed the approval of pull requests +*** Set up issue tracker, milestones and labels +*** Ensures that the team is on track and is able to complete each milestone's requirements +** Enhancements to existing features: +*** Added `EmployeeId`, `DateOfBirth`, `Department`, `Rank/Position`, `Salary` and `Bonus` fields (Pull request: https://github.com/CS2113-AY1819S1-T16-4/main/pull/18[#18]) +** Test Cases: +*** Wrote additional tests for existing features to increase coverage from 88.7% to 92.6% (https://github.com/CS2113-AY1819S1-T16-4/main/pull/240[#240]), 61.2% to 63.7% (https://github.com/CS2113-AY1819S1-T16-4/main/pull/223[#223]), 53.7% to 61.2% (https://github.com/CS2113-AY1819S1-T16-4/main/pull/221[#221]) and 72.9% to 74.1% (https://github.com/CS2113-AY1819S1-T16-4/main/pull/104[#104]) +** Documentation: +*** Updated Read Me (Pull requests: https://github.com/CS2113-AY1819S1-T16-4/main/pull/3/[#3], https://github.com/CS2113-AY1819S1-T16-4/main/pull/235[#235]) +*** Updated About Us to include team members' images, roles and responsibilities alongside GitHub and Portfolio links (Pull requests: https://github.com/CS2113-AY1819S1-T16-4/main/pull/1[#1], https://github.com/CS2113-AY1819S1-T16-4/main/pull/3[#3], https://github.com/CS2113-AY1819S1-T16-4/main/pull/8[#8], https://github.com/CS2113-AY1819S1-T16-4/main/pull/41[#41], https://github.com/CS2113-AY1819S1-T16-4/main/pull/127[#127]) +*** Updated Developer Guide on `filter` feature alongside model component, user stories, use cases and instructions for manual testing (Pull requests: https://github.com/CS2113-AY1819S1-T16-4/main/pull/11[#11], https://github.com/CS2113-AY1819S1-T16-4/main/pull/13[#13], https://github.com/CS2113-AY1819S1-T16-4/main/pull/14[#14], https://github.com/CS2113-AY1819S1-T16-4/main/pull/15[#15], https://github.com/CS2113-AY1819S1-T16-4/main/pull/97[#97], https://github.com/CS2113-AY1819S1-T16-4/main/pull/109[#109], https://github.com/CS2113-AY1819S1-T16-4/main/pull/235[#235], https://github.com/CS2113-AY1819S1-T16-4/main/pull/240[#240]) +*** Updated User Guide to ensure `add`, `edit`, `find` and `filter` commands alongside the fields added by me are up to date (Pull requests: https://github.com/CS2113-AY1819S1-T16-4/main/pull/95[#95], https://github.com/CS2113-AY1819S1-T16-4/main/pull/156[#156], https://github.com/CS2113-AY1819S1-T16-4/main/pull/167[#167], https://github.com/CS2113-AY1819S1-T16-4/main/pull/172[#172], https://github.com/CS2113-AY1819S1-T16-4/main/pull/183[#183]) +** Community: +*** PRs reviewed (with non-trivial review comments): https://github.com/CS2113-AY1819S1-T16-4/main/pull/73[#73], https://github.com/CS2113-AY1819S1-T16-4/main/pull/123[#123], https://github.com/CS2113-AY1819S1-T16-4/main/pull/125[#125], https://github.com/CS2113-AY1819S1-T16-4/main/pull/130[#130], https://github.com/CS2113-AY1819S1-T16-4/main/pull/140[#140], https://github.com/CS2113-AY1819S1-T16-4/main/pull/182[#182], https://github.com/CS2113-AY1819S1-T16-4/main/pull/218[#218] +*** Contributed to forum discussions (examples: https://github.com/nusCS2113-AY1819S1/forum/issues/40[1], https://github.com/nusCS2113-AY1819S1/forum/issues/62[2]) +*** Reported bugs and suggestions for other teams in the class (examples: https://github.com/CS2113-AY1819S1-W13-4/main/issues/106[1], https://github.com/CS2113-AY1819S1-W13-4/main/issues/104[2], https://github.com/CS2113-AY1819S1-W13-4/main/issues/99[3], https://github.com/CS2113-AY1819S1-W13-4/main/issues/109[4], https://github.com/CS2113-AY1819S1-W13-4/main/issues/122[5]) +** Tools: +*** Travis-CI (https://github.com/CS2113-AY1819S1-T16-4/main/pull/3[#3]) and AppVeyor (https://github.com/LimYiSheng/main/pull/9[#9]) + +== 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=addCommand] + +include::../UserGuide.adoc[tag=editCommand] + +include::../UserGuide.adoc[tag=findCommand] + +include::../UserGuide.adoc[tag=filterCommand] + +include::../UserGuide.adoc[tag=yisheng2.0Feature] + + +== 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=filter] diff --git a/docs/team/ryanchen2018.adoc b/docs/team/ryanchen2018.adoc new file mode 100644 index 000000000000..dfbea95d4907 --- /dev/null +++ b/docs/team/ryanchen2018.adoc @@ -0,0 +1,100 @@ += Chen Qunming - Project Portfolio +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets + +== PROJECT: Centralised Human Resource System (CHRS) + +--- + +== Overview + +Centralised Human Resource System (CHRS) is an application for managing employees within the company. +The application is created to assist the Human Resource Department of the company to better manage the employees' information. +CHRS is capable of checking work schedule, creating recruitment posts, checking of expenses claims and storage of various information of each employee such as salary, department, position, etc. + +== Summary of contributions + +* *Project Management*: +** Assists in approving, reviewing and merging pull requests. + +* *Major enhancement*: added `addRecruitmentPostCommand`, `deleteRecruitmentPostCommand`, `clearRecruitmentPostCommand` + `editRecruitmentPostCommand`, `selectRecruitmentPostCommand` +** What it does: Allow users to manage recruitment posts for the company. +** Justification: This feature improves the product significantly because managing recruitment is one of the main responsibilities for HR admin. +** Highlights: This enhancement provides an integrated function for HR admin to manage internal recruitment posts easily before publish to the public. + +* *Code contributed*: [https://nuscs2113-ay1819s1.github.io/dashboard/#=undefined&search=ryanchen2018&sort=displayName&since=2018-09-12&until=2018-11-04&timeframe=day&reverse=false&repoSort=true[Reposense Dashboard]] + +* *Other contributions*: + +** Test cases: +*** Wrote additional tests for existing features to increase coverage from 70.2% to 80.4%(https://github.com/CS2113-AY1819S1-T16-4/main/pull/229[#229]), +82.3% to 84.9%(https://github.com/CS2113-AY1819S1-T16-4/main/pull/239[#239]), +84.9% to 88.7%(https://github.com/CS2113-AY1819S1-T16-4/main/pull/243[#243]) + +** Documentation: +*** Update developer guide for recruitment feature(https://github.com/CS2113-AY1819S1-T16-4/main/pull/101[#101], +https://github.com/CS2113-AY1819S1-T16-4/main/pull/107[#107], +https://github.com/CS2113-AY1819S1-T16-4/main/pull/108[#108], +https://github.com/CS2113-AY1819S1-T16-4/main/pull/259[#259], +https://github.com/CS2113-AY1819S1-T16-4/main/pull/260[#260]) + +*** Update user guide for recruitment feature(https://github.com/CS2113-AY1819S1-T16-4/main/pull/108[#108], +https://github.com/CS2113-AY1819S1-T16-4/main/pull/125[#125], +https://github.com/CS2113-AY1819S1-T16-4/main/pull/147[#147], +https://github.com/CS2113-AY1819S1-T16-4/main/pull/149[#149], +https://github.com/CS2113-AY1819S1-T16-4/main/pull/151[#151], +https://github.com/CS2113-AY1819S1-T16-4/main/pull/155[#155], +https://github.com/CS2113-AY1819S1-T16-4/main/pull/157[#157], +https://github.com/CS2113-AY1819S1-T16-4/main/pull/160[#160], +https://github.com/CS2113-AY1819S1-T16-4/main/pull/175[#175], +https://github.com/CS2113-AY1819S1-T16-4/main/pull/179[#179], +https://github.com/CS2113-AY1819S1-T16-4/main/pull/239[#239], +https://github.com/CS2113-AY1819S1-T16-4/main/pull/259[#259]) + +** Community: +*** PRs reviewed (with non-trivial review comments): ( +https://github.com/CS2113-AY1819S1-T16-4/main/pull/74[#74], +https://github.com/CS2113-AY1819S1-T16-4/main/pull/75[#75], +https://github.com/CS2113-AY1819S1-T16-4/main/pull/76[#76], +https://github.com/CS2113-AY1819S1-T16-4/main/pull/95[#95], +https://github.com/CS2113-AY1819S1-T16-4/main/pull/97[#97], +https://github.com/CS2113-AY1819S1-T16-4/main/pull/112[#112], +https://github.com/CS2113-AY1819S1-T16-4/main/pull/120[#120]) +*** Reported bugs and suggestions for other teams in the class ( +https://github.com/CS2113-AY1819S1-W13-2/main/issues/83[#83], +https://github.com/CS2113-AY1819S1-W13-2/main/issues/85[#85], +https://github.com/CS2113-AY1819S1-W13-2/main/issues/91[#91], +https://github.com/CS2113-AY1819S1-W13-2/main/issues/95[#95], +https://github.com/CS2113-AY1819S1-W13-2/main/issues/99[#99], +https://github.com/CS2113-AY1819S1-W13-2/main/issues/103[#103]) + +== 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=addRecruitmentPostCommand] + +include::../UserGuide.adoc[tag=editRecruitmentPostCommand] + +include::../UserGuide.adoc[tag=selectRecruitmentPostCommand] + +include::../UserGuide.adoc[tag=deleteRecruitmentPostCommand] + +include::../UserGuide.adoc[tag=clearRecruitmentPostCommand] + +include::../UserGuide.adoc[tag=recruitment2.0Feature] + +== 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=RecruitmentPost] + + diff --git a/docs/team/xiiaopanda.adoc b/docs/team/xiiaopanda.adoc new file mode 100644 index 000000000000..2739ba3652f3 --- /dev/null +++ b/docs/team/xiiaopanda.adoc @@ -0,0 +1,64 @@ += Vernon Cher Chu Xiong - Project Portfolio +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets + +== PROJECT: Centralised Human Resource System (CHRS) + +--- + +== Overview + +Centralised Human Resource System (CHRS) is an application for managing employees within the company. +The application is created to assist the Human Resource Department of the company to better manage the employees' information. +CHRS is capable of checking work schedule, creating recruitment posts, checking of expenses claims and storage of various information of each employee such as salary, department, position, etc. + +== Summary of contributions + +* *Major enhancement*: `AddExpenses` Command and `DeleteExpenses` Command +** What it does: The `AddExpenses` command allows user to create an expenses or modify an expenses that already expenses. `DeleteExpenses` command allows user to delete an expenses from the expenses list. +** Justification: This feature improves the product significantly because a user can add or modify expenses that the employee wishes to claim. +** Highlights: User can add or modify an individual employee's single or multiple types of expenses in a single command. This enhancement enhances the user experience as the user can store new or update existing expenses and access it at any point time. + +* *Minor enhancement*: +*** Added `SelectExpenses` Command to allow user to select any expenses in expenses list + +*** Added `ClearExpenses` Command to allow user to clear the expenses list. + +* *Code contributed*: [https://nuscs2113-ay1819s1.github.io/dashboard/#=undefined&search=xiiaopanda&sort=displayName&since=2018-09-12&until=2018-11-04&timeframe=day&reverse=false&repoSort=true[Reposense Dashboard]] + +* *Other contributions*: + +** Project management: +*** Assist in approving, reviewing and merging pull requests +** Test cases: +*** Wrote additional tests for existing features to increase coverage from 68.678% to 69.978% (https://github.com/CS2113-AY1819S1-T16-4/main/pull/114[#114]), 67.323% to 70.223% (https://github.com/CS2113-AY1819S1-T16-4/main/pull/220[#220]), 80.621% to 80.921% (https://github.com/CS2113-AY1819S1-T16-4/main/pull/232[#232]), 84.889% to 87.789% (https://github.com/CS2113-AY1819S1-T16-4/main/pull/244[#244]), 87.744% to 93.744% (https://github.com/CS2113-AY1819S1-T16-4/main/pull/248[#248]) +** Documentation: +*** Updated Developer Guide on `addExpenses` feature alongside model component and instructions for manual testing (Pull requests: https://github.com/CS2113-AY1819S1-T16-4/main/pull/102[#102], https://github.com/CS2113-AY1819S1-T16-4/main/pull/232/files[#232], https://github.com/CS2113-AY1819S1-T16-4/main/pull/270[#270]) +*** Updated User Guide to ensure `addExpenses`, `deleteExpenses`, `SelectExpenses` and `clearExpenses` commands alongside the fields added by me are up to date (Pull requests: https://github.com/CS2113-AY1819S1-T16-4/main/pull/114[#114], https://github.com/CS2113-AY1819S1-T16-4/main/pull/154[#154], https://github.com/CS2113-AY1819S1-T16-4/main/pull/174[#174]) +** Community: +*** PRs reviewed (with non-trivial review comments): https://github.com/CS2113-AY1819S1-T16-4/main/pull/69[#69], https://github.com/CS2113-AY1819S1-T16-4/main/pull/103[#103], https://github.com/CS2113-AY1819S1-T16-4/main/pull/107[#107], https://github.com/CS2113-AY1819S1-T16-4/main/pull/99[#99], https://github.com/CS2113-AY1819S1-T16-4/main/pull/70[#70], https://github.com/CS2113-AY1819S1-T16-4/main/pull/129[#129] +*** Reported bugs and suggestions for other teams in the class (examples: https://github.com/CS2113-AY1819S1-F09-1/main/issues/110[1], https://github.com/CS2113-AY1819S1-F09-1/main/issues/107[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=addExpensesCommand] + +include::../UserGuide.adoc[tag=deleteExpensesCommand] + +include::../UserGuide.adoc[tag=selectExpensesCommand] + +include::../UserGuide.adoc[tag=clearExpensesCommand] + +include::../UserGuide.adoc[tag=expenses2.0Feature] + +== 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=Expenses] diff --git a/docs/team/zhihong8888.adoc b/docs/team/zhihong8888.adoc new file mode 100644 index 000000000000..a620139db49a --- /dev/null +++ b/docs/team/zhihong8888.adoc @@ -0,0 +1,104 @@ += William Ng Zhi Hong - Project Portfolio +:site-section: AboutUs +:imagesDir: ../images +:stylesDir: ../stylesheets + +== PROJECT: Centralised Human Resource System (CHRS) + +--- + +== Overview + +Centralised Human Resource System (CHRS) is an application for managing employees within the company. +The application is created to assist the Human Resource Department of the company to better manage the employees' information. +CHRS is capable of checking work schedule, creating recruitment posts, checking of expenses claims and storage of various information of each employee such as salary, department, position, etc. + +== Summary of contributions +* *Project Management*: +** Assists in approving, reviewing and merging pull requests. + +* *Major enhancement*: Implements Schedule feature with the following commands: +`addSchedule`, `deleteSchedule`, `addWorks`, `deleteWorks`, `addLeaves`, `deleteLeaves`, `calculateLeaves`, + `clearSchedules`, `selectSchedule`. +** What it does: Allows the user to perform scheduling for the company. +** Justification: This feature improves the product significantly because HR admin requires to schedule employees very often. +** Highlights: User is able to schedule work/leave for multiple employees and multiple dates within a single command. + +* *Minor enhancement*: Enhanced `undo/redo` by implementing VersionedModelList +to save and keep track of which storage has been committed changes, which is essential for undo/redo +to work properly with multiple storages. +(https://github.com/CS2113-AY1819S1-T16-4/main/pull/123[#123]). Enhanced `list` command to lists schedules as well +(https://github.com/CS2113-AY1819S1-T16-4/main/pull/73[#73]). +Enhanced `delete` command such that when an employee is deleted and all schedules linked to the person. +(https://github.com/CS2113-AY1819S1-T16-4/main/pull/73[#73]) + +* *Code contributed*: [https://nuscs2113-ay1819s1.github.io/dashboard/#=undefined&search=zhihong8888&sort=displayName&since=2018-09-12&until=2018-11-04&timeframe=day&reverse=false&repoSort=true[Reposense Dashboard]] + +* *Other contributions*: + +** GUI: +*** Updated the GUI color scheme, background, Menu Bar, Status bar, App icon and greeting when + program is lunched. (https://github.com/CS2113-AY1819S1-T16-4/main/pull/145[#145], + https://github.com/CS2113-AY1819S1-T16-4/main/pull/168[#168], + https://github.com/CS2113-AY1819S1-T16-4/main/pull/182[#182]) +*** Upgraded the Menu bar, when menu items clicked, edit text sets the command word, cursor points to the end of the word. Command ready to be entered. +(https://github.com/CS2113-AY1819S1-T16-4/main/pull/168[#168]) +** Test cases: +*** Wrote additional tests for existing features to increase coverage from 70.2% to 80.6% (https://github.com/CS2113-AY1819S1-T16-4/main/pull/222[#222]), +73.7% to 78.7% (https://github.com/CS2113-AY1819S1-T16-4/main/pull/73[#73]), 68.8% to 74.4% (https://github.com/CS2113-AY1819S1-T16-4/main/pull/70[#70]). +** Documentation: +*** Update user guide +(https://github.com/CS2113-AY1819S1-T16-4/main/pull/182[#182], +https://github.com/CS2113-AY1819S1-T16-4/main/pull/164[#164], +https://github.com/CS2113-AY1819S1-T16-4/main/pull/159[#159], +https://github.com/CS2113-AY1819S1-T16-4/main/pull/115[#115]) +and developer guide +(https://github.com/CS2113-AY1819S1-T16-4/main/pull/242[#242]) for schedule feature. + + +** Community: +*** PRs reviewed (with non-trivial review comments): ( +https://github.com/CS2113-AY1819S1-T16-4/main/pull/134[#134], +https://github.com/CS2113-AY1819S1-T16-4/main/pull/130[#130], +https://github.com/CS2113-AY1819S1-T16-4/main/pull/139[#139], +https://github.com/CS2113-AY1819S1-T16-4/main/pull/108[108], +https://github.com/CS2113-AY1819S1-T16-4/main/pull/101[#101]) + +** Tools: +*** Coveralls (https://github.com/CS2113-AY1819S1-T16-4/main/pull/45[#45]) and Codacy (https://github.com/CS2113-AY1819S1-T16-4/main/pull/46[#46]) + +== 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=addScheduleCommand] + +include::../UserGuide.adoc[tag=deleteScheduleCommand] + +include::../UserGuide.adoc[tag=addWorksCommand] + +include::../UserGuide.adoc[tag=deleteWorksCommand] + +include::../UserGuide.adoc[tag=addLeavesCommand] + +include::../UserGuide.adoc[tag=deleteLeavesCommand] + +include::../UserGuide.adoc[tag=calculateLeavesCommand] + +include::../UserGuide.adoc[tag=selectScheduleCommand] + +include::../UserGuide.adoc[tag=clearSchedulesCommand] + +== 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=schedule1] + +include::../DeveloperGuide.adoc[tag=schedule2] + diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java index ecdd043a4f81..25584ecce2e7 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/seedu/address/MainApp.java @@ -20,18 +20,30 @@ import seedu.address.commons.util.StringUtil; import seedu.address.logic.Logic; import seedu.address.logic.LogicManager; -import seedu.address.model.AddressBook; import seedu.address.model.Model; import seedu.address.model.ModelManager; -import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.UserPrefs; +import seedu.address.model.addressbook.AddressBook; +import seedu.address.model.addressbook.ReadOnlyAddressBook; +import seedu.address.model.expenses.ExpensesList; +import seedu.address.model.expenses.ReadOnlyExpensesList; +import seedu.address.model.recruitment.ReadOnlyRecruitmentList; +import seedu.address.model.recruitment.RecruitmentList; +import seedu.address.model.schedule.ReadOnlyScheduleList; +import seedu.address.model.schedule.ScheduleList; import seedu.address.model.util.SampleDataUtil; -import seedu.address.storage.AddressBookStorage; -import seedu.address.storage.JsonUserPrefsStorage; import seedu.address.storage.Storage; import seedu.address.storage.StorageManager; -import seedu.address.storage.UserPrefsStorage; -import seedu.address.storage.XmlAddressBookStorage; +import seedu.address.storage.addressbook.AddressBookStorage; +import seedu.address.storage.addressbook.XmlAddressBookStorage; +import seedu.address.storage.expenses.ExpensesListStorage; +import seedu.address.storage.expenses.XmlExpensesListStorage; +import seedu.address.storage.recruitment.RecruitmentListStorage; +import seedu.address.storage.recruitment.XmlRecruitmentListStorage; +import seedu.address.storage.schedule.ScheduleListStorage; +import seedu.address.storage.schedule.XmlScheduleListStorage; +import seedu.address.storage.userpref.JsonUserPrefsStorage; +import seedu.address.storage.userpref.UserPrefsStorage; import seedu.address.ui.Ui; import seedu.address.ui.UiManager; @@ -40,7 +52,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, 0, 0, true); private static final Logger logger = LogsCenter.getLogger(MainApp.class); @@ -62,9 +74,18 @@ public void init() throws Exception { UserPrefsStorage userPrefsStorage = new JsonUserPrefsStorage(config.getUserPrefsFilePath()); userPrefs = initPrefs(userPrefsStorage); + + //------------------------------------------------------------------ AddressBookStorage addressBookStorage = new XmlAddressBookStorage(userPrefs.getAddressBookFilePath()); - storage = new StorageManager(addressBookStorage, userPrefsStorage); + ScheduleListStorage scheduleListStorage = new XmlScheduleListStorage(userPrefs.getScheduleListFilePath()); + ExpensesListStorage expensesListStorage = new XmlExpensesListStorage(userPrefs.getExpensesListFilePath()); + RecruitmentListStorage recruitmentListStorage = new XmlRecruitmentListStorage( + userPrefs.getRecruitmentListFilePath()); + + storage = new StorageManager(addressBookStorage, expensesListStorage, scheduleListStorage, + recruitmentListStorage, userPrefsStorage); + //------------------------------------------------------------------ initLogging(config); model = initModelManager(storage, userPrefs); @@ -83,7 +104,15 @@ public void init() throws Exception { */ private Model initModelManager(Storage storage, UserPrefs userPrefs) { Optional addressBookOptional; + Optional expensesListOptional; + Optional scheduleListOptional; + Optional recruitmentListOptional; + ReadOnlyAddressBook initialData; + ReadOnlyExpensesList initialExpenses; + ReadOnlyScheduleList initialSchedule; + ReadOnlyRecruitmentList initialRecruitment; + try { addressBookOptional = storage.readAddressBook(); if (!addressBookOptional.isPresent()) { @@ -93,12 +122,64 @@ private Model initModelManager(Storage storage, UserPrefs userPrefs) { } catch (DataConversionException e) { logger.warning("Data file not in the correct format. Will be starting with an empty AddressBook"); initialData = new AddressBook(); + } catch (IOException e) { logger.warning("Problem while reading from the file. Will be starting with an empty AddressBook"); initialData = new AddressBook(); } - return new ModelManager(initialData, userPrefs); + try { + expensesListOptional = storage.readExpensesList(); + if (!expensesListOptional.isPresent()) { + logger.info("Data file not found. Will be starting with a sample AddressBook"); + initialExpenses = new ExpensesList(); + } else { + initialExpenses = expensesListOptional.get(); + } + } catch (DataConversionException e) { + logger.warning("Data file not in the correct format. Will be starting with an empty AddressBook"); + initialExpenses = new ExpensesList(); + + } catch (IOException e) { + logger.warning("Problem while reading from the file. Will be starting with an empty AddressBook"); + initialExpenses = new ExpensesList(); + } + + try { + scheduleListOptional = storage.readScheduleList(); + if (!scheduleListOptional.isPresent()) { + logger.info("Data file not found. Will be starting with a schedule List"); + initialSchedule = new ScheduleList(); + } else { + initialSchedule = scheduleListOptional.get(); + } + } catch (DataConversionException e) { + logger.warning("Data file not in the correct format. Will be starting with an empty ScheduleList"); + initialSchedule = new ScheduleList(); + + } catch (IOException e) { + logger.warning("Problem while reading from the file. Will be starting with an empty ScheduleList"); + initialSchedule = new ScheduleList(); + } + + try { + recruitmentListOptional = storage.readRecruitmentList(); + if (!recruitmentListOptional.isPresent()) { + logger.info("Data file not found. Will be starting with a recruitment List"); + initialRecruitment = new RecruitmentList(); + } else { + initialRecruitment = recruitmentListOptional.get(); + } + } catch (DataConversionException e) { + logger.warning("Data file not in the correct format. Will be starting with an empty RecruitmentList"); + initialRecruitment = new RecruitmentList(); + + } catch (IOException e) { + logger.warning("Problem while reading from the file. Will be starting with an empty RecruitmentList"); + initialRecruitment = new RecruitmentList(); + } + + return new ModelManager(initialData, initialExpenses, initialSchedule, initialRecruitment, userPrefs); } private void initLogging(Config config) { diff --git a/src/main/java/seedu/address/commons/core/Config.java b/src/main/java/seedu/address/commons/core/Config.java index e978d621e086..f2c343fbf2eb 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 = "Centralised Human Resource System (CHRS)"; 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..f6615d6380ee 100644 --- a/src/main/java/seedu/address/commons/core/Messages.java +++ b/src/main/java/seedu/address/commons/core/Messages.java @@ -5,9 +5,24 @@ */ public class Messages { - public static final String MESSAGE_UNKNOWN_COMMAND = "Unknown command"; + public static final String MESSAGE_UNKNOWN_COMMAND = "Unknown command!\n"; public static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format! \n%1$s"; - public static final String MESSAGE_INVALID_PERSON_DISPLAYED_INDEX = "The person index provided is invalid"; + public static final String MESSAGE_INVALID_COMMAND_OVERLOAD_PREFIX_FORMAT = + "Invalid command format! Too Many inputs found! \n%1$s"; + public static final String MESSAGE_INVALID_EXPENSES_DISPLAYED_INDEX = "The expenses index provided is invalid"; + public static final String MESSAGE_INVALID_PERSON_DISPLAYED_INDEX = "The employee index provided is invalid"; + public static final String MESSAGE_INVALID_SCHEDULE_DISPLAYED_INDEX = "The schedule index provided is invalid"; + public static final String MESSAGE_INVALID_RECRUITMENT_POST_DISPLAYED_INDEX = "The recruitmentPost " + + "index provided is invalid"; public static final String MESSAGE_PERSONS_LISTED_OVERVIEW = "%1$d persons listed!"; + public static final String GREETING_MESSAGE_NEWLINE = " Admin! " + + "\nWelcome to Centralised Human Resource System. " + + "\nEnter a command to begin. Press F1 for help."; + + public static final String GREETING_MESSAGE_NONEWLINE = " Admin! " + + "Welcome to Centralised Human Resource System. " + + "\nEnter a command to begin. Press F1 for help."; + public static final String MESSAGE_MODIFIED_PAY_OVERVIEW = "%1$d persons modified!"; + public static final String MESSAGE_STATUS_BAR_BOTTOM_RIGHT = "CS2113-T16-4"; } diff --git a/src/main/java/seedu/address/commons/events/model/AddressBookChangedEvent.java b/src/main/java/seedu/address/commons/events/model/AddressBookChangedEvent.java index b72ad4740e5a..6495283eeff9 100644 --- a/src/main/java/seedu/address/commons/events/model/AddressBookChangedEvent.java +++ b/src/main/java/seedu/address/commons/events/model/AddressBookChangedEvent.java @@ -1,7 +1,7 @@ package seedu.address.commons.events.model; import seedu.address.commons.events.BaseEvent; -import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.addressbook.ReadOnlyAddressBook; /** Indicates the AddressBook in the model has changed*/ public class AddressBookChangedEvent extends BaseEvent { diff --git a/src/main/java/seedu/address/commons/events/model/ExpensesListChangedEvent.java b/src/main/java/seedu/address/commons/events/model/ExpensesListChangedEvent.java new file mode 100644 index 000000000000..694ccd1129d4 --- /dev/null +++ b/src/main/java/seedu/address/commons/events/model/ExpensesListChangedEvent.java @@ -0,0 +1,18 @@ +package seedu.address.commons.events.model; +import seedu.address.commons.events.BaseEvent; +import seedu.address.model.expenses.ReadOnlyExpensesList; + +/** Indicates the ExpensesList in the model has changed*/ +public class ExpensesListChangedEvent extends BaseEvent { + + public final ReadOnlyExpensesList data; + + public ExpensesListChangedEvent(ReadOnlyExpensesList data) { + this.data = data; + } + + @Override + public String toString() { + return "number of persons " + data.getExpensesRequestList().size(); + } +} diff --git a/src/main/java/seedu/address/commons/events/model/RecruitmentListChangedEvent.java b/src/main/java/seedu/address/commons/events/model/RecruitmentListChangedEvent.java new file mode 100644 index 000000000000..ed6df33d0e77 --- /dev/null +++ b/src/main/java/seedu/address/commons/events/model/RecruitmentListChangedEvent.java @@ -0,0 +1,19 @@ +package seedu.address.commons.events.model; + +import seedu.address.commons.events.BaseEvent; +import seedu.address.model.recruitment.ReadOnlyRecruitmentList; + +/** Indicates the recruitmentList in the model has changed*/ +public class RecruitmentListChangedEvent extends BaseEvent { + + public final ReadOnlyRecruitmentList data; + + public RecruitmentListChangedEvent(ReadOnlyRecruitmentList data) { + this.data = data; + } + + @Override + public String toString() { + return "number of recruitment posts " + data.getRecruitmentList().size(); + } +} diff --git a/src/main/java/seedu/address/commons/events/model/ScheduleListChangedEvent.java b/src/main/java/seedu/address/commons/events/model/ScheduleListChangedEvent.java new file mode 100644 index 000000000000..2e8421ed4b68 --- /dev/null +++ b/src/main/java/seedu/address/commons/events/model/ScheduleListChangedEvent.java @@ -0,0 +1,19 @@ +package seedu.address.commons.events.model; + +import seedu.address.commons.events.BaseEvent; +import seedu.address.model.schedule.ReadOnlyScheduleList; + +/** Indicates the Schedule List in the model has changed*/ +public class ScheduleListChangedEvent extends BaseEvent { + + public final ReadOnlyScheduleList data; + + public ScheduleListChangedEvent(ReadOnlyScheduleList data) { + this.data = data; + } + + @Override + public String toString() { + return "number of schedules: " + data.getScheduleList().size(); + } +} diff --git a/src/main/java/seedu/address/commons/events/ui/ExpensesPanelSelectionChangedEvent.java b/src/main/java/seedu/address/commons/events/ui/ExpensesPanelSelectionChangedEvent.java new file mode 100644 index 000000000000..4a729d866487 --- /dev/null +++ b/src/main/java/seedu/address/commons/events/ui/ExpensesPanelSelectionChangedEvent.java @@ -0,0 +1,22 @@ +package seedu.address.commons.events.ui; + +import seedu.address.commons.events.BaseEvent; +import seedu.address.model.expenses.Expenses; + +/** + * Represents a selection change in the Expenses List Panel + */ +public class ExpensesPanelSelectionChangedEvent extends BaseEvent { + + private final Expenses newSelection; + + public ExpensesPanelSelectionChangedEvent(Expenses newSelection) { + this.newSelection = newSelection; + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } + +} diff --git a/src/main/java/seedu/address/commons/events/ui/JumpToListExpensesRequestEvent.java b/src/main/java/seedu/address/commons/events/ui/JumpToListExpensesRequestEvent.java new file mode 100644 index 000000000000..5b3546db0a53 --- /dev/null +++ b/src/main/java/seedu/address/commons/events/ui/JumpToListExpensesRequestEvent.java @@ -0,0 +1,22 @@ +package seedu.address.commons.events.ui; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.events.BaseEvent; + +/** + * Indicates a request to jump to the list of expenses + */ +public class JumpToListExpensesRequestEvent extends BaseEvent { + + public final int targetIndex; + + public JumpToListExpensesRequestEvent(Index targetIndex) { + this.targetIndex = targetIndex.getZeroBased(); + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } + +} diff --git a/src/main/java/seedu/address/commons/events/ui/JumpToListRecruitmentPostRequestEvent.java b/src/main/java/seedu/address/commons/events/ui/JumpToListRecruitmentPostRequestEvent.java new file mode 100644 index 000000000000..e4776c6b44fc --- /dev/null +++ b/src/main/java/seedu/address/commons/events/ui/JumpToListRecruitmentPostRequestEvent.java @@ -0,0 +1,22 @@ +package seedu.address.commons.events.ui; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.events.BaseEvent; + +/** + * Indicates a request to jump to the list of recruitment + */ +public class JumpToListRecruitmentPostRequestEvent extends BaseEvent { + + public final int targetIndex; + + public JumpToListRecruitmentPostRequestEvent(Index targetIndex) { + this.targetIndex = targetIndex.getZeroBased(); + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } + +} diff --git a/src/main/java/seedu/address/commons/events/ui/JumpToListScheduleRequestEvent.java b/src/main/java/seedu/address/commons/events/ui/JumpToListScheduleRequestEvent.java new file mode 100644 index 000000000000..493fff34a5b2 --- /dev/null +++ b/src/main/java/seedu/address/commons/events/ui/JumpToListScheduleRequestEvent.java @@ -0,0 +1,22 @@ +package seedu.address.commons.events.ui; + +import seedu.address.commons.core.index.Index; +import seedu.address.commons.events.BaseEvent; + +/** + * Indicates a request to jump to the list of schedules + */ +public class JumpToListScheduleRequestEvent extends BaseEvent { + + public final int targetIndex; + + public JumpToListScheduleRequestEvent(Index targetIndex) { + this.targetIndex = targetIndex.getZeroBased(); + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } + +} diff --git a/src/main/java/seedu/address/commons/events/ui/NewMenuBarCmdClickedEvent.java b/src/main/java/seedu/address/commons/events/ui/NewMenuBarCmdClickedEvent.java new file mode 100644 index 000000000000..94644c213c55 --- /dev/null +++ b/src/main/java/seedu/address/commons/events/ui/NewMenuBarCmdClickedEvent.java @@ -0,0 +1,19 @@ +package seedu.address.commons.events.ui; + +import seedu.address.commons.events.BaseEvent; + +/** + * Indicates that menu bar is clicked. + */ +public class NewMenuBarCmdClickedEvent extends BaseEvent { + public final String menuCommand; + + public NewMenuBarCmdClickedEvent(String message) { + this.menuCommand = message; + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } +} diff --git a/src/main/java/seedu/address/commons/events/ui/RecruitmentPanelSelectionChangedEvent.java b/src/main/java/seedu/address/commons/events/ui/RecruitmentPanelSelectionChangedEvent.java new file mode 100644 index 000000000000..5490e7d49530 --- /dev/null +++ b/src/main/java/seedu/address/commons/events/ui/RecruitmentPanelSelectionChangedEvent.java @@ -0,0 +1,23 @@ +package seedu.address.commons.events.ui; + +import seedu.address.commons.events.BaseEvent; +import seedu.address.model.recruitment.Recruitment; + +/** + * Represents a selection change in the recruitment List Panel + */ +public class RecruitmentPanelSelectionChangedEvent extends BaseEvent { + + + private final Recruitment newSelection; + + public RecruitmentPanelSelectionChangedEvent(Recruitment newSelection) { + this.newSelection = newSelection; + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } + +} diff --git a/src/main/java/seedu/address/commons/events/ui/SchedulePanelSelectionChangedEvent.java b/src/main/java/seedu/address/commons/events/ui/SchedulePanelSelectionChangedEvent.java new file mode 100644 index 000000000000..7f904744da64 --- /dev/null +++ b/src/main/java/seedu/address/commons/events/ui/SchedulePanelSelectionChangedEvent.java @@ -0,0 +1,23 @@ +package seedu.address.commons.events.ui; + +import seedu.address.commons.events.BaseEvent; +import seedu.address.model.schedule.Schedule; + +/** + * Represents a selection change in the schedule List Panel + */ +public class SchedulePanelSelectionChangedEvent extends BaseEvent { + + + private final Schedule newSelection; + + public SchedulePanelSelectionChangedEvent(Schedule newSelection) { + this.newSelection = newSelection; + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } + +} diff --git a/src/main/java/seedu/address/commons/util/StringUtil.java b/src/main/java/seedu/address/commons/util/StringUtil.java index 61cc8c9a1cb8..98389c70f7fc 100644 --- a/src/main/java/seedu/address/commons/util/StringUtil.java +++ b/src/main/java/seedu/address/commons/util/StringUtil.java @@ -38,6 +38,22 @@ public static boolean containsWordIgnoreCase(String sentence, String word) { .anyMatch(preppedWord::equalsIgnoreCase); } + /** + * Returns true if the {@code sentence} contains the {@code word}. + * Ignores case, but a full word match is required. + * @param sentence cannot be null + * @param word cannot be null, cannot be empty, must be a word + */ + public static boolean containsSentenceIgnoreCase(String sentence, String word) { + requireNonNull(sentence); + requireNonNull(word); + + String preppedWord = word.trim(); + checkArgument(!preppedWord.isEmpty(), "Word parameter cannot be empty"); + + return sentence.equals(word); + } + /** * Returns a detailed message of the t, including the stack trace. */ diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java index 8b34b862039a..619f7837a1f6 100644 --- a/src/main/java/seedu/address/logic/Logic.java +++ b/src/main/java/seedu/address/logic/Logic.java @@ -4,7 +4,10 @@ import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.expenses.Expenses; import seedu.address.model.person.Person; +import seedu.address.model.recruitment.Recruitment; +import seedu.address.model.schedule.Schedule; /** * API of the Logic component @@ -19,9 +22,18 @@ public interface Logic { */ CommandResult execute(String commandText) throws CommandException, ParseException; + /** Returns an unmodifiable view of the filtered list of persons */ + ObservableList getFilteredExpensesList(); + /** Returns an unmodifiable view of the filtered list of persons */ ObservableList getFilteredPersonList(); + /** Returns an unmodifiable view of the filtered list of schedule */ + ObservableList getFilteredScheduleList(); + + /** Returns an unmodifiable view of the filtered list of recruitmentList */ + ObservableList getFilteredRecruitmentList(); + /** Returns the list of input entered by the user, encapsulated in a {@code ListElementPointer} object */ ListElementPointer getHistorySnapshot(); } diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java index 9aff86fc33dc..d64c46deab14 100644 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ b/src/main/java/seedu/address/logic/LogicManager.java @@ -11,7 +11,10 @@ import seedu.address.logic.parser.AddressBookParser; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.Model; +import seedu.address.model.expenses.Expenses; import seedu.address.model.person.Person; +import seedu.address.model.recruitment.Recruitment; +import seedu.address.model.schedule.Schedule; /** * The main LogicManager of the app. @@ -39,12 +42,25 @@ public CommandResult execute(String commandText) throws CommandException, ParseE history.add(commandText); } } + @Override + public ObservableList getFilteredExpensesList() { + return model.getFilteredExpensesList(); + } @Override public ObservableList getFilteredPersonList() { return model.getFilteredPersonList(); } + @Override + public ObservableList getFilteredScheduleList() { + return model.getFilteredScheduleList(); + } + + @Override + public ObservableList getFilteredRecruitmentList() { + return model.getFilteredRecruitmentList(); } + @Override public ListElementPointer getHistorySnapshot() { return new ListElementPointer(history.getHistory()); diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddCommand.java index d88e831ff1ce..e2664d06360d 100644 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ b/src/main/java/seedu/address/logic/commands/AddCommand.java @@ -2,15 +2,27 @@ import static java.util.Objects.requireNonNull; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATEOFBIRTH; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DEPARTMENT; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMPLOYEEID; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_POSITION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SALARY; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import java.util.Collections; + import seedu.address.logic.CommandHistory; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; +import seedu.address.model.person.DateOfBirthContainsKeywordsPredicate; +import seedu.address.model.person.EmailContainsKeywordsPredicate; +import seedu.address.model.person.EmployeeIdContainsKeywordsPredicate; +import seedu.address.model.person.NameContainsKeywordsPredicate; import seedu.address.model.person.Person; +import seedu.address.model.person.PhoneContainsKeywordsPredicate; /** * Adds a person to the address book. @@ -21,22 +33,36 @@ public class AddCommand extends Command { public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a person to the address book. " + "Parameters: " + + PREFIX_EMPLOYEEID + "EMPLOYEEID " + PREFIX_NAME + "NAME " + + PREFIX_DATEOFBIRTH + "DATEOFBIRTH " + PREFIX_PHONE + "PHONE " + PREFIX_EMAIL + "EMAIL " + + PREFIX_DEPARTMENT + "DEPARTMENT " + + PREFIX_POSITION + "POSITION " + PREFIX_ADDRESS + "ADDRESS " + + PREFIX_SALARY + "SALARY " + "[" + PREFIX_TAG + "TAG]...\n" + "Example: " + COMMAND_WORD + " " + + PREFIX_EMPLOYEEID + "008888 " + PREFIX_NAME + "John Doe " + + PREFIX_DATEOFBIRTH + "03/12/1993 " + PREFIX_PHONE + "98765432 " + PREFIX_EMAIL + "johnd@example.com " + + PREFIX_DEPARTMENT + "Finance " + + PREFIX_POSITION + "Intern " + PREFIX_ADDRESS + "311, Clementi Ave 2, #02-25 " - + PREFIX_TAG + "friends " - + PREFIX_TAG + "owesMoney"; + + PREFIX_SALARY + "1000.00 " + + PREFIX_TAG + "FlyKite"; public static final String MESSAGE_SUCCESS = "New person added: %1$s"; + public static final String MESSAGE_DUPLICATE_EMPLOYEEID = "This employee ID already exists in the address book"; public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book"; + public static final String MESSAGE_DUPLICATE_EMAIL = "This email already exists in the address book"; + public static final String MESSAGE_DUPLICATE_PHONE = "This phone already exists in the address book"; + private static boolean isEmailDuplicated = false; + private static boolean isPhoneDuplicated = false; private final Person toAdd; /** @@ -47,11 +73,51 @@ public AddCommand(Person person) { toAdd = person; } + public static void setIsEmailDuplicated(boolean verifyEmailDuplication) { + isEmailDuplicated = verifyEmailDuplication; + } + + public static void setIsPhoneDuplicated(boolean verifyPhoneDuplication) { + isPhoneDuplicated = verifyPhoneDuplication; + } + + /** + * Execution of the command will depend on whether there are duplicated EmployeeIds, Email, Phone or Name & + * DateOfBirth. If any of the duplicated check is true, an exception will be thrown, otherwise, + * the command will be executed accordingly. + * @param model The actual model + * @param history The actual history + * @throws CommandException + */ @Override public CommandResult execute(Model model, CommandHistory history) throws CommandException { requireNonNull(model); - if (model.hasPerson(toAdd)) { + // Checks for duplicated employee id + if (model.hasEmployeeId(toAdd)) { + EmployeeIdContainsKeywordsPredicate employeeIdPredicate = + new EmployeeIdContainsKeywordsPredicate(toAdd.getEmployeeId().value); + model.updateFilteredPersonList(employeeIdPredicate); + throw new CommandException(MESSAGE_DUPLICATE_EMPLOYEEID); + // Checks for duplicated email + } else if (model.hasPerson(toAdd) && isEmailDuplicated && !isPhoneDuplicated) { + EmailContainsKeywordsPredicate emailPredicate = + new EmailContainsKeywordsPredicate(toAdd.getEmail().value); + model.updateFilteredPersonList(emailPredicate); + throw new CommandException(MESSAGE_DUPLICATE_EMAIL); + // Checks for duplicated phone + } else if (model.hasPerson(toAdd) && !isEmailDuplicated && isPhoneDuplicated) { + PhoneContainsKeywordsPredicate phonePredicate = + new PhoneContainsKeywordsPredicate(toAdd.getPhone().value); + model.updateFilteredPersonList(phonePredicate); + throw new CommandException(MESSAGE_DUPLICATE_PHONE); + // Checks for duplicated name & date of birth + } else if (model.hasPerson(toAdd) && ((!isEmailDuplicated && !isPhoneDuplicated))) { + NameContainsKeywordsPredicate namePredicate = + new NameContainsKeywordsPredicate(Collections.singletonList(toAdd.getName().fullName)); + DateOfBirthContainsKeywordsPredicate dobPredicate = + new DateOfBirthContainsKeywordsPredicate(toAdd.getDateOfBirth().value); + model.updateFilteredPersonList(namePredicate.and(dobPredicate)); throw new CommandException(MESSAGE_DUPLICATE_PERSON); } diff --git a/src/main/java/seedu/address/logic/commands/AddExpensesCommand.java b/src/main/java/seedu/address/logic/commands/AddExpensesCommand.java new file mode 100644 index 000000000000..6794d6d5e1bf --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AddExpensesCommand.java @@ -0,0 +1,391 @@ +package seedu.address.logic.commands; +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMPLOYEEID; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MEDICAL_EXPENSES; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MISCELLANEOUS_EXPENSES; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TRAVEL_EXPENSES; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_EXPENSES; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_SCHEDULES; + +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.Model; +import seedu.address.model.expenses.EmployeeIdExpensesContainsKeywordsPredicate; +import seedu.address.model.expenses.Expenses; +import seedu.address.model.expenses.ExpensesAmount; +import seedu.address.model.expenses.MedicalExpenses; +import seedu.address.model.expenses.MiscellaneousExpenses; +import seedu.address.model.expenses.TravelExpenses; +import seedu.address.model.person.EmployeeId; +import seedu.address.model.person.Person; + +/** + * Adds an expense to the Expenses List. + */ +public class AddExpensesCommand extends Command { + public static final String COMMAND_WORD = "addExpenses"; + public static final String COMMAND_ALIAS = "ae"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Request Expenses. " + + "\nParameters: " + + PREFIX_EMPLOYEEID + "EMPLOYEEID " + + PREFIX_TRAVEL_EXPENSES + "TRAVELEXPENSES " + + PREFIX_MEDICAL_EXPENSES + "MEDICALEXPENSES " + + PREFIX_MISCELLANEOUS_EXPENSES + "MISCELLANEOUSEXPENSES " + + "\nExample: " + COMMAND_WORD + " " + + PREFIX_EMPLOYEEID + "000001 " + + PREFIX_TRAVEL_EXPENSES + "34 " + + PREFIX_MEDICAL_EXPENSES + "87 " + + PREFIX_MISCELLANEOUS_EXPENSES + "35"; + + public static final String MESSAGE_SUCCESS = "Adding expenses requested."; + public static final String MESSAGE_NEGATIVE_LEFTOVER = "Cannot have negative expenses leftover."; + public static final String MESSAGE_NOT_EDITED = "No changes is made to expenses."; + public static final String MESSAGE_VALUE_OVER_LIMIT = "Values for travel expenses, medical expenses and " + + "miscellaneous expenses cannot exceed 999999.99"; + public static final String MESSAGE_EMPLOYEE_ID_NOT_FOUND = "Employee Id not found in CHRS"; + + public static final double MAX_EXPENSES_AMOUNT = 999999.99; + public static final double MAX_TOTAL_EXPENSES = 9999999.99; + + private Boolean isNegativeLeftover; + private Boolean isOverLimit; + private Person toCheckEmployeeId; + private final Expenses toAddExpenses; + private final EditExpensesDescriptor editExpensesDescriptor; + + public AddExpensesCommand(Expenses expenses, EditExpensesDescriptor editExpensesDescriptor) { + Expenses toAddFormatExpenses; + ExpensesAmount formattedExpenses = null; + TravelExpenses formattedTravelExpenses = null; + MedicalExpenses formattedMedicalExpenses = null; + MiscellaneousExpenses formattedMiscellaneousExpenses = null; + requireNonNull(expenses); + requireNonNull(editExpensesDescriptor); + + toAddFormatExpenses = expenses; + NumberFormat formatter = new DecimalFormat("#0.00"); + String formatExpenses = toAddFormatExpenses.getExpensesAmount().toString(); + String formatTravelExpenses = toAddFormatExpenses.getTravelExpenses().toString(); + String formatMedicalExpenses = toAddFormatExpenses.getMedicalExpenses().toString(); + String formatMiscellaneousExpenses = toAddFormatExpenses.getMiscellaneousExpenses().toString(); + try { + formattedTravelExpenses = ParserUtil.parseTravelExpenses( + String.valueOf(formatter.format(Double.parseDouble(formatTravelExpenses)))); + formattedMedicalExpenses = ParserUtil.parseMedicalExpenses( + String.valueOf(formatter.format(Double.parseDouble(formatMedicalExpenses)))); + formattedMiscellaneousExpenses = ParserUtil.parseMiscellaneousExpenses( + String.valueOf(formatter.format(Double.parseDouble(formatMiscellaneousExpenses)))); + formattedExpenses = ParserUtil.parseExpensesAmount( + String.valueOf(formatter.format(Double.parseDouble(formatExpenses)))); + } catch (ParseException pe) { + pe.printStackTrace(); + } + EmployeeId addEmployeeId = toAddFormatExpenses.getEmployeeId(); + toAddExpenses = new Expenses (addEmployeeId, formattedExpenses, formattedTravelExpenses, + formattedMedicalExpenses, formattedMiscellaneousExpenses); + toCheckEmployeeId = new Person(expenses.getEmployeeId()); + this.editExpensesDescriptor = new EditExpensesDescriptor(editExpensesDescriptor); + isNegativeLeftover = false; + isOverLimit = false; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + String messageToShow = ""; + if (!model.hasEmployeeId(toCheckEmployeeId)) { + throw new CommandException(MESSAGE_EMPLOYEE_ID_NOT_FOUND); + } else if (!model.hasExpenses(toAddExpenses)) { + if (Double.parseDouble(toAddExpenses.getExpensesAmount().toString()) < 0 + || Double.parseDouble(toAddExpenses.getTravelExpenses().toString()) < 0 + || Double.parseDouble(toAddExpenses.getMedicalExpenses().toString()) < 0 + || Double.parseDouble(toAddExpenses.getMiscellaneousExpenses().toString()) < 0) { + throw new CommandException(MESSAGE_NEGATIVE_LEFTOVER); + } else if (Double.parseDouble(toAddExpenses.getExpensesAmount().toString()) > MAX_TOTAL_EXPENSES + || Double.parseDouble(toAddExpenses.getTravelExpenses().toString()) > MAX_EXPENSES_AMOUNT + || Double.parseDouble(toAddExpenses.getMedicalExpenses().toString()) > MAX_EXPENSES_AMOUNT + || Double.parseDouble(toAddExpenses.getMiscellaneousExpenses().toString()) > MAX_EXPENSES_AMOUNT + ) { + throw new CommandException(MESSAGE_VALUE_OVER_LIMIT); + } else if (Double.parseDouble(toAddExpenses.getExpensesAmount().toString()) >= 0 + && Double.parseDouble(toAddExpenses.getTravelExpenses().toString()) >= 0 + && Double.parseDouble(toAddExpenses.getMedicalExpenses().toString()) >= 0 + && Double.parseDouble(toAddExpenses.getMiscellaneousExpenses().toString()) >= 0 + && Double.parseDouble(toAddExpenses.getExpensesAmount().toString()) <= MAX_TOTAL_EXPENSES + && Double.parseDouble(toAddExpenses.getTravelExpenses().toString()) <= MAX_EXPENSES_AMOUNT + && Double.parseDouble(toAddExpenses.getMedicalExpenses().toString()) <= MAX_EXPENSES_AMOUNT + && Double.parseDouble(toAddExpenses.getMiscellaneousExpenses().toString()) <= MAX_EXPENSES_AMOUNT + ) { + model.addExpenses(toAddExpenses); + model.commitExpensesList(); + messageToShow = MESSAGE_SUCCESS; + } + } else if (model.hasExpenses(toAddExpenses)) { + EmployeeIdExpensesContainsKeywordsPredicate predicatEmployeeId; + List employeeIdList = new ArrayList<>(); + List lastShownListExpenses; + + employeeIdList.add(toCheckEmployeeId.getEmployeeId().value); + predicatEmployeeId = new EmployeeIdExpensesContainsKeywordsPredicate(employeeIdList); + + model.updateFilteredExpensesList(predicatEmployeeId); + lastShownListExpenses = model.getFilteredExpensesList(); + + Expenses expensesToEdit = lastShownListExpenses.get(0); + Expenses editedExpenses = createEditedExpenses(expensesToEdit, editExpensesDescriptor); + + if (getIsNegativeLeftover()) { + throw new CommandException(MESSAGE_NEGATIVE_LEFTOVER); + } else if (getIsOverLimit()) { + throw new CommandException(MESSAGE_VALUE_OVER_LIMIT); + } else if (!getIsNegativeLeftover() && !getIsOverLimit()) { + messageToShow = MESSAGE_SUCCESS; + model.updateExpenses(expensesToEdit, editedExpenses); + model.commitExpensesList(); + } + model.updateFilteredExpensesList(PREDICATE_SHOW_ALL_EXPENSES); + } + model.updateFilteredExpensesList(PREDICATE_SHOW_ALL_EXPENSES); + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + model.updateFilteredScheduleList(PREDICATE_SHOW_ALL_SCHEDULES); + return new CommandResult(String.format(messageToShow, toAddExpenses)); + } + + /** + * Creates and returns a {@code Expenses} with the details of {@code expensesToEdit} + * edited with {@code editExpensesDescriptor}. + */ + private Expenses createEditedExpenses(Expenses expensesToEdit, EditExpensesDescriptor + editExpensesDescriptor) { + assert expensesToEdit != null; + ExpensesAmount updatedExpensesAmount = null; + TravelExpenses updatedTravelExpenses = null; + MedicalExpenses updatedMedicalExpenses = null; + MiscellaneousExpenses updatedMiscellaneousExpenses = null; + + EmployeeId updatedEmployeeId = expensesToEdit.getEmployeeId(); + try { + updatedExpensesAmount = ParserUtil.parseExpensesAmount(modifyExpensesAmount(expensesToEdit, + editExpensesDescriptor)); + updatedTravelExpenses = ParserUtil.parseTravelExpenses(modifyTravelExpenses(expensesToEdit, + editExpensesDescriptor)); + updatedMedicalExpenses = ParserUtil.parseMedicalExpenses(modifyMedicalExpenses(expensesToEdit, + editExpensesDescriptor)); + updatedMiscellaneousExpenses = ParserUtil.parseMiscellaneousExpenses(modifyMiscellaneousExpenses( + expensesToEdit, editExpensesDescriptor)); + } catch (ParseException pe) { + pe.printStackTrace(); + } + + return new Expenses(updatedEmployeeId, updatedExpensesAmount, updatedTravelExpenses, updatedMedicalExpenses, + updatedMiscellaneousExpenses); + } + + /** + * Creates and returns a new String of Expenses with the details of {@code expensesToEdit} + * edited with {@code editExpensesDescriptor}. + */ + private String modifyExpensesAmount (Expenses expensesToEdit, EditExpensesDescriptor + editExpensesDescriptor) { + NumberFormat formatter = new DecimalFormat("#0.00"); + String newExpensesAmount = expensesToEdit.getExpensesAmount().toString(); + double updateExpensesAmount = Double.parseDouble(newExpensesAmount); + String change = editExpensesDescriptor.getExpensesAmount().toString().replaceAll("[^0-9.-]", + ""); + updateExpensesAmount += Double.parseDouble(change); + if (updateExpensesAmount < 0) { + setIsNegativeLeftover(true); + } else if (updateExpensesAmount > MAX_TOTAL_EXPENSES) { + setIsOverLimit(true); + } else if (updateExpensesAmount >= 0) { + newExpensesAmount = String.valueOf(formatter.format(updateExpensesAmount)); + } + return newExpensesAmount; + } + + /** + * Creates and returns a new String of Expenses with the details of {@code expensesToEdit} + * edited with {@code editExpensesDescriptor}. + */ + private String modifyTravelExpenses (Expenses expensesToEdit, EditExpensesDescriptor + editExpensesDescriptor) { + NumberFormat formatter = new DecimalFormat("#0.00"); + String newTravelExpenses = expensesToEdit.getTravelExpenses().toString(); + double updateTravelExpenses = Double.parseDouble(newTravelExpenses); + String change = editExpensesDescriptor.getTravelExpenses().toString().replaceAll("[^0-9.-]", + ""); + updateTravelExpenses += Double.parseDouble(change); + if (updateTravelExpenses < 0) { + setIsNegativeLeftover(true); + } else if (updateTravelExpenses > MAX_EXPENSES_AMOUNT) { + setIsOverLimit(true); + } else if (updateTravelExpenses >= 0) { + newTravelExpenses = String.valueOf(formatter.format(updateTravelExpenses)); + } + return newTravelExpenses; + } + + /** + * Creates and returns a new String of Expenses with the details of {@code expensesToEdit} + * edited with {@code editExpensesDescriptor}. + */ + private String modifyMedicalExpenses (Expenses expensesToEdit, EditExpensesDescriptor + editExpensesDescriptor) { + NumberFormat formatter = new DecimalFormat("#0.00"); + String newMedicalExpenses = expensesToEdit.getMedicalExpenses().toString(); + double updateMedicalExpenses = Double.parseDouble(newMedicalExpenses); + String change = editExpensesDescriptor.getMedicalExpenses().toString().replaceAll("[^0-9.-]", + ""); + updateMedicalExpenses += Double.parseDouble(change); + if (updateMedicalExpenses < 0) { + setIsNegativeLeftover(true); + } else if (updateMedicalExpenses > MAX_EXPENSES_AMOUNT) { + setIsOverLimit(true); + } else if (updateMedicalExpenses >= 0) { + newMedicalExpenses = String.valueOf(formatter.format(updateMedicalExpenses)); + } + return newMedicalExpenses; + } + + /** + * Creates and returns a new String of Expenses with the details of {@code expensesToEdit} + * edited with {@code editExpensesDescriptor}. + */ + private String modifyMiscellaneousExpenses (Expenses expensesToEdit, EditExpensesDescriptor + editExpensesDescriptor) { + NumberFormat formatter = new DecimalFormat("#0.00"); + String newMiscellaneousExpenses = expensesToEdit.getMiscellaneousExpenses().toString(); + double updateMiscellaneousExpenses = Double.parseDouble(newMiscellaneousExpenses); + String change = editExpensesDescriptor.getMiscellaneousExpenses().toString().replaceAll("[^0-9.-]", + ""); + updateMiscellaneousExpenses += Double.parseDouble(change); + if (updateMiscellaneousExpenses < 0) { + setIsNegativeLeftover(true); + } else if (updateMiscellaneousExpenses > MAX_EXPENSES_AMOUNT) { + setIsOverLimit(true); + } else if (updateMiscellaneousExpenses >= 0) { + newMiscellaneousExpenses = String.valueOf(formatter.format(updateMiscellaneousExpenses)); + } + return newMiscellaneousExpenses; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof AddExpensesCommand // instanceof handles nulls + && toAddExpenses.equals(((AddExpensesCommand) other).toAddExpenses)); + } + + public void setIsNegativeLeftover(Boolean negativeLeftover) { + isNegativeLeftover = negativeLeftover; + } + + public boolean getIsNegativeLeftover() { + return isNegativeLeftover; + } + + public void setIsOverLimit(Boolean overLimit) { + isOverLimit = overLimit; + } + + public boolean getIsOverLimit() { + return isOverLimit; + } + + /** + * Stores the details to edit the person with. Each non-empty field value will replace the + * corresponding field value of the person. + */ + public static class EditExpensesDescriptor { + private ExpensesAmount expensesAmount; + private TravelExpenses travelExpenses; + private MedicalExpenses medicalExpenses; + private MiscellaneousExpenses miscellaneousExpenses; + + public EditExpensesDescriptor() { + } + + /** + * Copy constructor. + * A defensive copy of {@code tags} is used internally. + */ + public EditExpensesDescriptor(EditExpensesDescriptor toCopy) { + setExpensesAmount(toCopy.expensesAmount); + setTravelExpenses(toCopy.travelExpenses); + setMedicalExpenses(toCopy.medicalExpenses); + setMiscellaneousExpenses(toCopy.miscellaneousExpenses); + } + + /** + * Returns true if at least one field is edited. + */ + public boolean isAnyFieldEdited() { + return (Double.parseDouble(expensesAmount.toString()) != 0 + || Double.parseDouble(travelExpenses.toString()) != 0 + || Double.parseDouble(medicalExpenses.toString()) != 0 + || Double.parseDouble(miscellaneousExpenses.toString()) != 0); + } + + public void setExpensesAmount(ExpensesAmount expensesAmount) { + this.expensesAmount = expensesAmount; + } + + public Optional getExpensesAmount() { + return Optional.ofNullable(expensesAmount); + } + + public void setTravelExpenses(TravelExpenses travelExpenses) { + this.travelExpenses = travelExpenses; + } + + public Optional getTravelExpenses() { + return Optional.ofNullable(travelExpenses); + } + + public void setMedicalExpenses(MedicalExpenses medicalExpenses) { + this.medicalExpenses = medicalExpenses; + } + + public Optional getMedicalExpenses() { + return Optional.ofNullable(medicalExpenses); + } + + public void setMiscellaneousExpenses(MiscellaneousExpenses miscellaneousExpenses) { + this.miscellaneousExpenses = miscellaneousExpenses; + } + + public Optional getMiscellaneousExpenses() { + return Optional.ofNullable(miscellaneousExpenses); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditExpensesDescriptor)) { + return false; + } + + // state check + EditExpensesDescriptor e = (EditExpensesDescriptor) other; + + return getExpensesAmount().equals(e.getExpensesAmount()) + && getTravelExpenses().equals(e.getTravelExpenses()) + && getMedicalExpenses().equals(e.getMedicalExpenses()) + && getMiscellaneousExpenses().equals(e.getMiscellaneousExpenses()); + } + } +} + diff --git a/src/main/java/seedu/address/logic/commands/AddLeavesCommand.java b/src/main/java/seedu/address/logic/commands/AddLeavesCommand.java new file mode 100644 index 000000000000..9907f113cb67 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AddLeavesCommand.java @@ -0,0 +1,202 @@ +package seedu.address.logic.commands; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SCHEDULE_DATE; + +import java.util.HashSet; +import java.util.Set; + +import com.google.common.collect.Multimap; +import com.google.common.collect.TreeMultimap; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.EmployeeId; +import seedu.address.model.person.Person; +import seedu.address.model.schedule.Date; +import seedu.address.model.schedule.DateComparator; +import seedu.address.model.schedule.EmployeeIdComparator; +import seedu.address.model.schedule.Schedule; +import seedu.address.model.schedule.Type; + + +/** + * The {@code AddLeavesCommand} class is used for scheduling multiple employees with leave schedules. + * All the observable employees on the employees list panel before or after find/filter/list + * will be scheduled leaves based on the set of dates specified by the user. + * + * @see seedu.address.logic.parser.AddLeavesCommandParser class for the parser. + */ +public class AddLeavesCommand extends Command { + + public static final String COMMAND_WORD = "addLeaves"; + public static final String COMMAND_ALIAS = "al"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": schedule leave for all observable employees " + + "in the list by specifying the date of leave to take. " + + "\nParameters: " + + PREFIX_SCHEDULE_DATE + "[DD/MM/YYYY] .... *You can specify more than 1 date prefix to schedule*" + + "\nExample: " + COMMAND_WORD + " " + + PREFIX_SCHEDULE_DATE + "02/02/2019"; + + public static final String MESSAGE_SUCCESS_SOME_ADDED = "New leave schedules added for SOME of" + + " the observable employees whom are not yet added date: %1$s"; + + public static final String MESSAGE_SUCCESS_ALL_ADDED = "New leave schedules added for ALL of" + + " the observable employees wom are not yet added with date: %1$s"; + + public static final String MESSAGE_NO_PERSON_OBSERVED = + "No observable employees found in list! " + + "\nTry using %1$s/%2$s/%3$s commands get the employees you want to schedule work for."; + + public static final String MESSAGE_PERSON_ALL_ADDED_LEAVE = "Every observable employees in the list has " + + "already been added with leave on %1$s !"; + + public static final String MESSAGE_PERSON_ALL_ADDED_LEAVE_NOT_ON_WORK = "Every observable employees " + + "whom are not on work in the list has already been added with leave on %1$s !"; + + public static final String MESSAGE_PERSON_ALL_HAS_WORK_SAME_DATE = "Unable to schedule work for the following " + + "employees below whom are on work:"; + + public static final String MESSAGE_EMPLOYEE_ON_WORK = "Employee Id: %1$s Has work on: %2$s"; + + private final Set setOfDates = new HashSet<>(); + + /** + * AddLeavesCommand + * @param date Set of dates containing the date of leaves to schedule. + */ + public AddLeavesCommand(Set date) { + requireAllNonNull(date); + this.setOfDates.addAll(date); + } + + /** + * AddLeavesCommand execution. + *

+ * Each date specified by the user will be checked with every observable employee for the possibility + * of scheduling leave. No schedules will be created if existing work or leave is found on that date. + *

+ * @param model {@code Model} which the command will operate on the model. + * @param history {@code CommandHistory} which the command history will be added. + * @return CommandResult, String success feedback to the user. + * @throws CommandException String failure feedback to the user if error in execution. + */ + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + Type leave = new Type(Type.LEAVE); + Type work = new Type(Type.WORK); + Multimap employeeIdMapToWorks = TreeMultimap.create( + new EmployeeIdComparator(), new DateComparator()); + boolean commit = false; + + //model.updateFilteredPersonList(Model.PREDICATE_SHOW_ALL_PERSONS); + if (model.getFilteredPersonList().size() == 0) { + throw new CommandException(String.format(MESSAGE_NO_PERSON_OBSERVED, + FindCommand.COMMAND_WORD, ListCommand.COMMAND_WORD, FilterCommand.COMMAND_WORD)); + } + + for (Date date : setOfDates) { + for (Person person : model.getFilteredPersonList()) { + Schedule toWorkSchedule = new Schedule(person.getEmployeeId(), work, date); + Schedule toLeaveSchedule = new Schedule(person.getEmployeeId(), leave, date); + + if (model.hasSchedule(toWorkSchedule)) { + employeeIdMapToWorks.put(person.getEmployeeId(), date); + + } else if (!model.hasSchedule(toLeaveSchedule)) { + model.addSchedule(toLeaveSchedule); + commit = true; + } + } + } + + String textFeedbackToUser = getUserInteractionFeedback(employeeIdMapToWorks, commit, setOfDates); + if (!commit) { + throw new CommandException(String.format(textFeedbackToUser)); + } + + model.commitScheduleList(); + return new CommandResult(String.format(textFeedbackToUser)); + } + + /** + * Compares if both objects are equal. + * @param other similar object type to be compared with. + * @return Boolean, True if both objects are equal based on the defined conditions. + */ + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof AddLeavesCommand // instanceof handles nulls + && setOfDates.equals(((AddLeavesCommand) other).setOfDates)); + } + + /** + * User interaction feedback generator. + *

+ * There are 4 possibilities. + * 1) No leave schedule is committed because everyone has already been scheduled leave and + * no employees found with work. + * + * 2) No leave schedule is committed either because the employees have been scheduled leave or + * has work on that day. + * + * 3) Some leave schedules is committed because the employees are not yet scheduled leave, or + * not committed because the employee has work on that day. + * + * 4) Leave schedule is committed for all employees. + *

+ * @param employeeIdMapToWorks EmployeeId mapped to date of working schedules + * @param commit Boolean whether has a schedule been committed or not. + * @param setOfDates Set of dates to schedule + * @return String to feedback to user. + */ + public static String getUserInteractionFeedback (Multimap employeeIdMapToWorks, Boolean commit, + Set setOfDates) { + String noneCommitted; + StringBuilder someCommitted; + String allCommitted; + String textFeedbackToUser; + + //No schedule committed and no employees found with work + if ((!commit) && (employeeIdMapToWorks.isEmpty())) { + noneCommitted = String.format(MESSAGE_PERSON_ALL_ADDED_LEAVE, setOfDates); + textFeedbackToUser = noneCommitted; + + //Not Committed and some employees found with work + } else if ((!commit) && (!employeeIdMapToWorks.isEmpty())) { + someCommitted = new StringBuilder(); + someCommitted.append(String.format(MESSAGE_PERSON_ALL_ADDED_LEAVE_NOT_ON_WORK, setOfDates) + "\n"); + someCommitted.append(MESSAGE_PERSON_ALL_HAS_WORK_SAME_DATE); + someCommitted.append("\n"); + for (EmployeeId employeeId : employeeIdMapToWorks.keySet()) { + someCommitted.append( + String.format(MESSAGE_EMPLOYEE_ON_WORK, employeeId, employeeIdMapToWorks.get(employeeId))); + someCommitted.append("\n"); + } + textFeedbackToUser = someCommitted.toString(); + + //Committed and some employees found with work + } else if ((commit) && (!employeeIdMapToWorks.isEmpty())) { + someCommitted = new StringBuilder(String.format(MESSAGE_SUCCESS_SOME_ADDED, setOfDates) + "\n"); + someCommitted.append(MESSAGE_PERSON_ALL_HAS_WORK_SAME_DATE); + someCommitted.append("\n"); + for (EmployeeId employeeId : employeeIdMapToWorks.keySet()) { + someCommitted.append( + String.format(MESSAGE_EMPLOYEE_ON_WORK, employeeId, employeeIdMapToWorks.get(employeeId))); + someCommitted.append("\n"); + } + textFeedbackToUser = someCommitted.toString(); + + } else { + allCommitted = String.format(MESSAGE_SUCCESS_ALL_ADDED, setOfDates); + textFeedbackToUser = allCommitted; + + } + + return textFeedbackToUser; + } +} + diff --git a/src/main/java/seedu/address/logic/commands/AddRecruitmentPostCommand.java b/src/main/java/seedu/address/logic/commands/AddRecruitmentPostCommand.java new file mode 100644 index 000000000000..2f42911539e7 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AddRecruitmentPostCommand.java @@ -0,0 +1,85 @@ +package seedu.address.logic.commands; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_JOB_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_JOB_POSITION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MINIMUM_EXPERIENCE; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_EXPENSES; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_RECRUITMENT; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_SCHEDULES; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.recruitment.Recruitment; + +/** + * Adds a recruitment post on the address book. + */ +public class AddRecruitmentPostCommand extends Command { + + public static final String COMMAND_WORD = "addRecruitmentPost"; + public static final String COMMAND_ALIAS = "arp"; + + public static final String MESSAGE_USAGE2 = COMMAND_WORD + ": Available Jobs. " + + "\nParameters: " + + PREFIX_JOB_POSITION + "[Job Position:] " + + PREFIX_MINIMUM_EXPERIENCE + "[min working experience(Integer):] " + + PREFIX_JOB_DESCRIPTION + "[Job Description:]\n" + + "Example: " + COMMAND_WORD + " " + PREFIX_JOB_POSITION + + "IT Manager " + PREFIX_MINIMUM_EXPERIENCE + "3 " + + PREFIX_JOB_DESCRIPTION + "To maintain the network server in company"; + + + public static final String MESSAGE_SUCCESS = "New recruitment post is added: %1$s"; + public static final String MESSAGE_DUPLICATE_POST = "This recruitment post already exists in the address book"; + + private static boolean isPostDuplicated = false; + private static boolean isWorkExpDuplicated = false; + private static boolean isJobDescriptionDuplicated = false; + private final Recruitment toAddRecruitment; + + /** + * Creates an AddRecruitmentPostCommand to add the specified {@code Post} + */ + public AddRecruitmentPostCommand(Recruitment recruitment) { + requireAllNonNull(recruitment); + this.toAddRecruitment = recruitment; + } + + public static void setIsPostDuplicated(boolean verifyPostDuplication) { + isPostDuplicated = verifyPostDuplication; + } + + public static void setIsWorkExpDuplicated(boolean verifyWorkExpDuplication) { + isWorkExpDuplicated = verifyWorkExpDuplication; + } + + public static void setIsJobDescriptionDuplicated(boolean verifyJobDescriptionDuplication) { + isJobDescriptionDuplicated = verifyJobDescriptionDuplication; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + if (model.hasRecruitment(toAddRecruitment)) { + throw new CommandException(MESSAGE_DUPLICATE_POST); + } + + model.addRecruitment(toAddRecruitment); + model.commitRecruitmentPostList(); + model.updateFilteredExpensesList(PREDICATE_SHOW_ALL_EXPENSES); + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + model.updateFilteredScheduleList(PREDICATE_SHOW_ALL_SCHEDULES); + model.updateFilteredRecruitmentList(PREDICATE_SHOW_ALL_RECRUITMENT); + return new CommandResult(String.format(MESSAGE_SUCCESS, toAddRecruitment)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof AddRecruitmentPostCommand // instanceof handles nulls + && toAddRecruitment.equals(((AddRecruitmentPostCommand) other).toAddRecruitment)); + } + +} diff --git a/src/main/java/seedu/address/logic/commands/AddScheduleCommand.java b/src/main/java/seedu/address/logic/commands/AddScheduleCommand.java new file mode 100644 index 000000000000..3c158fac8635 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AddScheduleCommand.java @@ -0,0 +1,121 @@ +package seedu.address.logic.commands; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMPLOYEEID; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SCHEDULE_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SCHEDULE_TYPE; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_EXPENSES; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_SCHEDULES; + +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.schedule.Schedule; +import seedu.address.model.schedule.Type; + +/** + * The {@code AddScheduleCommand} class is used for scheduling an employee with a + * work or leave schedule on a specific date. + * + * @see seedu.address.logic.parser.AddScheduleCommandParser class for the parser. + */ +public class AddScheduleCommand extends Command { + + public static final String COMMAND_WORD = "addSchedule"; + public static final String COMMAND_ALIAS = "as"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": schedule work " + + "by specifying the Employee number, date and type. " + + "\nParameters: " + + PREFIX_EMPLOYEEID + "EMPLOYEEID " + + PREFIX_SCHEDULE_DATE + "DD/MM/YYYY " + + PREFIX_SCHEDULE_TYPE + "[WORK/LEAVE] \n" + + "Example: " + COMMAND_WORD + " " + + PREFIX_EMPLOYEEID + "000001 " + + PREFIX_SCHEDULE_DATE + "02/02/2019 " + + PREFIX_SCHEDULE_TYPE + "LEAVE"; + + public static final String MESSAGE_SUCCESS = "New schedule added: Employee Id:%1$s %2$s"; + public static final String MESSAGE_DUPLICATE_SCHEDULE = "This schedule already exists in the address book"; + public static final String MESSAGE_HAS_WORK = "This employee has work scheduled on same date!"; + public static final String MESSAGE_HAS_LEAVE = "This employee has leave scheduled on same date!"; + public static final String MESSAGE_EMPLOYEE_ID_NOT_FOUND = "Employee Id not found in address book"; + + private Person toCheckEmployeeId; + private final Schedule toAddSchedule; + + /** + * AddScheduleCommand + * @param schedule Schedule object containing the employee id, type of schedule and date. + */ + public AddScheduleCommand(Schedule schedule) { + requireAllNonNull(schedule); + this.toAddSchedule = schedule; + toCheckEmployeeId = new Person(schedule.getEmployeeId()); + } + + /** + * AddScheduleCommand execution. + *

+ * Checks if employee id exists, followed by checking if employee id has leave/work scheduled on same day. + * Schedule will be committed if all checks passed. + *

+ * @param model {@code Model} which the command will operate on the model. + * @param history {@code CommandHistory} which the command history will be added. + * @return CommandResult, String success feedback to the user. + * @throws CommandException String failure feedback to the user if error in execution. + */ + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + + Type type = toAddSchedule.getType(); + Type work = new Type(Type.WORK); + Type leave = new Type(Type.LEAVE); + + if (!model.hasEmployeeId(toCheckEmployeeId)) { + throw new CommandException(MESSAGE_EMPLOYEE_ID_NOT_FOUND); + + } else if (model.hasSchedule(toAddSchedule)) { + throw new CommandException(MESSAGE_DUPLICATE_SCHEDULE); + + } else if (type.equals(work)) { + Schedule toCheckLeave = new Schedule(toAddSchedule.getEmployeeId(), leave, + toAddSchedule.getScheduleDate()); + if (model.hasSchedule(toCheckLeave)) { + throw new CommandException(MESSAGE_HAS_LEAVE); + } + + } else if (type.equals(leave)) { + Schedule toCheckWork = new Schedule(toAddSchedule.getEmployeeId(), work, + toAddSchedule.getScheduleDate()); + if (model.hasSchedule(toCheckWork)) { + throw new CommandException(MESSAGE_HAS_WORK); + } + } + + model.addSchedule(toAddSchedule); + model.commitScheduleList(); + + model.updateFilteredExpensesList(PREDICATE_SHOW_ALL_EXPENSES); + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + model.updateFilteredScheduleList(PREDICATE_SHOW_ALL_SCHEDULES); + + return new CommandResult(String.format(MESSAGE_SUCCESS, toAddSchedule.getEmployeeId(), + toAddSchedule)); + } + + /** + * Compares if both objects are equal. + * @param other similar object type to be compared with. + * @return Boolean, True if both objects are equal based on the defined conditions. + */ + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof AddScheduleCommand // instanceof handles nulls + && toAddSchedule.equals(((AddScheduleCommand) other).toAddSchedule)); + } +} + diff --git a/src/main/java/seedu/address/logic/commands/AddWorksCommand.java b/src/main/java/seedu/address/logic/commands/AddWorksCommand.java new file mode 100644 index 000000000000..914e41f14c26 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AddWorksCommand.java @@ -0,0 +1,193 @@ +package seedu.address.logic.commands; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SCHEDULE_DATE; + +import java.util.HashSet; +import java.util.Set; + +import com.google.common.collect.Multimap; +import com.google.common.collect.TreeMultimap; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.EmployeeId; +import seedu.address.model.person.Person; +import seedu.address.model.schedule.Date; +import seedu.address.model.schedule.DateComparator; +import seedu.address.model.schedule.EmployeeIdComparator; +import seedu.address.model.schedule.Schedule; +import seedu.address.model.schedule.Type; + +/** + * The {@code AddWorksCommand} class is used for scheduling multiple employees with work schedules. + * All the observable employees on the employees list panel before or after find/filter/list + * will be scheduled work based on the dates specified by the user. + * + * @see seedu.address.logic.parser.AddWorksCommandParser class for the parser. + */ +public class AddWorksCommand extends Command { + + public static final String COMMAND_WORD = "addWorks"; + public static final String COMMAND_ALIAS = "aw"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": schedule working schedule for all observable " + + "employees in the list by specifying the date to work. " + + "\nParameters: " + + PREFIX_SCHEDULE_DATE + "[DD/MM/YYYY] .... *You can specify more than 1 date prefix to schedule*" + + "\nExample: " + COMMAND_WORD + " " + + PREFIX_SCHEDULE_DATE + "02/02/2019 "; + + public static final String MESSAGE_SUCCESS_SOME_ADDED = "New working schedules added for SOME of" + + " the observable employees whom are not yet added date: %1$s"; + + public static final String MESSAGE_SUCCESS_ALL_ADDED = "New working schedules added for ALL of" + + " the observable employees whom are not yet added with date: %1$s"; + + public static final String MESSAGE_NO_PERSON_OBSERVED = + "No observable employees found in list! " + + "\nTry using %1$s/%2$s/%3$s commands get the employees you want to schedule work for."; + + public static final String MESSAGE_PERSON_ALL_ADDED_WORK = "Every observable employees in the list has " + + "already been added with work on %1$s !"; + + public static final String MESSAGE_PERSON_ALL_ADDED_WORK_NOT_ON_LEAVE = "Every observable employees " + + "whom are not on leave in the list has already been added with work on %1$s !"; + + public static final String MESSAGE_PERSON_ALL_HAS_LEAVE_SAME_DATE = "Unable to schedule work for the following " + + "employees below whom are on leave:"; + + public static final String MESSAGE_EMPLOYEE_ON_LEAVE = "Employee Id: %1$s Has leave on: %2$s"; + private final Set setOfDates = new HashSet<>(); + + /** + * @param date Set of dates containing the date of work to schedule + */ + public AddWorksCommand(Set date) { + requireAllNonNull(date); + this.setOfDates.addAll(date); + } + + /** + * AddWorksCommand execution. + *

+ * Each date specified by the user will be checked with every observable employee for the possibility + * of scheduling work. No schedules will be created if existing work or leave is found on that date. + *

+ * @param model {@code Model} which the command will operate on the model. + * @param history {@code CommandHistory} which the command history will be added. + * @return CommandResult, String success feedback to the user. + * @throws CommandException String failure feedback to the user if error in execution. + */ + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + Type work = new Type(Type.WORK); + Type leave = new Type(Type.LEAVE); + Multimap employeeIdMapToLeaves = TreeMultimap.create( + new EmployeeIdComparator(), new DateComparator()); + boolean commit = false; + + //model.updateFilteredPersonList(Model.PREDICATE_SHOW_ALL_PERSONS); + if (model.getFilteredPersonList().size() == 0) { + throw new CommandException(String.format(MESSAGE_NO_PERSON_OBSERVED, + FindCommand.COMMAND_WORD, ListCommand.COMMAND_WORD, FilterCommand.COMMAND_WORD)); + } + + for (Date date :setOfDates) { + for (Person person : model.getFilteredPersonList()) { + Schedule toAddSchedule = new Schedule(person.getEmployeeId(), work , date); + Schedule hasLeaveSchedule = new Schedule(person.getEmployeeId(), leave , date); + if (model.hasSchedule(hasLeaveSchedule)) { + employeeIdMapToLeaves.put(person.getEmployeeId(), date); + } else if (!model.hasSchedule(toAddSchedule)) { + commit = true; + model.addSchedule(toAddSchedule); + } + } + } + String textFeedbackToUser = getUserInteractionFeedback(employeeIdMapToLeaves, commit, setOfDates); + if (!commit) { + throw new CommandException(textFeedbackToUser); + } + model.commitScheduleList(); + return new CommandResult(textFeedbackToUser); + } + + /** + * Compares if both objects are equal. + * @param other similar object type to be compared with. + * @return Boolean, True if both objects are equal based on the defined conditions. + */ + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof AddWorksCommand // instanceof handles nulls + && setOfDates.equals(((AddWorksCommand) other).setOfDates)); + } + + /** + * User interaction feedback generator. + *

+ * There are 4 possibilities. + * 1) No work schedule is committed because everyone has already been scheduled work and + * no employees found with leave. + * + * 2) No work schedule is committed either because the employees have been scheduled work or + * has leave on that day. + * + * 3) Some work schedules is committed because the employees are not yet scheduled work, or + * not committed because the employee has leave on that day. + * + * 4) Work schedule is committed for all employees. + *

+ * @param employeeIdMapToLeaves EmployeeId mapped to date of working schedules + * @param commit Boolean whether has a schedule been committed or not. + * @param setOfDates Set of dates to schedule + * @return String to feedback to user. + */ + public static String getUserInteractionFeedback (Multimap employeeIdMapToLeaves, Boolean commit, + Set setOfDates) { + String noneCommitted; + StringBuilder someCommitted; + String allCommitted; + String textFeedbackToUser; + + //No schedule committed and no employees found with leave + if ((!commit) && (employeeIdMapToLeaves.isEmpty())) { + noneCommitted = String.format(MESSAGE_PERSON_ALL_ADDED_WORK, setOfDates); + textFeedbackToUser = noneCommitted; + + //Not Committed and some employees found with leave + } else if ((!commit) && (!employeeIdMapToLeaves.isEmpty())) { + someCommitted = new StringBuilder(); + someCommitted.append(String.format(MESSAGE_PERSON_ALL_ADDED_WORK_NOT_ON_LEAVE, setOfDates) + "\n"); + someCommitted.append(MESSAGE_PERSON_ALL_HAS_LEAVE_SAME_DATE + "\n"); + for (EmployeeId employeeId : employeeIdMapToLeaves.keySet()) { + someCommitted.append( + String.format(MESSAGE_EMPLOYEE_ON_LEAVE, employeeId, employeeIdMapToLeaves.get(employeeId))); + someCommitted.append("\n"); + } + textFeedbackToUser = someCommitted.toString(); + + //Committed and some employees found with leave + } else if ((commit) && (!employeeIdMapToLeaves.isEmpty())) { + someCommitted = new StringBuilder(String.format(MESSAGE_SUCCESS_SOME_ADDED, setOfDates) + "\n"); + someCommitted.append(MESSAGE_PERSON_ALL_HAS_LEAVE_SAME_DATE + "\n"); + for (EmployeeId employeeId : employeeIdMapToLeaves.keySet()) { + someCommitted.append( + String.format(MESSAGE_EMPLOYEE_ON_LEAVE, employeeId, employeeIdMapToLeaves.get(employeeId))); + someCommitted.append("\n"); + } + textFeedbackToUser = someCommitted.toString(); + + } else { + allCommitted = String.format(MESSAGE_SUCCESS_ALL_ADDED, setOfDates); + textFeedbackToUser = allCommitted; + + } + + return textFeedbackToUser; + } +} + diff --git a/src/main/java/seedu/address/logic/commands/CalculateLeavesCommand.java b/src/main/java/seedu/address/logic/commands/CalculateLeavesCommand.java new file mode 100644 index 000000000000..e749123e76bc --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/CalculateLeavesCommand.java @@ -0,0 +1,123 @@ +package seedu.address.logic.commands; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMPLOYEEID; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SCHEDULE_YEAR; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_EXPENSES; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_SCHEDULES; + +import java.util.ArrayList; +import java.util.List; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.EmployeeId; +import seedu.address.model.person.Person; +import seedu.address.model.schedule.EmployeeIdScheduleContainsKeywordsPredicate; +import seedu.address.model.schedule.Schedule; +import seedu.address.model.schedule.Type; +import seedu.address.model.schedule.Year; + +/** + * The {@code CalculateLeavesCommand} class is used for calculating + * total number of leaves scheduled by an employee given a specified year. + * + * @see seedu.address.logic.parser.CalculateLeavesCommandParser class for the parser. + */ +public class CalculateLeavesCommand extends Command { + + public static final String COMMAND_WORD = "calculateLeaves"; + public static final String COMMAND_ALIAS = "cl"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Calculate total leaves schedule for the year " + + "by specifying the Employee number and year. " + + "\nParameters: " + + PREFIX_EMPLOYEEID + "EMPLOYEEID " + + PREFIX_SCHEDULE_YEAR + "YYYY" + + "\nExample: " + + COMMAND_WORD + " " + + PREFIX_EMPLOYEEID + "000001 " + + PREFIX_SCHEDULE_YEAR + "2019"; + + public static final String MESSAGE_SUCCESS = "Number of leaves scheduled for Employee %1$s year %2$s is: %3$s."; + + public static final String MESSAGE_NO_SCHEDULE_FOUND = "No leaves found for the employee in that year!"; + public static final String MESSAGE_EMPLOYEE_ID_NOT_FOUND = "Employee Id not found in system!"; + + private final Year year; + private final EmployeeId employeeId; + private Person toCheckEmployeeId; + + /** + * CalculateLeavesCommand + * @param id Employee id + * @param year Year to calculate leaves taken by the employee + */ + public CalculateLeavesCommand(EmployeeId id, Year year) { + requireAllNonNull(id); + requireAllNonNull(year); + this.employeeId = id; + this.year = year; + toCheckEmployeeId = new Person(id); + } + + /** + * CalculateLeavesCommand execution. + *

+ * Calculates total number of leaves scheduled for an employee + * for the entire specified year in the schedule list. + *

+ * @param model {@code Model} which the command will operate on the model. + * @param history {@code CommandHistory} which the command history will be added. + * @return CommandResult, String success feedback to the user. + * @throws CommandException String failure feedback to the user if error in execution. + */ + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + int numLeaves = 0; + + if (!model.hasEmployeeId(toCheckEmployeeId)) { + throw new CommandException(MESSAGE_EMPLOYEE_ID_NOT_FOUND); + } + + List employeeIdList = new ArrayList<>(); + employeeIdList.add(employeeId.value); + EmployeeIdScheduleContainsKeywordsPredicate employeeIdPredicate = + new EmployeeIdScheduleContainsKeywordsPredicate(employeeIdList); + model.updateFilteredScheduleList(employeeIdPredicate); + + if (model.getFilteredScheduleList().size() == 0) { + throw new CommandException(MESSAGE_NO_SCHEDULE_FOUND); + } + + for (Schedule schedule : model.getFilteredScheduleList()) { + if (schedule.getScheduleYear().equals(year.toString()) + && schedule.getType().toString().equals(Type.LEAVE)) { + numLeaves++; + } + } + + model.updateFilteredExpensesList(PREDICATE_SHOW_ALL_EXPENSES); + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + model.updateFilteredScheduleList(PREDICATE_SHOW_ALL_SCHEDULES); + + return new CommandResult(String.format(MESSAGE_SUCCESS, employeeId, year, numLeaves)); + } + + /** + * Compares if both objects are equal. + * @param other similar object type to be compared with. + * @return Boolean, True if both objects are equal based on the defined conditions. + */ + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof CalculateLeavesCommand // instanceof handles nulls + && year.equals(((CalculateLeavesCommand) other).year) + && employeeId.equals(((CalculateLeavesCommand) other).employeeId)); + } +} + diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/address/logic/commands/ClearCommand.java index 1f85bcfe85a8..a93bdaadb027 100644 --- a/src/main/java/seedu/address/logic/commands/ClearCommand.java +++ b/src/main/java/seedu/address/logic/commands/ClearCommand.java @@ -2,24 +2,68 @@ import static java.util.Objects.requireNonNull; +import java.util.HashSet; +import java.util.Set; + import seedu.address.logic.CommandHistory; -import seedu.address.model.AddressBook; import seedu.address.model.Model; +import seedu.address.model.ModelTypes; +import seedu.address.model.addressbook.AddressBook; +import seedu.address.model.expenses.ExpensesList; +import seedu.address.model.recruitment.RecruitmentList; +import seedu.address.model.schedule.ScheduleList; /** - * Clears the address book. + * The {@code ClearCommand} class is used for clearing all storage. + * The entire schedule, addressbook, expenses, recruitment storage will be reset to a clean state. */ public class ClearCommand extends Command { public static final String COMMAND_WORD = "clear"; - public static final String MESSAGE_SUCCESS = "Address book has been cleared!"; + public static final String MESSAGE_SUCCESS = "Address book, Schedule list, " + + "expenses list, and recruitment list have been cleared!"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Clears all employees, schedules, recruitment posts" + + " and expenses."; + /** + * ClearCommand execution. + *

+ * Checks if individual storage has data to be cleared, if so, clear it. + * Takes note of all storage that are cleared, places the type of storage cleared into a set + * and commit it. Important for undo and redo command to work properly. + *

+ * @param model {@code Model} which the command will operate on the model. + * @param history {@code CommandHistory} which the command history will be added. + * @return CommandResult, String success feedback to the user. + */ @Override public CommandResult execute(Model model, CommandHistory history) { requireNonNull(model); - model.resetData(new AddressBook()); - model.commitAddressBook(); + + Set set = new HashSet<>(); + set.add(ModelTypes.ADDRESS_BOOK); + model.resetAddressBookData(new AddressBook()); + + model.updateFilteredExpensesList(model.PREDICATE_SHOW_ALL_EXPENSES); + if (model.getFilteredExpensesList().size() > 0) { + model.resetDataExpenses(new ExpensesList()); + set.add(ModelTypes.EXPENSES_LIST); + } + + model.updateFilteredRecruitmentList(model.PREDICATE_SHOW_ALL_RECRUITMENT); + if (model.getFilteredRecruitmentList().size() > 0) { + model.resetRecruitmentListData(new RecruitmentList()); + set.add(ModelTypes.RECRUITMENT_LIST); + } + + model.updateFilteredScheduleList(model.PREDICATE_SHOW_ALL_SCHEDULES); + if (model.getFilteredScheduleList().size() > 0) { + model.resetScheduleListData(new ScheduleList()); + set.add(ModelTypes.SCHEDULES_LIST); + } + + model.commitMultipleLists(set); return new CommandResult(MESSAGE_SUCCESS); } } diff --git a/src/main/java/seedu/address/logic/commands/ClearExpensesCommand.java b/src/main/java/seedu/address/logic/commands/ClearExpensesCommand.java new file mode 100644 index 000000000000..5f4bfd662e99 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ClearExpensesCommand.java @@ -0,0 +1,37 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.expenses.ExpensesList; + +/** + * Clears the expenses list. + */ +public class ClearExpensesCommand extends Command { + + public static final String COMMAND_WORD = "clearExpenses"; + public static final String COMMAND_ALIAS = "ce"; + public static final String MESSAGE_SUCCESS = "Expenses list has been cleared!"; + public static final String MESSAGE_FAILURE_CLEARED = "Expenses list is empty!"; + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Clears the Expenses List\n" + + "Example: " + COMMAND_WORD; + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + + model.updateFilteredExpensesList(model.PREDICATE_SHOW_ALL_EXPENSES); + if (model.getFilteredExpensesList().size() > 0) { + model.resetDataExpenses(new ExpensesList()); + model.commitExpensesList(); + } else { + throw new CommandException(MESSAGE_FAILURE_CLEARED); + } + + return new CommandResult(MESSAGE_SUCCESS); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ClearRecruitmentPostCommand.java b/src/main/java/seedu/address/logic/commands/ClearRecruitmentPostCommand.java new file mode 100644 index 000000000000..1e8a97802718 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ClearRecruitmentPostCommand.java @@ -0,0 +1,47 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_EXPENSES; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_RECRUITMENT; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_SCHEDULES; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.recruitment.RecruitmentList; + +/** + * Clears the recruitment list. + */ +public class ClearRecruitmentPostCommand extends Command { + + public static final String COMMAND_WORD = "clearRecruitmentPost"; + public static final String COMMAND_ALIAS = "crp"; + public static final String MESSAGE_SUCCESS = "Recruitment list has been cleared!"; + public static final String MESSAGE_FAILURE_CLEARED = "Recruitment list is empty!"; + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Clears the Recruitment List\n" + + "Example: " + COMMAND_WORD; + + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + + model.updateFilteredRecruitmentList(model.PREDICATE_SHOW_ALL_RECRUITMENT); + if (model.getFilteredRecruitmentList().size() > 0) { + model.resetRecruitmentListData(new RecruitmentList()); + model.commitRecruitmentPostList(); + } else { + throw new CommandException(MESSAGE_FAILURE_CLEARED); + } + + model.updateFilteredExpensesList(PREDICATE_SHOW_ALL_EXPENSES); + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + model.updateFilteredScheduleList(PREDICATE_SHOW_ALL_SCHEDULES); + model.updateFilteredRecruitmentList(PREDICATE_SHOW_ALL_RECRUITMENT); + + return new CommandResult(MESSAGE_SUCCESS); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ClearScheduleCommand.java b/src/main/java/seedu/address/logic/commands/ClearScheduleCommand.java new file mode 100644 index 000000000000..43ef877e5ddb --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ClearScheduleCommand.java @@ -0,0 +1,57 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_EXPENSES; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_SCHEDULES; + +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.schedule.ScheduleList; + +/** + * The {@code ClearScheduleCommand} class is used for clearing the entire schedule list. + * The entire schedule storage will be reset to a clean state. + */ +public class ClearScheduleCommand extends Command { + + public static final String COMMAND_WORD = "clearSchedules"; + public static final String COMMAND_ALIAS = "cs"; + public static final String MESSAGE_SUCCESS = "Schedule list has been cleared!"; + public static final String MESSAGE_FAILURE_CLEARED = "Schedule list is empty!"; + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Clears the Schedule List\n" + + "Example: " + COMMAND_WORD; + + /** + * ClearScheduleCommand execution. + *

+ * Checks if schedule storage has data to be cleared, if so, clear it. + * Only commit if there are data cleared. + * Important for undo and redo command to work properly. + *

+ * @param model {@code Model} which the command will operate on the model. + * @param history {@code CommandHistory} which the command history will be added. + * @return CommandResult, String success feedback to the user. + * @throws CommandException String failure feedback to the user if error in execution. + */ + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + + model.updateFilteredScheduleList(model.PREDICATE_SHOW_ALL_SCHEDULES); + if (model.getFilteredScheduleList().size() > 0) { + model.resetScheduleListData(new ScheduleList()); + model.commitScheduleList(); + } else { + throw new CommandException(MESSAGE_FAILURE_CLEARED); + } + + model.updateFilteredExpensesList(PREDICATE_SHOW_ALL_EXPENSES); + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + model.updateFilteredScheduleList(PREDICATE_SHOW_ALL_SCHEDULES); + + return new CommandResult(MESSAGE_SUCCESS); + } +} diff --git a/src/main/java/seedu/address/logic/commands/Command.java b/src/main/java/seedu/address/logic/commands/Command.java index 34e99d786ec6..a361bddcf459 100644 --- a/src/main/java/seedu/address/logic/commands/Command.java +++ b/src/main/java/seedu/address/logic/commands/Command.java @@ -2,6 +2,7 @@ import seedu.address.logic.CommandHistory; import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.Model; /** @@ -17,6 +18,6 @@ public abstract class Command { * @return feedback message of the operation result for display * @throws CommandException If an error occurs during command execution. */ - public abstract CommandResult execute(Model model, CommandHistory history) throws CommandException; + public abstract CommandResult execute(Model model, CommandHistory history) throws CommandException, ParseException; } diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java index a20e9d49eac7..6ded7e44d21a 100644 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ b/src/main/java/seedu/address/logic/commands/DeleteCommand.java @@ -1,36 +1,58 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_EXPENSES; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_SCHEDULES; +import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +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.ModelTypes; +import seedu.address.model.expenses.EmployeeIdExpensesContainsKeywordsPredicate; +import seedu.address.model.expenses.Expenses; import seedu.address.model.person.Person; +import seedu.address.model.schedule.EmployeeIdScheduleContainsKeywordsPredicate; +import seedu.address.model.schedule.Schedule; /** - * Deletes a person identified using it's displayed index from the address book. + * The {@code DeleteCommand} class is used for deleting a person identified using it's + * displayed index from the employee observable panel list. */ public class DeleteCommand extends Command { - public static final String COMMAND_WORD = "delete"; + public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted Person: %1$s"; public static final String MESSAGE_USAGE = COMMAND_WORD + ": Deletes the person identified by the index number used in the displayed person list.\n" + "Parameters: INDEX (must be a positive integer)\n" + "Example: " + COMMAND_WORD + " 1"; - public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted Person: %1$s"; - private final Index targetIndex; public DeleteCommand(Index targetIndex) { this.targetIndex = targetIndex; } + /** + * DeleteCommand execution. + *

+ * Checks if schedule storage has schedule/expenses data containing the same employee id + * as the person to delete, if so, clear it. + * {@code Set set} takes note of the set of storage involved in clearing. + * Important for undo and redo command to work properly. + *

+ * @param model {@code Model} which the command will operate on the model. + * @param history {@code CommandHistory} which the command history will be added. + * @return CommandResult, String success feedback to the user. + * @throws CommandException String failure feedback to the user if error in execution. + */ @Override public CommandResult execute(Model model, CommandHistory history) throws CommandException { requireNonNull(model); @@ -41,15 +63,96 @@ public CommandResult execute(Model model, CommandHistory history) throws Command } Person personToDelete = lastShownList.get(targetIndex.getZeroBased()); + + Set set = new HashSet<>(); + set.add(ModelTypes.ADDRESS_BOOK); model.deletePerson(personToDelete); - model.commitAddressBook(); + + if (deleteAllSchedulesFromPerson(model, personToDelete)) { + set.add(ModelTypes.SCHEDULES_LIST); + } + + if (deleteAllExpensesFromPerson (model, personToDelete)) { + set.add(ModelTypes.EXPENSES_LIST); + } + + model.commitMultipleLists(set); + + model.updateFilteredExpensesList(PREDICATE_SHOW_ALL_EXPENSES); + model.updateFilteredScheduleList(PREDICATE_SHOW_ALL_SCHEDULES); + return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, personToDelete)); } + /** + * Compares if both objects are equal. + * @param other similar object type to be compared with. + * @return Boolean, True if both objects are equal based on the defined conditions. + */ @Override public boolean equals(Object other) { return other == this // short circuit if same object || (other instanceof DeleteCommand // instanceof handles nulls && targetIndex.equals(((DeleteCommand) other).targetIndex)); // state check } + + /** + * Deletes all expenses related to person + *

+ * Runs a O(n) loop on the expenses list to delete all expenses containing + * the employee id of the person to delete. + *

+ * @param model which the command will operate on the model. + * @param personToDelete Person to delete from the address book + * @return True if at least 1 expenses is deleted + */ + public boolean deleteAllExpensesFromPerson (Model model, Person personToDelete) { + EmployeeIdExpensesContainsKeywordsPredicate predicatEmployeeId; + List employeeIdList = new ArrayList<>(); + List lastShownListExpenses; + + employeeIdList.add(personToDelete.getEmployeeId().value); + predicatEmployeeId = new EmployeeIdExpensesContainsKeywordsPredicate(employeeIdList); + model.updateFilteredExpensesList(predicatEmployeeId); + lastShownListExpenses = model.getFilteredExpensesList(); + if (lastShownListExpenses.size() == 0) { + return false; + } + + while (lastShownListExpenses.size() != 0) { + Expenses expenseToDelete = lastShownListExpenses.get(0); + model.deleteExpenses(expenseToDelete); + } + return true; + } + + /** + * Deletes all schedules related to person + *

+ * Runs a O(n) loop on the schedule list to delete all schedules containing + * the employee id of the person to delete + *

+ * @param model which the command will operate on the model. + * @param personToDelete Person to delete from the address book + * @return True if at least 1 expenses is deleted + */ + public boolean deleteAllSchedulesFromPerson (Model model, Person personToDelete) { + EmployeeIdScheduleContainsKeywordsPredicate predicatEmployeeId; + List employeeIdList = new ArrayList<>(); + List lastShownListSchedule; + + employeeIdList.add(personToDelete.getEmployeeId().value); + predicatEmployeeId = new EmployeeIdScheduleContainsKeywordsPredicate(employeeIdList); + model.updateFilteredScheduleList(predicatEmployeeId); + lastShownListSchedule = model.getFilteredScheduleList(); + if (lastShownListSchedule.size() == 0) { + return false; + } + + while (lastShownListSchedule.size() != 0) { + Schedule scheduleToDelete = lastShownListSchedule.get(0); + model.deleteSchedule(scheduleToDelete); + } + return true; + } } diff --git a/src/main/java/seedu/address/logic/commands/DeleteLeavesCommand.java b/src/main/java/seedu/address/logic/commands/DeleteLeavesCommand.java new file mode 100644 index 000000000000..c2a45f11bd68 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/DeleteLeavesCommand.java @@ -0,0 +1,104 @@ +package seedu.address.logic.commands; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SCHEDULE_DATE; + +import java.util.Set; + +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.schedule.Date; +import seedu.address.model.schedule.Schedule; +import seedu.address.model.schedule.Type; + + +/** + * The {@code DeleteLeavesCommand} class is used for deleting multiple employees with leave schedules. + * All the observable employees on the employees list panel in the user interface will be deleted + * leaves based on the dates specified by the user. + * + * @see seedu.address.logic.parser.DeleteLeavesCommandParser class for the parser. + */ +public class DeleteLeavesCommand extends Command { + + public static final String COMMAND_WORD = "deleteLeaves"; + public static final String COMMAND_ALIAS = "dl"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Deletes leave schedules for all observable employees " + + "in the list by specifying the date of leave to delete. " + + "\nParameters: " + + PREFIX_SCHEDULE_DATE + "[DD/MM/YYYY] .... *You can specify more than 1 date prefix to schedule*" + + "\nExample: " + COMMAND_WORD + " " + + PREFIX_SCHEDULE_DATE + "02/02/2019"; + + public static final String MESSAGE_SUCCESS = "Leaves deleted for all observable employees that contain date : %1$s"; + + public static final String MESSAGE_NO_PERSON_FOUND = "No observable employees found in list! " + + "Try to list/find/filter the employees you want to delete leaves for"; + + public static final String MESSAGE_PERSON_ALL_DELETED_LEAVE = "Every observable employees in the list" + + " does not have leave on %1$s !"; + + private final Set setOfDates; + + /** + * DeleteLeavesCommand + * @param date Set of dates containing the date of leaves to delete. + */ + public DeleteLeavesCommand(Set date) { + requireAllNonNull(date); + this.setOfDates = date; + } + + /** + * DeleteLeavesCommand execution. + *

+ * Each date specified by the user will be checked with every observable employee for the possibility + * of deleting leave. Leave schedule will be deleted if found on that date. + *

+ * @param model {@code Model} which the command will operate on the model. + * @param history {@code CommandHistory} which the command history will be added. + * @return CommandResult, String success feedback to the user. + * @throws CommandException String failure feedback to the user if error in execution. + */ + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + Type leave = new Type(Type.LEAVE); + boolean commit = false; + + if (model.getFilteredPersonList().size() == 0) { + throw new CommandException(MESSAGE_NO_PERSON_FOUND); + } + for (Date date : setOfDates) { + for (Person person : model.getFilteredPersonList()) { + Schedule toDeleteSchedule = new Schedule(person.getEmployeeId(), leave , date); + if (model.hasSchedule(toDeleteSchedule)) { + model.deleteSchedule(toDeleteSchedule); + commit = true; + } + } + } + + if (!commit) { + throw new CommandException(String.format(MESSAGE_PERSON_ALL_DELETED_LEAVE, setOfDates)); + } + + model.commitScheduleList(); + return new CommandResult(String.format(MESSAGE_SUCCESS, setOfDates)); + } + + /** + * Compares if both objects are equal. + * @param other similar object type to be compared with. + * @return Boolean, True if both objects are equal based on the defined conditions. + */ + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof DeleteLeavesCommand // instanceof handles nulls + && setOfDates.equals(((DeleteLeavesCommand) other).setOfDates)); + } +} + diff --git a/src/main/java/seedu/address/logic/commands/DeleteRecruitmentPostCommand.java b/src/main/java/seedu/address/logic/commands/DeleteRecruitmentPostCommand.java new file mode 100644 index 000000000000..772847fc3490 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/DeleteRecruitmentPostCommand.java @@ -0,0 +1,55 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.recruitment.Recruitment; + +/** + * Deletes a recruitmentPost identified using it's displayed index from the recruitment list. + */ +public class DeleteRecruitmentPostCommand extends Command { + public static final String COMMAND_WORD = "deleteRecruitmentPost"; + public static final String COMMAND_ALIAS = "drp"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Deletes the recruitment post identified by the index number used in the displayed recruitment list.\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_DELETE_RECRUITMENT_POST_SUCCESS = "Deleted Recruitment Post: %1$s"; + + private final Index targetIndex; + + public DeleteRecruitmentPostCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredRecruitmentList(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_RECRUITMENT_POST_DISPLAYED_INDEX); + } + + Recruitment recruitmentPostToDelete = lastShownList.get(targetIndex.getZeroBased()); + model.deleteRecruitmentPost(recruitmentPostToDelete); + model.commitRecruitmentPostList(); + return new CommandResult(String.format(MESSAGE_DELETE_RECRUITMENT_POST_SUCCESS, recruitmentPostToDelete)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof DeleteRecruitmentPostCommand // instanceof handles nulls + && targetIndex.equals(((DeleteRecruitmentPostCommand) other).targetIndex)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/DeleteScheduleCommand.java b/src/main/java/seedu/address/logic/commands/DeleteScheduleCommand.java new file mode 100644 index 000000000000..c32ca0ee5161 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/DeleteScheduleCommand.java @@ -0,0 +1,68 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.schedule.Schedule; + +/** + * The {@code DeleteScheduleCommand} class is used for deleting a schedule identified using it's + * displayed index from the schedule list observable panel list. + */ +public class DeleteScheduleCommand extends Command { + public static final String COMMAND_WORD = "deleteSchedule"; + public static final String COMMAND_ALIAS = "ds"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Deletes the schedule identified by the index number used in the displayed schedule list." + + "\nParameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_DELETE_SCHEDULE_SUCCESS = "Deleted Schedule: %1$s"; + + private final Index targetIndex; + + public DeleteScheduleCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + /** + * CommandResult execution. + * @param model {@code Model} which the command will operate on the model. + * @param history {@code CommandHistory} which the command history will be added. + * @return CommandResult, String success feedback to the user. + * @throws CommandException String failure feedback to the user if error in execution. + */ + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredScheduleList(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_SCHEDULE_DISPLAYED_INDEX); + } + + Schedule scheduleToDelete = lastShownList.get(targetIndex.getZeroBased()); + model.deleteSchedule(scheduleToDelete); + model.commitScheduleList(); + return new CommandResult(String.format(MESSAGE_DELETE_SCHEDULE_SUCCESS, scheduleToDelete)); + } + + /** + * Compares if both objects are equal. + * @param other similar object type to be compared with. + * @return Boolean, True if both objects are equal based on the defined conditions. + */ + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof DeleteScheduleCommand // instanceof handles nulls + && targetIndex.equals(((DeleteScheduleCommand) other).targetIndex)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/DeleteWorksCommand.java b/src/main/java/seedu/address/logic/commands/DeleteWorksCommand.java new file mode 100644 index 000000000000..ead45398ed60 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/DeleteWorksCommand.java @@ -0,0 +1,106 @@ +package seedu.address.logic.commands; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SCHEDULE_DATE; + +import java.util.Set; + +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.schedule.Date; +import seedu.address.model.schedule.Schedule; +import seedu.address.model.schedule.Type; + + +/** + * The {@code DeleteWorksCommand} class is used for deleting multiple employees with work schedules. + * All the observable employees on the employees list panel in the user interface will be deleted + * leaves based on the dates specified by the user. + * + * @see seedu.address.logic.parser.DeleteWorksCommandParser class for the parser. + */ +public class DeleteWorksCommand extends Command { + + public static final String COMMAND_WORD = "deleteWorks"; + public static final String COMMAND_ALIAS = "dw"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Deletes work schedules for all observable employees " + + "in the list by specifying the date of work to delete. " + + "\nParameters: " + + PREFIX_SCHEDULE_DATE + "[DD/MM/YYYY] .... *You can specify more than 1 date prefix to schedule*" + + "\nExample: " + COMMAND_WORD + " " + + PREFIX_SCHEDULE_DATE + "02/02/2019"; + + public static final String MESSAGE_SUCCESS = "Working schedule deleted for all observable " + + "employees that contain date : %1$s"; + + public static final String MESSAGE_NO_PERSON_FOUND = "No observable employees found in list! " + + "Try to list/find/filter the employees you want to delete working schedules for"; + + public static final String MESSAGE_PERSON_ALL_DELETED_WORK = "Every observable employees in the list" + + " does not have working schedule on %1$s !"; + + private final Set setOfDates; + + /** + * DeleteWorksCommand + * @param date Set of dates containing the date of work to delete. + */ + public DeleteWorksCommand(Set date) { + requireAllNonNull(date); + this.setOfDates = date; + } + + /** + * DeleteWorksCommand execution. + *

+ * Each date specified by the user will be checked with every observable employee for the possibility + * of deleting leave. Work schedule will be deleted if found on that date. + *

+ * @param model {@code Model} which the command will operate on the model. + * @param history {@code CommandHistory} which the command history will be added. + * @return CommandResult, String success feedback to the user. + * @throws CommandException String failure feedback to the user if error in execution. + */ + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + Type work = new Type(Type.WORK); + boolean commit = false; + + if (model.getFilteredPersonList().size() == 0) { + throw new CommandException(MESSAGE_NO_PERSON_FOUND); + } + + for (Date date : setOfDates) { + for (Person person : model.getFilteredPersonList()) { + Schedule toDeleteSchedule = new Schedule(person.getEmployeeId(), work , date); + if (model.hasSchedule(toDeleteSchedule)) { + commit = true; + model.deleteSchedule(toDeleteSchedule); + } + } + } + + if (!commit) { + throw new CommandException(String.format(MESSAGE_PERSON_ALL_DELETED_WORK, setOfDates)); + } + + model.commitScheduleList(); + return new CommandResult(String.format(MESSAGE_SUCCESS, setOfDates)); + } + + /** + * Compares if both objects are equal. + * @param other similar object type to be compared with. + * @return Boolean, True if both objects are equal based on the defined conditions. + */ + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof DeleteWorksCommand // instanceof handles nulls + && setOfDates.equals(((DeleteWorksCommand) other).setOfDates)); + } +} + diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java index dc782d8e230f..01dc1410c8e8 100644 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ b/src/main/java/seedu/address/logic/commands/EditCommand.java @@ -2,12 +2,15 @@ import static java.util.Objects.requireNonNull; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DEPARTMENT; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_POSITION; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; +import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -21,11 +24,22 @@ import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; import seedu.address.model.person.Address; +import seedu.address.model.person.Bonus; +import seedu.address.model.person.DateOfBirth; +import seedu.address.model.person.DateOfBirthContainsKeywordsPredicate; +import seedu.address.model.person.Department; import seedu.address.model.person.Email; +import seedu.address.model.person.EmailContainsKeywordsPredicate; +import seedu.address.model.person.EmployeeId; +import seedu.address.model.person.EmployeeIdContainsKeywordsPredicate; import seedu.address.model.person.Name; +import seedu.address.model.person.NameContainsKeywordsPredicate; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; +import seedu.address.model.person.PhoneContainsKeywordsPredicate; +import seedu.address.model.person.Position; +import seedu.address.model.person.Salary; +import seedu.address.model.person.tag.Tag; /** * Edits the details of an existing person in the address book. @@ -41,20 +55,40 @@ public class EditCommand extends Command { + "[" + PREFIX_NAME + "NAME] " + "[" + PREFIX_PHONE + "PHONE] " + "[" + PREFIX_EMAIL + "EMAIL] " + + "[" + PREFIX_DEPARTMENT + "DEPARTMENT] " + + "[" + PREFIX_POSITION + "POSITION] " + "[" + PREFIX_ADDRESS + "ADDRESS] " + "[" + PREFIX_TAG + "TAG]...\n" + "Example: " + COMMAND_WORD + " 1 " + + PREFIX_NAME + "John Doe " + PREFIX_PHONE + "91234567 " - + PREFIX_EMAIL + "johndoe@example.com"; + + PREFIX_EMAIL + "johndoe@example.com " + + PREFIX_DEPARTMENT + "Finance " + + PREFIX_POSITION + "Manager " + + PREFIX_ADDRESS + "21 Lower Kent Ridge Rd, Singapore 119077 " + + PREFIX_TAG + "Fishing"; public static final String MESSAGE_EDIT_PERSON_SUCCESS = "Edited Person: %1$s"; - public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; - public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book."; - + public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided.\n" + + "Example: " + COMMAND_WORD + " 1 " + + PREFIX_NAME + "John Doe " + + PREFIX_PHONE + "91234567 " + + PREFIX_EMAIL + "johndoe@example.com " + + PREFIX_DEPARTMENT + "Finance " + + PREFIX_POSITION + "Manager " + + PREFIX_ADDRESS + "21 Lower Kent Ridge Rd, Singapore 119077 " + + PREFIX_TAG + "Fishing"; + public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book"; + public static final String MESSAGE_DUPLICATE_EMAIL = "This email already exists in the address book"; + public static final String MESSAGE_DUPLICATE_PHONE = "This phone already exists in the address book"; + + private static boolean isEmailDuplicated = false; + private static boolean isPhoneDuplicated = false; private final Index index; private final EditPersonDescriptor editPersonDescriptor; /** + * Creates an EditCommand to edit the specified employee at the specified {@code index} * @param index of the person in the filtered person list to edit * @param editPersonDescriptor details to edit the person with */ @@ -66,6 +100,22 @@ public EditCommand(Index index, EditPersonDescriptor editPersonDescriptor) { this.editPersonDescriptor = new EditPersonDescriptor(editPersonDescriptor); } + public static void setIsEmailDuplicated(boolean verifyEmailDuplication) { + isEmailDuplicated = verifyEmailDuplication; + } + + public static void setIsPhoneDuplicated(boolean verifyPhoneDuplication) { + isPhoneDuplicated = verifyPhoneDuplication; + } + + /** + * Execution of the command will depend on whether there are duplicated Email, Phone or Name & + * DateOfBirth. If any of the duplicated check is true, an exception will be thrown, otherwise, + * the command will be executed accordingly. + * @param model The actual model + * @param history The actual history + * @throws CommandException + */ @Override public CommandResult execute(Model model, CommandHistory history) throws CommandException { requireNonNull(model); @@ -77,8 +127,28 @@ public CommandResult execute(Model model, CommandHistory history) throws Command Person personToEdit = lastShownList.get(index.getZeroBased()); Person editedPerson = createEditedPerson(personToEdit, editPersonDescriptor); - - if (!personToEdit.isSamePerson(editedPerson) && model.hasPerson(editedPerson)) { + EmployeeIdContainsKeywordsPredicate predicate = + employeeIdPredicateCreation(model, personToEdit); + + // Checks for duplicated email + if (model.hasPerson(editedPerson, predicate) && isEmailDuplicated && !isPhoneDuplicated) { + EmailContainsKeywordsPredicate emailPredicate = + new EmailContainsKeywordsPredicate(editedPerson.getEmail().value); + model.updateFilteredPersonList(emailPredicate); + throw new CommandException(MESSAGE_DUPLICATE_EMAIL); + // Checks for duplicated phone + } else if (model.hasPerson(editedPerson, predicate) && !isEmailDuplicated && isPhoneDuplicated) { + PhoneContainsKeywordsPredicate phonePredicate = + new PhoneContainsKeywordsPredicate(editedPerson.getPhone().value); + model.updateFilteredPersonList(phonePredicate); + throw new CommandException(MESSAGE_DUPLICATE_PHONE); + // Checks for duplicated name & date of birth + } else if (model.hasPerson(editedPerson, predicate) && !isEmailDuplicated && !isPhoneDuplicated) { + NameContainsKeywordsPredicate namePredicate = + new NameContainsKeywordsPredicate(Collections.singletonList(editedPerson.getName().fullName)); + DateOfBirthContainsKeywordsPredicate datePredicate = + new DateOfBirthContainsKeywordsPredicate(editedPerson.getDateOfBirth().value); + model.updateFilteredPersonList(namePredicate.and(datePredicate)); throw new CommandException(MESSAGE_DUPLICATE_PERSON); } @@ -88,6 +158,25 @@ public CommandResult execute(Model model, CommandHistory history) throws Command return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, editedPerson)); } + /** + * Creates and returns a {@code EmployeeIdContainsKeywordsPredicate} that contains all the employee IDs + * except for the employee ID of the {@code person} that is being passed as the param. + */ + private static EmployeeIdContainsKeywordsPredicate employeeIdPredicateCreation(Model model, Person person) { + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + + List getFullList = model.getFilteredPersonList(); + List allEmployeeIds = new ArrayList<>(); + + for (Person employeeId : getFullList) { + if (person.getEmployeeId().value != employeeId.getEmployeeId().value) { + allEmployeeIds.add(employeeId.getEmployeeId().value); + } + } + + return new EmployeeIdContainsKeywordsPredicate(allEmployeeIds); + } + /** * Creates and returns a {@code Person} with the details of {@code personToEdit} * edited with {@code editPersonDescriptor}. @@ -95,13 +184,20 @@ public CommandResult execute(Model model, CommandHistory history) throws Command private static Person createEditedPerson(Person personToEdit, EditPersonDescriptor editPersonDescriptor) { assert personToEdit != null; + EmployeeId updatedEmployeeId = personToEdit.getEmployeeId(); Name updatedName = editPersonDescriptor.getName().orElse(personToEdit.getName()); + DateOfBirth updatedDateOfBirth = personToEdit.getDateOfBirth(); Phone updatedPhone = editPersonDescriptor.getPhone().orElse(personToEdit.getPhone()); Email updatedEmail = editPersonDescriptor.getEmail().orElse(personToEdit.getEmail()); + Department updatedDepartment = editPersonDescriptor.getDepartment().orElse(personToEdit.getDepartment()); + Position updatedPosition = editPersonDescriptor.getPosition().orElse(personToEdit.getPosition()); Address updatedAddress = editPersonDescriptor.getAddress().orElse(personToEdit.getAddress()); + Salary updatedSalary = personToEdit.getSalary(); + Bonus updatedBonus = personToEdit.getBonus(); Set updatedTags = editPersonDescriptor.getTags().orElse(personToEdit.getTags()); - return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags); + return new Person(updatedEmployeeId, updatedName, updatedDateOfBirth, updatedPhone, updatedEmail, + updatedDepartment, updatedPosition, updatedAddress, updatedSalary, updatedBonus, updatedTags); } @Override @@ -130,6 +226,8 @@ public static class EditPersonDescriptor { private Name name; private Phone phone; private Email email; + private Department department; + private Position position; private Address address; private Set tags; @@ -143,6 +241,8 @@ public EditPersonDescriptor(EditPersonDescriptor toCopy) { setName(toCopy.name); setPhone(toCopy.phone); setEmail(toCopy.email); + setDepartment(toCopy.department); + setPosition(toCopy.position); setAddress(toCopy.address); setTags(toCopy.tags); } @@ -151,7 +251,7 @@ public EditPersonDescriptor(EditPersonDescriptor toCopy) { * Returns true if at least one field is edited. */ public boolean isAnyFieldEdited() { - return CollectionUtil.isAnyNonNull(name, phone, email, address, tags); + return CollectionUtil.isAnyNonNull(name, phone, email, department, position, address, tags); } public void setName(Name name) { @@ -178,6 +278,22 @@ public Optional getEmail() { return Optional.ofNullable(email); } + public void setDepartment(Department department) { + this.department = department; + } + + public Optional getDepartment() { + return Optional.ofNullable(department); + } + + public void setPosition(Position position) { + this.position = position; + } + + public Optional getPosition() { + return Optional.ofNullable(position); + } + public void setAddress(Address address) { this.address = address; } @@ -221,6 +337,8 @@ public boolean equals(Object other) { return getName().equals(e.getName()) && getPhone().equals(e.getPhone()) && getEmail().equals(e.getEmail()) + && getDepartment().equals(e.getDepartment()) + && getPosition().equals(e.getPosition()) && getAddress().equals(e.getAddress()) && getTags().equals(e.getTags()); } diff --git a/src/main/java/seedu/address/logic/commands/EditRecruitmentPostCommand.java b/src/main/java/seedu/address/logic/commands/EditRecruitmentPostCommand.java new file mode 100644 index 000000000000..2e4eb6edd179 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/EditRecruitmentPostCommand.java @@ -0,0 +1,225 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_JOB_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_JOB_POSITION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MINIMUM_EXPERIENCE; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_RECRUITMENT; + +import java.util.List; +import java.util.Optional; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.CollectionUtil; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.recruitment.JobDescription; +import seedu.address.model.recruitment.Post; +import seedu.address.model.recruitment.Recruitment; +import seedu.address.model.recruitment.WorkExp; + +/** + * Edits the details of an existing recruitment post in the address book. + */ +public class EditRecruitmentPostCommand extends Command { + + public static final String COMMAND_WORD = "editRecruitmentPost"; + public static final String COMMAND_ALIAS = "erp"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the details of the recruitment post identified " + + "by the index number used in the displayed recruitment list. " + + "Existing values will be overwritten by the input values.\n" + + "Parameters: INDEX (must be a positive integer) " + + "[" + PREFIX_JOB_POSITION + "Job Position] " + + "[" + PREFIX_MINIMUM_EXPERIENCE + "Minimal Working experience] " + + "[" + PREFIX_JOB_DESCRIPTION + "Job description]\n" + + "Example: " + COMMAND_WORD + " 1 " + + PREFIX_JOB_POSITION + "IT Manager " + + PREFIX_MINIMUM_EXPERIENCE + "3 " + + PREFIX_JOB_DESCRIPTION + "To maintain the company server "; + + public static final String MESSAGE_EDIT_RECRUITMENT_POST_SUCCESS = "Edited Recruitment Post: %1$s"; + public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided.\n" + + "Example: " + COMMAND_WORD + " 1 " + + PREFIX_JOB_POSITION + "IT Manager " + + PREFIX_MINIMUM_EXPERIENCE + "3 " + + PREFIX_JOB_DESCRIPTION + "To maintain the company server "; + + public static final String MESSAGE_DUPLICATE_POSITION = "This job position already exists in the address book"; + public static final String MESSAGE_DUPLICATE_WORK_EXP = "This working experience already exists in the " + + "address book"; + public static final String MESSAGE_DUPLICATE_JOB_DESCRIPTION = "This job description already exists in " + + "the address book"; + + private static boolean isPostDuplicated = false; + private static boolean isWorkExpDuplicated = false; + private static boolean isJobDescriptionDuplicated = false; + private final Index index; + private final EditPostDescriptor editPostDescriptor; + + /** + * @param index of the recruitment post in the filtered recruitment list to edit + * @param editPostDescriptor details to edit the post with + */ + public EditRecruitmentPostCommand(Index index, EditPostDescriptor editPostDescriptor) { + requireNonNull(index); + requireNonNull(editPostDescriptor); + + this.index = index; + this.editPostDescriptor = new EditPostDescriptor(editPostDescriptor); + } + + + public static void setIsPostDuplicated(boolean verifyPostDuplication) { + isPostDuplicated = verifyPostDuplication; + } + + public static void setIsWorkExpDuplicated(boolean verifyWorkExpDuplication) { + isWorkExpDuplicated = verifyWorkExpDuplication; + } + + public static void setIsJobDescriptionDuplicated(boolean verifyJobDescriptionDuplication) { + isJobDescriptionDuplicated = verifyJobDescriptionDuplication; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredRecruitmentList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_RECRUITMENT_POST_DISPLAYED_INDEX); + } + + Recruitment postToEdit = lastShownList.get(index.getZeroBased()); + Recruitment editedPost = createEditedPost(postToEdit, editPostDescriptor); + + if (model.hasRecruitment(editedPost) && isPostDuplicated + && !isWorkExpDuplicated && !isJobDescriptionDuplicated) { + throw new CommandException(MESSAGE_DUPLICATE_POSITION); + } else if (model.hasRecruitment(editedPost) && !isPostDuplicated + && isWorkExpDuplicated && !isJobDescriptionDuplicated) { + throw new CommandException(MESSAGE_DUPLICATE_WORK_EXP); + } else if (model.hasRecruitment(editedPost) && !isPostDuplicated + && !isWorkExpDuplicated && isJobDescriptionDuplicated) { + throw new CommandException(MESSAGE_DUPLICATE_JOB_DESCRIPTION); + } + + + model.updateRecruitment(postToEdit, editedPost); + model.updateFilteredRecruitmentList(PREDICATE_SHOW_ALL_RECRUITMENT); + model.commitRecruitmentPostList(); + return new CommandResult(String.format(MESSAGE_EDIT_RECRUITMENT_POST_SUCCESS, editedPost)); + } + + + /** + * Creates and returns a {@code recruitmentPost} with the details of {@code postToEdit} + * edited with {@code editPostDescriptor}. + */ + private static Recruitment createEditedPost(Recruitment postToEdit, EditPostDescriptor editPostDescriptor) { + assert postToEdit != null; + + Post updatedPost = editPostDescriptor.getPost().orElse(postToEdit.getPost()); + WorkExp updatedWorkExp = editPostDescriptor.getWorkExp().orElse(postToEdit.getWorkExp()); + JobDescription updatedJobDescription = editPostDescriptor.getJobDescription().orElse( + postToEdit.getJobDescription()); + + return new Recruitment(updatedPost, updatedWorkExp, updatedJobDescription); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditRecruitmentPostCommand)) { + return false; + } + + // state check + EditRecruitmentPostCommand e = (EditRecruitmentPostCommand) other; + return index.equals(e.index) + && editPostDescriptor.equals(e.editPostDescriptor); + } + + /** + * Stores the details to edit the recruitmentPost with. Each non-empty field value will replace the + * corresponding field value of the recruitmentPost. + */ + public static class EditPostDescriptor { + private Post post; + private WorkExp workExp; + private JobDescription jobDescription; + + /** + * Copy constructor. + * A defensive copy of {@code tags} is used internally. + */ + public EditPostDescriptor(EditPostDescriptor toCopy) { + setPost(toCopy.post); + setWorkExp(toCopy.workExp); + setJobDescription(toCopy.jobDescription); + } + + public EditPostDescriptor() { + + } + + /** + * Returns true if at least one field is edited. + */ + public boolean isAnyFieldEdited() { + return CollectionUtil.isAnyNonNull(post, workExp, jobDescription); + } + + public void setPost(Post post) { + this.post = post; + } + + public Optional getPost() { + return Optional.ofNullable(post); + } + + public void setWorkExp(WorkExp workExp) { + this.workExp = workExp; + } + + public Optional getWorkExp() { + return Optional.ofNullable(workExp); + } + + public void setJobDescription(JobDescription jobDescription) { + this.jobDescription = jobDescription; + } + + public Optional getJobDescription() { + return Optional.ofNullable(jobDescription); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EditPostDescriptor)) { + return false; + } + + // state check + EditPostDescriptor e = (EditPostDescriptor) other; + + return getPost().equals(e.getPost()) + && getWorkExp().equals(e.getWorkExp()) + && getJobDescription().equals(e.getJobDescription()); + } + } +} diff --git a/src/main/java/seedu/address/logic/commands/FilterCommand.java b/src/main/java/seedu/address/logic/commands/FilterCommand.java new file mode 100644 index 000000000000..8ddc058a3bcb --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/FilterCommand.java @@ -0,0 +1,210 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DEPARTMENT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_POSITION; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import seedu.address.commons.core.Messages; +import seedu.address.logic.CommandHistory; +import seedu.address.model.Model; +import seedu.address.model.expenses.EmployeeIdExpensesContainsKeywordsPredicate; +import seedu.address.model.person.DepartmentContainsKeywordsPredicate; +import seedu.address.model.person.Person; +import seedu.address.model.person.PositionContainsKeywordsPredicate; +import seedu.address.model.schedule.EmployeeIdScheduleContainsKeywordsPredicate; + +/** + * Filters and lists all persons in address book whose department and/or position contains any of the argument keywords. + * Keyword matching is case insensitive. + * The list is sorted either in ascending or descending name order based on the user's input + */ +public class FilterCommand extends Command { + + public static final String COMMAND_WORD = "filter"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Filters all persons whose department or position " + + "contain any of the specified keywords (case-insensitive) and displays them as a sorted list in either " + + "ascending or descending order with index numbers.\nParameters: ORDER " + PREFIX_DEPARTMENT + + "DEPARTMENT AND/OR " + PREFIX_POSITION + "POSITION\n" + + "Example: " + COMMAND_WORD + " dsc " + PREFIX_DEPARTMENT + "Human Resource" + " " + + PREFIX_POSITION + "Intern"; + + public static final String ASCENDING = "asc"; + public static final String DESCENDING = "dsc"; + + private final String sortOrder; + private DepartmentContainsKeywordsPredicate departmentPredicate; + private PositionContainsKeywordsPredicate positionPredicate; + + private boolean isDepartmentPrefixPresent; + private boolean isPositionPrefixPresent; + + /** + * Creates a FilterCommand to filter out the employees based on the specified {@code departmentPredicate} or + * {@code positionPredicate} or both and list them out in ascending or descending name order based on + * {@code sortOrder}. + * @param departmentPredicate The predicate that holds the matched department(s) + * @param positionPredicate The predicate that holds the matched position(s) + * @param sortOrder The sortOrder indicated by the user (either ascending or descending) + */ + public FilterCommand(DepartmentContainsKeywordsPredicate departmentPredicate, + PositionContainsKeywordsPredicate positionPredicate, String sortOrder) { + this.departmentPredicate = departmentPredicate; + this.positionPredicate = positionPredicate; + this.sortOrder = sortOrder; + } + + public void setIsDepartmentPrefixPresent(boolean isDepartmentPrefixPresent) { + this.isDepartmentPrefixPresent = isDepartmentPrefixPresent; + } + + public void setIsPositionPrefixPresent(boolean isPositionPrefixPresent) { + this.isPositionPrefixPresent = isPositionPrefixPresent; + } + + public void setDepartmentPredicate(DepartmentContainsKeywordsPredicate departmentPredicate) { + this.departmentPredicate = departmentPredicate; + } + + public void setPositionPredicate(PositionContainsKeywordsPredicate positionPredicate) { + this.positionPredicate = positionPredicate; + } + + /** + * Execution of the command will be carried out after the checks for the presence of department and position + * prefixes are completed. The command will filter the person, schedule and expenses list to only show employees of + * the input department(s) and/or position(s). + * @param model The actual model + * @param history The actual history + */ + @Override + public CommandResult execute(Model model, CommandHistory history) { + requireNonNull(model); + String allAvailableDepartments = listAvailableDepartments(model); + String allAvailablePositions = listAvailablePositions(model); + + if (isDepartmentPrefixPresent && !isPositionPrefixPresent) { + model.updateFilteredPersonList(departmentPredicate, sortOrder); + } else if (isPositionPrefixPresent && !isDepartmentPrefixPresent) { + model.updateFilteredPersonList(positionPredicate, sortOrder); + } else if (isDepartmentPrefixPresent && isPositionPrefixPresent) { + model.updateFilteredPersonList(departmentPredicate.and(positionPredicate), sortOrder); + } + + EmployeeIdExpensesContainsKeywordsPredicate expensesPredicate = generateEmployeeIdExpensesPredicate(model); + EmployeeIdScheduleContainsKeywordsPredicate schedulePredicate = generateEmployeeIdSchedulePredicate(model); + model.updateFilteredExpensesList(expensesPredicate); + model.updateFilteredScheduleList(schedulePredicate); + + return new CommandResult(feedbackToUser(model, allAvailableDepartments, allAvailablePositions)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof FilterCommand // instanceof handles nulls + && (departmentPredicate.and(positionPredicate).equals(((FilterCommand) other) + .departmentPredicate.and(positionPredicate)) + || (departmentPredicate.equals(((FilterCommand) other).departmentPredicate)) + || (positionPredicate.equals(((FilterCommand) other).positionPredicate)))); // state check + } + + /** + * Creates and returns a {@code String} that holds all the currently available departments to filter by + * in the Address Book. + * @param model The actual model + */ + public String listAvailableDepartments(Model model) { + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + + List getFullList = model.getFilteredPersonList(); + Set allDepartments = new HashSet<>(); + + for (Person department : getFullList) { + allDepartments.add(department.getDepartment().toString().toUpperCase()); + } + + String availableDepartments = String.join(", ", allDepartments); + + return "\nAvailable Departments: " + availableDepartments; + } + + /** + * Creates and returns a {@code String} that holds all the currently available positions to filter by + * in the Address Book. + * @param model The actual model + */ + public String listAvailablePositions(Model model) { + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + + List getFullList = model.getFilteredPersonList(); + Set allPositions = new HashSet<>(); + + for (Person position : getFullList) { + allPositions.add(position.getPosition().toString().toUpperCase()); + } + + String availablePositions = String.join(", ", allPositions); + + return "\nAvailable Positions: " + availablePositions; + } + + /** + * Creates and returns a {@code EmployeeIdExpensesContainsKeywordsPredicate} that contains all the employee ID(s) + * that matches the matched persons' employee ID. + * @param model The actual model + */ + public EmployeeIdExpensesContainsKeywordsPredicate generateEmployeeIdExpensesPredicate(Model model) { + List getFilteredList = model.getFilteredPersonList(); + List matchedEmployeeIds = new ArrayList<>(); + + for (Person person : getFilteredList) { + matchedEmployeeIds.add(person.getEmployeeId().value); + } + + return new EmployeeIdExpensesContainsKeywordsPredicate(matchedEmployeeIds); + } + + /** + * Creates and returns a {@code EmployeeIdScheduleContainsKeywordsPredicate} that contains all the employee ID(s) + * that matches the matched persons' employee ID. + * @param model The actual model + */ + public EmployeeIdScheduleContainsKeywordsPredicate generateEmployeeIdSchedulePredicate(Model model) { + List getFilteredList = model.getFilteredPersonList(); + List matchedEmployeeIds = new ArrayList<>(); + + for (Person person : getFilteredList) { + matchedEmployeeIds.add(person.getEmployeeId().value); + } + + return new EmployeeIdScheduleContainsKeywordsPredicate(matchedEmployeeIds); + } + + /** + * Creates and return a {@code String} that contains the feedback to be printed to the user on the CLI. + * @param model The actual model + * @param allAvailableDepartments The available department(s) in the address book to filter by + * @param allAvailablePositions The available position(s) in the address book to filter by + */ + public String feedbackToUser(Model model, String allAvailableDepartments, String allAvailablePositions) { + String toBeConcatenated = ""; + + if (model.getFilteredPersonList().isEmpty() && isDepartmentPrefixPresent && !isPositionPrefixPresent) { + toBeConcatenated = allAvailableDepartments; + } else if (model.getFilteredPersonList().isEmpty() && !isDepartmentPrefixPresent && isPositionPrefixPresent) { + toBeConcatenated = allAvailablePositions; + } else if (model.getFilteredPersonList().isEmpty() && isPositionPrefixPresent && isPositionPrefixPresent) { + toBeConcatenated = allAvailableDepartments + allAvailablePositions; + } + + return String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size()) + + toBeConcatenated; + } +} diff --git a/src/main/java/seedu/address/logic/commands/FindCommand.java b/src/main/java/seedu/address/logic/commands/FindCommand.java index beb178e3a3f5..a4380d1b41da 100644 --- a/src/main/java/seedu/address/logic/commands/FindCommand.java +++ b/src/main/java/seedu/address/logic/commands/FindCommand.java @@ -1,35 +1,86 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +import java.util.ArrayList; +import java.util.List; import seedu.address.commons.core.Messages; import seedu.address.logic.CommandHistory; import seedu.address.model.Model; +import seedu.address.model.expenses.EmployeeIdExpensesContainsKeywordsPredicate; +import seedu.address.model.person.EmployeeId; +import seedu.address.model.person.EmployeeIdContainsKeywordsPredicate; +import seedu.address.model.person.Name; import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.address.model.person.Person; +import seedu.address.model.schedule.EmployeeIdScheduleContainsKeywordsPredicate; /** - * Finds and lists all persons in address book whose name contains any of the argument keywords. + * Finds and lists all persons in the address book whose name contains the keyword or employee id is the keyword. * Keyword matching is case insensitive. */ public class FindCommand extends Command { public static final String COMMAND_WORD = "find"; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose names contain any of " - + "the specified keywords (case-insensitive) and displays them as a list with index numbers.\n" - + "Parameters: KEYWORD [MORE_KEYWORDS]...\n" - + "Example: " + COMMAND_WORD + " alice bob charlie"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all person(s) whose name(s) contains the input " + + "(case-insensitive) or find the person whose employee ID matches the input and displays them as a list " + + "with index number.\nParameters: NAME OR EMPLOYEEID\n" + + "Example: " + COMMAND_WORD + " Alex yeoh\n" + "Example: " + COMMAND_WORD + " 000001"; + + public static final String MESSAGE_VALID_INPUT = "Please enter either a valid Name or valid Employee ID\n" + + "Valid Name: " + Name.MESSAGE_NAME_CONSTRAINTS + "\n" + + "Valid Employee Id: " + EmployeeId.MESSAGE_EMPLOYEEID_CONSTRAINTS; + + private final String keyword; + private EmployeeIdContainsKeywordsPredicate employeeIdPredicate; + private NameContainsKeywordsPredicate namePredicate; + private boolean isInputName; + private boolean isInputEmployeeId; - private final NameContainsKeywordsPredicate predicate; + /** + * Creates a FindCommand to find the specific employee of the specified {@code keyword} + * @param keyword The keyword (either employee id or name) that the user wishes to search for + */ + public FindCommand(String keyword) { + this.keyword = keyword; + } + + public void setIsInputName(boolean isInputName) { + this.isInputName = isInputName; + } - public FindCommand(NameContainsKeywordsPredicate predicate) { - this.predicate = predicate; + public void setIsInputEmployeeId(boolean isInputEmployeeId) { + this.isInputEmployeeId = isInputEmployeeId; } + + /** + * Execution of the command will be carried out after the checks for whether the keyword is a name or an employee id + * is completed. The command will filter person, schedule and expenses list to only show the data of the matched + * name or employee id. + * @param model The actual model + * @param history Tbe actual history + */ @Override public CommandResult execute(Model model, CommandHistory history) { requireNonNull(model); - model.updateFilteredPersonList(predicate); + + if (isInputName && !isInputEmployeeId) { + namePredicate = generateNamesPredicate(model, keyword); + model.updateFilteredPersonList(namePredicate); + } else if (!isInputName && isInputEmployeeId) { + employeeIdPredicate = new EmployeeIdContainsKeywordsPredicate(keyword); + model.updateFilteredPersonList(employeeIdPredicate); + } + + EmployeeIdExpensesContainsKeywordsPredicate expensesPredicate = generateEmployeeIdExpensesPredicate(model); + EmployeeIdScheduleContainsKeywordsPredicate schedulePredicate = generateEmployeeIdSchedulePredicate(model); + model.updateFilteredExpensesList(expensesPredicate); + model.updateFilteredScheduleList(schedulePredicate); + return new CommandResult( String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size())); } @@ -38,6 +89,60 @@ public CommandResult execute(Model model, CommandHistory history) { public boolean equals(Object other) { return other == this // short circuit if same object || (other instanceof FindCommand // instanceof handles nulls - && predicate.equals(((FindCommand) other).predicate)); // state check + && keyword.equals(((FindCommand) other).keyword)); // state check + } + + /** + * Creates and returns a {@code EmployeeIdExpensesContainsKeywordsPredicate} that contains all the employee ID(s) + * that matches the matched person's employee ID. + * @param model The actual model + */ + public EmployeeIdExpensesContainsKeywordsPredicate generateEmployeeIdExpensesPredicate(Model model) { + List getFilteredList = model.getFilteredPersonList(); + List matchedEmployeeIds = new ArrayList<>(); + + for (Person person : getFilteredList) { + matchedEmployeeIds.add(person.getEmployeeId().value); + } + + return new EmployeeIdExpensesContainsKeywordsPredicate(matchedEmployeeIds); + } + + + /** + * Creates and returns a {@code EmployeeIdScheduleContainsKeywordsPredicate} that contains all the employee ID(s) + * that matches the matched person's employee ID. + * @param model The actual model + */ + public EmployeeIdScheduleContainsKeywordsPredicate generateEmployeeIdSchedulePredicate(Model model) { + List getFilteredList = model.getFilteredPersonList(); + List matchedEmployeeIds = new ArrayList<>(); + + for (Person person : getFilteredList) { + matchedEmployeeIds.add(person.getEmployeeId().value); + } + + return new EmployeeIdScheduleContainsKeywordsPredicate(matchedEmployeeIds); + } + + /** + * Creates and returns a {@code NameContainsKeywordsPredicate} that holds all the name(s) that contains + * the input keyword. + * @param model The actual model + * @param keyword The user's input + */ + public NameContainsKeywordsPredicate generateNamesPredicate (Model model, String keyword) { + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + + List getFullList = model.getFilteredPersonList(); + List matchingNames = new ArrayList<>(); + + for (Person name : getFullList) { + if (name.getName().fullName.toLowerCase().contains(keyword.toLowerCase())) { + matchingNames.add(name.getName().fullName); + } + } + + return new NameContainsKeywordsPredicate(matchingNames); } } diff --git a/src/main/java/seedu/address/logic/commands/HistoryCommand.java b/src/main/java/seedu/address/logic/commands/HistoryCommand.java index f1541fb57f20..161c49b0a6ce 100644 --- a/src/main/java/seedu/address/logic/commands/HistoryCommand.java +++ b/src/main/java/seedu/address/logic/commands/HistoryCommand.java @@ -16,6 +16,7 @@ public class HistoryCommand extends Command { public static final String COMMAND_WORD = "history"; public static final String MESSAGE_SUCCESS = "Entered commands (from most recent to earliest):\n%1$s"; public static final String MESSAGE_NO_HISTORY = "You have not yet entered any commands."; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": List of commands (from most recent to earliest)"; @Override public CommandResult execute(Model model, CommandHistory history) { diff --git a/src/main/java/seedu/address/logic/commands/ListCommand.java b/src/main/java/seedu/address/logic/commands/ListCommand.java index 6d44824c7d1b..850b275aab43 100644 --- a/src/main/java/seedu/address/logic/commands/ListCommand.java +++ b/src/main/java/seedu/address/logic/commands/ListCommand.java @@ -1,7 +1,9 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_EXPENSES; import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_SCHEDULES; import seedu.address.logic.CommandHistory; import seedu.address.model.Model; @@ -13,13 +15,19 @@ public class ListCommand extends Command { public static final String COMMAND_WORD = "list"; - public static final String MESSAGE_SUCCESS = "Listed all persons"; + public static final String MESSAGE_SUCCESS = "Listed all persons, schedules and expenses."; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Lists all employees, schedules, recruitment posts" + + " and expenses."; + + private static final String BY_ASCENDING = "asc"; @Override public CommandResult execute(Model model, CommandHistory history) { requireNonNull(model); + model.updateFilteredExpensesList(PREDICATE_SHOW_ALL_EXPENSES); model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + model.updateFilteredScheduleList(PREDICATE_SHOW_ALL_SCHEDULES); return new CommandResult(MESSAGE_SUCCESS); } } diff --git a/src/main/java/seedu/address/logic/commands/ModifyAllPayCommand.java b/src/main/java/seedu/address/logic/commands/ModifyAllPayCommand.java new file mode 100644 index 000000000000..3724618819f9 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ModifyAllPayCommand.java @@ -0,0 +1,281 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_MODIFIED_PAY_OVERVIEW; +import static seedu.address.logic.parser.CliSyntax.PREFIX_BONUS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SALARY; + +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import seedu.address.commons.util.CollectionUtil; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.Model; +import seedu.address.model.person.Address; +import seedu.address.model.person.Bonus; +import seedu.address.model.person.DateOfBirth; +import seedu.address.model.person.Department; +import seedu.address.model.person.Email; +import seedu.address.model.person.EmployeeId; +import seedu.address.model.person.Name; +import seedu.address.model.person.Person; +import seedu.address.model.person.Phone; +import seedu.address.model.person.Position; +import seedu.address.model.person.Salary; +import seedu.address.model.person.tag.Tag; + +/** + * Modify the salary and bonus of an employee's in CHRS + */ +public class ModifyAllPayCommand extends Command { + + public static final String COMMAND_WORD = "modifyAllPay"; + public static final String COMMAND_ALIAS = "map"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Modify the pay of all the employee(s) " + + "shown in displayed list. " + + "Existing salary will be updated based on the user input.\n" + + "Parameters: " + + PREFIX_SALARY + "[INCREASE AMOUNT]" + + " OR " + + PREFIX_SALARY + "%[PERCENTAGE INCREASE] AND/OR " + + PREFIX_BONUS + "[MONTH(S) OF SALARY FOR BONUS]\n" + + "Example 1: " + COMMAND_WORD + " " + + PREFIX_SALARY + "300 " + + PREFIX_BONUS + "2\n" + + "Example 2: " + COMMAND_WORD + " " + + PREFIX_SALARY + "%10 " + + PREFIX_BONUS + "2"; + + public static final String MESSAGE_NEGATIVE_PAY = "Pay are not allowed to be zero or negative in value"; + public static final String MESSAGE_NOT_MODIFIED = "Employee's pay not modified yet. " + + "Min of 1 field " + + PREFIX_SALARY + + " AND/OR " + + PREFIX_BONUS + + " must be provided"; + private static final double LIMIT = 0.0; + private static final double PERCENT = 100.0; + private static final String OUTPUT_FORMAT = "#0.00"; + private final ModSalaryDescriptor modSalaryDescriptor; + + /** + * + * @param modSalaryDescriptor details to modify the person with. + */ + + public ModifyAllPayCommand(ModSalaryDescriptor modSalaryDescriptor) { + requireNonNull(modSalaryDescriptor); + + this.modSalaryDescriptor = new ModSalaryDescriptor(modSalaryDescriptor); + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException, ParseException { + requireNonNull(model); + List lastShownList = model.getFilteredPersonList(); + List modifiedList = new ArrayList<>(); + List newList = new ArrayList<>(); + + for (Person person : lastShownList) { + Person modifiedPerson = createModifiedPerson(person, modSalaryDescriptor); + + newList.add(person); + modifiedList.add(modifiedPerson); + } + + for (int i = 0; i < newList.size(); i++) { + model.updatePerson(newList.get(i), modifiedList.get(i)); + } + + model.commitAddressBook(); + return new CommandResult(String.format(MESSAGE_MODIFIED_PAY_OVERVIEW, lastShownList.size())); + } + + /** + * Creates and returns a boolean with the details of {@code salary} + */ + private static boolean isNegative (double salary) { + return salary <= LIMIT; + } + /** + * Creates and returns a new String of Salary with the details of {@code personToEdit} + * edited with {@code modSalaryDescriptor}. + */ + private static double addSalaryAmount (Person personToEdit, ModSalaryDescriptor modSalaryDescriptor) { + String newSalary = personToEdit.getSalary().toString(); + double payOut = Double.parseDouble(newSalary); + String change = modSalaryDescriptor.getSalary().toString().replaceAll("[^0-9.-]", ""); + payOut += Double.parseDouble(change); + + return payOut; + } + + /** + * Creates and returns a new String of Salary with the details of {@code personToEdit} + * edited with {@code modSalaryDescriptor}. + */ + private static double modifySalaryPercent (Person personToEdit, ModSalaryDescriptor modSalaryDescriptor) { + String newSalary = personToEdit.getSalary().toString(); + double payOut = Double.parseDouble(newSalary); + String change = modSalaryDescriptor.getSalary().toString().replaceAll("[^0-9.-]", ""); + payOut += Math.abs(payOut) * (Double.parseDouble(change) / PERCENT); + + return payOut; + } + + /** + * Creates and returns a new String of Salary with the functions modifySalaryPercent and addSalaryAmount + * details of {@code personToEdit} + * edited with {@code modSalaryDescriptor}. + */ + private static String typeOfSalaryMod (Person personToEdit, ModSalaryDescriptor modSalaryDescriptor) + throws CommandException { + String newSalary = personToEdit.getSalary().toString(); + NumberFormat formatter = new DecimalFormat(OUTPUT_FORMAT); + double payOut = Double.parseDouble(newSalary); + + if (!modSalaryDescriptor.getSalary().equals(Optional.empty())) { + String change = modSalaryDescriptor.getSalary().toString(); + char type = change.charAt(9); + + if (type == '%') { + payOut = modifySalaryPercent(personToEdit, modSalaryDescriptor); + } else { + payOut = addSalaryAmount(personToEdit, modSalaryDescriptor); + } + } + + if (isNegative(payOut)) { + throw new CommandException(MESSAGE_NEGATIVE_PAY); + } + + newSalary = String.valueOf(formatter.format(payOut)); + + return newSalary; + } + + /** + * Creates and returns a new String value of Bonus with the details of {@code personToEdit} + * edited with {@code modSalaryDescriptor}. + */ + private static String modifyBonusMonth (Person personToEdit, ModSalaryDescriptor modSalaryDescriptor, + Salary newSalary) { + NumberFormat formatter = new DecimalFormat("#0.00"); + String bonus = personToEdit.getBonus().toString(); + double currentSalary = Double.parseDouble(newSalary.toString()); + if (!modSalaryDescriptor.getBonus().equals(Optional.empty())) { + String bonusMonth = modSalaryDescriptor.getBonus().toString().replaceAll("[^0-9.]", ""); + double payOut = currentSalary * Double.parseDouble(bonusMonth); + bonus = String.valueOf(formatter.format(payOut)); + } + return bonus; + } + + /** + * Creates and returns a {@code Person} with the details of {@code personToEdit} + * edited with {@code modSalaryDescriptor}. + */ + private static Person createModifiedPerson(Person personToEdit, + ModSalaryDescriptor modSalaryDescriptor) throws ParseException, CommandException { + assert personToEdit != null; + + EmployeeId updatedEmployeeId = personToEdit.getEmployeeId(); + Name updatedName = personToEdit.getName(); + DateOfBirth updatedDateOfBirth = personToEdit.getDateOfBirth(); + Phone updatedPhone = personToEdit.getPhone(); + Email updatedEmail = personToEdit.getEmail(); + Department updatedDepartment = personToEdit.getDepartment(); + Position updatedPosition = personToEdit.getPosition(); + Address updatedAddress = personToEdit.getAddress(); + Salary updatedSalary = ParserUtil.parseSalary(typeOfSalaryMod(personToEdit, modSalaryDescriptor)); + Bonus updatedBonus = ParserUtil.parseBonus(modifyBonusMonth(personToEdit, modSalaryDescriptor, updatedSalary)); + Set updatedTags = personToEdit.getTags(); + + return new Person(updatedEmployeeId, updatedName, updatedDateOfBirth, updatedPhone, updatedEmail, + updatedDepartment, updatedPosition, updatedAddress, updatedSalary, updatedBonus, updatedTags); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ModifyAllPayCommand)) { + return false; + } + + // state check + ModifyAllPayCommand m = (ModifyAllPayCommand) other; + return modSalaryDescriptor.equals(m.modSalaryDescriptor); + } + + /** + * Stores the details to modify the person with. Each non-empty field value will replace the + * corresponding field value of the person. + */ + public static class ModSalaryDescriptor { + private Salary salary; + private Bonus bonus; + + public ModSalaryDescriptor() {} + /** + * Copy constructor. + * A defensive copy of {@code tags} is used internally. + */ + + public ModSalaryDescriptor(ModSalaryDescriptor toCopy) { + setSalary(toCopy.salary); + setBonus(toCopy.bonus); + } + + public boolean isAnyFieldEdited() { + return CollectionUtil.isAnyNonNull(salary, bonus); + } + + public void setSalary(Salary salary) { + this.salary = salary; + } + + public Optional getSalary() { + return Optional.ofNullable(salary); + } + + public void setBonus(Bonus bonus) { + this.bonus = bonus; + } + + public Optional getBonus() { + return Optional.ofNullable(bonus); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ModSalaryDescriptor)) { + return false; + } + + // state check + ModSalaryDescriptor m = (ModSalaryDescriptor) other; + + return getSalary().equals(m.getSalary()) + && getBonus().equals(m.getBonus()); + } + } +} diff --git a/src/main/java/seedu/address/logic/commands/ModifyPayCommand.java b/src/main/java/seedu/address/logic/commands/ModifyPayCommand.java new file mode 100644 index 000000000000..7d45ecb21bc2 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ModifyPayCommand.java @@ -0,0 +1,284 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import static seedu.address.logic.parser.CliSyntax.PREFIX_BONUS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SALARY; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.List; +import java.util.Optional; +import java.util.Set; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.CollectionUtil; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.parser.ParserUtil; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.Model; +import seedu.address.model.person.Address; +import seedu.address.model.person.Bonus; +import seedu.address.model.person.DateOfBirth; +import seedu.address.model.person.Department; +import seedu.address.model.person.Email; +import seedu.address.model.person.EmployeeId; +import seedu.address.model.person.Name; +import seedu.address.model.person.Person; +import seedu.address.model.person.Phone; +import seedu.address.model.person.Position; +import seedu.address.model.person.Salary; +import seedu.address.model.person.tag.Tag; + + +/** + * Modify the salary and bonus of an employee's in CHRS + */ +public class ModifyPayCommand extends Command { + + public static final String COMMAND_WORD = "modifyPay"; + public static final String COMMAND_ALIAS = "mp"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Modify the pay of the employee " + + "identified by the index used in the displayed list. " + + "Existing salary will be updated based on the user input.\n" + + "Parameters: index " + + PREFIX_SALARY + "[INCREASE AMOUNT]" + + " OR " + + PREFIX_SALARY + "%[PERCENTAGE INCREASE] AND/OR " + + PREFIX_BONUS + "[MONTH(S) OF SALARY FOR BONUS]\n" + + "Example 1: " + COMMAND_WORD + " 1 " + + PREFIX_SALARY + "300 " + + PREFIX_BONUS + "2\n" + + "Example 2: " + COMMAND_WORD + " 1 " + + PREFIX_SALARY + "%10" + + PREFIX_BONUS + "2"; + + public static final String MESSAGE_MODIFIED_SUCCESS = "Employee's pay modified."; + public static final String MESSAGE_NEGATIVE_PAY = "Pay are not allowed to be zero or negative in value"; + public static final String MESSAGE_NOT_MODIFIED = "Employee's pay not modified yet. " + + "[INDEX] and min of 1 field " + + PREFIX_SALARY + + " AND/OR " + + PREFIX_BONUS + + " must be provided"; + private static final double LIMIT = 0.0; + private static final double PERCENT = 100.0; + private static final String OUTPUT_FORMAT = "#0.00"; + private final Index index; + private final ModSalaryDescriptor modSalaryDescriptor; + + /** + * + * @param index of the person in employee list to modify. + * @param modSalaryDescriptor details to modify the person with. + */ + + public ModifyPayCommand(Index index, ModSalaryDescriptor modSalaryDescriptor) { + requireNonNull(index); + requireNonNull(modSalaryDescriptor); + + this.index = index; + this.modSalaryDescriptor = new ModSalaryDescriptor(modSalaryDescriptor); + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException, ParseException { + requireNonNull(model); + List lastShownList = model.getFilteredPersonList(); + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Person salaryToModify = lastShownList.get(index.getZeroBased()); + Person modifiedPerson = createModifiedPerson(salaryToModify, modSalaryDescriptor); + + model.updatePerson(salaryToModify, modifiedPerson); + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + model.commitAddressBook(); + return new CommandResult(String.format(MESSAGE_MODIFIED_SUCCESS, modifiedPerson)); + } + + /** + * Creates and returns a boolean with the details of {@code salary} + */ + private static boolean isNegative (double salary) { + return salary <= LIMIT; + } + /** + * Creates and returns a new String of Salary with the details of {@code personToEdit} + * edited with {@code modSalaryDescriptor}. + */ + private static double addSalaryAmount (Person personToEdit, ModSalaryDescriptor modSalaryDescriptor) { + String newSalary = personToEdit.getSalary().toString(); + double payOut = Double.parseDouble(newSalary); + String change = modSalaryDescriptor.getSalary().toString().replaceAll("[^0-9.-]", ""); + payOut += Double.parseDouble(change); + + return payOut; + } + + /** + * Creates and returns a new String of Salary with the details of {@code personToEdit} + * edited with {@code modSalaryDescriptor}. + */ + private static double modifySalaryPercent (Person personToEdit, ModSalaryDescriptor modSalaryDescriptor) { + String newSalary = personToEdit.getSalary().toString(); + double payOut = Double.parseDouble(newSalary); + String change = modSalaryDescriptor.getSalary().toString().replaceAll("[^0-9.-]", ""); + payOut += Math.abs(payOut) * (Double.parseDouble(change) / PERCENT); + + return payOut; + } + + /** + * Creates and returns a new String of Salary with the functions modifySalaryPercent and addSalaryAmount + * details of {@code personToEdit} + * edited with {@code modSalaryDescriptor}. + */ + private static String typeOfSalaryMod (Person personToEdit, ModSalaryDescriptor modSalaryDescriptor) + throws CommandException { + String newSalary = personToEdit.getSalary().toString(); + NumberFormat formatter = new DecimalFormat(OUTPUT_FORMAT); + double payOut = Double.parseDouble(newSalary); + + if (!modSalaryDescriptor.getSalary().equals(Optional.empty())) { + String change = modSalaryDescriptor.getSalary().toString(); + char type = change.charAt(9); + + if (type == '%') { + payOut = modifySalaryPercent(personToEdit, modSalaryDescriptor); + } else { + payOut = addSalaryAmount(personToEdit, modSalaryDescriptor); + } + } + if (isNegative(payOut)) { + throw new CommandException(MESSAGE_NEGATIVE_PAY); + } + + newSalary = String.valueOf(formatter.format(payOut)); + + return newSalary; + } + + /** + * Creates and returns a new String value of Bonus with the details of {@code personToEdit} + * edited with {@code modSalaryDescriptor}. + */ + private static String modifyBonusMonth (Person personToEdit, ModSalaryDescriptor modSalaryDescriptor, + Salary newSalary) { + NumberFormat formatter = new DecimalFormat("#0.00"); + String bonus = personToEdit.getBonus().toString(); + double currentSalary = Double.parseDouble(newSalary.toString()); + if (!modSalaryDescriptor.getBonus().equals(Optional.empty())) { + String bonusMonth = modSalaryDescriptor.getBonus().toString().replaceAll("[^0-9.]", ""); + double payOut = currentSalary * Double.parseDouble(bonusMonth); + bonus = String.valueOf(formatter.format(payOut)); + } + return bonus; + } + + /** + * Creates and returns a {@code Person} with the details of {@code personToEdit} + * edited with {@code modSalaryDescriptor}. + */ + private static Person createModifiedPerson(Person personToEdit, + ModSalaryDescriptor modSalaryDescriptor) throws ParseException, CommandException { + assert personToEdit != null; + + EmployeeId updatedEmployeeId = personToEdit.getEmployeeId(); + Name updatedName = personToEdit.getName(); + DateOfBirth updatedDateOfBirth = personToEdit.getDateOfBirth(); + Phone updatedPhone = personToEdit.getPhone(); + Email updatedEmail = personToEdit.getEmail(); + Department updatedDepartment = personToEdit.getDepartment(); + Position updatedPosition = personToEdit.getPosition(); + Address updatedAddress = personToEdit.getAddress(); + Salary updatedSalary = ParserUtil.parseSalary(typeOfSalaryMod(personToEdit, modSalaryDescriptor)); + Bonus updatedBonus = ParserUtil.parseBonus(modifyBonusMonth(personToEdit, modSalaryDescriptor, updatedSalary)); + + Set updatedTags = personToEdit.getTags(); + + return new Person(updatedEmployeeId, updatedName, updatedDateOfBirth, updatedPhone, updatedEmail, + updatedDepartment, updatedPosition, updatedAddress, updatedSalary, updatedBonus, updatedTags); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ModifyPayCommand)) { + return false; + } + + // state check + ModifyPayCommand m = (ModifyPayCommand) other; + return index.equals(m.index) + && modSalaryDescriptor.equals(m.modSalaryDescriptor); + } + + /** + * Stores the details to modify the person with. Each non-empty field value will replace the + * corresponding field value of the person. + */ + public static class ModSalaryDescriptor { + private Salary salary; + private Bonus bonus; + + public ModSalaryDescriptor() {} + /** + * Copy constructor. + * A defensive copy of {@code tags} is used internally. + */ + + public ModSalaryDescriptor(ModSalaryDescriptor toCopy) { + setSalary(toCopy.salary); + setBonus(toCopy.bonus); + } + + public boolean isAnyFieldEdited() { + return CollectionUtil.isAnyNonNull(salary, bonus); + } + + public void setSalary(Salary salary) { + this.salary = salary; + } + + public Optional getSalary() { + return Optional.ofNullable(salary); + } + + public void setBonus(Bonus bonus) { + this.bonus = bonus; } + + public Optional getBonus() { + return Optional.ofNullable(bonus); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ModSalaryDescriptor)) { + return false; + } + + // state check + ModSalaryDescriptor m = (ModSalaryDescriptor) other; + + return getSalary().equals(m.getSalary()) + && getBonus().equals(m.getBonus()); + } + } +} diff --git a/src/main/java/seedu/address/logic/commands/RedoCommand.java b/src/main/java/seedu/address/logic/commands/RedoCommand.java index 227771a4eef6..415e319c65ef 100644 --- a/src/main/java/seedu/address/logic/commands/RedoCommand.java +++ b/src/main/java/seedu/address/logic/commands/RedoCommand.java @@ -1,31 +1,89 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_EXPENSES; import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_RECRUITMENT; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_SCHEDULES; + +import java.util.Set; import seedu.address.logic.CommandHistory; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; +import seedu.address.model.ModelTypes; /** - * Reverts the {@code model}'s address book to its previously undone state. + * Reverts the {@code model}'s address book, schedule list, expenses list, recruitment list + * to its previously undone state. */ public class RedoCommand extends Command { - public static final String COMMAND_WORD = "redo"; public static final String MESSAGE_SUCCESS = "Redo success!"; public static final String MESSAGE_FAILURE = "No more commands to redo!"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Redo the previous command."; + /** + * RedoCommand execution. + * @see seedu.address.model.VersionedModelList class for the tracking of storage commits across + * all storage types (addressbook, expensesList, scheduleList, recruitmentList) + *

+ * Get the last commit type from {@code VersionedModelList} class, which is a set + * containing which storage has been committed. Hence, the same storage + * will be allowed to perform redo. + *

+ * @param model {@code Model} which the command will operate on the model. + * @param history {@code CommandHistory} which the command history will be added. + * @return CommandResult, String success feedback to the user. + * @throws CommandException String failure feedback to the user if error in execution. + */ @Override public CommandResult execute(Model model, CommandHistory history) throws CommandException { requireNonNull(model); - if (!model.canRedoAddressBook()) { + if (!model.canRedoModel()) { throw new CommandException(MESSAGE_FAILURE); } + Set myModelRedoSet = model.getNextCommitType(); + + for (ModelTypes myModel : myModelRedoSet) { + + switch (myModel) { + case ADDRESS_BOOK: + if (model.canRedoAddressBook()) { + model.redoAddressBook(); + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + } + break; + + case EXPENSES_LIST: + if (model.canRedoExpensesList()) { + model.redoExpensesList(); + model.updateFilteredExpensesList(PREDICATE_SHOW_ALL_EXPENSES); + } - model.redoAddressBook(); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + break; + + case RECRUITMENT_LIST: + if (model.canRedoRecruitmentList()) { + model.redoRecruitmentList(); + model.updateFilteredRecruitmentList(PREDICATE_SHOW_ALL_RECRUITMENT); + } + + break; + + case SCHEDULES_LIST: + if (model.canRedoScheduleList()) { + model.redoScheduleList(); + model.updateFilteredScheduleList(PREDICATE_SHOW_ALL_SCHEDULES); + } + break; + + default: + break; + } + } + model.redoModelList(); return new CommandResult(MESSAGE_SUCCESS); } } diff --git a/src/main/java/seedu/address/logic/commands/RemoveExpensesCommand.java b/src/main/java/seedu/address/logic/commands/RemoveExpensesCommand.java new file mode 100644 index 000000000000..4dba7bd4cc24 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/RemoveExpensesCommand.java @@ -0,0 +1,57 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.expenses.Expenses; + +/** + * Remove an expenses amount of the employee from the Expenses List. + */ +public class RemoveExpensesCommand extends Command { + public static final String COMMAND_WORD = "deleteExpenses"; + public static final String COMMAND_ALIAS = "de"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Remove the expenses identified by the index number used in the displayed person list.\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_REMOVE_EXPENSES_SUCCESS = "Remove Expenses: %1$s"; + + private final Index targetIndex; + + public RemoveExpensesCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredExpensesList(); + + if (targetIndex.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_EXPENSES_DISPLAYED_INDEX); + } + + Expenses expensesToRemove = lastShownList.get(targetIndex.getZeroBased()); + model.deleteExpenses(expensesToRemove); + model.commitExpensesList(); + return new CommandResult(String.format(MESSAGE_REMOVE_EXPENSES_SUCCESS, expensesToRemove)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof RemoveExpensesCommand // instanceof handles nulls + && targetIndex.equals(((RemoveExpensesCommand) other).targetIndex)); // state check + } +} + + diff --git a/src/main/java/seedu/address/logic/commands/SelectCommand.java b/src/main/java/seedu/address/logic/commands/SelectCommand.java index f5e8c1a8722e..c3d82f382e6f 100644 --- a/src/main/java/seedu/address/logic/commands/SelectCommand.java +++ b/src/main/java/seedu/address/logic/commands/SelectCommand.java @@ -18,14 +18,15 @@ */ public class SelectCommand extends Command { - public static final String COMMAND_WORD = "select"; + public static final String COMMAND_WORD = "selectPerson"; + public static final String COMMAND_ALIAS = "sp"; public static final String MESSAGE_USAGE = COMMAND_WORD - + ": Selects the person identified by the index number used in the displayed person list.\n" + + ": Selects the employee identified by the index number used in the displayed employee list.\n" + "Parameters: INDEX (must be a positive integer)\n" + "Example: " + COMMAND_WORD + " 1"; - public static final String MESSAGE_SELECT_PERSON_SUCCESS = "Selected Person: %1$s"; + public static final String MESSAGE_SELECT_PERSON_SUCCESS = "Selected Employee: %1$s"; private final Index targetIndex; diff --git a/src/main/java/seedu/address/logic/commands/SelectExpensesCommand.java b/src/main/java/seedu/address/logic/commands/SelectExpensesCommand.java new file mode 100644 index 000000000000..d203ad0482fc --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/SelectExpensesCommand.java @@ -0,0 +1,58 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.commons.core.EventsCenter; +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.events.ui.JumpToListExpensesRequestEvent; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.expenses.Expenses; + +/** + * Selects a expenses identified using it's displayed index from the expenses list. + */ +public class SelectExpensesCommand extends Command { + + public static final String COMMAND_WORD = "selectExpenses"; + public static final String COMMAND_ALIAS = "se"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Selects the expenses identified by the index number used in the displayed expenses list.\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_SELECT_EXPENSES_SUCCESS = "Selected Expenses: %1$s"; + + private final Index targetIndex; + + public SelectExpensesCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + + List filteredExpensesList = model.getFilteredExpensesList(); + + if (targetIndex.getZeroBased() >= filteredExpensesList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_EXPENSES_DISPLAYED_INDEX); + } + + EventsCenter.getInstance().post(new JumpToListExpensesRequestEvent(targetIndex)); + return new CommandResult(String.format(MESSAGE_SELECT_EXPENSES_SUCCESS, targetIndex.getOneBased())); + + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof SelectExpensesCommand // instanceof handles nulls + && targetIndex.equals(((SelectExpensesCommand) other).targetIndex)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/SelectRecruitmentPostCommand.java b/src/main/java/seedu/address/logic/commands/SelectRecruitmentPostCommand.java new file mode 100644 index 000000000000..4e56e4687b37 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/SelectRecruitmentPostCommand.java @@ -0,0 +1,49 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.commons.core.EventsCenter; +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.events.ui.JumpToListRecruitmentPostRequestEvent; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.recruitment.Recruitment; + +/** + * Selects a schedule identified using it's displayed index from the address book. + */ +public class SelectRecruitmentPostCommand extends Command { + public static final String COMMAND_WORD = "selectRecruitmentPost"; + public static final String COMMAND_ALIAS = "srp"; + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Selects the recruitment post identified by the index number used in the displayed recruitment list.\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + public static final String MESSAGE_SELECT_RECRUITMENT_SUCCESS = "Selected Recruitment Post: %1$s"; + private final Index targetIndex; + public SelectRecruitmentPostCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + List filteredRecruitmentList = model.getFilteredRecruitmentList(); + if (targetIndex.getZeroBased() >= filteredRecruitmentList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_RECRUITMENT_POST_DISPLAYED_INDEX); + } + EventsCenter.getInstance().post(new JumpToListRecruitmentPostRequestEvent(targetIndex)); + return new CommandResult(String.format(MESSAGE_SELECT_RECRUITMENT_SUCCESS, targetIndex.getOneBased())); + } + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof SelectRecruitmentPostCommand // instanceof handles nulls + && targetIndex.equals(((SelectRecruitmentPostCommand) other).targetIndex)); // state check + + } + +} diff --git a/src/main/java/seedu/address/logic/commands/SelectScheduleCommand.java b/src/main/java/seedu/address/logic/commands/SelectScheduleCommand.java new file mode 100644 index 000000000000..b8c28f4d7971 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/SelectScheduleCommand.java @@ -0,0 +1,73 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import seedu.address.commons.core.EventsCenter; +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.events.ui.JumpToListScheduleRequestEvent; +import seedu.address.logic.CommandHistory; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.schedule.Schedule; + +/** + * The {@code SelectScheduleCommand} class is used selecting a schedule identified using + * it's displayed index from the schedule list panel. + */ +public class SelectScheduleCommand extends Command { + public static final String COMMAND_WORD = "selectSchedule"; + public static final String COMMAND_ALIAS = "ss"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Selects the Schedule identified by the index number used in the displayed schedule list.\n" + + "Parameters: INDEX (must be a positive integer)\n" + + "Example: " + COMMAND_WORD + " 1"; + + public static final String MESSAGE_SELECT_SCHEDULE_SUCCESS = "Selected Schedule: %1$s"; + + private final Index targetIndex; + + public SelectScheduleCommand(Index targetIndex) { + this.targetIndex = targetIndex; + } + + /** + * SelectScheduleCommand execution. + *

+ * Selects a schedule identified using it's displayed index from the schedule list panel + *

+ * @param model {@code Model} which the command will operate on the model. + * @param history {@code CommandHistory} which the command history will be added. + * @return CommandResult, String success feedback to the user. + * @throws CommandException String failure feedback to the user if error in execution. + */ + @Override + public CommandResult execute(Model model, CommandHistory history) throws CommandException { + requireNonNull(model); + + List filteredScheduleList = model.getFilteredScheduleList(); + + if (targetIndex.getZeroBased() >= filteredScheduleList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_SCHEDULE_DISPLAYED_INDEX); + } + + EventsCenter.getInstance().post(new JumpToListScheduleRequestEvent(targetIndex)); + return new CommandResult(String.format(MESSAGE_SELECT_SCHEDULE_SUCCESS, targetIndex.getOneBased())); + + } + + /** + * Compares if both objects are equal. + * @param other similar object type to be compared with. + * @return Boolean, True if both objects are equal based on the defined conditions. + */ + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof SelectScheduleCommand // instanceof handles nulls + && targetIndex.equals(((SelectScheduleCommand) other).targetIndex)); // state check + } +} diff --git a/src/main/java/seedu/address/logic/commands/UndoCommand.java b/src/main/java/seedu/address/logic/commands/UndoCommand.java index 40441264f346..b56ce05ddada 100644 --- a/src/main/java/seedu/address/logic/commands/UndoCommand.java +++ b/src/main/java/seedu/address/logic/commands/UndoCommand.java @@ -1,31 +1,88 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_EXPENSES; import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_RECRUITMENT; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_SCHEDULES; + +import java.util.Set; import seedu.address.logic.CommandHistory; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; +import seedu.address.model.ModelTypes; /** - * Reverts the {@code model}'s address book to its previous state. + * Reverts the {@code model}'s address book, schedule list, expenses list, recruitment list + * to its previously undone state. */ public class UndoCommand extends Command { - public static final String COMMAND_WORD = "undo"; public static final String MESSAGE_SUCCESS = "Undo success!"; public static final String MESSAGE_FAILURE = "No more commands to undo!"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Undo the previous command."; + /** + * UndoCommand execution. + * @see seedu.address.model.VersionedModelList class for the tracking of storage commits across + * all storage types (addressbook, expensesList, scheduleList, recruitmentList) + *

+ * Get the last commit type from {@code VersionedModelList} class, which is a set + * containing which storage has been committed. Hence, the same storage + * will be allowed to perform undo. + *

+ * @param model {@code Model} which the command will operate on the model. + * @param history {@code CommandHistory} which the command history will be added. + * @return CommandResult, String success feedback to the user. + * @throws CommandException String failure feedback to the user if error in execution. + */ @Override public CommandResult execute(Model model, CommandHistory history) throws CommandException { requireNonNull(model); - if (!model.canUndoAddressBook()) { + if (!model.canUndoModel()) { throw new CommandException(MESSAGE_FAILURE); } + Set myModelUndoSet = model.getLastCommitType(); + + for (ModelTypes myModel : myModelUndoSet) { + + switch(myModel) { + case SCHEDULES_LIST: + if (model.canUndoScheduleList()) { + model.undoScheduleList(); + model.updateFilteredScheduleList(PREDICATE_SHOW_ALL_SCHEDULES); + } + break; + + case EXPENSES_LIST: + if (model.canUndoExpensesList()) { + model.undoExpensesList(); + model.updateFilteredExpensesList(PREDICATE_SHOW_ALL_EXPENSES); + } + break; + + case RECRUITMENT_LIST: + if (model.canUndoRecruitmentList()) { + model.undoRecruitmentList(); + model.updateFilteredRecruitmentList(PREDICATE_SHOW_ALL_RECRUITMENT); + } + break; + + case ADDRESS_BOOK: + if (model.canUndoAddressBook()) { + model.undoAddressBook(); + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + } + break; + + default: + break; + } + } - model.undoAddressBook(); - model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + model.undoModelList(); return new CommandResult(MESSAGE_SUCCESS); } } diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddCommandParser.java index 3b8bfa035e83..b9b4e7da3092 100644 --- a/src/main/java/seedu/address/logic/parser/AddCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/AddCommandParser.java @@ -2,22 +2,34 @@ import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATEOFBIRTH; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DEPARTMENT; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMPLOYEEID; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_POSITION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SALARY; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; import java.util.Set; +import java.util.regex.Pattern; import java.util.stream.Stream; import seedu.address.logic.commands.AddCommand; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.person.Address; +import seedu.address.model.person.Bonus; +import seedu.address.model.person.DateOfBirth; +import seedu.address.model.person.Department; import seedu.address.model.person.Email; +import seedu.address.model.person.EmployeeId; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; +import seedu.address.model.person.Position; +import seedu.address.model.person.Salary; +import seedu.address.model.person.tag.Tag; /** * Parses input arguments and creates a new AddCommand object @@ -31,20 +43,34 @@ public class AddCommandParser implements Parser { */ public AddCommand parse(String args) throws ParseException { ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); + ArgumentTokenizer.tokenize(args, PREFIX_EMPLOYEEID, PREFIX_NAME, PREFIX_DATEOFBIRTH, PREFIX_PHONE, + PREFIX_EMAIL, PREFIX_DEPARTMENT, PREFIX_POSITION, PREFIX_ADDRESS, PREFIX_SALARY, PREFIX_TAG); - if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ADDRESS, PREFIX_PHONE, PREFIX_EMAIL) - || !argMultimap.getPreamble().isEmpty()) { + if (!arePrefixesPresent(argMultimap, PREFIX_EMPLOYEEID, PREFIX_NAME, PREFIX_DATEOFBIRTH, PREFIX_PHONE, + PREFIX_EMAIL, PREFIX_DEPARTMENT, PREFIX_POSITION, PREFIX_ADDRESS, PREFIX_SALARY) + || !argMultimap.getPreamble().isEmpty() || !didPrefixesAppearOnlyOnce(args)) { throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); } - +; + EmployeeId employeeId = ParserUtil.parseEmployeeId(argMultimap.getValue(PREFIX_EMPLOYEEID).get()); Name name = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()); + DateOfBirth dateOfBirth = ParserUtil.parseDateOfBirth(argMultimap.getValue(PREFIX_DATEOFBIRTH).get()); Phone phone = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get()); Email email = ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get()); + Department department = ParserUtil.parseDepartment(argMultimap.getValue(PREFIX_DEPARTMENT).get()); + Position position = ParserUtil.parsePosition(argMultimap.getValue(PREFIX_POSITION).get()); Address address = ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get()); + Salary salary = ParserUtil.parseSalary(argMultimap.getValue(PREFIX_SALARY).get()); + + if (!checkSalaryFormat(salary.toString())) { + throw new ParseException(Salary.MESSAGE_SALARY_CONSTRAINTS); + } + + Bonus bonus = new Bonus("0.0"); Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); - Person person = new Person(name, phone, email, address, tagList); + Person person = new Person(employeeId, name, dateOfBirth, phone, email, department, position, address, + salary, bonus, tagList); return new AddCommand(person); } @@ -57,4 +83,37 @@ private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Pre return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); } + /** + * Check whether the input salary from the user conforms to the required format + * @param salary The user's input after salary prefix. + */ + private static boolean checkSalaryFormat(String salary) { + return Pattern.matches("[0-9.]+", salary); + } + + /** + * Check whether prefixes except tag's prefix appeared more than once within the argument. + * @param argument The user's input + */ + public boolean didPrefixesAppearOnlyOnce(String argument) { + String employeeIdPrefix = " " + PREFIX_EMPLOYEEID.toString(); + String namePrefix = " " + PREFIX_NAME.toString(); + String dateOfBirthPrefix = " " + PREFIX_DATEOFBIRTH.toString(); + String phonePrefix = " " + PREFIX_PHONE.toString(); + String emailPrefix = " " + PREFIX_EMAIL.toString(); + String departmentPrefix = " " + PREFIX_DEPARTMENT.toString(); + String positionPrefix = " " + PREFIX_POSITION.toString(); + String addressPrefix = " " + PREFIX_ADDRESS.toString(); + String salaryPrefix = " " + PREFIX_SALARY.toString(); + + return argument.indexOf(employeeIdPrefix) == argument.lastIndexOf(employeeIdPrefix) + && argument.indexOf(namePrefix) == argument.lastIndexOf(namePrefix) + && argument.indexOf(dateOfBirthPrefix) == argument.lastIndexOf(dateOfBirthPrefix) + && argument.indexOf(phonePrefix) == argument.lastIndexOf(phonePrefix) + && argument.indexOf(emailPrefix) == argument.lastIndexOf(emailPrefix) + && argument.indexOf(departmentPrefix) == argument.lastIndexOf(departmentPrefix) + && argument.indexOf(positionPrefix) == argument.lastIndexOf(positionPrefix) + && argument.indexOf(addressPrefix) == argument.lastIndexOf(addressPrefix) + && argument.indexOf(salaryPrefix) == argument.lastIndexOf(salaryPrefix); + } } diff --git a/src/main/java/seedu/address/logic/parser/AddExpensesCommandParser.java b/src/main/java/seedu/address/logic/parser/AddExpensesCommandParser.java new file mode 100644 index 000000000000..140b1689e897 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AddExpensesCommandParser.java @@ -0,0 +1,101 @@ +package seedu.address.logic.parser; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_OVERLOAD_PREFIX_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMPLOYEEID; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MEDICAL_EXPENSES; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MISCELLANEOUS_EXPENSES; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TRAVEL_EXPENSES; + +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.StringTokenizer; + +import seedu.address.logic.commands.AddExpensesCommand; +import seedu.address.logic.commands.AddExpensesCommand.EditExpensesDescriptor; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.expenses.Expenses; +import seedu.address.model.expenses.ExpensesAmount; +import seedu.address.model.expenses.MedicalExpenses; +import seedu.address.model.expenses.MiscellaneousExpenses; +import seedu.address.model.expenses.TravelExpenses; +import seedu.address.model.person.EmployeeId; + +/** + * Parses input arguments and creates a new AddExpensesCommand object + */ +public class AddExpensesCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the AddExpensesCommand + * and returns an AddExpensesCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public AddExpensesCommand parse(String args) throws ParseException { + NumberFormat formatter = new DecimalFormat("#0.00"); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_EMPLOYEEID, PREFIX_TRAVEL_EXPENSES, + PREFIX_MEDICAL_EXPENSES, PREFIX_MISCELLANEOUS_EXPENSES); + + int totalNumTokensSize = 4; + StringTokenizer st = new StringTokenizer(args); + if (st.countTokens() > totalNumTokensSize) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_OVERLOAD_PREFIX_FORMAT, + AddExpensesCommand.MESSAGE_USAGE)); + } + if (args.isEmpty() || (!argMultimap.getValue(PREFIX_TRAVEL_EXPENSES).isPresent() + && !argMultimap.getValue(PREFIX_MEDICAL_EXPENSES).isPresent() + && !argMultimap.getValue(PREFIX_MISCELLANEOUS_EXPENSES).isPresent())) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddExpensesCommand.MESSAGE_USAGE)); + } + + if (!didPrefixAppearOnlyOnce(args, PREFIX_MEDICAL_EXPENSES.toString()) || !didPrefixAppearOnlyOnce(args, + PREFIX_MISCELLANEOUS_EXPENSES.toString()) || !didPrefixAppearOnlyOnce(args, + PREFIX_TRAVEL_EXPENSES.toString())) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddExpensesCommand.MESSAGE_USAGE)); + } + + EmployeeId employeeId = ParserUtil.parseEmployeeId(argMultimap.getValue + (PREFIX_EMPLOYEEID).get()); + TravelExpenses travelExpenses = ParserUtil.parseTravelExpenses("0"); + MedicalExpenses medicalExpenses = ParserUtil.parseMedicalExpenses("0"); + MiscellaneousExpenses miscellaneousExpenses = ParserUtil.parseMiscellaneousExpenses("0"); + ExpensesAmount expensesAmount; + + if (argMultimap.getValue(PREFIX_TRAVEL_EXPENSES).isPresent()) { + travelExpenses = ParserUtil.parseTravelExpenses(argMultimap.getValue(PREFIX_TRAVEL_EXPENSES) + .get()); + } + if (argMultimap.getValue(PREFIX_MEDICAL_EXPENSES).isPresent()) { + medicalExpenses = ParserUtil.parseMedicalExpenses(argMultimap.getValue(PREFIX_MEDICAL_EXPENSES).get()); + } + if (argMultimap.getValue(PREFIX_MISCELLANEOUS_EXPENSES).isPresent()) { + miscellaneousExpenses = ParserUtil.parseMiscellaneousExpenses(argMultimap.getValue( + PREFIX_MISCELLANEOUS_EXPENSES).get()); + } + + double sumOfExpenses = Double.parseDouble((travelExpenses).toString()) + + Double.parseDouble((medicalExpenses).toString()) + + Double.parseDouble((miscellaneousExpenses).toString()); + expensesAmount = ParserUtil.parseExpensesAmount(String.valueOf(formatter.format(sumOfExpenses))); + + + Expenses expenses = new Expenses (employeeId, expensesAmount, travelExpenses, medicalExpenses, + miscellaneousExpenses); + + EditExpensesDescriptor editExpensesDescriptor = new EditExpensesDescriptor(); + editExpensesDescriptor.setExpensesAmount(expensesAmount); + editExpensesDescriptor.setTravelExpenses(travelExpenses); + editExpensesDescriptor.setMedicalExpenses(medicalExpenses); + editExpensesDescriptor.setMiscellaneousExpenses(miscellaneousExpenses); + if (!editExpensesDescriptor.isAnyFieldEdited()) { + throw new ParseException(AddExpensesCommand.MESSAGE_NOT_EDITED); + } + return new AddExpensesCommand(expenses, editExpensesDescriptor); + } + + /** + * Returns true if prefix has been repeated + */ + private boolean didPrefixAppearOnlyOnce(String argument, String prefix) { + return argument.indexOf(prefix) == argument.lastIndexOf(prefix); + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddLeavesCommandParser.java b/src/main/java/seedu/address/logic/parser/AddLeavesCommandParser.java new file mode 100644 index 000000000000..d4e525ac9af6 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AddLeavesCommandParser.java @@ -0,0 +1,61 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SCHEDULE_DATE; +import static seedu.address.model.schedule.Date.MESSAGE_DATE_OF_SCHEDULE_BEFORE_TODAY_DATE; + +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Stream; + +import seedu.address.logic.commands.AddLeavesCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +import seedu.address.model.schedule.Date; + + +/** + * Parses input arguments and creates a new {@code AddLeavesCommand} object + */ +public class AddLeavesCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddLeavesCommand + * and returns an AddLeavesCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public AddLeavesCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_SCHEDULE_DATE); + + if (!arePrefixesPresent(argMultimap, PREFIX_SCHEDULE_DATE) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddLeavesCommand.MESSAGE_USAGE)); + } + + Set dateSet = ParserUtil.parseDates(argMultimap.getAllValues(PREFIX_SCHEDULE_DATE)); + + Set datePastSet = new HashSet<>(); + for (Date date: dateSet) { + if (Date.isBeforeTodayDate(date.value)) { + datePastSet.add(date); + } + } + if (!datePastSet.isEmpty()) { + throw new ParseException(String.format(MESSAGE_DATE_OF_SCHEDULE_BEFORE_TODAY_DATE, + datePastSet, Date.todayDate())); + } + + return new AddLeavesCommand(dateSet); + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } + +} + diff --git a/src/main/java/seedu/address/logic/parser/AddRecruitmentPostCommandParser.java b/src/main/java/seedu/address/logic/parser/AddRecruitmentPostCommandParser.java new file mode 100644 index 000000000000..bd1842d01d82 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AddRecruitmentPostCommandParser.java @@ -0,0 +1,55 @@ +package seedu.address.logic.parser; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_JOB_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_JOB_POSITION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MINIMUM_EXPERIENCE; + +import java.util.stream.Stream; + +import seedu.address.logic.commands.AddRecruitmentPostCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.recruitment.JobDescription; +import seedu.address.model.recruitment.Post; +import seedu.address.model.recruitment.Recruitment; +import seedu.address.model.recruitment.WorkExp; + + +/** + * Parses input arguments and creates a new AddRecruitmentPostCommand object + */ +public class AddRecruitmentPostCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddRecruitmentPostCommand + * and returns an AddRecruitmentPostCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public AddRecruitmentPostCommand parse(String args) throws ParseException { + + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize( + args, PREFIX_JOB_POSITION, PREFIX_MINIMUM_EXPERIENCE, PREFIX_JOB_DESCRIPTION); + + if (!arePrefixesPresent(argMultimap, PREFIX_JOB_POSITION, PREFIX_MINIMUM_EXPERIENCE, PREFIX_JOB_DESCRIPTION) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format( + MESSAGE_INVALID_COMMAND_FORMAT, AddRecruitmentPostCommand.MESSAGE_USAGE2)); + } + Post post = ParserUtil.parsePost(argMultimap.getValue + (PREFIX_JOB_POSITION).get()); + WorkExp workExp = ParserUtil.parseWorkExp(argMultimap.getValue(PREFIX_MINIMUM_EXPERIENCE) + .get()); + JobDescription jobDescription = ParserUtil.parseJobDescription(argMultimap.getValue(PREFIX_JOB_DESCRIPTION) + .get()); + Recruitment recruitment = new Recruitment (post, workExp, jobDescription); + return new AddRecruitmentPostCommand(recruitment); + } + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/AddScheduleCommandParser.java b/src/main/java/seedu/address/logic/parser/AddScheduleCommandParser.java new file mode 100644 index 000000000000..15fdc0cbecdf --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AddScheduleCommandParser.java @@ -0,0 +1,75 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_OVERLOAD_PREFIX_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMPLOYEEID; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SCHEDULE_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SCHEDULE_TYPE; +import static seedu.address.model.schedule.Date.MESSAGE_DATE_OF_SCHEDULE_BEFORE_TODAY_DATE; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.StringTokenizer; +import java.util.stream.Stream; + +import seedu.address.logic.commands.AddScheduleCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +import seedu.address.model.person.EmployeeId; +import seedu.address.model.schedule.Date; +import seedu.address.model.schedule.Schedule; +import seedu.address.model.schedule.Type; + +/** + * Parses input arguments and creates a new {@code AddScheduleCommand} object + */ +public class AddScheduleCommandParser implements Parser { + + public static final int TOTAL_NUM_TOKEN_ADD_SCHEDULE = 3; + + /** + * Parses the given {@code String} of arguments in the context of the AddScheduleCommand + * and returns an AddCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public AddScheduleCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_EMPLOYEEID, PREFIX_SCHEDULE_TYPE, PREFIX_SCHEDULE_DATE); + + StringTokenizer st = new StringTokenizer(args); + if (st.countTokens() > TOTAL_NUM_TOKEN_ADD_SCHEDULE) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_OVERLOAD_PREFIX_FORMAT, + AddScheduleCommand.MESSAGE_USAGE)); + } + + if (!arePrefixesPresent(argMultimap, PREFIX_EMPLOYEEID, PREFIX_SCHEDULE_TYPE, PREFIX_SCHEDULE_DATE) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddScheduleCommand.MESSAGE_USAGE)); + } + + Type type = ParserUtil.parseStatus(argMultimap.getValue(PREFIX_SCHEDULE_TYPE).get()); + Date date = ParserUtil.parseDate(argMultimap.getValue(PREFIX_SCHEDULE_DATE).get()); + EmployeeId id = ParserUtil.parseEmployeeId(argMultimap.getValue(PREFIX_EMPLOYEEID).get()); + + LocalDate localDate = LocalDate.now(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/YYYY"); + String todayDate = localDate.format(formatter); + + if (Date.isBeforeTodayDate(date.value)) { + throw new ParseException(String.format(MESSAGE_DATE_OF_SCHEDULE_BEFORE_TODAY_DATE, date, todayDate)); + } + + Schedule schedule = new Schedule(id, type, date); + return new AddScheduleCommand(schedule); + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } + +} + diff --git a/src/main/java/seedu/address/logic/parser/AddWorksCommandParser.java b/src/main/java/seedu/address/logic/parser/AddWorksCommandParser.java new file mode 100644 index 000000000000..4803aebe24e2 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AddWorksCommandParser.java @@ -0,0 +1,61 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SCHEDULE_DATE; +import static seedu.address.model.schedule.Date.MESSAGE_DATE_OF_SCHEDULE_BEFORE_TODAY_DATE; + +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Stream; + +import seedu.address.logic.commands.AddWorksCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +import seedu.address.model.schedule.Date; + + +/** + * Parses input arguments and creates a new {@code AddWorksCommand} object + */ +public class AddWorksCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the AddWorksCommand + * and returns an AddWorksCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public AddWorksCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_SCHEDULE_DATE); + + if (!arePrefixesPresent(argMultimap, PREFIX_SCHEDULE_DATE) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddWorksCommand.MESSAGE_USAGE)); + } + + Set dateSet = ParserUtil.parseDates(argMultimap.getAllValues(PREFIX_SCHEDULE_DATE)); + + Set datePastSet = new HashSet<>(); + for (Date date: dateSet) { + if (Date.isBeforeTodayDate(date.value)) { + datePastSet.add(date); + } + } + if (!datePastSet.isEmpty()) { + throw new ParseException(String.format(MESSAGE_DATE_OF_SCHEDULE_BEFORE_TODAY_DATE, + datePastSet, Date.todayDate())); + } + + return new AddWorksCommand(dateSet); + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } +} + + + diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java index b7d57f5db86a..03c74cd156b7 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -1,5 +1,6 @@ package seedu.address.logic.parser; +import static seedu.address.commons.core.Messages.GREETING_MESSAGE_NONEWLINE; import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; import static seedu.address.commons.core.Messages.MESSAGE_UNKNOWN_COMMAND; @@ -7,19 +8,41 @@ import java.util.regex.Pattern; import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.AddExpensesCommand; +import seedu.address.logic.commands.AddLeavesCommand; +import seedu.address.logic.commands.AddRecruitmentPostCommand; +import seedu.address.logic.commands.AddScheduleCommand; +import seedu.address.logic.commands.AddWorksCommand; +import seedu.address.logic.commands.CalculateLeavesCommand; import seedu.address.logic.commands.ClearCommand; +import seedu.address.logic.commands.ClearExpensesCommand; +import seedu.address.logic.commands.ClearRecruitmentPostCommand; +import seedu.address.logic.commands.ClearScheduleCommand; import seedu.address.logic.commands.Command; import seedu.address.logic.commands.DeleteCommand; +import seedu.address.logic.commands.DeleteLeavesCommand; +import seedu.address.logic.commands.DeleteRecruitmentPostCommand; +import seedu.address.logic.commands.DeleteScheduleCommand; +import seedu.address.logic.commands.DeleteWorksCommand; import seedu.address.logic.commands.EditCommand; +import seedu.address.logic.commands.EditRecruitmentPostCommand; import seedu.address.logic.commands.ExitCommand; +import seedu.address.logic.commands.FilterCommand; import seedu.address.logic.commands.FindCommand; import seedu.address.logic.commands.HelpCommand; import seedu.address.logic.commands.HistoryCommand; import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.ModifyAllPayCommand; +import seedu.address.logic.commands.ModifyPayCommand; import seedu.address.logic.commands.RedoCommand; +import seedu.address.logic.commands.RemoveExpensesCommand; import seedu.address.logic.commands.SelectCommand; +import seedu.address.logic.commands.SelectExpensesCommand; +import seedu.address.logic.commands.SelectRecruitmentPostCommand; +import seedu.address.logic.commands.SelectScheduleCommand; import seedu.address.logic.commands.UndoCommand; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.addressbook.DayHourGreeting; /** * Parses user input. @@ -40,26 +63,41 @@ public class AddressBookParser { */ public Command parseCommand(String userInput) throws ParseException { final Matcher matcher = BASIC_COMMAND_FORMAT.matcher(userInput.trim()); + DayHourGreeting greeting = new DayHourGreeting(); if (!matcher.matches()) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, HelpCommand.MESSAGE_USAGE)); + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + greeting.getGreeting() + GREETING_MESSAGE_NONEWLINE)); } final String commandWord = matcher.group("commandWord"); final String arguments = matcher.group("arguments"); - switch (commandWord) { + switch (commandWord) { case AddCommand.COMMAND_WORD: return new AddCommandParser().parse(arguments); case EditCommand.COMMAND_WORD: return new EditCommandParser().parse(arguments); + case EditRecruitmentPostCommand.COMMAND_WORD: + case EditRecruitmentPostCommand.COMMAND_ALIAS: + return new EditRecruitmentPostCommandParser().parse(arguments); + case SelectCommand.COMMAND_WORD: + case SelectCommand.COMMAND_ALIAS: return new SelectCommandParser().parse(arguments); case DeleteCommand.COMMAND_WORD: return new DeleteCommandParser().parse(arguments); + case DeleteScheduleCommand.COMMAND_ALIAS: + case DeleteScheduleCommand.COMMAND_WORD: + return new DeleteScheduleCommandParser().parse(arguments); + + case DeleteRecruitmentPostCommand.COMMAND_WORD: + case DeleteRecruitmentPostCommand.COMMAND_ALIAS: + return new DeleteRecruitmentPostCommandParser().parse(arguments); + case ClearCommand.COMMAND_WORD: return new ClearCommand(); @@ -69,6 +107,14 @@ public Command parseCommand(String userInput) throws ParseException { case ListCommand.COMMAND_WORD: return new ListCommand(); + case ModifyPayCommand.COMMAND_WORD: + case ModifyPayCommand.COMMAND_ALIAS: + return new ModifyPayCommandParser().parse(arguments); + + case ModifyAllPayCommand.COMMAND_WORD: + case ModifyAllPayCommand.COMMAND_ALIAS: + return new ModifyAllPayCommandParser().parse(arguments); + case HistoryCommand.COMMAND_WORD: return new HistoryCommand(); @@ -84,8 +130,71 @@ public Command parseCommand(String userInput) throws ParseException { case RedoCommand.COMMAND_WORD: return new RedoCommand(); + case AddExpensesCommand.COMMAND_WORD: + case AddExpensesCommand.COMMAND_ALIAS: + return new AddExpensesCommandParser().parse(arguments); + + case RemoveExpensesCommand.COMMAND_WORD: + case RemoveExpensesCommand.COMMAND_ALIAS: + return new RemoveExpensesCommandParser().parse(arguments); + + case AddScheduleCommand.COMMAND_ALIAS: + case AddScheduleCommand.COMMAND_WORD: + return new AddScheduleCommandParser().parse(arguments); + + case AddLeavesCommand.COMMAND_ALIAS: + case AddLeavesCommand.COMMAND_WORD: + return new AddLeavesCommandParser().parse(arguments); + + case DeleteLeavesCommand.COMMAND_ALIAS: + case DeleteLeavesCommand.COMMAND_WORD: + return new DeleteLeavesCommandParser().parse(arguments); + + case AddWorksCommand.COMMAND_ALIAS: + case AddWorksCommand.COMMAND_WORD: + return new AddWorksCommandParser().parse(arguments); + + case DeleteWorksCommand.COMMAND_ALIAS: + case DeleteWorksCommand.COMMAND_WORD: + return new DeleteWorksCommandParser().parse(arguments); + + case CalculateLeavesCommand.COMMAND_ALIAS: + case CalculateLeavesCommand.COMMAND_WORD: + return new CalculateLeavesCommandParser().parse(arguments); + + case SelectExpensesCommand.COMMAND_WORD: + case SelectExpensesCommand.COMMAND_ALIAS: + return new SelectExpensesCommandParser().parse(arguments); + + case SelectScheduleCommand.COMMAND_ALIAS: + case SelectScheduleCommand.COMMAND_WORD: + return new SelectScheduleCommandParser().parse(arguments); + + case SelectRecruitmentPostCommand.COMMAND_WORD: + case SelectRecruitmentPostCommand.COMMAND_ALIAS: + return new SelectRecruitmentPostCommandParser().parse(arguments); + + case ClearScheduleCommand.COMMAND_ALIAS: + case ClearScheduleCommand.COMMAND_WORD: + return new ClearScheduleCommand(); + + case ClearExpensesCommand.COMMAND_WORD: + case ClearExpensesCommand.COMMAND_ALIAS: + return new ClearExpensesCommand(); + + case ClearRecruitmentPostCommand.COMMAND_WORD: + case ClearRecruitmentPostCommand.COMMAND_ALIAS: + return new ClearRecruitmentPostCommand(); + + case AddRecruitmentPostCommand.COMMAND_WORD: + case AddRecruitmentPostCommand.COMMAND_ALIAS: + return new AddRecruitmentPostCommandParser().parse(arguments); + + case FilterCommand.COMMAND_WORD: + return new FilterCommandParser().parse(arguments); + default: - throw new ParseException(MESSAGE_UNKNOWN_COMMAND); + throw new ParseException(MESSAGE_UNKNOWN_COMMAND + greeting.getGreeting() + GREETING_MESSAGE_NONEWLINE); } } diff --git a/src/main/java/seedu/address/logic/parser/CalculateLeavesCommandParser.java b/src/main/java/seedu/address/logic/parser/CalculateLeavesCommandParser.java new file mode 100644 index 000000000000..7a0dc92c7d24 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/CalculateLeavesCommandParser.java @@ -0,0 +1,59 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_OVERLOAD_PREFIX_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMPLOYEEID; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SCHEDULE_YEAR; + +import java.util.StringTokenizer; +import java.util.stream.Stream; + +import seedu.address.logic.commands.CalculateLeavesCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +import seedu.address.model.person.EmployeeId; +import seedu.address.model.schedule.Year; + +/** + * Parses input arguments and creates a new {@code CalculateLeavesCommand} object + */ +public class CalculateLeavesCommandParser implements Parser { + + public static final int TOTAL_NUM_TOKEN_CALCULATE_SCHEDULE = 2; + + /** + * Parses the given {@code String} of arguments in the context of the CalculateLeavesCommand + * and returns an CalculateLeavesCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public CalculateLeavesCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_EMPLOYEEID, PREFIX_SCHEDULE_YEAR); + + StringTokenizer st = new StringTokenizer(args); + if (st.countTokens() > TOTAL_NUM_TOKEN_CALCULATE_SCHEDULE) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_OVERLOAD_PREFIX_FORMAT, + CalculateLeavesCommand.MESSAGE_USAGE)); + } + + if (!arePrefixesPresent(argMultimap, PREFIX_SCHEDULE_YEAR, PREFIX_EMPLOYEEID) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + CalculateLeavesCommand.MESSAGE_USAGE)); + } + + Year year = ParserUtil.parseYear(argMultimap.getValue(PREFIX_SCHEDULE_YEAR).get()); + EmployeeId id = ParserUtil.parseEmployeeId(argMultimap.getValue(PREFIX_EMPLOYEEID).get()); + + return new CalculateLeavesCommand(id, year); + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } +} + diff --git a/src/main/java/seedu/address/logic/parser/CliSyntax.java b/src/main/java/seedu/address/logic/parser/CliSyntax.java index 75b1a9bf1190..8bb6e691de01 100644 --- a/src/main/java/seedu/address/logic/parser/CliSyntax.java +++ b/src/main/java/seedu/address/logic/parser/CliSyntax.java @@ -6,10 +6,28 @@ public class CliSyntax { /* Prefix definitions */ + public static final Prefix PREFIX_EMPLOYEEID = new Prefix("id/"); public static final Prefix PREFIX_NAME = new Prefix("n/"); + public static final Prefix PREFIX_DATEOFBIRTH = new Prefix("dob/"); public static final Prefix PREFIX_PHONE = new Prefix("p/"); public static final Prefix PREFIX_EMAIL = new Prefix("e/"); + public static final Prefix PREFIX_DEPARTMENT = new Prefix("d/"); + public static final Prefix PREFIX_POSITION = new Prefix("r/"); public static final Prefix PREFIX_ADDRESS = new Prefix("a/"); + public static final Prefix PREFIX_SALARY = new Prefix("s/"); + public static final Prefix PREFIX_BONUS = new Prefix("b/"); public static final Prefix PREFIX_TAG = new Prefix("t/"); + public static final Prefix PREFIX_EXPENSES_AMOUNT = new Prefix("ex/"); + public static final Prefix PREFIX_TRAVEL_EXPENSES = new Prefix("tra/"); + public static final Prefix PREFIX_MEDICAL_EXPENSES = new Prefix("med/"); + public static final Prefix PREFIX_MISCELLANEOUS_EXPENSES = new Prefix("misc/"); + + public static final Prefix PREFIX_SCHEDULE_DATE = new Prefix("d/"); + public static final Prefix PREFIX_SCHEDULE_TYPE = new Prefix("t/"); + public static final Prefix PREFIX_SCHEDULE_YEAR = new Prefix("y/"); + + public static final Prefix PREFIX_JOB_POSITION = new Prefix("jp/"); + public static final Prefix PREFIX_MINIMUM_EXPERIENCE = new Prefix("me/"); + public static final Prefix PREFIX_JOB_DESCRIPTION = new Prefix("jd/"); } diff --git a/src/main/java/seedu/address/logic/parser/DeleteLeavesCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteLeavesCommandParser.java new file mode 100644 index 000000000000..d860e1bc281f --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/DeleteLeavesCommandParser.java @@ -0,0 +1,48 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SCHEDULE_DATE; + +import java.util.Set; +import java.util.stream.Stream; + +import seedu.address.logic.commands.DeleteLeavesCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +import seedu.address.model.schedule.Date; + + +/** + * Parses input arguments and creates a new {@code DeleteLeavesCommand} object + */ +public class DeleteLeavesCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the DeleteLeavesCommand + * and returns an DeleteLeavesCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public DeleteLeavesCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_SCHEDULE_DATE); + + if (!arePrefixesPresent(argMultimap, PREFIX_SCHEDULE_DATE) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteLeavesCommand.MESSAGE_USAGE)); + } + + Set dates = ParserUtil.parseDates(argMultimap.getAllValues(PREFIX_SCHEDULE_DATE)); + + return new DeleteLeavesCommand(dates); + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } + +} + diff --git a/src/main/java/seedu/address/logic/parser/DeleteRecruitmentPostCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteRecruitmentPostCommandParser.java new file mode 100644 index 000000000000..bd393a6b70c2 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/DeleteRecruitmentPostCommandParser.java @@ -0,0 +1,29 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.DeleteRecruitmentPostCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new DeleteRecruitmentPost object + */ +public class DeleteRecruitmentPostCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the DeleteCommand + * and returns an DeleteRecruitmentPost object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public DeleteRecruitmentPostCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new DeleteRecruitmentPostCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteRecruitmentPostCommand.MESSAGE_USAGE), pe); + } + } + +} diff --git a/src/main/java/seedu/address/logic/parser/DeleteScheduleCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteScheduleCommandParser.java new file mode 100644 index 000000000000..a814fbe2652f --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/DeleteScheduleCommandParser.java @@ -0,0 +1,29 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.DeleteScheduleCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new {@code DeleteScheduleCommand} object + */ +public class DeleteScheduleCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the DeleteScheduleCommand + * and returns an DeleteScheduleCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public DeleteScheduleCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new DeleteScheduleCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteScheduleCommand.MESSAGE_USAGE), pe); + } + } + +} diff --git a/src/main/java/seedu/address/logic/parser/DeleteWorksCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteWorksCommandParser.java new file mode 100644 index 000000000000..b895af916ae3 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/DeleteWorksCommandParser.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_SCHEDULE_DATE; + +import java.util.Set; +import java.util.stream.Stream; + +import seedu.address.logic.commands.DeleteWorksCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +import seedu.address.model.schedule.Date; + + +/** + * Parses input arguments and creates a new {@code DeleteWorksCommand} object + */ +public class DeleteWorksCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the DeleteWorksCommand + * and returns an DeleteWorksCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public DeleteWorksCommand parse(String args) throws ParseException { + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_SCHEDULE_DATE); + + if (!arePrefixesPresent(argMultimap, PREFIX_SCHEDULE_DATE) + || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteWorksCommand.MESSAGE_USAGE)); + } + + Set dates = ParserUtil.parseDates(argMultimap.getAllValues(PREFIX_SCHEDULE_DATE)); + + return new DeleteWorksCommand(dates); + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/seedu/address/logic/parser/EditCommandParser.java index 845644b7dea1..80da07f716d2 100644 --- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/EditCommandParser.java @@ -3,9 +3,11 @@ import static java.util.Objects.requireNonNull; import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DEPARTMENT; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_POSITION; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; import java.util.Collection; @@ -17,7 +19,7 @@ import seedu.address.logic.commands.EditCommand; import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.tag.Tag; +import seedu.address.model.person.tag.Tag; /** * Parses input arguments and creates a new EditCommand object @@ -32,7 +34,8 @@ public class EditCommandParser implements Parser { public EditCommand parse(String args) throws ParseException { requireNonNull(args); ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_DEPARTMENT, + PREFIX_POSITION, PREFIX_ADDRESS, PREFIX_TAG); Index index; @@ -42,6 +45,10 @@ public EditCommand parse(String args) throws ParseException { throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE), pe); } + if (!didPrefixesAppearOnlyOnce(args)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE)); + } + EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor(); if (argMultimap.getValue(PREFIX_NAME).isPresent()) { editPersonDescriptor.setName(ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get())); @@ -52,6 +59,13 @@ public EditCommand parse(String args) throws ParseException { if (argMultimap.getValue(PREFIX_EMAIL).isPresent()) { editPersonDescriptor.setEmail(ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get())); } + if (argMultimap.getValue(PREFIX_DEPARTMENT).isPresent()) { + editPersonDescriptor.setDepartment(ParserUtil.parseDepartment(argMultimap + .getValue(PREFIX_DEPARTMENT).get())); + } + if (argMultimap.getValue(PREFIX_POSITION).isPresent()) { + editPersonDescriptor.setPosition(ParserUtil.parsePosition(argMultimap.getValue(PREFIX_POSITION).get())); + } if (argMultimap.getValue(PREFIX_ADDRESS).isPresent()) { editPersonDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); } @@ -79,4 +93,24 @@ private Optional> parseTagsForEdit(Collection tags) throws Pars return Optional.of(ParserUtil.parseTags(tagSet)); } + /** + * Check whether prefixes except tag's prefix appeared more than once within the argument. + * @param argument The user's input + */ + public boolean didPrefixesAppearOnlyOnce(String argument) { + String namePrefix = " " + PREFIX_NAME.toString(); + String phonePrefix = " " + PREFIX_PHONE.toString(); + String emailPrefix = " " + PREFIX_EMAIL.toString(); + String departmentPrefix = " " + PREFIX_DEPARTMENT.toString(); + String positionPrefix = " " + PREFIX_POSITION.toString(); + String addressPrefix = " " + PREFIX_ADDRESS.toString(); + + return argument.indexOf(namePrefix) == argument.lastIndexOf(namePrefix) + && argument.indexOf(phonePrefix) == argument.lastIndexOf(phonePrefix) + && argument.indexOf(emailPrefix) == argument.lastIndexOf(emailPrefix) + && argument.indexOf(departmentPrefix) == argument.lastIndexOf(departmentPrefix) + && argument.indexOf(positionPrefix) == argument.lastIndexOf(positionPrefix) + && argument.indexOf(addressPrefix) == argument.lastIndexOf(addressPrefix); + } + } diff --git a/src/main/java/seedu/address/logic/parser/EditRecruitmentPostCommandParser.java b/src/main/java/seedu/address/logic/parser/EditRecruitmentPostCommandParser.java new file mode 100644 index 000000000000..6b6c9244c4cc --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/EditRecruitmentPostCommandParser.java @@ -0,0 +1,59 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_JOB_DESCRIPTION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_JOB_POSITION; +import static seedu.address.logic.parser.CliSyntax.PREFIX_MINIMUM_EXPERIENCE; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.EditRecruitmentPostCommand; +import seedu.address.logic.commands.EditRecruitmentPostCommand.EditPostDescriptor; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new EditRecruitmentPostCommand object + */ +public class EditRecruitmentPostCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the EditRecruitmentPostCommand + * and returns an EditRecruitmentPostCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public EditRecruitmentPostCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_JOB_POSITION, + PREFIX_MINIMUM_EXPERIENCE, PREFIX_JOB_DESCRIPTION); + + Index index; + + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + EditRecruitmentPostCommand.MESSAGE_USAGE), pe); + } + + EditPostDescriptor editPostDescriptor = new EditPostDescriptor(); + if (argMultimap.getValue(PREFIX_JOB_POSITION).isPresent()) { + editPostDescriptor.setPost(ParserUtil.parsePost(argMultimap.getValue(PREFIX_JOB_POSITION).get())); + } + if (argMultimap.getValue(PREFIX_MINIMUM_EXPERIENCE).isPresent()) { + editPostDescriptor.setWorkExp(ParserUtil.parseWorkExp( + argMultimap.getValue(PREFIX_MINIMUM_EXPERIENCE).get())); + } + if (argMultimap.getValue(PREFIX_JOB_DESCRIPTION).isPresent()) { + editPostDescriptor.setJobDescription(ParserUtil.parseJobDescription( + argMultimap.getValue(PREFIX_JOB_DESCRIPTION).get())); + } + + if (!editPostDescriptor.isAnyFieldEdited()) { + throw new ParseException(EditRecruitmentPostCommand.MESSAGE_NOT_EDITED); + } + + return new EditRecruitmentPostCommand(index, editPostDescriptor); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/FilterCommandParser.java b/src/main/java/seedu/address/logic/parser/FilterCommandParser.java new file mode 100644 index 000000000000..fbf7c81ade8d --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/FilterCommandParser.java @@ -0,0 +1,162 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DEPARTMENT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_POSITION; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import seedu.address.logic.commands.FilterCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Department; +import seedu.address.model.person.DepartmentContainsKeywordsPredicate; +import seedu.address.model.person.Position; +import seedu.address.model.person.PositionContainsKeywordsPredicate; + +/** + * Parses input arguments and creates a new FilterCommand object + */ +public class FilterCommandParser implements Parser { + + private static final List ACCEPTED_ORDERS = new ArrayList<>(Arrays.asList(FilterCommand.ASCENDING, + FilterCommand.DESCENDING)); + private static final String DEPARTMENT_KEYWORD_VALIDATION_REGEX = "[A-Za-z ]{1,30}"; + private static final String POSITION_KEYWORD_VALIDATION_REGEX = "[A-Za-z ]{1,30}"; + private static final int INDEX_ONE = 0; + + /** + * Parses the given {@code String} of arguments in the context of the FilterCommand + * and returns an FilterCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public FilterCommand parse(String args) throws ParseException { + requireNonNull(args); + + String trimmedArgs = args.trim(); + String sortOrder = trimmedArgs.split("\\s")[INDEX_ONE].toLowerCase(); + String[] departmentKeywords = new String[]{""}; + String[] positionKeywords = new String[]{""}; + FilterCommand filterCommand = new FilterCommand(new DepartmentContainsKeywordsPredicate(Arrays + .asList(departmentKeywords)), new PositionContainsKeywordsPredicate(Arrays.asList(positionKeywords)), + sortOrder); + + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_DEPARTMENT, PREFIX_POSITION); + + if (trimmedArgs.isEmpty() || (!argMultimap.getValue(PREFIX_DEPARTMENT).isPresent() + && !argMultimap.getValue(PREFIX_POSITION).isPresent()) || !didPrefixesAppearOnlyOnce(trimmedArgs) + || !ACCEPTED_ORDERS.contains(sortOrder)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, FilterCommand.MESSAGE_USAGE)); + } + + if (!argMultimap.getValue(PREFIX_DEPARTMENT).isPresent()) { + filterCommand.setIsDepartmentPrefixPresent(false); + } else if (argMultimap.getValue(PREFIX_DEPARTMENT).isPresent() + && !processDepartmentKeywords(argMultimap, filterCommand)) { + throw new ParseException(Department.MESSAGE_DEPARTMENT_KEYWORD_CONSTRAINTS); + } + + if (!argMultimap.getValue(PREFIX_POSITION).isPresent()) { + filterCommand.setIsPositionPrefixPresent(false); + } else if (argMultimap.getValue(PREFIX_POSITION).isPresent() + && !processPositionKeywords(argMultimap, filterCommand)) { + throw new ParseException(Position.MESSAGE_POSITION_KEYWORD_CONSTRAINTS); + } + + return filterCommand; + } + + /** + * Process the department keyword(s) from the user that are to be searched. + * @param argMultimap The user input that has been tokenized based on the prefixes + * @param command The FilterCommand to be returned for execution + */ + public boolean processDepartmentKeywords(ArgumentMultimap argMultimap, FilterCommand command) { + String trimmedDepartment = (argMultimap.getValue(PREFIX_DEPARTMENT).get().trim()); + String[] departmentKeywords = trimmedDepartment.split("\\s+"); + + return validityCheckForDepartments(command, departmentKeywords); + } + + /** + * Validity check for department(s) and setting the department predicate of filter command. + * @param command The FilterCommand to be returned for execution + * @param keywords The user's input split by space + */ + public boolean validityCheckForDepartments(FilterCommand command, String[] keywords) { + if (!areDepartmentKeywordsValid(keywords)) { + return false; + } + + command.setIsDepartmentPrefixPresent(true); + command.setDepartmentPredicate(new DepartmentContainsKeywordsPredicate(Arrays.asList(keywords))); + return true; + } + + /** + * Checks whether given keyword(s) are valid department(s). + * @param keywords The user input + */ + public boolean areDepartmentKeywordsValid(String[] keywords) { + for (String keyword: keywords) { + if (!keyword.matches(DEPARTMENT_KEYWORD_VALIDATION_REGEX)) { + return false; + } + } + return true; + } + + /** + * Process the position keyword(s) from the user that are to be searched. + * @param argMultimap The user input that has been tokenized based on the prefixes + * @param command The FilterCommand to be returned for execution + */ + public boolean processPositionKeywords(ArgumentMultimap argMultimap, FilterCommand command) { + String trimmedPosition = (argMultimap.getValue(PREFIX_POSITION).get().trim()); + String[] positionKeywords = trimmedPosition.split("\\s+"); + + return validityCheckForPositions(command, positionKeywords); + } + + /** + * Validity check for position(s) and setting the position predicate of filter command. + * @param command The FilterCommand to be returned for execution + * @param keywords The user's input split by space + */ + public boolean validityCheckForPositions(FilterCommand command, String[] keywords) { + if (!arePositionKeywordsValid(keywords)) { + return false; + } + + command.setIsPositionPrefixPresent(true); + command.setPositionPredicate(new PositionContainsKeywordsPredicate(Arrays.asList(keywords))); + return true; + } + + /** + * Checks whether given keyword(s) are valid position(s). + * @param keywords The user's input + */ + public boolean arePositionKeywordsValid(String[] keywords) { + for (String keyword: keywords) { + if (!keyword.matches(POSITION_KEYWORD_VALIDATION_REGEX)) { + return false; + } + } + return true; + } + + /** + * Check whether department and position prefix appeared more than once within the argument. + * @param argument The user's input + */ + public boolean didPrefixesAppearOnlyOnce(String argument) { + String departmentPrefix = " " + PREFIX_DEPARTMENT.toString(); + String positionPrefix = " " + PREFIX_POSITION.toString(); + + return argument.indexOf(departmentPrefix) == argument.lastIndexOf(departmentPrefix) + && argument.indexOf(positionPrefix) == argument.lastIndexOf(positionPrefix); + } +} diff --git a/src/main/java/seedu/address/logic/parser/FindCommandParser.java b/src/main/java/seedu/address/logic/parser/FindCommandParser.java index b186a967cb94..8bdea1500047 100644 --- a/src/main/java/seedu/address/logic/parser/FindCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/FindCommandParser.java @@ -1,33 +1,80 @@ package seedu.address.logic.parser; +import static java.util.Objects.requireNonNull; import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import java.util.Arrays; - import seedu.address.logic.commands.FindCommand; import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.address.model.person.EmployeeId; +import seedu.address.model.person.Name; /** * Parses input arguments and creates a new FindCommand object */ public class FindCommandParser implements Parser { + private static final int INDEX_FIRST_CHARACTER = 0; + private boolean isInputName = false; + private boolean isInputEmployeeId = false; + /** * Parses the given {@code String} of arguments in the context of the FindCommand * and returns an FindCommand object for execution. * @throws ParseException if the user input does not conform the expected format */ public FindCommand parse(String args) throws ParseException { - String trimmedArgs = args.trim(); - if (trimmedArgs.isEmpty()) { + requireNonNull(args); + + String keyword = args.trim(); + if (keyword.isEmpty()) { throw new ParseException( String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); } - String[] nameKeywords = trimmedArgs.split("\\s+"); + FindCommand findCommand = new FindCommand(keyword); + isNameOrEmployeeId(keyword); + + if (!isInputName && !isInputEmployeeId) { + throw new ParseException(FindCommand.MESSAGE_VALID_INPUT); + } else if (isInputName && !isInputEmployeeId) { + findCommand.setIsInputName(true); + findCommand.setIsInputEmployeeId(false); + } else if (!isInputName && isInputEmployeeId) { + findCommand.setIsInputName(false); + findCommand.setIsInputEmployeeId(true); + } + + return findCommand; + } + + /** + * Checks whether the given keyword is a valid name. + * @param keyword The user input + */ + public boolean isNameValid(String keyword) { + return Name.isValidName(keyword); + } + + /** + * Checks whether the given keyword is a valid employeeId. + * @param keyword The user input + */ + public boolean isEmployeeIdValid(String keyword) { + return EmployeeId.isValidEmployeeId(keyword); + } - return new FindCommand(new NameContainsKeywordsPredicate(Arrays.asList(nameKeywords))); + /** + * Check whether given keyword is a name or employeeId. + * @param keyword The user input + */ + public void isNameOrEmployeeId(String keyword) { + if (Character.isLetter(keyword.charAt(INDEX_FIRST_CHARACTER))) { + isInputName = isNameValid(keyword); + isInputEmployeeId = false; + } else if (Character.isDigit(keyword.charAt(INDEX_FIRST_CHARACTER))) { + isInputEmployeeId = isEmployeeIdValid(keyword); + isInputName = false; + } } } diff --git a/src/main/java/seedu/address/logic/parser/ModifyAllPayCommandParser.java b/src/main/java/seedu/address/logic/parser/ModifyAllPayCommandParser.java new file mode 100644 index 000000000000..e9fe0dc6ac26 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ModifyAllPayCommandParser.java @@ -0,0 +1,75 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_BONUS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SALARY; + +import seedu.address.logic.commands.ModifyAllPayCommand; +import seedu.address.logic.commands.ModifyAllPayCommand.ModSalaryDescriptor; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Bonus; + +/** + * Parses input arguments and creates a new EditCommand object + */ +public class ModifyAllPayCommandParser implements Parser { + private static final double BONUS_UPPER_LIMIT = 24.0; + /** + * Parses the given {@code String} of arguments in the context of the EditCommand + * and returns an EditCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public ModifyAllPayCommand parse(String args) throws ParseException { + requireNonNull(args); + String trimmedArgs = args.trim().toLowerCase(); + + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_SALARY, PREFIX_BONUS); + + if (trimmedArgs.isEmpty() || (!argMultimap.getValue(PREFIX_BONUS).isPresent() + && !argMultimap.getValue(PREFIX_SALARY).isPresent()) || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ModifyAllPayCommand.MESSAGE_USAGE)); + } + + if (!didPrefixAppearOnlyOnce(args)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ModifyAllPayCommand.MESSAGE_USAGE)); + } + + ModSalaryDescriptor modSalaryDescriptor = new ModSalaryDescriptor(); + + if (argMultimap.getValue(PREFIX_SALARY).isPresent()) { + modSalaryDescriptor.setSalary(ParserUtil.parseSalary(argMultimap.getValue(PREFIX_SALARY).get())); + } + + if (argMultimap.getValue(PREFIX_BONUS).isPresent()) { + Bonus bonusInput = ParserUtil.parseBonus(argMultimap.getValue(PREFIX_BONUS).get()); + + double bonus = Double.parseDouble(argMultimap.getValue(PREFIX_BONUS).get()); + + if (bonus > BONUS_UPPER_LIMIT) { + throw new ParseException(Bonus.MESSAGE_BONUS_CONSTRAINTS); + } + + modSalaryDescriptor.setBonus(bonusInput); + } + + if (!modSalaryDescriptor.isAnyFieldEdited()) { + throw new ParseException(ModifyAllPayCommand.MESSAGE_NOT_MODIFIED); + } + + return new ModifyAllPayCommand(modSalaryDescriptor); + } + + /** + * Check whether prefixes appeared more than once within the argument. + * @param argument The user's input + */ + private boolean didPrefixAppearOnlyOnce(String argument) { + String prefixSalary = " " + PREFIX_SALARY.toString(); + String prefixBonus = " " + PREFIX_BONUS.toString(); + + return argument.indexOf(prefixSalary) == argument.lastIndexOf(prefixSalary) + && argument.indexOf(prefixBonus) == argument.lastIndexOf(prefixBonus); + } +} diff --git a/src/main/java/seedu/address/logic/parser/ModifyPayCommandParser.java b/src/main/java/seedu/address/logic/parser/ModifyPayCommandParser.java new file mode 100644 index 000000000000..b6e6e0914ddf --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ModifyPayCommandParser.java @@ -0,0 +1,77 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_BONUS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SALARY; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.ModifyPayCommand; +import seedu.address.logic.commands.ModifyPayCommand.ModSalaryDescriptor; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Bonus; + +/** + * Parses input arguments and creates a new EditCommand object + */ +public class ModifyPayCommandParser implements Parser { + private static final double BONUS_UPPER_LIMIT = 24.0; + /** + * Parses the given {@code String} of arguments in the context of the EditCommand + * and returns an EditCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public ModifyPayCommand parse(String args) throws ParseException { + requireNonNull(args); + + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_SALARY, PREFIX_BONUS); + + Index index; + + try { + index = ParserUtil.parseIndex(argMultimap.getPreamble()); + } catch (ParseException pe) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ModifyPayCommand.MESSAGE_USAGE), pe); + } + + if (!didPrefixAppearOnlyOnce(args)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, ModifyPayCommand.MESSAGE_USAGE)); + } + + ModSalaryDescriptor modSalaryDescriptor = new ModSalaryDescriptor(); + + if (argMultimap.getValue(PREFIX_SALARY).isPresent()) { + modSalaryDescriptor.setSalary(ParserUtil.parseSalary(argMultimap.getValue(PREFIX_SALARY).get())); + } + + if (argMultimap.getValue(PREFIX_BONUS).isPresent()) { + Bonus bonusInput = ParserUtil.parseBonus(argMultimap.getValue(PREFIX_BONUS).get()); + + double bonus = Double.parseDouble(argMultimap.getValue(PREFIX_BONUS).get()); + + if (bonus > BONUS_UPPER_LIMIT) { + throw new ParseException(Bonus.MESSAGE_BONUS_CONSTRAINTS); + } + + modSalaryDescriptor.setBonus(bonusInput); + } + + if (!modSalaryDescriptor.isAnyFieldEdited()) { + throw new ParseException(ModifyPayCommand.MESSAGE_NOT_MODIFIED); + } + + return new ModifyPayCommand(index, modSalaryDescriptor); + } + + /** + * Check whether prefixes appeared more than once within the argument. + * @param argument The user's input + */ + private boolean didPrefixAppearOnlyOnce(String argument) { + String salaryPrefix = " " + PREFIX_SALARY.toString(); + String bonusPrefix = " " + PREFIX_BONUS.toString(); + + return argument.indexOf(salaryPrefix) == argument.lastIndexOf(salaryPrefix) + && argument.indexOf(bonusPrefix) == argument.lastIndexOf(bonusPrefix); + } +} diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java index 76daf40807e2..ee3b00b4b60c 100644 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java @@ -9,11 +9,27 @@ import seedu.address.commons.core.index.Index; import seedu.address.commons.util.StringUtil; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.expenses.ExpensesAmount; +import seedu.address.model.expenses.MedicalExpenses; +import seedu.address.model.expenses.MiscellaneousExpenses; +import seedu.address.model.expenses.TravelExpenses; import seedu.address.model.person.Address; +import seedu.address.model.person.Bonus; +import seedu.address.model.person.DateOfBirth; +import seedu.address.model.person.Department; import seedu.address.model.person.Email; +import seedu.address.model.person.EmployeeId; import seedu.address.model.person.Name; import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; +import seedu.address.model.person.Position; +import seedu.address.model.person.Salary; +import seedu.address.model.person.tag.Tag; +import seedu.address.model.recruitment.JobDescription; +import seedu.address.model.recruitment.Post; +import seedu.address.model.recruitment.WorkExp; +import seedu.address.model.schedule.Date; +import seedu.address.model.schedule.Type; +import seedu.address.model.schedule.Year; /** * Contains utility methods used for parsing strings in the various *Parser classes. @@ -35,6 +51,123 @@ public static Index parseIndex(String oneBasedIndex) throws ParseException { return Index.fromOneBased(Integer.parseInt(trimmedIndex)); } + /** + * Parses a {@code String employeeId} into a {@code EmployeeId}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code employeeId} is invalid. + */ + public static EmployeeId parseEmployeeId(String employeeId) throws ParseException { + requireNonNull(employeeId); + String trimmedEmployeeId = employeeId.trim(); + if (!EmployeeId.isValidEmployeeId(trimmedEmployeeId)) { + throw new ParseException(EmployeeId.MESSAGE_EMPLOYEEID_CONSTRAINTS); + } + return new EmployeeId(trimmedEmployeeId); + } + + /** + * Parses a {@code String job position} into a {@code jobPosition}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code jobPosition} is invalid. + */ + public static Post parsePost(String post) throws ParseException { + requireNonNull(post); + String trimmedPost = post.trim(); + if (!Post.isValidPost(trimmedPost)) { + throw new ParseException(Post.MESSAGE_POST_CONSTRAINTS); + } + return new Post(trimmedPost); + } + + /** + * Parses a {@code String job description} into a {@code jobDescription}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code jobDescription} is invalid. + */ + public static JobDescription parseJobDescription(String jobDescription) throws ParseException { + requireNonNull(jobDescription); + String trimmedJobDescription = jobDescription.trim(); + if (!JobDescription.isValidJobDescription(trimmedJobDescription)) { + throw new ParseException(JobDescription.MESSAGE_JOB_DESCRIPTION_CONSTRAINTS); + } + return new JobDescription(trimmedJobDescription); + } + + /** + * Parses a {@code String workExp} into a {@code WorkExp}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code workExp} is invalid. + */ + public static WorkExp parseWorkExp(String workExp) throws ParseException { + requireNonNull(workExp); + String trimmedWorkExp = workExp.trim(); + if (!WorkExp.isValidWorkExp(trimmedWorkExp)) { + throw new ParseException(WorkExp.MESSAGE_WORK_EXP_CONSTRAINTS); + } + return new WorkExp(trimmedWorkExp); + } + + /** + * Parses a {@code String name} into a {@code Name}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code name} is invalid. + */ + public static Date parseDate(String date) throws ParseException { + requireNonNull(date); + String trimmedDate = date.trim(); + if (!Date.isValidScheduleDate(trimmedDate)) { + throw new ParseException(Date.getDateConstraintsError()); + } + return new Date(trimmedDate); + } + + /** + * Parses a {@code String name} into a {@code Name}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code name} is invalid. + */ + public static Year parseYear(String date) throws ParseException { + requireNonNull(date); + String trimmedYear = date.trim(); + if (!Year.isValidYear(trimmedYear)) { + throw new ParseException(Year.MESSAGE_YEAR_CONSTRAINTS); + } + return new Year(trimmedYear); + } + + /** + * Parses {@code Collection dates} into a {@code Set}. + */ + public static Set parseDates(Collection dates) throws ParseException { + requireNonNull(dates); + final Set dateSet = new HashSet<>(); + for (String dateFormat : dates) { + dateSet.add(parseDate(dateFormat)); + } + return dateSet; + } + + /** + * Parses a {@code String name} into a {@code Name}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code name} is invalid. + */ + public static Type parseStatus(String status) throws ParseException { + requireNonNull(status); + String trimmedStatus = status.trim(); + if (!Type.isValidType(trimmedStatus)) { + throw new ParseException(Type.MESSAGE_TYPE_CONSTRAINTS); + } + return new Type(trimmedStatus); + } + /** * Parses a {@code String name} into a {@code Name}. * Leading and trailing whitespaces will be trimmed. @@ -50,6 +183,21 @@ public static Name parseName(String name) throws ParseException { return new Name(trimmedName); } + /** + * Parses a {@code String dateOfBirth} into a {@code DateOfBirth}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code dateOfBirth} is invalid. + */ + public static DateOfBirth parseDateOfBirth(String dateOfBirth) throws ParseException { + requireNonNull(dateOfBirth); + String trimmedDateOfBirth = dateOfBirth.trim(); + if (!DateOfBirth.isValidDateOfBirth(trimmedDateOfBirth)) { + throw new ParseException(DateOfBirth.getMessageDateOfBirthConstraints()); + } + return new DateOfBirth(trimmedDateOfBirth); + } + /** * Parses a {@code String phone} into a {@code Phone}. * Leading and trailing whitespaces will be trimmed. @@ -95,6 +243,66 @@ public static Email parseEmail(String email) throws ParseException { return new Email(trimmedEmail); } + /** + * Parses a {@code String department} into an {@code Department}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code department} is invalid. + */ + public static Department parseDepartment(String department) throws ParseException { + requireNonNull(department); + String trimmedDepartment = department.trim(); + if (!Department.isValidDepartment(trimmedDepartment)) { + throw new ParseException(Department.MESSAGE_DEPARTMENT_CONSTRAINTS); + } + return new Department(trimmedDepartment); + } + + /** + * Parses a {@code String position} into an {@code Position}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code position} is invalid. + */ + public static Position parsePosition(String position) throws ParseException { + requireNonNull(position); + String trimmedPosition = position.trim(); + if (!Position.isValidPosition(trimmedPosition)) { + throw new ParseException(Position.MESSAGE_POSITION_CONSTRAINTS); + } + return new Position(trimmedPosition); + } + + /** + * Parses a {@code String salary} into an {@code Salary}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code salary} is invalid. + */ + public static Salary parseSalary(String salary) throws ParseException { + requireNonNull(salary); + String trimmedSalary = salary.trim(); + if (!Salary.isValidSalary(trimmedSalary)) { + throw new ParseException(Salary.MESSAGE_SALARY_CONSTRAINTS); + } + return new Salary(trimmedSalary); + } + + /** + * Parses a {@code String bonus} into an {@code Bonus}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code bonus} is invalid. + */ + public static Bonus parseBonus(String bonus) throws ParseException { + requireNonNull(bonus); + String trimmedBonus = bonus.trim(); + if (!Bonus.isValidBonus(trimmedBonus)) { + throw new ParseException(Bonus.MESSAGE_BONUS_CONSTRAINTS); + } + return new Bonus(trimmedBonus); + } + /** * Parses a {@code String tag} into a {@code Tag}. * Leading and trailing whitespaces will be trimmed. @@ -121,4 +329,64 @@ public static Set parseTags(Collection tags) throws ParseException } return tagSet; } + + /** + * Parses a {@code String expensesAmount} into a {@code ExpensesAmount}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code expensesAmount} is invalid. + */ + public static ExpensesAmount parseExpensesAmount(String expensesAmount) throws ParseException { + requireNonNull(expensesAmount); + String trimmedExpensesAmount = expensesAmount.trim(); + if (!ExpensesAmount.isValidExpensesAmount(trimmedExpensesAmount)) { + throw new ParseException(ExpensesAmount.MESSAGE_EXPENSES_AMOUNT_CONSTRAINTS); + } + return new ExpensesAmount(trimmedExpensesAmount); + } + + /** + * Parses a {@code String expensesAmount} into a {@code ExpensesAmount}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code expensesAmount} is invalid. + */ + public static TravelExpenses parseTravelExpenses(String travelExpenses) throws ParseException { + requireNonNull(travelExpenses); + String trimmedTravelExpenses = travelExpenses.trim(); + if (!TravelExpenses.isValidTravelExpenses(trimmedTravelExpenses)) { + throw new ParseException(TravelExpenses.MESSAGE_TRAVEL_EXPENSES_CONSTRAINTS); + } + return new TravelExpenses(trimmedTravelExpenses); + } + + /** + * Parses a {@code String expensesAmount} into a {@code ExpensesAmount}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code expensesAmount} is invalid. + */ + public static MedicalExpenses parseMedicalExpenses(String medicalExpenses) throws ParseException { + requireNonNull(medicalExpenses); + String trimmedMedicalExpenses = medicalExpenses.trim(); + if (!MedicalExpenses.isValidMedicalExpenses(trimmedMedicalExpenses)) { + throw new ParseException(MedicalExpenses.MESSAGE_MEDICAL_EXPENSES_CONSTRAINTS); + } + return new MedicalExpenses(trimmedMedicalExpenses); + } + + /** + * Parses a {@code String expensesAmount} into a {@code ExpensesAmount}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code expensesAmount} is invalid. + */ + public static MiscellaneousExpenses parseMiscellaneousExpenses(String miscellaneousExpenses) throws ParseException { + requireNonNull(miscellaneousExpenses); + String trimmedMiscellaneousExpenses = miscellaneousExpenses.trim(); + if (!MiscellaneousExpenses.isValidMiscellaneousExpenses(trimmedMiscellaneousExpenses)) { + throw new ParseException(MiscellaneousExpenses.MESSAGE_MISCELLANEOUS_EXPENSES_CONSTRAINTS); + } + return new MiscellaneousExpenses(trimmedMiscellaneousExpenses); + } } diff --git a/src/main/java/seedu/address/logic/parser/RemoveExpensesCommandParser.java b/src/main/java/seedu/address/logic/parser/RemoveExpensesCommandParser.java new file mode 100644 index 000000000000..25a2c01175d3 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/RemoveExpensesCommandParser.java @@ -0,0 +1,29 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.RemoveExpensesCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new RemoveExpensesCommand object + */ +public class RemoveExpensesCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the RemoveExpensesCommand + * and returns an RemoveExpensesCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public RemoveExpensesCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new RemoveExpensesCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, RemoveExpensesCommand.MESSAGE_USAGE), pe); + } + } + +} diff --git a/src/main/java/seedu/address/logic/parser/SelectExpensesCommandParser.java b/src/main/java/seedu/address/logic/parser/SelectExpensesCommandParser.java new file mode 100644 index 000000000000..3c968275524a --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/SelectExpensesCommandParser.java @@ -0,0 +1,28 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.SelectExpensesCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new SelectExpensesCommand object + */ +public class SelectExpensesCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the SelectExpensesCommand + * and returns an SelectExpensesCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public SelectExpensesCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new SelectExpensesCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SelectExpensesCommand.MESSAGE_USAGE), pe); + } + } +} diff --git a/src/main/java/seedu/address/logic/parser/SelectRecruitmentPostCommandParser.java b/src/main/java/seedu/address/logic/parser/SelectRecruitmentPostCommandParser.java new file mode 100644 index 000000000000..ae0048586c56 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/SelectRecruitmentPostCommandParser.java @@ -0,0 +1,30 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.SelectRecruitmentPostCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new SelectRecruitmentPostCommand object + */ +public class SelectRecruitmentPostCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the SelectRecruitmentPostCommand + * and returns an SelectRecruitmentPostCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public SelectRecruitmentPostCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new SelectRecruitmentPostCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SelectRecruitmentPostCommand.MESSAGE_USAGE), pe); + + } + } + +} diff --git a/src/main/java/seedu/address/logic/parser/SelectScheduleCommandParser.java b/src/main/java/seedu/address/logic/parser/SelectScheduleCommandParser.java new file mode 100644 index 000000000000..1808f259699a --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/SelectScheduleCommandParser.java @@ -0,0 +1,28 @@ +package seedu.address.logic.parser; + +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; + +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.SelectScheduleCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new SelectScheduleCommand object + */ +public class SelectScheduleCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the SelectScheduleCommand + * and returns an SelectScheduleCommand object for execution. + * @throws ParseException if the user input does not conform the expected format + */ + public SelectScheduleCommand parse(String args) throws ParseException { + try { + Index index = ParserUtil.parseIndex(args); + return new SelectScheduleCommand(index); + } catch (ParseException pe) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, SelectScheduleCommand.MESSAGE_USAGE), pe); + } + } +} diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index ac4521f33199..9937a30c6b79 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -1,78 +1,151 @@ package seedu.address.model; +import java.util.Set; import java.util.function.Predicate; import javafx.collections.ObservableList; +import javafx.collections.transformation.FilteredList; +import seedu.address.model.addressbook.ReadOnlyAddressBook; +import seedu.address.model.expenses.Expenses; +import seedu.address.model.expenses.ReadOnlyExpensesList; import seedu.address.model.person.Person; +import seedu.address.model.recruitment.ReadOnlyRecruitmentList; +import seedu.address.model.recruitment.Recruitment; +import seedu.address.model.schedule.ReadOnlyScheduleList; +import seedu.address.model.schedule.Schedule; /** * The API of the Model component. */ public interface Model { + /** {@code Predicate} that always evaluate to true */ + Predicate PREDICATE_SHOW_ALL_EXPENSES = unused -> true; + /** {@code Predicate} that always evaluate to true */ Predicate PREDICATE_SHOW_ALL_PERSONS = unused -> true; + /** {@code Predicate} that always evaluate to true */ + Predicate PREDICATE_SHOW_ALL_SCHEDULES = unused -> true; + + /** {@code Predicate} that always evaluate to true */ + Predicate PREDICATE_SHOW_ALL_RECRUITMENT = unused -> true; + + /** Checks if model can be redo or undo */ + boolean canRedoModel(); + boolean canUndoModel(); + + /** Get the last or next commit storage type */ + Set getNextCommitType(); + Set getLastCommitType(); + /** Clears existing backing model and replaces with the provided new data. */ - void resetData(ReadOnlyAddressBook newData); + void resetAddressBookData(ReadOnlyAddressBook newData); + void resetDataExpenses(ReadOnlyExpensesList newData); + void resetScheduleListData(ReadOnlyScheduleList newData); + void resetRecruitmentListData(ReadOnlyRecruitmentList newData); /** Returns the AddressBook */ ReadOnlyAddressBook getAddressBook(); + ReadOnlyExpensesList getExpensesList(); + ReadOnlyScheduleList getScheduleList(); + ReadOnlyRecruitmentList getRecruitmentList(); /** * Returns true if a person with the same identity as {@code person} exists in the address book. */ + boolean hasExpenses(Expenses expenses); boolean hasPerson(Person person); + boolean hasPerson(Person person, Predicate predicate); + boolean hasSchedule(Schedule schedule); + boolean hasRecruitment(Recruitment recruitment); + boolean hasEmployeeId(Person person); /** * Deletes the given person. * The person must exist in the address book. */ + void deleteExpenses(Expenses target); void deletePerson(Person target); + void deleteSchedule(Schedule target); + void deleteRecruitmentPost(Recruitment target); /** * Adds the given person. * {@code person} must not already exist in the address book. */ + void addExpenses (Expenses expenses); void addPerson(Person person); + void addSchedule(Schedule schedule); + void addRecruitment(Recruitment recruitment); /** * Replaces the given person {@code target} with {@code editedPerson}. * {@code target} must exist in the address book. * The person identity of {@code editedPerson} must not be the same as another existing person in the address book. */ + void updateExpenses(Expenses target, Expenses editedExpenses); void updatePerson(Person target, Person editedPerson); + void updateSchedule(Schedule target, Schedule editedSchedule); + void updateRecruitment(Recruitment target, Recruitment editedSchedule); /** Returns an unmodifiable view of the filtered person list */ + ObservableList getFilteredExpensesList(); ObservableList getFilteredPersonList(); + ObservableList getFilteredPersonList(FilteredList dummyFilteredPersons); + ObservableList getFilteredScheduleList(); + ObservableList getFilteredRecruitmentList(); /** * Updates the filter of the filtered person list to filter by the given {@code predicate}. * @throws NullPointerException if {@code predicate} is null. */ + void updateFilteredExpensesList(Predicate predicate); void updateFilteredPersonList(Predicate predicate); + void updateFilteredPersonList(Predicate predicate, String sortOrder); + void updateFilteredScheduleList(Predicate predicate); + void updateFilteredRecruitmentList(Predicate predicate); /** * Returns true if the model has previous address book states to restore. */ boolean canUndoAddressBook(); + boolean canUndoExpensesList(); + boolean canUndoScheduleList(); + boolean canUndoRecruitmentList(); /** * Returns true if the model has undone address book states to restore. */ boolean canRedoAddressBook(); + boolean canRedoExpensesList(); + boolean canRedoScheduleList(); + boolean canRedoRecruitmentList(); /** * Restores the model's address book to its previous state. */ void undoAddressBook(); + void undoExpensesList(); + void undoScheduleList(); + void undoRecruitmentList(); + void undoModelList(); /** * Restores the model's address book to its previously undone state. */ void redoAddressBook(); + void redoExpensesList(); + void redoScheduleList(); + void redoRecruitmentList(); + void redoModelList(); /** * Saves the current address book state for undo/redo. */ void commitAddressBook(); + void commitExpensesList(); + void commitScheduleList(); + void commitRecruitmentPostList(); + void commitMultipleLists(Set stack); + } diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index a664602ef5b1..9ba654503194 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/seedu/address/model/ModelManager.java @@ -3,6 +3,7 @@ import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import java.util.Set; import java.util.function.Predicate; import java.util.logging.Logger; @@ -12,123 +13,518 @@ import seedu.address.commons.core.ComponentManager; import seedu.address.commons.core.LogsCenter; import seedu.address.commons.events.model.AddressBookChangedEvent; +import seedu.address.commons.events.model.ExpensesListChangedEvent; +import seedu.address.commons.events.model.RecruitmentListChangedEvent; +import seedu.address.commons.events.model.ScheduleListChangedEvent; +import seedu.address.model.addressbook.AddressBook; +import seedu.address.model.addressbook.ReadOnlyAddressBook; +import seedu.address.model.addressbook.VersionedAddressBook; +import seedu.address.model.expenses.Expenses; +import seedu.address.model.expenses.ExpensesList; +import seedu.address.model.expenses.ReadOnlyExpensesList; +import seedu.address.model.expenses.VersionedExpensesList; import seedu.address.model.person.Person; +import seedu.address.model.recruitment.ReadOnlyRecruitmentList; +import seedu.address.model.recruitment.Recruitment; +import seedu.address.model.recruitment.RecruitmentList; +import seedu.address.model.recruitment.VersionedRecruitmentList; +import seedu.address.model.schedule.ReadOnlyScheduleList; +import seedu.address.model.schedule.Schedule; +import seedu.address.model.schedule.ScheduleList; +import seedu.address.model.schedule.VersionedScheduleList; /** * Represents the in-memory model of the address book data. */ public class ModelManager extends ComponentManager implements Model { private static final Logger logger = LogsCenter.getLogger(ModelManager.class); + private static final String ASCENDING_ORDER = "asc"; + private final VersionedModelList versionedModelList; private final VersionedAddressBook versionedAddressBook; + private final VersionedExpensesList versionedExpensesList; + private final VersionedScheduleList versionedScheduleList; + private final VersionedRecruitmentList versionedRecruitmentList; private final FilteredList filteredPersons; + private final FilteredList filteredExpenses; + private final FilteredList filteredSchedules; + private final FilteredList filteredRecruitment; /** * Initializes a ModelManager with the given addressBook and userPrefs. */ - public ModelManager(ReadOnlyAddressBook addressBook, UserPrefs userPrefs) { + public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyExpensesList expensesList, + ReadOnlyScheduleList scheduleList, + ReadOnlyRecruitmentList recruitmentList, + UserPrefs userPrefs) { super(); requireAllNonNull(addressBook, userPrefs); logger.fine("Initializing with address book: " + addressBook + " and user prefs " + userPrefs); versionedAddressBook = new VersionedAddressBook(addressBook); + versionedExpensesList = new VersionedExpensesList(expensesList); + versionedScheduleList = new VersionedScheduleList(scheduleList); + versionedRecruitmentList = new VersionedRecruitmentList(recruitmentList); + filteredExpenses = new FilteredList<>(versionedExpensesList.getExpensesRequestList()); filteredPersons = new FilteredList<>(versionedAddressBook.getPersonList()); + filteredSchedules = new FilteredList<>(versionedScheduleList.getScheduleList()); + filteredRecruitment = new FilteredList<>(versionedRecruitmentList.getRecruitmentList()); + versionedModelList = new VersionedModelList(); + } public ModelManager() { - this(new AddressBook(), new UserPrefs()); + this(new AddressBook(), new ExpensesList(), new ScheduleList(), new RecruitmentList(), new UserPrefs()); + } + + public boolean canRedoModel() { + return versionedModelList.canRedoStorage(); + } + public boolean canUndoModel() { + return versionedModelList.canUndoStorage(); + } + public Set getNextCommitType() { + return versionedModelList.getNextCommitType(); + } + public Set getLastCommitType() { + return versionedModelList.getLastCommitType(); } + //----------------------------------------------------------------------------- @Override - public void resetData(ReadOnlyAddressBook newData) { + public void resetAddressBookData(ReadOnlyAddressBook newData) { versionedAddressBook.resetData(newData); indicateAddressBookChanged(); } + @Override + public void resetDataExpenses(ReadOnlyExpensesList newData) { + versionedExpensesList.resetData(newData); + indicateExpensesListChanged(); + } + + @Override + public void resetScheduleListData(ReadOnlyScheduleList newData) { + versionedScheduleList.resetData(newData); + indicateScheduleListChanged(); + } + + @Override + public void resetRecruitmentListData(ReadOnlyRecruitmentList newData) { + versionedRecruitmentList.resetData(newData); + indicateRecruitmentListChanged(); + } + + //----------------------------------------------------------------------------- @Override public ReadOnlyAddressBook getAddressBook() { return versionedAddressBook; } + @Override + public ReadOnlyExpensesList getExpensesList() { + return versionedExpensesList; } + + @Override + public ReadOnlyScheduleList getScheduleList() { + return versionedScheduleList; } + + @Override + public ReadOnlyRecruitmentList getRecruitmentList() { + return versionedRecruitmentList; } + + //----------------------------------------------------------------------------- + /** Raises an event to indicate the model has changed */ private void indicateAddressBookChanged() { raise(new AddressBookChangedEvent(versionedAddressBook)); } + private void indicateExpensesListChanged() { + raise(new ExpensesListChangedEvent(versionedExpensesList)); } + + private void indicateScheduleListChanged() { + raise(new ScheduleListChangedEvent(versionedScheduleList)); + } + + private void indicateRecruitmentListChanged() { + raise(new RecruitmentListChangedEvent(versionedRecruitmentList)); + } + + + //----------------------------------------------------------------------------- + @Override + public boolean hasExpenses(Expenses expenses) { + requireNonNull(expenses); + return versionedExpensesList.hasExpenses(expenses); + } + @Override public boolean hasPerson(Person person) { requireNonNull(person); return versionedAddressBook.hasPerson(person); } + @Override + public boolean hasPerson(Person person, Predicate predicate) { + requireAllNonNull(person, predicate); + final FilteredList dummyFilteredPersons = + new FilteredList<>(versionedAddressBook.getPersonList()); + dummyFilteredPersons.setPredicate(predicate); + return versionedAddressBook.hasPerson(person, getFilteredPersonList(dummyFilteredPersons)); + } + + @Override + public boolean hasEmployeeId(Person person) { + requireNonNull(person); + return versionedAddressBook.hasEmployeeId(person); + } + + @Override + public boolean hasSchedule(Schedule target) { + requireNonNull(target); + return versionedScheduleList.hasSchedule(target); + } + + @Override + public boolean hasRecruitment(Recruitment target) { + requireNonNull(target); + return versionedRecruitmentList.hasRecruitment(target); + } + + //----------------------------------------------------------------------------- + @Override + public void deleteExpenses(Expenses target) { + versionedExpensesList.removeExpenses(target); + versionedExpensesList.sortExpensesBy(); + indicateExpensesListChanged(); + } + @Override public void deletePerson(Person target) { versionedAddressBook.removePerson(target); + versionedAddressBook.sortEmployeesBy(ASCENDING_ORDER); indicateAddressBookChanged(); } + @Override + public void deleteSchedule(Schedule target) { + versionedScheduleList.removeSchedule(target); + versionedScheduleList.sortSchedulesBy(); + indicateScheduleListChanged(); + } + + @Override + public void deleteRecruitmentPost(Recruitment target) { + versionedRecruitmentList.removeRecruitment(target); + indicateRecruitmentListChanged(); + } + + + //----------------------------------------------------------------------------- + @Override + public void addExpenses(Expenses expenses) { + versionedExpensesList.addExpenses(expenses); + updateFilteredExpensesList(PREDICATE_SHOW_ALL_EXPENSES); + versionedExpensesList.sortExpensesBy(); + indicateExpensesListChanged(); + } + + @Override public void addPerson(Person person) { versionedAddressBook.addPerson(person); updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + updateFilteredExpensesList(PREDICATE_SHOW_ALL_EXPENSES); + updateFilteredScheduleList(PREDICATE_SHOW_ALL_SCHEDULES); + updateFilteredRecruitmentList(PREDICATE_SHOW_ALL_RECRUITMENT); + versionedAddressBook.sortEmployeesBy(ASCENDING_ORDER); indicateAddressBookChanged(); } + @Override + public void addSchedule(Schedule schedule) { + versionedScheduleList.addSchedule(schedule); + updateFilteredScheduleList(PREDICATE_SHOW_ALL_SCHEDULES); + versionedScheduleList.sortSchedulesBy(); + indicateScheduleListChanged(); + } + + @Override + public void addRecruitment(Recruitment recruitment) { + versionedRecruitmentList.addRecruitment(recruitment); + updateFilteredRecruitmentList(PREDICATE_SHOW_ALL_RECRUITMENT); + indicateRecruitmentListChanged(); + } + + //----------------------------------------------------------------------------- + @Override + public void updateExpenses(Expenses target, Expenses editedExpenses) { + requireAllNonNull(target, editedExpenses); + versionedExpensesList.updateExpenses(target, editedExpenses); + versionedExpensesList.sortExpensesBy(); + indicateExpensesListChanged(); + } + @Override public void updatePerson(Person target, Person editedPerson) { requireAllNonNull(target, editedPerson); - versionedAddressBook.updatePerson(target, editedPerson); + versionedAddressBook.sortEmployeesBy(ASCENDING_ORDER); indicateAddressBookChanged(); } + @Override + public void updateSchedule(Schedule target, Schedule editedSchedule) { + requireAllNonNull(target, editedSchedule); + versionedScheduleList.updateSchedule(target, editedSchedule); + versionedScheduleList.sortSchedulesBy(); + indicateScheduleListChanged(); + } + + @Override + public void updateRecruitment(Recruitment target, Recruitment editedRecruitment) { + requireAllNonNull(target, editedRecruitment); + + versionedRecruitmentList.updateRecruitment(target, editedRecruitment); + indicateRecruitmentListChanged(); + } + //=========== Filtered Person List Accessors ============================================================= /** * Returns an unmodifiable view of the list of {@code Person} backed by the internal list of * {@code versionedAddressBook} */ + @Override + public ObservableList getFilteredExpensesList() { + return FXCollections.unmodifiableObservableList(filteredExpenses); + } + @Override public ObservableList getFilteredPersonList() { return FXCollections.unmodifiableObservableList(filteredPersons); } + @Override + public ObservableList getFilteredPersonList(FilteredList dummyFilteredPersons) { + return FXCollections.unmodifiableObservableList(dummyFilteredPersons); + } + + @Override + public ObservableList getFilteredScheduleList() { + return FXCollections.unmodifiableObservableList(filteredSchedules); + } + + @Override + public ObservableList getFilteredRecruitmentList() { + return FXCollections.unmodifiableObservableList(filteredRecruitment); + } + + //=========== Filtered Person List Accessors ============================================================= + @Override + public void updateFilteredExpensesList(Predicate predicate) { + requireNonNull(predicate); + versionedExpensesList.sortExpensesBy(); + filteredExpenses.setPredicate(predicate); + } + @Override public void updateFilteredPersonList(Predicate predicate) { requireNonNull(predicate); + versionedAddressBook.sortEmployeesBy(ASCENDING_ORDER); + filteredPersons.setPredicate(predicate); + } + + @Override + public void updateFilteredPersonList(Predicate predicate, String sortOrder) { + requireNonNull(predicate); + versionedAddressBook.sortEmployeesBy(sortOrder); filteredPersons.setPredicate(predicate); } - //=========== Undo/Redo ================================================================================= + @Override + public void updateFilteredScheduleList(Predicate predicate) { + requireNonNull(predicate); + versionedScheduleList.sortSchedulesBy(); + filteredSchedules.setPredicate(predicate); + } + + @Override + public void updateFilteredRecruitmentList(Predicate predicate) { + requireNonNull(predicate); + filteredRecruitment.setPredicate(predicate); + } + + + //=========== Undo ================================================================================= @Override public boolean canUndoAddressBook() { return versionedAddressBook.canUndo(); } + @Override + public boolean canUndoExpensesList() { + return versionedExpensesList.canUndo(); + } + + @Override + public boolean canUndoScheduleList() { + return versionedScheduleList.canUndo(); + } + + @Override + public boolean canUndoRecruitmentList() { + return versionedRecruitmentList.canUndo(); + } + + //=========== Redo ================================================================================= @Override public boolean canRedoAddressBook() { return versionedAddressBook.canRedo(); } + @Override + public boolean canRedoExpensesList() { + return versionedExpensesList.canRedo(); + } + + @Override + public boolean canRedoScheduleList() { + return versionedScheduleList.canRedo(); + } + + @Override + public boolean canRedoRecruitmentList() { + return versionedRecruitmentList.canRedo(); + } + + //----------------------------------------------------------------------------- + @Override public void undoAddressBook() { versionedAddressBook.undo(); indicateAddressBookChanged(); } + @Override + public void undoExpensesList() { + versionedExpensesList.undo(); + indicateExpensesListChanged(); + } + + @Override + public void undoScheduleList() { + versionedScheduleList.undo(); + indicateScheduleListChanged(); + } + + @Override + public void undoRecruitmentList() { + versionedRecruitmentList.undo(); + indicateRecruitmentListChanged(); + } + + @Override + public void undoModelList() { + versionedModelList.undo(); + } + + + //----------------------------------------------------------------------------- @Override public void redoAddressBook() { versionedAddressBook.redo(); indicateAddressBookChanged(); } + @Override + public void redoExpensesList() { + versionedExpensesList.redo(); + indicateExpensesListChanged(); + } + + @Override + public void redoScheduleList() { + versionedScheduleList.redo(); + indicateScheduleListChanged(); + } + + @Override + public void redoRecruitmentList() { + versionedRecruitmentList.redo(); + indicateRecruitmentListChanged(); + } + + @Override + public void redoModelList() { + versionedModelList.redo(); + } + + //----------------------------------------------------------------------------- + /** + * Commits the address book storage and sets the last commit storage type + */ @Override public void commitAddressBook() { versionedAddressBook.commit(); + versionedModelList.add(ModelTypes.ADDRESS_BOOK); + } + + /** + * Commits the expenses list storage and sets the last commit storage type + */ + public void commitExpensesList() { + versionedExpensesList.commit(); + versionedModelList.add(ModelTypes.EXPENSES_LIST); } + /** + * Commits the schedule list storage and sets the last commit storage type + */ + public void commitScheduleList() { + versionedScheduleList.commit(); + versionedModelList.add(ModelTypes.SCHEDULES_LIST); + } + + /** + * Commits the recruitment list storage and sets the last commit storage type + */ + public void commitRecruitmentPostList() { + versionedRecruitmentList.commit(); + versionedModelList.add(ModelTypes.RECRUITMENT_LIST); + } + + /** + * Commits the multiple storages list and sets the commit storage type + */ + public void commitMultipleLists(Set set) { + + for (ModelTypes myModel : set) { + switch(myModel) { + case SCHEDULES_LIST: + versionedScheduleList.commit(); + break; + case EXPENSES_LIST: + versionedExpensesList.commit(); + break; + case RECRUITMENT_LIST: + versionedRecruitmentList.commit(); + break; + case ADDRESS_BOOK: + versionedAddressBook.commit(); + break; + default: + break; + } + } + versionedModelList.addMultiple(set); + } + + //----------------------------------------------------------------------------- + @Override public boolean equals(Object obj) { // short circuit if same object @@ -144,7 +540,13 @@ public boolean equals(Object obj) { // state check ModelManager other = (ModelManager) obj; return versionedAddressBook.equals(other.versionedAddressBook) - && filteredPersons.equals(other.filteredPersons); + && filteredPersons.equals(other.filteredPersons) + && versionedRecruitmentList.equals(other.versionedRecruitmentList) + && filteredRecruitment.equals(other.filteredRecruitment) + && versionedScheduleList.equals(other.versionedScheduleList) + && filteredSchedules.equals(other.filteredSchedules) + && versionedExpensesList.equals(other.versionedExpensesList) + && filteredExpenses.equals(other.filteredExpenses); } } diff --git a/src/main/java/seedu/address/model/ModelTypes.java b/src/main/java/seedu/address/model/ModelTypes.java new file mode 100644 index 000000000000..724a3d5ef719 --- /dev/null +++ b/src/main/java/seedu/address/model/ModelTypes.java @@ -0,0 +1,8 @@ +package seedu.address.model; + +/** + * All the storage model types available + */ +public enum ModelTypes { + SCHEDULES_LIST, EXPENSES_LIST, RECRUITMENT_LIST, ADDRESS_BOOK +} diff --git a/src/main/java/seedu/address/model/UserPrefs.java b/src/main/java/seedu/address/model/UserPrefs.java index 980b2b388852..a8c6d877147c 100644 --- a/src/main/java/seedu/address/model/UserPrefs.java +++ b/src/main/java/seedu/address/model/UserPrefs.java @@ -12,10 +12,15 @@ public class UserPrefs { private GuiSettings guiSettings; + + //----------------------------------------------- private Path addressBookFilePath = Paths.get("data" , "addressbook.xml"); + private Path expensesListFilePath = Paths.get("data" , "expenses.xml"); + private Path scheduleListFilePath = Paths.get("data" , "schedulelist.xml"); + private Path recruitmentListFilePath = Paths.get("data" , "recruitmentlist.xml"); public UserPrefs() { - setGuiSettings(500, 500, 0, 0); + setGuiSettings(1200, 500, 0, 0); } public GuiSettings getGuiSettings() { @@ -30,14 +35,32 @@ public void setGuiSettings(double width, double height, int x, int y) { guiSettings = new GuiSettings(width, height, x, y); } + //----------------------------------------------- public Path getAddressBookFilePath() { return addressBookFilePath; } + public Path getExpensesListFilePath() { + return expensesListFilePath; } + public Path getScheduleListFilePath() { + return scheduleListFilePath; + } + public Path getRecruitmentListFilePath() { + return recruitmentListFilePath; + } public void setAddressBookFilePath(Path addressBookFilePath) { this.addressBookFilePath = addressBookFilePath; } + public void setExpensesListFilePath(Path expensesListFilePath) { + this.expensesListFilePath = expensesListFilePath; } + public void setScheduleListFilePath(Path scheduleListFilePath) { + this.scheduleListFilePath = scheduleListFilePath; + } + public void setRecruitmentListFilePath(Path recruitmentListFilePath) { + this.recruitmentListFilePath = recruitmentListFilePath; + } + //----------------------------------------------- @Override public boolean equals(Object other) { if (other == this) { diff --git a/src/main/java/seedu/address/model/VersionedModelList.java b/src/main/java/seedu/address/model/VersionedModelList.java new file mode 100644 index 000000000000..083283e80651 --- /dev/null +++ b/src/main/java/seedu/address/model/VersionedModelList.java @@ -0,0 +1,129 @@ +package seedu.address.model; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +/** + * {@code VersionedModelList} keeps track of a list of models committed defined in ModelTypes.java. + */ + +public class VersionedModelList { + public static final String MESSAGE_NO_REDOABLE_STATE_EXCEPTION = + "Current state pointer at start of storage list in all Storages, unable to redo."; + public static final String MESSAGE_NO_UNDOABLE_STATE_EXCEPTION = + "Current state pointer at start of storage list in all Storages, unable to undo."; + private boolean hasUndo; + private int currentStatePointer; + private List> modelTypesStateList; + + public VersionedModelList() { + currentStatePointer = 0; + modelTypesStateList = new ArrayList<>(); + hasUndo = false; + } + + //----------------------------------------------------------------------------- + + /** + * Undo Versioned Model List + */ + public void undo() { + if (!canUndoStorage()) { + throw new NoUndoableStateException(); + } + hasUndo = true; + currentStatePointer--; + } + + /** + * Redo Versioned Model List + */ + public void redo() { + if (!canRedoStorage()) { + throw new NoRedoableStateException(); + } + hasUndo = false; + currentStatePointer++; + } + + /** + * Add to the list to keep track of which model is committed. + * @param type of storage model committed + */ + public void add (ModelTypes type) { + Set set = new HashSet<>(); + set.add(type); + modelTypesStateList.add(set); + if (hasUndo) { + modelTypesStateList.remove(currentStatePointer); + hasUndo = false; + } + currentStatePointer = modelTypesStateList.size(); + } + + /** + * Add to the list to keep track of which model is committed. + * @param set of the types of storage models committed + */ + public void addMultiple (Set set) { + modelTypesStateList.add(set); + if (hasUndo) { + modelTypesStateList.remove(currentStatePointer); + hasUndo = false; + } + currentStatePointer = modelTypesStateList.size(); + } + + /** + * Used to check which model was last committed with a command. + * Important for undo and redo class. + */ + public Set getLastCommitType () { + if (!canUndoStorage()) { + throw new NoUndoableStateException(); + } + return modelTypesStateList.get(currentStatePointer - 1); + } + + public Set getNextCommitType () { + if (!canRedoStorage()) { + throw new NoRedoableStateException(); + } + return modelTypesStateList.get(currentStatePointer); + } + + /** + * Returns true if {@code redo()} has states to redo in any of the model. + */ + public boolean canRedoStorage() { + return currentStatePointer < modelTypesStateList.size(); + } + + /** + * Returns true if {@code undo()} has states to undo in any of the model. + */ + public boolean canUndoStorage() { + return currentStatePointer > 0; + } + + + /** + * Thrown when trying to {@code undo()} but can't. + */ + public static class NoUndoableStateException extends RuntimeException { + private NoUndoableStateException() { + super(MESSAGE_NO_UNDOABLE_STATE_EXCEPTION); + } + } + + /** + * Thrown when trying to {@code redo()} but can't. + */ + public static class NoRedoableStateException extends RuntimeException { + private NoRedoableStateException() { + super(MESSAGE_NO_REDOABLE_STATE_EXCEPTION); + } + } +} diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/addressbook/AddressBook.java similarity index 77% rename from src/main/java/seedu/address/model/AddressBook.java rename to src/main/java/seedu/address/model/addressbook/AddressBook.java index 7f85c8b9258b..3c96f0094a79 100644 --- a/src/main/java/seedu/address/model/AddressBook.java +++ b/src/main/java/seedu/address/model/addressbook/AddressBook.java @@ -1,4 +1,4 @@ -package seedu.address.model; +package seedu.address.model.addressbook; import static java.util.Objects.requireNonNull; @@ -66,6 +66,25 @@ public boolean hasPerson(Person person) { return persons.contains(person); } + /** + * Returns true if a person with the same identity as {@code person} exists in the address book. + * However, it does not check itself, it checks the rest of the address book (for edit command). + */ + public boolean hasPerson(Person person, ObservableList filteredPersonList) { + requireNonNull(person); + requireNonNull(filteredPersonList); + return persons.contains(person, filteredPersonList); + } + + + /** + * Returns true if a person with the same identity as {@code person} exists in the address book. + */ + public boolean hasEmployeeId(Person person) { + requireNonNull(person); + return persons.containsEmployeeId(person); + } + /** * Adds a person to the address book. * The person must not already exist in the address book. @@ -81,7 +100,6 @@ public void addPerson(Person p) { */ public void updatePerson(Person target, Person editedPerson) { requireNonNull(editedPerson); - persons.setPerson(target, editedPerson); } @@ -93,6 +111,14 @@ public void removePerson(Person key) { persons.remove(key); } + /** + * Sort Employees within CHRS by name + * Sorting will be done in either ascending or descending order based on user's input + */ + public void sortEmployeesBy(String sortOrder) { + persons.sortByName(sortOrder); + } + //// util methods @Override diff --git a/src/main/java/seedu/address/model/addressbook/DayHourGreeting.java b/src/main/java/seedu/address/model/addressbook/DayHourGreeting.java new file mode 100644 index 000000000000..70e2ddd4c38b --- /dev/null +++ b/src/main/java/seedu/address/model/addressbook/DayHourGreeting.java @@ -0,0 +1,49 @@ +package seedu.address.model.addressbook; + +import java.util.Calendar; + +/** + * DayHourGreeting is used to get the greeting message based on current time. + * E.g Good Morning, afternoon, evening, night + */ +public class DayHourGreeting { + + public static final String AFTERNOON = "Good Morning"; + public static final String EVENING = "Good Evening"; + public static final String MORNING = "Good Morning"; + public static final String NIGHT = "Good Night"; + + private static String dayHourGreeting; + + /** + * Time of the day based on Operating system clock, sets the greeting message based on current time. + */ + public DayHourGreeting () { + Calendar c = Calendar.getInstance(); + int hourOfDay = c.get(Calendar.HOUR_OF_DAY); + setGreeting(hourOfDay); + } + + /** + * Sets greeting based on hour of the day. + */ + public static void setGreeting(int hourOfDay) { + if (hourOfDay >= 0 && hourOfDay < 12) { + dayHourGreeting = MORNING; + } else if (hourOfDay >= 12 && hourOfDay < 16) { + dayHourGreeting = AFTERNOON; + } else if (hourOfDay >= 16 && hourOfDay < 21) { + dayHourGreeting = EVENING; + } else if (hourOfDay >= 21 && hourOfDay < 24) { + dayHourGreeting = NIGHT; + } + } + + + /** + * Returns greeting to user. + */ + public static String getGreeting() { + return dayHourGreeting; + } +} diff --git a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java b/src/main/java/seedu/address/model/addressbook/ReadOnlyAddressBook.java similarity index 89% rename from src/main/java/seedu/address/model/ReadOnlyAddressBook.java rename to src/main/java/seedu/address/model/addressbook/ReadOnlyAddressBook.java index 6ddc2cd9a290..25289b49061e 100644 --- a/src/main/java/seedu/address/model/ReadOnlyAddressBook.java +++ b/src/main/java/seedu/address/model/addressbook/ReadOnlyAddressBook.java @@ -1,4 +1,4 @@ -package seedu.address.model; +package seedu.address.model.addressbook; import javafx.collections.ObservableList; import seedu.address.model.person.Person; diff --git a/src/main/java/seedu/address/model/VersionedAddressBook.java b/src/main/java/seedu/address/model/addressbook/VersionedAddressBook.java similarity index 98% rename from src/main/java/seedu/address/model/VersionedAddressBook.java rename to src/main/java/seedu/address/model/addressbook/VersionedAddressBook.java index 227a335045d7..a1c1e6f03616 100644 --- a/src/main/java/seedu/address/model/VersionedAddressBook.java +++ b/src/main/java/seedu/address/model/addressbook/VersionedAddressBook.java @@ -1,4 +1,4 @@ -package seedu.address.model; +package seedu.address.model.addressbook; import java.util.ArrayList; import java.util.List; diff --git a/src/main/java/seedu/address/model/expenses/EmployeeIdExpensesContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/expenses/EmployeeIdExpensesContainsKeywordsPredicate.java new file mode 100644 index 000000000000..943bcaef8eac --- /dev/null +++ b/src/main/java/seedu/address/model/expenses/EmployeeIdExpensesContainsKeywordsPredicate.java @@ -0,0 +1,32 @@ +package seedu.address.model.expenses; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.StringUtil; + +/** + * Tests that a {@code Expenses}'s {@code Employee_Id} matches any of the Id given. + */ + + +public class EmployeeIdExpensesContainsKeywordsPredicate implements Predicate { + private final List keywords; + + public EmployeeIdExpensesContainsKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Expenses expenses) { + return keywords.stream() + .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(expenses.getEmployeeId().value, keyword)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof EmployeeIdExpensesContainsKeywordsPredicate // instanceof handles nulls + && keywords.equals(((EmployeeIdExpensesContainsKeywordsPredicate) other).keywords)); // state check + } +} diff --git a/src/main/java/seedu/address/model/expenses/Expenses.java b/src/main/java/seedu/address/model/expenses/Expenses.java new file mode 100644 index 000000000000..aeaeb5b76142 --- /dev/null +++ b/src/main/java/seedu/address/model/expenses/Expenses.java @@ -0,0 +1,109 @@ +package seedu.address.model.expenses; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Objects; + +import seedu.address.model.person.EmployeeId; + +/** + * Represents an Expenses of an employee in the expensesList. + * Guarantees: details are present and not null, field values are validated, immutable. + */ +public class Expenses { + + // Identity fields + private final EmployeeId id; + + // Data fields + private final ExpensesAmount expensesAmount; + private final TravelExpenses travelExpenses; + private final MedicalExpenses medicalExpenses; + private final MiscellaneousExpenses miscellaneousExpenses; + + /** + * Every field must be present and not null. + */ + public Expenses(EmployeeId id, ExpensesAmount expensesAmount, TravelExpenses travelExpenses, + MedicalExpenses medicalExpenses, MiscellaneousExpenses miscellaneousExpenses) { + requireAllNonNull(id, expensesAmount, travelExpenses, medicalExpenses, miscellaneousExpenses); + this.id = id; + this.expensesAmount = expensesAmount; + this.travelExpenses = travelExpenses; + this.medicalExpenses = medicalExpenses; + this.miscellaneousExpenses = miscellaneousExpenses; + } + + public EmployeeId getEmployeeId() { + return id; + } + + public ExpensesAmount getExpensesAmount() { + return expensesAmount; + } + + public TravelExpenses getTravelExpenses() { + return travelExpenses; + } + + public MedicalExpenses getMedicalExpenses() { + return medicalExpenses; + } + + public MiscellaneousExpenses getMiscellaneousExpenses() { + return miscellaneousExpenses; + } + + /** + * Returns true if both persons of the same id have expenses that is the same. + * This defines a weaker notion of equality between two persons. + */ + public boolean isSameExpensesRequest(Expenses otherExpenses) { + if (otherExpenses == this) { + return true; + } + + return otherExpenses != null + && otherExpenses.getEmployeeId().equals(getEmployeeId()); + } + + /** + * Returns true if both persons have the same identity and data fields. + * This defines a stronger notion of equality between two persons. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Expenses)) { + return false; + } + + Expenses otherExpenses = (Expenses) other; + return otherExpenses.getEmployeeId().equals(getEmployeeId()); + } + + @Override + public int hashCode() { + // use this method for custom fields hashing instead of implementing your own + return Objects.hash(expensesAmount, travelExpenses, medicalExpenses, miscellaneousExpenses); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append(getEmployeeId()) + .append(" Expenses Amount: ") + .append(getExpensesAmount()) + .append(" Travel Expenses: ") + .append(getTravelExpenses()) + .append(" Medical Expenses: ") + .append(getMedicalExpenses()) + .append(" Miscellaneous Expenses: ") + .append(getMiscellaneousExpenses()); + return builder.toString(); + } + +} diff --git a/src/main/java/seedu/address/model/expenses/ExpensesAmount.java b/src/main/java/seedu/address/model/expenses/ExpensesAmount.java new file mode 100644 index 000000000000..c9dda37e7151 --- /dev/null +++ b/src/main/java/seedu/address/model/expenses/ExpensesAmount.java @@ -0,0 +1,53 @@ +package seedu.address.model.expenses; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Person's Expenses Amount in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidExpensesAmount(String)} + */ +public class ExpensesAmount { + + public static final String MESSAGE_EXPENSES_AMOUNT_CONSTRAINTS = + "Expenses Amount should only contain numbers, and it should be at least 1 digits long"; + public static final String EMPLOYE_EXPENSES_AMOUNT_VALIDATION_REGEX = "[-]?[0-9]+([.][0-9]{1,2})?"; + + public final String expensesAmount; + + /** + * Constructs a {@code ExpensesAmount}. + * + * @param expensesAmount A valid Expenses. + */ + public ExpensesAmount(String expensesAmount) { + requireNonNull(expensesAmount); + checkArgument(isValidExpensesAmount(expensesAmount), MESSAGE_EXPENSES_AMOUNT_CONSTRAINTS); + this.expensesAmount = expensesAmount; + } + + /** + * Returns true if a given string is a valid Expenses Amount. + */ + public static boolean isValidExpensesAmount(String test) { + return test.matches(EMPLOYE_EXPENSES_AMOUNT_VALIDATION_REGEX); + } + + @Override + public String toString() { + return expensesAmount; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ExpensesAmount // instanceof handles nulls + && expensesAmount.equals(((ExpensesAmount) other).expensesAmount)); // state check + } + + @Override + public int hashCode() { + return expensesAmount.hashCode(); + } + +} diff --git a/src/main/java/seedu/address/model/expenses/ExpensesList.java b/src/main/java/seedu/address/model/expenses/ExpensesList.java new file mode 100644 index 000000000000..550855aee740 --- /dev/null +++ b/src/main/java/seedu/address/model/expenses/ExpensesList.java @@ -0,0 +1,127 @@ +package seedu.address.model.expenses; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import javafx.collections.ObservableList; + +/** + * Wraps all data at the address-book level + * Duplicates are not allowed (by .isSameEmployeeId comparison) + */ +public class ExpensesList implements ReadOnlyExpensesList { + + private final UniqueExpensesList multiExpenses; + + /* + * The 'unusual' code block below is an non-static initialization block, sometimes used to avoid duplication + * between constructors. See https://docs.oracle.com/javase/tutorial/java/javaOO/initial.html + * + * Note that non-static init blocks are not recommended to use. There are other ways to avoid duplication + * among constructors. + */ + { + multiExpenses = new UniqueExpensesList(); + } + + public ExpensesList() {} + + /** + * Creates an AddressBook using the multiExpenses in the {@code toBeCopied} + */ + public ExpensesList(ReadOnlyExpensesList toBeCopied) { + this(); + resetData(toBeCopied); + } + + //// list overwrite operations + + /** + * Replaces the contents of the expenses list with {@code multiExpenses}. + * {@code multiExpenses} must not contain duplicate expenses. + */ + public void setMultiExpenses(List multiExpenses) { + this.multiExpenses.setMultiExpenses(multiExpenses); + } + + /** + * Resets the existing data of this {@code AddressBook} with {@code newData}. + */ + public void resetData(ReadOnlyExpensesList newData) { + requireNonNull(newData); + + setMultiExpenses(newData.getExpensesRequestList()); + } + + //// person-level operations + + /** + * Returns true if a person with the same identity as {@code expenses} exists in the address book. + */ + public boolean hasExpenses(Expenses expenses) { + requireNonNull(expenses); + return multiExpenses.contains(expenses); + } + + /** + * Adds an expenses to the address book. + * The employee expenses id must not already exist in the address book. + */ + public void addExpenses(Expenses e) { + multiExpenses.add(e); + } + + /** + * Replaces the given expenses {@code target} in the list with {@code editedExpenses}. + * {@code target} must exist in the expenses list. + * The person identity of {@code editedExpenses} must not be the same as another existing expenses in the expenses + * list. + */ + public void updateExpenses(Expenses target, Expenses editedExpenses) { + requireNonNull(editedExpenses); + + multiExpenses.setExpenses(target, editedExpenses); + } + + /** + * Removes {@code key} from this {@code ExpensesList}. + * {@code key} must exist in the expenses list. + */ + public void removeExpenses(Expenses key) { + multiExpenses.remove(key); + } + + /** + * Sort Expenses within CHRS by employeeId + */ + public void sortExpensesBy() { + multiExpenses.sortByEmployeeId(); + } + + + //// util methods + + @Override + public String toString() { + return multiExpenses.asUnmodifiableObservableList().size() + " multiExpenses"; + // TODO: refine later + } + + @Override + public ObservableList getExpensesRequestList() { + return multiExpenses.asUnmodifiableObservableList(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ExpensesList // instanceof handles nulls + && multiExpenses.equals(((ExpensesList) other).multiExpenses)); + } + + @Override + public int hashCode() { + return multiExpenses.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/expenses/MedicalExpenses.java b/src/main/java/seedu/address/model/expenses/MedicalExpenses.java new file mode 100644 index 000000000000..da4ef9b82606 --- /dev/null +++ b/src/main/java/seedu/address/model/expenses/MedicalExpenses.java @@ -0,0 +1,54 @@ +package seedu.address.model.expenses; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Person's Medical Expenses in the expenses list. + * Guarantees: immutable; is valid as declared in {@link #isValidMedicalExpenses(String)} + */ +public class MedicalExpenses { + + public static final String MESSAGE_MEDICAL_EXPENSES_CONSTRAINTS = + "Medical Expenses should only contain numbers, maximum of 6 whole numbers and 2 decimal points and " + + "minimum 1 digit long"; + public static final String EMPLOYEE_MEDICAL_EXPENSES_VALIDATION_REGEX = "[-]?[0-9]{1,6}+([.][0-9]{1,2})?"; + + public final String medicalExpenses; + + /** + * Constructs a {@code ExpensesAmount}. + * + * @param medicalExpenses A valid Expenses. + */ + public MedicalExpenses(String medicalExpenses) { + requireNonNull(medicalExpenses); + checkArgument(isValidMedicalExpenses(medicalExpenses), MESSAGE_MEDICAL_EXPENSES_CONSTRAINTS); + this.medicalExpenses = medicalExpenses; + } + + /** + * Returns true if a given string is a valid Expenses Amount. + */ + public static boolean isValidMedicalExpenses(String test) { + return test.matches(EMPLOYEE_MEDICAL_EXPENSES_VALIDATION_REGEX); + } + + @Override + public String toString() { + return medicalExpenses; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof MedicalExpenses // instanceof handles nulls + && medicalExpenses.equals(((MedicalExpenses) other).medicalExpenses)); // state check + } + + @Override + public int hashCode() { + return medicalExpenses.hashCode(); + } + +} diff --git a/src/main/java/seedu/address/model/expenses/MiscellaneousExpenses.java b/src/main/java/seedu/address/model/expenses/MiscellaneousExpenses.java new file mode 100644 index 000000000000..f47df78f6aaf --- /dev/null +++ b/src/main/java/seedu/address/model/expenses/MiscellaneousExpenses.java @@ -0,0 +1,54 @@ +package seedu.address.model.expenses; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Person's Miscellaneous Expenses in the expenses list. + * Guarantees: immutable; is valid as declared in {@link #isValidMiscellaneousExpenses(String)} + */ +public class MiscellaneousExpenses { + + public static final String MESSAGE_MISCELLANEOUS_EXPENSES_CONSTRAINTS = + "Miscellaneous Expenses should only contain numbers, maximum of 6 whole numbers and 2 decimal points and " + + "minimum 1 digit long"; + public static final String EMPLOYEE_MISCELLANEOUS_EXPENSES_VALIDATION_REGEX = "[-]?[0-9]{1,6}+([.][0-9]{1,2})?"; + + public final String miscellaneousExpenses; + + /** + * Constructs a {@code ExpensesAmount}. + * + * @param miscellaneousExpenses A valid Expenses. + */ + public MiscellaneousExpenses(String miscellaneousExpenses) { + requireNonNull(miscellaneousExpenses); + checkArgument(isValidMiscellaneousExpenses(miscellaneousExpenses), MESSAGE_MISCELLANEOUS_EXPENSES_CONSTRAINTS); + this.miscellaneousExpenses = miscellaneousExpenses; + } + + /** + * Returns true if a given string is a valid Expenses Amount. + */ + public static boolean isValidMiscellaneousExpenses(String test) { + return test.matches(EMPLOYEE_MISCELLANEOUS_EXPENSES_VALIDATION_REGEX); + } + + @Override + public String toString() { + return miscellaneousExpenses; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof MiscellaneousExpenses // instanceof handles nulls + && miscellaneousExpenses.equals(((MiscellaneousExpenses) other).miscellaneousExpenses)); // state check + } + + @Override + public int hashCode() { + return miscellaneousExpenses.hashCode(); + } + +} diff --git a/src/main/java/seedu/address/model/expenses/ReadOnlyExpensesList.java b/src/main/java/seedu/address/model/expenses/ReadOnlyExpensesList.java new file mode 100644 index 000000000000..5531d485ee4c --- /dev/null +++ b/src/main/java/seedu/address/model/expenses/ReadOnlyExpensesList.java @@ -0,0 +1,12 @@ +package seedu.address.model.expenses; +import javafx.collections.ObservableList; +/** + * Unmodifiable view of an Expenses list + */ +public interface ReadOnlyExpensesList { + /** + * Returns an unmodifiable view of the Expenses list. + * This list will not contain any duplicate Expenses. + */ + ObservableList getExpensesRequestList(); +} diff --git a/src/main/java/seedu/address/model/expenses/TravelExpenses.java b/src/main/java/seedu/address/model/expenses/TravelExpenses.java new file mode 100644 index 000000000000..92d08669b0e2 --- /dev/null +++ b/src/main/java/seedu/address/model/expenses/TravelExpenses.java @@ -0,0 +1,54 @@ +package seedu.address.model.expenses; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Person's Travel Expenses in the expenses list + * Guarantees: immutable; is valid as declared in {@link #isValidTravelExpenses(String)} + */ +public class TravelExpenses { + + public static final String MESSAGE_TRAVEL_EXPENSES_CONSTRAINTS = + "Travel Expenses should only contain numbers, maximum of 6 whole numbers and 2 decimal points and " + + "minimum 1 digit long"; + public static final String EMPLOYEE_TRAVEL_EXPENSES_VALIDATION_REGEX = "[-]?[0-9]{1,6}+([.][0-9]{1,2})?"; + + public final String travelExpenses; + + /** + * Constructs a {@code TravelExpenses}. + * + * @param travelExpenses A valid travel expenses. + */ + public TravelExpenses(String travelExpenses) { + requireNonNull(travelExpenses); + checkArgument(isValidTravelExpenses(travelExpenses), MESSAGE_TRAVEL_EXPENSES_CONSTRAINTS); + this.travelExpenses = travelExpenses; + } + + /** + * Returns true if a given string is a valid Travel Expenses. + */ + public static boolean isValidTravelExpenses(String test) { + return test.matches(EMPLOYEE_TRAVEL_EXPENSES_VALIDATION_REGEX); + } + + @Override + public String toString() { + return travelExpenses; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof TravelExpenses // instanceof handles nulls + && travelExpenses.equals(((TravelExpenses) other).travelExpenses)); // state check + } + + @Override + public int hashCode() { + return travelExpenses.hashCode(); + } + +} diff --git a/src/main/java/seedu/address/model/expenses/UniqueExpensesList.java b/src/main/java/seedu/address/model/expenses/UniqueExpensesList.java new file mode 100644 index 000000000000..4ebdb5c752c8 --- /dev/null +++ b/src/main/java/seedu/address/model/expenses/UniqueExpensesList.java @@ -0,0 +1,153 @@ +package seedu.address.model.expenses; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seedu.address.model.expenses.exceptions.DuplicateExpensesException; +import seedu.address.model.expenses.exceptions.ExpensesNotFoundException; + +/** + * A list of expenses that enforces uniqueness between its elements and does not allow nulls. + * An expenses is considered unique by comparing using {@code Expenses#isSameExpensesRequest(Expenses)}. As such, + * adding and updating of expenses uses Expenses#isSameExpensesRequest(Expenses) for equality so as to ensure that the + * expenses being added or updated is unique in terms of identity in the UniqueExpensesList. However, the removal of a + * expenses usesExpenses#equals(Object) so as to ensure that the expenses with exactly the same fields will be removed. + * + * Supports a minimal set of list operations. + * + * @see Expenses#isSameExpensesRequest(Expenses) + */ +public class UniqueExpensesList implements Iterable { + + private final ObservableList internalExpensesList = FXCollections.observableArrayList(); + + /** + * Returns true if the list contains an equivalent expenses as the given argument. + */ + public boolean contains(Expenses toCheck) { + requireNonNull(toCheck); + return internalExpensesList.stream().anyMatch(toCheck::isSameExpensesRequest); + } + + /** + * Adds a expenses to the list. + * The expenses must not already exist in the list. + */ + public void add(Expenses toAdd) { + requireNonNull(toAdd); + if (contains(toAdd)) { + throw new DuplicateExpensesException(); + } + internalExpensesList.add(toAdd); + } + + /** + * Replaces the expenses {@code target} in the list with {@code editedExpenses}. + * {@code target} must exist in the list. + * The expenses identity of {@code editedExpenses} must not be the same as another existing expenses in the list. + */ + public void setExpenses(Expenses target, Expenses editedExpenses) { + requireAllNonNull(target, editedExpenses); + + int index = internalExpensesList.indexOf(target); + if (index == -1) { + throw new ExpensesNotFoundException(); + } + + if (!target.isSameExpensesRequest(editedExpenses) && contains(editedExpenses)) { + throw new DuplicateExpensesException(); + } + + internalExpensesList.set(index, editedExpenses); + } + + /** + * Removes the equivalent expenses from the list. + * The expenses must exist in the list. + */ + public void remove(Expenses toRemove) { + requireNonNull(toRemove); + if (!internalExpensesList.remove(toRemove)) { + throw new ExpensesNotFoundException(); + } + } + + public void setMultiExpenses(UniqueExpensesList replacement) { + requireNonNull(replacement); + internalExpensesList.setAll(replacement.internalExpensesList); + } + + /** + * Replaces the contents of this list with {@code multiExpenses}. + * {@code multiExpenses} must not contain duplicate expenses. + */ + public void setMultiExpenses(List multiExpenses) { + requireAllNonNull(multiExpenses); + if (!multiExpensesAreUnique(multiExpenses)) { + throw new DuplicateExpensesException(); + } + + internalExpensesList.setAll(multiExpenses); + } + + //Reused from https://github.com/CS2103JAN2018-F14-B1/main/pull/57 with minor modifications + /** + * Sorts ExpensesList by employeeId in ascending order + */ + public void sortByEmployeeId() { + Comparator expensesComparator = new Comparator() { + @Override + public int compare(Expenses expensesA, Expenses expensesB) { + return expensesA.getEmployeeId().value.compareTo(expensesB.getEmployeeId().value); + } + }; + + Collections.sort(internalExpensesList, expensesComparator); + } + + /** + * Returns the backing list as an unmodifiable {@code ObservableList}. + */ + public ObservableList asUnmodifiableObservableList() { + return FXCollections.unmodifiableObservableList(internalExpensesList); + } + + @Override + public Iterator iterator() { + return internalExpensesList.iterator(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof UniqueExpensesList // instanceof handles nulls + && internalExpensesList.equals(((UniqueExpensesList) other).internalExpensesList)); + } + + @Override + public int hashCode() { + return internalExpensesList.hashCode(); + } + + /** + * Returns true if {@code multiExpenses} contains only unique expenses. + */ + private boolean multiExpensesAreUnique(List multiExpenses) { + for (int i = 0; i < multiExpenses.size() - 1; i++) { + for (int j = i + 1; j < multiExpenses.size(); j++) { + if (multiExpenses.get(i).isSameExpensesRequest(multiExpenses.get(j))) { + return false; + } + } + } + return true; + } +} + diff --git a/src/main/java/seedu/address/model/expenses/VersionedExpensesList.java b/src/main/java/seedu/address/model/expenses/VersionedExpensesList.java new file mode 100644 index 000000000000..8af4663dcab9 --- /dev/null +++ b/src/main/java/seedu/address/model/expenses/VersionedExpensesList.java @@ -0,0 +1,108 @@ +package seedu.address.model.expenses; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@code AddressBook} that keeps track of its own history. + */ +public class VersionedExpensesList extends ExpensesList { + private final List expensesStateList; + private int currentStatePointer; + + public VersionedExpensesList(ReadOnlyExpensesList initialState) { + super(initialState); + + expensesStateList = new ArrayList<>(); + expensesStateList.add(new ExpensesList(initialState)); + currentStatePointer = 0; + } + + /** + * Saves a copy of the current {@code AddressBook} state at the end of the state list. + * Undone states are removed from the state list. + */ + public void commit() { + removeStatesAfterCurrentPointer(); + expensesStateList.add(new ExpensesList(this)); + currentStatePointer++; + } + + private void removeStatesAfterCurrentPointer() { + expensesStateList.subList(currentStatePointer + 1, expensesStateList.size()).clear(); + } + + /** + * Restores the address book to its previous state. + */ + public void undo() { + if (!canUndo()) { + throw new NoUndoableStateException(); + } + currentStatePointer--; + resetData(expensesStateList.get(currentStatePointer)); + } + + /** + * Restores the address book to its previously undone state. + */ + public void redo() { + if (!canRedo()) { + throw new NoRedoableStateException(); + } + currentStatePointer++; + resetData(expensesStateList.get(currentStatePointer)); + } + + /** + * Returns true if {@code undo()} has address book states to undo. + */ + public boolean canUndo() { + return currentStatePointer > 0; + } + + /** + * Returns true if {@code redo()} has address book states to redo. + */ + public boolean canRedo() { + return currentStatePointer < expensesStateList.size() - 1; + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof VersionedExpensesList)) { + return false; + } + + VersionedExpensesList otherVersionedExpensesList = (VersionedExpensesList) other; + + // state check + return super.equals(otherVersionedExpensesList) + && expensesStateList.equals(otherVersionedExpensesList.expensesStateList) + && currentStatePointer == otherVersionedExpensesList.currentStatePointer; + } + + /** + * Thrown when trying to {@code undo()} but can't. + */ + public static class NoUndoableStateException extends RuntimeException { + private NoUndoableStateException() { + super("Current state pointer at start of addressBookState list, unable to undo."); + } + } + + /** + * Thrown when trying to {@code redo()} but can't. + */ + public static class NoRedoableStateException extends RuntimeException { + private NoRedoableStateException() { + super("Current state pointer at end of expensesListState list, unable to redo."); + } + } +} diff --git a/src/main/java/seedu/address/model/expenses/exceptions/DuplicateExpensesException.java b/src/main/java/seedu/address/model/expenses/exceptions/DuplicateExpensesException.java new file mode 100644 index 000000000000..31b25d795cf2 --- /dev/null +++ b/src/main/java/seedu/address/model/expenses/exceptions/DuplicateExpensesException.java @@ -0,0 +1,11 @@ +package seedu.address.model.expenses.exceptions; + +/** + * Signals that the operation will result in duplicate Expenses (Expenses are considered duplicates if they have the + * same identity). + */ +public class DuplicateExpensesException extends RuntimeException { + public DuplicateExpensesException() { + super("Operation would result in duplicate Expenses"); + } +} diff --git a/src/main/java/seedu/address/model/expenses/exceptions/ExpensesNotFoundException.java b/src/main/java/seedu/address/model/expenses/exceptions/ExpensesNotFoundException.java new file mode 100644 index 000000000000..230069b9224f --- /dev/null +++ b/src/main/java/seedu/address/model/expenses/exceptions/ExpensesNotFoundException.java @@ -0,0 +1,6 @@ +package seedu.address.model.expenses.exceptions; + +/** + * Signals that the operation is unable to find the expenses for the specified Employee Expenses Id. + */ +public class ExpensesNotFoundException extends RuntimeException {} diff --git a/src/main/java/seedu/address/model/person/Bonus.java b/src/main/java/seedu/address/model/person/Bonus.java new file mode 100644 index 000000000000..1f32e949f58c --- /dev/null +++ b/src/main/java/seedu/address/model/person/Bonus.java @@ -0,0 +1,54 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Person's Bonus in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidBonus(String)} + */ +public class Bonus { + public static final String MESSAGE_BONUS_CONSTRAINTS = + "Bonus should only contain positive numbers and maximum of 2 decimal places from 0 to 24," + + " and it should not be blank"; + public static final String BONUS_VALIDATION_REGEX = "(([0-9]{1,7}([.][0-9]{1,2})?)|(1[0-9]{7}([.][0-9]{1,2})?)" + + "|(2[0-3]([0-9]{1,6})([.][0-9]{1,2})?))"; + public final String value; + + /** + * Constructs a {@code bonus}. + * + * @param bonus A valid bonus. + */ + + public Bonus(String bonus) { + requireNonNull(bonus); + checkArgument(isValidBonus(bonus), MESSAGE_BONUS_CONSTRAINTS); + value = bonus; + } + + /** + * Returns true if a given string is a valid bonus. + */ + public static boolean isValidBonus(String test) { + return test.matches(BONUS_VALIDATION_REGEX); + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Bonus // instanceof handles nulls + && value.equals(((Bonus) other).value)); // state check + } + + @Override + public int hashCode() { + return value.hashCode(); + } + +} diff --git a/src/main/java/seedu/address/model/person/DateOfBirth.java b/src/main/java/seedu/address/model/person/DateOfBirth.java new file mode 100644 index 000000000000..b296d9063566 --- /dev/null +++ b/src/main/java/seedu/address/model/person/DateOfBirth.java @@ -0,0 +1,148 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Represents a Person's date of birth in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidDateOfBirth(String)} + */ +public class DateOfBirth { + public static final String DATEOFBIRTH_VALIDATION_REGEX = + "(0?[0-9]|[12][0-9]|3[01])/(0?[0-9]|1[0-2])/(19[0-9]{2}|200[0-2])"; + + // Error message for different date cases + public static final String MESSAGE_DATEOFBIRTH_CONSTRAINTS_DEFAULT = + "Date of Birth should only be integers in the format of DD/MM/YYYY, it should not be blank and within " + + "01/01/1900 to 31/12/2002"; + private static final String MESSAGE_DATE_INVALID_FEB_DATE_LEAPYEAR = + "There are only 29 days in February on a leap year "; + private static final String MESSAGE_DATE_INVALID_FEB_DATE_NONLEAPYEAR = + "There are only 28 days in February "; + private static final String MESSAGE_DATE_INVALID_MONTH_DATE = + "There are only 30 days in April, June, September and November"; + private static final String DAY_THIRTYFIRST = "31"; + private static final int INDEX_DAY = 0; + private static final int INDEX_MONTH = 1; + private static final int INDEX_YEAR = 2; + private static final List DAYS_INVALID_FEBRUARY_LEAPYEAR = + new ArrayList<>(Arrays.asList("30", "31")); + private static final List DAYS_INVALID_FEBRUARY_NONLEAPYEAR = + new ArrayList<>(Arrays.asList("29", "30", "31")); + private static final List FEBRUARY = + new ArrayList<>(Arrays.asList("2", "02")); + private static final List MONTHS_WITHOUT_THIRSTYFIRST = + new ArrayList<>(Arrays.asList("4", "04", "6", "06", "9", "09", "11")); + + private static String messageDateOfBirthConstraints = MESSAGE_DATEOFBIRTH_CONSTRAINTS_DEFAULT; + public final String value; + + /** + * Constructs a {@code DateOfBirth}. + * + * @param dateOfBirth A valid date of birth. + */ + + public DateOfBirth(String dateOfBirth) { + requireNonNull(dateOfBirth); + dateOfBirth = formatDateOfBirth(dateOfBirth); + checkArgument(isValidDateOfBirth(dateOfBirth), messageDateOfBirthConstraints); + value = dateOfBirth; + } + + public static String getMessageDateOfBirthConstraints() { + return messageDateOfBirthConstraints; + } + + public static void setMessageDateOfBirthConstraints(String error) { + messageDateOfBirthConstraints = error; + } + + /** + * Formats date of birth to pad leading zeroes at the front to form length of 2 for day and month + * @param dateOfBirth A non-padded date of birth string + */ + public static String formatDateOfBirth (String dateOfBirth) { + String[] dateOfBirthPadding = dateOfBirth.split("/"); + + String day = String.format("%02d", Integer.parseInt(dateOfBirthPadding[INDEX_DAY])); + String month = String.format("%02d", Integer.parseInt(dateOfBirthPadding[INDEX_MONTH])); + String year = dateOfBirthPadding[INDEX_YEAR]; + + return String.format(day + "/" + month + "/" + year); + } + + /** + * Returns true if a given string is a valid date of birth. + */ + public static boolean isValidDateOfBirth(String test) { + String day; + String month; + String year; + + if (test.matches(DATEOFBIRTH_VALIDATION_REGEX)) { + String[] date = test.split("/"); + + day = date[INDEX_DAY]; + month = date[INDEX_MONTH]; + year = date[INDEX_YEAR]; + + return checkDateValidity(day, month, year); + } + + setMessageDateOfBirthConstraints(MESSAGE_DATEOFBIRTH_CONSTRAINTS_DEFAULT); + return false; + } + + /** + * Check if the given date is a valid date on the calendar. + * @param day + * @param month + * @param year + */ + + public static boolean checkDateValidity (String day, String month, String year) { + // Algorithm for leap year adapted from https://stackoverflow.com/questions/725098/leap-year-calculation + boolean isLeapYear = ((Integer.valueOf(year) % 4 == 0) && ((Integer.valueOf(year) % 100 != 0) + || (Integer.valueOf(year) % 400 == 0))); + + // Check whether given date is valid if its in February + if (isLeapYear && FEBRUARY.contains(month) && DAYS_INVALID_FEBRUARY_LEAPYEAR.contains(day)) { + setMessageDateOfBirthConstraints(MESSAGE_DATE_INVALID_FEB_DATE_LEAPYEAR); + return false; + } else if (!isLeapYear && FEBRUARY.contains(month) && DAYS_INVALID_FEBRUARY_NONLEAPYEAR.contains(day)) { + setMessageDateOfBirthConstraints(MESSAGE_DATE_INVALID_FEB_DATE_NONLEAPYEAR); + return false; + } + + // Check whether given date is valid if its in April, June, September or November + if (MONTHS_WITHOUT_THIRSTYFIRST.contains(month) && day.equals(DAY_THIRTYFIRST)) { + setMessageDateOfBirthConstraints(MESSAGE_DATE_INVALID_MONTH_DATE); + return false; + } + + return true; + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof DateOfBirth // instanceof handles nulls + && value.equals(((DateOfBirth) other).value)); // state check + } + + @Override + public int hashCode() { + return value.hashCode(); + } + +} diff --git a/src/main/java/seedu/address/model/person/DateOfBirthContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/DateOfBirthContainsKeywordsPredicate.java new file mode 100644 index 000000000000..c97c397b1fcd --- /dev/null +++ b/src/main/java/seedu/address/model/person/DateOfBirthContainsKeywordsPredicate.java @@ -0,0 +1,26 @@ +package seedu.address.model.person; + +import java.util.function.Predicate; + +/** + * Tests that a {@code Person}'s {@code DateOfBirth} matches any of the keywords given. + */ +public class DateOfBirthContainsKeywordsPredicate implements Predicate { + private final String keyword; + + public DateOfBirthContainsKeywordsPredicate(String keyword) { + this.keyword = keyword; + } + + @Override + public boolean test(Person person) { + return keyword.equals(person.getDateOfBirth().value); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof DateOfBirthContainsKeywordsPredicate // instanceof handles nulls + && keyword.equals(((DateOfBirthContainsKeywordsPredicate) other).keyword)); // state check + } +} diff --git a/src/main/java/seedu/address/model/person/Department.java b/src/main/java/seedu/address/model/person/Department.java new file mode 100644 index 000000000000..bb81a7ed4173 --- /dev/null +++ b/src/main/java/seedu/address/model/person/Department.java @@ -0,0 +1,54 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Person's department in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidDepartment(String)} + */ +public class Department { + public static final String MESSAGE_DEPARTMENT_CONSTRAINTS = + "Department should only contain alphabets and spaces, it should not be blank " + + "and it should be within 2 to 30 characters"; + public static final String MESSAGE_DEPARTMENT_KEYWORD_CONSTRAINTS = + "Department only contains alphabets and spaces, please ensure input parameter(s) are relevant"; + public static final String DEPARTMENT_VALIDATION_REGEX = "[A-Za-z ]{2,30}"; + public final String value; + + /** + * Constructs a {@code Department}. + * + * @param department A valid department. + */ + + public Department (String department) { + requireNonNull(department); + checkArgument(isValidDepartment(department), MESSAGE_DEPARTMENT_CONSTRAINTS); + value = department; + } + + /** + * Returns true if a given string is a valid department. + */ + public static boolean isValidDepartment(String test) { + return test.matches(DEPARTMENT_VALIDATION_REGEX); + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Department // instanceof handles nulls + && value.equals(((Department) other).value)); // state check + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/person/DepartmentContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/DepartmentContainsKeywordsPredicate.java new file mode 100644 index 000000000000..bfd9e6ea3981 --- /dev/null +++ b/src/main/java/seedu/address/model/person/DepartmentContainsKeywordsPredicate.java @@ -0,0 +1,29 @@ +package seedu.address.model.person; + +import java.util.List; +import java.util.function.Predicate; + +/** + * Tests that a {@code Person}'s {@code Department} matches any of the keywords given. + */ +public class DepartmentContainsKeywordsPredicate implements Predicate { + private final List keywords; + + public DepartmentContainsKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Person person) { + return keywords.stream() + .anyMatch(keyword -> person.getDepartment().value.toLowerCase().contains(keyword.toLowerCase())); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof DepartmentContainsKeywordsPredicate // instanceof handles nulls + && keywords.equals(((DepartmentContainsKeywordsPredicate) other).keywords)); // state check + } + +} diff --git a/src/main/java/seedu/address/model/person/Email.java b/src/main/java/seedu/address/model/person/Email.java index 38a7629e9a2d..8dd1a77030df 100644 --- a/src/main/java/seedu/address/model/person/Email.java +++ b/src/main/java/seedu/address/model/person/Email.java @@ -56,7 +56,7 @@ public String toString() { public boolean equals(Object other) { return other == this // short circuit if same object || (other instanceof Email // instanceof handles nulls - && value.equals(((Email) other).value)); // state check + && value.equalsIgnoreCase(((Email) other).value)); // state check } @Override diff --git a/src/main/java/seedu/address/model/person/EmailContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/EmailContainsKeywordsPredicate.java new file mode 100644 index 000000000000..dbcca0b8bc36 --- /dev/null +++ b/src/main/java/seedu/address/model/person/EmailContainsKeywordsPredicate.java @@ -0,0 +1,26 @@ +package seedu.address.model.person; + +import java.util.function.Predicate; + +/** + * Tests that a {@code Person}'s {@code Email} matches the keyword given. + */ +public class EmailContainsKeywordsPredicate implements Predicate { + private final String keyword; + + public EmailContainsKeywordsPredicate(String keyword) { + this.keyword = keyword; + } + + @Override + public boolean test(Person person) { + return keyword.equalsIgnoreCase(person.getEmail().value); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof EmailContainsKeywordsPredicate // instanceof handles nulls + && keyword.equals(((EmailContainsKeywordsPredicate) other).keyword)); // state check + } +} diff --git a/src/main/java/seedu/address/model/person/EmployeeId.java b/src/main/java/seedu/address/model/person/EmployeeId.java new file mode 100644 index 000000000000..abb2b60c5aa2 --- /dev/null +++ b/src/main/java/seedu/address/model/person/EmployeeId.java @@ -0,0 +1,52 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Person's employeeId in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidEmployeeId(String)} + */ +public class EmployeeId { + + public static final String MESSAGE_EMPLOYEEID_CONSTRAINTS = + "Employee Ids should only contain a 6 digit number, and it should not be blank"; + public static final String EMPLOYEEID_VALIDATION_REGEX = "[0-9]{6}"; + public final String value; + + /** + * Constructs a {@code EmployeeId}. + * + * @param employeeId A valid employee id. + */ + + public EmployeeId(String employeeId) { + requireNonNull(employeeId); + checkArgument(isValidEmployeeId(employeeId), MESSAGE_EMPLOYEEID_CONSTRAINTS); + value = employeeId; + } + + /** + * Returns true if a given string is a valid employee id. + */ + public static boolean isValidEmployeeId(String test) { + return test.matches(EMPLOYEEID_VALIDATION_REGEX); + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof EmployeeId // instanceof handles nulls + && value.equals(((EmployeeId) other).value)); // state check + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/person/EmployeeIdContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/EmployeeIdContainsKeywordsPredicate.java new file mode 100644 index 000000000000..1609b648fc7c --- /dev/null +++ b/src/main/java/seedu/address/model/person/EmployeeIdContainsKeywordsPredicate.java @@ -0,0 +1,47 @@ +package seedu.address.model.person; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.StringUtil; + +/** + * Tests that a {@code Person}'s {@code EmployeeId} matches any of the keyword(s) given + * or matches the keyword given. + */ +public class EmployeeIdContainsKeywordsPredicate implements Predicate { + private List keywords = new ArrayList<>(); + private String keyword = ""; + + public EmployeeIdContainsKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + public EmployeeIdContainsKeywordsPredicate(String keyword) { + this.keyword = keyword; + } + + @Override + public boolean test(Person person) { + if (!keyword.isEmpty()) { + return keyword.equals(person.getEmployeeId().value); + } + + return keywords.stream() + .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(person.getEmployeeId().value, keyword)); + } + + @Override + public boolean equals(Object other) { + if (!keyword.isEmpty()) { + return other == this // short circuit if same object + || (other instanceof EmployeeIdContainsKeywordsPredicate // instanceof handles nulls + && keyword.equals(((EmployeeIdContainsKeywordsPredicate) other).keyword)); // state check + } + + return other == this // short circuit if same object + || (other instanceof EmployeeIdContainsKeywordsPredicate // instanceof handles nulls + && keywords.equals(((EmployeeIdContainsKeywordsPredicate) other).keywords)); // state check + } + +} diff --git a/src/main/java/seedu/address/model/person/Name.java b/src/main/java/seedu/address/model/person/Name.java index 9982393dabb5..7a5cf0b42fc4 100644 --- a/src/main/java/seedu/address/model/person/Name.java +++ b/src/main/java/seedu/address/model/person/Name.java @@ -10,13 +10,10 @@ public class Name { public static final String MESSAGE_NAME_CONSTRAINTS = - "Names should only contain alphanumeric characters and spaces, and it should not be blank"; + "Names should only contain alphabets and spaces, it should not be blank and " + + "should at least be 3 characters long"; - /* - * The first character of the address must not be a whitespace, - * otherwise " " (a blank string) becomes a valid input. - */ - public static final String NAME_VALIDATION_REGEX = "[\\p{Alnum}][\\p{Alnum} ]*"; + public static final String NAME_VALIDATION_REGEX = "[A-Za-z ]{3,}"; public final String fullName; @@ -48,7 +45,7 @@ public String toString() { public boolean equals(Object other) { return other == this // short circuit if same object || (other instanceof Name // instanceof handles nulls - && fullName.equals(((Name) other).fullName)); // state check + && fullName.equalsIgnoreCase(((Name) other).fullName)); // state check } @Override diff --git a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java index c9b5868427ca..98058c8d6805 100644 --- a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java +++ b/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java @@ -3,10 +3,8 @@ import java.util.List; import java.util.function.Predicate; -import seedu.address.commons.util.StringUtil; - /** - * Tests that a {@code Person}'s {@code Name} matches any of the keywords given. + * Tests that a {@code Person}'s {@code Name} matches any of the keyword(s) given. */ public class NameContainsKeywordsPredicate implements Predicate { private final List keywords; @@ -18,7 +16,7 @@ public NameContainsKeywordsPredicate(List keywords) { @Override public boolean test(Person person) { return keywords.stream() - .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(person.getName().fullName, keyword)); + .anyMatch(keyword -> person.getName().fullName.equalsIgnoreCase(keyword)); } @Override diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java index 557a7a60cd51..f2fc668cfdb3 100644 --- a/src/main/java/seedu/address/model/person/Person.java +++ b/src/main/java/seedu/address/model/person/Person.java @@ -7,7 +7,10 @@ import java.util.Objects; import java.util.Set; -import seedu.address.model.tag.Tag; +import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.EditCommand; +import seedu.address.model.person.tag.Tag; +import seedu.address.model.util.SampleDataUtil; /** * Represents a Person in the address book. @@ -16,30 +19,68 @@ public class Person { // Identity fields + private final EmployeeId employeeId; private final Name name; + private final DateOfBirth dateOfBirth; private final Phone phone; private final Email email; // Data fields + private final Department department; + private final Position position; private final Address address; + private final Salary salary; + private final Bonus bonus; private final Set tags = new HashSet<>(); /** * Every field must be present and not null. */ - public Person(Name name, Phone phone, Email email, Address address, Set tags) { - requireAllNonNull(name, phone, email, address, tags); + public Person(EmployeeId employeeId, Name name, DateOfBirth dateOfBirth, Phone phone, Email email, + Department department, Position position, Address address, Salary salary, Bonus bonus, + Set tags) { + requireAllNonNull(name, dateOfBirth, phone, email, department, position, address, salary, tags); + this.employeeId = employeeId; this.name = name; + this.dateOfBirth = dateOfBirth; this.phone = phone; this.email = email; + this.department = department; + this.position = position; this.address = address; + this.salary = salary; + this.bonus = bonus; this.tags.addAll(tags); } + public Person(EmployeeId employeeId) { + SampleDataUtil sample = new SampleDataUtil(); + requireAllNonNull(employeeId); + this.employeeId = employeeId; + this.name = new Name(sample.SAMPLE_NAME.toString()); + this.dateOfBirth = new DateOfBirth(sample.SAMPLE_DATEOFBIRTH.toString()); + this.phone = new Phone(sample.SAMPLE_PHONE.toString()); + this.email = new Email(sample.SAMPLE_EMAIL.toString()); + this.department = new Department(sample.SAMPLE_DEPARTMENT.toString()); + this.position = new Position(sample.SAMPLE_POSITION.toString()); + this.address = new Address(sample.SAMPLE_ADDRESS.toString()); + this.salary = new Salary(sample.SAMPLE_SALARY.toString()); + this.bonus = new Bonus(sample.SAMPLE_BONUS.toString()); + this.tags.addAll(new HashSet<>()); + } + + public EmployeeId getEmployeeId() { + return employeeId; + } + public Name getName() { return name; } + public DateOfBirth getDateOfBirth() { + return dateOfBirth; + } + public Phone getPhone() { return phone; } @@ -48,10 +89,26 @@ public Email getEmail() { return email; } + public Department getDepartment() { + return department; + } + + public Position getPosition() { + return position; + } + public Address getAddress() { return address; } + public Salary getSalary() { + return salary; + } + + public Bonus getBonus() { + return bonus; + } + /** * Returns an immutable tag set, which throws {@code UnsupportedOperationException} * if modification is attempted. @@ -61,7 +118,7 @@ public Set getTags() { } /** - * Returns true if both persons of the same name have at least one other identity field that is the same. + * Returns true if both persons have the same phone or same email or same name and date of birth. * This defines a weaker notion of equality between two persons. */ public boolean isSamePerson(Person otherPerson) { @@ -69,9 +126,36 @@ public boolean isSamePerson(Person otherPerson) { return true; } - return otherPerson != null - && otherPerson.getName().equals(getName()) - && (otherPerson.getPhone().equals(getPhone()) || otherPerson.getEmail().equals(getEmail())); + AddCommand.setIsEmailDuplicated(false); + EditCommand.setIsEmailDuplicated(false); + AddCommand.setIsPhoneDuplicated(false); + EditCommand.setIsPhoneDuplicated(false); + + if (otherPerson != null && otherPerson.getEmail().equals(getEmail())) { + AddCommand.setIsEmailDuplicated(true); + EditCommand.setIsEmailDuplicated(true); + } else if (otherPerson != null && otherPerson.getPhone().equals(getPhone())) { + AddCommand.setIsPhoneDuplicated(true); + EditCommand.setIsPhoneDuplicated(true); + } + + return otherPerson != null && ((otherPerson.getEmail().equals(getEmail())) + || (otherPerson.getPhone().equals(getPhone())) + || ((otherPerson.getName().equals(getName())) + && (otherPerson.getDateOfBirth().equals(getDateOfBirth())))); + } + + /** + * Returns true if both persons have the same employee id. + * This defines a weaker notion of equality between two persons. + */ + public boolean isSameEmployeeId(Person person) { + if (person.getEmployeeId() == this.getEmployeeId()) { + return true; + } + + return person.getEmployeeId() != null + && person.getEmployeeId().equals(getEmployeeId()); } /** @@ -89,10 +173,13 @@ public boolean equals(Object other) { } Person otherPerson = (Person) other; - return otherPerson.getName().equals(getName()) + return otherPerson.getEmployeeId().equals(getEmployeeId()) + && otherPerson.getName().equals(getName()) + && otherPerson.getDateOfBirth().equals(getDateOfBirth()) && otherPerson.getPhone().equals(getPhone()) && otherPerson.getEmail().equals(getEmail()) && otherPerson.getAddress().equals(getAddress()) + && otherPerson.getSalary().equals(getSalary()) && otherPerson.getTags().equals(getTags()); } @@ -105,13 +192,25 @@ public int hashCode() { @Override public String toString() { final StringBuilder builder = new StringBuilder(); - builder.append(getName()) + builder.append(getEmployeeId()) + .append(" Name: ") + .append(getName()) + .append(" Date Of Birth: ") + .append(getDateOfBirth()) .append(" Phone: ") .append(getPhone()) .append(" Email: ") .append(getEmail()) + .append(" Department: ") + .append(getDepartment()) + .append(" Position: ") + .append(getPosition()) .append(" Address: ") .append(getAddress()) + .append(" Salary: ") + .append(getSalary()) + .append(" Bonus ") + .append(getBonus()) .append(" Tags: "); getTags().forEach(builder::append); return builder.toString(); diff --git a/src/main/java/seedu/address/model/person/PhoneContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/PhoneContainsKeywordsPredicate.java new file mode 100644 index 000000000000..1a3ec4113735 --- /dev/null +++ b/src/main/java/seedu/address/model/person/PhoneContainsKeywordsPredicate.java @@ -0,0 +1,27 @@ +package seedu.address.model.person; + +import java.util.function.Predicate; + +/** + * Tests that a {@code Person}'s {@code Name} matches any of the keywords given. + */ +public class PhoneContainsKeywordsPredicate implements Predicate { + private final String keyword; + + public PhoneContainsKeywordsPredicate(String keyword) { + this.keyword = keyword; + } + + @Override + public boolean test(Person person) { + return keyword.equals(person.getPhone().value); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof PhoneContainsKeywordsPredicate // instanceof handles nulls + && keyword.equals(((PhoneContainsKeywordsPredicate) other).keyword)); // state check + } + +} diff --git a/src/main/java/seedu/address/model/person/Position.java b/src/main/java/seedu/address/model/person/Position.java new file mode 100644 index 000000000000..c7819aac3e6e --- /dev/null +++ b/src/main/java/seedu/address/model/person/Position.java @@ -0,0 +1,55 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Person's Position in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidPosition(String)} + */ +public class Position { + public static final String MESSAGE_POSITION_CONSTRAINTS = + "Position should only contain alphabets and spaces, it should not be blank " + + "and it should be within 2 to 30 characters"; + public static final String MESSAGE_POSITION_KEYWORD_CONSTRAINTS = + "Position only contains alphabets and spaces, please ensure input parameter(s) are relevant"; + public static final String POSITION_VALIDATION_REGEX = "[A-Za-z ]{2,30}"; + public final String value; + + /** + * Constructs a {@code Position}. + * + * @param position A valid position. + */ + + public Position(String position) { + requireNonNull(position); + checkArgument(isValidPosition(position), MESSAGE_POSITION_CONSTRAINTS); + value = position; + } + + /** + * Returns true if a given string is a valid position. + */ + public static boolean isValidPosition(String test) { + return test.matches(POSITION_VALIDATION_REGEX); + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Position // instanceof handles nulls + && value.equals(((Position) other).value)); // state check + } + + @Override + public int hashCode() { + return value.hashCode(); + } + +} diff --git a/src/main/java/seedu/address/model/person/PositionContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/PositionContainsKeywordsPredicate.java new file mode 100644 index 000000000000..3102538067da --- /dev/null +++ b/src/main/java/seedu/address/model/person/PositionContainsKeywordsPredicate.java @@ -0,0 +1,29 @@ +package seedu.address.model.person; + +import java.util.List; +import java.util.function.Predicate; + +/** + * Tests that a {@code Person}'s {@code Position} matches any of the keywords given. + */ +public class PositionContainsKeywordsPredicate implements Predicate { + private final List keywords; + + public PositionContainsKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Person person) { + return keywords.stream() + .anyMatch(keyword -> person.getPosition().value.toLowerCase().contains(keyword.toLowerCase())); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof PositionContainsKeywordsPredicate // instanceof handles nulls + && keywords.equals(((PositionContainsKeywordsPredicate) other).keywords)); // state check + } + +} diff --git a/src/main/java/seedu/address/model/person/Salary.java b/src/main/java/seedu/address/model/person/Salary.java new file mode 100644 index 000000000000..8b431c789fe9 --- /dev/null +++ b/src/main/java/seedu/address/model/person/Salary.java @@ -0,0 +1,53 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Person's Salary in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidSalary(String)} + */ +public class Salary { + public static final String MESSAGE_SALARY_CONSTRAINTS = + "Salary should only contain numbers, and it should not be blank. Only a maximum of 6 whole numbers and " + + "2 decimal place are allowed. (Max Salary store value is 999999.99)\n"; + public static final String SALARY_VALIDATION_REGEX = "[%]?[-]?[0-9]{1,6}([.][0-9]{1,2})?"; + public final String value; + + /** + * Constructs a {@code salary}. + * + * @param salary A valid salary. + */ + + public Salary(String salary) { + requireNonNull(salary); + checkArgument(isValidSalary(salary), MESSAGE_SALARY_CONSTRAINTS); + value = salary; + } + + /** + * Returns true if a given string is a valid salary. + */ + public static boolean isValidSalary(String test) { + return test.matches(SALARY_VALIDATION_REGEX); + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Salary // instanceof handles nulls + && value.equals(((Salary) other).value)); // state check + } + + @Override + public int hashCode() { + return value.hashCode(); + } + +} diff --git a/src/main/java/seedu/address/model/person/UniquePersonList.java b/src/main/java/seedu/address/model/person/UniquePersonList.java index 5856aa42e6b5..37c1786ce678 100644 --- a/src/main/java/seedu/address/model/person/UniquePersonList.java +++ b/src/main/java/seedu/address/model/person/UniquePersonList.java @@ -3,11 +3,14 @@ import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import java.util.Collections; +import java.util.Comparator; import java.util.Iterator; import java.util.List; import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import seedu.address.logic.commands.FilterCommand; import seedu.address.model.person.exceptions.DuplicatePersonException; import seedu.address.model.person.exceptions.PersonNotFoundException; @@ -34,6 +37,25 @@ public boolean contains(Person toCheck) { return internalList.stream().anyMatch(toCheck::isSamePerson); } + /** + * Returns true if the list contains an equivalent person as the given argument. + * However, it does not check itself, it checks the rest of the address book (for edit command). + */ + public boolean contains(Person toCheck, ObservableList filteredPersonList) { + requireNonNull(toCheck); + requireNonNull(filteredPersonList); + return filteredPersonList.stream().anyMatch(toCheck::isSamePerson); + } + + /** + * Returns true if the list contains an equivalent employeeId as the given person's employeeId. + */ + public boolean containsEmployeeId(Person toCheck) { + requireNonNull(toCheck); + return internalList.stream().anyMatch(toCheck::isSameEmployeeId); + } + + /** * Adds a person to the list. * The person must not already exist in the list. @@ -95,6 +117,31 @@ public void setPersons(List persons) { internalList.setAll(persons); } + //Reused from https://github.com/CS2103JAN2018-F14-B1/main/pull/57 with minor modifications + /** + * Sorts PersonList by name in either ascending or descending order. + * @param order The sort order input by the user (either ascending or descending) + */ + public void sortByName(String order) { + Comparator nameComparator = new Comparator() { + @Override + public int compare(Person personA, Person personB) { + return personA.getName().fullName.compareToIgnoreCase(personB.getName().fullName); + } + }; + + switch (order) { + case FilterCommand.ASCENDING: + Collections.sort(internalList, nameComparator); + break; + case FilterCommand.DESCENDING: + Collections.sort(internalList, Collections.reverseOrder(nameComparator)); + break; + default: + throw new AssertionError("Invalid parameter for order entered"); + } + } + /** * Returns the backing list as an unmodifiable {@code ObservableList}. */ diff --git a/src/main/java/seedu/address/model/tag/Tag.java b/src/main/java/seedu/address/model/person/tag/Tag.java similarity index 97% rename from src/main/java/seedu/address/model/tag/Tag.java rename to src/main/java/seedu/address/model/person/tag/Tag.java index 8cdff2773ac9..508dedf71fc2 100644 --- a/src/main/java/seedu/address/model/tag/Tag.java +++ b/src/main/java/seedu/address/model/person/tag/Tag.java @@ -1,4 +1,4 @@ -package seedu.address.model.tag; +package seedu.address.model.person.tag; import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.AppUtil.checkArgument; diff --git a/src/main/java/seedu/address/model/recruitment/JobDescription.java b/src/main/java/seedu/address/model/recruitment/JobDescription.java new file mode 100644 index 000000000000..3a6d58c5eab5 --- /dev/null +++ b/src/main/java/seedu/address/model/recruitment/JobDescription.java @@ -0,0 +1,52 @@ +package seedu.address.model.recruitment; +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a RecruitmentPost's remark in the address book. + * Guarantees: immutable; is always valid + */ +public class JobDescription { + + public static final String MESSAGE_JOB_DESCRIPTION_CONSTRAINTS = + "Job description accepts only characters. It should not include numbers or should not be blank. " + + "For the purpose of using punctuation marks, it only allows comma, " + + "full stop and single right quote. The length of job description is from 1 to 200 characters."; + + + /* + * The first character of the recruitment post must not be a whitespace, + * otherwise " " (a blank string) becomes a valid input. + */ + public static final String JOB_DESCRIPTION_VALIDATION_REGEX = "[a-zA-Z ,.'’]{1,200}"; + + + public final String value; + public JobDescription(String jobDescription) { + requireNonNull(jobDescription); + checkArgument(isValidJobDescription(jobDescription), MESSAGE_JOB_DESCRIPTION_CONSTRAINTS); + value = jobDescription; + } + @Override + public String toString() { + return value; + } + + /** + * Returns true if a given string is a valid name. + */ + public static boolean isValidJobDescription(String test) { + return test.matches(JOB_DESCRIPTION_VALIDATION_REGEX); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof JobDescription // instanceof handles nulls + && value.equals(((JobDescription) other).value)); // state check + } + @Override + public int hashCode() { + return value.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/recruitment/Post.java b/src/main/java/seedu/address/model/recruitment/Post.java new file mode 100644 index 000000000000..ac1a93560918 --- /dev/null +++ b/src/main/java/seedu/address/model/recruitment/Post.java @@ -0,0 +1,51 @@ +package seedu.address.model.recruitment; +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a RecruitmentPost's remark in the address book. + * Guarantees: immutable; is always valid + */ +public class Post { + + public static final String MESSAGE_POST_CONSTRAINTS = + "Job Position accepts only characters. It should not include numbers or should not be blank. " + + "And the maximum length of the job position is 20 characters"; + + + /* + * The first character of the recruitment post must not be a whitespace + * otherwise " " (a blank string) becomes a valid input. + */ + public static final String POST_VALIDATION_REGEX = "[a-zA-Z ]{1,20}"; + + + public final String value; + public Post(String post) { + requireNonNull(post); + checkArgument(isValidPost(post), MESSAGE_POST_CONSTRAINTS); + value = post; + } + @Override + public String toString() { + return value; + } + + /** + * Returns true if a given string is a valid name. + */ + public static boolean isValidPost(String test) { + return test.matches(POST_VALIDATION_REGEX); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Post // instanceof handles nulls + && value.equals(((Post) other).value)); // state check + } + @Override + public int hashCode() { + return value.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/recruitment/PostContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/recruitment/PostContainsKeywordsPredicate.java new file mode 100644 index 000000000000..d1870f490712 --- /dev/null +++ b/src/main/java/seedu/address/model/recruitment/PostContainsKeywordsPredicate.java @@ -0,0 +1,30 @@ +package seedu.address.model.recruitment; +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.StringUtil; + +/** + * Tests that a {@code Recruitment}'s {@code Name} matches any of the post position given. + */ +public class PostContainsKeywordsPredicate implements Predicate { + private final List keywords; + + public PostContainsKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Recruitment recruitment) { + return keywords.stream() + .anyMatch(keyword -> StringUtil.containsSentenceIgnoreCase(recruitment.getPost().value, keyword)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof PostContainsKeywordsPredicate // instanceof handles nulls + && keywords.equals(((PostContainsKeywordsPredicate) other).keywords)); // state check + } + +} diff --git a/src/main/java/seedu/address/model/recruitment/ReadOnlyRecruitmentList.java b/src/main/java/seedu/address/model/recruitment/ReadOnlyRecruitmentList.java new file mode 100644 index 000000000000..d2a0f202280a --- /dev/null +++ b/src/main/java/seedu/address/model/recruitment/ReadOnlyRecruitmentList.java @@ -0,0 +1,16 @@ +package seedu.address.model.recruitment; + +import javafx.collections.ObservableList; + +/** + * Unmodifiable view of an recruitmentList + */ +public interface ReadOnlyRecruitmentList { + + /** + * Returns an unmodifiable view of the recruitmentPost list. + * This list will not contain any duplicate recruitment posts. + */ + ObservableList getRecruitmentList(); + +} diff --git a/src/main/java/seedu/address/model/recruitment/Recruitment.java b/src/main/java/seedu/address/model/recruitment/Recruitment.java new file mode 100644 index 000000000000..fdbcf9f61862 --- /dev/null +++ b/src/main/java/seedu/address/model/recruitment/Recruitment.java @@ -0,0 +1,112 @@ +package seedu.address.model.recruitment; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Objects; + +import seedu.address.logic.commands.AddRecruitmentPostCommand; +import seedu.address.logic.commands.EditRecruitmentPostCommand; + +/** + * Represents a recruitment list in the address book. + * Guarantees: immutable; is always valid + */ +public class Recruitment { + + // Data fields + private final Post post; + private final WorkExp workExp; + private final JobDescription jobDescription; + + public Recruitment (Post post, WorkExp workExp, JobDescription jobDescription) { + requireAllNonNull(post, workExp, jobDescription); + this.post = post; + this.workExp = workExp; + this.jobDescription = jobDescription; + } + + public Post getPost() { + return post; + } + public WorkExp getWorkExp() { + return workExp; + } + public JobDescription getJobDescription() { + return jobDescription; + } + + /** + * Returns true if both recruitmentPosts have the same identity and data fields. + * This defines a stronger notion of equality between two recruitmentPosts. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Recruitment)) { + return false; + } + + Recruitment otherRecruitment = (Recruitment) other; + return otherRecruitment.getPost().equals(getPost()) + && otherRecruitment.getWorkExp().equals(getWorkExp()) + && otherRecruitment.getJobDescription().equals(getJobDescription()); + } + + @Override + public int hashCode() { + // use this method for custom fields hashing instead of implementing your own + return Objects.hash(post, workExp, jobDescription); + } + + /** + * Returns true if both recruitmentPosts of the same job position + * have at least one other identity field that is the same. + * This defines a weaker notion of equality between two recruitmentPosts. + */ + public boolean isSameRecruitment (Recruitment otherRecruitment) { + if (otherRecruitment == this) { + return true; + } + + AddRecruitmentPostCommand.setIsPostDuplicated(false); + EditRecruitmentPostCommand.setIsPostDuplicated(false); + AddRecruitmentPostCommand.setIsWorkExpDuplicated(false); + EditRecruitmentPostCommand.setIsWorkExpDuplicated(false); + AddRecruitmentPostCommand.setIsJobDescriptionDuplicated(false); + EditRecruitmentPostCommand.setIsJobDescriptionDuplicated(false); + + if (otherRecruitment != null && otherRecruitment.getPost().equals(getPost())) { + AddRecruitmentPostCommand.setIsPostDuplicated(true); + EditRecruitmentPostCommand.setIsPostDuplicated(true); + } else if (otherRecruitment != null && otherRecruitment.getWorkExp().equals(getWorkExp())) { + AddRecruitmentPostCommand.setIsWorkExpDuplicated(true); + EditRecruitmentPostCommand.setIsWorkExpDuplicated(true); + } else if (otherRecruitment != null && otherRecruitment.getJobDescription().equals(getJobDescription())) { + AddRecruitmentPostCommand.setIsJobDescriptionDuplicated(true); + EditRecruitmentPostCommand.setIsJobDescriptionDuplicated(true); + } + + return otherRecruitment != null + && otherRecruitment.getPost().equals(getPost()) + && otherRecruitment.getWorkExp().equals(getWorkExp()) + && otherRecruitment.getJobDescription().equals(getJobDescription()); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("\nJob Position: ") + .append(getPost()) + .append("\n") + .append("Minimal years of working experience: ") + .append(getWorkExp()) + .append("\n") + .append("Job Description: ") + .append(getJobDescription()); + return builder.toString(); + } + +} diff --git a/src/main/java/seedu/address/model/recruitment/RecruitmentList.java b/src/main/java/seedu/address/model/recruitment/RecruitmentList.java new file mode 100644 index 000000000000..1041a429afb4 --- /dev/null +++ b/src/main/java/seedu/address/model/recruitment/RecruitmentList.java @@ -0,0 +1,119 @@ +package seedu.address.model.recruitment; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import javafx.collections.ObservableList; + +/** + * Wraps all data at the address-book level + * Duplicates are not allowed (by .isSameRecruitment comparison) + */ +public class RecruitmentList implements ReadOnlyRecruitmentList { + + private final UniqueRecruitmentList recruitments; + + /* + * The 'unusual' code block below is an non-static initialization block, sometimes used to avoid duplication + * between constructors. See https://docs.oracle.com/javase/tutorial/java/javaOO/initial.html + * + * Note that non-static init blocks are not recommended to use. There are other ways to avoid duplication + * among constructors. + */ + { + recruitments = new UniqueRecruitmentList(); + } + + public RecruitmentList() {} + + /** + * Creates an AddressBook using the Recruitment in the {@code toBeCopied} + */ + public RecruitmentList(ReadOnlyRecruitmentList toBeCopied) { + this(); + resetData(toBeCopied); + } + + //// list overwrite operations + + /** + * Replaces the contents of the recruitmentPost list with {@code recruitment}. + * {@code recruitment} must not contain duplicate recruitment. + */ + public void setRecruitments(List recruitments) { + this.recruitments.setRecruitments(recruitments); + } + + /** + * Resets the existing data of this {@code AddressBook} with {@code newData}. + */ + public void resetData(ReadOnlyRecruitmentList newData) { + requireNonNull(newData); + + setRecruitments(newData.getRecruitmentList()); + } + + //// recruitment-level operations + + /** + * Returns true if a recruitment with the same identity as {@code recruitment} exists in the address book. + */ + public boolean hasRecruitment(Recruitment recruitment) { + requireNonNull(recruitment); + return recruitments.contains(recruitment); + } + + /** + * Adds a recruitment to the address book. + * The recruitmentPost must not already exist in the address book. + */ + public void addRecruitment(Recruitment recruitment) { + recruitments.add(recruitment); + } + + /** + * Replaces the given recruitmentPost {@code target} in the recruitmentList with {@code editedRecruitment}. + * {@code target} must exist in the address book. + * The recruitmentPost identity of {@code editedRecruitment} must not be the same as another existing + * recruitmentPost in the address book. + */ + public void updateRecruitment(Recruitment recruitment, Recruitment editedRecruitment) { + requireNonNull(editedRecruitment); + + recruitments.setRecruitment(recruitment, editedRecruitment); + } + + /** + * Removes {@code key} from this {@code AddressBook}. + * {@code key} must exist in the address book. + */ + public void removeRecruitment(Recruitment key) { + recruitments.remove(key); + } + + //// util methods + + @Override + public String toString() { + return recruitments.asUnmodifiableObservableList().size() + " recruitments"; + // TODO: refine later + } + + @Override + public ObservableList getRecruitmentList() { + return recruitments.asUnmodifiableObservableList(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof RecruitmentList // instanceof handles nulls + && recruitments.equals(((RecruitmentList) other).recruitments)); + } + + @Override + public int hashCode() { + return recruitments.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/recruitment/UniqueRecruitmentList.java b/src/main/java/seedu/address/model/recruitment/UniqueRecruitmentList.java new file mode 100644 index 000000000000..dba47e73bdf3 --- /dev/null +++ b/src/main/java/seedu/address/model/recruitment/UniqueRecruitmentList.java @@ -0,0 +1,140 @@ +package seedu.address.model.recruitment; + +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.recruitment.exceptions.DuplicateRecruitmentException; +import seedu.address.model.recruitment.exceptions.RecruitmentNotFoundException; + +/** + * A list of recruitmentPost that enforces uniqueness between its elements and does not allow nulls. + * A recruitmentPost is considered unique by comparing using {@code Recruitment#isSameRecruitment(Recruitment)}. + * As such, + * adding and updating of + * recruitmentPosts uses Recruitment#isSameRecruitment(Recruitment) for equality so as to ensure + * that the recruitmentPost being added or updated is + * unique in terms of identity in the UniqueRecruitmentList. However, the removal of + * a recruitmentPost uses Recruitment#equals(Object) so + * as to ensure that the recruitmentPost with exactly the same fields will be removed. + * + * Supports a minimal set of list operations. + * + * @see Recruitment#isSameRecruitment(Recruitment) + */ +public class UniqueRecruitmentList implements Iterable { + + private final ObservableList internalList = FXCollections.observableArrayList(); + + /** + * Returns true if the list contains an equivalent recruitment as the given argument. + */ + public boolean contains(Recruitment toCheck) { + requireNonNull(toCheck); + return internalList.stream().anyMatch(toCheck::isSameRecruitment); + } + + /** + * Adds a recruitmentPost to the list. + * The recruitmentPost must not already exist in the list. + */ + public void add(Recruitment toAddRecruitment) { + requireNonNull(toAddRecruitment); + if (contains(toAddRecruitment)) { + throw new DuplicateRecruitmentException(); + } + internalList.add(toAddRecruitment); + } + + /** + * Replaces the recruitmentPost {@code target} in the list with {@code editedRecruitment}. + * {@code target} must exist in the list. + * The recruitmentPost identity of {@code editedRecruitment} must not be the same as another existing + * recruitmentPost in the list. + */ + public void setRecruitment(Recruitment target, Recruitment editedRecruitment) { + requireAllNonNull(target, editedRecruitment); + + int index = internalList.indexOf(target); + if (index == -1) { + throw new RecruitmentNotFoundException(); + } + + if (!target.isSameRecruitment(editedRecruitment) && contains(editedRecruitment)) { + throw new DuplicateRecruitmentException(); + } + + internalList.set(index, editedRecruitment); + } + + /** + * Removes the equivalent recruitmentPost from the list. + * The recruitmentPost must exist in the list. + */ + public void remove(Recruitment toRemove) { + requireNonNull(toRemove); + if (!internalList.remove(toRemove)) { + throw new RecruitmentNotFoundException(); + } + } + + public void setRecruitments(UniqueRecruitmentList replacement) { + requireNonNull(replacement); + internalList.setAll(replacement.internalList); + } + + /** + * Replaces the contents of this list with {@code recruitment}. + * {@code recruitment} must not contain duplicate recruitmentPosts. + */ + public void setRecruitments(List recruitments) { + requireAllNonNull(recruitments); + if (!personsAreUnique(recruitments)) { + throw new DuplicateRecruitmentException(); + } + + internalList.setAll(recruitments); + } + + /** + * Returns the backing list as an unmodifiable {@code ObservableList}. + */ + public ObservableList asUnmodifiableObservableList() { + return FXCollections.unmodifiableObservableList(internalList); + } + + @Override + public Iterator iterator() { + return internalList.iterator(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof UniqueRecruitmentList // instanceof handles nulls + && internalList.equals(((UniqueRecruitmentList) other).internalList)); + } + + @Override + public int hashCode() { + return internalList.hashCode(); + } + + /** + * Returns true if {@code recruitments} contains only unique recruitmentPosts. + */ + private boolean personsAreUnique(List persons) { + for (int i = 0; i < persons.size() - 1; i++) { + for (int j = i + 1; j < persons.size(); j++) { + if (persons.get(i).isSameRecruitment(persons.get(j))) { + return false; + } + } + } + return true; + } +} diff --git a/src/main/java/seedu/address/model/recruitment/VersionedRecruitmentList.java b/src/main/java/seedu/address/model/recruitment/VersionedRecruitmentList.java new file mode 100644 index 000000000000..b6478e9f3371 --- /dev/null +++ b/src/main/java/seedu/address/model/recruitment/VersionedRecruitmentList.java @@ -0,0 +1,109 @@ +package seedu.address.model.recruitment; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@code AddressBook} that keeps track of its own history. + */ +public class VersionedRecruitmentList extends RecruitmentList { + + private final List recruitmentListStateList; + private int currentStatePointer; + + public VersionedRecruitmentList(ReadOnlyRecruitmentList initialState) { + super(initialState); + + recruitmentListStateList = new ArrayList<>(); + recruitmentListStateList.add(new RecruitmentList(initialState)); + currentStatePointer = 0; + } + + /** + * Saves a copy of the current {@code AddressBook} state at the end of the state list. + * Undone states are removed from the state list. + */ + public void commit() { + removeStatesAfterCurrentPointer(); + recruitmentListStateList.add(new RecruitmentList(this)); + currentStatePointer++; + } + + private void removeStatesAfterCurrentPointer() { + recruitmentListStateList.subList(currentStatePointer + 1, recruitmentListStateList.size()).clear(); + } + + /** + * Restores the address book to its previous state. + */ + public void undo() { + if (!canUndo()) { + throw new NoUndoableStateException(); + } + currentStatePointer--; + resetData(recruitmentListStateList.get(currentStatePointer)); + } + + /** + * Restores the address book to its previously undone state. + */ + public void redo() { + if (!canRedo()) { + throw new NoRedoableStateException(); + } + currentStatePointer++; + resetData(recruitmentListStateList.get(currentStatePointer)); + } + + /** + * Returns true if {@code undo()} has address book states to undo. + */ + public boolean canUndo() { + return currentStatePointer > 0; + } + + /** + * Returns true if {@code redo()} has address book states to redo. + */ + public boolean canRedo() { + return currentStatePointer < recruitmentListStateList.size() - 1; + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof VersionedRecruitmentList)) { + return false; + } + + VersionedRecruitmentList otherVersionedRecruitmentList = (VersionedRecruitmentList) other; + + // state check + return super.equals(otherVersionedRecruitmentList) + && recruitmentListStateList.equals(otherVersionedRecruitmentList.recruitmentListStateList) + && currentStatePointer == otherVersionedRecruitmentList.currentStatePointer; + } + + /** + * Thrown when trying to {@code undo()} but can't. + */ + public static class NoUndoableStateException extends RuntimeException { + private NoUndoableStateException() { + super("Current state pointer at start of addressBookState list, unable to undo."); + } + } + + /** + * Thrown when trying to {@code redo()} but can't. + */ + public static class NoRedoableStateException extends RuntimeException { + private NoRedoableStateException() { + super("Current state pointer at end of addressBookState list, unable to redo."); + } + } +} diff --git a/src/main/java/seedu/address/model/recruitment/WorkExp.java b/src/main/java/seedu/address/model/recruitment/WorkExp.java new file mode 100644 index 000000000000..8f7d64af6cf9 --- /dev/null +++ b/src/main/java/seedu/address/model/recruitment/WorkExp.java @@ -0,0 +1,54 @@ +package seedu.address.model.recruitment; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a RecruitmentPost's minimal working experience in the address book. + * Guarantees: immutable; is always valid + */ +public class WorkExp { + + public static final String MESSAGE_WORK_EXP_CONSTRAINTS = + "Working Experience should only contain integers with length of at least 1 digit long" + + "And the range of the working experience is from 0 to 30"; + public static final String EMPLOYEE_WORK_EXP_VALIDATION_REGEX = "^(0?[0-9]|[12][0-9]|30)"; + + public final String workExp; + + /** + * Constructs a {@code WorkExp}. + * + * @param workExp A valid Working Experience. + */ + public WorkExp(String workExp) { + requireNonNull(workExp); + checkArgument(isValidWorkExp(workExp), MESSAGE_WORK_EXP_CONSTRAINTS); + this.workExp = workExp; + } + + /** + * Returns true if a given string is a valid Working Experience. + */ + public static boolean isValidWorkExp(String test) { + return test.matches(EMPLOYEE_WORK_EXP_VALIDATION_REGEX); + } + + @Override + public String toString() { + return workExp; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof WorkExp // instanceof handles nulls + && workExp.equals(((WorkExp) other).workExp)); // state check + } + + @Override + public int hashCode() { + return workExp.hashCode(); + } + +} diff --git a/src/main/java/seedu/address/model/recruitment/exceptions/DuplicateRecruitmentException.java b/src/main/java/seedu/address/model/recruitment/exceptions/DuplicateRecruitmentException.java new file mode 100644 index 000000000000..bda87906785a --- /dev/null +++ b/src/main/java/seedu/address/model/recruitment/exceptions/DuplicateRecruitmentException.java @@ -0,0 +1,12 @@ +package seedu.address.model.recruitment.exceptions; + +/** + * Signals that the operation will result in duplicate recruitments (Recruitments are considered duplicates if they + * have the + * same identity). + */ +public class DuplicateRecruitmentException extends RuntimeException { + public DuplicateRecruitmentException() { + super("Operation would result in duplicate recruitment"); + } +} diff --git a/src/main/java/seedu/address/model/recruitment/exceptions/RecruitmentNotFoundException.java b/src/main/java/seedu/address/model/recruitment/exceptions/RecruitmentNotFoundException.java new file mode 100644 index 000000000000..f5116677ff30 --- /dev/null +++ b/src/main/java/seedu/address/model/recruitment/exceptions/RecruitmentNotFoundException.java @@ -0,0 +1,7 @@ +package seedu.address.model.recruitment.exceptions; + +/** + * Signals that the operation is unable to find the recruitment for the specified. + */ +public class RecruitmentNotFoundException extends RuntimeException { +} diff --git a/src/main/java/seedu/address/model/schedule/Date.java b/src/main/java/seedu/address/model/schedule/Date.java new file mode 100644 index 000000000000..3e396b1cc061 --- /dev/null +++ b/src/main/java/seedu/address/model/schedule/Date.java @@ -0,0 +1,174 @@ +package seedu.address.model.schedule; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +/** + * Represents a Schedule's date in the Schedule list. + * Guarantees: immutable; is valid as declared in {@link #Date(String)} + */ +public class Date { + public static final String DATE_PATTERN = "dd/MM/yyyy"; + + public static final String DATE_VALIDATION_REGEX = "^(0?[1-9]|[12][0-9]|3[01])\\/(0?[1-9]|1[012])\\/((20)\\d\\d)$"; + + public static final String MESSAGE_DATE_CONSTRAINTS_DEFAULT = + "Date should only be integers in the format of DD/MM/YYYY, it should not be blank and within " + + "01/01/2000 to 31/12/2099"; + + public static final String MESSAGE_DATE_OF_SCHEDULE_BEFORE_TODAY_DATE = + "Date of schedule %1$s should not be before today's date %2$s. " + + "\nScheduling for past dates is not allowed! "; + + private static final String MESSAGE_DATE_INVALID_FEB_DATE = + "29, 30 and 31 are invalid dates of February "; + private static final String MESSAGE_DATE_INVALID_FEB_DATE_LEAP_YEAR = + "30 and 31 are invalid dates on a leap year of February "; + + private static final String MESSAGE_DATE_INVALID_MONTH_DATE = + "april, june, sep, nov does not have 31 days"; + + private static String dateConstraintsError = MESSAGE_DATE_CONSTRAINTS_DEFAULT; + + public final String value; + + /** + * Constructs a {@code date}. + * @param date A valid date of Schedule. + */ + + public Date(String date) { + requireNonNull(date); + checkArgument(isValidScheduleDate(date), dateConstraintsError); + date = formatDate(date); + value = date; + } + + public static void setDateConstraintsError(String error) { + dateConstraintsError = error; + } + + public static String getDateConstraintsError() { + return dateConstraintsError; + } + + /** + * Checks whether the date has past. + * @param inputDate to be checked with today's date + * @return Boolean, true if input date is before today's date. + */ + public static boolean isBeforeTodayDate (String inputDate) { + inputDate = formatDate(inputDate); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_PATTERN); + LocalDate todayDate = LocalDate.now(); + LocalDate toLocalInputDate = LocalDate.parse(inputDate, formatter); + if (toLocalInputDate.isBefore(todayDate)) { + dateConstraintsError = String.format(MESSAGE_DATE_OF_SCHEDULE_BEFORE_TODAY_DATE, inputDate, todayDate()); + return true; + } + return false; + } + + /** + * Get today's date + * @return String, today's date. + */ + public static String todayDate () { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_PATTERN); + return formatter.format(LocalDate.now()); + } + + /** + * Formats date to add leading 0's to form width of 2 for day and month. + * @param inputDate date to be padded with 0 for day and month + * @return String, date padded with 0's if necessary for day and month. + */ + public static String formatDate (String inputDate) { + String day; + String month; + String year; + + String[] date = inputDate.split("/"); + + day = String.format("%02d", Integer.parseInt(date[0])); + month = String.format("%02d", Integer.parseInt(date[1])); + year = date[2]; + + return String.format(day + "/" + month + "/" + year); + } + + /** + * Returns true if a given string is a valid date found in calendar, and not before today's date + * @param inputDate date to be checked if valid for scheduling. + * @return Boolean, true if it passes the regular expression and {@code checkValidDate()} checks. + */ + public static boolean isValidScheduleDate(String inputDate) { + String day; + String month; + String year; + + if (inputDate.matches(DATE_VALIDATION_REGEX)) { + inputDate = formatDate(inputDate); + String[] date = inputDate.split("/"); + + day = date[0]; + month = date[1]; + year = date[2]; + return checkValidDate(year, month, day); + } + setDateConstraintsError(MESSAGE_DATE_CONSTRAINTS_DEFAULT); + return false; + } + + /** + * Check if date is a valid date on the Calendar. + * @param year year to check + * @param month month to check + * @param day day to check + */ + public static boolean checkValidDate (String year, String month, String day) { + boolean isLeapYear = ((Integer.valueOf(year) % 4 == 0) + && (Integer.valueOf(year) % 100 != 0) || (Integer.valueOf(year) % 400 == 0)); + + if (("02".equals(month)) || ("2".equals(month))) { + if ((isLeapYear) && (( + "30".equals(day)) || ("31".equals(day)))) { + setDateConstraintsError(MESSAGE_DATE_INVALID_FEB_DATE_LEAP_YEAR + year); + return false; //29 Feb is a valid leap year. 30, 31 is invalid. + } else if ((!isLeapYear) && (("29".equals(day)) || ("30".equals(day)) || ("31".equals(day)))) { + setDateConstraintsError(MESSAGE_DATE_INVALID_FEB_DATE + year); + return false; //29,30,31 Feb is a invalid in non-leap year + } + } + + if (("31".equals(day)) && (( + "04".equals(month)) || ("4".equals(month)) || ("06".equals(month)) || ("6".equals(month)) + || ("09".equals(month)) || ("9".equals(month)) || ("11".equals(month)))) { + setDateConstraintsError(MESSAGE_DATE_INVALID_MONTH_DATE); + return false; // april, june, sep, nov does not have 31 days + } + setDateConstraintsError(MESSAGE_DATE_CONSTRAINTS_DEFAULT); + return true; + } + + @Override + public String toString() { + return value; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Date // instanceof handles nulls + && value.equals(((Date) other).value)); // state check + } + + @Override + public int hashCode() { + return value.hashCode(); + } + +} diff --git a/src/main/java/seedu/address/model/schedule/DateComparator.java b/src/main/java/seedu/address/model/schedule/DateComparator.java new file mode 100644 index 000000000000..6626f1eea9c5 --- /dev/null +++ b/src/main/java/seedu/address/model/schedule/DateComparator.java @@ -0,0 +1,30 @@ +package seedu.address.model.schedule; + +import static seedu.address.model.schedule.Date.DATE_PATTERN; + +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; + +import java.util.Comparator; + +/** + * The {@code DateComparator} class is used for comparing which date is larger. + * In ascending normal order DD/MM/YYYY. + */ +public class DateComparator implements Comparator { + + /** + * Compare in ascending order + * @param o1 Date 1 to be compared + * @param o2 Date 2 to be compared + * @return Int, true if ascending + */ + public int compare(Date o1, Date o2) { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_PATTERN); + LocalDate s1; + LocalDate s2; + s1 = LocalDate.parse(o1.value, formatter); + s2 = LocalDate.parse(o2.value, formatter); + return s1.compareTo(s2); + } +} diff --git a/src/main/java/seedu/address/model/schedule/EmployeeIdComparator.java b/src/main/java/seedu/address/model/schedule/EmployeeIdComparator.java new file mode 100644 index 000000000000..adf93d934754 --- /dev/null +++ b/src/main/java/seedu/address/model/schedule/EmployeeIdComparator.java @@ -0,0 +1,31 @@ +package seedu.address.model.schedule; + +import java.util.Comparator; + +import seedu.address.model.person.EmployeeId; + +/** + * The {@code EmployeeIdComparator} class is used for comparing which employee id is larger. + * In ascending normal order. + */ +public class EmployeeIdComparator implements Comparator { + + /** + * Compare in ascending order + * @param o1 Employee 1 to be compared + * @param o2 Employee 2 to be compared + * @return Int, true if ascending + */ + public int compare(EmployeeId o1, EmployeeId o2) { + int s1 = Integer.parseInt(o1.value); + int s2 = Integer.parseInt(o2.value); + + if (s1 == s2) { + return 0; + } else if (s1 > s2) { + return 1; + } else { + return -1; + } + } +} diff --git a/src/main/java/seedu/address/model/schedule/EmployeeIdScheduleContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/schedule/EmployeeIdScheduleContainsKeywordsPredicate.java new file mode 100644 index 000000000000..3006773dae75 --- /dev/null +++ b/src/main/java/seedu/address/model/schedule/EmployeeIdScheduleContainsKeywordsPredicate.java @@ -0,0 +1,31 @@ +package seedu.address.model.schedule; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.StringUtil; + +/** + * Tests that a {@code Schedule}'s {@code Name} matches any of the Id given. + */ +public class EmployeeIdScheduleContainsKeywordsPredicate implements Predicate { + private final List keywords; + + public EmployeeIdScheduleContainsKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Schedule schedule) { + return keywords.stream() + .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(schedule.getEmployeeId().value, keyword)); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof EmployeeIdScheduleContainsKeywordsPredicate // instanceof handles nulls + && keywords.equals(((EmployeeIdScheduleContainsKeywordsPredicate) other).keywords)); // state check + } + +} diff --git a/src/main/java/seedu/address/model/schedule/ReadOnlyScheduleList.java b/src/main/java/seedu/address/model/schedule/ReadOnlyScheduleList.java new file mode 100644 index 000000000000..10dc0dea7137 --- /dev/null +++ b/src/main/java/seedu/address/model/schedule/ReadOnlyScheduleList.java @@ -0,0 +1,16 @@ +package seedu.address.model.schedule; + +import javafx.collections.ObservableList; + +/** + * Unmodifiable view of an schedule list + */ +public interface ReadOnlyScheduleList { + + /** + * Returns an unmodifiable view of the schedules list. + * This list will not contain any duplicate schedules. + */ + ObservableList getScheduleList(); + +} diff --git a/src/main/java/seedu/address/model/schedule/Schedule.java b/src/main/java/seedu/address/model/schedule/Schedule.java new file mode 100644 index 000000000000..f4e79ad7cc84 --- /dev/null +++ b/src/main/java/seedu/address/model/schedule/Schedule.java @@ -0,0 +1,95 @@ +package seedu.address.model.schedule; + +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Objects; + +import seedu.address.model.person.EmployeeId; + +/** + * Represents a schedule list. + * Guarantees: immutable; is always valid + */ +public class Schedule { + + // Data fields + private final EmployeeId id; + private final Type type; + private final Date date; + + public Schedule (EmployeeId id, Type type, Date date) { + requireAllNonNull(id, type, date); + this.type = type; + this.date = date; + this.id = id; + } + + public Type getType() { + return type; + } + + public Date getScheduleDate() { + return date; + } + + public EmployeeId getEmployeeId() { + return id; + } + + public String getScheduleYear() { + String year; + String[] date = getScheduleDate().toString().split("/"); + year = date[2]; + return year; + } + + /** + * Returns true if both schedules have the same identity and data fields. + */ + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof Schedule)) { + return false; + } + + Schedule otherSchedule = (Schedule) other; + return otherSchedule.getScheduleDate().equals(getScheduleDate()) + && otherSchedule.getType().equals(getType()) + && otherSchedule.getEmployeeId().equals(getEmployeeId()); + } + + @Override + public int hashCode() { + // use this method for custom fields hashing instead of implementing your own + return Objects.hash(type, date); + } + + /** + * Returns true if both schedules identity fields are the same. + */ + public boolean isSameSchedule(Schedule otherSchedule) { + if (otherSchedule == this) { + return true; + } + + return otherSchedule != null + && otherSchedule.getEmployeeId().equals(getEmployeeId()) + && (otherSchedule.getType().equals(getType()) + && otherSchedule.getScheduleDate().equals(getScheduleDate())); + } + + @Override + public String toString() { + final StringBuilder builder = new StringBuilder(); + builder.append("Date: ") + .append(getScheduleDate()) + .append(" Type: ") + .append(getType()); + return builder.toString(); + } + +} diff --git a/src/main/java/seedu/address/model/schedule/ScheduleList.java b/src/main/java/seedu/address/model/schedule/ScheduleList.java new file mode 100644 index 000000000000..1f5ad5477ae6 --- /dev/null +++ b/src/main/java/seedu/address/model/schedule/ScheduleList.java @@ -0,0 +1,122 @@ +package seedu.address.model.schedule; + +import static java.util.Objects.requireNonNull; + +import java.util.List; + +import javafx.collections.ObservableList; + +/** + * Wraps all data at the schedule-list level + * Duplicates are not allowed (by .isSameSchedule comparison) + */ +public class ScheduleList implements ReadOnlyScheduleList { + + private final UniqueScheduleList schedules; + + /* + * The 'unusual' code block below is an non-static initialization block, sometimes used to avoid duplication + * between constructors. See https://docs.oracle.com/javase/tutorial/java/javaOO/initial.html + * + * Note that non-static init blocks are not recommended to use. There are other ways to avoid duplication + * among constructors. + */ + { + schedules = new UniqueScheduleList(); + } + + public ScheduleList() {} + + /** + * Creates an ScheduleList using the Schedules in the {@code toBeCopied} + */ + public ScheduleList(ReadOnlyScheduleList toBeCopied) { + this(); + resetData(toBeCopied); + } + + //// list overwrite operations + + /** + * Replaces the contents of the schedule list with {@code schedules}. + * {@code schedules} must not contain duplicate schedules. + */ + public void setSchedules(List schedules) { + this.schedules.setSchedules(schedules); + } + + /** + * Resets the existing data of this {@code ScheduleList} with {@code newData}. + */ + public void resetData(ReadOnlyScheduleList newData) { + requireNonNull(newData); + setSchedules(newData.getScheduleList()); + } + + //// schedule-level operations + + /** + * Returns true if a schedule with the same identity as {@code schedule} exists in the schedule list. + */ + public boolean hasSchedule(Schedule schedule) { + requireNonNull(schedule); + return schedules.contains(schedule); + } + + /** + * Adds a schedule to the schedule list. + * The schedule must not already exist in the schedule list. + */ + public void addSchedule(Schedule schedule) { + schedules.add(schedule); + } + + /** + * Replaces the given schedule {@code schedule} in the list with {@code editedSchedule}. + * {@code schedule} must exist in the schedule list. + */ + public void updateSchedule(Schedule schedule, Schedule editedSchedule) { + requireNonNull(editedSchedule); + schedules.setSchedule(schedule, editedSchedule); + } + + /** + * Removes {@code key} from this {@code ScheduleList}. + * {@code key} must exist in the schedule list. + */ + public void removeSchedule(Schedule key) { + schedules.remove(key); + } + + /** + * Sort Schedules within CHRS by employeeId + */ + public void sortSchedulesBy() { + schedules.sortByEmployeeId(); + } + + //// util methods + + @Override + public String toString() { + return schedules.asUnmodifiableObservableList().size() + " schedules"; + // TODO: refine later + } + + @Override + public ObservableList getScheduleList() { + return schedules.asUnmodifiableObservableList(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof ScheduleList // instanceof handles nulls + && schedules.equals(((ScheduleList) other).schedules)); + } + + @Override + public int hashCode() { + return schedules.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/schedule/Type.java b/src/main/java/seedu/address/model/schedule/Type.java new file mode 100644 index 000000000000..c7e174e606c3 --- /dev/null +++ b/src/main/java/seedu/address/model/schedule/Type.java @@ -0,0 +1,57 @@ +package seedu.address.model.schedule; +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Schedule's type in the address book. + * Guarantees: immutable; is always valid + */ +public class Type { + + public static final String MESSAGE_TYPE_CONSTRAINTS = + "TYPE should only be WORK or LEAVE, case not sensitive and it should not be blank"; + + /* + * Type must either be WORK or LEAVE, exact match. + */ + public static final String TYPE_VALIDATION_REGEX = "(^LEAVE$)|(^WORK$)"; + public static final String LEAVE = "LEAVE"; + public static final String WORK = "WORK"; + + + public final String value; + public Type(String type) { + requireNonNull(type); + checkArgument(isValidType(type.toUpperCase()), MESSAGE_TYPE_CONSTRAINTS); + value = type.toUpperCase(); + } + @Override + public String toString() { + return value; + } + + /** + * Checks if the Schedule type is valid. + * @param type to be tested with the regular expression. + * @return Boolean, true if matches. + */ + public static boolean isValidType(String type) { + return type.toUpperCase().matches(TYPE_VALIDATION_REGEX); + } + + /** + * Compares if both objects are equal. + * @param other similar object type to be compared with. + * @return Boolean, True if both objects are equal based on the defined conditions. + */ + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Type // instanceof handles nulls + && value.equals(((Type) other).value)); // state check + } + @Override + public int hashCode() { + return value.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/schedule/UniqueScheduleList.java b/src/main/java/seedu/address/model/schedule/UniqueScheduleList.java new file mode 100644 index 000000000000..1d16fa335cd3 --- /dev/null +++ b/src/main/java/seedu/address/model/schedule/UniqueScheduleList.java @@ -0,0 +1,153 @@ +package seedu.address.model.schedule; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; + +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import seedu.address.model.schedule.exceptions.DuplicateScheduleException; +import seedu.address.model.schedule.exceptions.ScheduleNotFoundException; + +/** + * A list of schedules that enforces uniqueness between its elements and does not allow nulls. + * A schedule is considered unique by comparing using {@code Schedule#isSameSchedule(Schedule)}. + * As such, adding and updating of schedules uses Schedule#isSameSchedule(Schedule) for equality + * so as to ensure that the schedule being added or updated is unique in terms of identity in the UniqueScheduleList. + * However, the removal of a schedule uses Schedule#equals(Object) so + * as to ensure that the schedule with exactly the same fields will be removed. + * + * Supports a minimal set of list operations. + * + * @see Schedule#isSameSchedule(Schedule) + */ +public class UniqueScheduleList implements Iterable { + + private final ObservableList internalList = FXCollections.observableArrayList(); + + /** + * Returns true if the list contains an equivalent schedule as the given argument. + */ + public boolean contains(Schedule toCheck) { + requireNonNull(toCheck); + return internalList.stream().anyMatch(toCheck::isSameSchedule); + } + + /** + * Adds a schedule to the list. + * The schedule must not already exist in the list. + */ + public void add(Schedule toAddSchedule) { + requireNonNull(toAddSchedule); + if (contains(toAddSchedule)) { + throw new DuplicateScheduleException(); + } + internalList.add(toAddSchedule); + } + + /** + * Replaces the schedule {@code target} in the list with {@code editedSchedule}. + * {@code target} must exist in the list. + * The schedule identity of {@code editedSchedule} must not be the same as another existing schedule in the list. + */ + public void setSchedule(Schedule target, Schedule editedSchedule) { + requireAllNonNull(target, editedSchedule); + + int index = internalList.indexOf(target); + if (index == -1) { + throw new ScheduleNotFoundException(); + } + + if (!target.isSameSchedule(editedSchedule) && contains(editedSchedule)) { + throw new DuplicateScheduleException(); + } + + internalList.set(index, editedSchedule); + } + + /** + * Removes the equivalent schedule from the list. + * The schedule must exist in the list. + */ + public void remove(Schedule toRemove) { + requireNonNull(toRemove); + if (!internalList.remove(toRemove)) { + throw new ScheduleNotFoundException(); + } + } + + public void setSchedules(UniqueScheduleList replacement) { + requireNonNull(replacement); + internalList.setAll(replacement.internalList); + } + + /** + * Replaces the contents of this list with {@code schedules}. + * {@code schedules} must not contain duplicate schedules. + */ + public void setSchedules(List schedules) { + requireAllNonNull(schedules); + if (!schedulesAreUnique(schedules)) { + throw new DuplicateScheduleException(); + } + + internalList.setAll(schedules); + } + + //Reused from https://github.com/CS2103JAN2018-F14-B1/main/pull/57 with minor modifications + /** + * Sorts ScheduleList by employeeId in ascending order + */ + public void sortByEmployeeId() { + Comparator scheduleComparator = new Comparator() { + @Override + public int compare(Schedule scheduleA, Schedule scheduleB) { + return scheduleA.getEmployeeId().value.compareTo(scheduleB.getEmployeeId().value); + } + }; + + Collections.sort(internalList, scheduleComparator); + } + + /** + * Returns the backing list as an unmodifiable {@code ObservableList}. + */ + public ObservableList asUnmodifiableObservableList() { + return FXCollections.unmodifiableObservableList(internalList); + } + + @Override + public Iterator iterator() { + return internalList.iterator(); + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof UniqueScheduleList // instanceof handles nulls + && internalList.equals(((UniqueScheduleList) other).internalList)); + } + + @Override + public int hashCode() { + return internalList.hashCode(); + } + + /** + * Returns true if {@code schedules} contains only unique schedules. + */ + private boolean schedulesAreUnique(List schedules) { + for (int i = 0; i < schedules.size() - 1; i++) { + for (int j = i + 1; j < schedules.size(); j++) { + if (schedules.get(i).isSameSchedule(schedules.get(j))) { + return false; + } + } + } + return true; + } +} diff --git a/src/main/java/seedu/address/model/schedule/VersionedScheduleList.java b/src/main/java/seedu/address/model/schedule/VersionedScheduleList.java new file mode 100644 index 000000000000..f4def4352615 --- /dev/null +++ b/src/main/java/seedu/address/model/schedule/VersionedScheduleList.java @@ -0,0 +1,109 @@ +package seedu.address.model.schedule; + +import java.util.ArrayList; +import java.util.List; + +/** + * {@code ScheduleList} that keeps track of its own history. + */ +public class VersionedScheduleList extends ScheduleList { + + private final List scheduleListStateList; + private int currentStatePointer; + + public VersionedScheduleList(ReadOnlyScheduleList initialState) { + super(initialState); + + scheduleListStateList = new ArrayList<>(); + scheduleListStateList.add(new ScheduleList(initialState)); + currentStatePointer = 0; + } + + /** + * Saves a copy of the current {@code ScheduleList} state at the end of the state list. + * Undone states are removed from the state list. + */ + public void commit() { + removeStatesAfterCurrentPointer(); + scheduleListStateList.add(new ScheduleList(this)); + currentStatePointer++; + } + + private void removeStatesAfterCurrentPointer() { + scheduleListStateList.subList(currentStatePointer + 1, scheduleListStateList.size()).clear(); + } + + /** + * Restores the schedule list to its previous state. + */ + public void undo() { + if (!canUndo()) { + throw new NoUndoableStateException(); + } + currentStatePointer--; + resetData(scheduleListStateList.get(currentStatePointer)); + } + + /** + * Restores the schedule list to its previously undone state. + */ + public void redo() { + if (!canRedo()) { + throw new NoRedoableStateException(); + } + currentStatePointer++; + resetData(scheduleListStateList.get(currentStatePointer)); + } + + /** + * Returns true if {@code undo()} has schedule list states to undo. + */ + public boolean canUndo() { + return currentStatePointer > 0; + } + + /** + * Returns true if {@code redo()} has schedule list states to redo. + */ + public boolean canRedo() { + return currentStatePointer < scheduleListStateList.size() - 1; + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof VersionedScheduleList)) { + return false; + } + + VersionedScheduleList otherVersionedScheduleList = (VersionedScheduleList) other; + + // state check + return super.equals(otherVersionedScheduleList) + && scheduleListStateList.equals(otherVersionedScheduleList.scheduleListStateList) + && currentStatePointer == otherVersionedScheduleList.currentStatePointer; + } + + /** + * Thrown when trying to {@code undo()} but can't. + */ + public static class NoUndoableStateException extends RuntimeException { + private NoUndoableStateException() { + super("Current state pointer at start of scheduleListState list, unable to undo."); + } + } + + /** + * Thrown when trying to {@code redo()} but can't. + */ + public static class NoRedoableStateException extends RuntimeException { + private NoRedoableStateException() { + super("Current state pointer at end of scheduleListState list, unable to redo."); + } + } +} diff --git a/src/main/java/seedu/address/model/schedule/Year.java b/src/main/java/seedu/address/model/schedule/Year.java new file mode 100644 index 000000000000..b5a46c8a1906 --- /dev/null +++ b/src/main/java/seedu/address/model/schedule/Year.java @@ -0,0 +1,58 @@ +package seedu.address.model.schedule; + +import static java.util.Objects.requireNonNull; + +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Schedule's year in the schedule List. + * Guarantees: immutable; is valid as declared in {@link #isValidYear(String)} + */ +public class Year { + public static final String MESSAGE_YEAR_CONSTRAINTS = + "Year should only be integers between 2000 to 2099"; + public static final String YEAR_VALIDATION_REGEX = "^(20)\\d\\d$"; + public final String value; + + /** + * Constructs a {@code year}. + * @param year A valid year. + */ + + public Year (String year) { + requireNonNull(year); + checkArgument(isValidYear(year), MESSAGE_YEAR_CONSTRAINTS); + value = year; + } + + /** + * Checks if the year is valid. + * @param year to be tested with the regular expression + * @return Boolean, true if matches. + */ + public static boolean isValidYear(String year) { + return year.matches(YEAR_VALIDATION_REGEX); + } + + @Override + public String toString() { + return value; + } + + /** + * Compares if both objects are equal. + * @param other similar object type to be compared with. + * @return Boolean, True if both objects are equal based on the defined conditions. + */ + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof Year // instanceof handles nulls + && value.equals(((Year) other).value)); // state check + } + + @Override + public int hashCode() { + return value.hashCode(); + } +} diff --git a/src/main/java/seedu/address/model/schedule/exceptions/DuplicateScheduleException.java b/src/main/java/seedu/address/model/schedule/exceptions/DuplicateScheduleException.java new file mode 100644 index 000000000000..f715d0f1c59a --- /dev/null +++ b/src/main/java/seedu/address/model/schedule/exceptions/DuplicateScheduleException.java @@ -0,0 +1,11 @@ +package seedu.address.model.schedule.exceptions; + +/** + * Signals that the operation will result in duplicate Schedules (Schedules are considered duplicates if they have the + * same identity). + */ +public class DuplicateScheduleException extends RuntimeException { + public DuplicateScheduleException() { + super("Operation would result in duplicate schedule"); + } +} diff --git a/src/main/java/seedu/address/model/schedule/exceptions/ScheduleNotFoundException.java b/src/main/java/seedu/address/model/schedule/exceptions/ScheduleNotFoundException.java new file mode 100644 index 000000000000..5bb4e0e98ec5 --- /dev/null +++ b/src/main/java/seedu/address/model/schedule/exceptions/ScheduleNotFoundException.java @@ -0,0 +1,7 @@ +package seedu.address.model.schedule.exceptions; + +/** + * Signals that the operation is unable to find the schedule for the specified Employee schedule Id. + */ +public class ScheduleNotFoundException extends RuntimeException { +} diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java index 1806da4facfa..76705fb4e6c9 100644 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ b/src/main/java/seedu/address/model/util/SampleDataUtil.java @@ -4,39 +4,63 @@ import java.util.Set; import java.util.stream.Collectors; -import seedu.address.model.AddressBook; -import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.addressbook.AddressBook; +import seedu.address.model.addressbook.ReadOnlyAddressBook; import seedu.address.model.person.Address; +import seedu.address.model.person.Bonus; +import seedu.address.model.person.DateOfBirth; +import seedu.address.model.person.Department; import seedu.address.model.person.Email; +import seedu.address.model.person.EmployeeId; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; +import seedu.address.model.person.Position; +import seedu.address.model.person.Salary; +import seedu.address.model.person.tag.Tag; /** * Contains utility methods for populating {@code AddressBook} with sample data. */ public class SampleDataUtil { + + public static final Email SAMPLE_EMAIL = new Email ("bobbyyeoh@example.com"); + public static final Name SAMPLE_NAME = new Name("Bobby"); + public static final Phone SAMPLE_PHONE = new Phone ("81234567"); + public static final Address SAMPLE_ADDRESS = new Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18"); + public static final DateOfBirth SAMPLE_DATEOFBIRTH = new DateOfBirth("12/12/1995"); + public static final Department SAMPLE_DEPARTMENT = new Department("Human Resource"); + public static final Position SAMPLE_POSITION = new Position("Intern"); + public static final Salary SAMPLE_SALARY = new Salary("1000.00"); + public static final Bonus SAMPLE_BONUS = new Bonus("00.00"); + public static Person[] getSamplePersons() { + return new Person[] { - new Person(new Name("Alex Yeoh"), new Phone("87438807"), new Email("alexyeoh@example.com"), - new Address("Blk 30 Geylang Street 29, #06-40"), - getTagSet("friends")), - new Person(new Name("Bernice Yu"), new Phone("99272758"), new Email("berniceyu@example.com"), - new Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), - getTagSet("colleagues", "friends")), - new Person(new Name("Charlotte Oliveiro"), new Phone("93210283"), new Email("charlotte@example.com"), - new Address("Blk 11 Ang Mo Kio Street 74, #11-04"), - getTagSet("neighbours")), - new Person(new Name("David Li"), new Phone("91031282"), new Email("lidavid@example.com"), - new Address("Blk 436 Serangoon Gardens Street 26, #16-43"), - getTagSet("family")), - new Person(new Name("Irfan Ibrahim"), new Phone("92492021"), new Email("irfan@example.com"), - new Address("Blk 47 Tampines Street 20, #17-35"), - getTagSet("classmates")), - new Person(new Name("Roy Balakrishnan"), new Phone("92624417"), new Email("royb@example.com"), - new Address("Blk 45 Aljunied Street 85, #11-31"), - getTagSet("colleagues")) + new Person(new EmployeeId("000001"), new Name("Alex Yeoh"), new DateOfBirth("31/10/1975"), + new Phone("87438807"), new Email("alexyeoh@example.com"), new Department("Finance"), + new Position("Director"), new Address("Blk 30 Geylang Street 29, #06-40"), new Salary("8000.00"), + SAMPLE_BONUS, getTagSet("Fishing")), + new Person(new EmployeeId("000002"), new Name("Bernice Yu"), new DateOfBirth("26/03/2000"), + new Phone("99272758"), new Email("berniceyu@example.com"), new Department("IT"), new Position("Intern"), + new Address("Blk 30 Lorong 3 Serangoon Gardens, #07-18"), new Salary("1000.00"), SAMPLE_BONUS, + getTagSet("Cycling")), + new Person(new EmployeeId("000003"), new Name("Charlotte Oliveiro"), new DateOfBirth("13/06/1990"), + new Phone("93210283"), new Email("charlotte@example.com"), new Department("Finance"), + new Position("Manager"), new Address("Blk 11 Ang Mo Kio Street 74, #11-04"), new Salary("4000.00"), + SAMPLE_BONUS, getTagSet("Cooking")), + new Person(new EmployeeId("000004"), new Name("David Li"), new DateOfBirth("18/08/1999"), + new Phone("91031282"), new Email("lidavid@example.com"), new Department("Human Resource"), + new Position("Intern"), new Address("Blk 436 Serangoon Gardens Street 26, #16-43"), + new Salary("800.00"), SAMPLE_BONUS, getTagSet("FlyKite")), + new Person(new EmployeeId("000005"), new Name("Irfan Ibrahim"), new DateOfBirth("01/03/1965"), + new Phone("92492021"), new Email("irfan@example.com"), new Department("IT"), new Position("Director"), + new Address("Blk 47 Tampines Street 20, #17-35"), new Salary("10000.00"), SAMPLE_BONUS, + getTagSet("Pool")), + new Person(new EmployeeId("000006"), new Name("Roy Balakrishnan"), new DateOfBirth("16/06/1987"), + new Phone("92624417"), new Email("royb@example.com"), new Department("Human Resource"), + new Position("Manager"), new Address("Blk 45 Aljunied Street 85, #11-31"), new Salary("5000.00"), + SAMPLE_BONUS, getTagSet("Soccer")) }; } diff --git a/src/main/java/seedu/address/storage/Storage.java b/src/main/java/seedu/address/storage/Storage.java index 28791127999b..4c9a0d99ae3b 100644 --- a/src/main/java/seedu/address/storage/Storage.java +++ b/src/main/java/seedu/address/storage/Storage.java @@ -5,15 +5,27 @@ import java.util.Optional; import seedu.address.commons.events.model.AddressBookChangedEvent; +import seedu.address.commons.events.model.ExpensesListChangedEvent; +import seedu.address.commons.events.model.RecruitmentListChangedEvent; +import seedu.address.commons.events.model.ScheduleListChangedEvent; import seedu.address.commons.events.storage.DataSavingExceptionEvent; import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.UserPrefs; +import seedu.address.model.addressbook.ReadOnlyAddressBook; +import seedu.address.model.expenses.ReadOnlyExpensesList; +import seedu.address.model.recruitment.ReadOnlyRecruitmentList; +import seedu.address.model.schedule.ReadOnlyScheduleList; +import seedu.address.storage.addressbook.AddressBookStorage; +import seedu.address.storage.expenses.ExpensesListStorage; +import seedu.address.storage.recruitment.RecruitmentListStorage; +import seedu.address.storage.schedule.ScheduleListStorage; +import seedu.address.storage.userpref.UserPrefsStorage; /** * API of the Storage component */ -public interface Storage extends AddressBookStorage, UserPrefsStorage { +public interface Storage extends AddressBookStorage, ExpensesListStorage, RecruitmentListStorage, UserPrefsStorage, + ScheduleListStorage { @Override Optional readUserPrefs() throws DataConversionException, IOException; @@ -24,16 +36,55 @@ public interface Storage extends AddressBookStorage, UserPrefsStorage { @Override Path getAddressBookFilePath(); + @Override + Path getExpensesListFilePath(); + + @Override + Path getScheduleListFilePath(); + + @Override + Path getRecruitmentListFilePath(); + @Override Optional readAddressBook() throws DataConversionException, IOException; + @Override + Optional readExpensesList() throws DataConversionException, IOException; + + @Override + Optional readScheduleList() throws DataConversionException, IOException; + + @Override + Optional readRecruitmentList() throws DataConversionException, IOException; + @Override void saveAddressBook(ReadOnlyAddressBook addressBook) throws IOException; + @Override + void saveExpensesList(ReadOnlyExpensesList expensesList) throws IOException; + + @Override + void saveScheduleList(ReadOnlyScheduleList scheduleList) throws IOException; + + @Override + void saveRecruitmentList(ReadOnlyRecruitmentList scheduleList) throws IOException; + /** * Saves the current version of the Address Book to the hard disk. * Creates the data file if it is missing. * Raises {@link DataSavingExceptionEvent} if there was an error during saving. */ void handleAddressBookChangedEvent(AddressBookChangedEvent abce); + + /** + * Saves the current version of the Expenses List to the hard disk. + * Creates the data file if it is missing. + * Raises {@link DataSavingExceptionEvent} if there was an error during saving. + */ + + void handleExpensesListChangedEvent(ExpensesListChangedEvent elce); + + void handleScheduleListChangedEvent(ScheduleListChangedEvent abce); + + void handleRecruitmentListChangedEvent(RecruitmentListChangedEvent abce); } diff --git a/src/main/java/seedu/address/storage/StorageManager.java b/src/main/java/seedu/address/storage/StorageManager.java index b0df908a76a7..9bd06123ac2b 100644 --- a/src/main/java/seedu/address/storage/StorageManager.java +++ b/src/main/java/seedu/address/storage/StorageManager.java @@ -10,10 +10,21 @@ import seedu.address.commons.core.ComponentManager; import seedu.address.commons.core.LogsCenter; import seedu.address.commons.events.model.AddressBookChangedEvent; +import seedu.address.commons.events.model.ExpensesListChangedEvent; +import seedu.address.commons.events.model.RecruitmentListChangedEvent; +import seedu.address.commons.events.model.ScheduleListChangedEvent; import seedu.address.commons.events.storage.DataSavingExceptionEvent; import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.UserPrefs; +import seedu.address.model.addressbook.ReadOnlyAddressBook; +import seedu.address.model.expenses.ReadOnlyExpensesList; +import seedu.address.model.recruitment.ReadOnlyRecruitmentList; +import seedu.address.model.schedule.ReadOnlyScheduleList; +import seedu.address.storage.addressbook.AddressBookStorage; +import seedu.address.storage.expenses.ExpensesListStorage; +import seedu.address.storage.recruitment.RecruitmentListStorage; +import seedu.address.storage.schedule.ScheduleListStorage; +import seedu.address.storage.userpref.UserPrefsStorage; /** * Manages storage of AddressBook data in local storage. @@ -22,13 +33,22 @@ public class StorageManager extends ComponentManager implements Storage { private static final Logger logger = LogsCenter.getLogger(StorageManager.class); private AddressBookStorage addressBookStorage; + private ExpensesListStorage expensesListStorage; + private ScheduleListStorage scheduleListStorage; + private RecruitmentListStorage recruitmentListStorage; private UserPrefsStorage userPrefsStorage; - public StorageManager(AddressBookStorage addressBookStorage, UserPrefsStorage userPrefsStorage) { + public StorageManager(AddressBookStorage addressBookStorage, ExpensesListStorage expensesListStorage, + ScheduleListStorage scheduleListStorage, + RecruitmentListStorage recruitmentListStorage, + UserPrefsStorage userPrefsStorage) { super(); this.addressBookStorage = addressBookStorage; + this.expensesListStorage = expensesListStorage; this.userPrefsStorage = userPrefsStorage; + this.scheduleListStorage = scheduleListStorage; + this.recruitmentListStorage = recruitmentListStorage; } // ================ UserPrefs methods ============================== @@ -90,4 +110,118 @@ public void handleAddressBookChangedEvent(AddressBookChangedEvent event) { } } + // ================ ExpensesList methods ============================== + @Override + public Path getExpensesListFilePath() { + return expensesListStorage.getExpensesListFilePath(); + } + @Override + public Optional readExpensesList() throws DataConversionException, IOException { + return readExpensesList(expensesListStorage.getExpensesListFilePath()); + } + @Override + public Optional readExpensesList(Path filePath) throws DataConversionException, IOException { + logger.fine("Attempting to read data from file: " + filePath); + return expensesListStorage.readExpensesList(filePath); + } + @Override + public void saveExpensesList(ReadOnlyExpensesList expensesList) throws IOException { + saveExpensesList(expensesList, expensesListStorage.getExpensesListFilePath()); + } + @Override + public void saveExpensesList(ReadOnlyExpensesList expensesList, Path filePath) throws IOException { + logger.fine("Attempting to write to data file: " + filePath); + expensesListStorage.saveExpensesList(expensesList, filePath); + } + @Override + @Subscribe + public void handleExpensesListChangedEvent(ExpensesListChangedEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event, "Local data changed, saving to file")); + try { + saveExpensesList(event.data); + } catch (IOException e) { + raise(new DataSavingExceptionEvent(e)); + } + } + + + // ================ RecruitmentList methods ============================== + @Override + public Path getRecruitmentListFilePath() { + return recruitmentListStorage.getRecruitmentListFilePath(); + } + + @Override + public Optional readRecruitmentList() throws DataConversionException, IOException { + return readRecruitmentList(recruitmentListStorage.getRecruitmentListFilePath()); + } + @Override + public Optional readRecruitmentList(Path filePath) throws DataConversionException, + IOException { + logger.fine("Attempting to read data from file: " + filePath); + return recruitmentListStorage.readRecruitmentList(filePath); + } + @Override + public void saveRecruitmentList(ReadOnlyRecruitmentList recruitmentList) throws IOException { + saveRecruitmentList(recruitmentList, recruitmentListStorage.getRecruitmentListFilePath()); + } + @Override + public void saveRecruitmentList(ReadOnlyRecruitmentList recruitmentList, Path filePath) throws IOException { + logger.fine("Attempting to write to data file: " + filePath); + recruitmentListStorage.saveRecruitmentList(recruitmentList, filePath); + } + + @Override + @Subscribe + public void handleRecruitmentListChangedEvent(RecruitmentListChangedEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event, "Local data changed, saving to file")); + try { + saveRecruitmentList(event.data); + } catch (IOException e) { + raise(new DataSavingExceptionEvent(e)); + } + } + + // ================ ScheduleList methods ============================== + + @Override + public Path getScheduleListFilePath() { + return scheduleListStorage.getScheduleListFilePath(); + } + + @Override + public Optional readScheduleList() throws DataConversionException, IOException { + return readScheduleList(scheduleListStorage.getScheduleListFilePath()); + } + + @Override + public Optional readScheduleList(Path filePath) throws DataConversionException, IOException { + logger.fine("Attempting to read data from file: " + filePath); + return scheduleListStorage.readScheduleList(filePath); + } + + @Override + public void saveScheduleList(ReadOnlyScheduleList scheduleList) throws IOException { + saveScheduleList(scheduleList, scheduleListStorage.getScheduleListFilePath()); + } + + @Override + public void saveScheduleList(ReadOnlyScheduleList scheduleList, Path filePath) throws IOException { + logger.fine("Attempting to write to data file: " + filePath); + scheduleListStorage.saveScheduleList(scheduleList, filePath); + } + + + @Override + @Subscribe + public void handleScheduleListChangedEvent(ScheduleListChangedEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event, "Local data changed, saving to file")); + + try { + saveScheduleList(event.data); + } catch (IOException e) { + raise(new DataSavingExceptionEvent(e)); + } + } + } diff --git a/src/main/java/seedu/address/storage/AddressBookStorage.java b/src/main/java/seedu/address/storage/addressbook/AddressBookStorage.java similarity index 86% rename from src/main/java/seedu/address/storage/AddressBookStorage.java rename to src/main/java/seedu/address/storage/addressbook/AddressBookStorage.java index 4599182b3f92..35dad0ace2d5 100644 --- a/src/main/java/seedu/address/storage/AddressBookStorage.java +++ b/src/main/java/seedu/address/storage/addressbook/AddressBookStorage.java @@ -1,14 +1,15 @@ -package seedu.address.storage; +package seedu.address.storage.addressbook; import java.io.IOException; import java.nio.file.Path; import java.util.Optional; import seedu.address.commons.exceptions.DataConversionException; -import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.addressbook.AddressBook; +import seedu.address.model.addressbook.ReadOnlyAddressBook; /** - * Represents a storage for {@link seedu.address.model.AddressBook}. + * Represents a storage for {@link AddressBook}. */ public interface AddressBookStorage { diff --git a/src/main/java/seedu/address/storage/XmlAdaptedPerson.java b/src/main/java/seedu/address/storage/addressbook/XmlAdaptedPerson.java similarity index 50% rename from src/main/java/seedu/address/storage/XmlAdaptedPerson.java rename to src/main/java/seedu/address/storage/addressbook/XmlAdaptedPerson.java index c03785e5700f..1bae228bffb3 100644 --- a/src/main/java/seedu/address/storage/XmlAdaptedPerson.java +++ b/src/main/java/seedu/address/storage/addressbook/XmlAdaptedPerson.java @@ -1,4 +1,4 @@ -package seedu.address.storage; +package seedu.address.storage.addressbook; import java.util.ArrayList; import java.util.HashSet; @@ -11,11 +11,17 @@ import seedu.address.commons.exceptions.IllegalValueException; import seedu.address.model.person.Address; +import seedu.address.model.person.Bonus; +import seedu.address.model.person.DateOfBirth; +import seedu.address.model.person.Department; import seedu.address.model.person.Email; +import seedu.address.model.person.EmployeeId; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; +import seedu.address.model.person.Position; +import seedu.address.model.person.Salary; +import seedu.address.model.person.tag.Tag; /** * JAXB-friendly version of the Person. @@ -24,14 +30,26 @@ public class XmlAdaptedPerson { public static final String MISSING_FIELD_MESSAGE_FORMAT = "Person's %s field is missing!"; + @XmlElement(required = true) + private String employeeId; @XmlElement(required = true) private String name; + @XmlElement (required = true) + private String dateOfBirth; @XmlElement(required = true) private String phone; @XmlElement(required = true) private String email; @XmlElement(required = true) + private String department; + @XmlElement(required = true) + private String position; + @XmlElement(required = true) private String address; + @XmlElement(required = true) + private String salary; + @XmlElement + private String bonus; @XmlElement private List tagged = new ArrayList<>(); @@ -45,11 +63,19 @@ public XmlAdaptedPerson() {} /** * Constructs an {@code XmlAdaptedPerson} with the given person details. */ - public XmlAdaptedPerson(String name, String phone, String email, String address, List tagged) { + public XmlAdaptedPerson(String employeeId, String name, String dateOfBirth, String phone, String email, + String department, String position, String address, String salary, String bonus, + List tagged) { + this.employeeId = employeeId; this.name = name; + this.dateOfBirth = dateOfBirth; this.phone = phone; this.email = email; + this.department = department; + this.position = position; this.address = address; + this.salary = salary; + this.bonus = bonus; if (tagged != null) { this.tagged = new ArrayList<>(tagged); } @@ -61,10 +87,16 @@ public XmlAdaptedPerson(String name, String phone, String email, String address, * @param source future changes to this will not affect the created XmlAdaptedPerson */ public XmlAdaptedPerson(Person source) { + employeeId = source.getEmployeeId().value; name = source.getName().fullName; + dateOfBirth = source.getDateOfBirth().value; phone = source.getPhone().value; email = source.getEmail().value; + department = source.getDepartment().value; + position = source.getPosition().value; address = source.getAddress().value; + salary = source.getSalary().value; + bonus = source.getBonus().value; tagged = source.getTags().stream() .map(XmlAdaptedTag::new) .collect(Collectors.toList()); @@ -81,6 +113,15 @@ public Person toModelType() throws IllegalValueException { personTags.add(tag.toModelType()); } + if (employeeId == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + EmployeeId.class.getSimpleName())); + } + if (!EmployeeId.isValidEmployeeId(employeeId)) { + throw new IllegalValueException(EmployeeId.MESSAGE_EMPLOYEEID_CONSTRAINTS); + } + final EmployeeId modelEmployeeId = new EmployeeId(employeeId); + if (name == null) { throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName())); } @@ -89,6 +130,15 @@ public Person toModelType() throws IllegalValueException { } final Name modelName = new Name(name); + if (dateOfBirth == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + DateOfBirth.class.getSimpleName())); + } + if (!DateOfBirth.isValidDateOfBirth(dateOfBirth)) { + throw new IllegalValueException(DateOfBirth.getMessageDateOfBirthConstraints()); + } + final DateOfBirth modelDateOfBirth = new DateOfBirth(dateOfBirth); + if (phone == null) { throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Phone.class.getSimpleName())); } @@ -105,6 +155,24 @@ public Person toModelType() throws IllegalValueException { } final Email modelEmail = new Email(email); + if (department == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + Department.class.getSimpleName())); + } + if (!Department.isValidDepartment(department)) { + throw new IllegalValueException(Department.MESSAGE_DEPARTMENT_CONSTRAINTS); + } + final Department modelDepartment = new Department(department); + + if (position == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + Position.class.getSimpleName())); + } + if (!Position.isValidPosition(position)) { + throw new IllegalValueException(Position.MESSAGE_POSITION_CONSTRAINTS); + } + final Position modelPosition = new Position(position); + if (address == null) { throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Address.class.getSimpleName())); } @@ -113,8 +181,18 @@ public Person toModelType() throws IllegalValueException { } final Address modelAddress = new Address(address); + if (salary == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Salary.class.getSimpleName())); + } + if (!Salary.isValidSalary(salary)) { + throw new IllegalValueException(Salary.MESSAGE_SALARY_CONSTRAINTS); + } + final Salary modelSalary = new Salary(salary); + + final Bonus modelBonus = new Bonus(bonus); final Set modelTags = new HashSet<>(personTags); - return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags); + return new Person(modelEmployeeId, modelName, modelDateOfBirth, modelPhone, modelEmail, modelDepartment, + modelPosition, modelAddress, modelSalary, modelBonus, modelTags); } @Override @@ -128,10 +206,16 @@ public boolean equals(Object other) { } XmlAdaptedPerson otherPerson = (XmlAdaptedPerson) other; - return Objects.equals(name, otherPerson.name) + return Objects.equals(employeeId, otherPerson.employeeId) + && Objects.equals(name, otherPerson.name) + && Objects.equals(dateOfBirth, otherPerson.dateOfBirth) && Objects.equals(phone, otherPerson.phone) && Objects.equals(email, otherPerson.email) + && Objects.equals(department, otherPerson.department) + && Objects.equals(position, otherPerson.position) && Objects.equals(address, otherPerson.address) + && Objects.equals(salary, otherPerson.salary) + && Objects.equals(bonus, otherPerson.bonus) && tagged.equals(otherPerson.tagged); } } diff --git a/src/main/java/seedu/address/storage/XmlAdaptedTag.java b/src/main/java/seedu/address/storage/addressbook/XmlAdaptedTag.java similarity index 94% rename from src/main/java/seedu/address/storage/XmlAdaptedTag.java rename to src/main/java/seedu/address/storage/addressbook/XmlAdaptedTag.java index d3e2d8be9c4f..500b49b9e470 100644 --- a/src/main/java/seedu/address/storage/XmlAdaptedTag.java +++ b/src/main/java/seedu/address/storage/addressbook/XmlAdaptedTag.java @@ -1,9 +1,9 @@ -package seedu.address.storage; +package seedu.address.storage.addressbook; import javax.xml.bind.annotation.XmlValue; import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.tag.Tag; +import seedu.address.model.person.tag.Tag; /** * JAXB-friendly adapted version of the Tag. diff --git a/src/main/java/seedu/address/storage/XmlFileStorage.java b/src/main/java/seedu/address/storage/addressbook/XmlAddressBookFileStorage.java similarity index 89% rename from src/main/java/seedu/address/storage/XmlFileStorage.java rename to src/main/java/seedu/address/storage/addressbook/XmlAddressBookFileStorage.java index d8f65dc036ab..7eaa2c227b2f 100644 --- a/src/main/java/seedu/address/storage/XmlFileStorage.java +++ b/src/main/java/seedu/address/storage/addressbook/XmlAddressBookFileStorage.java @@ -1,4 +1,4 @@ -package seedu.address.storage; +package seedu.address.storage.addressbook; import java.io.FileNotFoundException; import java.nio.file.Path; @@ -7,11 +7,11 @@ import seedu.address.commons.exceptions.DataConversionException; import seedu.address.commons.util.XmlUtil; - /** * Stores addressbook data in an XML file */ -public class XmlFileStorage { +public class XmlAddressBookFileStorage { + /** * Saves the given addressbook data to the specified file. */ @@ -28,7 +28,7 @@ public static void saveDataToFile(Path file, XmlSerializableAddressBook addressB * Returns address book in the file or an empty address book */ public static XmlSerializableAddressBook loadDataFromSaveFile(Path file) throws DataConversionException, - FileNotFoundException { + FileNotFoundException { try { return XmlUtil.getDataFromFile(file, XmlSerializableAddressBook.class); } catch (JAXBException e) { @@ -36,4 +36,5 @@ public static XmlSerializableAddressBook loadDataFromSaveFile(Path file) throws } } + } diff --git a/src/main/java/seedu/address/storage/XmlAddressBookStorage.java b/src/main/java/seedu/address/storage/addressbook/XmlAddressBookStorage.java similarity index 88% rename from src/main/java/seedu/address/storage/XmlAddressBookStorage.java rename to src/main/java/seedu/address/storage/addressbook/XmlAddressBookStorage.java index ecf0e7ec23a8..a115e30e1f9b 100644 --- a/src/main/java/seedu/address/storage/XmlAddressBookStorage.java +++ b/src/main/java/seedu/address/storage/addressbook/XmlAddressBookStorage.java @@ -1,4 +1,4 @@ -package seedu.address.storage; +package seedu.address.storage.addressbook; import static java.util.Objects.requireNonNull; @@ -13,7 +13,7 @@ import seedu.address.commons.exceptions.DataConversionException; import seedu.address.commons.exceptions.IllegalValueException; import seedu.address.commons.util.FileUtil; -import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.addressbook.ReadOnlyAddressBook; /** * A class to access AddressBook data stored as an xml file on the hard disk. @@ -51,7 +51,7 @@ public Optional readAddressBook(Path filePath) throws DataC return Optional.empty(); } - XmlSerializableAddressBook xmlAddressBook = XmlFileStorage.loadDataFromSaveFile(filePath); + XmlSerializableAddressBook xmlAddressBook = XmlAddressBookFileStorage.loadDataFromSaveFile(filePath); try { return Optional.of(xmlAddressBook.toModelType()); } catch (IllegalValueException ive) { @@ -74,7 +74,7 @@ public void saveAddressBook(ReadOnlyAddressBook addressBook, Path filePath) thro requireNonNull(filePath); FileUtil.createIfMissing(filePath); - XmlFileStorage.saveDataToFile(filePath, new XmlSerializableAddressBook(addressBook)); + XmlAddressBookFileStorage.saveDataToFile(filePath, new XmlSerializableAddressBook(addressBook)); } } diff --git a/src/main/java/seedu/address/storage/XmlSerializableAddressBook.java b/src/main/java/seedu/address/storage/addressbook/XmlSerializableAddressBook.java similarity index 92% rename from src/main/java/seedu/address/storage/XmlSerializableAddressBook.java rename to src/main/java/seedu/address/storage/addressbook/XmlSerializableAddressBook.java index b85fa4a8f07e..ea351a633dc0 100644 --- a/src/main/java/seedu/address/storage/XmlSerializableAddressBook.java +++ b/src/main/java/seedu/address/storage/addressbook/XmlSerializableAddressBook.java @@ -1,4 +1,4 @@ -package seedu.address.storage; +package seedu.address.storage.addressbook; import java.util.ArrayList; import java.util.List; @@ -8,8 +8,8 @@ import javax.xml.bind.annotation.XmlRootElement; import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.AddressBook; -import seedu.address.model.ReadOnlyAddressBook; +import seedu.address.model.addressbook.AddressBook; +import seedu.address.model.addressbook.ReadOnlyAddressBook; import seedu.address.model.person.Person; /** diff --git a/src/main/java/seedu/address/storage/expenses/ExpensesListStorage.java b/src/main/java/seedu/address/storage/expenses/ExpensesListStorage.java new file mode 100644 index 000000000000..41c05a6c93fe --- /dev/null +++ b/src/main/java/seedu/address/storage/expenses/ExpensesListStorage.java @@ -0,0 +1,44 @@ +package seedu.address.storage.expenses; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; + +import seedu.address.commons.exceptions.DataConversionException; +import seedu.address.model.expenses.ReadOnlyExpensesList; + +/** + * Represents a storage for {@link seedu.address.model.expenses.ExpensesList}. + */ +public interface ExpensesListStorage { + /** + * Returns the file path of the data file. + */ + Path getExpensesListFilePath(); + + /** + * Returns ExpensesList data as a {@link ReadOnlyExpensesList}. + * Returns {@code Optional.empty()} if storage file is not found. + * @throws DataConversionException if the data in storage is not in the expected format. + * @throws IOException if there was any problem when reading from the storage. + */ + Optional readExpensesList() throws DataConversionException, IOException; + + /** + * @see #getExpensesListFilePath() + */ + Optional readExpensesList(Path filePath) throws DataConversionException, IOException; + + /** + * Saves the given {@link ReadOnlyExpensesList} to the storage. + * @param expensesList cannot be null. + * @throws IOException if there was any problem writing to the file. + */ + void saveExpensesList(ReadOnlyExpensesList expensesList) throws IOException; + + /** + * @see #saveExpensesList(ReadOnlyExpensesList) + */ + void saveExpensesList(ReadOnlyExpensesList expensesList, Path filePath) throws IOException; + +} diff --git a/src/main/java/seedu/address/storage/expenses/XmlAdaptedExpenses.java b/src/main/java/seedu/address/storage/expenses/XmlAdaptedExpenses.java new file mode 100644 index 000000000000..9956f2b0776c --- /dev/null +++ b/src/main/java/seedu/address/storage/expenses/XmlAdaptedExpenses.java @@ -0,0 +1,132 @@ +package seedu.address.storage.expenses; + +import java.util.Objects; + +import javax.xml.bind.annotation.XmlElement; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.expenses.Expenses; +import seedu.address.model.expenses.ExpensesAmount; +import seedu.address.model.expenses.MedicalExpenses; +import seedu.address.model.expenses.MiscellaneousExpenses; +import seedu.address.model.expenses.TravelExpenses; +import seedu.address.model.person.EmployeeId; + +/** + * JAXB-friendly version of the Expenses. + */ +public class XmlAdaptedExpenses { + public static final String MISSING_FIELD_MESSAGE_FORMAT = "Expenses's %s field is missing!"; + + @XmlElement(required = true) + private String id; + @XmlElement(required = true) + private String expensesAmount; + @XmlElement + private String travelExpenses; + @XmlElement + private String medicalExpenses; + @XmlElement + private String miscellaneousExpenses; + + /** + * Constructs an XmlAdaptedExpenses. + * This is the no-arg constructor that is required by JAXB. + */ + public XmlAdaptedExpenses() {} + + /** + * Constructs an {@code XmlAdaptedExpenses} with the given expenses details. + */ + public XmlAdaptedExpenses(String id, String expensesAmount, String travelExpenses, String medicalExpenses, + String miscellaneousExpenses) { + this.id = id; + this.expensesAmount = expensesAmount; + this.travelExpenses = travelExpenses; + this.medicalExpenses = medicalExpenses; + this.miscellaneousExpenses = miscellaneousExpenses; + } + + /** + * Converts a given expenses into this class for JAXB use. + * + * @param source future changes to this will not affect the created XmlAdaptedExpenses + */ + public XmlAdaptedExpenses(Expenses source) { + id = source.getEmployeeId().value; + expensesAmount = source.getExpensesAmount().expensesAmount; + travelExpenses = source.getTravelExpenses().travelExpenses; + medicalExpenses = source.getMedicalExpenses().medicalExpenses; + miscellaneousExpenses = source.getMiscellaneousExpenses().miscellaneousExpenses; + + } + + /** + * Converts this jaxb-friendly adapted expenses object into the model's Expenses object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted expenses + */ + public Expenses toModelType() throws IllegalValueException { + if (id == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + EmployeeId.class.getSimpleName())); + } + if (!EmployeeId.isValidEmployeeId(id)) { + throw new IllegalValueException(EmployeeId.MESSAGE_EMPLOYEEID_CONSTRAINTS); + } + final EmployeeId modelEmployeeId = new EmployeeId(id); + + if (expensesAmount == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + ExpensesAmount.class.getSimpleName())); + } + if (!ExpensesAmount.isValidExpensesAmount(expensesAmount)) { + throw new IllegalValueException(ExpensesAmount.MESSAGE_EXPENSES_AMOUNT_CONSTRAINTS); + } + if (travelExpenses == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + TravelExpenses.class.getSimpleName())); + } + if (!TravelExpenses.isValidTravelExpenses(travelExpenses)) { + throw new IllegalValueException(TravelExpenses.MESSAGE_TRAVEL_EXPENSES_CONSTRAINTS); + } + if (medicalExpenses == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + MedicalExpenses.class.getSimpleName())); + } + if (!MedicalExpenses.isValidMedicalExpenses(medicalExpenses)) { + throw new IllegalValueException(MedicalExpenses.MESSAGE_MEDICAL_EXPENSES_CONSTRAINTS); + } + if (miscellaneousExpenses == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + MiscellaneousExpenses.class.getSimpleName())); + } + if (!MiscellaneousExpenses.isValidMiscellaneousExpenses(miscellaneousExpenses)) { + throw new IllegalValueException(MiscellaneousExpenses.MESSAGE_MISCELLANEOUS_EXPENSES_CONSTRAINTS); + } + final ExpensesAmount modelExpensesAmount = new ExpensesAmount(expensesAmount); + final TravelExpenses modelTravelExpenses = new TravelExpenses(travelExpenses); + final MedicalExpenses modelMedicalExpenses = new MedicalExpenses(medicalExpenses); + final MiscellaneousExpenses modelMiscellaneousExpenses = new MiscellaneousExpenses(miscellaneousExpenses); + return new Expenses(modelEmployeeId, modelExpensesAmount, modelTravelExpenses, modelMedicalExpenses, + modelMiscellaneousExpenses); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof XmlAdaptedExpenses)) { + return false; + } + + XmlAdaptedExpenses otherExpenses = (XmlAdaptedExpenses) other; + return Objects.equals(id, otherExpenses.id) + && Objects.equals(expensesAmount, otherExpenses.expensesAmount) + && Objects.equals(travelExpenses, otherExpenses.travelExpenses) + && Objects.equals(medicalExpenses, otherExpenses.medicalExpenses) + && Objects.equals(miscellaneousExpenses, otherExpenses.miscellaneousExpenses); + } +} diff --git a/src/main/java/seedu/address/storage/expenses/XmlExpensesFileStorage.java b/src/main/java/seedu/address/storage/expenses/XmlExpensesFileStorage.java new file mode 100644 index 000000000000..fd3022d1f7ef --- /dev/null +++ b/src/main/java/seedu/address/storage/expenses/XmlExpensesFileStorage.java @@ -0,0 +1,38 @@ +package seedu.address.storage.expenses; + +import java.io.FileNotFoundException; +import java.nio.file.Path; + +import javax.xml.bind.JAXBException; + +import seedu.address.commons.exceptions.DataConversionException; +import seedu.address.commons.util.XmlUtil; + +/** + * Stores addressbook data in an XML file + */ +public class XmlExpensesFileStorage { + /** + * Saves the given expenses list data to the specified file. + */ + public static void saveDataToFile(Path file, XmlSerializableExpensesList expensesList) + throws FileNotFoundException { + try { + XmlUtil.saveDataToFile(file, expensesList); + } catch (JAXBException e) { + throw new AssertionError("Unexpected exception " + e.getMessage(), e); + } + } + + /** + * Returns expenses list in the file or an empty expenses list + */ + public static XmlSerializableExpensesList loadDataFromExpensesSaveFile(Path file) throws DataConversionException, + FileNotFoundException { + try { + return XmlUtil.getDataFromFile(file, XmlSerializableExpensesList.class); + } catch (JAXBException e) { + throw new DataConversionException(e); + } + } +} diff --git a/src/main/java/seedu/address/storage/expenses/XmlExpensesListStorage.java b/src/main/java/seedu/address/storage/expenses/XmlExpensesListStorage.java new file mode 100644 index 000000000000..d5e3abfe2e3f --- /dev/null +++ b/src/main/java/seedu/address/storage/expenses/XmlExpensesListStorage.java @@ -0,0 +1,80 @@ +package seedu.address.storage.expenses; + +import static java.util.Objects.requireNonNull; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.exceptions.DataConversionException; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.commons.util.FileUtil; +import seedu.address.model.expenses.ReadOnlyExpensesList; + +/** + * A class to access ExpensesList data stored as an xml file on the hard disk. + */ +public class XmlExpensesListStorage implements ExpensesListStorage { + + private static final Logger logger = LogsCenter.getLogger(XmlExpensesListStorage.class); + + private Path filePath; + + public XmlExpensesListStorage(Path filePath) { + this.filePath = filePath; + } + + public Path getExpensesListFilePath() { + return filePath; + } + + @Override + public Optional readExpensesList() throws DataConversionException, IOException { + return readExpensesList(filePath); + } + + /** + * Similar to {@link #readExpensesList()} + * @param filePath location of the data. Cannot be null + * @throws DataConversionException if the file is not in the correct format. + */ + public Optional readExpensesList(Path filePath) throws DataConversionException, + FileNotFoundException { + requireNonNull(filePath); + + if (!Files.exists(filePath)) { + logger.info("ExpensesList file " + filePath + " not found"); + return Optional.empty(); + } + + XmlSerializableExpensesList xmlExpensesList = XmlExpensesFileStorage.loadDataFromExpensesSaveFile(filePath); + try { + return Optional.of(xmlExpensesList.toModelType()); + } catch (IllegalValueException ive) { + logger.info("Illegal values found in " + filePath + ": " + ive.getMessage()); + throw new DataConversionException(ive); + } + } + + @Override + public void saveExpensesList(ReadOnlyExpensesList expensesList) throws IOException { + saveExpensesList(expensesList, filePath); + } + + /** + * Similar to {@link #saveExpensesList(ReadOnlyExpensesList)} + * @param filePath location of the data. Cannot be null + */ + public void saveExpensesList(ReadOnlyExpensesList expensesList, Path filePath) throws IOException { + requireNonNull(expensesList); + requireNonNull(filePath); + + FileUtil.createIfMissing(filePath); + XmlExpensesFileStorage.saveDataToFile(filePath, new XmlSerializableExpensesList(expensesList)); + } + +} diff --git a/src/main/java/seedu/address/storage/expenses/XmlSerializableExpensesList.java b/src/main/java/seedu/address/storage/expenses/XmlSerializableExpensesList.java new file mode 100644 index 000000000000..c201ee8544f5 --- /dev/null +++ b/src/main/java/seedu/address/storage/expenses/XmlSerializableExpensesList.java @@ -0,0 +1,72 @@ +package seedu.address.storage.expenses; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.expenses.Expenses; +import seedu.address.model.expenses.ExpensesList; +import seedu.address.model.expenses.ReadOnlyExpensesList; + +/** + * An Immutable ExpensesList that is serializable to XML format + */ +@XmlRootElement(name = "expenseslist") +public class XmlSerializableExpensesList { + + public static final String MESSAGE_DUPLICATE_EXPENSES = "Expenses list contains duplicate expenses)."; + + @XmlElement + private List multiExpenses; + + /** + * Creates an empty XmlSerializableExpensesList. + * This empty constructor is required for marshalling. + */ + public XmlSerializableExpensesList() { + multiExpenses = new ArrayList<>(); + } + + /** + * Conversion + */ + public XmlSerializableExpensesList(ReadOnlyExpensesList src) { + this(); + multiExpenses.addAll(src.getExpensesRequestList().stream().map(XmlAdaptedExpenses::new) + .collect(Collectors.toList())); + } + + /** + * Converts this expenseslist into the model's {@code ExpensesList} object. + * + * @throws IllegalValueException if there were any data constraints violated or duplicates in the + * {@code XmlAdaptedExpenses}. + */ + public ExpensesList toModelType() throws IllegalValueException { + ExpensesList expensesList = new ExpensesList(); + for (XmlAdaptedExpenses e : multiExpenses) { + Expenses expenses = e.toModelType(); + if (expensesList.hasExpenses(expenses)) { + throw new IllegalValueException(MESSAGE_DUPLICATE_EXPENSES); + } + expensesList.addExpenses(expenses); + } + return expensesList; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof XmlSerializableExpensesList)) { + return false; + } + return multiExpenses.equals(((XmlSerializableExpensesList) other).multiExpenses); + } +} diff --git a/src/main/java/seedu/address/storage/recruitment/RecruitmentListStorage.java b/src/main/java/seedu/address/storage/recruitment/RecruitmentListStorage.java new file mode 100644 index 000000000000..fe2138e6e084 --- /dev/null +++ b/src/main/java/seedu/address/storage/recruitment/RecruitmentListStorage.java @@ -0,0 +1,45 @@ +package seedu.address.storage.recruitment; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; + +import seedu.address.commons.exceptions.DataConversionException; +import seedu.address.model.recruitment.ReadOnlyRecruitmentList; + +/** + * Represents a storage for {@link Recruitment}. + */ +public interface RecruitmentListStorage { + + /** + * Returns the file path of the data file. + */ + Path getRecruitmentListFilePath(); + + /** + * Returns RecruitmentList data as a {@link ReadOnlyRecruitmentList}. + * Returns {@code Optional.empty()} if storage file is not found. + * @throws DataConversionException if the data in storage is not in the expected format. + * @throws IOException if there was any problem when reading from the storage. + */ + Optional readRecruitmentList() throws DataConversionException, IOException; + + /** + * @see #getRecruitmentListFilePath() + */ + Optional readRecruitmentList(Path filePath) throws DataConversionException, IOException; + + /** + * Saves the given {@link ReadOnlyRecruitmentList} to the storage. + * @param recruitmentList cannot be null. + * @throws IOException if there was any problem writing to the file. + */ + void saveRecruitmentList(ReadOnlyRecruitmentList recruitmentList) throws IOException; + + /** + * @see #saveRecruitmentList(ReadOnlyRecruitmentList) + */ + void saveRecruitmentList(ReadOnlyRecruitmentList recruitmentList, Path filePath) throws IOException; + +} diff --git a/src/main/java/seedu/address/storage/recruitment/XmlAdaptedRecruitment.java b/src/main/java/seedu/address/storage/recruitment/XmlAdaptedRecruitment.java new file mode 100644 index 000000000000..f7b1d21bf0c3 --- /dev/null +++ b/src/main/java/seedu/address/storage/recruitment/XmlAdaptedRecruitment.java @@ -0,0 +1,106 @@ +package seedu.address.storage.recruitment; + +import java.util.Objects; + +import javax.xml.bind.annotation.XmlElement; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.recruitment.JobDescription; +import seedu.address.model.recruitment.Post; +import seedu.address.model.recruitment.Recruitment; +import seedu.address.model.recruitment.WorkExp; + + +/** + * JAXB-friendly version of the Recruitment. + */ +public class XmlAdaptedRecruitment { + + public static final String MISSING_FIELD_MESSAGE_FORMAT = "recruitment's %s field is missing!"; + + @XmlElement(required = true) + private String post; + @XmlElement(required = true) + private String workExp; + @XmlElement(required = true) + private String jobDescription; + + /** + * Constructs an XmlAdaptedRecruitment. + * This is the no-arg constructor that is required by JAXB. + */ + public XmlAdaptedRecruitment() {} + + /** + * Constructs an {@code XmlAdaptedRecruitment} with the given recruitmentPost details. + */ + public XmlAdaptedRecruitment(String post, String workExp, String jobDescription) { + this.post = post; + this.workExp = workExp; + this.jobDescription = jobDescription; + } + + /** + * Converts a given Recruitment into this class for JAXB use. + * + * @param source future changes to this will not affect the created XmlAdaptedRecruitment + */ + public XmlAdaptedRecruitment(Recruitment source) { + post = source.getPost().value; + workExp = source.getWorkExp().workExp; + jobDescription = source.getJobDescription().value; + } + + /** + * Converts this jaxb-friendly adapted recruitment object into the model's Recruitment object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted recruitment + */ + public Recruitment toModelPost() throws IllegalValueException { + + if (post == null) { + throw new IllegalValueException(String.format( + MISSING_FIELD_MESSAGE_FORMAT, Post.class.getSimpleName())); + } + if (!Post.isValidPost(post)) { + throw new IllegalValueException(Post.MESSAGE_POST_CONSTRAINTS); + } + final Post modelPost = new Post(post); + + if (workExp == null) { + throw new IllegalValueException(String.format( + MISSING_FIELD_MESSAGE_FORMAT, WorkExp.class.getSimpleName())); + } + if (!WorkExp.isValidWorkExp(workExp)) { + throw new IllegalValueException(WorkExp.MESSAGE_WORK_EXP_CONSTRAINTS); + } + final WorkExp modelWorkExp = new WorkExp(workExp); + + if (jobDescription == null) { + throw new IllegalValueException(String.format( + MISSING_FIELD_MESSAGE_FORMAT, JobDescription.class.getSimpleName())); + } + if (!JobDescription.isValidJobDescription(jobDescription)) { + throw new IllegalValueException(JobDescription.MESSAGE_JOB_DESCRIPTION_CONSTRAINTS); + } + final JobDescription modelJobDescription = new JobDescription(jobDescription); + + return new Recruitment(modelPost, modelWorkExp, modelJobDescription); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof XmlAdaptedRecruitment)) { + return false; + } + + XmlAdaptedRecruitment otherRecruitment = (XmlAdaptedRecruitment) other; + return Objects.equals(post, otherRecruitment.post) + && Objects.equals(workExp, otherRecruitment.workExp) + && Objects.equals(jobDescription, otherRecruitment.jobDescription); + } +} diff --git a/src/main/java/seedu/address/storage/recruitment/XmlRecruitmentFileStorage.java b/src/main/java/seedu/address/storage/recruitment/XmlRecruitmentFileStorage.java new file mode 100644 index 000000000000..c71157800596 --- /dev/null +++ b/src/main/java/seedu/address/storage/recruitment/XmlRecruitmentFileStorage.java @@ -0,0 +1,41 @@ +package seedu.address.storage.recruitment; + +import java.io.FileNotFoundException; +import java.nio.file.Path; + +import javax.xml.bind.JAXBException; + +import seedu.address.commons.exceptions.DataConversionException; +import seedu.address.commons.util.XmlUtil; + +/** + * Stores addressbook data in an XML file + */ +public class XmlRecruitmentFileStorage { + + + /** + * Saves the given recruitmentlist data to the specified file. + */ + public static void saveDataToFile(Path file, XmlSerializableRecruitmentList recruitmentList) + throws FileNotFoundException { + try { + XmlUtil.saveDataToFile(file, recruitmentList); + } catch (JAXBException e) { + throw new AssertionError("Unexpected exception " + e.getMessage(), e); + } + } + + /** + * Returns recruitment list in the file or an empty recruitment list + */ + public static XmlSerializableRecruitmentList loadDataFromSaveRecruitmentListFile(Path file) + throws DataConversionException, FileNotFoundException { + try { + return XmlUtil.getDataFromFile(file, XmlSerializableRecruitmentList.class); + } catch (JAXBException e) { + throw new DataConversionException(e); + } + } + +} diff --git a/src/main/java/seedu/address/storage/recruitment/XmlRecruitmentListStorage.java b/src/main/java/seedu/address/storage/recruitment/XmlRecruitmentListStorage.java new file mode 100644 index 000000000000..f6a9d51a7f47 --- /dev/null +++ b/src/main/java/seedu/address/storage/recruitment/XmlRecruitmentListStorage.java @@ -0,0 +1,81 @@ +package seedu.address.storage.recruitment; + +import static java.util.Objects.requireNonNull; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.exceptions.DataConversionException; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.commons.util.FileUtil; +import seedu.address.model.recruitment.ReadOnlyRecruitmentList; + +/** + * A class to access RecruitmentList data stored as an xml file on the hard disk. + */ +public class XmlRecruitmentListStorage implements RecruitmentListStorage { + + private static final Logger logger = LogsCenter.getLogger(XmlRecruitmentListStorage.class); + + private Path filePath; + + public XmlRecruitmentListStorage(Path filePath) { + this.filePath = filePath; + } + + public Path getRecruitmentListFilePath() { + return filePath; + } + + @Override + public Optional readRecruitmentList() throws DataConversionException, IOException { + return readRecruitmentList(filePath); + } + + /** + * Similar to {@link #readRecruitmentList()} + * @param filePath location of the data. Cannot be null + * @throws DataConversionException if the file is not in the correct format. + */ + public Optional readRecruitmentList(Path filePath) throws DataConversionException, + FileNotFoundException { + requireNonNull(filePath); + + if (!Files.exists(filePath)) { + logger.info("RecruitmentList file " + filePath + " not found"); + return Optional.empty(); + } + + XmlSerializableRecruitmentList + xmlRecruitmentList = XmlRecruitmentFileStorage.loadDataFromSaveRecruitmentListFile(filePath); + try { + return Optional.of(xmlRecruitmentList.toModelType()); + } catch (IllegalValueException ive) { + logger.info("Illegal values found in " + filePath + ": " + ive.getMessage()); + throw new DataConversionException(ive); + } + } + + @Override + public void saveRecruitmentList(ReadOnlyRecruitmentList recruitmentList) throws IOException { + saveRecruitmentList(recruitmentList, filePath); + } + + /** + * Similar to {@link #saveRecruitmentList(ReadOnlyRecruitmentList)} + * @param filePath location of the data. Cannot be null + */ + public void saveRecruitmentList(ReadOnlyRecruitmentList recruitmentList, Path filePath) throws IOException { + requireNonNull(recruitmentList); + requireNonNull(filePath); + + FileUtil.createIfMissing(filePath); + XmlRecruitmentFileStorage.saveDataToFile(filePath, new XmlSerializableRecruitmentList(recruitmentList)); + } + +} diff --git a/src/main/java/seedu/address/storage/recruitment/XmlSerializableRecruitmentList.java b/src/main/java/seedu/address/storage/recruitment/XmlSerializableRecruitmentList.java new file mode 100644 index 000000000000..671466e56786 --- /dev/null +++ b/src/main/java/seedu/address/storage/recruitment/XmlSerializableRecruitmentList.java @@ -0,0 +1,73 @@ +package seedu.address.storage.recruitment; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.recruitment.ReadOnlyRecruitmentList; +import seedu.address.model.recruitment.Recruitment; +import seedu.address.model.recruitment.RecruitmentList; + + +/** + * An Immutable RecruitmentList that is serializable to XML format + */ +@XmlRootElement(name = "recruitmentlist") +public class XmlSerializableRecruitmentList { + + public static final String MESSAGE_DUPLICATE_RECRUITMENT = "RecruitmentLists contain duplicate recruitmentPost(s)."; + + @XmlElement + private List recruitments; + + /** + * Creates an empty XmlSerializableRecruitmentList. + * This empty constructor is required for marshalling. + */ + public XmlSerializableRecruitmentList() { + recruitments = new ArrayList<>(); + } + + /** + * Conversion + */ + public XmlSerializableRecruitmentList(ReadOnlyRecruitmentList src) { + this(); + recruitments.addAll(src.getRecruitmentList().stream().map(XmlAdaptedRecruitment::new).collect(Collectors.toList + ())); + } + + /** + * Converts this RecruitmentList into the model's {@code RecruitmentList} object. + * + * @throws IllegalValueException if there were any data constraints violated or duplicates in the + * {@code XmlAdaptedRecruitment}. + */ + public RecruitmentList toModelType() throws IllegalValueException { + RecruitmentList recruitmentList = new RecruitmentList(); + for (XmlAdaptedRecruitment p : recruitments) { + Recruitment recruitment = p.toModelPost(); + if (recruitmentList.hasRecruitment(recruitment)) { + throw new IllegalValueException(MESSAGE_DUPLICATE_RECRUITMENT); + } + recruitmentList.addRecruitment(recruitment); + } + return recruitmentList; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof XmlSerializableRecruitmentList)) { + return false; + } + return recruitments.equals(((XmlSerializableRecruitmentList) other).recruitments); + } +} diff --git a/src/main/java/seedu/address/storage/schedule/ScheduleListStorage.java b/src/main/java/seedu/address/storage/schedule/ScheduleListStorage.java new file mode 100644 index 000000000000..7eb6c2cb4205 --- /dev/null +++ b/src/main/java/seedu/address/storage/schedule/ScheduleListStorage.java @@ -0,0 +1,46 @@ +package seedu.address.storage.schedule; + +import java.io.IOException; +import java.nio.file.Path; +import java.util.Optional; + +import seedu.address.commons.exceptions.DataConversionException; +import seedu.address.model.addressbook.AddressBook; +import seedu.address.model.schedule.ReadOnlyScheduleList; + +/** + * Represents a storage for {@link AddressBook}. + */ +public interface ScheduleListStorage { + + /** + * Returns the file path of the data file. + */ + Path getScheduleListFilePath(); + + /** + * Returns ScheduleList data as a {@link ReadOnlyScheduleList}. + * Returns {@code Optional.empty()} if storage file is not found. + * @throws DataConversionException if the data in storage is not in the expected format. + * @throws IOException if there was any problem when reading from the storage. + */ + Optional readScheduleList() throws DataConversionException, IOException; + + /** + * @see #getScheduleListFilePath() + */ + Optional readScheduleList(Path filePath) throws DataConversionException, IOException; + + /** + * Saves the given {@link ReadOnlyScheduleList} to the storage. + * @param scheduleList cannot be null. + * @throws IOException if there was any problem writing to the file. + */ + void saveScheduleList(ReadOnlyScheduleList scheduleList) throws IOException; + + /** + * @see #saveScheduleList(ReadOnlyScheduleList) + */ + void saveScheduleList(ReadOnlyScheduleList scheduleList, Path filePath) throws IOException; + +} diff --git a/src/main/java/seedu/address/storage/schedule/XmlAdaptedSchedule.java b/src/main/java/seedu/address/storage/schedule/XmlAdaptedSchedule.java new file mode 100644 index 000000000000..57a9ff2f6748 --- /dev/null +++ b/src/main/java/seedu/address/storage/schedule/XmlAdaptedSchedule.java @@ -0,0 +1,104 @@ +package seedu.address.storage.schedule; + +import java.util.Objects; + +import javax.xml.bind.annotation.XmlElement; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.person.EmployeeId; +import seedu.address.model.schedule.Date; +import seedu.address.model.schedule.Schedule; +import seedu.address.model.schedule.Type; + +/** + * JAXB-friendly version of the Schedule. + */ +public class XmlAdaptedSchedule { + + public static final String MISSING_FIELD_MESSAGE_FORMAT = "schedule's %s field is missing!"; + + @XmlElement(required = true) + private String employeeId; + @XmlElement(required = true) + private String type; + @XmlElement(required = true) + private String date; + + /** + * Constructs an XmlAdaptedSchedule. + * This is the no-arg constructor that is required by JAXB. + */ + public XmlAdaptedSchedule() {} + + /** + * Constructs an {@code XmlAdaptedSchedule} with the given schedule details. + */ + public XmlAdaptedSchedule(String employeeId, String type, String date) { + this.employeeId = employeeId; + this.type = type; + this.date = date; + } + + /** + * Converts a given Schedule into this class for JAXB use. + * + * @param source future changes to this will not affect the created XmlAdaptedSchedule + */ + public XmlAdaptedSchedule(Schedule source) { + employeeId = source.getEmployeeId().value; + date = source.getScheduleDate().value; + type = source.getType().value; + } + + /** + * Converts this jaxb-friendly adapted schedule object into the model's Schedule object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted schedule + */ + public Schedule toModelType() throws IllegalValueException { + + if (employeeId == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + EmployeeId.class.getSimpleName())); + } + if (!EmployeeId.isValidEmployeeId(employeeId)) { + throw new IllegalValueException(EmployeeId.MESSAGE_EMPLOYEEID_CONSTRAINTS); + } + final EmployeeId modelEmployeeId = new EmployeeId(employeeId); + + if (type == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Type.class.getSimpleName())); + } + if (!Type.isValidType(type)) { + throw new IllegalValueException(Type.MESSAGE_TYPE_CONSTRAINTS); + } + final Type modelType = new Type(type); + + if (date == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + Date.class.getSimpleName())); + } + if (!Date.isValidScheduleDate(date)) { + throw new IllegalValueException(Date.getDateConstraintsError()); + } + final Date modelDate = new Date(date); + + return new Schedule(modelEmployeeId, modelType, modelDate); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof XmlAdaptedSchedule)) { + return false; + } + + XmlAdaptedSchedule otherScehdule = (XmlAdaptedSchedule) other; + return Objects.equals(employeeId, otherScehdule.employeeId) + && Objects.equals(date, otherScehdule.date) + && Objects.equals(type, otherScehdule.type); + } +} diff --git a/src/main/java/seedu/address/storage/schedule/XmlScheduleFileStorage.java b/src/main/java/seedu/address/storage/schedule/XmlScheduleFileStorage.java new file mode 100644 index 000000000000..bcfc706e4d47 --- /dev/null +++ b/src/main/java/seedu/address/storage/schedule/XmlScheduleFileStorage.java @@ -0,0 +1,41 @@ +package seedu.address.storage.schedule; + +import java.io.FileNotFoundException; +import java.nio.file.Path; + +import javax.xml.bind.JAXBException; + +import seedu.address.commons.exceptions.DataConversionException; +import seedu.address.commons.util.XmlUtil; + +/** + * Stores schedule list data in an XML file + */ +public class XmlScheduleFileStorage { + + + /** + * Saves the given schedule list data to the specified file. + */ + public static void saveDataToFile(Path file, XmlSerializableScheduleList scheduleList) + throws FileNotFoundException { + try { + XmlUtil.saveDataToFile(file, scheduleList); + } catch (JAXBException e) { + throw new AssertionError("Unexpected exception " + e.getMessage(), e); + } + } + + /** + * Returns schedule list in the file or an empty schedule list + */ + public static XmlSerializableScheduleList loadDataFromSaveScheduleListFile(Path file) + throws DataConversionException, FileNotFoundException { + try { + return XmlUtil.getDataFromFile(file, XmlSerializableScheduleList.class); + } catch (JAXBException e) { + throw new DataConversionException(e); + } + } + +} diff --git a/src/main/java/seedu/address/storage/schedule/XmlScheduleListStorage.java b/src/main/java/seedu/address/storage/schedule/XmlScheduleListStorage.java new file mode 100644 index 000000000000..3f2578156563 --- /dev/null +++ b/src/main/java/seedu/address/storage/schedule/XmlScheduleListStorage.java @@ -0,0 +1,80 @@ +package seedu.address.storage.schedule; + +import static java.util.Objects.requireNonNull; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Optional; +import java.util.logging.Logger; + +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.exceptions.DataConversionException; +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.commons.util.FileUtil; +import seedu.address.model.schedule.ReadOnlyScheduleList; + +/** + * A class to access ScheduleList data stored as an xml file on the hard disk. + */ +public class XmlScheduleListStorage implements ScheduleListStorage { + + private static final Logger logger = LogsCenter.getLogger(XmlScheduleListStorage.class); + + private Path filePath; + + public XmlScheduleListStorage(Path filePath) { + this.filePath = filePath; + } + + public Path getScheduleListFilePath() { + return filePath; + } + + @Override + public Optional readScheduleList() throws DataConversionException, IOException { + return readScheduleList(filePath); + } + + /** + * Similar to {@link #readScheduleList()} + * @param filePath location of the data. Cannot be null + * @throws DataConversionException if the file is not in the correct format. + */ + public Optional readScheduleList(Path filePath) throws DataConversionException, + FileNotFoundException { + requireNonNull(filePath); + + if (!Files.exists(filePath)) { + logger.info("ScheduleList file " + filePath + " not found"); + return Optional.empty(); + } + + XmlSerializableScheduleList xmlScheduleList = XmlScheduleFileStorage.loadDataFromSaveScheduleListFile(filePath); + try { + return Optional.of(xmlScheduleList.toModelType()); + } catch (IllegalValueException ive) { + logger.info("Illegal values found in " + filePath + ": " + ive.getMessage()); + throw new DataConversionException(ive); + } + } + + @Override + public void saveScheduleList(ReadOnlyScheduleList scheduleList) throws IOException { + saveScheduleList(scheduleList, filePath); + } + + /** + * Similar to {@link #saveScheduleList(ReadOnlyScheduleList)} + * @param filePath location of the data. Cannot be null + */ + public void saveScheduleList(ReadOnlyScheduleList scheduleList, Path filePath) throws IOException { + requireNonNull(scheduleList); + requireNonNull(filePath); + + FileUtil.createIfMissing(filePath); + XmlScheduleFileStorage.saveDataToFile(filePath, new XmlSerializableScheduleList(scheduleList)); + } + +} diff --git a/src/main/java/seedu/address/storage/schedule/XmlSerializableScheduleList.java b/src/main/java/seedu/address/storage/schedule/XmlSerializableScheduleList.java new file mode 100644 index 000000000000..f3bb6247db77 --- /dev/null +++ b/src/main/java/seedu/address/storage/schedule/XmlSerializableScheduleList.java @@ -0,0 +1,72 @@ +package seedu.address.storage.schedule; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.schedule.ReadOnlyScheduleList; +import seedu.address.model.schedule.Schedule; +import seedu.address.model.schedule.ScheduleList; + + +/** + * An Immutable ScheduleList that is serializable to XML format + */ +@XmlRootElement(name = "schedulelist") +public class XmlSerializableScheduleList { + + public static final String MESSAGE_DUPLICATE_SCHEDULE = "Schedules list contains duplicate schedule(s)."; + + @XmlElement + private List schedules; + + /** + * Creates an empty XmlSerializableScheduleList. + * This empty constructor is required for marshalling. + */ + public XmlSerializableScheduleList() { + schedules = new ArrayList<>(); + } + + /** + * Conversion + */ + public XmlSerializableScheduleList(ReadOnlyScheduleList src) { + this(); + schedules.addAll(src.getScheduleList().stream().map(XmlAdaptedSchedule::new).collect(Collectors.toList())); + } + + /** + * Converts this ScheduleList into the model's {@code ScheduleList} object. + * + * @throws IllegalValueException if there were any data constraints violated or duplicates in the + * {@code XmlAdaptedSchedule}. + */ + public ScheduleList toModelType() throws IllegalValueException { + ScheduleList scheduleList = new ScheduleList(); + for (XmlAdaptedSchedule p : schedules) { + Schedule schedule = p.toModelType(); + if (scheduleList.hasSchedule(schedule)) { + throw new IllegalValueException(MESSAGE_DUPLICATE_SCHEDULE); + } + scheduleList.addSchedule(schedule); + } + return scheduleList; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + if (!(other instanceof XmlSerializableScheduleList)) { + return false; + } + return schedules.equals(((XmlSerializableScheduleList) other).schedules); + } +} diff --git a/src/main/java/seedu/address/storage/JsonUserPrefsStorage.java b/src/main/java/seedu/address/storage/userpref/JsonUserPrefsStorage.java similarity index 96% rename from src/main/java/seedu/address/storage/JsonUserPrefsStorage.java rename to src/main/java/seedu/address/storage/userpref/JsonUserPrefsStorage.java index 2ab927023cc4..e232183f624b 100644 --- a/src/main/java/seedu/address/storage/JsonUserPrefsStorage.java +++ b/src/main/java/seedu/address/storage/userpref/JsonUserPrefsStorage.java @@ -1,4 +1,4 @@ -package seedu.address.storage; +package seedu.address.storage.userpref; import java.io.IOException; import java.nio.file.Path; diff --git a/src/main/java/seedu/address/storage/UserPrefsStorage.java b/src/main/java/seedu/address/storage/userpref/UserPrefsStorage.java similarity index 96% rename from src/main/java/seedu/address/storage/UserPrefsStorage.java rename to src/main/java/seedu/address/storage/userpref/UserPrefsStorage.java index 877b0ee5c4f0..99cac65f868d 100644 --- a/src/main/java/seedu/address/storage/UserPrefsStorage.java +++ b/src/main/java/seedu/address/storage/userpref/UserPrefsStorage.java @@ -1,4 +1,4 @@ -package seedu.address.storage; +package seedu.address.storage.userpref; import java.io.IOException; import java.nio.file.Path; diff --git a/src/main/java/seedu/address/ui/CommandBox.java b/src/main/java/seedu/address/ui/CommandBox.java index 3d7aaded5640..bb0fec5fb177 100644 --- a/src/main/java/seedu/address/ui/CommandBox.java +++ b/src/main/java/seedu/address/ui/CommandBox.java @@ -2,12 +2,17 @@ import java.util.logging.Logger; +import com.google.common.eventbus.Subscribe; + +import javafx.application.Platform; import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.control.TextField; import javafx.scene.input.KeyEvent; import javafx.scene.layout.Region; + import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.events.ui.NewMenuBarCmdClickedEvent; import seedu.address.commons.events.ui.NewResultAvailableEvent; import seedu.address.logic.ListElementPointer; import seedu.address.logic.Logic; @@ -36,6 +41,7 @@ public CommandBox(Logic logic) { // calls #setStyleToDefault() whenever there is a change to the text of the command box. commandTextField.textProperty().addListener((unused1, unused2, unused3) -> setStyleToDefault()); historySnapshot = logic.getHistorySnapshot(); + registerAsAnEventHandler(this); } /** @@ -148,4 +154,11 @@ private void setStyleToIndicateCommandFailure() { styleClass.add(ERROR_STYLE_CLASS); } + @Subscribe + private void handleNewMenuBarCmdClickedEvent(NewMenuBarCmdClickedEvent event) { + logger.fine(LogsCenter.getEventHandlingLogMessage(event)); + Platform.runLater(() -> commandTextField.setText(event.menuCommand)); + Platform.runLater(() -> commandTextField.positionCaret(event.menuCommand.length())); + } + } diff --git a/src/main/java/seedu/address/ui/ExpensesCard.java b/src/main/java/seedu/address/ui/ExpensesCard.java new file mode 100644 index 000000000000..bb38a0d7b8f1 --- /dev/null +++ b/src/main/java/seedu/address/ui/ExpensesCard.java @@ -0,0 +1,81 @@ +package seedu.address.ui; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.Region; +import seedu.address.model.expenses.Expenses; + +/** + * An UI component that displays information of a {@code Expenses}. + */ +public class ExpensesCard extends UiPart { + + private static final String FXML = "ExpensesListCard.fxml"; + + /** + * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. + * As a consequence, UI elements' variable names cannot be set to such keywords + * or an exception will be thrown by JavaFX during runtime. + * + * @see The issue on AddressBook level 4 + */ + + public final Expenses expenses; + + @FXML + private Label id; + @FXML + private Label employeeId; + @FXML + private Label expensesAmount; + @FXML + private Label travelExpenses; + @FXML + private Label medicalExpenses; + @FXML + private Label miscellaneousExpenses; + @FXML + private Label employeeIdLabel; + @FXML + private Label expensesAmountLabel; + @FXML + private Label travelExpensesLabel; + @FXML + private Label medicalExpensesLabel; + @FXML + private Label miscellaneousExpensesLabel; + + public ExpensesCard(Expenses expenses, int displayedIndex) { + super(FXML); + this.expenses = expenses; + id.setText(displayedIndex + ". "); + employeeIdLabel.setText("Employee ID: "); + employeeId.setText(expenses.getEmployeeId().value); + expensesAmountLabel.setText("Total Expenses: "); + expensesAmount.setText(expenses.getExpensesAmount().expensesAmount); + travelExpensesLabel.setText("Travel Expenses: "); + travelExpenses.setText(expenses.getTravelExpenses().travelExpenses); + medicalExpensesLabel.setText("Medical Expenses: "); + medicalExpenses.setText(expenses.getMedicalExpenses().medicalExpenses); + miscellaneousExpensesLabel.setText("Miscellaneous Expenses: "); + miscellaneousExpenses.setText(expenses.getMiscellaneousExpenses().miscellaneousExpenses); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ExpensesCard)) { + return false; + } + + // state check + ExpensesCard card = (ExpensesCard) other; + return id.getText().equals(card.id.getText()) + && expenses.equals(card.expenses); + } +} diff --git a/src/main/java/seedu/address/ui/ExpensesListPanel.java b/src/main/java/seedu/address/ui/ExpensesListPanel.java new file mode 100644 index 000000000000..f261e058f762 --- /dev/null +++ b/src/main/java/seedu/address/ui/ExpensesListPanel.java @@ -0,0 +1,83 @@ +package seedu.address.ui; + +import java.util.logging.Logger; + +import com.google.common.eventbus.Subscribe; + +import javafx.application.Platform; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.Region; +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.events.ui.ExpensesPanelSelectionChangedEvent; +import seedu.address.commons.events.ui.JumpToListExpensesRequestEvent; +import seedu.address.model.expenses.Expenses; + +/** + * Panel containing the list of expenses. + */ +public class ExpensesListPanel extends UiPart { + private static final String FXML = "ExpensesListPanel.fxml"; + private final Logger logger = LogsCenter.getLogger(ExpensesListPanel.class); + + @FXML + private ListView expensesListView; + + public ExpensesListPanel(ObservableList expensesList) { + super(FXML); + setConnections(expensesList); + registerAsAnEventHandler(this); + } + + private void setConnections(ObservableList expensesList) { + expensesListView.setItems(expensesList); + expensesListView.setCellFactory(listView -> new ExpensesListViewCell()); + setEventHandlerForSelectionChangeEvent(); + } + + private void setEventHandlerForSelectionChangeEvent() { + expensesListView.getSelectionModel().selectedItemProperty() + .addListener((observable, oldValue, newValue) -> { + if (newValue != null) { + logger.fine("Selection in expenses list panel changed to : '" + newValue + "'"); + raise(new ExpensesPanelSelectionChangedEvent(newValue)); + } + }); + } + + /** + * Scrolls to the {@code PersonCard} at the {@code index} and selects it. + */ + private void scrollTo(int index) { + Platform.runLater(() -> { + expensesListView.scrollTo(index); + expensesListView.getSelectionModel().clearAndSelect(index); + }); + } + + @Subscribe + private void handleJumpToListExpensesRequestEvent(JumpToListExpensesRequestEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event)); + scrollTo(event.targetIndex); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code expenses} using a {@code ExpensesCard}. + */ + class ExpensesListViewCell extends ListCell { + @Override + protected void updateItem(Expenses expenses, boolean empty) { + super.updateItem(expenses, empty); + + if (empty || expenses == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new ExpensesCard(expenses, getIndex() + 1).getRoot()); + } + } + } + +} diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java index 0e361a4d7baf..2dc3fbc547fd 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/address/ui/MainWindow.java @@ -16,8 +16,41 @@ import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; import seedu.address.commons.events.ui.ExitAppRequestEvent; +import seedu.address.commons.events.ui.NewMenuBarCmdClickedEvent; +import seedu.address.commons.events.ui.NewResultAvailableEvent; import seedu.address.commons.events.ui.ShowHelpRequestEvent; import seedu.address.logic.Logic; +import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.AddExpensesCommand; +import seedu.address.logic.commands.AddLeavesCommand; +import seedu.address.logic.commands.AddRecruitmentPostCommand; +import seedu.address.logic.commands.AddScheduleCommand; +import seedu.address.logic.commands.AddWorksCommand; +import seedu.address.logic.commands.CalculateLeavesCommand; +import seedu.address.logic.commands.ClearCommand; +import seedu.address.logic.commands.ClearExpensesCommand; +import seedu.address.logic.commands.ClearRecruitmentPostCommand; +import seedu.address.logic.commands.ClearScheduleCommand; +import seedu.address.logic.commands.DeleteCommand; +import seedu.address.logic.commands.DeleteLeavesCommand; +import seedu.address.logic.commands.DeleteRecruitmentPostCommand; +import seedu.address.logic.commands.DeleteScheduleCommand; +import seedu.address.logic.commands.DeleteWorksCommand; +import seedu.address.logic.commands.EditCommand; +import seedu.address.logic.commands.EditRecruitmentPostCommand; +import seedu.address.logic.commands.FilterCommand; +import seedu.address.logic.commands.FindCommand; +import seedu.address.logic.commands.HistoryCommand; +import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.ModifyAllPayCommand; +import seedu.address.logic.commands.ModifyPayCommand; +import seedu.address.logic.commands.RedoCommand; +import seedu.address.logic.commands.RemoveExpensesCommand; +import seedu.address.logic.commands.SelectCommand; +import seedu.address.logic.commands.SelectExpensesCommand; +import seedu.address.logic.commands.SelectRecruitmentPostCommand; +import seedu.address.logic.commands.SelectScheduleCommand; +import seedu.address.logic.commands.UndoCommand; import seedu.address.model.UserPrefs; /** @@ -26,6 +59,7 @@ */ public class MainWindow extends UiPart { + public static final String COMMAND_USAGE = "[Command usage] "; private static final String FXML = "MainWindow.fxml"; private final Logger logger = LogsCenter.getLogger(getClass()); @@ -35,13 +69,16 @@ public class MainWindow extends UiPart { // Independent Ui parts residing in this Ui container private BrowserPanel browserPanel; + private ExpensesListPanel expensesListPanel; private PersonListPanel personListPanel; + private ScheduleListPanel scheduleListPanel; + private RecruitmentListPanel recruitmentListPanel; private Config config; private UserPrefs prefs; private HelpWindow helpWindow; - @FXML - private StackPane browserPlaceholder; + //@FXML + //private StackPane browserPlaceholder; @FXML private StackPane commandBoxPlaceholder; @@ -49,9 +86,18 @@ public class MainWindow extends UiPart { @FXML private MenuItem helpMenuItem; + @FXML + private StackPane expensesListPanelPlaceholder; + @FXML private StackPane personListPanelPlaceholder; + @FXML + private StackPane scheduleListPanelPlaceholder; + + @FXML + private StackPane recruitmentListPanelPlaceholder; + @FXML private StackPane resultDisplayPlaceholder; @@ -119,12 +165,21 @@ private void setAccelerator(MenuItem menuItem, KeyCombination keyCombination) { * Fills up all the placeholders of this window. */ void fillInnerParts() { - browserPanel = new BrowserPanel(); - browserPlaceholder.getChildren().add(browserPanel.getRoot()); + //browserPanel = new BrowserPanel(); + //browserPlaceholder.getChildren().add(browserPanel.getRoot()); + + expensesListPanel = new ExpensesListPanel(logic.getFilteredExpensesList()); + expensesListPanelPlaceholder.getChildren().add(expensesListPanel.getRoot()); personListPanel = new PersonListPanel(logic.getFilteredPersonList()); personListPanelPlaceholder.getChildren().add(personListPanel.getRoot()); + scheduleListPanel = new ScheduleListPanel(logic.getFilteredScheduleList()); + scheduleListPanelPlaceholder.getChildren().add(scheduleListPanel.getRoot()); + + recruitmentListPanel = new RecruitmentListPanel(logic.getFilteredRecruitmentList()); + recruitmentListPanelPlaceholder.getChildren().add(recruitmentListPanel.getRoot()); + ResultDisplay resultDisplay = new ResultDisplay(); resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot()); @@ -175,6 +230,287 @@ public void handleHelp() { } } + //------------------------------------------Person Related Menu ---------------------------------------------// + + /** + * CHRS related commands + */ + @FXML + public void handleAdd() { + raise(new NewMenuBarCmdClickedEvent(AddCommand.COMMAND_WORD + " ")); + raise(new NewResultAvailableEvent(COMMAND_USAGE + AddCommand.MESSAGE_USAGE)); + } + + /** + * CHRS related commands + */ + @FXML + public void handleEdit() { + raise(new NewMenuBarCmdClickedEvent(EditCommand.COMMAND_WORD + " ")); + raise(new NewResultAvailableEvent(COMMAND_USAGE + EditCommand.MESSAGE_USAGE)); + } + + /** + * CHRS related commands + */ + @FXML + public void handleSelect() { + raise(new NewMenuBarCmdClickedEvent(SelectCommand.COMMAND_WORD + " ")); + raise(new NewResultAvailableEvent(COMMAND_USAGE + SelectCommand.MESSAGE_USAGE)); + } + + /** + * CHRS related commands + */ + @FXML + public void handleClear() { + raise(new NewMenuBarCmdClickedEvent(ClearCommand.COMMAND_WORD + " ")); + raise(new NewResultAvailableEvent(COMMAND_USAGE + ClearCommand.MESSAGE_USAGE)); + } + + /** + * CHRS related commands + */ + @FXML + public void handleList() { + raise(new NewMenuBarCmdClickedEvent(ListCommand.COMMAND_WORD + " ")); + raise(new NewResultAvailableEvent(COMMAND_USAGE + ListCommand.MESSAGE_USAGE)); + } + + /** + * CHRS related commands + */ + @FXML + public void handleDelete() { + raise(new NewMenuBarCmdClickedEvent(DeleteCommand.COMMAND_WORD + " ")); + raise(new NewResultAvailableEvent(COMMAND_USAGE + DeleteCommand.MESSAGE_USAGE)); + } + + /** + * CHRS related commands + */ + @FXML + public void handleFind() { + raise(new NewMenuBarCmdClickedEvent(FindCommand.COMMAND_WORD + " ")); + raise(new NewResultAvailableEvent(COMMAND_USAGE + FindCommand.MESSAGE_USAGE)); + } + + /** + * CHRS related commands + */ + @FXML + public void handleFilter() { + raise(new NewMenuBarCmdClickedEvent(FilterCommand.COMMAND_WORD + " ")); + raise(new NewResultAvailableEvent(COMMAND_USAGE + FilterCommand.MESSAGE_USAGE)); + } + + /** + * CHRS related commands + */ + @FXML + public void handleModifyPay() { + raise(new NewMenuBarCmdClickedEvent(ModifyPayCommand.COMMAND_WORD + " ")); + raise(new NewResultAvailableEvent(COMMAND_USAGE + ModifyPayCommand.MESSAGE_USAGE)); + } + + /** + * CHRS related commands + */ + @FXML + public void handleAllModifyPay() { + raise(new NewMenuBarCmdClickedEvent(ModifyAllPayCommand.COMMAND_WORD + " ")); + raise(new NewResultAvailableEvent(COMMAND_USAGE + ModifyAllPayCommand.MESSAGE_USAGE)); + } + /** + * CHRS related commands + */ + @FXML + public void handleAddSchedule() { + raise(new NewMenuBarCmdClickedEvent(AddScheduleCommand.COMMAND_WORD + " ")); + raise(new NewResultAvailableEvent(COMMAND_USAGE + AddScheduleCommand.MESSAGE_USAGE)); + } + + /** + * CHRS related commands + */ + @FXML + public void handleDeleteSchedule() { + raise(new NewMenuBarCmdClickedEvent(DeleteScheduleCommand.COMMAND_WORD + " ")); + raise(new NewResultAvailableEvent(COMMAND_USAGE + DeleteScheduleCommand.MESSAGE_USAGE)); + } + + /** + * CHRS related commands + */ + @FXML + public void handleAddWorks() { + raise(new NewMenuBarCmdClickedEvent(AddWorksCommand.COMMAND_WORD + " ")); + raise(new NewResultAvailableEvent(COMMAND_USAGE + AddWorksCommand.MESSAGE_USAGE)); + } + + /** + * CHRS related commands + */ + @FXML + public void handleDeleteWorks() { + raise(new NewMenuBarCmdClickedEvent(DeleteWorksCommand.COMMAND_WORD + " ")); + raise(new NewResultAvailableEvent(COMMAND_USAGE + DeleteWorksCommand.MESSAGE_USAGE)); + } + + /** + * CHRS related commands + */ + @FXML + public void handleAddLeaves() { + raise(new NewMenuBarCmdClickedEvent(AddLeavesCommand.COMMAND_WORD + " ")); + raise(new NewResultAvailableEvent(COMMAND_USAGE + AddLeavesCommand.MESSAGE_USAGE)); + } + + /** + * CHRS related commands + */ + @FXML + public void handleDeleteLeaves() { + raise(new NewMenuBarCmdClickedEvent(DeleteLeavesCommand.COMMAND_WORD + " ")); + raise(new NewResultAvailableEvent(COMMAND_USAGE + DeleteLeavesCommand.MESSAGE_USAGE)); + } + + /** + * CHRS related commands + */ + @FXML + public void handleCalculateLeaves() { + raise(new NewMenuBarCmdClickedEvent(CalculateLeavesCommand.COMMAND_WORD + " ")); + raise(new NewResultAvailableEvent(COMMAND_USAGE + CalculateLeavesCommand.MESSAGE_USAGE)); + } + + + /** + * CHRS related commands + */ + @FXML + public void handleSelectSchedule() { + raise(new NewMenuBarCmdClickedEvent(SelectScheduleCommand.COMMAND_WORD + " ")); + raise(new NewResultAvailableEvent(COMMAND_USAGE + SelectScheduleCommand.MESSAGE_USAGE)); + } + + /** + * CHRS related commands + */ + @FXML + public void handleClearSchedules() { + raise(new NewMenuBarCmdClickedEvent(ClearScheduleCommand.COMMAND_WORD + " ")); + raise(new NewResultAvailableEvent(COMMAND_USAGE + ClearScheduleCommand.MESSAGE_USAGE)); + } + + /** + * CHRS related commands + */ + @FXML + public void handleHistory() { + raise(new NewMenuBarCmdClickedEvent(HistoryCommand.COMMAND_WORD + " ")); + raise(new NewResultAvailableEvent(COMMAND_USAGE + HistoryCommand.MESSAGE_USAGE)); + } + + /** + * CHRS related commands + */ + @FXML + public void handleUndo() { + raise(new NewMenuBarCmdClickedEvent(UndoCommand.COMMAND_WORD + " ")); + raise(new NewResultAvailableEvent(COMMAND_USAGE + UndoCommand.MESSAGE_USAGE)); + } + + /** + * CHRS related commands + */ + @FXML + public void handleRedo() { + raise(new NewMenuBarCmdClickedEvent(RedoCommand.COMMAND_WORD + " ")); + raise(new NewResultAvailableEvent(COMMAND_USAGE + RedoCommand.MESSAGE_USAGE)); + } + + /** + * CHRS related commands + */ + @FXML + public void handleAddExpenses() { + raise(new NewMenuBarCmdClickedEvent(AddExpensesCommand.COMMAND_WORD + " ")); + raise(new NewResultAvailableEvent(COMMAND_USAGE + AddExpensesCommand.MESSAGE_USAGE)); + } + + /** + * CHRS related commands + */ + @FXML + public void handleDeleteExpenses() { + raise(new NewMenuBarCmdClickedEvent(RemoveExpensesCommand.COMMAND_WORD + " ")); + raise(new NewResultAvailableEvent(COMMAND_USAGE + RemoveExpensesCommand.MESSAGE_USAGE)); + } + + /** + * CHRS related commands + */ + @FXML + public void handleClearExpenses() { + raise(new NewMenuBarCmdClickedEvent(ClearExpensesCommand.COMMAND_WORD + " ")); + raise(new NewResultAvailableEvent(COMMAND_USAGE + ClearExpensesCommand.MESSAGE_USAGE)); + } + + /** + * CHRS related commands + */ + @FXML + public void handleSelectExpenses() { + raise(new NewMenuBarCmdClickedEvent(SelectExpensesCommand.COMMAND_WORD + " ")); + raise(new NewResultAvailableEvent(COMMAND_USAGE + SelectExpensesCommand.MESSAGE_USAGE)); + } + + /** + * CHRS related commands + */ + @FXML + public void handleAddRecruitmentPost() { + raise(new NewMenuBarCmdClickedEvent(AddRecruitmentPostCommand.COMMAND_WORD + " ")); + raise(new NewResultAvailableEvent(COMMAND_USAGE + AddRecruitmentPostCommand.MESSAGE_USAGE2)); + } + + /** + * CHRS related commands + */ + @FXML + public void handleDeleteRecruitmentPost() { + raise(new NewMenuBarCmdClickedEvent(DeleteRecruitmentPostCommand.COMMAND_WORD + " ")); + raise(new NewResultAvailableEvent(COMMAND_USAGE + DeleteRecruitmentPostCommand.MESSAGE_USAGE)); + } + + /** + * CHRS related commands + */ + @FXML + public void handleSelectRecruitment() { + raise(new NewMenuBarCmdClickedEvent(SelectRecruitmentPostCommand.COMMAND_WORD + " ")); + raise(new NewResultAvailableEvent(COMMAND_USAGE + SelectRecruitmentPostCommand.MESSAGE_USAGE)); + } + + /** + * CHRS related commands + */ + @FXML + public void handleEditRecruitment() { + raise(new NewMenuBarCmdClickedEvent(EditRecruitmentPostCommand.COMMAND_WORD + " ")); + raise(new NewResultAvailableEvent(COMMAND_USAGE + EditRecruitmentPostCommand.MESSAGE_USAGE)); + } + + /** + * CHRS related commands + */ + @FXML + public void handleClearRecruitment() { + raise(new NewMenuBarCmdClickedEvent(ClearRecruitmentPostCommand.COMMAND_WORD + " ")); + raise(new NewResultAvailableEvent(COMMAND_USAGE + ClearRecruitmentPostCommand.MESSAGE_USAGE)); + } + void show() { primaryStage.show(); } @@ -187,13 +523,23 @@ private void handleExit() { raise(new ExitAppRequestEvent()); } + public ExpensesListPanel getExpensesListPanel() { + return expensesListPanel; + } + public PersonListPanel getPersonListPanel() { return personListPanel; } + public ScheduleListPanel getScheduleListPanel() { + return scheduleListPanel; + } + + /* void releaseResources() { browserPanel.freeResources(); } + */ @Subscribe private void handleShowHelpEvent(ShowHelpRequestEvent event) { diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java index f6727ea83abd..32cb9005413e 100644 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ b/src/main/java/seedu/address/ui/PersonCard.java @@ -27,6 +27,8 @@ public class PersonCard extends UiPart { @FXML private HBox cardPane; @FXML + private Label employeeId; + @FXML private Label name; @FXML private Label id; @@ -37,6 +39,34 @@ public class PersonCard extends UiPart { @FXML private Label email; @FXML + private Label dateOfBirth; + @FXML + private Label department; + @FXML + private Label position; + @FXML + private Label salary; + @FXML + private Label bonus; + @FXML + private Label employeeIdLabel; + @FXML + private Label phoneLabel; + @FXML + private Label addressLabel; + @FXML + private Label emailLabel; + @FXML + private Label dateOfBirthLabel; + @FXML + private Label departmentLabel; + @FXML + private Label positionLabel; + @FXML + private Label salaryLabel; + @FXML + private Label bonusLabel; + @FXML private FlowPane tags; public PersonCard(Person person, int displayedIndex) { @@ -44,9 +74,24 @@ public PersonCard(Person person, int displayedIndex) { this.person = person; id.setText(displayedIndex + ". "); name.setText(person.getName().fullName); + employeeIdLabel.setText("Employee ID: "); + employeeId.setText(person.getEmployeeId().value); + phoneLabel.setText("Phone: "); phone.setText(person.getPhone().value); + addressLabel.setText("Address: "); address.setText(person.getAddress().value); + emailLabel.setText("Email: "); email.setText(person.getEmail().value); + dateOfBirthLabel.setText("Date Of Birth: "); + dateOfBirth.setText(person.getDateOfBirth().value); + departmentLabel.setText("Department: "); + department.setText(person.getDepartment().value); + positionLabel.setText("Position: "); + position.setText(person.getPosition().value); + salaryLabel.setText("Salary: "); + salary.setText(person.getSalary().value); + bonusLabel.setText("Bonus: "); + bonus.setText(person.getBonus().value); person.getTags().forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); } diff --git a/src/main/java/seedu/address/ui/RecruitmentCard.java b/src/main/java/seedu/address/ui/RecruitmentCard.java new file mode 100644 index 000000000000..f97b0ae8b3d2 --- /dev/null +++ b/src/main/java/seedu/address/ui/RecruitmentCard.java @@ -0,0 +1,63 @@ +package seedu.address.ui; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.Region; +import seedu.address.model.recruitment.Recruitment; + +/** + * An UI component that displays information of a {@code Recruitment}. + */ +public class RecruitmentCard extends UiPart { + + private static final String FXML = "RecruitmentListCard.fxml"; + + /** + * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. + * As a consequence, UI elements' variable names cannot be set to such keywords + * or an exception will be thrown by JavaFX during runtime. + * + * @see The issue on AddressBook level 4 + */ + + public final Recruitment recruitment; + + @FXML + private Label id; + @FXML + private Label post; + @FXML + private Label workExp; + @FXML + private Label workExpLabel; + @FXML + private Label jobDescription; + + public RecruitmentCard (Recruitment recruitment, int displayedIndex) { + super(FXML); + this.recruitment = recruitment; + id.setText(displayedIndex + ". "); + post.setText(recruitment.getPost().value); + workExpLabel.setText("Min Work Exp:"); + workExp.setText(recruitment.getWorkExp().workExp + " years"); + jobDescription.setText(recruitment.getJobDescription().value); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof RecruitmentCard)) { + return false; + } + + // state check + RecruitmentCard card = (RecruitmentCard) other; + return id.getText().equals(card.id.getText()) + && recruitment.equals(card.recruitment); + } +} diff --git a/src/main/java/seedu/address/ui/RecruitmentListPanel.java b/src/main/java/seedu/address/ui/RecruitmentListPanel.java new file mode 100644 index 000000000000..4b1873ad3f23 --- /dev/null +++ b/src/main/java/seedu/address/ui/RecruitmentListPanel.java @@ -0,0 +1,83 @@ +package seedu.address.ui; + +import java.util.logging.Logger; + +import com.google.common.eventbus.Subscribe; + +import javafx.application.Platform; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.Region; +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.events.ui.JumpToListRecruitmentPostRequestEvent; +import seedu.address.commons.events.ui.RecruitmentPanelSelectionChangedEvent; +import seedu.address.model.recruitment.Recruitment; + +/** + * Panel containing the list of recruitment. + */ +public class RecruitmentListPanel extends UiPart { + private static final String FXML = "RecruitmentListPanel.fxml"; + private final Logger logger = LogsCenter.getLogger(RecruitmentListPanel.class); + + @FXML + private ListView recruitmentListView; + + public RecruitmentListPanel(ObservableList recruitmentList) { + super(FXML); + setConnections(recruitmentList); + registerAsAnEventHandler(this); + } + + private void setConnections(ObservableList recruitmentList) { + recruitmentListView.setItems(recruitmentList); + recruitmentListView.setCellFactory(listView -> new RecruitmentListViewCell()); + setEventHandlerForSelectionChangeEvent(); + } + + @Subscribe + private void handleJumpToListRequestEvent(JumpToListRecruitmentPostRequestEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event)); + scrollTo(event.targetIndex); + } + + /** + * Scrolls to the {@code PersonCard} at the {@code index} and selects it. + */ + private void scrollTo(int index) { + Platform.runLater(() -> { + recruitmentListView.scrollTo(index); + recruitmentListView.getSelectionModel().clearAndSelect(index); + }); + } + + private void setEventHandlerForSelectionChangeEvent() { + recruitmentListView.getSelectionModel().selectedItemProperty() + .addListener((observable, oldValue, newValue) -> { + if (newValue != null) { + logger.fine("Selection in recruitment list panel changed to : '" + newValue + "'"); + raise(new RecruitmentPanelSelectionChangedEvent(newValue)); + } + }); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code recruitment} using a {@code Recruitment}. + */ + class RecruitmentListViewCell extends ListCell { + @Override + protected void updateItem(Recruitment recruitment, boolean empty) { + super.updateItem(recruitment, empty); + + if (empty || recruitment == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new RecruitmentCard(recruitment, getIndex() + 1).getRoot()); + } + } + } + +} diff --git a/src/main/java/seedu/address/ui/ResultDisplay.java b/src/main/java/seedu/address/ui/ResultDisplay.java index d05536bbee96..f174f1efa2af 100644 --- a/src/main/java/seedu/address/ui/ResultDisplay.java +++ b/src/main/java/seedu/address/ui/ResultDisplay.java @@ -1,5 +1,7 @@ package seedu.address.ui; +import static seedu.address.commons.core.Messages.GREETING_MESSAGE_NEWLINE; + import java.util.logging.Logger; import com.google.common.eventbus.Subscribe; @@ -12,6 +14,7 @@ import javafx.scene.layout.Region; import seedu.address.commons.core.LogsCenter; import seedu.address.commons.events.ui.NewResultAvailableEvent; +import seedu.address.model.addressbook.DayHourGreeting; /** * A ui for the status bar that is displayed at the header of the application. @@ -21,13 +24,15 @@ public class ResultDisplay extends UiPart { private static final Logger logger = LogsCenter.getLogger(ResultDisplay.class); private static final String FXML = "ResultDisplay.fxml"; - private final StringProperty displayed = new SimpleStringProperty(""); + private final StringProperty displayed; @FXML private TextArea resultDisplay; public ResultDisplay() { super(FXML); + DayHourGreeting greeting = new DayHourGreeting(); + displayed = new SimpleStringProperty(greeting.getGreeting() + GREETING_MESSAGE_NEWLINE); resultDisplay.textProperty().bind(displayed); registerAsAnEventHandler(this); } diff --git a/src/main/java/seedu/address/ui/ScheduleCard.java b/src/main/java/seedu/address/ui/ScheduleCard.java new file mode 100644 index 000000000000..b057b7453d48 --- /dev/null +++ b/src/main/java/seedu/address/ui/ScheduleCard.java @@ -0,0 +1,72 @@ +package seedu.address.ui; + +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.Region; +import seedu.address.model.schedule.Schedule; + +/** + * An UI component that displays information of a {@code schedule}. + */ +public class ScheduleCard extends UiPart { + + private static final String FXML = "ScheduleListCard.fxml"; + + /** + * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. + * As a consequence, UI elements' variable names cannot be set to such keywords + * or an exception will be thrown by JavaFX during runtime. + * + * @see The issue on AddressBook level 4 + */ + + public final Schedule schedule; + + @FXML + private Label id; + @FXML + private Label employeeId; + @FXML + private Label employeeIdLabel; + @FXML + private Label type; + @FXML + private Label typeLabel; + @FXML + private Label dateOfSchedule; + @FXML + private Label dateOfScheduleLabel; + + public ScheduleCard(Schedule schedule, int displayedIndex) { + super(FXML); + this.schedule = schedule; + id.setText(displayedIndex + ". "); + employeeId.setText(schedule.getEmployeeId().value); + employeeIdLabel.setText("Employee ID: "); + typeLabel.setText("Type :"); + type.setText(schedule.getType().value); + dateOfScheduleLabel.setText("Date: "); + dateOfSchedule.setText(schedule.getScheduleDate().value); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof ScheduleCard)) { + return false; + } + + // state check + ScheduleCard card = (ScheduleCard) other; + return id.getText().equals(card.id.getText()) + && employeeId.getText().equals(card.employeeId.getText()) + && type.getText().equals(card.type.getText()) + && dateOfSchedule.getText().equals(card.dateOfSchedule.getText()) + && schedule.equals(card.schedule); + } +} diff --git a/src/main/java/seedu/address/ui/ScheduleListPanel.java b/src/main/java/seedu/address/ui/ScheduleListPanel.java new file mode 100644 index 000000000000..b2380308f4aa --- /dev/null +++ b/src/main/java/seedu/address/ui/ScheduleListPanel.java @@ -0,0 +1,84 @@ +package seedu.address.ui; + +import java.util.logging.Logger; + +import com.google.common.eventbus.Subscribe; + +import javafx.application.Platform; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.Region; +import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.events.ui.JumpToListScheduleRequestEvent; +import seedu.address.commons.events.ui.SchedulePanelSelectionChangedEvent; +import seedu.address.model.schedule.Schedule; + +/** + * Panel containing the list of persons. + */ +public class ScheduleListPanel extends UiPart { + private static final String FXML = "ScheduleListPanel.fxml"; + private final Logger logger = LogsCenter.getLogger(ScheduleListPanel.class); + + @FXML + private ListView scheduleListView; + + public ScheduleListPanel(ObservableList scheduleList) { + super(FXML); + setConnections(scheduleList); + registerAsAnEventHandler(this); + } + + private void setConnections(ObservableList scheduleList) { + scheduleListView.setItems(scheduleList); + scheduleListView.setCellFactory(listView -> new ScheduleListViewCell()); + setEventHandlerForSelectionChangeEvent(); + } + + + @Subscribe + private void handleJumpToListRequestEvent(JumpToListScheduleRequestEvent event) { + logger.info(LogsCenter.getEventHandlingLogMessage(event)); + scrollTo(event.targetIndex); + } + + /** + * Scrolls to the {@code PersonCard} at the {@code index} and selects it. + */ + private void scrollTo(int index) { + Platform.runLater(() -> { + scheduleListView.scrollTo(index); + scheduleListView.getSelectionModel().clearAndSelect(index); + }); + } + + private void setEventHandlerForSelectionChangeEvent() { + scheduleListView.getSelectionModel().selectedItemProperty() + .addListener((observable, oldValue, newValue) -> { + if (newValue != null) { + logger.fine("Selection in person list panel changed to : '" + newValue + "'"); + raise(new SchedulePanelSelectionChangedEvent(newValue)); + } + }); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code schedule} using a {@code ScheduleCard}. + */ + class ScheduleListViewCell extends ListCell { + @Override + protected void updateItem(Schedule schedule, boolean empty) { + super.updateItem(schedule, empty); + + if (empty || schedule == null) { + setGraphic(null); + setText(null); + } else { + setGraphic(new ScheduleCard(schedule, getIndex() + 1).getRoot()); + } + } + } + +} diff --git a/src/main/java/seedu/address/ui/StatusBarFooter.java b/src/main/java/seedu/address/ui/StatusBarFooter.java index f6ba29502422..f4f4eef22546 100644 --- a/src/main/java/seedu/address/ui/StatusBarFooter.java +++ b/src/main/java/seedu/address/ui/StatusBarFooter.java @@ -1,5 +1,7 @@ package seedu.address.ui; +import static seedu.address.commons.core.Messages.MESSAGE_STATUS_BAR_BOTTOM_RIGHT; + import java.nio.file.Path; import java.nio.file.Paths; import java.time.Clock; @@ -13,8 +15,11 @@ import javafx.application.Platform; import javafx.fxml.FXML; import javafx.scene.layout.Region; + import seedu.address.commons.core.LogsCenter; import seedu.address.commons.events.model.AddressBookChangedEvent; +import seedu.address.commons.events.model.ExpensesListChangedEvent; +import seedu.address.commons.events.model.ScheduleListChangedEvent; /** * A ui for the status bar that is displayed at the footer of the application. @@ -66,7 +71,7 @@ public static Clock getClock() { } private void setSaveLocation(String location) { - Platform.runLater(() -> saveLocationStatus.setText(location)); + Platform.runLater(() -> saveLocationStatus.setText(MESSAGE_STATUS_BAR_BOTTOM_RIGHT)); } private void setSyncStatus(String status) { @@ -80,4 +85,21 @@ public void handleAddressBookChangedEvent(AddressBookChangedEvent abce) { logger.info(LogsCenter.getEventHandlingLogMessage(abce, "Setting last updated status to " + lastUpdated)); setSyncStatus(String.format(SYNC_STATUS_UPDATED, lastUpdated)); } + + @Subscribe + public void handleScheduleListChangedEvent(ScheduleListChangedEvent abce) { + long now = clock.millis(); + String lastUpdated = new Date(now).toString(); + logger.info(LogsCenter.getEventHandlingLogMessage(abce, "Setting last updated status to " + lastUpdated)); + setSyncStatus(String.format(SYNC_STATUS_UPDATED, lastUpdated)); + } + + @Subscribe + public void handleExpensesListChangedEvent(ExpensesListChangedEvent elce) { + long now = clock.millis(); + String lastUpdated = new Date(now).toString(); + logger.info(LogsCenter.getEventHandlingLogMessage(elce, "Setting last updated status to " + + lastUpdated)); + setSyncStatus(String.format(SYNC_STATUS_UPDATED, lastUpdated)); + } } diff --git a/src/main/java/seedu/address/ui/UiManager.java b/src/main/java/seedu/address/ui/UiManager.java index 3fd3c17be156..76fe198edefb 100644 --- a/src/main/java/seedu/address/ui/UiManager.java +++ b/src/main/java/seedu/address/ui/UiManager.java @@ -66,7 +66,7 @@ public void start(Stage primaryStage) { public void stop() { prefs.updateLastUsedGuiSetting(mainWindow.getCurrentGuiSetting()); mainWindow.hide(); - mainWindow.releaseResources(); + //mainWindow.releaseResources(); } private void showFileOperationAlertAndWait(String description, String details, Throwable cause) { diff --git a/src/main/java/seedu/address/ui/UiPart.java b/src/main/java/seedu/address/ui/UiPart.java index 5c237e57154b..dade95a3800f 100644 --- a/src/main/java/seedu/address/ui/UiPart.java +++ b/src/main/java/seedu/address/ui/UiPart.java @@ -64,7 +64,7 @@ public T getRoot() { * Raises the event via {@link EventsCenter#post(BaseEvent)} * @param event */ - protected void raise(BaseEvent event) { + protected static void raise(BaseEvent event) { EventsCenter.getInstance().post(event); } diff --git a/src/main/resources/docs/images/Ui.png b/src/main/resources/docs/images/Ui.png new file mode 100644 index 000000000000..b9870fab2534 Binary files /dev/null and b/src/main/resources/docs/images/Ui.png differ diff --git a/src/main/resources/images/address_book_32.png b/src/main/resources/images/address_book_32.png index 29810cf1fd93..ca07a401712d 100644 Binary files a/src/main/resources/images/address_book_32.png and b/src/main/resources/images/address_book_32.png differ diff --git a/src/main/resources/images/address_book_32_old.png b/src/main/resources/images/address_book_32_old.png new file mode 100644 index 000000000000..29810cf1fd93 Binary files /dev/null and b/src/main/resources/images/address_book_32_old.png differ diff --git a/src/main/resources/images/address_book_32_old1.png b/src/main/resources/images/address_book_32_old1.png new file mode 100644 index 000000000000..c83821039ac1 Binary files /dev/null and b/src/main/resources/images/address_book_32_old1.png differ diff --git a/src/main/resources/images/background.jpg b/src/main/resources/images/background.jpg new file mode 100644 index 000000000000..c51417453a28 Binary files /dev/null and b/src/main/resources/images/background.jpg differ diff --git a/src/main/resources/images/border.jpg b/src/main/resources/images/border.jpg new file mode 100644 index 000000000000..62e70fb700f3 Binary files /dev/null and b/src/main/resources/images/border.jpg differ diff --git a/src/main/resources/images/border2.jpg b/src/main/resources/images/border2.jpg new file mode 100644 index 000000000000..cc1ff782b8c2 Binary files /dev/null and b/src/main/resources/images/border2.jpg differ diff --git a/src/main/resources/images/command_text_field.jpg b/src/main/resources/images/command_text_field.jpg new file mode 100644 index 000000000000..a4e0dc126857 Binary files /dev/null and b/src/main/resources/images/command_text_field.jpg differ diff --git a/src/main/resources/images/result_display.jpg b/src/main/resources/images/result_display.jpg new file mode 100644 index 000000000000..a4e0dc126857 Binary files /dev/null and b/src/main/resources/images/result_display.jpg differ diff --git a/src/main/resources/images/split_pane.jpg b/src/main/resources/images/split_pane.jpg new file mode 100644 index 000000000000..62e70fb700f3 Binary files /dev/null and b/src/main/resources/images/split_pane.jpg differ diff --git a/src/main/resources/images/toolbar-selected.png b/src/main/resources/images/toolbar-selected.png new file mode 100644 index 000000000000..c7d16bd69336 Binary files /dev/null and b/src/main/resources/images/toolbar-selected.png differ diff --git a/src/main/resources/images/toolbar.jpg b/src/main/resources/images/toolbar.jpg new file mode 100644 index 000000000000..a4e0dc126857 Binary files /dev/null and b/src/main/resources/images/toolbar.jpg differ diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index c8941ea18263..14276f50f199 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -1,6 +1,6 @@ .background { -fx-background-color: derive(#1d1d1d, 20%); - background-color: #383838; /* Used in the default.html file */ + background-color: #383838; } .label { @@ -37,6 +37,21 @@ -fx-padding: 0 0 0 0; -fx-min-height: 0; -fx-max-height: 0; + -fx-background-image: url("../images/toolbar.jpg"); +} + +.tab-pane .tab-label { + -fx-text-fill: white; +} + +.tab-pane .tab:selected { + -fx-border-color: transparent !important; + -fx-background-image: url("../images/toolbar-selected.png"); +} + + +.tab-pane .tab-header-area .tab-header-background { + -fx-opacity: 0; } .table-view { @@ -84,7 +99,7 @@ .split-pane { -fx-border-radius: 1; -fx-border-width: 1; - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-image: url("../images/split_pane.jpg"); } .list-view { @@ -138,6 +153,7 @@ .pane-with-border { -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-image: url("../images/border.jpg"); -fx-border-color: derive(#1d1d1d, 10%); -fx-border-top-width: 1px; } @@ -192,7 +208,7 @@ } .menu-bar { - -fx-background-color: derive(#1d1d1d, 20%); + -fx-background-image: url("../images/background.jpg"); } .menu-bar .label { @@ -317,7 +333,7 @@ } #commandTextField { - -fx-background-color: transparent #383838 transparent #383838; + -fx-background-image: url("../images/command_text_field.jpg"); -fx-background-insets: 0; -fx-border-color: #383838 #383838 #ffffff #383838; -fx-border-insets: 0; @@ -332,8 +348,8 @@ } #resultDisplay .content { - -fx-background-color: transparent, #383838, transparent, #383838; - -fx-background-radius: 0; + -fx-background-image: url("../images/result_display.jpg"); + background-repeat: repeat-y; } #tags { diff --git a/src/main/resources/view/ExpensesListCard.fxml b/src/main/resources/view/ExpensesListCard.fxml new file mode 100644 index 000000000000..2faca3ee65ed --- /dev/null +++ b/src/main/resources/view/ExpensesListCard.fxml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/ExpensesListPanel.fxml b/src/main/resources/view/ExpensesListPanel.fxml new file mode 100644 index 000000000000..84df211d717c --- /dev/null +++ b/src/main/resources/view/ExpensesListPanel.fxml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml index daf386d8f5b8..fb8e78a29301 100644 --- a/src/main/resources/view/MainWindow.fxml +++ b/src/main/resources/view/MainWindow.fxml @@ -11,58 +11,167 @@ + + - - - - - - - - - + minWidth="1200" minHeight="600" onCloseRequest="#handleExit"> + + + + + + + + + - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - + + + + diff --git a/src/main/resources/view/PersonListCard.fxml b/src/main/resources/view/PersonListCard.fxml index f08ea32ad558..949b930d71c5 100644 --- a/src/main/resources/view/PersonListCard.fxml +++ b/src/main/resources/view/PersonListCard.fxml @@ -25,12 +25,100 @@ -