diff --git a/README.md b/README.md index 13f5c77403f..63d2e239304 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,20 @@ -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) +[![Java CI](https://github.com/AY2122S1-CS2103T-W11-2/tp/actions/workflows/gradle.yml/badge.svg)](https://github.com/AY2122S1-CS2103T-W11-2/tp/actions/workflows/gradle.yml) ![Ui](docs/images/Ui.png) +### **Staff'd** -* This is **a sample project for Software Engineering (SE) students**.
- Example usages: - * as a starting point of a course project (as opposed to writing everything from scratch) - * as a case study -* The project simulates an ongoing software project for a desktop application (called _AddressBook_) used for managing contact details. - * It is **written in OOP fashion**. It provides a **reasonably well-written** code base **bigger** (around 6 KLoC) than what students usually write in beginner-level SE modules, without being overwhelmingly big. - * It comes with a **reasonable level of user and developer documentation**. -* It is named `AddressBook Level 3` (`AB3` for short) because it was initially created as a part of a series of `AddressBook` projects (`Level 1`, `Level 2`, `Level 3` ...). -* For the detailed documentation of this project, see the **[Address Book Product Website](https://se-education.org/addressbook-level3)**. -* This project is a **part of the se-education.org** initiative. If you would like to contribute code to this project, see [se-education.org](https://se-education.org#https://se-education.org/#contributing) for more info. +**Introduction** + +This is project by Software Engineering students. + +It is targeted towards tech-savvy managers of food chain services who want to manage their staff. + +This is because it can be complicated and tedious for managers of such food chain services to manually keep track of their staff information, schedules, working hours, and salaries. Staff’d provides a central management system of staff that allows for easy and intuitive tracking and handling of the aforementioned data. + +**Example usages:** + * as a manager, you can add, delete, view, and edit staff information (including their schedules!) easily through Command Line Interface (CLI) + * view the weekly schedule of a staff, or even view the overall weekly schedule + * as a manager, you can automatically calculate their salary and view the salary report + * additionally, you can find and view the information of any staff easily by searching up their name, staff ID, or even index in the staff list generated + +This project is based on the AddressBook-Level3 project created by the [SE-EDU initiative](https://se-education.org). diff --git a/build.gradle b/build.gradle index be2d2905dde..78434bd1812 100644 --- a/build.gradle +++ b/build.gradle @@ -16,6 +16,10 @@ repositories { maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } } +run { + enableAssertions = true +} + checkstyle { toolVersion = '8.29' } @@ -66,7 +70,7 @@ dependencies { } shadowJar { - archiveName = 'addressbook.jar' + archiveName = 'staffd.jar' } defaultTasks 'clean', 'test' diff --git a/docs/AboutUs.md b/docs/AboutUs.md index 1c9514e966a..a8b087fb321 100644 --- a/docs/AboutUs.md +++ b/docs/AboutUs.md @@ -5,55 +5,53 @@ title: About Us We are a team based in the [School of Computing, National University of Singapore](http://www.comp.nus.edu.sg). -You can reach us at the email `seer[at]comp.nus.edu.sg` - ## Project team -### John Doe +### Gabriel Au - + -[[homepage](http://www.comp.nus.edu.sg/~damithch)] -[[github](https://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/Gabau)] +[[portfolio](team/gabau.md)] -* Role: Project Advisor +* Role: Developer +* Responsibilities: UI, Code Quality, Testing and Integration -### Jane Doe +### Jonathan Tan - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/fullfatwasabi)] +[[portfolio](team/fullfatwasabi.md)] -* Role: Team Lead -* Responsibilities: UI +* Role: Developer +* Responsibilities: Tool, Documentation, Scheduling and tracking -### Johnny Doe +### Sreenivasa Kalpana Surya - + -[[github](http://github.com/johndoe)] [[portfolio](team/johndoe.md)] +[[github](http://github.com/tetrerox)] +[[portfolio](team/tetrerox.md)] * Role: Developer -* Responsibilities: Data +* Responsibilities: Integration, Code Quality and Documentation -### Jean Doe +### He Outong - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/IrvingHe000)] +[[portfolio](team/irvinghe000.md)] * Role: Developer -* Responsibilities: Dev Ops + Threading +* Responsibilities: Testing, Integration, Tool expert -### James Doe +### Megan Wee Rui En - + -[[github](http://github.com/johndoe)] -[[portfolio](team/johndoe.md)] +[[github](http://github.com/mweeruien)][[portfolio](team/mweeruien.md)] * Role: Developer -* Responsibilities: UI +* Responsibilities: Team Lead, Deliverables and deadlines, and Assistant GUI Expert diff --git a/docs/DeveloperGuide.md b/docs/DeveloperGuide.md index 46eae8ee565..de8832b2641 100644 --- a/docs/DeveloperGuide.md +++ b/docs/DeveloperGuide.md @@ -1,15 +1,37 @@ --- -layout: page +layout: page title: Developer Guide --- -* Table of Contents + +## Table of Contents + +* Table of Contents {:toc} -------------------------------------------------------------------------------------------------------------------- +## **Introduction** + +Staff’d helps food & beverage managers manage details and schedules of their staff. It is optimized for CLI users so +that frequent tasks can be done faster by typing in commands. It is optimized for restaurants with two active shifts. +Please refer to the [_User Guide_](https://ay2122s1-cs2103t-w11-2.github.io/tp/UserGuide.html) for more information +about Staff'd. + +-------------------------------------------------------------------------------------------------------------------- + +## **Overview** + +This guide is intended for future developers, current contributors and users. This guide mainly aims to explain the +implementation of Staff'd to future developers and deepen their knowledge in software development. By the end of this +guide, you can expect to get an overview of the design architecture of Staff'd and comprehensive details of some of its +core features, backed up by UML diagrams. + +-------------------------------------------------------------------------------------------------------------------- + ## **Acknowledgements** -* {list here sources of all reused/adapted ideas, code, documentation, and third-party libraries -- include links to the original source as well} +This project is based on the [AddressBook-Level3](https://github.com/nus-cs2103-AY2122S1/tp) ([UG](https://se-education.org/addressbook-level3/UserGuide.html), +[DG](https://se-education.org/addressbook-level3/DeveloperGuide.html)) project created by the [SE-EDU initiative](https://se-education.org). -------------------------------------------------------------------------------------------------------------------- @@ -23,7 +45,10 @@ Refer to the guide [_Setting up and getting started_](SettingUp.md).
-:bulb: **Tip:** The `.puml` files used to create diagrams in this document can be found in the [diagrams](https://github.com/se-edu/addressbook-level3/tree/master/docs/diagrams/) folder. Refer to the [_PlantUML Tutorial_ at se-edu/guides](https://se-education.org/guides/tutorials/plantUml.html) to learn how to create and edit diagrams. +:bulb: **Tip:** The `.puml` files used to create diagrams in this document can be found in +the [diagrams](https://github.com/AY2122S1-CS2103T-W11-2/tp/tree/master/docs/diagrams/) folder. Refer to the [_PlantUML +Tutorial_ at se-edu/guides](https://se-education.org/guides/tutorials/plantUml.html) to learn how to create and edit +diagrams.
### Architecture @@ -36,7 +61,11 @@ Given below is a quick overview of main components and how they interact with ea **Main components of the architecture** -**`Main`** has two classes called [`Main`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/Main.java) and [`MainApp`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/MainApp.java). It is responsible for, +**`Main`** has two classes +called [`Main`](https://github.com/AY2122S1-CS2103T-W11-2/tp/tree/master/src/main/java/seedu/address/Main.java) +and [`MainApp`](https://github.com/AY2122S1-CS2103T-W11-2/tp/tree/master/src/main/java/seedu/address/MainApp.java). It +is responsible for, + * At app launch: Initializes the components in the correct sequence, and connects them up with each other. * At shut down: Shuts down the components and invokes cleanup methods where necessary. @@ -49,19 +78,23 @@ The rest of the App consists of four components. * [**`Model`**](#model-component): Holds the data of the App in memory. * [**`Storage`**](#storage-component): Reads data from, and writes data to, the hard disk. - **How the architecture components interact with each other** -The *Sequence Diagram* below shows how the components interact with each other for the scenario where the user issues the command `delete 1`. +The *Sequence Diagram* below shows how the components interact with each other for the scenario where the user issues +the command `delete -i 1`. Each of the four main components (also shown in the diagram above), * defines its *API* in an `interface` with the same name as the Component. -* implements its functionality using a concrete `{Component Name}Manager` class (which follows the corresponding API `interface` mentioned in the previous point. +* implements its functionality using a concrete `{Component Name}Manager` class (which follows the corresponding + API `interface` mentioned in the previous point. -For example, the `Logic` component defines its API in the `Logic.java` interface and implements its functionality using the `LogicManager.java` class which follows the `Logic` interface. Other components interact with a given component through its interface rather than the concrete class (reason: to prevent outside component's being coupled to the implementation of a component), as illustrated in the (partial) class diagram below. +For example, the `Logic` component defines its API in the `Logic.java` interface and implements its functionality using +the `LogicManager.java` class which follows the `Logic` interface. Other components interact with a given component +through its interface rather than the concrete class (reason: to prevent outside component's being coupled to the +implementation of a component), as illustrated in the (partial) class diagram below. @@ -69,13 +102,20 @@ The sections below give more details of each component. ### UI component -The **API** of this component is specified in [`Ui.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/ui/Ui.java) +The **API** of this component is specified +in [`Ui.java`](https://github.com/AY2122S1-CS2103T-W11-2/tp/tree/master/src/main/java/seedu/address/ui/Ui.java) ![Structure of the UI Component](images/UiClassDiagram.png) -The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `PersonListPanel`, `StatusBarFooter` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class which captures the commonalities between classes that represent parts of the visible GUI. +The UI consists of a `MainWindow` that is made up of parts e.g.`CommandBox`, `ResultDisplay`, `PersonListPanel` +, `StatusBarFooter` etc. All these, including the `MainWindow`, inherit from the abstract `UiPart` class which captures +the commonalities between classes that represent parts of the visible GUI. -The `UI` component uses the JavaFx UI framework. The layout of these UI parts are defined in matching `.fxml` files that are in the `src/main/resources/view` folder. For example, the layout of the [`MainWindow`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/ui/MainWindow.java) is specified in [`MainWindow.fxml`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/resources/view/MainWindow.fxml) +The `UI` component uses the JavaFx UI framework. The layout of these UI parts are defined in matching `.fxml` files that +are in the `src/main/resources/view` folder. For example, the layout of +the [`MainWindow`](https://github.com/AY2122S1-CS2103T-W11-2/tp/tree/master/src/main/java/seedu/address/ui/MainWindow.java) +is specified +in [`MainWindow.fxml`](https://github.com/AY2122S1-CS2103T-W11-2/tp/tree/master/src/main/resources/view/MainWindow.fxml) The `UI` component, @@ -84,23 +124,33 @@ The `UI` component, * keeps a reference to the `Logic` component, because the `UI` relies on the `Logic` to execute commands. * depends on some classes in the `Model` component, as it displays `Person` object residing in the `Model`. + + +The `Schedule view` subcomponent of the `UI` component meant to facilitate the display of the [schedule view](UserGuide.md#schedule-display), + +* displays 2 `SlotCard` for each `DayCard` +* listens to changes in `Model` data to be updated with the modified data + ### Logic component -**API** : [`Logic.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/logic/Logic.java) +**API** : [`Logic.java`](https://github.com/AY2122S1-CS2103T-W11-2/tp/tree/master/src/main/java/seedu/address/logic/Logic.java) Here's a (partial) class diagram of the `Logic` component: How the `Logic` component works: + 1. When `Logic` is called upon to execute a command, it uses the `AddressBookParser` class to parse the user command. -1. This results in a `Command` object (more precisely, an object of one of its subclasses e.g., `AddCommand`) which is executed by the `LogicManager`. +1. This results in a `Command` object (more precisely, an object of one of its subclasses e.g., `AddCommand`) which is + executed by the `LogicManager`. 1. The command can communicate with the `Model` when it is executed (e.g. to add a person). 1. The result of the command execution is encapsulated as a `CommandResult` object which is returned back from `Logic`. -The Sequence Diagram below illustrates the interactions within the `Logic` component for the `execute("delete 1")` API call. +The Sequence Diagram below illustrates the interactions within the `Logic` component for the `execute("delete -i 1")` API +call. -![Interactions Inside the Logic Component for the `delete 1` Command](images/DeleteSequenceDiagram.png) +![Interactions Inside the Logic Component for the `delete -i 1` Command](images/DeleteSequenceDiagram.png)
:information_source: **Note:** The lifeline for `DeleteCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram.
@@ -110,39 +160,67 @@ Here are the other classes in `Logic` (omitted from the class diagram above) tha How the parsing works: -* When called upon to parse a user command, the `AddressBookParser` class creates an `XYZCommandParser` (`XYZ` is a placeholder for the specific command name e.g., `AddCommandParser`) which uses the other classes shown above to parse the user command and create a `XYZCommand` object (e.g., `AddCommand`) which the `AddressBookParser` returns back as a `Command` object. -* All `XYZCommandParser` classes (e.g., `AddCommandParser`, `DeleteCommandParser`, ...) inherit from the `Parser` interface so that they can be treated similarly where possible e.g, during testing. + +* When called upon to parse a user command, the `AddressBookParser` class creates an `XYZCommandParser` (`XYZ` is a + placeholder for the specific command name e.g., `AddCommandParser`) which uses the other classes shown above to parse + the user command and create a `XYZCommand` object (e.g., `AddCommand`) which the `AddressBookParser` returns back as + a `Command` object. +* All `XYZCommandParser` classes (e.g., `AddCommandParser`, `DeleteCommandParser`, ...) inherit from the `Parser` + interface so that they can be treated similarly where possible e.g, during testing. ### Model component -**API** : [`Model.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/model/Model.java) - +**API** : [`Model.java`](https://github.com/AY2122S1-CS2103T-W11-2/tp/tree/master/src/main/java/seedu/address/model/Model.java) + The `Model` component, -* stores the address book data i.e., all `Person` objects (which are contained in a `UniquePersonList` object). -* stores the currently 'selected' `Person` objects (e.g., results of a search query) as a separate _filtered_ list which is exposed to outsiders as an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be bound to this list so that the UI automatically updates when the data in the list change. -* stores a `UserPref` object that represents the user’s preferences. This is exposed to the outside as a `ReadOnlyUserPref` objects. -* does not depend on any of the other three components (as the `Model` represents data entities of the domain, they should make sense on their own without depending on other components) +* stores the staff data i.e., all `Person` objects (which are contained in a `UniquePersonList` object). +* stores the currently 'selected' `Person` objects (e.g., results of a search query) as a separate _filtered_ list which + is exposed to outsiders as an unmodifiable `ObservableList` that can be 'observed' e.g. the UI can be bound to + this list so that the UI automatically updates when the data in the list change. +* stores a `UserPref` object that represents the user’s preferences. This is exposed to the outside as + a `ReadOnlyUserPref` objects. +* does not depend on any of the other three components (as the `Model` represents data entities of the domain, they + should make sense on their own without depending on other components) -
:information_source: **Note:** An alternative (arguably, a more OOP) model is given below. It has a `Tag` list in the `AddressBook`, which `Person` references. This allows `AddressBook` to only require one `Tag` object per unique tag, instead of each `Person` needing their own `Tag` objects.
+
:information_source: **Note:** An alternative (arguably, a more OOP) model is given below. It has a `Tag` list and a `Role` list in the `AddressBook`, which `Person` references. This allows `AddressBook` to only require one `Tag` object per unique tag and one `Role` object per unique role, instead of each `Person` needing their own `Tag` and `Role` objects.
+ + +The `Person` subcomponent, + +* stores what a `Person` should have, i.e. all `Field` objects and details of employment (e.g. Schedule, dates of being absent). +* does not depend on any of the other three components as it is part of the `Model` component. ### Storage component -**API** : [`Storage.java`](https://github.com/se-edu/addressbook-level3/tree/master/src/main/java/seedu/address/storage/Storage.java) +**API** : [`Storage.java`](https://github.com/AY2122S1-CS2103T-W11-2/tp/tree/master/src/main/java/seedu/address/storage/Storage.java) The `Storage` component, -* can save both address book data and user preference data in json format, and read them back into corresponding objects. -* inherits from both `AddressBookStorage` and `UserPrefStorage`, which means it can be treated as either one (if only the functionality of only one is needed). -* depends on some classes in the `Model` component (because the `Storage` component's job is to save/retrieve objects that belong to the `Model`) + +* can save both address book data and user preference data in json format, and read them back into corresponding + objects. +* inherits from both `AddressBookStorage` and `UserPrefStorage`, which means it can be treated as either one (if only + the functionality of only one is needed). +* depends on some classes in the `Model` component (because the `Storage` component's job is to save/retrieve objects + that belong to the `Model`) + + + +The storage of `Person`, + +* can save all the details of a staff in `JsonAdaptedPerson` +* depends on the `Model` component as it is in the `Storage` component + + ### Common classes @@ -154,90 +232,323 @@ Classes used by multiple components are in the `seedu.addressbook.commons` packa This section describes some noteworthy details on how certain features are implemented. -### \[Proposed\] Undo/redo feature +### Mark/unmark feature -#### Proposed Implementation +#### Implementation -The proposed undo/redo mechanism is facilitated by `VersionedAddressBook`. It extends `AddressBook` with an undo/redo history, stored internally as an `addressBookStateList` and `currentStatePointer`. Additionally, it implements the following operations: +The mark/unmark feature is facillated by `Person`. It uses the following +operations of `Person`. The marked periods are stored as `Set` objects. -* `VersionedAddressBook#commit()` — Saves the current address book state in its history. -* `VersionedAddressBook#undo()` — Restores the previous address book state from its history. -* `VersionedAddressBook#redo()` — Restores a previously undone address book state from its history. + * `Person#mark()` — Adds the input time period to the person to be marked as absent. + * `Person#unmark()` — Removes the input time period from the person. -These operations are exposed in the `Model` interface as `Model#commitAddressBook()`, `Model#undoAddressBook()` and `Model#redoAddressBook()` respectively. +These operations make use of `Period#union()` and `Period#complement` and are exposed +by `MarkCommand#execute()` and `RemoveMarkCommand#execute()`. -Given below is an example usage scenario and how the undo/redo mechanism behaves at each step. -Step 1. The user launches the application for the first time. The `VersionedAddressBook` will be initialized with the initial address book state, and the `currentStatePointer` pointing to that single address book state. +Given below is an example usage scenario and how the mark/unmark command +changes the information stored. -![UndoRedoState0](images/UndoRedoState0.png) +Step 1. The user launches the application for the first time. The staff all +the staff information is read from the save file `staffd.json`. +The initial information of the staff that we are looking at +will look something like this. The `Period` object that we are using can be taken to +represent 1/1/2001 to 2/1/2001 -Step 2. The user executes `delete 5` command to delete the 5th person in the address book. The `delete` command calls `Model#commitAddressBook()`, causing the modified state of the address book after the `delete 5` command executes to be saved in the `addressBookStateList`, and the `currentStatePointer` is shifted to the newly inserted address book state. -![UndoRedoState1](images/UndoRedoState1.png) +![state0](images/MarkState0.png) -Step 3. The user executes `add n/David …​` to add a new person. The `add` command also calls `Model#commitAddressBook()`, causing another modified address book state to be saved into the `addressBookStateList`. -![UndoRedoState2](images/UndoRedoState2.png) +Step 2. The user executes the command `mark -i 1 d/2001-01-04`. The `mark` command +calls `ParserUtil#parsePeriod()`, to obtain the time period to mark, and `Person#mark()` to mark this time period on this person. This method uses +`Period#union()` to union the time period with the set. -
:information_source: **Note:** If a command fails its execution, it will not call `Model#commitAddressBook()`, so the address book state will not be saved into the `addressBookStateList`. -
+![state1](images/MarkState1.png) -Step 4. The user now decides that adding the person was a mistake, and decides to undo that action by executing the `undo` command. The `undo` command will call `Model#undoAddressBook()`, which will shift the `currentStatePointer` once to the left, pointing it to the previous address book state, and restores the address book to that state. +Step 3. The user executes the command `mark -i 1 d/2001-01-03`. The `mark` command +calls `ParserUtil#parsePeriod()`, to obtain the period to mark as before. As this time period makes the current range, 1/1/2001 to 2/1/2001 and 4/1/2001 +become a single range, `Period#union()` that is called, merges the two `Period` +objects into one. -![UndoRedoState3](images/UndoRedoState3.png) -
:information_source: **Note:** If the `currentStatePointer` is at index 0, pointing to the initial AddressBook state, then there are no previous AddressBook states to restore. The `undo` command uses `Model#canUndoAddressBook()` to check if this is the case. If so, it will return an error to the user rather -than attempting to perform the undo. +![state2](images/MarkState2.png) -
+The following sequence diagram shows how the mark command works. -The following sequence diagram shows how the undo operation works: -![UndoSequenceDiagram](images/UndoSequenceDiagram.png) +![seq](images/MarkSequenceDiagram.png) -
:information_source: **Note:** The lifeline for `UndoCommand` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. -
-The `redo` command does the opposite — it calls `Model#redoAddressBook()`, which shifts the `currentStatePointer` once to the right, pointing to the previously undone state, and restores the address book to that state. +The following is an activity diagram showing the general activity of the mark command. -
:information_source: **Note:** If the `currentStatePointer` is at index `addressBookStateList.size() - 1`, pointing to the latest address book state, then there are no undone AddressBook states to restore. The `redo` command uses `Model#canRedoAddressBook()` to check if this is the case. If so, it will return an error to the user rather than attempting to perform the redo. +![activity](images/MarkActivityDiagram.png) -
+The merging of the periods in `mark` command is facilitated by `Period#union()`, which takes +in a collection of Period and performs a mathematical union on the collection, treating the +collection as a set of dates. +The code is found [here](https://github.com/AY2122S1-CS2103T-W11-2/tp/tree/master/src/main/java/seedu/address/model/person/Period.java). + +The following is an activity diagram showcasing a simplified view of the activity of the union method. + +![activity](images/PeriodUnionActivityDiagram.png) + +The `unmark` command does the opposite — it calls `Person#unmark()`, which replaces the +`Period` that are contained in the `Person` with the `Period` objects representing +the initial `Period` without the input `Period`. This command is facilitated by the +`Period#complement()` method which is called on all the stored `Period` objects. + + + + + + +#### Design considerations + +**Aspect: How to mark attendance** + + * **Alternative 1 (current implementation):** Mark absent. + * Pros: Changes less amount of data in the case of high turnout rate. + * Cons: Harder to implement features that make use of a staff not being present. + * **Alternative 2**: Mark present. + * Pros: Easier to implement other features that make use of a staff being present. + * Cons: Changes more data in the case of high turnout rate. + +**Aspect: How mark and unmark is represented** + + * **Alternative 1 (current implementation):** Stored in the class for a staff. + * Pros: Easy to implement. + * Cons: Increased reliance on + * **Alternative 2**: Stored in the class representing a shift. + * Pros: Easier to implement features related to both shifts and attendance. + * Cons: More memory intensive. + +**Aspect: Representation of a `Period`** + + * **Alternative 1(current implementation):** Use `LocalDate` to represent a period + * Pros: Makes the command easier to enter. + * Cons: Makes shift related features and attendance related features harder to use. + * **Alternative 2(possible implementation):** Use `LocalDateTime` to represent a period + * Pros: Makes shift related features easier to implement. + * Cons: Parser becomes more complicated. + +-------------------------------------------------------------------------------------------------------------------- + +### Add shift to staff's schedule + +#### Implementation + +`addShift` is a command for the app to add a shift into a staff's schedule. When the user want to use this command, the +target staff, and the specific shift should be indicated. + +The activity diagram of `addShift` command is shown below: + +![AddShiftActivity](images/AddShiftActivityDiagram.png) + + +The add shift functionality is facilitated by `ModelManager`. It uses the following operation of `ModelManager`. + +- `ModelManager#findPersonByName()` — Find the first person with given Name from the address book. If the person is not + found, null will be returned. + +- `ModelManager#addShift()` — Add a shift to the target person's schedule. If that shift has already existed, a + `DuplicateShiftException` will be thrown. + +Also, `AddShiftCommandParser` and `AddShiftCommand` are created to achieve this functionality. + +![AddShiftClass](images/AddShiftClassDiagram.png) + +The way of implementing `addShift` functionality follows the architecture of the app. Given below is an example usage +scenario and the workflow of the +`addShift` command. + +Step 1. The user executes command `addShift -n Steve d/Monday-0`. +`Ui` component reads the command as a string from user's input. After that, `MainWindow` +passes the string to `LogicManager` to manipulate the command. + +Step 2. `LogicManager` passes the command to `AddressBookParser` to parse the command. Since the command starts +with `addShift`, a new `AddShiftCommandParser` is created to parse the command further. + +Step 3. `AddShiftCommandParser` uses `ArgumentMultimap` to tokenize the prefixes part the command. After extracting the +information the target staff, the shift, and the date, A new +`AddShiftCommand` is created with the information. In this case, the name of the target staff is "Steve", and the +proposed shift is on Monday morning, and the period is seven days starting from the current date when the user runs the +command. + +Step 4. `AddShiftCommand` passes the given name to `ModelManager#findPersonByName()`. After finding the specific +staff, `AddShiftCommand` passes the `targetStaff` +`dayOfWeek`, `slot` and `startDate`, `endDate` of the shift to `ModelManager#addShift()`. + +Step 5. `Modelmanager#addShift()` updates the schedule of the `targetStaff` with a new `Shift` created with the given +`dayOfWeek`, `slot` and `startDate`, `endDate`. + +The sequence diagram of this `addShift` command is shown below: + +![AddShiftSequence](images/AddShiftSequenceDiagram.png) + + +Notes: + +1. User can also search the target staff with the staff's index in `lastShownList` +2. A command is considered as a valid `addShift` command if it follows these formats: + +- `addShift -n name d/fullDayName-slotNumber` +- `addShift -i index d/fullDayName-slotNumber` + +3. `fullDayName` can be any day from "monday" to "sunday". Noticed that it's not case-sensitive. +4. `slotNumber` can only be 0 or 1 currently. + +#### Design considerations + +**Aspect: Add the shift to a group of staffs** + +* **Alternative 1 (current implementation):** Add one by one + * Pros: Easy to implemented. Decrease the chance to add shift to a wrong person. + * Cons: Time consuming for user to add one by one. +* **Alternative 2 (proposed implementation):** Add shift to a group of staffs. + * Pros: Save time for users. + * Cons: User need to specify the group of staffs before adding shift to their schedules. + + +-------------------------------------------------------------------------------------------------------------------- + +### Find Staff + +#### Implementation + +`Find` is a command for the app to search for staff by a specific index or name. When the command is called, +whether it is a search by name or search by index should be indicated with the respective tags `-n` and `-i` along with any additional fields. + +The Find functionality is facilitated by `ModelManager`. It uses the following operation of `ModelManager`. + +- `ModelManager#updateFilteredPersonList(Predicate +Notes: -#### Design considerations: +1. The process is similar for a search by index, but with `NameContainsKeywordsPredicate` replaced with `StaffHasCorrectIndexPredicate` +2. A command is considered as a valid `Find` command if it follows these formats: -**Aspect: How undo & redo executes:** +- `find -n keywords` +- `find -i index` -* **Alternative 1 (current choice):** Saves the entire address book. - * Pros: Easy to implement. - * Cons: May have performance issues in terms of memory usage. +3. The keywords are not case-sensitive. +4. Index search operates on the filtered list, which is the displayed staff list, and not the overall staff list. -* **Alternative 2:** Individual command knows how to undo/redo by - itself. - * Pros: Will use less memory (e.g. for `delete`, just save the person being deleted). - * Cons: We must ensure that the implementation of each individual command are correct. -_{more aspects and alternatives to be added}_ +-------------------------------------------------------------------------------------------------------------------- +### ViewShift + +#### Implementation + +`viewShift` is a command for the app to search for the staff working at a specific day and shift. The search is either conducted +by a specific time or by the slot number. When the command is used, whether it is a search by slot number or search by time +should be indicated with the respective tags `-d` and `-ti`. + +The activity diagram of `viewShift` command is shown below: + +![ViewShiftActivity](images/ViewShiftActivityDiagram.png) + +The Find functionality is facilitated by `ModelManager`. It uses the following operation of `ModelManager`. + +- `ModelManager#updateFilteredPersonList(Predicate + 2. Re-launch the app by double-clicking the jar file.
Expected: The most recent window size and location is retained. -1. _{ more test cases …​ }_ + + + +### Adding a person +1. Adding a person to the address book + + 1. Test case: `add n/John Doe p/98765432 e/johnd@example.com $/100`
+ Expected: If there is already a person called `John Doe` in the address book, then an error message will appear + at the left output box. Otherwise, a new staff will be added to the list in the right output box, with name `John Doe`, + phone number `98765432`, email `johnd@example.com`, and salary `100`. + + 1. Test case: `add n/John Doe p/98765432`
+ Expected: No person is added. Error details shown in the status message. + Status bar remains the same. + + 1. Other incorrect `add` commands to try: `add p/98765432 e/johnd@example.com $/100`, + `add n/John Doe p/98765432 e/johnd@example.com`, `...` (where one or more attributes + are missing in the command) + Expected: A wrong message of `Invalid command format` will be shown in the status message. + ### Deleting a person 1. Deleting a person while all persons are being shown - 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. + 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. + + 1. Test case: `delete -i 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. + + 1. Test case: `delete -i 0`
+ Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. + + 1. Other incorrect `delete` commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
+ Expected: Similar to previous. + + +### Adding a shift to a person's schedule + +1. Adding a shift to an existing person's schedule, given the target person's index in the list. + + 1. Prerequisites: List all persons using the `list` command. Multiple persons in the list. + + 1. Test case: `addShift -i 1 d/Monday-0` + Expected: A shift on next Monday morning will be added to the first staff's schedule in the list, + if that slot does not have a shift yet. Otherwise, no shift is added and error details will be shown + in the status message. + + 1. Test case: `addShift -i 1 d/Monday-0 da/2021-11-07` + Expected: A new shift will be added to the first person in the list schedule. In this case, + the date of that shift will be `2021-11-08`, and the slot is `morning`. + Otherwise, no shift is added and error details will be shown in the status message. + + 1. Test case: `addShift -i 1 d/Monday-0 da/2021-11-06 da/2021-12-06` + Expected: New shifts will be added to the first person in the list schedule. In this case, + the date of shifts will be `2021-11-08`, `2021-11-15`, `2021-11-22`, `2021-11-29`, `2021-12-06` + and the slot is `morning`. Otherwise, no shift is added and error details will be shown in the status message. + + 1. Other incorrect `addShift` commands to try: `addShift -i 0 d/Monday-0`, `addShift -i x d/Monday-0`, (where + x is larger than the list size) `addShift -i 0 d/mon-0`, `...`. + Expected: A wrong details will be shown in the status message. + +### Saving data - 1. Test case: `delete 1`
- Expected: First contact is deleted from the list. Details of the deleted contact shown in the status message. Timestamp in the status bar is updated. +1. Getting the default save file. - 1. Test case: `delete 0`
- Expected: No person is deleted. Error details shown in the status message. Status bar remains the same. + 1. Prerequisites: Place _staffd.jar_ in an empty _home folder_. - 1. Other incorrect delete commands to try: `delete`, `delete x`, `...` (where x is larger than the list size)
- Expected: Similar to previous. + 1. Test case: `addShift -i 1 d/monday-0`
+ Expected: _staffd.json_ appears in _data_ folder inside _home folder_. Staff with name `Alex Yeoh` has a field history in _staffd.json_ whereas the other staff do not. Sample output [here](sample/addShift.json). Note the time period output will be different with date. -1. _{ more test cases …​ }_ + 1. Test case: `delete -i 1`
+ Expected: _staffd.json_ is updated with `Alex Yeoh removed`. Sample output [here](sample/deleteAlex.json). -### Saving data -1. Dealing with missing/corrupted data files +1. Clearing the save file. - 1. _{explain how to simulate a missing/corrupted file, and the expected behavior}_ + 1. Prerequisites: Have _staffd.json_ in the _data_ folder. Perform the previous step if the file isn't there. + + 1. Test case: `clear`
+ Expected: An empty _staffd.json_ file like [here](sample/empty.json). + +-------------------------------------------------------------------------------------------------------------------- -1. _{ more test cases …​ }_ +## **Appendix C: Effort** + +During the V1.2 iteration, we had set out to revamp the entire codebase to accommodate F&B staff and their details as +compared to regular people. This task was moderately difficult as on one hand, we only had to refactor any instances of +`person` to `staff` but on the other hand, we had to spend a lot of time implementing and adding fields such as `salary`, +`schedule`, `role` and `status`. The implementation of `schedule` alone warranted a 4-hour meeting as there was a debate +on how staff shifts should be stored and how many shifts we would have or even whether we should allow users to choose +how many shifts they would want in a day and the timing of each shift. This iteration was also the time when we just +started adding code to the existing codebase and as such, we faced the difficulty of understanding it. Not only did we +have to understand the code, we had to also understand the abstraction behind the code and adhere to it when adding new +`person` fields and functions. For all of us, this was the portion that required the most of our effort. + +V1.3 was the most difficult and time-consuming iteration. The GUI was one of the hardest parts of this project. Due to +our unfamiliarity with JavaFX (the library that we used for our GUI), we spent a significant amount of our effort +changing the GUI to fit the staff details, showing the staff schedule and putting it on another tab. From having almost +zero knowledge about JavaFX to changing the whole GUI was a huge achievement for our team as a whole. Besides that, this +was the last iteration where we could add new features, so we ramped up the number of commands that we added such as +`mark`, `swapShift` and `setShiftTime` just to name a few. However, this also led to some unfortunate circumstances. +By the end of this iteration and subsequent iterations, this led to numerous bugs. A majority of our time was spent on +fixing these bugs and add comprehensive tests for them. diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 3716f3ca8a4..32dbcb140cd 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -3,190 +3,875 @@ layout: page title: User Guide --- -AddressBook Level 3 (AB3) is a **desktop app for managing contacts, optimized for use via a Command Line Interface** (CLI) while still having the benefits of a Graphical User Interface (GUI). If you can type fast, AB3 can get your contact management tasks done faster than traditional GUI apps. +# Introduction to Staff'd +## Overview +Staff’d is a **consolidated staff management platform** designed for **Food & Beverage store managers**, to aid them with the **management of staff details and schedules**. Staff'd is designed for stores with two shifts - a morning shift and afternoon shift, centrally supervised by one manager. + +As former or present part-timers in the F&B industry, we understand that managers have a high administrative workload on top of their existing store duties. On top of that, any administrative mistakes or oversights can increase that workload exponentially. To address the aforementioned issues, Staff'd was designed as a consolidated staff management system which can **dramatically decrease administrative mistakes and workload**. + +Staff'd is optimized for use via a [**Command Line Interface (CLI)**](#glossary):mag:, and is best suited for users who are adept at typing. A [**Graphical User Interface (GUI)**](#glossary):mag: is also available for ease of visualization and usage. For fast typists, Staff'd can **improve the efficiency of staff management and the ease of handling administrative duties**. After all, any task would be one [Command](#staffd-commands) away! Additionally, the usage of Staff'd requires **no former technical background**, and is **simple to learn**! + +The purpose of this User Guide is to aid Staff'd users **optimize their utilizisation of the Staff'd application** and **improve their Staff'd experience**. + +
+ +## Features Overview +### Staff Information Management +Staff'd aids with the **management of staff information**. The basic functions such as the [_adding_](#adding-a-staff--add), [_editing_](#editing-a-staff--edit), and [_deleting_](#deleting-a-staff--delete) of a staff member and their information are easy to implement. In addition, Staff'd supports the **management of additional information** such as their _status_ as a part-time or full-time worker, _salaries_, and [_roles in the store_](#glossary):mag:. Managers can also _tag_ the Staff members with short descriptions if desired. The information is consolidated and displayed in the Staff Tab of Staff'd as shown below. + +### Staff Schedule Management +Staff'd also supports the management of **staff schedules**. [_Adding_](#adding-a-shift-to-a-staffs-schedule--addshift), [_swapping_](#swapping-shifts--swapshift), or [_deleting_](#deleting-a-shift-from-a-staff--deleteshift) a shift from a staff's schedule can easily be done. Staff'd consolidates these schedules and displays them in an intuitive manner in the Schedule Tab for ease fo reference, as shown below. + +### Staff Salary Calculation +Staff'd supports **salary calculation of staff**. By inputting the salaries and shifts of each staff member, their salaries for the month can be calculated. Staff'd calculates [individual staff statistics](#viewing-individual-staff-statistics--istaff) as well as [overall statistics](#viewing-overall-staff-statistics--stats). + +
+ +-------------------------------------------------------------------------------------------------------------------- +# Table of Contents * Table of Contents {:toc} +
+ -------------------------------------------------------------------------------------------------------------------- -## Quick start +# Quick Start -1. Ensure you have Java `11` or above installed in your Computer. +## Firing up Staff'd -1. Download the latest `addressbook.jar` from [here](https://github.com/se-edu/addressbook-level3/releases). +1. Ensure you have [Java 11](https://java.com/en/download/help/download_options.html) or above installed in your Computer. -1. Copy the file to the folder you want to use as the _home folder_ for your AddressBook. +1. Download the latest **staffd.jar** from [here](https://github.com/AY2122S1-CS2103T-W11-2/tp/releases). -1. Double-click the file to start the app. The GUI similar to the below should appear in a few seconds. Note how the app contains some sample data.
- ![Ui](images/Ui.png) +1. Copy the file to the folder you want to use as the [**home folder**](#glossary):mag: for your Staff’d. -1. Type the command in the command box and press Enter to execute it. e.g. typing **`help`** and pressing Enter will open the help window.
- Some example commands you can try: +1. Double-click the file to start the app. The GUI similar to the below should appear in a few seconds. If there are any issues opening the file, refer to our [FAQ](#faq). Note how the app contains some sample data. - * **`list`** : Lists all contacts. +1. Opening the application creates a [**data folder**](#glossary):mag: inside the home folder. This will contained a **staffd.json** file which is the save file of Staff'd. - * **`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. +![Ui](images/Ui.png) - * **`delete`**`3` : Deletes the 3rd contact shown in the current list. +
- * **`clear`** : Deletes all contacts. +## Staff'd Commands +A Staff'd Command is an **instruction** that signals Staff’d to perform an operation. It consists of a [Command Word](#glossary):mag: and [Parameters](#glossary):mag:. +![CommandTextExample](images/CommandTextExample.png)![CommandTextExample2](images/CommandTextExample2.png) - * **`exit`** : Exits the app. +
-1. Refer to the [Features](#features) below for details of each command. +**:exclamation: Notes about the command format:**
--------------------------------------------------------------------------------------------------------------------- +* Words in `UPPER_CASE` are the **parameters to be supplied**. + e.g. in `add n/NAME`, `NAME` is a parameter which can be replaced with a name, such as `add n/John Doe`. -## Features +* Items in **square brackets** are **optional**. + e.g `n/NAME [t/TAG]` can be inputted as `n/John Doe t/friend` or as `n/John Doe`. -
+* Items with `…` can be used **multiple times**. + e.g. `[t/TAG]…` can be used as ` ` (i.e. 0 times), `t/friend` (i.e 1 times), `t/friend t/family` etc. -**:information_source: Notes about the command format:**
+* Parameters can be inputted in **any order**. + e.g. if the command specifies `n/NAME p/PHONE_NUMBER`, `p/PHONE_NUMBER n/NAME` is also acceptable. -* 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`. +* Parameters must be **inputted with a space** before them. + e.g. `add n/NAME p/PHONE_NUMBER` is accepted, but `addn/NAME p/PHONE_NUMBER` and `add n/NAMEp/PHONE_NUMBER` will not be accepted. -* 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`. +* If a parameter is expected only once in the command but you specified it multiple times, only the **last occurrence** of the parameter will be used. + e.g. if you specify `p/12341234 p/56785678`, only `p/56785678` will be used. -* Items with `…`​ after them can be used multiple times including zero times.
- e.g. `[t/TAG]…​` can be used as ` ` (i.e. 0 times), `t/friend`, `t/friend t/family` etc. +* **Extra parameters will be ignored** for commands that do not take in parameters (such as `help`, `list`, `exit` and `clear`). + e.g. If the command specifies `help 123`, it will be interpreted as `help`. -* 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. +* Some parameters have additional conditions that must be met when inputted. More information can be found from the [Flag Legend](#flag-legend):triangular_flag_on_post:. -* If a parameter is expected only once in the command but you specified it multiple times, only the last occurrence of the parameter will be taken.
- e.g. if you specify `p/12341234 p/56785678`, only `p/56785678` will be taken. -* Extraneous parameters for commands that do not take in parameters (such as `help`, `list`, `exit` and `clear`) will be ignored.
- e.g. if the command specifies `help 123`, it will be interpreted as `help`. +
+ +
+ +## Example Usage of Staff'd +The following is an example of usage of Staff'd by a manager who wishes to **add a new staff member**, and **assign them to the first shift on Monday**. + +
+ +:information_source: Example Usage of **Adding a Staff** to the Restaurant +
+1. Joe wants to join your restaurant. Joe has the following information: +- Name: Joe +- Phone Number: 98765432 +- Email: Joe@example.com +- Status: Full-time worker +- Role: Kitchen staff +1. Add Joe and his information using the [`add`](#adding-a-staff--add) command.
`add n/Joe s/fulltime r/kitchen p/98765432 $/1234789 e/Joe@example.com` +1. Type the command in the command box and press Enter to execute it. + +

+ +:information_source: Example Usage of **Adding a Shift** to a Staff +
+1. Joe is going to work the first shift this Monday. +1. Use the [`addShift`](#adding-a-shift-to-a-staffs-schedule--addshift) command to add the staff to the morning shift.
`addShift -n Joe d/monday-0` +1. Type the command in the command box and press Enter to execute it. +1. Enter [`tab`](#changing-tabs--tab) into the command box and press Enter to change tabs to the Schedule Tab.
-### Viewing help : `help` +Refer to the [Features](#features) section for more details of each command. -Shows a message explaning how to access the help page. +
-![help message](images/helpMessage.png) +## User Guide Usage -Format: `help` +### User Guide Formatting +Formatting Item|Description +---------------|----------- +**Bold** | Emphasizes key points +_Italics_ | Examples to complement descrptions +[Blue Words](#user-guide-formatting) | Links to relevant information +`Code Block` | [Commands](#staffd-commands), Command Parameters, or Command Examples +:information_source: | Extra Information or Usage Examples +:exclamation: | Important information +:triangular_flag_on_post: | Links to [Flag Legend](#flag-legend):triangular_flag_on_post: +:mag: | Links to [Glossary](#glossary):mag: -### Adding a person: `add` +
-Adds a person to the address book. +### User Guide Navigation -Format: `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​` +Item to Clarify | Section in User Guide +--------------- | --------------------- +Navigation | [Table of Contents](#table-of-contents) +Staff'd Command Format | [Staff'd Commands](#staffd-commands) +[GUI](#glossary):mag: | [GUI Breakdown](#gui-breakdown) +Staff'd Command Flags | [Flag Legend](#flag-legend):triangular_flag_on_post: +Technical Terms | [Glossary](#glossary):mag: + +
+ +### User Guide Example Usage +The following is an example of how a user can refer to the User Guide to **learn how to add a new staff member**. + +
+ +:information_source: Example User Guide Usage on **Adding a New Staff** +
+1. Let's say you want to learn more about how to use Staff'd, and thus you read the [Introduction](#introduction-to-staffd) and [Quick Start Guide](#quick-start) of the User Guide. +1. After opening the app, you want to know how to **add a staff member** into the Staff List. Hence, you refer to the [_**Add Staff Command**_](#adding-a-staff--add) section for more information. +1. You now want to know how to **interpret the format of the command**, and thus you refer to the [**Staff'd Commands**](#staffd-commands) section for more information. +1. You are still unsure of what the **flags** in the command mean and refer to the [**Flag Legend**](#flag-legend):triangular_flag_on_post: for more information. +1. After understanding the command and flags, you return to the [_Add Staff Command_](#adding-a-staff--add) section. You read through the description of the command and the extra important information on how to use the command. +1. For a clearer understanding on how to use the command, you can **read through the examples** provided. +1. After fully understanding the command, you input the command into the command box of Staff'd, and check the result message in the output box. + - If the command is successful, a success message will be displayed. + - If the command is unsuccessful, a help message will be displayed, which the you can use to understand where the error is. +1. At any point, if you want to **clarify any technical terms**, you can refer to the [**Glossary**](#glossary):mag:. -
:bulb: **Tip:** -A person can have any number of tags (including 0)
-Examples: -* `add n/John Doe p/98765432 e/johnd@example.com a/John street, block 123, #01-01` -* `add n/Betsy Crowe t/friend e/betsycrowe@example.com a/Newgate Prison p/1234567 t/criminal` +[Return to Table of Contents](#table-of-contents) -### Listing all persons : `list` +
-Shows a list of all persons in the address book. -Format: `list` +## GUI Breakdown +### GUI Overview +![StaffList](images/StaffList.png) +_Staff’d User Interface - Staff Display_ -### Editing a person : `edit` +
-Edits an existing person in the address book. +![StaffSchedule](images/StaffSchedule.png) +_Staff’d User Interface - Schedule Display_ -Format: `edit INDEX [n/NAME] [p/PHONE] [e/EMAIL] [a/ADDRESS] [t/TAG]…​` +GUI Component | Description +------------- | ----------- +Menu Bar | Contains the **File** and **Help** buttons which contains an Exit button and Staff'd's User Guide respectively. +Tabs | The two tabs are **Staff View** and **Schedule**. Clicking on the desired tab name navigates to that tab. +Output Box | **Displays output messages** from Staff'd. These can be **success messages** indicating an operation has succeeded, or **help messages** to assist in understanding how to use Staff'd. +Command Box | Box where users **type in their inputs**. To submit an input, click the **Enter** button. +Staff Display | **Displays** the **staff information** or **schedule information** depending on which tab you are in. -* 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. -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. +### Staff Display +![Staff Fields](images/StaffFields.png) +_Staff Display Breakdown_ -### Locating persons by name: `find` +Staff Field | Description +----------- | ----------- +Index | The index in the displayed staff list +Name | Staff Name +Phone | Staff Phone Number +Email | Staff Email Address +Absent Period | Periods that a staff will be absent from work +Roles | Staff Roles (Available Roles: `bartender`, `floor`, `kitchen`) +Status | Staff Employment Status (Either `fulltime`, `parttime` and `nostatus`) +Salary | Staff Hourly Pay (in Dollars) +Tags | Additional Staff Information that can be used to reference staff. -Finds persons whose names contain any of the given keywords. +For commands relevant to staff details management, refer to [these features](#basic-management-of-staff-details). -Format: `find KEYWORD [MORE_KEYWORDS]` +
-* 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` +### Schedule Display +![Schedule Fields](images/ScheduleFields.png) +_Schedule Display Breakdown_ -Examples: -* `find John` returns `john` and `John Doe` -* `find alex david` returns `Alex Yeoh`, `David Li`
- ![result for 'find alex david'](images/findAlexDavidResult.png) +For commands relevant to staff schedule management, refer to [these features](#basic-management-of-staff-schedules). -### Deleting a person : `delete` +[Return to Table of Contents](#table-of-contents) -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, …​ +# Features -Examples: -* `list` followed by `delete 2` deletes the 2nd person in the address book. -* `find Betsy` followed by `delete 1` deletes the 1st person in the results of the `find` command. +## Utility Features + +### Viewing help : `help` +Opens a **help window** which contains a link to Staff'd information page. + +**Format:** `help` + +![HelpMessage](images/helpMessage.png) + +**Go to:** +[Table of Contents](#table-of-contents) + +
+ +### Changing tabs : `tab` +Switches between the tabs: **Staff View** and **Schedule View**. Reference images for these tabs can be seen [here](#gui-breakdown). + +**Format:** `tab` + +**Go to:** +[Table of Contents](#table-of-contents) + +
+ +### Listing all staffs : `list` + +This **resets the displayed staff list** in both the staff view and schedule view to shows all of the current staffs. Also resets the schedule view to show the schedule for all the staffs to the period set by the [`Change`](#viewing-schedule-for-the-week-change) Command. + +**Format:** `list` + +**Output**: +![SetRoleReq](images/ListCommand.png) +_Full Staff List is displayed._ + +**See Also:**[`setRoleReq` Command](#setting-role-requirements--setrolereq) + + +**Go to:** +[Table of Contents](#table-of-contents) + +
### Clearing all entries : `clear` -Clears all entries from the address book. +**Clears all staff information** from the Staff List. The [role requirements](#glossary):mag: are also reset. -Format: `clear` +**Format:** `clear` -### Exiting the program : `exit` +**See Also:** [`setRoleReq` Command](#setting-role-requirements--setrolereq) + +**Go to:** +[Table of Contents](#table-of-contents) + +
+ +### Setting Role Requirements : `setRoleReq` + +**Sets the role requirements**, which is the minimum number of staff required for each [role](#glossary):mag: in all shifts. +* The role "norole" cannot be set. +* The default role requirements are 0 for all three roles. +* The [Clear Command](#clearing-all-entries--clear) also resets the role requirements to the default. +* Refer to the [Flag Legend](#flag-legend):triangular_flag_on_post: for more information on how to format the input for role requirements. +* The number inputted must be between 0 and 2147483647 (inclusive). +* Multiple roles can be set in the same command, but if the same role is set multiple times in a command, the last instance would be the one set. +* Staff with multiple roles are considered to be fulfilling all of their roles for a shift. For example, if there is one staff working on a shift with the roles _bartender_ and _floor_, and the requirements are 1 for both of those roles, the program considers both roles as fulfilled. + +**Format:** + +`setRoleReq rr/ROLE-NUMBER_REQUIRED...` + +**Examples:** + +`setRoleReq rr/floor-3`\ +`setRoleReq rr/kitchen-4 rr/bartender-2` + +**Output**: +![SetRoleReq](images/SetRoleReq.png) +_After changing role requirements: `viewShift` Command notifies if there is a staff shortage for the viewed shift._ + +**See Also:** [Clear Command](#clearing-all-entries--clear) -Exits the program. +**Go to:** +[Table of Contents](#table-of-contents) +[Flag Legend](#flag-legend):triangular_flag_on_post: -Format: `exit` +
### Saving the data -AddressBook data are saved in the hard disk automatically after any command that changes the data. There is no need to save manually. +Staff'd data are saved in the hard disk **automatically** after any command that changes the data. There is no need to save manually. + +**Go to:** +[Table of Contents](#table-of-contents) + +
### Editing the data file -AddressBook data are saved as a JSON file `[JAR file location]/data/addressbook.json`. Advanced users are welcome to update data directly by editing that data file. +In the [data folder](#glossary):mag:, staff data and [role requirements](#glossary):mag: data is stored. Advanced users can update data directly by **editing that data file**. More information can be found in the [FAQ](#faq). + +
+ +:exclamation: **Caution:** -
:exclamation: **Caution:** -If your changes to the data file makes its format invalid, AddressBook will discard all data and start with an empty data file at the next run. +If your changes to the data file makes its format invalid, Staff'd will discard all data and start with an empty data file at the next run.
-### Archiving data files `[coming in v2.0]` +**Go to:** +[Table of Contents](#table-of-contents) -_Details coming soon ..._ +
--------------------------------------------------------------------------------------------------------------------- +### Exiting the program : `exit` + +**Exits** the program. + +**Format:** `exit` + +**Go to:** +[Table of Contents](#table-of-contents) + +
+ + +## Basic management of Staff Details + +### Adding a staff : `add` + +**Adds** a staff to the system. + +* Upon the addition of a staff, the system provides an `index` for them in the [Staff List](#glossary):mag:, which can be use in other commands, to refer to them. + +**Format:** \ +`add n/NAME p/PHONE_NUMBER e/EMAIL $/SALARY [s/STATUS] [r/ROLE]... [t/TAG]...` + +**Examples:** \ +`add n/Joe s/fulltime r/kitchen p/98765432 $/1234789 e/Joe@example.com` +`add n/Candice s/parttime p/91234567 $/2 e/candice@example.com` + +**See Also:** +[`delete` Command](#deleting-a-staff--delete) + +**Go to:** +[Table of Contents](#table-of-contents) +[Flag Legend](#flag-legend):triangular_flag_on_post: + + +
+ +### Editing a staff : `edit` + +**Edits** an existing staff in the Staff List. + +* Edits the staff of the specified `NAME` or `INDEX`. +* At least one of the optional parameters must be provided. +* Existing values will be replaced with the input values. For the role and tag parameters, if an empty parameter (i.e. `r/` or `t/`) is provided, the roles and tags will be cleared respectively. + +**Formats:** +`edit -n NAME [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [$/SALARY] [s/STATUS] [r/ROLE]... [t/TAG]...` +`edit -i INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [$/SALARY] [s/STATUS] [r/ROLE]... [t/TAG]...` + +**Examples:** +`edit -i 1 p/91234567 e/johndoe@example.com` +`edit -n Bob p/69696969 e/candicepleasedateme@tinder.com` +`edit -n Candice r/cook` + +**Go to:** +[Table of Contents](#table-of-contents) +[Flag Legend](#flag-legend):triangular_flag_on_post: + +
+ +### Deleting a Staff : `delete` + +**Deletes** all the specified staff from the Stafflist. + +* Requires exactly one of the following [fields](#flag-legend):triangular_flag_on_post:: `NAME`, `ROLE`, `STATUS`, `INDEX`. +* Deletes all the staff that have the provided field. + +**Formats:** +`delete -n NAME` \ +`delete -i INDEX` +`delete -r ROLE` +`delete -s STATUS` + +**Examples:** +`delete -n Candice` +`delete -i 2` +`delete -r floor` +`delete -s fulltime` + +**See Also:** [`add` Command](#adding-a-staff--add) + +**Go to:** +[Table of Contents](#table-of-contents) +[Flag Legend](#flag-legend):triangular_flag_on_post: + +
+ +### Finding staff : `find` + +**Searches for staff** by the input [fields](#flag-legend):triangular_flag_on_post:. + +Searches can be conducted by `Name`, `Index`, or other fields. The search also filters the [displayed Staff List](#glossary):mag: and schedule display to only display the staff that meet the requirements. + +**Index Search:** + +* Searches for staff by their **index in the staff list**. +* This search is performed on the displayed Staff List. +* Additional fields can be added to search for a more specific staff. +* This search will take priority over the the Name Search if it is present. +* Only single search is supported, and this search will return only the specific Staff at that index. +* If the staff at the index does not satisfy the other conditions, nothing will be done. +* Cannot be done in conjunction with **Name Search**. + +**Name Search:** + +* Searches for staff whose **names contain any of the given keywords**. +* This search is performed on the overall Staff List. +* Additional fields can be added to search for a more specific staff. +* The search is case-insensitive. (e.g `bob` will match `Bob`). +* The order of the keywords does not matter (e.g. `Candice Dee` will match `Dee Candice`). +* Only full words will be matched (e.g. `Bob` will not match `Boba`). +* Staff matching at least one keyword will be returned (e.g. `John Nathan` will return `John Wick`, `Nathan Tan`). + +**Field Search:** + +* Searches for staff based on the **provided fields**. At least one field must be provided. +* This search is performed on the overall Staff List. +* Can perform field search alone **or** combined with a name search. +* Staff who have fields that exactly match all the provided fields will be displayed. +* Note that the field search (apart from the Name Field) is case sensitive. + +**Format:** +`find -i INDEX [-p PHONE] [-e EMAIL] [-$ SALARY] [-s STATUS] [-r ROLE...] [-t TAG...]` +`find [-n NAME_KEYWORDS...] [-p PHONE] [-e EMAIL] [-$ SALARY] [-s STATUS] [-r ROLE...] [-t TAG...]` + +**Examples:** +`find -n John` +`find -n alex david` +`find -i 3` +`find -t Friend` +`find -t fren -n john` +`find -n John -t friend` or `find -t friend -n John` + +**Output:** + +![FindCommand](images/FindCommand.png) +_Only staff matching the fields inputted are displayed in the Staff Display_ + + +![FindCommandSchedule](images/FindCommandSchedule.png) +_Only staff matching the fields inputted are displayed in the Schedule Display_ + +**Go to:** +[Table of Contents](#table-of-contents) +[Flag Legend](#flag-legend):triangular_flag_on_post: + +
+ + + +### Marking a staff as absent : `mark` -## FAQ +Marks a specified staff as not working for a specified date. -**Q**: How do I transfer my data to another Computer?
-**A**: Install the app in the other computer and overwrite the empty data file it creates with the file that contains the data of your previous AddressBook home folder. +* The salary for that date will not be included in calculation, depending on the staff's status. By default, the staff is recorded as present for all shifts. +* The format of the input date is in: `YYYY-MM-DD`. +* Takes [0, 1 or 2 date inputs](#flag-legend):triangular_flag_on_post: representing the period to to mark the staff absent. +* Regardless of the presence of shifts, the staff will be marked absent for that period. Staff'd will not indicate any existing shifts in the period provided. +* If a period has previously been marked, it cannot be marked again. However, the absent period can be extended. For instance, if a staff has an absent period from `2021-11-01` to `2021-11-07`, and `mark [staff] da/2021-11-06` is called, the absent period will be extended to be `2021-11-01` to `2021-11-12`. +* If multiple staffs are called in a `mark` command, none of the staff should have an existing overlapping absent period previously marked. + +**Formats:** +Marking a period: + +`mark [-i INDEX] [-n NAME] [-p PHONE] [-e EMAIL] [-$ SALARY] [-s STATUS] [-r ROLE]... [da/DATE] [da/END DATE]` + + +Marking a single date: + +`mark -t TAG da/DATE` +`mark -n NAME da/DATE` + +**Examples:** +`mark -i 1 da/2020-01-03 da/2021-01-03` +`mark -n Alex Yeoh da/2020-01-03` + +**See Also:** [`unmark` Command](#removing-the-absent-mark--unmark) + +**Output:** + +![MarkCommandList](images/MarkCommandList.png) +_Staff’s absent periods updated in Staff View_ + +![MarkCommandSchedule](images/MarkCommandSchedule.png) +_Staff marked as absent in Schedule View_ + +**Go to:** +[Table of Contents](#table-of-contents) +[Flag Legend](#flag-legend):triangular_flag_on_post: + +
+ +### Removing the absent mark : `unmark` + +Removes the period that was marked by the [`mark`](#marking-a-staff-as-absent--mark) command. + +The format of the input date is in: `YYYY-MM-DD`. + +* Takes [0, 1 or 2 date inputs](#flag-legend):triangular_flag_on_post: representing the period to to remove the [`marks`](#marking-a-staff-as-absent--mark). +* If only one date input is provided, the next occurrence of that shift, after the provided date is unmarked. For instance, if the shift is on Monday 1/11/2021, with da/2021-10-27 as the date input, the shift on 1/10/2021 would be unmarked. +* If multiple staffs are called in an `unmark` command, if one of the staff does not have a mark in the period provided, none of the markings will be removed. + +**Format:** +`unmark [-i INDEX] [-n NAME] [-p PHONE] [-e EMAIL] [-$ SALARY] [-s STATUS] [-r ROLE]... [da/DATE] [da/END DATE]` + +**Examples:** +`unmark -i 1 da/2020-01-03 da/2021-01-03` +`unmark -t friends da/2020-01-03` + +**See Also:** [`mark` Command](#marking-a-staff-as-absent--mark) + +**Output:** + +![UnmarkCommandList](images/UnmarkCommandList.png) +_Staff’s absent periods updated in Staff View_ + +![UnmarkCommandSchedule](images/UnmarkCommandSchedule.png) +_Staff marked as present in Schedule View_ + +**Go to:** +[Table of Contents](#table-of-contents) +[Flag Legend](#flag-legend):triangular_flag_on_post: + +
+ + +## Basic Management of Staff Schedules + +### Adding a shift to a staff's schedule : `addShift` + +Adds a [shift](#flag-legend):triangular_flag_on_post: to a specified staff's schedule. + +* Takes [0, 1, or 2 date inputs](#flag-legend):triangular_flag_on_post:. Shifts would be added to all occurances in the period. +* The start time and end time will be set to the default [shift timings](#glossary):mag: + + +**Formats:** +`addShift -n NAME d/DAYOFWEEK-SHIFTNUMBER [da/START_DATE] [da/END_DATE]`\ +`addShift -i INDEX d/DAYOFWEEK-SHIFTNUMBER [da/START_DATE] [da/END_DATE]` + +**Examples:** +`addShift -n Candice d/Monday-1 da/2021-10-01`\ +`addShift -i 1234 d/tuesday-0` + +**Notes:** + +* The start date and the end date are included in the period. +* The `DAYOFWEEK` stands for the day in the week, such as `monday`, `sunday` etc. +* Field required to specify shifts are not case-sensitive, but must be full. + +**See Also:** [`deleteShift` Command](#deleting-a-shift-from-a-staff--deleteshift) + +**Go to:** +[Table of Contents](#table-of-contents) +[Flag Legend](#flag-legend):triangular_flag_on_post: + +
+ +### Swapping shifts : `swapShift` +Swaps a shift between two staff. + +* The two staff are only identified using their [names](#flag-legend):triangular_flag_on_post:. The name provided has to exactly match the name of the staff to swapped. +* Takes [0, 1 or 2 date inputs](#flag-legend):triangular_flag_on_post: representing the period to swap. +* The staff identified using the first name provided, is associated with the first shift. The staff identified using the second name provided, is associated with the second shift. +* This command resets any changes in shift timing done using the [setShiftTime](#updating-the-start-time-and-end-time-for-a-shift--setshifttime) command. + +**Formats:** +`swapShift -n NAME -n NAME d/DAYOFWEEK-SHIFT_NUMBER d/DAYOFWEEK-SHIFT_NUMBER [da/START_DATE] [da/END_DATE]`\ +`swapShift -n NAME d/day-shift_number -n NAME d/day-shift_number [da/START_DATE] [da/END_DATE]` + +**Examples:** +`swapShift -n Candice -n Bob d/monday-0 d/tuesday-1 da/2021-10-01`\ +`swapShift -n Candice d/monday-0 -n Bob d/tuesday-1` + +**See Also:** [`setShiftTime` Command](#updating-the-start-time-and-end-time-for-a-shift--setshifttime) + +**Go to:** +[Table of Contents](#table-of-contents) +[Flag Legend](#flag-legend):triangular_flag_on_post: + +
+ +### Deleting a shift from a staff : `deleteShift` + +Deletes a shift from the staff's schedule. + +* The staff can be indicated by their [name or index](#flag-legend):triangular_flag_on_post:. +* Takes [0, 1 or 2 date inputs](#flag-legend):triangular_flag_on_post:. Shifts would be deleted from all occurances in the period. + +**Formats:**\ +`deleteShift -n NAME d/DAYOFWEEK-shiftNumber [da/START_DATE] [da/END_DATE]`\ +`deleteShift -i INDEX d/DAYOFWEEK-shiftNumber [da/START_DATE] [da/END_DATE]` + +**Examples:**\ +`deleteShift -n Joe d/tuesday-1 da/2021-10-01`\ +`deleteShift -i 1278 d/friday-1` + +**See Also:** [`addShift` Command](#adding-a-shift-to-a-staffs-schedule--addshift) + +**Go to:** +[Table of Contents](#table-of-contents) +[Flag Legend](#flag-legend):triangular_flag_on_post: + +
+ +### Viewing all the staff working a shift : `viewShift` + +Finds all the staff working at a particular shift. + +* The shift can be specified by indicating the day of the week, and either the time or the slot number. +* Takes [0, 1 or 2 date inputs](#flag-legend):triangular_flag_on_post:. Only the first occurance of the shift in the period would be displayed. +* If no date or day input is provided, the current shift is displayed along with a help message of the format of `viewShift`. +* This command filters the [displayed Staff List](#glossary):mag: to only display the staff that are scheduled to work on the shift. +* This command also filters the schedule display to show only the staff that have been assigned to work on the shift. +* Additionally, it filters the schedule display to only show those staff. To return to the full schedule display, use the [`list` command](#listing-all-staffs--list). + +**Formats:** +`viewShift -d DAYOFWEEK-SHIFT_NUMBER [da/START_DATE] [da/END_DATE]`\ +`viewShift -ti DAYOFWEEK-TIME [da/START_DATE] [da/END_DATE]` + +Note that day refers to the day of the week, and it is case-insensitive. However, it should be spelt in full (e.g. MONDAY instead of Mon). + +**Examples:** +`viewShift -d monday-1 da/2021-10-01`\ +`viewShift -d TUESDAY-0`\ +`viewShift -ti wednesday-12:00`\ +`viewShift -ti THURSDAY-16:30` + +**Output:** + +![ViewShiftCommandList](images/ViewShiftCommandList.png) +_Staff working at the specified shift are displayed in Staff View_ + +![ViewShiftCommandSchedule](images/ViewShiftCommandSchedule.png) +_Staff working at the specified shift are displayed in Schedule View_ + +**Go to:** +[Table of Contents](#table-of-contents) +[Flag Legend](#flag-legend):triangular_flag_on_post: + +
+ +### Viewing schedule for the week: `change` + +The change command changes the default period for shift-related commands, when no date(s) are provided. + +* This command also changes the days displayed in the [Schedule Display](#glossary):mag: in the schedule tab. +* The default period is the next 7 days inclusive of today. +* Takes only [1 date input](#flag-legend):triangular_flag_on_post:, and displays the next 7 days, inclusive of the date provided. + +**Format:** +`change da/DATE` + +**Examples:** +`change da/2021-12-28` + +**Output:** + +![ChangeCommand](images/ChangeCommand.png) +_Schedule View shows the new specified period._ + +![ChangeCommandAddShift](images/ChangeCommandAddShift.png) +_Shift-related commands such as `addShift` will be now be added to the new period._ + +**Go to:** +[Table of Contents](#table-of-contents) +[Flag Legend](#flag-legend):triangular_flag_on_post: + +
+ +### Updating the start time and end time for a shift : `setShiftTime` + +Updates the start time and end time of a specific shift of a specific staff. + +* Time inputs should follow the requirements specified in the Flag Legend: [Shift Time](#flag-legend):triangular_flag_on_post:. +* Takes [0, 1 or 2 date inputs](#flag-legend):triangular_flag_on_post:. The specified shift times would be set for all occurances of the shift in the period would be displayed. +* If the shift does not exist in the staff's schedule, it will be added. + +**Formats:** +`setShiftTime -n NAME d/DAYOFWEEK-SHIFTNUMBER st/START_TIME-END_TIME [da/START_DATE] [da/END_DATE]`\ +`setShiftTime -i INDEX d/DAYOFWEEK-SHIFTNUMBER st/START_TIME-END_TIME [da/START_DATE] [da/END_DATE]` + +**Examples:** +`setShiftTime -i 12 d/tuesday-1 st/17:00-21:30` +`setShiftTime -n Candice d/Monday-0 st/10:30-12:30 da/2021-10-01`\ +`setShiftTime -i 1 d/wednesday-1 st/17:00-21:30 da/2021-10-01 da/2021-11-01` + +**Go to:** +[Table of Contents](#table-of-contents) +[Flag Legend](#flag-legend):triangular_flag_on_post: + +
+ +### Viewing individual staff statistics : `istaff` + +Displays the staff statistics for an individual staff or a group of staff for the current month. + +**Format:** +`istaff [-i INDEX] [-n NAME] [-p PHONE] [-e EMAIL] [-a ADDRESS] [-$ SALARY] [-s STATUS] [-r ROLE]...` + +**Examples:** +`istaff -i 1` +`istaff -p 999` +`istaff -n Joe` + +**Output:** + +![IStaffCommand](images/IstaffCommand.png) +_Statistics of the specified staff for the current month of November._ + +**Go to:** +[Table of Contents](#table-of-contents) +[Flag Legend](#flag-legend):triangular_flag_on_post: + +
+ +### Viewing overall staff statistics : `stats` + +Displays the staff statistics for all the staff: the total working hours and total salary for this month. + +**Format:** +`stats` + +**Output:** + +![StatsCommand](images/StatsCommand.png) +_Statistics all the staff for the current month of November._ + +**Go to:** +[Table of Contents](#table-of-contents) +[Flag Legend](#flag-legend):triangular_flag_on_post: + +
-------------------------------------------------------------------------------------------------------------------- -## Command summary +# Command summary Action | Format, Examples --------|------------------ -**Add** | `add n/NAME p/PHONE_NUMBER e/EMAIL a/ADDRESS [t/TAG]…​`
e.g., `add n/James Ho p/22224444 e/jamesho@example.com a/123, Clementi Rd, 1234665 t/friend t/colleague` -**Clear** | `clear` -**Delete** | `delete INDEX`
e.g., `delete 3` -**Edit** | `edit INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [a/ADDRESS] [t/TAG]…​`
e.g.,`edit 2 n/James Lee e/jameslee@example.com` -**Find** | `find KEYWORD [MORE_KEYWORDS]`
e.g., `find James Jake` -**List** | `list` -**Help** | `help` +[**Help**](#viewing-help--help) | `help` +[**Tab**](#changing-tabs--tab) | `tab` +[**List**](#listing-all-staffs--list) | `list` +[**Clear**](#clearing-all-entries--clear) | `clear` +[**Set role requirements**](#setting-role-requirements--setrolereq) | `setRoleReq r/ROLE-NUMBER_REQUIRED...` +[**Exit**](#exiting-the-program--exit) | `exit` +[**Add**](#adding-a-staff--add) | `add n/NAME p/PHONE_NUMBER e/EMAIL $/SALARY [s/STATUS] [r/ROLE]... [t/TAG]...` +[**Edit**](#editing-a-staff--edit) | `edit -n NAME [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [$/SALARY] [s/STATUS] [r/ROLE]... [t/TAG]...`
`edit -i INDEX [n/NAME] [p/PHONE_NUMBER] [e/EMAIL] [$/SALARY] [s/STATUS] [r/ROLE]... [t/TAG]...` +[**Delete**](#deleting-a-staff--delete) | `delete -n NAME`
`delete -i INDEX`
`delete -r role`
`delete -s STATUS` +[**Find**](#finding-staff--find) | `find -i INDEX [-p PHONE] [-e EMAIL] [-$ SALARY] [-s STATUS] [-r ROLE...] [-t TAG...]`
`find [-n NAME_KEYWORDS...] [-p PHONE] [-e EMAIL] [-$ SALARY] [-s STATUS] [-r ROLE...] [-t TAG...]` +[**Mark absent**](#marking-a-staff-as-absent--mark) | `mark [-i INDEX] [-n NAME] [-p PHONE] [-e EMAIL] [-$ SALARY] [-s STATUS] [-r ROLE]... [da/START_DATE] [da/END_DATE]` +[**Remove mark**](#removing-the-absent-mark--unmark) | `unmark [-i INDEX] [-n NAME] [-p PHONE] [-e EMAIL] [-$ SALARY] [-s STATUS] [-r ROLE]... [da/START_DATE] [da/END_DATE]` +[**Add shift to staff**](#adding-a-shift-to-a-staffs-schedule--addshift) | `addShift -n NAME d/DAYOFWEEK-SHIFTNUMBER [da/START_DATE] [da/END_DATE]`
`addShift -i INDEX d/DAYOFWEEK-SHIFTNUMBER [da/START_DATE] [da/END_DATE]` +[**Swap shifts**](#swapping-shifts--swapshift) | `swapShift -n NAME -n NAME d/day-shift_number d/day-shift_number [da/START_DATE] [da/END_DATE]`
`swapShift -n NAME d/day-shift_number -n NAME d/day-shift_number [da/START_DATE] [da/END_DATE]` +[**Delete staff shift**](#deleting-a-shift-from-a-staff--deleteshift) | `deleteShift -n NAME d/DAY-SHIFTNUMBER [da/START_DATE] [da/END_DATE]`
`deleteShift -i INDEX d/DAY-SHIFTNUMBER [da/START_DATE] [da/END_DATE]` +[**View shift**](#viewing-all-the-staff-working-a-shift--viewshift) | `viewShift -d DAY-SHIFTNUMBER [da/START_DATE] [da/END_DATE]`
`viewShift -ti DAY-HH:mm [da/START_DATE] [da/END_DATE]` +[**Change schedule**](#viewing-schedule-for-the-week-change) | `change da/START_DATE` +[**Set shift time**](#updating-the-start-time-and-end-time-for-a-shift--setshifttime) | `setShiftTime -n NAME d/DAYOFWEEK-SHIFTNUMBER st/hh:mm-hh:mm [da/START_DATE] [da/END_DATE]`
`setShiftTime -i INDEX d/DAYOFWEEK-SHIFTNUMBER st/hh:mm-hh:mm [da/START_DATE] [da/END_DATE]` +[**Individual staff statistics**](#viewing-individual-staff-statistics--istaff) | `istaff [-i INDEX] [-n NAME] [-p PHONE] [-e EMAIL] [-$ SALARY] [-s STATUS] [-r ROLE]...` +[**Total staff statistics**](#viewing-overall-staff-statistics--stats) | `stats` + +
+ +-------------------------------------------------------------------------------------------------------------------- +# Flag Legend +These flags are used for identifying fields and parameters in a command. The tables below are provided for reference + +Flags for Specific Fields|Flags for Lookup|Name|Description/Conditions +-----|-----|-----|---------- +i/|-i|Index|{::nomarkdown}
  • Represents the corresponding staff index in the displayed staff list.{:/} +n/|-n|Name|{::nomarkdown}
    • Represents the name of the staff.
    • Compulsory field.
    • Names are case sensitive, so _john_ and _John_ are regarded as two different people.
    • Duplicate names are not accepted.{:/} +p/|-p|Phone Number|{::nomarkdown}
      • Represents the phone number of the staff.
      • Compulsory field.
      • Phone number must be longer than 3 digits, containing only numeric characters.{:/} +e/|-e|Email|{::nomarkdown}
        • Represents the email of a staff.
        • Compulsory field.
        • Must be of format local-part@domain.
        • The local-part must only contain alphanumeric characters, excluding and the special characters "+ _ . -", but cannot start or end with any special character.
        • The domain is made up of domain labels separated by periods. It must end with a domain label which is at least 2 characters long.
        • The domain labels must consist of alphanumeric characters, separated only by hyphens, if any, and they must start and end with alphanumeric characters.{:/} +r/|-r|Role|{::nomarkdown}
          • Represents the responsibilities that a staff member has in the store.
          • The accepted roles are: floor, kitchen, or bartender.
          • The absence of a role would be denoted as "norole".{:/} +t/|-t|Tags|{::nomarkdown}
            • Tags are used to store additional information about staff, and can be used to reference staff.
            • Tags are sorted in alphabetical order.{:/} +s/|-s|Status|{::nomarkdown}
              • Represents the employment status of a staff.
              • The accepted statuses are: fulltime, partime or nostatus.{:/} +$/|-$|Salary (per hour)|{::nomarkdown}
                • Represents the salary of a staff.
                • Compulsory field.
                • Must be a non-negative number representing the staff's pay in dollars.
                • Cents can be added, by adding a period, followed by the cents, which then also has to be a positive number of either 1 or 2 digits.
                • The maximum salary that can be set is $9999.99.{:/} +d/|-d|Shift (for shift related commands)|{::nomarkdown}
                  • Represents a shift of a staff.
                  • Represented as a DAYOFWEEK-SLOT_NUMBER.
                  • DAYOFWEEK refers to the day of the week, such as _monday_ or _saturday_. The DAYOFWEEK is case insensitive.
                  • SLOT_NUMBER refers to either the first or second shift. The first shift (morning) is represented with 0, and the second shift (afternoon) is represented with 1.
                  • Example: TUESDAY-0 and wednesday-1 are valid shifts.{:/} +rr/|NA|Role Requirements|{::nomarkdown}
                    • Represents the requirements of each role.
                    • When used as an input, it is represented as ROLE-REQUIRED_NUMBER.
                    • The required number refers to the number of staff of that role required per shift.{:/} +da/|NA|Date|{::nomarkdown}
                      • Dates must be of format YYYY-MM-DD when provided.
                      • When no dates are provided: the resultant period for the command is the next 7 days, inclusive of the current one.
                      • When one date is provided: the resultant period for the command is the next following the one provided, inclusive.
                      • When two dates are provided: the resultant period for the command is between the first and the second date provided, inclusive. The first date provided has to be before or equal to the second date.
                      • To specify a single date, provide two dates representing same date.
                      • {:/} +st/|NA| Shift Time |{::nomarkdown}
                        • Shift time must have start and end time.
                        • Time is in the format of HH:mm. (e.g. 10:30 represents 10.30 am)
                        • Start and end time must have a dash to delimit the two. (e.g. 10:30-11:30 represents 10.30 am to 11.30 a.m)
                        • {:/} +NA |-ti|Shift (by Time)|{::nomarkdown}
                          • Represents in the format DAYOFWEEK-TIME
                          • The time must be of format HH:mm, and follows the 24hr format.{:/} + +:exclamation: Note: "NA" means that the tag does not exist. + +
                            + +-------------------------------------------------------------------------------------------------------------------- + +# FAQ + +**Q**: What do I do if double-clicking isn’t opening the app? +**A**: Open your [Command Prompt](#glossary):mag: (Windows) or your [Terminal](#glossary):mag: (MacOS, Linux) and navigate to the folder containing the **staff'd.jar** file. Run the file by using the command `java -jar staffd.jar`. On an Linux Operating System, it might be necessary to run `chmod +x staffd.jar` on Terminal so that double-clicking opens the application. + +**Q**: Why is the staff information being truncated in the staff list? +**A**: The staff information exceeded the character limit of that field! To access the full information, the [`find` command](#finding-staff--find) can be used. + +**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 save file that contains the data of your previous Staff'd. + +**Q**: Where is the save file located? +**A**: In the [**data folder**](#glossary):mag: located in the [**home folder**](#glossary):mag: called **staffd.json**. The save file for the [**role requirements**](#glossary):mag: is also located in the data folder, and is called **RoleReq.txt**. + +[Return to Table of Contents](#table-of-contents) + +
                            + + +-------------------------------------------------------------------------------------------------------------------- + +# Glossary + +Name | Explanation +--------|------------------ +**Command** | In Staff'd, it is an instruction that signals Staff'd to perform a function. It consists of a **Command Word** and **Parameters**. Refer [here](#staffd-commands) for more examples on Staff'd commands. +**Command Line Interface (CLI)** | A **text-based user interface** where users interact with the software by typing in text commands. +**Command Prompt** | A **text-based user interface** for **Windows**. It can be used to execute text commands. To open the Command Prompt, navigate to the Start Menu's Search Bar, and search `cmd`. The first option is the Command Prompt, and should be clicked to open it. +**Command Word** | A **word** used in a Staff'd Command to **identify the type of operation** being conducted. It is commonly used with Parameters. +**Data Folder** | The **folder** containing the **save file** of Staff'd (which is the **staffd.json** file). +**Field**| The **information categories** of a Staff saved in the Staff List. +**Home Folder** | The **folder** containing the **application** (which is the **staffd.jar** file). +**Index** | The **number corresponding to a staff** in the **displayed staff list**. +**Graphical User Interface (GUI)** | A **visual-based user interface** where users interact with the software through graphical icons and pictures. +**Parameters** | **Additional information** to supplement a Command Word in a **Command**. For example, `n/NAME` is a name parameter. +**Shift Timings** | The start and end times of shifts. The default shift timings are from 10:00 to 16:00 for morning shift and 16:00 to 22:00 for afternoon shift. +**Staff Role Requirements** | The **minimum number of staff required** for **each role** for a shift. +**Staff List** | The **list of staff members**. Staff'd has an **overall list** with all the members, and a **displayed staff list** which can be filtered by other commands. +**Terminal** | A **text-based user interface** for **Linux Operating Systems**. It can be used to execute text commands. +**User Interface** | The use of **input devices and software** by which a **user and computer interacts**. + + +
                            + +[Return to Table of Contents](#table-of-contents) diff --git a/docs/_config.yml b/docs/_config.yml index 6bd245d8f4e..8eba6069aa0 100644 --- a/docs/_config.yml +++ b/docs/_config.yml @@ -1,4 +1,4 @@ -title: "AB-3" +title: "Staff'd" theme: minima header_pages: @@ -8,7 +8,7 @@ header_pages: markdown: kramdown -repository: "se-edu/addressbook-level3" +repository: "AY2122S1-CS2103T-W11-2/tp" github_icon: "images/github-icon.png" plugins: diff --git a/docs/_sass/minima/_base.scss b/docs/_sass/minima/_base.scss index 0d3f6e80ced..72e12368aaf 100644 --- a/docs/_sass/minima/_base.scss +++ b/docs/_sass/minima/_base.scss @@ -288,7 +288,7 @@ table { text-align: center; } .site-header:before { - content: "AB-3"; + content: "Staff'd"; font-size: 32px; } } diff --git a/docs/diagrams/AddShiftActivityDiagram.puml b/docs/diagrams/AddShiftActivityDiagram.puml new file mode 100644 index 00000000000..bbe68b7111a --- /dev/null +++ b/docs/diagrams/AddShiftActivityDiagram.puml @@ -0,0 +1,23 @@ +@startuml +start +:User executes command; +:Parse the command; + +'Since the beta syntax does not support placing the condition outside the +'diamond we place it as the true branch instead. + +if () then ([The staff is in the address book]) + if () then ([The date and slot input are valid]) + :Find the target slot(s); + if () then ([At least one of the target slot[s] is empty in the target staff's schedule]) + :Add the shift to the target staff's schedule; + :Save to staffd.json; + else ([else]) + endif + else ([else]) + endif +else ([else]) +endif +:Display respond message in the output box; +stop +@enduml diff --git a/docs/diagrams/AddShiftClassDiagram.puml b/docs/diagrams/AddShiftClassDiagram.puml new file mode 100644 index 00000000000..73e3038fa9c --- /dev/null +++ b/docs/diagrams/AddShiftClassDiagram.puml @@ -0,0 +1,21 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor LOGIC_COLOR_T4 +skinparam classBackgroundColor LOGIC_COLOR + + +Interface Parser <> +abstract class Command + + + +Class AddShiftCommandParser { + String data +} + +Parser <|.. AddShiftCommandParser +Command <|-- AddShiftCommand + +@end + diff --git a/docs/diagrams/AddShiftSequenceDiagram.puml b/docs/diagrams/AddShiftSequenceDiagram.puml new file mode 100644 index 00000000000..a4a84f42c01 --- /dev/null +++ b/docs/diagrams/AddShiftSequenceDiagram.puml @@ -0,0 +1,73 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":AddShiftCommandParser" as AddShiftCommandParser LOGIC_COLOR +participant "command:AddShiftCommand" as AddShiftCommand LOGIC_COLOR +participant "result:CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute("addShift -n Steve d/Monday-0") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand() +activate AddressBookParser + +create AddShiftCommandParser +AddressBookParser -> AddShiftCommandParser +activate AddShiftCommandParser + +AddShiftCommandParser --> AddressBookParser +deactivate AddShiftCommandParser + +AddressBookParser -> AddShiftCommandParser : parse() +activate AddShiftCommandParser + +create AddShiftCommand +AddShiftCommandParser -> AddShiftCommand +activate AddShiftCommand + +AddShiftCommand --> AddShiftCommandParser +deactivate AddShiftCommand + +AddShiftCommandParser --> AddressBookParser +deactivate AddShiftCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +AddShiftCommandParser -[hidden]-> AddressBookParser +destroy AddShiftCommandParser + +AddressBookParser --> LogicManager : command +deactivate AddressBookParser + +LogicManager -> AddShiftCommand : execute() +activate AddShiftCommand + +AddShiftCommand -> Model : findPersonByName() +activate Model + +Model --> AddShiftCommand : staff + +AddShiftCommand -> Model : addShift() + +Model --> AddShiftCommand +deactivate Model + +create CommandResult +AddShiftCommand -> CommandResult +activate CommandResult + +CommandResult --> AddShiftCommand +deactivate CommandResult + +AddShiftCommand --> LogicManager +deactivate AddShiftCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/ArchitectureSequenceDiagram.puml b/docs/diagrams/ArchitectureSequenceDiagram.puml index ef81d18c337..0757a11fb1e 100644 --- a/docs/diagrams/ArchitectureSequenceDiagram.puml +++ b/docs/diagrams/ArchitectureSequenceDiagram.puml @@ -7,10 +7,10 @@ Participant ":Logic" as logic LOGIC_COLOR Participant ":Model" as model MODEL_COLOR Participant ":Storage" as storage STORAGE_COLOR -user -[USER_COLOR]> ui : "delete 1" +user -[USER_COLOR]> ui : "delete -i 1" activate ui UI_COLOR -ui -[UI_COLOR]> logic : execute("delete 1") +ui -[UI_COLOR]> logic : execute("delete -i 1") activate logic LOGIC_COLOR logic -[LOGIC_COLOR]> model : deletePerson(p) diff --git a/docs/diagrams/BetterModelClassDiagram.puml b/docs/diagrams/BetterModelClassDiagram.puml index 5731f9cbaa1..1a946433ffc 100644 --- a/docs/diagrams/BetterModelClassDiagram.puml +++ b/docs/diagrams/BetterModelClassDiagram.puml @@ -6,16 +6,19 @@ skinparam classBackgroundColor MODEL_COLOR AddressBook *-right-> "1" UniquePersonList AddressBook *-right-> "1" UniqueTagList +AddressBook *-right-> "1" UniqueRoleList UniqueTagList -[hidden]down- UniquePersonList UniqueTagList -[hidden]down- UniquePersonList UniqueTagList *-right-> "*" Tag +UniqueRoleList *-right-> "*" Role UniquePersonList -right-> Person Person -up-> "*" Tag +Person -up-> "*" Role Person *--> Name Person *--> Phone Person *--> Email -Person *--> Address + @enduml diff --git a/docs/diagrams/DeleteSequenceDiagram.puml b/docs/diagrams/DeleteSequenceDiagram.puml index 1dc2311b245..9d9c959d43d 100644 --- a/docs/diagrams/DeleteSequenceDiagram.puml +++ b/docs/diagrams/DeleteSequenceDiagram.puml @@ -13,10 +13,10 @@ box Model MODEL_COLOR_T1 participant ":Model" as Model MODEL_COLOR end box -[-> LogicManager : execute("delete 1") +[-> LogicManager : execute("delete -i 1") activate LogicManager -LogicManager -> AddressBookParser : parseCommand("delete 1") +LogicManager -> AddressBookParser : parseCommand() activate AddressBookParser create DeleteCommandParser @@ -26,29 +26,29 @@ activate DeleteCommandParser DeleteCommandParser --> AddressBookParser deactivate DeleteCommandParser -AddressBookParser -> DeleteCommandParser : parse("1") +AddressBookParser -> DeleteCommandParser : parse() activate DeleteCommandParser create DeleteCommand DeleteCommandParser -> DeleteCommand activate DeleteCommand -DeleteCommand --> DeleteCommandParser : d +DeleteCommand --> DeleteCommandParser deactivate DeleteCommand -DeleteCommandParser --> AddressBookParser : d +DeleteCommandParser --> AddressBookParser deactivate DeleteCommandParser 'Hidden arrow to position the destroy marker below the end of the activation bar. DeleteCommandParser -[hidden]-> AddressBookParser destroy DeleteCommandParser -AddressBookParser --> LogicManager : d +AddressBookParser --> LogicManager deactivate AddressBookParser LogicManager -> DeleteCommand : execute() activate DeleteCommand -DeleteCommand -> Model : deletePerson(1) +DeleteCommand -> Model : deletePerson() activate Model Model --> DeleteCommand @@ -61,7 +61,7 @@ activate CommandResult CommandResult --> DeleteCommand deactivate CommandResult -DeleteCommand --> LogicManager : result +DeleteCommand --> LogicManager deactivate DeleteCommand [<--LogicManager diff --git a/docs/diagrams/FindActivityDiagram.puml b/docs/diagrams/FindActivityDiagram.puml new file mode 100644 index 00000000000..cf2059eecd7 --- /dev/null +++ b/docs/diagrams/FindActivityDiagram.puml @@ -0,0 +1,18 @@ +@startuml +'https://plantuml.com/activity-diagram-beta + +start +:User executes command; + +if () then ([Command arguments\n are correct]) + : Filter the staff list based on the given conditions; + if () then ([At least one staff\n satisfies the\n conditions]) + : Display the filtered staff\n list to the user; + else ([else]) + : Inform the user that no staff\n matches the conditions; + endif +else ([else]) +endif +stop + +@enduml diff --git a/docs/diagrams/FindClassDiagram.puml b/docs/diagrams/FindClassDiagram.puml new file mode 100644 index 00000000000..d1fe1e71ab4 --- /dev/null +++ b/docs/diagrams/FindClassDiagram.puml @@ -0,0 +1,23 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor LOGIC_COLOR_T4 +skinparam classBackgroundColor LOGIC_COLOR + + +Interface Parser <> +abstract class Command +Parser <|.. FindCommandParser +Command <|-- FindCommand + + +Class FindCommandParser { + NameContainsKeywordsPredicate namePredicate + int index + StaffHasCorrectIndexPredicate indexPredicate + execute() + executeNameSearch() + executeIndexSearch() +} + +@end diff --git a/docs/diagrams/FindSequenceDiagram.puml b/docs/diagrams/FindSequenceDiagram.puml new file mode 100644 index 00000000000..f25c27c2de3 --- /dev/null +++ b/docs/diagrams/FindSequenceDiagram.puml @@ -0,0 +1,66 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":FindCommandParser" as FindCommandParser LOGIC_COLOR +participant "command:FindCommand" as FindCommand LOGIC_COLOR + +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute() +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand() +activate AddressBookParser + +create FindCommandParser +AddressBookParser -> FindCommandParser +activate FindCommandParser + +FindCommandParser --> AddressBookParser +deactivate FindCommandParser + +AddressBookParser -> FindCommandParser : parse() +activate FindCommandParser + +create FindCommand +FindCommandParser -> FindCommand +activate FindCommand + + + +FindCommand --> FindCommandParser +deactivate FindCommand + +FindCommandParser --> AddressBookParser +deactivate FindCommandParser + + +destroy FindCommandParser + +AddressBookParser --> LogicManager +deactivate AddressBookParser + +LogicManager -> FindCommand : execute() +activate FindCommand + +FindCommand -> Model : updateFilteredPersonList() +activate Model +Model --> FindCommand +deactivate Model + + + + +FindCommand --> LogicManager +deactivate FindCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/FindSequenceDiagramReference.puml b/docs/diagrams/FindSequenceDiagramReference.puml new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/diagrams/MarkActivityDiagram.puml b/docs/diagrams/MarkActivityDiagram.puml new file mode 100644 index 00000000000..bb72b46150a --- /dev/null +++ b/docs/diagrams/MarkActivityDiagram.puml @@ -0,0 +1,15 @@ +@startuml +start +:User executes command; + +'Since the beta syntax does not support placing the condition outside the +'diamond we place it as the true branch instead. + +if () then ([All staff(s) queried for \n not marked for period]) + :mark staff(s) for period; + :Save to staffd.json; +else ([else]) + :Inform user of staff already marked for period; +endif +stop +@enduml diff --git a/docs/diagrams/ModelClassDiagram.puml b/docs/diagrams/ModelClassDiagram.puml index 1122257bd9a..d9125a2dab1 100644 --- a/docs/diagrams/ModelClassDiagram.puml +++ b/docs/diagrams/ModelClassDiagram.puml @@ -18,11 +18,7 @@ Class ReadOnlyUserPrefs Class UniquePersonList Class Person -Class Address -Class Email -Class Name -Class Phone -Class Tag + } @@ -40,15 +36,7 @@ UserPrefs .up.|> ReadOnlyUserPrefs AddressBook *--> "1" UniquePersonList UniquePersonList --> "~* all" Person -Person *--> Name -Person *--> Phone -Person *--> Email -Person *--> Address -Person *--> "*" Tag - -Name -[hidden]right-> Phone -Phone -[hidden]right-> Address -Address -[hidden]right-> Email + ModelManager -->"~* filtered" Person @enduml diff --git a/docs/diagrams/PersonClassDiagram.puml b/docs/diagrams/PersonClassDiagram.puml new file mode 100644 index 00000000000..3dd4ae33499 --- /dev/null +++ b/docs/diagrams/PersonClassDiagram.puml @@ -0,0 +1,50 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor MODEL_COLOR +skinparam classBackgroundColor MODEL_COLOR + + +Package Person <>{ + +Interface Field <> + +Class Person +Class Role +Class Salary +Class Schedule +Class Status +Class Email +Class Name +Class Phone +Class Period +Class Tag + +} + +Class HiddenOutside #FFFFFF + +HiddenOutside ..> Person + +Person *--> "1" Name +Person *--> "1" Phone +Person *--> "1" Email +Person *--> "1" Schedule +Person *--> "1" Status +Person *--> "1" Salary +Person *--> "*" Period +Person *--> "*" Tag +Person *--> "*" Role +Person *--> "5..*" Field + +Name .up.|> Field +Role .up.|> Field +Salary .up.|> Field +Status .up.|> Field +Email .up.|> Field +Phone .up.|> Field +Tag .up.|>Field + + + +@enduml diff --git a/docs/diagrams/StorageClassDiagram.puml b/docs/diagrams/StorageClassDiagram.puml index 85ac3ea2dee..a5a67e6684a 100644 --- a/docs/diagrams/StorageClassDiagram.puml +++ b/docs/diagrams/StorageClassDiagram.puml @@ -19,7 +19,6 @@ Interface AddressBookStorage <> Class JsonAddressBookStorage Class JsonSerializableAddressBook Class JsonAdaptedPerson -Class JsonAdaptedTag } } @@ -38,6 +37,5 @@ JsonUserPrefsStorage .up.|> UserPrefsStorage JsonAddressBookStorage .up.|> AddressBookStorage JsonAddressBookStorage ..> JsonSerializableAddressBook JsonSerializableAddressBook --> "*" JsonAdaptedPerson -JsonAdaptedPerson --> "*" JsonAdaptedTag @enduml diff --git a/docs/diagrams/StoragePersonDiagram.puml b/docs/diagrams/StoragePersonDiagram.puml new file mode 100644 index 00000000000..c1008ed52d5 --- /dev/null +++ b/docs/diagrams/StoragePersonDiagram.puml @@ -0,0 +1,35 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor STORAGE_COLOR +skinparam classBackgroundColor STORAGE_COLOR + +Class HiddenOutside #FFFFFF +package "Person Storage" { +class JsonAdaptedPerson +package "Schedule Storage" #F4F6F6{ +class JsonAdaptedSchedule +class JsonAdaptedShift +class JsonAdaptedRecurrencePeriod +} + + +class JsonAdaptedPeriod +class JsonAdaptedTag +class JsonAdaptedRole + +} + +JsonAdaptedPerson --> "*" JsonAdaptedPeriod +JsonAdaptedPerson --> "*" JsonAdaptedTag +JsonAdaptedPerson --> "0..3" JsonAdaptedRole +HiddenOutside ..> JsonAdaptedPerson +JsonAdaptedSchedule --> "0..14" JsonAdaptedShift +JsonAdaptedShift --> "*" JsonAdaptedRecurrencePeriod +JsonAdaptedPerson -left-> "1" JsonAdaptedSchedule + + + + + +@enduml diff --git a/docs/diagrams/UiClassDiagram.puml b/docs/diagrams/UiClassDiagram.puml index ecae4876432..1188eccb565 100644 --- a/docs/diagrams/UiClassDiagram.puml +++ b/docs/diagrams/UiClassDiagram.puml @@ -15,6 +15,7 @@ Class PersonListPanel Class PersonCard Class StatusBarFooter Class CommandBox +class WeekShiftsPane } package Model <> { @@ -34,6 +35,7 @@ MainWindow *-down-> "1" CommandBox MainWindow *-down-> "1" ResultDisplay MainWindow *-down-> "1" PersonListPanel MainWindow *-down-> "1" StatusBarFooter +MainWindow *-up-> "1" WeekShiftsPane MainWindow --> "0..1" HelpWindow PersonListPanel -down-> "*" PersonCard @@ -46,6 +48,7 @@ PersonListPanel --|> UiPart PersonCard --|> UiPart StatusBarFooter --|> UiPart HelpWindow --|> UiPart +WeekShiftsPane --|> UiPart PersonCard ..> Model UiManager -right-> Logic diff --git a/docs/diagrams/ViewShiftActivityDiagram.puml b/docs/diagrams/ViewShiftActivityDiagram.puml new file mode 100644 index 00000000000..f8121327107 --- /dev/null +++ b/docs/diagrams/ViewShiftActivityDiagram.puml @@ -0,0 +1,17 @@ +@startuml +start +:User executes command; +:Parse the command; + +'Since the beta syntax does not support placing the condition outside the +'diamond we place it as the true branch instead. + + +if () then ([The time or date input is valid]) + :Find staffs who are working at that time; + :Display found staff's information in the staff fields; +else ([else]) +endif +:Display respond message in the output box; +stop +@enduml diff --git a/docs/diagrams/ViewShiftClassDiagram.puml b/docs/diagrams/ViewShiftClassDiagram.puml new file mode 100644 index 00000000000..b01fa25b883 --- /dev/null +++ b/docs/diagrams/ViewShiftClassDiagram.puml @@ -0,0 +1,21 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor LOGIC_COLOR_T4 +skinparam classBackgroundColor LOGIC_COLOR + +Interface Parser <> +abstract class Command +Parser <|.. ViewShiftCommandParser +Command <|-- ViewShiftCommand + +class ViewShiftCommand { + dayOfWeek : DayOfWeek + slotNum : Integer + time : LocalTime + execute() + executeViewShiftBySlot() + executeViewShiftByTime() +} + +@enduml diff --git a/docs/diagrams/ViewShiftCommandExecutionSequenceDiagram.puml b/docs/diagrams/ViewShiftCommandExecutionSequenceDiagram.puml new file mode 100644 index 00000000000..cac4b4458f8 --- /dev/null +++ b/docs/diagrams/ViewShiftCommandExecutionSequenceDiagram.puml @@ -0,0 +1,48 @@ +@startuml +!include style.puml + +mainframe **sd View Shift Command Execution** + +box Logic LOGIC_COLOR_T1 + +participant "command:ViewShiftCommand" as ViewShiftCommand LOGIC_COLOR + +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + + +activate ViewShiftCommand + + +ViewShiftCommand -> Model : updateFilteredPersonList() +activate Model +Model --> ViewShiftCommand +deactivate Model + +alt viewShift by Slot Number + ViewShiftCommand -> ViewShiftCommand : executeViewShiftBySlot() + activate ViewShiftCommand + ViewShiftCommand -> Model : updateFilteredPersonList() + activate Model + Model --> ViewShiftCommand + deactivate Model + deactivate ViewShiftCommand + +else viewShift by Time + ViewShiftCommand -> ViewShiftCommand : executeViewShiftByTime() + activate ViewShiftCommand + ViewShiftCommand -> Model : updateFilteredPersonList() + activate Model + Model --> ViewShiftCommand + deactivate Model + + deactivate ViewShiftCommand +end + + +deactivate Model +@enduml + diff --git a/docs/diagrams/ViewShiftSequenceDiagram.puml b/docs/diagrams/ViewShiftSequenceDiagram.puml new file mode 100644 index 00000000000..e119cb194ac --- /dev/null +++ b/docs/diagrams/ViewShiftSequenceDiagram.puml @@ -0,0 +1,60 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":ViewShiftCommandParser" as ViewShiftCommandParser LOGIC_COLOR +participant "command:ViewShiftCommand" as ViewShiftCommand LOGIC_COLOR +end box + + +[-> LogicManager : execute() +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand() +activate AddressBookParser + +create ViewShiftCommandParser +AddressBookParser -> ViewShiftCommandParser +activate ViewShiftCommandParser + +ViewShiftCommandParser --> AddressBookParser +deactivate ViewShiftCommandParser + +AddressBookParser -> ViewShiftCommandParser : parse() +activate ViewShiftCommandParser + +create ViewShiftCommand +ViewShiftCommandParser -> ViewShiftCommand +activate ViewShiftCommand + + + +ViewShiftCommand --> ViewShiftCommandParser +deactivate ViewShiftCommand + +ViewShiftCommandParser --> AddressBookParser +deactivate ViewShiftCommandParser +'Hidden arrow to position the destroy marker below the end of the activation bar. +ViewShiftCommandParser -[hidden]-> AddressBookParser +destroy ViewShiftCommandParser + +AddressBookParser --> LogicManager +deactivate AddressBookParser + +LogicManager -> ViewShiftCommand : execute() + +activate ViewShiftCommand +ref over ViewShiftCommand : View Shift Command Execution + +note right : Refer to the View Shift Command Execution \nfor details on this function + +ViewShiftCommand --> LogicManager + + +deactivate ViewShiftCommand + +[<--LogicManager +deactivate LogicManager +@enduml diff --git a/docs/diagrams/WeekShiftsPaneClassDiagram.puml b/docs/diagrams/WeekShiftsPaneClassDiagram.puml new file mode 100644 index 00000000000..08615ca3fc9 --- /dev/null +++ b/docs/diagrams/WeekShiftsPaneClassDiagram.puml @@ -0,0 +1,27 @@ +@startuml +!include style.puml +skinparam arrowThickness 1.1 +skinparam arrowColor UI_COLOR_T4 +skinparam classBackgroundColor UI_COLOR + +Class "{abstract}\nUiPart" as UiPart + +package "Schedule view" <>{ + +class WeekShiftsPane +class DayCard +class SlotCard + +} + +WeekShiftsPane -|> UiPart +DayCard -|> UiPart +SlotCard -|> UiPart + +Class HiddenOutside #FFFFFF +HiddenOutside ..> WeekShiftsPane +WeekShiftsPane --> "7" DayCard +DayCard --> "2" SlotCard + + +@enduml diff --git a/docs/diagrams/gabau/MarkSequenceDiagram.puml b/docs/diagrams/gabau/MarkSequenceDiagram.puml new file mode 100644 index 00000000000..c8fe19669c1 --- /dev/null +++ b/docs/diagrams/gabau/MarkSequenceDiagram.puml @@ -0,0 +1,94 @@ +@startuml +!include style.puml + +skinparam defaultFontSize 20 +skinparam NoteFontSize 20 +skinparam SequenceBoxFontSize 20 + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":MarkCommandParser" as MarkCommandParser LOGIC_COLOR +participant ":ParserUtil" as ParserUtil LOGIC_COLOR +participant ":MarkCommand" as MarkCommand LOGIC_COLOR + +end box + +box Model MODEL_COLOR_T1 +participant ":ModelManager" as Model MODEL_COLOR +participant ":Person" as Person MODEL_COLOR +participant ":Period" as Period MODEL_COLOR +end box + +[-> LogicManager : execute() +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand() +activate AddressBookParser + +create MarkCommandParser +AddressBookParser -> MarkCommandParser +activate MarkCommandParser +MarkCommandParser --> AddressBookParser +deactivate MarkCommandParser + +AddressBookParser -> MarkCommandParser : parse() +activate MarkCommandParser + + +MarkCommandParser -> ParserUtil : parsePeriod() +activate ParserUtil + +create Period +ParserUtil -> Period +activate Period + +Period --> ParserUtil +deactivate Period + +ParserUtil --> MarkCommandParser +deactivate ParserUtil + +create MarkCommand +MarkCommandParser -> MarkCommand +activate MarkCommand +MarkCommand --> MarkCommandParser +deactivate MarkCommand + +MarkCommandParser --> AddressBookParser +deactivate MarkCommandParser + +AddressBookParser --> LogicManager +deactivate AddressBookParser + +LogicManager -> MarkCommand : execute() +activate MarkCommand + +MarkCommand -> Model : getUnfilteredList() +activate Model + + +Model --> MarkCommand +deactivate Model +MarkCommand -> Person : mark() + +note right : Details of the union \n method are provided below +activate Person +Person -> Period : union() +activate Period +Period --> Person +deactivate Period +Person --> MarkCommand +deactivate Person +MarkCommand --> LogicManager +deactivate MarkCommand +LogicManager -->[ +deactivate LogicManager + + + + + + + +@enduml diff --git a/docs/diagrams/gabau/MarkState0.puml b/docs/diagrams/gabau/MarkState0.puml new file mode 100644 index 00000000000..f5642bb96ee --- /dev/null +++ b/docs/diagrams/gabau/MarkState0.puml @@ -0,0 +1,22 @@ +@startuml +!include style.puml +skinparam ClassFontColor #000000 +skinparam ClassBorderColor #000000 + +title "Initial absentDates" + +package Periods <>{ +Class Period as "__initial:Period__" +Class P1 as "__additionalPeriod:Period__" +Class P2 as "__:Period__" + +} + +P1 -[hidden]left-> Period +P2 -[hidden]left-> Period +hide P1 +hide P2 + + + +@enduml diff --git a/docs/diagrams/gabau/MarkState1.puml b/docs/diagrams/gabau/MarkState1.puml new file mode 100644 index 00000000000..5d3896f89e7 --- /dev/null +++ b/docs/diagrams/gabau/MarkState1.puml @@ -0,0 +1,22 @@ +@startuml +!include style.puml +skinparam ClassFontColor #000000 +skinparam ClassBorderColor #000000 + +title "After mark -i 1 d/4/1/2001" + +package Periods <>{ +Class Period as "__initial:Period__" +Class P1 as "__additionalPeriod:Period__" +Class P2 as "__:Period__" + +} + +P1 -[hidden]left-> Period +P2 -[hidden]left-> Period +P1 -[hidden]right-> P2 + + + +hide P2 +@enduml diff --git a/docs/diagrams/gabau/MarkState2.puml b/docs/diagrams/gabau/MarkState2.puml new file mode 100644 index 00000000000..d02626895f9 --- /dev/null +++ b/docs/diagrams/gabau/MarkState2.puml @@ -0,0 +1,23 @@ +@startuml +!include style.puml +skinparam ClassFontColor #000000 +skinparam ClassBorderColor #000000 + +title "Final absentDates" + +package Periods <>{ +Class Period as "__initial:Period__" +Class P1 as "__additionalPeriod:Period__" +Class P2 as "__merged:Period__" + +} + +P1 -[hidden]left-> Period +P2 -[hidden]left-> Period +P2 -[hidden]left-> P1 +hide P1 +hide Period + + + +@enduml diff --git a/docs/diagrams/gabau/PeriodUnionActivityDiagram.puml b/docs/diagrams/gabau/PeriodUnionActivityDiagram.puml new file mode 100644 index 00000000000..23505d5fa3e --- /dev/null +++ b/docs/diagrams/gabau/PeriodUnionActivityDiagram.puml @@ -0,0 +1,18 @@ +@startuml +start +:Staff select to be marked for period p; + +'Since the beta syntax does not support placing the condition outside the +'diamond we place it as the true branch instead. + +if () then ([Staff has been absent]) + if () then ([p overlaps with an absent period]) + :Combine p with absent period; + else ([else]) + :Add period p to set; + endif +else ([else]) + :Add period p to set; +endif +stop +@enduml diff --git a/docs/diagrams/gabau/style.puml b/docs/diagrams/gabau/style.puml new file mode 100644 index 00000000000..fad8b0adeaa --- /dev/null +++ b/docs/diagrams/gabau/style.puml @@ -0,0 +1,75 @@ +/' + 'Commonly used styles and colors across diagrams. + 'Refer to https://plantuml-documentation.readthedocs.io/en/latest for a more + 'comprehensive list of skinparams. + '/ + + +'T1 through T4 are shades of the original color from lightest to darkest + +!define UI_COLOR #1D8900 +!define UI_COLOR_T1 #83E769 +!define UI_COLOR_T2 #3FC71B +!define UI_COLOR_T3 #166800 +!define UI_COLOR_T4 #0E4100 + +!define LOGIC_COLOR #3333C4 +!define LOGIC_COLOR_T1 #C8C8FA +!define LOGIC_COLOR_T2 #6A6ADC +!define LOGIC_COLOR_T3 #1616B0 +!define LOGIC_COLOR_T4 #101086 + +!define MODEL_COLOR #9D0012 +!define MODEL_COLOR_T1 #F97181 +!define MODEL_COLOR_T2 #E41F36 +!define MODEL_COLOR_T3 #7B000E +!define MODEL_COLOR_T4 #51000A + +!define STORAGE_COLOR #A38300 +!define STORAGE_COLOR_T1 #FFE374 +!define STORAGE_COLOR_T2 #EDC520 +!define STORAGE_COLOR_T3 #806600 +!define STORAGE_COLOR_T2 #544400 + +!define USER_COLOR #000000 + +skinparam BackgroundColor #FFFFFFF + +skinparam Shadowing false + +skinparam Class { + FontColor #FFFFFF + BorderThickness 1 + BorderColor #FFFFFF + StereotypeFontColor #FFFFFF + FontName Arial +} + +skinparam Actor { + BorderColor USER_COLOR + Color USER_COLOR + FontName Arial +} + +skinparam Sequence { + MessageAlign center + BoxFontSize 15 + BoxPadding 0 + BoxFontColor #FFFFFF + FontName Arial +} + +skinparam Participant { + FontColor #FFFFFFF + Padding 20 +} + +skinparam MinClassWidth 50 +skinparam ParticipantPadding 10 +skinparam Shadowing false +skinparam DefaultTextAlignment center +skinparam packageStyle Rectangle + +hide footbox +hide members +hide circle diff --git a/docs/images/AddShiftActivityDiagram.png b/docs/images/AddShiftActivityDiagram.png new file mode 100644 index 00000000000..83b411e5e5b Binary files /dev/null and b/docs/images/AddShiftActivityDiagram.png differ diff --git a/docs/images/AddShiftClassDiagram.png b/docs/images/AddShiftClassDiagram.png new file mode 100644 index 00000000000..4df6ace351e Binary files /dev/null and b/docs/images/AddShiftClassDiagram.png differ diff --git a/docs/images/AddShiftSequenceDiagram.png b/docs/images/AddShiftSequenceDiagram.png new file mode 100644 index 00000000000..285375d580c Binary files /dev/null and b/docs/images/AddShiftSequenceDiagram.png differ diff --git a/docs/images/ArchitectureSequenceDiagram.png b/docs/images/ArchitectureSequenceDiagram.png index 2f1346869d0..205493fc24b 100644 Binary files a/docs/images/ArchitectureSequenceDiagram.png and b/docs/images/ArchitectureSequenceDiagram.png differ diff --git a/docs/images/BetterModelClassDiagram.png b/docs/images/BetterModelClassDiagram.png index 1ec62caa2a5..701aafb4231 100644 Binary files a/docs/images/BetterModelClassDiagram.png and b/docs/images/BetterModelClassDiagram.png differ diff --git a/docs/images/ChangeCommand.png b/docs/images/ChangeCommand.png new file mode 100644 index 00000000000..0d4857dbcce Binary files /dev/null and b/docs/images/ChangeCommand.png differ diff --git a/docs/images/ChangeCommandAddShift.png b/docs/images/ChangeCommandAddShift.png new file mode 100644 index 00000000000..b0c40f89c23 Binary files /dev/null and b/docs/images/ChangeCommandAddShift.png differ diff --git a/docs/images/CommandTextExample.png b/docs/images/CommandTextExample.png new file mode 100644 index 00000000000..f2babe66fca Binary files /dev/null and b/docs/images/CommandTextExample.png differ diff --git a/docs/images/CommandTextExample2.png b/docs/images/CommandTextExample2.png new file mode 100644 index 00000000000..aed7c26a0d3 Binary files /dev/null and b/docs/images/CommandTextExample2.png differ diff --git a/docs/images/DeleteResult.png b/docs/images/DeleteResult.png new file mode 100644 index 00000000000..dcac0aa3d32 Binary files /dev/null and b/docs/images/DeleteResult.png differ diff --git a/docs/images/DeleteSequenceDiagram.png b/docs/images/DeleteSequenceDiagram.png index fa327b39618..955ef119aae 100644 Binary files a/docs/images/DeleteSequenceDiagram.png and b/docs/images/DeleteSequenceDiagram.png differ diff --git a/docs/images/FindActivityDiagram.png b/docs/images/FindActivityDiagram.png new file mode 100644 index 00000000000..a816f2c3a7a Binary files /dev/null and b/docs/images/FindActivityDiagram.png differ diff --git a/docs/images/FindCommand.png b/docs/images/FindCommand.png new file mode 100644 index 00000000000..64dc5131491 Binary files /dev/null and b/docs/images/FindCommand.png differ diff --git a/docs/images/FindCommandSchedule.png b/docs/images/FindCommandSchedule.png new file mode 100644 index 00000000000..de89a76fe3b Binary files /dev/null and b/docs/images/FindCommandSchedule.png differ diff --git a/docs/images/IstaffCommand.png b/docs/images/IstaffCommand.png new file mode 100644 index 00000000000..a71173cc8bf Binary files /dev/null and b/docs/images/IstaffCommand.png differ diff --git a/docs/images/ListCommand.png b/docs/images/ListCommand.png new file mode 100644 index 00000000000..bcfbf77f484 Binary files /dev/null and b/docs/images/ListCommand.png differ diff --git a/docs/images/MarkActivityDiagram.png b/docs/images/MarkActivityDiagram.png new file mode 100644 index 00000000000..b97e3e209bf Binary files /dev/null and b/docs/images/MarkActivityDiagram.png differ diff --git a/docs/images/MarkCommandList.png b/docs/images/MarkCommandList.png new file mode 100644 index 00000000000..836608ae254 Binary files /dev/null and b/docs/images/MarkCommandList.png differ diff --git a/docs/images/MarkCommandSchedule.png b/docs/images/MarkCommandSchedule.png new file mode 100644 index 00000000000..86944f2f3b6 Binary files /dev/null and b/docs/images/MarkCommandSchedule.png differ diff --git a/docs/images/MarkSequenceDiagram.png b/docs/images/MarkSequenceDiagram.png new file mode 100644 index 00000000000..2b16b045d52 Binary files /dev/null and b/docs/images/MarkSequenceDiagram.png differ diff --git a/docs/images/MarkState0.png b/docs/images/MarkState0.png new file mode 100644 index 00000000000..f68329348fe Binary files /dev/null and b/docs/images/MarkState0.png differ diff --git a/docs/images/MarkState1.png b/docs/images/MarkState1.png new file mode 100644 index 00000000000..bd4ec75d8c7 Binary files /dev/null and b/docs/images/MarkState1.png differ diff --git a/docs/images/MarkState2.png b/docs/images/MarkState2.png new file mode 100644 index 00000000000..da130244d0f Binary files /dev/null and b/docs/images/MarkState2.png differ diff --git a/docs/images/ModelClassDiagram.png b/docs/images/ModelClassDiagram.png index 39d7aec4b33..e2f3d9504ba 100644 Binary files a/docs/images/ModelClassDiagram.png and b/docs/images/ModelClassDiagram.png differ diff --git a/docs/images/PeriodUnionActivityDiagram.png b/docs/images/PeriodUnionActivityDiagram.png new file mode 100644 index 00000000000..62fdf87f1ed Binary files /dev/null and b/docs/images/PeriodUnionActivityDiagram.png differ diff --git a/docs/images/PersonClassDiagram.png b/docs/images/PersonClassDiagram.png new file mode 100644 index 00000000000..3ca9ba70fef Binary files /dev/null and b/docs/images/PersonClassDiagram.png differ diff --git a/docs/images/ScheduleFields.png b/docs/images/ScheduleFields.png new file mode 100644 index 00000000000..41744c8e0cb Binary files /dev/null and b/docs/images/ScheduleFields.png differ diff --git a/docs/images/SetRoleReq.png b/docs/images/SetRoleReq.png new file mode 100644 index 00000000000..8a40ae8f17d Binary files /dev/null and b/docs/images/SetRoleReq.png differ diff --git a/docs/images/StaffFields.png b/docs/images/StaffFields.png new file mode 100644 index 00000000000..a50f948f6c4 Binary files /dev/null and b/docs/images/StaffFields.png differ diff --git a/docs/images/StaffList.png b/docs/images/StaffList.png new file mode 100644 index 00000000000..fe44c598074 Binary files /dev/null and b/docs/images/StaffList.png differ diff --git a/docs/images/StaffSchedule.png b/docs/images/StaffSchedule.png new file mode 100644 index 00000000000..f49ac0f7c19 Binary files /dev/null and b/docs/images/StaffSchedule.png differ diff --git a/docs/images/StatsCommand.png b/docs/images/StatsCommand.png new file mode 100644 index 00000000000..fb34f0e62ed Binary files /dev/null and b/docs/images/StatsCommand.png differ diff --git a/docs/images/StorageClassDiagram.png b/docs/images/StorageClassDiagram.png index 82c66f8f16e..a01339cb7ec 100644 Binary files a/docs/images/StorageClassDiagram.png and b/docs/images/StorageClassDiagram.png differ diff --git a/docs/images/StoragePersonDiagram.png b/docs/images/StoragePersonDiagram.png new file mode 100644 index 00000000000..4c0e7c2e9bf Binary files /dev/null and b/docs/images/StoragePersonDiagram.png differ diff --git a/docs/images/Ui.png b/docs/images/Ui.png index 91488fd1a0f..caefd5cc89f 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 4bb8b2ce591..2102f8c7f5e 100644 Binary files a/docs/images/UiClassDiagram.png and b/docs/images/UiClassDiagram.png differ diff --git a/docs/images/UnmarkCommandList.png b/docs/images/UnmarkCommandList.png new file mode 100644 index 00000000000..c57728b147d Binary files /dev/null and b/docs/images/UnmarkCommandList.png differ diff --git a/docs/images/UnmarkCommandSchedule.png b/docs/images/UnmarkCommandSchedule.png new file mode 100644 index 00000000000..c5aef02ae15 Binary files /dev/null and b/docs/images/UnmarkCommandSchedule.png differ diff --git a/docs/images/ViewShiftActivityDiagram.png b/docs/images/ViewShiftActivityDiagram.png new file mode 100644 index 00000000000..97553d27a5e Binary files /dev/null and b/docs/images/ViewShiftActivityDiagram.png differ diff --git a/docs/images/ViewShiftCommandList.png b/docs/images/ViewShiftCommandList.png new file mode 100644 index 00000000000..cecd794b9ec Binary files /dev/null and b/docs/images/ViewShiftCommandList.png differ diff --git a/docs/images/ViewShiftCommandSchedule.png b/docs/images/ViewShiftCommandSchedule.png new file mode 100644 index 00000000000..c829ce5115c Binary files /dev/null and b/docs/images/ViewShiftCommandSchedule.png differ diff --git a/docs/images/WeekShiftsPaneClassDiagram.png b/docs/images/WeekShiftsPaneClassDiagram.png new file mode 100644 index 00000000000..0741368e3f7 Binary files /dev/null and b/docs/images/WeekShiftsPaneClassDiagram.png differ diff --git a/docs/images/findCommand/AfterFindCommand.jpg b/docs/images/findCommand/AfterFindCommand.jpg new file mode 100644 index 00000000000..7a781f1bc6c Binary files /dev/null and b/docs/images/findCommand/AfterFindCommand.jpg differ diff --git a/docs/images/findCommand/BeforeFindCommand.jpg b/docs/images/findCommand/BeforeFindCommand.jpg new file mode 100644 index 00000000000..df4916caca4 Binary files /dev/null and b/docs/images/findCommand/BeforeFindCommand.jpg differ diff --git a/docs/images/findCommand/FindClassDiagram.png b/docs/images/findCommand/FindClassDiagram.png new file mode 100644 index 00000000000..8d6f7fd2334 Binary files /dev/null and b/docs/images/findCommand/FindClassDiagram.png differ diff --git a/docs/images/findCommand/FindSequenceDiagram.png b/docs/images/findCommand/FindSequenceDiagram.png new file mode 100644 index 00000000000..bdfb6873409 Binary files /dev/null and b/docs/images/findCommand/FindSequenceDiagram.png differ diff --git a/docs/images/fullfatwasabi.png b/docs/images/fullfatwasabi.png new file mode 100644 index 00000000000..257989be09d Binary files /dev/null and b/docs/images/fullfatwasabi.png differ diff --git a/docs/images/gabau.png b/docs/images/gabau.png new file mode 100644 index 00000000000..6bb18622af0 Binary files /dev/null and b/docs/images/gabau.png differ diff --git a/docs/images/helpMessage.png b/docs/images/helpMessage.png index b1f70470137..bfad0276f19 100644 Binary files a/docs/images/helpMessage.png and b/docs/images/helpMessage.png differ diff --git a/docs/images/irvinghe000.png b/docs/images/irvinghe000.png new file mode 100644 index 00000000000..73498bcf521 Binary files /dev/null and b/docs/images/irvinghe000.png differ diff --git a/docs/images/mweeruien.png b/docs/images/mweeruien.png new file mode 100644 index 00000000000..7f29ad41d40 Binary files /dev/null and b/docs/images/mweeruien.png differ diff --git a/docs/images/newUI.jpg b/docs/images/newUI.jpg new file mode 100644 index 00000000000..9e150257a6b Binary files /dev/null and b/docs/images/newUI.jpg differ diff --git a/docs/images/previous_images/MockUi.png b/docs/images/previous_images/MockUi.png new file mode 100644 index 00000000000..24e0c1a19c4 Binary files /dev/null and b/docs/images/previous_images/MockUi.png differ diff --git a/docs/images/previous_images/helpMessageOLD.png b/docs/images/previous_images/helpMessageOLD.png new file mode 100644 index 00000000000..b1f70470137 Binary files /dev/null and b/docs/images/previous_images/helpMessageOLD.png differ diff --git a/docs/images/staff_view.png b/docs/images/staff_view.png new file mode 100644 index 00000000000..13763e1c649 Binary files /dev/null and b/docs/images/staff_view.png differ diff --git a/docs/images/tetrerox.png b/docs/images/tetrerox.png new file mode 100644 index 00000000000..f79666f0275 Binary files /dev/null and b/docs/images/tetrerox.png differ diff --git a/docs/images/viewScheduleImage.png b/docs/images/viewScheduleImage.png new file mode 100644 index 00000000000..c906ce8ec48 Binary files /dev/null and b/docs/images/viewScheduleImage.png differ diff --git a/docs/images/viewShiftCommand/ViewShiftClassDiagram.png b/docs/images/viewShiftCommand/ViewShiftClassDiagram.png new file mode 100644 index 00000000000..3cf444aa44b Binary files /dev/null and b/docs/images/viewShiftCommand/ViewShiftClassDiagram.png differ diff --git a/docs/images/viewShiftCommand/ViewShiftCommandExecutionSequenceDiagram.png b/docs/images/viewShiftCommand/ViewShiftCommandExecutionSequenceDiagram.png new file mode 100644 index 00000000000..a5dd6fa6b3d Binary files /dev/null and b/docs/images/viewShiftCommand/ViewShiftCommandExecutionSequenceDiagram.png differ diff --git a/docs/images/viewShiftCommand/ViewShiftSequenceDiagram.png b/docs/images/viewShiftCommand/ViewShiftSequenceDiagram.png new file mode 100644 index 00000000000..1db76c24790 Binary files /dev/null and b/docs/images/viewShiftCommand/ViewShiftSequenceDiagram.png differ diff --git a/docs/images/viewShiftCommand/viewShift.jpg b/docs/images/viewShiftCommand/viewShift.jpg new file mode 100644 index 00000000000..d44d8b070ad Binary files /dev/null and b/docs/images/viewShiftCommand/viewShift.jpg differ diff --git a/docs/index.md b/docs/index.md index 7601dbaad0d..a7b9cddfb35 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,17 +1,17 @@ --- layout: page -title: AddressBook Level-3 +title: Staff'd --- -[![CI Status](https://github.com/se-edu/addressbook-level3/workflows/Java%20CI/badge.svg)](https://github.com/se-edu/addressbook-level3/actions) -[![codecov](https://codecov.io/gh/se-edu/addressbook-level3/branch/master/graph/badge.svg)](https://codecov.io/gh/se-edu/addressbook-level3) +[![Java CI](https://github.com/AY2122S1-CS2103T-W11-2/tp/actions/workflows/gradle.yml/badge.svg)](https://github.com/AY2122S1-CS2103T-W11-2/tp/actions/workflows/gradle.yml) +[![codecov](https://codecov.io/gh/AY2122S1-CS2103T-W11-2/tp/branch/master/graph/badge.svg?token=VVZ5LZPCSV)](https://codecov.io/gh/AY2122S1-CS2103T-W11-2/tp) ![Ui](images/Ui.png) -**AddressBook is a desktop application for managing your contact details.** While it has a GUI, most of the user interactions happen using a CLI (Command Line Interface). +**Staff'd is a desktop application for managing your staff.** While it has a GUI, most of the user interactions happen using a CLI (Command Line Interface). -* If you are interested in using AddressBook, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). -* If you are interested about developing AddressBook, the [**Developer Guide**](DeveloperGuide.html) is a good place to start. +* If you are interested in using Staff'd, head over to the [_Quick Start_ section of the **User Guide**](UserGuide.html#quick-start). +* If you are interested about developing Staff'd, the [**Developer Guide**](DeveloperGuide.html) is a good place to start. **Acknowledgements** diff --git a/docs/sample/addShift.json b/docs/sample/addShift.json new file mode 100644 index 00000000000..f3a28440fdb --- /dev/null +++ b/docs/sample/addShift.json @@ -0,0 +1,84 @@ +{ + "persons" : [ { + "name" : "Alex Yeoh", + "phone" : "87438807", + "email" : "alexyeoh@example.com", + "salary" : "11.00", + "status" : "parttime", + "tagged" : [ "friends" ], + "schedule" : { + "shifts" : [ [ { + "dayOfWeek" : "MONDAY", + "slot" : "morning", + "history" : [ { + "period" : "2021-11-06 2021-11-12", + "startTime" : "10:00", + "endTime" : "16:00" + } ], + "isEmpty" : false + }, null ], [ null, null ], [ null, null ], [ null, null ], [ null, null ], [ null, null ], [ null, null ] ] + }, + "absentDates" : [ ], + "roles" : [ "floor" ] + }, { + "name" : "Bernice Yu", + "phone" : "99272758", + "email" : "berniceyu@example.com", + "salary" : "6.00", + "status" : "fulltime", + "tagged" : [ "colleagues", "friends" ], + "schedule" : { + "shifts" : [ [ null, null ], [ null, null ], [ null, null ], [ null, null ], [ null, null ], [ null, null ], [ null, null ] ] + }, + "absentDates" : [ ], + "roles" : [ "kitchen" ] + }, { + "name" : "Charlotte Oliveiro", + "phone" : "93210283", + "email" : "charlotte@example.com", + "salary" : "7.50", + "status" : "parttime", + "tagged" : [ "neighbours" ], + "schedule" : { + "shifts" : [ [ null, null ], [ null, null ], [ null, null ], [ null, null ], [ null, null ], [ null, null ], [ null, null ] ] + }, + "absentDates" : [ ], + "roles" : [ "bartender" ] + }, { + "name" : "David Li", + "phone" : "91031282", + "email" : "lidavid@example.com", + "salary" : "8.00", + "status" : "fulltime", + "tagged" : [ "family" ], + "schedule" : { + "shifts" : [ [ null, null ], [ null, null ], [ null, null ], [ null, null ], [ null, null ], [ null, null ], [ null, null ] ] + }, + "absentDates" : [ ], + "roles" : [ "kitchen" ] + }, { + "name" : "Irfan Ibrahim", + "phone" : "92492021", + "email" : "irfan@example.com", + "salary" : "8.50", + "status" : "parttime", + "tagged" : [ "classmates" ], + "schedule" : { + "shifts" : [ [ null, null ], [ null, null ], [ null, null ], [ null, null ], [ null, null ], [ null, null ], [ null, null ] ] + }, + "absentDates" : [ ], + "roles" : [ "kitchen" ] + }, { + "name" : "Roy Balakrishnan", + "phone" : "92624417", + "email" : "royb@example.com", + "salary" : "7.00", + "status" : "parttime", + "tagged" : [ "colleagues" ], + "schedule" : { + "shifts" : [ [ null, null ], [ null, null ], [ null, null ], [ null, null ], [ null, null ], [ null, null ], [ null, null ] ] + }, + "absentDates" : [ ], + "roles" : [ "floor" ] + } ] +} diff --git a/docs/sample/deleteAlex.json b/docs/sample/deleteAlex.json new file mode 100644 index 00000000000..e19a72451b2 --- /dev/null +++ b/docs/sample/deleteAlex.json @@ -0,0 +1,63 @@ +{ + "persons" : [ { + "name" : "Bernice Yu", + "phone" : "99272758", + "email" : "berniceyu@example.com", + "salary" : "6.00", + "status" : "fulltime", + "tagged" : [ "colleagues", "friends" ], + "schedule" : { + "shifts" : [ [ null, null ], [ null, null ], [ null, null ], [ null, null ], [ null, null ], [ null, null ], [ null, null ] ] + }, + "absentDates" : [ ], + "roles" : [ "kitchen" ] + }, { + "name" : "Charlotte Oliveiro", + "phone" : "93210283", + "email" : "charlotte@example.com", + "salary" : "7.50", + "status" : "parttime", + "tagged" : [ "neighbours" ], + "schedule" : { + "shifts" : [ [ null, null ], [ null, null ], [ null, null ], [ null, null ], [ null, null ], [ null, null ], [ null, null ] ] + }, + "absentDates" : [ ], + "roles" : [ "bartender" ] + }, { + "name" : "David Li", + "phone" : "91031282", + "email" : "lidavid@example.com", + "salary" : "8.00", + "status" : "fulltime", + "tagged" : [ "family" ], + "schedule" : { + "shifts" : [ [ null, null ], [ null, null ], [ null, null ], [ null, null ], [ null, null ], [ null, null ], [ null, null ] ] + }, + "absentDates" : [ ], + "roles" : [ "kitchen" ] + }, { + "name" : "Irfan Ibrahim", + "phone" : "92492021", + "email" : "irfan@example.com", + "salary" : "8.50", + "status" : "parttime", + "tagged" : [ "classmates" ], + "schedule" : { + "shifts" : [ [ null, null ], [ null, null ], [ null, null ], [ null, null ], [ null, null ], [ null, null ], [ null, null ] ] + }, + "absentDates" : [ ], + "roles" : [ "kitchen" ] + }, { + "name" : "Roy Balakrishnan", + "phone" : "92624417", + "email" : "royb@example.com", + "salary" : "7.00", + "status" : "parttime", + "tagged" : [ "colleagues" ], + "schedule" : { + "shifts" : [ [ null, null ], [ null, null ], [ null, null ], [ null, null ], [ null, null ], [ null, null ], [ null, null ] ] + }, + "absentDates" : [ ], + "roles" : [ "floor" ] + } ] +} diff --git a/docs/sample/empty.json b/docs/sample/empty.json new file mode 100644 index 00000000000..db326afff19 --- /dev/null +++ b/docs/sample/empty.json @@ -0,0 +1,3 @@ +{ + "persons" : [ ] +} diff --git a/docs/team/fullfatwasabi.md b/docs/team/fullfatwasabi.md new file mode 100644 index 00000000000..5df4650fbd4 --- /dev/null +++ b/docs/team/fullfatwasabi.md @@ -0,0 +1,41 @@ +--- +layout: page +title: Jonathan's Project Portfolio Page +--- + +### Project: Staff'd + +Staff'd is a desktop staff tracking application used managing F&B staff. 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. + +Given below are my contributions to the project: + +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2122s1.github.io/tp-dashboard/?search=&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code&since=2021-09-17&tabOpen=true&tabAuthor=fullfatwasabi&tabRepo=AY2122S1-CS2103T-W11-2%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false&tabType=authorship) + +* **Enhancements implemented** + +* **New Feature**: Changing Tabs + * What it does: Allows the user to change tabs between the Schedule GUI and the Staff List. + * Justification: This feature allows the user to swap between these two tabs via CLI. + +* **New Feature**: Schedule GUI + * What it does: GUI to allow the users to see the schedules. + * Justification: Allows our managers to easily see relevant schedule related information quickly, enhances usability of Staff'd. + * Was pretty challenging for me and took up a lot of my time. + +* **Changes to existing features**: Adding and removing fields (such as salaries and roles) from Person, as well as handling its related functionality e.g. saving, editing and testing. + * Makes the Person class have more relevant fields and removing unnecessary ones. + +* **Changes to existing features**: Updating the PersonCard look in the GUI to show necessary information such as absent periods and roles. + * Similarly to the Schedule GUI point, was pretty challenging for me and took up a lot of my time. + +* **Documentation**: + * User Guide: + * Organized User Guide and added new sections such as GUI Breakdown, User Guide Usage, and Glossary. Improved sections such as Flag Legend and Introduction. [#330](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/330) + * Added and annotated images [#335](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/335) + +* **Contributions to team-based tasks**: + * Set up team repo and organization + * Set up codecov, jekyll. + +* **Community**: + * PRs reviewed (with non-trivial review comments): [#102](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/102), [#149](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/149), [#234](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/234). Some feedback was provided through direct messages also, and not reviewed on the PR directly. diff --git a/docs/team/gabau.md b/docs/team/gabau.md new file mode 100644 index 00000000000..7b2a9bf50d1 --- /dev/null +++ b/docs/team/gabau.md @@ -0,0 +1,47 @@ +--- +layout: page +title: Gabriel's Project Portfolio Page +--- + +### Project: Staff'd + +Staff'd is a desktop staff tracking application used managing F&B staff. 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. + +Given below are my contributions to the project. + +* **New Feature**: Added the ability to mark and unmark a staff from attendance. + * What it does: Allows the user to mark a staff as absent for a range of dates. + * Justification: This feature improves the product significantly because an employee may be on vacation over a group of dates, and this feature simplifies the recording of such information. + * Highlights: This enhancement used the start and endpoint to record the startdate and enddate to be marked, which meant that to remove and add such dates, some form of complement and union had to be implemented. + +* **New Feature**: Added the ability to add a duration for which a shift is active. + * What it does: Allows the user to select a group of dates where a staff is working a shift. + * Justification: In order for obtaining the salary over a month, the shifts must be assigned to dates to make it clear for the user when the staff is working and when the staff is not. + * Highlights: How these commands interacted with the `setShiftTIme` command had to be addressed. Delete and add ignored the setShiftTime and just delete by the range of dates. How the enhancement was stored also needed to be dealt with, as a list of all the assigned duration needed to be maintained for each shift. + +* **Code contributed**: [RepoSense link](https://nus-cs2103-ay2122s1.github.io/tp-dashboard/?search=&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2021-09-17&tabOpen=true&tabType=authorship&tabAuthor=Gabau&tabRepo=AY2122S1-CS2103T-W11-2%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false#:~:text=Click%20to%20view%20author%27s%20contribution.) + +* **Project management**: + * Managed releases `v1.2` - `v1.4` (4 releases) on GitHub + * Managed milestone tracking + +* **Enhancements to existing features**: + * Enhanced `find` command to take in tags. [\#233](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/#233), [\#47](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/#47) + +[comment]: <> ( * Wrote additional tests for existing features to increase coverage from 88% to 92% (Pull requests [\#36](), [\#38]())) + +* **Documentation**: + * User Guide: + * Added documentation for the features `mark`, `unmark` [\#106](https://github.com/AY2122S1-CS2103T-W11-2/tp/#106), `staff`, `istaff` [\#124](https://github.com/AY2122S1-CS2103T-W11-2/tp/#124) + * Added documentation for shift modifications with the active periods. + * Developer Guide: + * Added implementation details of the `mark` and `unmark` feature. + * Updated glossary and non-functional requirements. + * Updated diagrams of Logic, Model, Storage and UI component [\#287](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/287). + * Updated diagrams of attendance feature in [\#106](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/106). + +* **Community**: + * PRs reviewed (with non-trivial review comments): [\#73](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/73), [\#64](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/64), [\#28](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/28), [\#95](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/95), [\#120](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/120) + + + diff --git a/docs/team/irvinghe000.md b/docs/team/irvinghe000.md new file mode 100644 index 00000000000..493d190052d --- /dev/null +++ b/docs/team/irvinghe000.md @@ -0,0 +1,67 @@ +--- +layout: page +title: Outong's Project Portfolio Page +--- + +### Project: Staff'd + +Staff'd 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. + +Given below are my contributions to the project. + +* **New Feature**: Added an add shift command that allows the user to add shift to a staff's schedule. + * What it does: It searches the target staff with given index or name, and add a shift to + * Justification: This is a fundamental command user needs when they manipulate the schedule of staffs. User can add + a new shift to a specific slot on a specific day to the target staff. + +* **New Feature**: Added the ability to change the time of a specific shift. + * What it does: It locates a shift first with the given index, staff's name, date and slot, then it updates the time + of that shift with the given start time and end time. + * Justification: This functionality improves the software's flexibility significantly, because when a shift is + added, it will only be set to the default time. With this command, users can update the time according to the real + situation. + +* **Code + contributed**: [RepoSense link](https://nus-cs2103-ay2122s1.github.io/tp-dashboard/?search=IrvingHe000&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByAuthors&breakdown=true&checkedFileTypes=docs~functional-code~test-code&since=2021-09-17&tabOpen=true&tabAuthor=IrvingHe000&tabRepo=AY2122S1-CS2103T-W11-2%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false&tabType=authorship) + +* **Project management**: + * Managed releases `v1.2` - `v1.4` (4 releases) on GitHub + * Managed milestone tracking + +* **Enhancements to existing features**: + * Updated existing fields name to adapt our product (Pull requests + [\#29](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/29)) + * Add new attributes to Person (Pull requests + [\#37](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/37), + [\#93](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/93)) + +* **Documentation**: + * User Guide: + * Add documentation for features `addShift` and `setShiftTime` + (Pull requests + [\#73](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/73), + [\#132](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/132), + [\#241](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/241)) + * Add UI mockup at the beginning + (Pull requests + [\#16](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/16), + [\#40](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/40)) + + * Developer Guide: + * Add documentation for implementing `addShift` command and update documentation for other commands + (Pull requests + [\#116](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/116), + [\#120](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/120), + [\#324](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/324)) + * Add documentation for acknowledgement, manual testing, and proposed future features + (Pull requests + [\#292](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/292)) + +* **Community**: + * PRs reviewed (with non-trivial review comments): + [\#47](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/47), + [\#84](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/84), + [\#92](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/92), + [\#124](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/124), + [\#233](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/233) diff --git a/docs/team/johndoe.md b/docs/team/johndoe.md deleted file mode 100644 index 773a07794e2..00000000000 --- a/docs/team/johndoe.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -layout: page -title: John Doe's Project Portfolio Page ---- - -### Project: AddressBook Level 3 - -AddressBook - Level 3 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. - -Given below are my contributions to the project. - -* **New Feature**: 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}* - -* **New Feature**: Added a history command that allows the user to navigate to previous commands using up/down keys. - -* **Code contributed**: [RepoSense link]() - -* **Project management**: - * Managed releases `v1.3` - `v1.5rc` (3 releases) on GitHub - -* **Enhancements to existing features**: - * Updated the GUI color scheme (Pull requests [\#33](), [\#34]()) - * Wrote additional tests for existing features to increase coverage from 88% to 92% (Pull requests [\#36](), [\#38]()) - -* **Documentation**: - * User Guide: - * Added documentation for the features `delete` and `find` [\#72]() - * Did cosmetic tweaks to existing documentation of features `clear`, `exit`: [\#74]() - * Developer Guide: - * Added implementation details of the `delete` feature. - -* **Community**: - * PRs reviewed (with non-trivial review comments): [\#12](), [\#32](), [\#19](), [\#42]() - * Contributed to forum discussions (examples: [1](), [2](), [3](), [4]()) - * Reported bugs and suggestions for other teams in the class (examples: [1](), [2](), [3]()) - * Some parts of the history feature I added was adopted by several other class mates ([1](), [2]()) - -* **Tools**: - * Integrated a third party library (Natty) to the project ([\#42]()) - * Integrated a new Github plugin (CircleCI) to the team repo - -* _{you can add/remove categories in the list above}_ diff --git a/docs/team/mweeruien.md b/docs/team/mweeruien.md new file mode 100644 index 00000000000..19ba60ebca5 --- /dev/null +++ b/docs/team/mweeruien.md @@ -0,0 +1,44 @@ +--- +layout: page +title: Megan's Project Portfolio Page +--- + +### Project: Staff'd + +Staff'd is a desktop staff tracking application used managing F&B staff. The user interacts with it using a CLI, and it has a GUI created with JavaFX. It is written in Java, and has about 10k Lines of Code. + +Below are my contributions to the project. + +* **New Feature**: Added the `viewShift` Command which allows employees to search for staff working on a particular shift. + * What it does: Allows users to search for a specific shift by indicating the day of the week, and either a specific time or the shift number. + * Justification: This feature improves the product significantly because employers can now quickly and easily access the list of staff working at any particular shift. + +* **New Feature**: Added the ability to set the role requirements for each shift. + * What it does: Allows the users to set the requirements for each role every shift. The role requirements will be displayed when the command is called. Additionally, Staff'd will refer to these role requirements to check for staff shortages when the `viewShift` command is utilized. + * Justification: This feature allows managers to quickly ascertain whether they have sufficient staff of each role working for a particular shift. + * Highlights: A data file will be created to store the role requirements information so that users only need to adjust the role requirements once. + +* **Enhancements to existing features**: + * Enhanced `find` command to search by indexes. [\#72](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/72) + * Enhanced `clear` command to also reset the save file for role requirements + +* Conducted manual testing and fixed bugs and help messages for v1.4. [\#299](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/299) + +* **Code contributed**: [RepoSense Link](https://nus-cs2103-ay2122s1.github.io/tp-dashboard/?search=mweeruien&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2021-09-17&tabOpen=true&tabType=authorship&zFR=false&tabAuthor=mweeruien&tabRepo=AY2122S1-CS2103T-W11-2%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code~other&authorshipIsBinaryFileTypeChecked=false) + +* Designed and created the **Staff'd Icon** + +* **Project management**: + * Managed releases `v1.2` - `v1.4` (4 releases) on GitHub + * Managed issue tracking + +* **Documentation**: + * User Guide: + * Organized User Guide and added new sections such as GUI Breakdown, User Guide Usage, and Glossary. Improved sections such as Flag Legend and Introduction. [\#330](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/330) + * Added documentation for the features: `find` [\#72](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/72), `viewShift`[\#102](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/102), `setRoleReq` [\#149](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/149/) + * Developer Guide: + * Added implementation details for the `find` and `viewShift` features + * Updated diagrams for the `find` and `viewShift` features [\#119](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/119) + +* **Community**: + * PRs reviewed (with non-trivial review comments): [\#47](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/47/), [\#136](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/136). [\#241](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/241), [\#282](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/282) diff --git a/docs/team/tetrerox.md b/docs/team/tetrerox.md new file mode 100644 index 00000000000..008b3b11a72 --- /dev/null +++ b/docs/team/tetrerox.md @@ -0,0 +1,39 @@ +--- +layout: page +title: Surya's Project Portfolio Page +--- + +### Project: Staff'd + +Staff’d is a desktop staff tracking application that manages F&B staff, their schedule and salaries. 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. + +Given below are my contributions to the project. + +* **New Feature**: Added the ability to swap shifts between 2 staff. + * _What it does_: Takes a shift from each of the 2 staff and swaps them between the 2 of them. + * _Justification_: This allows staff to have flexibility in their work schedule to take breaks while not affecting the business. The user only has to input 1 command instead of using 2 `delete` and 2 `add` commands. + +* **New Feature**: Added the ability for the user to delete a shift from a staff's schedule. + * _What it does_: It locates a staff with the given index or staff's name and deletes the shift with the given date and slot. + * _Justification_: This command is necessary for the manager to manage his/her staff's shifts and overall schedule. + +* **Enhancements to existing features**: + * Modified the delete command to locate staff using their names and also to support mass deletion using staff role and status. + +* **Code contributed**: [RepoSense Link](https://nus-cs2103-ay2122s1.github.io/tp-dashboard/?search=tetrerox&sort=groupTitle&sortWithin=title&timeframe=commit&mergegroup=&groupSelect=groupByRepos&breakdown=true&checkedFileTypes=docs~functional-code~test-code~other&since=2021-09-17&tabOpen=true&tabType=authorship&tabAuthor=Tetrerox&tabRepo=AY2122S1-CS2103T-W11-2%2Ftp%5Bmaster%5D&authorshipIsMergeGroup=false&authorshipFileTypes=docs~functional-code~test-code&authorshipIsBinaryFileTypeChecked=false) + +* **Project management**: + * Managed releases `v1.2` - `v1.4` (4 releases) on GitHub. + * Managed milestone tracking. + +* **Documentation**: + * User Guide: + * Updated the documentation for `delete` and added documentation for `deleteShift` and `swapShift`. + * Developer Guide: + * Updated the product scope and user stories. + * Added introduction, overview and effort in the appendix. + * Added activity diagrams and use cases. + +* **Community**: + * PRs reviewed (with non-trivial review comments): [#136](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/136), [#155](https://github.com/AY2122S1-CS2103T-W11-2/tp/pull/155) diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java index 4133aaa0151..d91aa3ec963 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/seedu/address/MainApp.java @@ -25,6 +25,7 @@ import seedu.address.storage.AddressBookStorage; import seedu.address.storage.JsonAddressBookStorage; import seedu.address.storage.JsonUserPrefsStorage; +import seedu.address.storage.RoleReqStorage; import seedu.address.storage.Storage; import seedu.address.storage.StorageManager; import seedu.address.storage.UserPrefsStorage; @@ -36,7 +37,7 @@ */ public class MainApp extends Application { - public static final Version VERSION = new Version(0, 2, 0, true); + public static final Version VERSION = new Version(1, 4, 0, true); private static final Logger logger = LogsCenter.getLogger(MainApp.class); @@ -48,7 +49,7 @@ public class MainApp extends Application { @Override public void init() throws Exception { - logger.info("=============================[ Initializing AddressBook ]==========================="); + logger.info("=============================[ Initializing Staff'd ]==========================="); super.init(); AppParameters appParameters = AppParameters.parse(getParameters()); @@ -66,6 +67,8 @@ public void init() throws Exception { logic = new LogicManager(model, storage); ui = new UiManager(logic); + + RoleReqStorage.load(); } /** @@ -167,13 +170,13 @@ protected UserPrefs initPrefs(UserPrefsStorage storage) { @Override public void start(Stage primaryStage) { - logger.info("Starting AddressBook " + MainApp.VERSION); + logger.info("Starting Staff'd " + MainApp.VERSION); ui.start(primaryStage); } @Override public void stop() { - logger.info("============================ [ Stopping Address Book ] ============================="); + logger.info("============================ [ Stopping Staff'd ] ============================="); try { storage.saveUserPrefs(model.getUserPrefs()); } catch (IOException e) { diff --git a/src/main/java/seedu/address/commons/core/LogsCenter.java b/src/main/java/seedu/address/commons/core/LogsCenter.java index 431e7185e76..17d157e065d 100644 --- a/src/main/java/seedu/address/commons/core/LogsCenter.java +++ b/src/main/java/seedu/address/commons/core/LogsCenter.java @@ -12,13 +12,13 @@ * Configures and manages loggers and handlers, including their logging level * Named {@link Logger}s can be obtained from this class
                            * These loggers have been configured to output messages to the console and a {@code .log} file by default, - * at the {@code INFO} level. A new {@code .log} file with a new numbering will be created after the log - * file reaches 5MB big, up to a maximum of 5 files.
                            + * at the {@code INFO} level. A new {@code .log} file with a new numbering will be created after the log + * file reaches 5MB big, up to a maximum of 5 files.
                            */ public class LogsCenter { private static final int MAX_FILE_COUNT = 5; private static final int MAX_FILE_SIZE_IN_BYTES = (int) (Math.pow(2, 20) * 5); // 5MB - private static final String LOG_FILE = "addressbook.log"; + private static final String LOG_FILE = "staffd.log"; private static Level currentLogLevel = Level.INFO; private static final Logger logger = LogsCenter.getLogger(LogsCenter.class); private static FileHandler fileHandler; @@ -95,6 +95,7 @@ private static void addFileHandler(Logger logger) { /** * Creates a {@code FileHandler} for the log file. + * * @throws IOException if there are problems opening the file. */ private static FileHandler createFileHandler() throws IOException { diff --git a/src/main/java/seedu/address/commons/core/Messages.java b/src/main/java/seedu/address/commons/core/Messages.java index 1deb3a1e469..dce0f18fb59 100644 --- a/src/main/java/seedu/address/commons/core/Messages.java +++ b/src/main/java/seedu/address/commons/core/Messages.java @@ -1,5 +1,7 @@ package seedu.address.commons.core; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; + /** * Container for user visible messages. */ @@ -9,5 +11,17 @@ public class Messages { public static final String MESSAGE_INVALID_COMMAND_FORMAT = "Invalid command format! \n%1$s"; public static final String MESSAGE_INVALID_PERSON_DISPLAYED_INDEX = "The person index provided is invalid"; public static final String MESSAGE_PERSONS_LISTED_OVERVIEW = "%1$d persons listed!"; - + public static final String MESSAGE_INVALID_PERSON_SEARCHED = "This staff does not exist in the address book!"; + public static final String MESSAGE_INVALID_DATE_PARSED = "This date cannot be parsed, expected: YYYY-MM-DD"; + public static final String MESSAGE_INVALID_SHIFT_TIME = "The shift time provided is invalid, expected: HH:MM-HH:MM"; + public static final String MESSAGE_INVALID_TIME = "The format or range of input time is invalid"; + public static final String WRONG_NUMBER_OF_DATES = "Expects 0, 1 or 2 dates as input, received $d"; + public static final String DATES_IN_WRONG_ORDER = "The inputted end date must be after the start date."; + public static final String FILE_NOT_FOUND = "The following file could not be accessed: "; + public static final String SHIFT_PERIOD_PARSING_DEFAULT = "When date input is provided, it should be in " + + "chronological order. If " + + "only one date is provided, the end date is assumed to be seven days later. If no date is provided, " + + "the period is assumed to be from the current date to seven days later. Expects 0, 1 or 2 date inputs."; + public static final String DATE_RANGE_INPUT = "[" + PREFIX_DATE + "START_DATE]" + + " [" + PREFIX_DATE + "END_DATE]"; } diff --git a/src/main/java/seedu/address/commons/core/Version.java b/src/main/java/seedu/address/commons/core/Version.java index 12142ec1e32..4db1b82365c 100644 --- a/src/main/java/seedu/address/commons/core/Version.java +++ b/src/main/java/seedu/address/commons/core/Version.java @@ -50,6 +50,7 @@ public boolean isEarlyAccess() { /** * Parses a version number string in the format V1.2.3. + * * @param versionString version number string * @return a Version object */ diff --git a/src/main/java/seedu/address/commons/exceptions/InvalidShiftTimeException.java b/src/main/java/seedu/address/commons/exceptions/InvalidShiftTimeException.java new file mode 100644 index 00000000000..c2aad3fac83 --- /dev/null +++ b/src/main/java/seedu/address/commons/exceptions/InvalidShiftTimeException.java @@ -0,0 +1,7 @@ +package seedu.address.commons.exceptions; + +public class InvalidShiftTimeException extends Exception { + public InvalidShiftTimeException() { + super("The input start time and end time is invalid for the target shift"); + } +} diff --git a/src/main/java/seedu/address/commons/util/CollectionUtil.java b/src/main/java/seedu/address/commons/util/CollectionUtil.java index eafe4dfd681..942edda97d7 100644 --- a/src/main/java/seedu/address/commons/util/CollectionUtil.java +++ b/src/main/java/seedu/address/commons/util/CollectionUtil.java @@ -12,7 +12,9 @@ */ public class CollectionUtil { - /** @see #requireAllNonNull(Collection) */ + /** + * @see #requireAllNonNull(Collection) + */ public static void requireAllNonNull(Object... items) { requireNonNull(items); Stream.of(items).forEach(Objects::requireNonNull); diff --git a/src/main/java/seedu/address/commons/util/DateTimeUtil.java b/src/main/java/seedu/address/commons/util/DateTimeUtil.java new file mode 100644 index 00000000000..ca195721ca6 --- /dev/null +++ b/src/main/java/seedu/address/commons/util/DateTimeUtil.java @@ -0,0 +1,104 @@ +package seedu.address.commons.util; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; + +import seedu.address.model.person.Period; + +/** + * Utility methods related to Times + */ +public class DateTimeUtil { + + public static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm"); + private static LocalTime defaultMorningStartTime = LocalTime.parse("10:00", TIME_FORMATTER); + private static LocalTime defaultMorningEndTime = LocalTime.parse("16:00", TIME_FORMATTER); + private static LocalTime defaultAfternoonStartTime = LocalTime.parse("16:00", TIME_FORMATTER); + private static LocalTime defaultAfternoonEndTime = LocalTime.parse("22:00", TIME_FORMATTER); + private static Period displayedPeriod = Period.oneWeekFrom(LocalDate.now()); + + /** + * Tests whether a string can be parsed to a time. + * @param str The input string. + * @return Whether a string can be parsed to a time. + */ + public static boolean isValidTime(String str) { + try { + LocalTime.parse(str, TIME_FORMATTER); + } catch (DateTimeParseException dateTimeParseException) { + return false; + } + return true; + } + + /** + * Update the default timing fields based on a LocalTime[] of new Timings. + * + * @param newTimings a LocalTime[] containing the new timings. + */ + public static void updateTimings(LocalTime[] newTimings) { + defaultMorningStartTime = newTimings[0]; + defaultMorningEndTime = newTimings[1]; + defaultAfternoonStartTime = newTimings[2]; + defaultAfternoonEndTime = newTimings[3]; + } + + /** + * Update the Period that represents the week that is displayed on the GUI. + * + * @param newPeriod new Period being displayed on the GUI. + */ + public static void updateDisplayedPeriod(Period newPeriod) { + displayedPeriod = newPeriod; + } + + /** + * Returns the default morning shift start time. + * @return default morning shift start time. + */ + public static LocalTime getDefaultMorningStartTime() { + return defaultMorningStartTime; + } + + /** + * Returns the default morning shift end time. + * @return default morning shift end time. + */ + public static LocalTime getDefaultMorningEndTime() { + return defaultMorningEndTime; + } + + /** + * Returns the default afternoon shift start time. + * @return default afternoon shift start time. + */ + public static LocalTime getDefaultAfternoonStartTime() { + return defaultAfternoonStartTime; + } + + /** + * Returns the default afternoon shift end time. + * @return default afternoon shift end time. + */ + public static LocalTime getDefaultAfternoonEndTime() { + return defaultAfternoonEndTime; + } + + /** + * Creates an array of {@code LocalDate} of size 2, that includes the start and end date + * from {@code displayedPeriod}. + * + */ + public static LocalDate[] getDisplayedDateArray() { + return new LocalDate[]{displayedPeriod.getStartDate(), displayedPeriod.getStartDate().plusDays(6)}; + } + + /** + * Gets the current period being displayed. + */ + public static Period getDisplayedPeriod() { + return displayedPeriod; + } +} diff --git a/src/main/java/seedu/address/commons/util/FileUtil.java b/src/main/java/seedu/address/commons/util/FileUtil.java index b1e2767cdd9..f811215474c 100644 --- a/src/main/java/seedu/address/commons/util/FileUtil.java +++ b/src/main/java/seedu/address/commons/util/FileUtil.java @@ -20,6 +20,7 @@ public static boolean isFileExists(Path file) { /** * Returns true if {@code path} can be converted into a {@code Path} via {@link Paths#get(String)}, * otherwise returns false. + * * @param path A string representing the file path. Cannot be null. */ public static boolean isValidPath(String path) { @@ -33,6 +34,7 @@ public static boolean isValidPath(String path) { /** * Creates a file if it does not exist along with its missing parent directories. + * * @throws IOException if the file or directory cannot be created. */ public static void createIfMissing(Path file) throws IOException { diff --git a/src/main/java/seedu/address/commons/util/JsonUtil.java b/src/main/java/seedu/address/commons/util/JsonUtil.java index 8ef609f055d..8d1143d5ce0 100644 --- a/src/main/java/seedu/address/commons/util/JsonUtil.java +++ b/src/main/java/seedu/address/commons/util/JsonUtil.java @@ -51,7 +51,8 @@ static T deserializeObjectFromJsonFile(Path jsonFile, Class classOfObject /** * Returns the Json object from the given file or {@code Optional.empty()} object if the file is not found. * If any values are missing from the file, default values will be used, as long as the file is a valid json file. - * @param filePath cannot be null. + * + * @param filePath cannot be null. * @param classOfObjectToDeserialize Json file has to correspond to the structure in the class given here. * @throws DataConversionException if the file format is not as expected. */ @@ -79,6 +80,7 @@ public static Optional readJsonFile( /** * Saves the Json object to the specified file. * Overwrites existing file if it exists, creates a new file if it doesn't. + * * @param jsonFile cannot be null * @param filePath cannot be null * @throws IOException if there was an error during writing to the file @@ -93,6 +95,7 @@ public static void saveJsonFile(T jsonFile, Path filePath) throws IOExceptio /** * Converts a given string representation of a JSON data to instance of a class + * * @param The generic type to create an instance of * @return The instance of T with the specified values in the JSON string */ @@ -102,8 +105,9 @@ public static T fromJsonString(String json, Class instanceClass) throws I /** * Converts a given instance of a class into its JSON data string representation + * * @param instance The T object to be converted into the JSON string - * @param The generic type to create an instance of + * @param The generic type to create an instance of * @return JSON data representation of the given class instance, in string */ public static String toJsonString(T instance) throws JsonProcessingException { @@ -128,7 +132,6 @@ protected Level _deserialize(String value, DeserializationContext ctxt) { * Gets the logging level that matches loggingLevelString *

                            * Returns null if there are no matches - * */ private Level getLoggingLevel(String loggingLevelString) { return Level.parse(loggingLevelString); diff --git a/src/main/java/seedu/address/commons/util/StringUtil.java b/src/main/java/seedu/address/commons/util/StringUtil.java index 61cc8c9a1cb..edacbf58bba 100644 --- a/src/main/java/seedu/address/commons/util/StringUtil.java +++ b/src/main/java/seedu/address/commons/util/StringUtil.java @@ -14,14 +14,15 @@ public class StringUtil { /** * Returns true if the {@code sentence} contains the {@code word}. - * Ignores case, but a full word match is required. - *
                            examples:

                            +     * Ignores case, but a full word match is required.
                            +     * 
                            examples:
                                  *       containsWordIgnoreCase("ABc def", "abc") == true
                                  *       containsWordIgnoreCase("ABc def", "DEF") == true
                                  *       containsWordIgnoreCase("ABc def", "AB") == false //not a full word match
                                  *       
                            + * * @param sentence cannot be null - * @param word cannot be null, cannot be empty, must be a single word + * @param word cannot be null, cannot be empty, must be a single word */ public static boolean containsWordIgnoreCase(String sentence, String word) { requireNonNull(sentence); @@ -53,6 +54,7 @@ public static String getDetails(Throwable t) { * e.g. 1, 2, 3, ..., {@code Integer.MAX_VALUE}
                            * Will return false for any other non-null string input * e.g. empty string, "-1", "0", "+1", and " 2 " (untrimmed), "3 0" (contains whitespace), "1 a" (contains letters) + * * @throws NullPointerException if {@code s} is null. */ public static boolean isNonZeroUnsignedInteger(String s) { diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java index 92cd8fa605a..ec60af76e34 100644 --- a/src/main/java/seedu/address/logic/Logic.java +++ b/src/main/java/seedu/address/logic/Logic.java @@ -16,10 +16,11 @@ public interface Logic { /** * Executes the command and returns the result. + * * @param commandText The command as entered by the user. * @return the result of the command execution. * @throws CommandException If an error occurs during command execution. - * @throws ParseException If an error occurs during parsing. + * @throws ParseException If an error occurs during parsing. */ CommandResult execute(String commandText) throws CommandException, ParseException; @@ -30,7 +31,9 @@ public interface Logic { */ ReadOnlyAddressBook getAddressBook(); - /** Returns an unmodifiable view of the filtered list of persons */ + /** + * Returns an unmodifiable view of the filtered list of persons + */ ObservableList getFilteredPersonList(); /** diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddCommand.java index 71656d7c5c8..5d91aeb9722 100644 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ b/src/main/java/seedu/address/logic/commands/AddCommand.java @@ -1,10 +1,12 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ROLE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SALARY; +import static seedu.address.logic.parser.CliSyntax.PREFIX_STATUS; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; import seedu.address.logic.commands.exceptions.CommandException; @@ -18,18 +20,23 @@ public class AddCommand extends Command { public static final String COMMAND_WORD = "add"; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a person to the address book. " - + "Parameters: " + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a person to the address book.\n\n" + + "Parameters:\n" + PREFIX_NAME + "NAME " + PREFIX_PHONE + "PHONE " + PREFIX_EMAIL + "EMAIL " - + PREFIX_ADDRESS + "ADDRESS " - + "[" + PREFIX_TAG + "TAG]...\n" - + "Example: " + COMMAND_WORD + " " + + PREFIX_SALARY + "SALARY " + + "[" + PREFIX_STATUS + "STATUS] " + + "[" + PREFIX_ROLE + "ROLE]... " + + "[" + PREFIX_TAG + "TAG]...\n\n" + + "Example:\n" + COMMAND_WORD + " " + PREFIX_NAME + "John Doe " + PREFIX_PHONE + "98765432 " + PREFIX_EMAIL + "johnd@example.com " - + PREFIX_ADDRESS + "311, Clementi Ave 2, #02-25 " + + PREFIX_SALARY + "100 " + + PREFIX_ROLE + "bartender " + + PREFIX_STATUS + "fulltime " + + PREFIX_ROLE + "kitchen " + PREFIX_TAG + "friends " + PREFIX_TAG + "owesMoney"; @@ -41,9 +48,9 @@ public class AddCommand extends Command { /** * Creates an AddCommand to add the specified {@code Person} */ - public AddCommand(Person person) { - requireNonNull(person); - toAdd = person; + public AddCommand(Person staff) { + requireNonNull(staff); + toAdd = staff; } @Override diff --git a/src/main/java/seedu/address/logic/commands/AddShiftCommand.java b/src/main/java/seedu/address/logic/commands/AddShiftCommand.java new file mode 100644 index 00000000000..eb3dbf83feb --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AddShiftCommand.java @@ -0,0 +1,135 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.DATE_RANGE_INPUT; +import static seedu.address.commons.core.Messages.SHIFT_PERIOD_PARSING_DEFAULT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_INDEX; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DAY_SHIFT; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.util.List; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Name; +import seedu.address.model.person.Person; +import seedu.address.model.person.Slot; +import seedu.address.model.person.exceptions.DuplicateShiftException; + +/** + * Adds a shift to a staff's schedule. + */ +public class AddShiftCommand extends Command { + + public static final String COMMAND_WORD = "addShift"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a shift to the staff identified " + + "by the index number used in the displayed staff list or the name of staff. the period over" + + "which the shift is active over is optional. Date inputs are used to indicate the period of" + + "the shift to add and they are optional." + + SHIFT_PERIOD_PARSING_DEFAULT + "\n\n" + + "Parameters:\n" + + PREFIX_DASH_INDEX + " INDEX or " + + PREFIX_DASH_NAME + " NAME " + + PREFIX_DAY_SHIFT + "DAY_AND_SLOT " + + DATE_RANGE_INPUT + "\n\n" + + "Example:\n" + COMMAND_WORD + " " + + PREFIX_DASH_INDEX + " 1 " + + PREFIX_DAY_SHIFT + "monday-1\n" + + COMMAND_WORD + " " + + PREFIX_DASH_NAME + " JOE " + + PREFIX_DAY_SHIFT + "TUESDAY-0 " + PREFIX_DATE + "2020-01-01"; + + public static final String MESSAGE_ADD_SHIFT_SUCCESS = "New shift added to the schedule of %s: %s, %s."; + public static final String MESSAGE_DUPLICATE_SHIFT = "This shift already exists in the staff's schedule."; + + private final Index index; + private final Name name; + private final DayOfWeek dayOfWeek; + private final Slot slot; + private final LocalDate startDate; + private final LocalDate endDate; + + /** + * Creates an AddShiftCommand to add the specified {@code Shift} to a {@code Person}. + */ + public AddShiftCommand(Index index, Name name, String shiftDateAndSlot, LocalDate startDate, LocalDate endDate) { + requireNonNull(shiftDateAndSlot); + this.index = index; + this.name = name; + String[] strings = shiftDateAndSlot.split("-"); + dayOfWeek = DayOfWeek.valueOf(strings[0].toUpperCase()); + slot = Slot.getSlotByOrder(strings[1]); + this.startDate = startDate; + this.endDate = endDate; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + //check if the input dates contain the dayOfWeek + CommandUtil.checkDateForDayOfWeek(startDate, endDate, dayOfWeek); + + + List lastShownList = model.getFilteredPersonList(); + + Person staffToEdit; + + if (index != null) { + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + staffToEdit = lastShownList.get(index.getZeroBased()); + } else { + if (name != null) { + staffToEdit = model.findPersonByName(name); + } else { + throw new CommandException(MESSAGE_USAGE); + } + } + + if (staffToEdit == null || !model.hasPerson(staffToEdit)) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_SEARCHED); + } + + try { + model.addShift(staffToEdit, dayOfWeek, slot, startDate, endDate); + } catch (DuplicateShiftException de) { + throw new CommandException(MESSAGE_DUPLICATE_SHIFT); + } + + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + return new CommandResult(String.format(MESSAGE_ADD_SHIFT_SUCCESS, staffToEdit.getName(), dayOfWeek, slot)); + + } + + + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof AddShiftCommand)) { + return false; + } + + // state check + AddShiftCommand command = (AddShiftCommand) other; + return ((index == null && command.index == null) || (index != null && index.equals(command.index))) + && ((name == null && command.name == null) || (name != null && name.equals(command.name))) + && dayOfWeek.equals(command.dayOfWeek) + && slot.equals(command.slot) + && startDate.equals(command.startDate) + && endDate.equals(command.endDate); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/address/logic/commands/ClearCommand.java index 9c86b1fa6e4..85fed5e81f4 100644 --- a/src/main/java/seedu/address/logic/commands/ClearCommand.java +++ b/src/main/java/seedu/address/logic/commands/ClearCommand.java @@ -2,8 +2,13 @@ import static java.util.Objects.requireNonNull; +import java.io.IOException; + +import seedu.address.commons.core.Messages; +import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.AddressBook; import seedu.address.model.Model; +import seedu.address.storage.RoleReqStorage; /** * Clears the address book. @@ -15,9 +20,16 @@ public class ClearCommand extends Command { @Override - public CommandResult execute(Model model) { + public CommandResult execute(Model model) throws CommandException { requireNonNull(model); model.setAddressBook(new AddressBook()); + + try { + RoleReqStorage.reset(); + } catch (IOException e) { + throw new CommandException(Messages.FILE_NOT_FOUND + "\n" + RoleReqStorage.FILEPATH); + } + return new CommandResult(MESSAGE_SUCCESS); } } diff --git a/src/main/java/seedu/address/logic/commands/CommandResult.java b/src/main/java/seedu/address/logic/commands/CommandResult.java index 92f900b7916..22df9927c65 100644 --- a/src/main/java/seedu/address/logic/commands/CommandResult.java +++ b/src/main/java/seedu/address/logic/commands/CommandResult.java @@ -4,6 +4,8 @@ import java.util.Objects; +import seedu.address.model.person.Period; + /** * Represents the result of a command execution. */ @@ -11,19 +13,34 @@ public class CommandResult { private final String feedbackToUser; - /** Help information should be shown to the user. */ + /** + * Help information should be shown to the user. + */ private final boolean showHelp; - /** The application should exit. */ + /** + * The application should exit. + */ private final boolean exit; + /** + * The application should switch tabs. + */ + private final boolean switchTab; + + private boolean changeSchedule; + + private Period period; + /** * Constructs a {@code CommandResult} with the specified fields. */ - public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) { + public CommandResult(String feedbackToUser, boolean showHelp, boolean exit, boolean switchTab) { this.feedbackToUser = requireNonNull(feedbackToUser); this.showHelp = showHelp; this.exit = exit; + this.switchTab = switchTab; + this.changeSchedule = false; } /** @@ -31,9 +48,21 @@ public CommandResult(String feedbackToUser, boolean showHelp, boolean exit) { * and other fields set to their default value. */ public CommandResult(String feedbackToUser) { - this(feedbackToUser, false, false); + this(feedbackToUser, false, false, false); } + + /** + * Constructs a {@code CommandResult} with the specified {@code feedbackToUser} + * and {@code Period} to set the schedule to. + */ + public CommandResult(String feedbackToUser, Period period) { + this(feedbackToUser, false, false, false); + this.changeSchedule = true; + this.period = period; + } + + public String getFeedbackToUser() { return feedbackToUser; } @@ -46,6 +75,20 @@ public boolean isExit() { return exit; } + public boolean isSwitchTab() { + return switchTab; + } + + public boolean isChangeSchedule() { + return changeSchedule; + } + + public Period getPeriod() { + assert period != null; + return period; + } + + @Override public boolean equals(Object other) { if (other == this) { @@ -60,12 +103,13 @@ public boolean equals(Object other) { CommandResult otherCommandResult = (CommandResult) other; return feedbackToUser.equals(otherCommandResult.feedbackToUser) && showHelp == otherCommandResult.showHelp - && exit == otherCommandResult.exit; + && exit == otherCommandResult.exit + && switchTab == otherCommandResult.switchTab; } @Override public int hashCode() { - return Objects.hash(feedbackToUser, showHelp, exit); + return Objects.hash(feedbackToUser, showHelp, exit, switchTab); } } diff --git a/src/main/java/seedu/address/logic/commands/CommandUtil.java b/src/main/java/seedu/address/logic/commands/CommandUtil.java new file mode 100644 index 00000000000..a44e9253719 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/CommandUtil.java @@ -0,0 +1,46 @@ +package seedu.address.logic.commands; + +import java.time.DayOfWeek; +import java.time.LocalDate; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.person.Period; + +public abstract class CommandUtil { + + public static final String MESSAGE_SHIFT_NOT_IN_RANGE = "The date range provided does not contain the shift."; + + /** + * Throws a {@code CommandException} if the {@code DayOfWeek} is not found in the date range provided. + * + * @param startDate The start date of the range. + * @param endDate The end date of the range. + * @param day The day to find. + * @throws CommandException The exception thrown. + */ + public static void checkDateForDayOfWeek(LocalDate startDate, + LocalDate endDate, DayOfWeek day) throws CommandException { + Period period = new Period(startDate, endDate); + checkDateForDayOfWeek(period, day); + } + + /** + * Throws a {@code CommandException} if the {@code DayOfWeek} is not in the input {@code Period}. + * + * @param period The period to look through. + * @param day The day to find. + * @throws CommandException The exception to throw. + */ + public static void checkDateForDayOfWeek(Period period, DayOfWeek day) throws CommandException { + if (!isDayInPeriod(period, day)) { + throw new CommandException(MESSAGE_SHIFT_NOT_IN_RANGE); + } + } + + private static boolean isDayInPeriod(Period period, DayOfWeek day) { + long result = period.toList().stream() + .filter(d -> d.getDayOfWeek().equals(day)) + .count(); + return result != 0; + } +} diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java index 02fd256acba..f3c027329af 100644 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ b/src/main/java/seedu/address/logic/commands/DeleteCommand.java @@ -1,53 +1,173 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_INDEX; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_ROLE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_STATUS; +import java.util.ArrayList; import java.util.List; +import java.util.Objects; import seedu.address.commons.core.Messages; import seedu.address.commons.core.index.Index; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; +import seedu.address.model.person.Name; import seedu.address.model.person.Person; +import seedu.address.model.person.Role; +import seedu.address.model.person.Status; /** - * Deletes a person identified using it's displayed index from the address book. + * Deletes a person identified using it's displayed index or name from the address book. */ public class DeleteCommand extends Command { public static final String COMMAND_WORD = "delete"; + public static final int INVALID_INDEX = -1; 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"; + + ": Deletes the person(s) identified " + + "by the index number used in the displayed person list or by their name " + + "or by their status or role.\n\n" + + "Parameters:\n" + + PREFIX_DASH_INDEX + " INDEX\n" + + PREFIX_DASH_NAME + " NAME\n" + + PREFIX_DASH_ROLE + " ROLE\n" + + PREFIX_DASH_STATUS + " STATUS\n\n" + + "Examples:\n" + COMMAND_WORD + " " + + PREFIX_DASH_INDEX + " 1\n" + + COMMAND_WORD + " " + PREFIX_DASH_NAME + " Alex Yeoh\n" + + COMMAND_WORD + " " + PREFIX_DASH_ROLE + " kitchen\n" + + COMMAND_WORD + " " + PREFIX_DASH_STATUS + " parttime"; public static final String MESSAGE_DELETE_PERSON_SUCCESS = "Deleted Person: %1$s"; + public static final String MESSAGE_DELETE_PEOPLE_SUCCESS = "Deleted these people:\n\n"; - private final Index targetIndex; + private Index targetIndex = null; + private Name name = null; + private Role role = null; + private Status status = null; + /** + * Constructs delete command using a targetIndex. + * + * @param targetIndex The index of a person to be deleted + */ public DeleteCommand(Index targetIndex) { this.targetIndex = targetIndex; } + /** + * Constructs delete command using a name. + * + * @param name The name of a person to be deleted + */ + public DeleteCommand(Name name) { + this.name = name; + } + + /** + * Constructs delete command using a role. + * + * @param role The role of the people to be deleted + */ + public DeleteCommand(Role role) { + this.role = role; + } + + /** + * Constructs delete command using a status. + * + * @param status The status of the people to be deleted + */ + public DeleteCommand(Status status) { + this.status = status; + } + @Override public CommandResult execute(Model model) throws CommandException { requireNonNull(model); List lastShownList = model.getFilteredPersonList(); + List staffs = new ArrayList<>(model.getUnFilteredPersonList()); + if (role != null) { + return executeBasedOnRole(model, staffs); + } else if (status != null) { + return executeBasedOnStatus(model, staffs); + } else if (targetIndex != null) { + return executeBasedOnTargetIndex(model, lastShownList); + } else { + return executeBasedOnName(model, lastShownList); + } + } + + private CommandResult executeBasedOnRole(Model model, List staffs) { + StringBuilder deletedPeople = new StringBuilder(MESSAGE_DELETE_PEOPLE_SUCCESS); + for (Person staff : staffs) { + if (staff.getRoles().contains(role)) { + model.deletePerson(staff); + deletedPeople.append(staff).append("\n\n"); + } + } + return new CommandResult(deletedPeople.toString()); + } + + private CommandResult executeBasedOnStatus(Model model, List staffs) { + StringBuilder deletedPeople = new StringBuilder(MESSAGE_DELETE_PEOPLE_SUCCESS); + for (Person staff : staffs) { + if (staff.getStatus() == status) { + model.deletePerson(staff); + deletedPeople.append(staff).append("\n\n"); + } + } + return new CommandResult(deletedPeople.toString()); + } + + private CommandResult executeBasedOnTargetIndex(Model model, List lastShownList) + throws CommandException { if (targetIndex.getZeroBased() >= lastShownList.size()) { throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); } - Person personToDelete = lastShownList.get(targetIndex.getZeroBased()); - model.deletePerson(personToDelete); - return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, personToDelete)); + Person staffToDelete = lastShownList.get(targetIndex.getZeroBased()); + model.deletePerson(staffToDelete); + return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, staffToDelete)); + } + + private CommandResult executeBasedOnName(Model model, List lastShownList) + throws CommandException { + int index = getIndexByName(name, lastShownList); + if (index == INVALID_INDEX) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_SEARCHED); + } + + Person staffToDelete = lastShownList.get(index); + model.deletePerson(staffToDelete); + return new CommandResult(String.format(MESSAGE_DELETE_PERSON_SUCCESS, staffToDelete)); + } + + + private int getIndexByName(Name name, List lastShownList) { + for (int i = 0; i < lastShownList.size(); i++) { + if (lastShownList.get(i).getName().equals(name)) { + return i; + } + } + return INVALID_INDEX; } @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 + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + DeleteCommand that = (DeleteCommand) o; + return Objects.equals(targetIndex, that.targetIndex) && Objects.equals(name, that.name) + && role == that.role && status == that.status; } } diff --git a/src/main/java/seedu/address/logic/commands/DeleteShiftCommand.java b/src/main/java/seedu/address/logic/commands/DeleteShiftCommand.java new file mode 100644 index 00000000000..719747a6a08 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/DeleteShiftCommand.java @@ -0,0 +1,123 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.DATE_RANGE_INPUT; +import static seedu.address.commons.core.Messages.SHIFT_PERIOD_PARSING_DEFAULT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_INDEX; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DAY_SHIFT; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.util.List; +import java.util.Objects; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Name; +import seedu.address.model.person.Person; +import seedu.address.model.person.Slot; +import seedu.address.model.person.exceptions.NoShiftException; + +/** + * Deletes a shift from a staff's schedule. + */ +public class DeleteShiftCommand extends Command { + public static final String COMMAND_WORD = "deleteShift"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": deletes a shift from the staff identified " + + "by the index number used in the displayed staff list or the name of staff. Date input is used " + + "to indicate the duration of the shift to delete. " + + SHIFT_PERIOD_PARSING_DEFAULT + "\n\n" + + "Parameters:\n" + + "[" + PREFIX_DASH_INDEX + " INDEX] or " + + "[" + PREFIX_DASH_NAME + " NAME] " + + PREFIX_DAY_SHIFT + "DAY_AND_SLOT " + + DATE_RANGE_INPUT + + "\n\n" + + "Examples:\n" + COMMAND_WORD + " " + + PREFIX_DASH_INDEX + " 1 " + + PREFIX_DAY_SHIFT + "monday-1" + "\n" + + COMMAND_WORD + " " + + PREFIX_DASH_NAME + " Alex Yeoh " + + PREFIX_DAY_SHIFT + "TUESDAY-0" + + PREFIX_DATE + "2021-01-01" + " " + + PREFIX_DATE + "2021-01-05"; + + public static final String MESSAGE_DELETE_SHIFT_SUCCESS = "Shift deleted from the schedule of %s: %s, %s."; + public static final String MESSAGE_SHIFT_DOESNT_EXIST = "The shift that you are trying to delete does not exist!"; + + private final Index index; + private final Name name; + private final DayOfWeek dayOfWeek; + private final Slot slot; + private final LocalDate startDate; + private final LocalDate endDate; + + /** + * Creates a DeleteShiftCommand to add the specified {@code Shift} to a {@code Person}. + */ + public DeleteShiftCommand(Index index, Name name, String shiftDateAndSlot, LocalDate startDate, + LocalDate endDate) { + requireNonNull(shiftDateAndSlot); + this.index = index; + this.name = name; + String[] strings = shiftDateAndSlot.split("-"); + dayOfWeek = DayOfWeek.valueOf(strings[0].toUpperCase()); + slot = Slot.getSlotByOrder(strings[1]); + this.endDate = endDate; + this.startDate = startDate; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + CommandUtil.checkDateForDayOfWeek(startDate, endDate, dayOfWeek); + List lastShownList = model.getFilteredPersonList(); + + Person staffToEdit; + + if (index != null) { + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + staffToEdit = lastShownList.get(index.getZeroBased()); + } else { + if (name != null) { + staffToEdit = model.findPersonByName(name); + } else { + throw new CommandException(MESSAGE_USAGE); + } + } + + if (staffToEdit == null || !model.hasPerson(staffToEdit)) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_SEARCHED); + } + + try { + model.deleteShift(staffToEdit, dayOfWeek, slot, startDate, endDate); + } catch (NoShiftException e) { + throw new CommandException(MESSAGE_SHIFT_DOESNT_EXIST); + } + + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + return new CommandResult(String.format(MESSAGE_DELETE_SHIFT_SUCCESS, staffToEdit.getName(), dayOfWeek, slot)); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof DeleteShiftCommand)) { + return false; + } + DeleteShiftCommand that = (DeleteShiftCommand) o; + return Objects.equals(index, that.index) && Objects.equals(name, that.name) && dayOfWeek == that.dayOfWeek + && slot == that.slot && startDate.equals(that.startDate) && endDate.equals(that.endDate); + } +} diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java index 7e36114902f..b66305cbc62 100644 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ b/src/main/java/seedu/address/logic/commands/EditCommand.java @@ -1,10 +1,14 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_INDEX; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_NAME; 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_ROLE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SALARY; +import static seedu.address.logic.parser.CliSyntax.PREFIX_STATUS; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; @@ -19,87 +23,164 @@ import seedu.address.commons.util.CollectionUtil; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; -import seedu.address.model.person.Address; import seedu.address.model.person.Email; import seedu.address.model.person.Name; +import seedu.address.model.person.Period; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.Role; +import seedu.address.model.person.Salary; +import seedu.address.model.person.Schedule; +import seedu.address.model.person.Status; import seedu.address.model.tag.Tag; + /** * Edits the details of an existing person in the address book. */ public class EditCommand extends Command { + private enum Identifier { + INDEX, NAME + } + public static final String COMMAND_WORD = "edit"; public static final String MESSAGE_USAGE = COMMAND_WORD + ": Edits the details of the person identified " - + "by the index number used in the displayed person list. " - + "Existing values will be overwritten by the input values.\n" - + "Parameters: INDEX (must be a positive integer) " + + "by the index number used in the displayed person list or by the name identifier.\n" + + "Existing values will be overwritten by the input values.\n\n" + + "Parameters:\n" + + PREFIX_DASH_INDEX + " INDEX or " + + PREFIX_DASH_NAME + " NAME " + "[" + PREFIX_NAME + "NAME] " + "[" + PREFIX_PHONE + "PHONE] " + "[" + PREFIX_EMAIL + "EMAIL] " - + "[" + PREFIX_ADDRESS + "ADDRESS] " - + "[" + PREFIX_TAG + "TAG]...\n" - + "Example: " + COMMAND_WORD + " 1 " + + "[" + PREFIX_SALARY + "SALARY] " + + "[" + PREFIX_STATUS + "STATUS] " + + "[" + PREFIX_ROLE + "ROLE]... " + + "[" + PREFIX_TAG + "TAG]...\n\n" + + "Examples:\n" + COMMAND_WORD + " " + + PREFIX_DASH_INDEX + " 1 " + + PREFIX_PHONE + "91234567 " + + PREFIX_EMAIL + "johndoe@example.com\n" + + COMMAND_WORD + " " + + PREFIX_DASH_NAME + " john " + PREFIX_PHONE + "91234567 " - + PREFIX_EMAIL + "johndoe@example.com"; + + PREFIX_EMAIL + "johndoe@example.com\n"; public static final String MESSAGE_EDIT_PERSON_SUCCESS = "Edited Person: %1$s"; public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book."; - private final Index index; - private final EditPersonDescriptor editPersonDescriptor; + private Index index; + private Name name; + private final Identifier identifier; + private final EditPersonDescriptor editStaffDescriptor; /** * @param index of the person in the filtered person list to edit - * @param editPersonDescriptor details to edit the person with + * @param editStaffDescriptor details to edit the person with */ - public EditCommand(Index index, EditPersonDescriptor editPersonDescriptor) { + public EditCommand(Index index, EditPersonDescriptor editStaffDescriptor) { requireNonNull(index); - requireNonNull(editPersonDescriptor); + requireNonNull(editStaffDescriptor); this.index = index; - this.editPersonDescriptor = new EditPersonDescriptor(editPersonDescriptor); + this.editStaffDescriptor = new EditPersonDescriptor(editStaffDescriptor); + this.identifier = Identifier.INDEX; + } + + /** + * @param name of the person in the staffd database to edit. + * @param editStaffDescriptor details to edit the person with + */ + public EditCommand(Name name, EditPersonDescriptor editStaffDescriptor) { + requireNonNull(name); + requireNonNull(editStaffDescriptor); + + this.editStaffDescriptor = editStaffDescriptor; + this.name = name; + this.identifier = Identifier.NAME; } @Override public CommandResult execute(Model model) throws CommandException { - requireNonNull(model); - List lastShownList = model.getFilteredPersonList(); + switch(this.identifier) { + case INDEX: + return editBasedOnIndex(model); + case NAME: + return editBasedOnName(model); + default: + throw new CommandException(Messages.MESSAGE_INVALID_COMMAND_FORMAT); - if (index.getZeroBased() >= lastShownList.size()) { - throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); } - Person personToEdit = lastShownList.get(index.getZeroBased()); - Person editedPerson = createEditedPerson(personToEdit, editPersonDescriptor); + } - if (!personToEdit.isSamePerson(editedPerson) && model.hasPerson(editedPerson)) { + private CommandResult editBasedOnName(Model model) throws CommandException { + requireNonNull(model); + List underlyingList = model.getUnFilteredPersonList(); + Optional person = underlyingList + .stream() + .filter(staff -> staff.getName().equals(this.name)) + .findFirst(); + if (!person.isPresent()) { + //if the person is not in the list + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_SEARCHED); + } + Person staffToEdit = person.get(); + return editStaffOnModel(model, staffToEdit); + + + } + + private CommandResult editStaffOnModel(Model model, Person staffToEdit) throws CommandException { + Person editedStaff = createEditedPerson(staffToEdit, editStaffDescriptor); + if (!staffToEdit.isSamePerson(editedStaff) && model.hasPerson(editedStaff)) { throw new CommandException(MESSAGE_DUPLICATE_PERSON); } - model.setPerson(personToEdit, editedPerson); + model.setPerson(staffToEdit, editedStaff); model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); - return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, editedPerson)); + return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, editedStaff)); + } + + private CommandResult editBasedOnIndex(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredPersonList(); + + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Person staffToEdit = lastShownList.get(index.getZeroBased()); + return editStaffOnModel(model, staffToEdit); } /** * Creates and returns a {@code Person} with the details of {@code personToEdit} * edited with {@code editPersonDescriptor}. */ - private static Person createEditedPerson(Person personToEdit, EditPersonDescriptor editPersonDescriptor) { - assert personToEdit != null; - - Name updatedName = editPersonDescriptor.getName().orElse(personToEdit.getName()); - Phone updatedPhone = editPersonDescriptor.getPhone().orElse(personToEdit.getPhone()); - Email updatedEmail = editPersonDescriptor.getEmail().orElse(personToEdit.getEmail()); - Address updatedAddress = editPersonDescriptor.getAddress().orElse(personToEdit.getAddress()); - Set updatedTags = editPersonDescriptor.getTags().orElse(personToEdit.getTags()); - - return new Person(updatedName, updatedPhone, updatedEmail, updatedAddress, updatedTags); + private static Person createEditedPerson(Person staffToEdit, EditPersonDescriptor editPersonDescriptor) { + assert staffToEdit != null; + + Name updatedName = editPersonDescriptor.getName().orElse(staffToEdit.getName()); + Phone updatedPhone = editPersonDescriptor.getPhone().orElse(staffToEdit.getPhone()); + Email updatedEmail = editPersonDescriptor.getEmail().orElse(staffToEdit.getEmail()); + Set updatedRoles = editPersonDescriptor.getRoles().orElse(staffToEdit.getRoles()); + Salary updatedSalary = editPersonDescriptor.getSalary().orElse(staffToEdit.getSalary()); + Status updatedStatus = editPersonDescriptor.getStatus().orElse(staffToEdit.getStatus()); + Set updatedTags = editPersonDescriptor.getTags().orElse(staffToEdit.getTags()); + //currently do not allow modifications to period via edit person descriptor + //exception would be during tests. + Set updatedPeriod = editPersonDescriptor.getPeriod().orElse(staffToEdit.getAbsentDates()); + Schedule sameSchedule = staffToEdit.getSchedule(); + + + Person updatedPerson = new Person(updatedName, updatedPhone, updatedEmail, updatedRoles, + updatedSalary, updatedStatus, updatedTags, updatedPeriod); + updatedPerson.setSchedule(sameSchedule); + return updatedPerson; } @Override @@ -117,7 +198,7 @@ public boolean equals(Object other) { // state check EditCommand e = (EditCommand) other; return index.equals(e.index) - && editPersonDescriptor.equals(e.editPersonDescriptor); + && editStaffDescriptor.equals(e.editStaffDescriptor); } /** @@ -128,10 +209,15 @@ public static class EditPersonDescriptor { private Name name; private Phone phone; private Email email; - private Address address; + private Set roles; + private Salary salary; + private Status status; private Set tags; + private Set absentPeriods; + private Schedule schedule; - public EditPersonDescriptor() {} + public EditPersonDescriptor() { + } /** * Copy constructor. @@ -141,15 +227,19 @@ public EditPersonDescriptor(EditPersonDescriptor toCopy) { setName(toCopy.name); setPhone(toCopy.phone); setEmail(toCopy.email); - setAddress(toCopy.address); + setRoles(toCopy.roles); + setSalary(toCopy.salary); + setStatus(toCopy.status); setTags(toCopy.tags); + setPeriod(toCopy.absentPeriods); + setSchedule(toCopy.schedule); } /** * 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, roles, salary, status, tags, absentPeriods); } public void setName(Name name) { @@ -160,6 +250,14 @@ public Optional getName() { return Optional.ofNullable(name); } + public void setSchedule(Schedule schedule) { + this.schedule = schedule; + } + + public Optional getSchedule() { + return Optional.ofNullable(schedule); + } + public void setPhone(Phone phone) { this.phone = phone; } @@ -176,12 +274,57 @@ public Optional getEmail() { return Optional.ofNullable(email); } - public void setAddress(Address address) { - this.address = address; + /** + * Sets {@code roles} to this object's {@code roles}. + * A defensive copy of {@code roles} is used internally. + */ + public void setRoles(Set roles) { + this.roles = (roles != null) ? new HashSet<>(roles) : null; } - public Optional
                            getAddress() { - return Optional.ofNullable(address); + /** + * Returns an unmodifiable role set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + * Returns {@code Optional#empty()} if {@code roles} is null. + */ + public Optional> getRoles() { + return (roles != null) ? Optional.of(Collections.unmodifiableSet(roles)) : Optional.empty(); + } + + public void setSalary(Salary salary) { + this.salary = salary; + } + + public Optional getSalary() { + return Optional.ofNullable(salary); + } + + public void setStatus(Status status) { + this.status = status; + } + + public Optional getStatus() { + return Optional.ofNullable(status); + } + + /** + * Sets {@code periods} to this object's {@code periods}. + * A defensive copy of {@code periods is used internally.} + * @param periods + */ + public void setPeriod(Set periods) { + this.absentPeriods = (periods != null) ? new HashSet<>(periods) : null; + } + + /** + * Returns an unmodifiable period set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + * Returns {@code Optional#empty()} if {@code period} is null. + */ + public Optional> getPeriod() { + return absentPeriods != null + ? Optional.of(Collections.unmodifiableSet(absentPeriods)) + : Optional.empty(); } /** @@ -219,8 +362,11 @@ public boolean equals(Object other) { return getName().equals(e.getName()) && getPhone().equals(e.getPhone()) && getEmail().equals(e.getEmail()) - && getAddress().equals(e.getAddress()) - && getTags().equals(e.getTags()); + && getRoles().equals(e.getRoles()) + && getSalary().equals(e.getSalary()) + && getStatus().equals(e.getStatus()) + && getTags().equals(e.getTags()) + && getSchedule().equals(e.getSchedule()); } } } diff --git a/src/main/java/seedu/address/logic/commands/ExitCommand.java b/src/main/java/seedu/address/logic/commands/ExitCommand.java index 3dd85a8ba90..acac9a21374 100644 --- a/src/main/java/seedu/address/logic/commands/ExitCommand.java +++ b/src/main/java/seedu/address/logic/commands/ExitCommand.java @@ -13,7 +13,7 @@ public class ExitCommand extends Command { @Override public CommandResult execute(Model model) { - return new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, false, true); + return new CommandResult(MESSAGE_EXIT_ACKNOWLEDGEMENT, false, true, false); } } diff --git a/src/main/java/seedu/address/logic/commands/FindCommand.java b/src/main/java/seedu/address/logic/commands/FindCommand.java index d6b19b0a0de..07b03469a00 100644 --- a/src/main/java/seedu/address/logic/commands/FindCommand.java +++ b/src/main/java/seedu/address/logic/commands/FindCommand.java @@ -1,42 +1,205 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_INDEX; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_NAME; +import java.util.function.Predicate; + +import javafx.collections.ObservableList; import seedu.address.commons.core.Messages; +import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.model.Model; -import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.address.model.person.Person; +import seedu.address.model.person.predicates.NameContainsKeywordsPredicate; +import seedu.address.model.person.predicates.PersonContainsFieldsPredicate; +import seedu.address.model.person.predicates.StaffHasCorrectIndexPredicate; /** * Finds and lists all persons in address book whose name contains any of the argument keywords. - * Keyword matching is case insensitive. + * Keyword matching is case-insensitive. */ public class FindCommand extends Command { public static final String COMMAND_WORD = "find"; + public static final String FIND_COMMAND_ONLY_NAME_OR_INDEX = COMMAND_WORD + ": Index and name cannot " + + "be used as lookup together."; 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"; + + "the specified keywords (case-insensitive) along with other fields or the index specified and " + + "displays them as a list with index numbers.\n\n" + + "Parameters:\n" + + PREFIX_DASH_INDEX + " INDEX [-p PHONE] [-e EMAIL] [-$ SALARY] [-s STATUS] [-r ROLE...] [-t TAG...] or" + + PREFIX_DASH_NAME + " KEYWORD [MORE_KEYWORDS]... [-p PHONE] [-e EMAIL] [-$ SALARY] [-s STATUS] " + + "[-r ROLE...] [-t TAG...]\n\n" + + "Examples:\n" + COMMAND_WORD + " " + + PREFIX_DASH_INDEX + " 1\n" + + COMMAND_WORD + " " + + PREFIX_DASH_NAME + " alice bob charlie\n"; + + public static final String INDEX_WITH_OTHER_INPUT = COMMAND_WORD + ": With index input, " + + "Only the index is expected. No other field is needed.\n" + + "Examples:\n" + + COMMAND_WORD + " " + PREFIX_DASH_INDEX + " 2"; + public static final String NO_ONE_SATISFIES_QUERY = "Search conditions indicated is not" + + " satisfied by anyone in staff'd"; + + private static final int NAME_AND_FIELD_PREDICATE = -1; + private static final int FIELD_PREDICATE_ONLY = -2; + private final NameContainsKeywordsPredicate namePredicate; + private final PersonContainsFieldsPredicate predicate; + private final int index; + private StringBuilder successMessage = new StringBuilder(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW).append("\n"); + private StaffHasCorrectIndexPredicate indexPredicate = null; + + /** + * Constructs a FindCommand object which searches by name + * + * @param namePredicate Predicate to filter the list by names that match a given name. + */ + public FindCommand(NameContainsKeywordsPredicate namePredicate, PersonContainsFieldsPredicate predicate) { + this.namePredicate = namePredicate; + this.predicate = predicate; + this.index = NAME_AND_FIELD_PREDICATE; // not used + } - private final NameContainsKeywordsPredicate predicate; + /** + * Constructs a FindCommand object which searches for the person at a specific index. + * + * @param index The index that the user searched for. + */ + public FindCommand(int index, PersonContainsFieldsPredicate predicate) { + assert index >= 0; + this.namePredicate = NameContainsKeywordsPredicate.EMPTY; + this.index = index; + this.predicate = predicate; + } - public FindCommand(NameContainsKeywordsPredicate predicate) { + /** + * Constructs a FindCommand object which searches for the person with the specified fields. + * + * @param predicate The predicate to test if the person containes the specified fields. + */ + public FindCommand(PersonContainsFieldsPredicate predicate) { + assert predicate != null; + assert !predicate.isEmpty(); this.predicate = predicate; + this.index = FIELD_PREDICATE_ONLY; + this.namePredicate = NameContainsKeywordsPredicate.EMPTY; } @Override - public CommandResult execute(Model model) { + public CommandResult execute(Model model) throws CommandException { requireNonNull(model); + //with name and field + if (index == NAME_AND_FIELD_PREDICATE) { + return executeNameAndFieldSearch(model); + } + //empty case cannot search + if (index == FIELD_PREDICATE_ONLY) { + return executeFieldSearch(model); + } + if (index >= 0) { + checkIndex(model); + indexPredicate = new StaffHasCorrectIndexPredicate(index, model); + return executeIndexSearch(model); + } + throw new CommandException("Check if your input are correct: -n for name, -i for index,\n" + + "and that the index given is correct!"); + + + } + + /** + * Executes a search by name. + * + * @param model The model which contains the list to be searched on. + * @return a CommandResult to be displayed. + */ + private CommandResult executeNameAndFieldSearch(Model model) throws CommandException { + checkModel(model, person -> namePredicate.test(person) + && predicate.test(person)); + model.updateFilteredPersonList(person -> namePredicate.test(person) + && predicate.test(person)); + ObservableList staffs = model.getFilteredPersonList(); + int counter = 1; + for (Person p : staffs) { + successMessage.append(counter).append(". ").append(p.toString()).append("\n\n"); + counter++; + } + return new CommandResult( + String.format(successMessage.toString(), model.getFilteredPersonList().size())); + } + + private CommandResult executeFieldSearch(Model model) throws CommandException { + checkModel(model, predicate); model.updateFilteredPersonList(predicate); + ObservableList staffs = model.getFilteredPersonList(); + int counter = 1; + for (Person p : staffs) { + successMessage.append(counter).append(". ").append(p.toString()).append("\n\n"); + counter++; + } + return new CommandResult( - String.format(Messages.MESSAGE_PERSONS_LISTED_OVERVIEW, model.getFilteredPersonList().size())); + String.format(successMessage.toString(), model.getFilteredPersonList().size())); + } + + + private void checkModel(Model model, Predicate predicate) throws CommandException { + ObservableList staffs = model.getUnFilteredPersonList(); + if (staffs.filtered(predicate).size() == 0) { + throw new CommandException(NO_ONE_SATISFIES_QUERY); + } + } + + + + /** + * Executes a search by index. + * + * @param model The model which contains the list to be searched on. + * @return a CommandResult to be displayed. + */ + private CommandResult executeIndexSearch(Model model) throws CommandException { + checkModel(model, p -> indexPredicate.test(p) && predicate.test(p)); + model.updateFilteredPersonList(p -> indexPredicate.test(p) && predicate.test(p)); + ObservableList staffs = model.getFilteredPersonList(); + int counter = 1; + for (Person p : staffs) { + successMessage.append(counter).append(". ").append(p.toString()).append("\n\n"); + counter++; + } + return new CommandResult( + String.format(successMessage.toString(), model.getFilteredPersonList().size())); + } + + /** + * Checks if the index is within a suitable range for the list contained in the model. + * + * @param model The model which contains the list to be searched on. + * @throws CommandException When the index inputted is not within range. + */ + private void checkIndex(Model model) throws CommandException { + int personListSize = model.getFilteredPersonList().size(); + if (index > personListSize - 1) { // -1 so that index starts from 0 + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + } + + @Override public boolean equals(Object other) { + if (other == null) { + return false; + } return other == this // short circuit if same object || (other instanceof FindCommand // instanceof handles nulls - && predicate.equals(((FindCommand) other).predicate)); // state check + && ((FindCommand) other).namePredicate.equals(this.namePredicate) + && ((FindCommand) other).index == index + && predicate.equals(((FindCommand) other).predicate)); } + } diff --git a/src/main/java/seedu/address/logic/commands/HelpCommand.java b/src/main/java/seedu/address/logic/commands/HelpCommand.java index bf824f91bd0..07d26e2a23c 100644 --- a/src/main/java/seedu/address/logic/commands/HelpCommand.java +++ b/src/main/java/seedu/address/logic/commands/HelpCommand.java @@ -16,6 +16,6 @@ public class HelpCommand extends Command { @Override public CommandResult execute(Model model) { - return new CommandResult(SHOWING_HELP_MESSAGE, true, false); + return new CommandResult(SHOWING_HELP_MESSAGE, true, false, false); } } diff --git a/src/main/java/seedu/address/logic/commands/MarkCommand.java b/src/main/java/seedu/address/logic/commands/MarkCommand.java new file mode 100644 index 00000000000..6e59f46bc1f --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/MarkCommand.java @@ -0,0 +1,140 @@ +package seedu.address.logic.commands; + +import static seedu.address.logic.commands.RemoveMarkCommand.NO_STAFF_SATISFIES_QUERY; +import static seedu.address.logic.commands.RemoveMarkCommand.listToString; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_INDEX; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_ROLE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_SALARY; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_STATUS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import javafx.collections.transformation.FilteredList; +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Period; +import seedu.address.model.person.Person; +import seedu.address.model.person.predicates.PersonContainsFieldsPredicate; + +/** + * Class representing the command for marking a person as absent. + */ +public class MarkCommand extends Command { + + public static final String COMMAND_WORD = "mark"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Used to mark someone as absent " + + "for the input duration." + + Messages.SHIFT_PERIOD_PARSING_DEFAULT + "\n\n" + + "Parameters:\n" + + "[" + PREFIX_DASH_INDEX + " INDEX] " + + "[" + PREFIX_DASH_NAME + " NAME] " + + "[" + PREFIX_DASH_PHONE + " PHONE] " + + "[" + PREFIX_DASH_EMAIL + " EMAIL] " + + "[" + PREFIX_DASH_SALARY + " SALARY] " + + "[" + PREFIX_DASH_STATUS + " STATUS] " + + "[" + PREFIX_DASH_ROLE + " ROLE]... " + + Messages.DATE_RANGE_INPUT + "\n\n" + + "Examples:\n" + + COMMAND_WORD + " " + PREFIX_DASH_INDEX + "1" + + " " + PREFIX_DATE + "2021-11-18\n" + + COMMAND_WORD + " " + PREFIX_DASH_NAME + "Jace " + + PREFIX_DATE + "2021-11-11" + " " + PREFIX_DATE + "2021-11-13"; + + public static final String DEFAULT_EXECUTION = "For the period: \n%2$s\n\n%1$d staff(s) have been marked:\n" + + "%3$s"; + + public static final String NOTHING_CHANGED = "For the input duration: " + + "\n%1$s\n\nThe staff(s) have already been marked, no change has been done:\n%2$s"; + public static final String NO_ONE_SATISFIES_QUERY = "The field(s) indicated is/are not " + + "satisfied by any staff in Staff'd"; + + private final Period period; + private final PersonContainsFieldsPredicate predicate; + private final int index; + + /** + * Constructs an {@code MarkCommand} to indicate that a person who satisfies + * the {@code PersonContainsFieldsPredicate} has been marked as not working + * in {@code period}. + */ + public MarkCommand(PersonContainsFieldsPredicate predicate, Period period) { + this.period = period; + this.predicate = predicate; + this.index = -1; + } + + /** + * Constructs an {@code MarkCommand} to indicate that a person who is + * at {@code index} during the execution of the command is not working during + * {@code period}. + */ + public MarkCommand(Index index, Period period, PersonContainsFieldsPredicate predicate) { + this.period = period; + this.predicate = predicate; + this.index = index.getZeroBased(); + } + + @Override + public CommandResult execute(Model model) throws CommandException { + + if (index != -1) { + return executeIndex(model); + } + FilteredList toModify = model.getFilteredPersonList().filtered(predicate); + + int total = toModify.size(); + if (total == 0) { + throw new CommandException(NO_STAFF_SATISFIES_QUERY); + } + List conflicts = new ArrayList<>(); + for (Person p : toModify) { + if (p.mark(period).equals(p)) { + conflicts.add(p.getName().toString()); + } + } + if (conflicts.size() != 0) { + throw new CommandException(String.format(NOTHING_CHANGED, period, listToString(conflicts))); + } + for (Person p : toModify) { + model.setPerson(p, p.mark(period)); + } + List names = toModify.stream() + .map(staff -> staff.getName().toString()) + .collect(Collectors.toList()); + return new CommandResult(String.format(DEFAULT_EXECUTION, total, period, listToString(names))); + } + + private CommandResult executeIndex(Model model) throws CommandException { + if (index >= model.getFilteredPersonList().size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + Person staffToModify = model.getFilteredPersonList().get(index); + if (!predicate.test(staffToModify)) { + throw new CommandException(NO_ONE_SATISFIES_QUERY); + } + Person changedStaff = staffToModify.mark(period); + if (staffToModify.equals(changedStaff)) { + throw new CommandException(String.format(NOTHING_CHANGED, period, staffToModify.getName())); + } + model.setPerson(staffToModify, changedStaff); + return new CommandResult(String.format(DEFAULT_EXECUTION, 1, period, changedStaff.getName())); + + } + + @Override + public boolean equals(Object other) { + return other != null + && other instanceof MarkCommand + && ((MarkCommand) other).period.equals(period) + && ((MarkCommand) other).index == index + && ((MarkCommand) other).predicate.equals(predicate); + } +} diff --git a/src/main/java/seedu/address/logic/commands/RemoveMarkCommand.java b/src/main/java/seedu/address/logic/commands/RemoveMarkCommand.java new file mode 100644 index 00000000000..956eeb80659 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/RemoveMarkCommand.java @@ -0,0 +1,171 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_INDEX; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_ROLE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_SALARY; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_STATUS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import javafx.collections.transformation.FilteredList; +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Period; +import seedu.address.model.person.Person; +import seedu.address.model.person.predicates.PersonContainsFieldsPredicate; + +/** + * Class representing the command to remove a mark. + */ +public class RemoveMarkCommand extends Command { + + public static final String COMMAND_WORD = "unmark"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Used to remove the marking of an absentee." + + Messages.SHIFT_PERIOD_PARSING_DEFAULT + "\n\n" + + "Parameters:\n" + + "[" + PREFIX_DASH_INDEX + " INDEX] " + + "[" + PREFIX_DASH_NAME + " NAME] " + + "[" + PREFIX_DASH_PHONE + " PHONE] " + + "[" + PREFIX_DASH_EMAIL + " EMAIL] " + + "[" + PREFIX_DASH_SALARY + " SALARY] " + + "[" + PREFIX_DASH_STATUS + " STATUS] " + + "[" + PREFIX_DASH_ROLE + " ROLE]... " + + Messages.DATE_RANGE_INPUT + "\n\n" + + "Example:\n" + + COMMAND_WORD + " " + PREFIX_DASH_INDEX + "1" + + " " + PREFIX_DATE + "2021-11-18\n" + + COMMAND_WORD + " " + PREFIX_DASH_NAME + "Jace " + + PREFIX_DATE + "2021-11-11" + " " + PREFIX_DATE + "2021-11-13"; + + + public static final String NO_STAFF_SATISFIES_QUERY = "No one satisfies the conditions specified"; + public static final String STAFF_NOT_MARKED = "The following staff is not marked for the period specified (%2$s)," + + " no change has been done: \n%1$s"; + + public static final String STAFF_UNMARKED = "Staff unmarked for period %2$s:\n%1$s"; + + private final PersonContainsFieldsPredicate predicate; + private final int index; + private final Period period; + + /** + * Constructs an {@code RemoveMarkCommand} to indicate that a person who satisfies + * the {@code PersonContainsFieldsPredicate} has been marked as working + * in {@code period}. + */ + public RemoveMarkCommand(PersonContainsFieldsPredicate predicate, Period period) { + index = -1; + this.predicate = predicate; + this.period = period; + } + + /** + * Constructs an {@code RemoveMarkCommand} to indicate that a person who satisfies the + * {@code PersonContainsFieldsPredicate} has been marked as working in + * {@code period}. + */ + public RemoveMarkCommand(PersonContainsFieldsPredicate predicate, Index index, Period period) { + this.index = index.getZeroBased(); + this.predicate = predicate; + this.period = period; + } + + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + if (index != -1) { + return executeIndex(model); + } + FilteredList toEdit = model.getUnFilteredPersonList() + .filtered(this.predicate); + if (toEdit.size() == 0) { + throw new CommandException(NO_STAFF_SATISFIES_QUERY); + } + List conflicts = new ArrayList<>(); + for (Person p : toEdit) { + if (p.unMark(period).equals(p)) { + conflicts.add(p.getName().toString()); + } + } + if (conflicts.size() != 0) { + throw new CommandException(String.format(STAFF_NOT_MARKED, listToString(conflicts), period)); + } + for (Person p : toEdit) { + model.setPerson(p, checkPerson(p)); + } + List toPrint = toEdit.stream() + .map(Person::getName) + .map(Object::toString) + .collect(Collectors.toList()); + return new CommandResult(String.format(STAFF_UNMARKED, listToString(toPrint), period)); + } + + + private CommandResult executeIndex(Model model) throws CommandException { + requireNonNull(model); + if (index > model.getFilteredPersonList().size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + Person toTest = model.getFilteredPersonList().get(index); + model.setPerson(toTest, checkPerson(toTest)); + return new CommandResult(String.format(STAFF_UNMARKED, toTest.getName(), period)); + } + + /** + * Converts a {@code List strings} of strings to the following format. + * e.g. ["Rudy", "Roxy", "Paul"] would output + * Rudy + * Roxy + * Paul + * Note that there is no new line at the end. + */ + public static String listToString(List strings) { + StringBuilder result = new StringBuilder(); + for (String string : strings) { + result.append(string); + result.append("\n"); + } + return result.toString().trim(); + } + + /** + * Checks if the staff satisfies the {@code predicate} and + * has {@code period} to be removed from the unmark command. + * + * @throws CommandException When the staff does not satisfy the conditions. + */ + private Person checkPerson(Person toTest) throws CommandException { + requireNonNull(toTest); + //ensures that the staff to unmark satisfies the predicate + if (!this.predicate.test(toTest)) { + throw new CommandException(NO_STAFF_SATISFIES_QUERY); + } + Person result = toTest.unMark(period); + //when nothing has changed + if (result.equals(toTest)) { + throw new CommandException(String.format(STAFF_NOT_MARKED, toTest.getName(), period)); + } + return result; + + } + + @Override + public boolean equals(Object obj) { + return (obj != null) + && (obj instanceof RemoveMarkCommand) + && ((RemoveMarkCommand) obj).index == index + && ((RemoveMarkCommand) obj).predicate.equals(predicate) + && ((RemoveMarkCommand) obj).period.equals(period); + } + +} diff --git a/src/main/java/seedu/address/logic/commands/SchedulePeriodChangeCommand.java b/src/main/java/seedu/address/logic/commands/SchedulePeriodChangeCommand.java new file mode 100644 index 00000000000..36e6d26492c --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/SchedulePeriodChangeCommand.java @@ -0,0 +1,37 @@ +package seedu.address.logic.commands; + +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; + +import seedu.address.commons.util.DateTimeUtil; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Period; + +/** + * Class representing the command to change the schedule view + * by the period. + */ +public class SchedulePeriodChangeCommand extends Command { + + public static final String COMMAND_WORD = "change"; + public static final String DEFAULT_MESSAGE = "Changed schedule to period: %1$s"; + public static final String HELP_MESSAGE = COMMAND_WORD + + ":Changes the week that the schedule displays. Takes in a single " + + "date within the week and outputs a full week starting from that date. Date is expected in " + + "YYYY-MM-DD format.\n\n" + + "Parameters:\n" + + PREFIX_DATE + "DATE" + + "Examples:\n" + COMMAND_WORD + " " + PREFIX_DATE + "2021-12-25"; + + private final Period period; + + public SchedulePeriodChangeCommand(Period period) { + this.period = period; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + DateTimeUtil.updateDisplayedPeriod(period); + return new CommandResult(String.format(DEFAULT_MESSAGE, period), period); + } +} diff --git a/src/main/java/seedu/address/logic/commands/SetRoleReqCommand.java b/src/main/java/seedu/address/logic/commands/SetRoleReqCommand.java new file mode 100644 index 00000000000..0523e21bf3d --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/SetRoleReqCommand.java @@ -0,0 +1,81 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.FILE_NOT_FOUND; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ROLE_REQUIREMENTS; + +import java.io.IOException; +import java.util.Set; + +import javafx.collections.ObservableList; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Person; +import seedu.address.storage.RoleReqStorage; + +public class SetRoleReqCommand extends Command { + public static final String COMMAND_WORD = "setRoleReq"; + public static final String SUCCESS_MESSAGE = "Role requirements successfully updated:\n\n"; + private static final String HELP_MESSAGE = COMMAND_WORD + " Sets the minimum number of staff required for " + + "the specified role.\n\n" + + "Parameters:\n" + + PREFIX_ROLE_REQUIREMENTS + "ROLE-NUMBER\n\n" + + "Examples:\n" + + COMMAND_WORD + " " + PREFIX_ROLE_REQUIREMENTS + "kitchen-1 " + PREFIX_ROLE_REQUIREMENTS + "bartender-1\n" + + COMMAND_WORD + " " + PREFIX_ROLE_REQUIREMENTS + "floor-3\n\n" + + "Currently, the role requirements per shift are:\n" + + "%s"; + + private final Set roleReqList; + + public SetRoleReqCommand(Set roleReqList) { + this.roleReqList = roleReqList; + } + + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + ObservableList staffs = model.getUnFilteredPersonList(); + for (String roleReq : roleReqList) { + String[] roleReqSplit = roleReq.split("-"); + + try { + RoleReqStorage.update(roleReqSplit[0], Integer.parseInt(roleReqSplit[1])); + } catch (IOException e) { + throw new CommandException(FILE_NOT_FOUND + RoleReqStorage.FILEPATH); + } + } + return new CommandResult(SUCCESS_MESSAGE + RoleReqStorage.getRoleReqs()); + } + + /** + * Returns the Help Message with the updated role requirements. + * + * @return the Help Message with the updated role requirements. + */ + public static String getHelpMessage() { + return String.format(HELP_MESSAGE, RoleReqStorage.getRoleReqs()); + } + + /** + * Returns the Set of Role Requirements. + * + * @return roleReqList. + */ + public Set getRoleReqList() { + return this.roleReqList; + } + + @Override + public boolean equals(Object other) { + if (other == null) { + return false; + } + + // short circuit if same object + return other == this + || !(other instanceof SetRoleReqCommand) + || this.roleReqList.equals(((SetRoleReqCommand) other).getRoleReqList()); + } +} diff --git a/src/main/java/seedu/address/logic/commands/SetShiftTimeCommand.java b/src/main/java/seedu/address/logic/commands/SetShiftTimeCommand.java new file mode 100644 index 00000000000..12b15ebad37 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/SetShiftTimeCommand.java @@ -0,0 +1,147 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.DATE_RANGE_INPUT; +import static seedu.address.commons.core.Messages.SHIFT_PERIOD_PARSING_DEFAULT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_INDEX; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DAY_SHIFT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SHIFT_TIME; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.exceptions.InvalidShiftTimeException; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Name; +import seedu.address.model.person.Person; +import seedu.address.model.person.Slot; + +public class SetShiftTimeCommand extends Command { + public static final String COMMAND_WORD = "setShiftTime"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Set time for a shift of the staff identified " + + "by the index number used in the displayed staff list or the name of staff.\n" + + "If the shift does not exist, a new one will be created.\n" + + "Notice that the startTime must be earlier than the endTime, " + + "and if the range of time is [10:00-16:00] and [16:00-22:00] for morning " + + "slot and afternoon slot respectively. Date input is used to indicate the period of the shift to modify." + + SHIFT_PERIOD_PARSING_DEFAULT + "\n\n" + + "Parameters:\n" + + PREFIX_DASH_INDEX + " INDEX or " + + PREFIX_DASH_NAME + " NAME " + + PREFIX_DAY_SHIFT + "DAY_AND_SLOT " + + PREFIX_SHIFT_TIME + "START_TIME:END_TIME " + + DATE_RANGE_INPUT + + "\n\n" + + "Example:\n" + COMMAND_WORD + " " + + PREFIX_DASH_INDEX + " 1 " + + PREFIX_DAY_SHIFT + "monday-1 " + + PREFIX_SHIFT_TIME + "10:00-12:00\n" + + COMMAND_WORD + " " + + PREFIX_DASH_NAME + " JOE " + + PREFIX_DAY_SHIFT + "monday-1 " + + PREFIX_SHIFT_TIME + "10:00-12:00 da/2021-11-01\n"; + + public static final String MESSAGE_SET_SHIFT_TIME_SUCCESS = "%s's shift on %s %s from %s to %s is successfully " + + "updated to: " + "From %s to %s."; + public static final String MESSAGE_SHIFT_TIME_OUT_OF_BOUND = "The start time or end time of the shift is out" + + " of bound."; + public static final String MESSAGE_WRONG_TIME_ORDER = "The end time is earlier than the start time."; + + private final Index index; + private final Name name; + private final DayOfWeek dayOfWeek; + private final Slot slot; + private final LocalTime startTime; + private final LocalTime endTime; + private final LocalDate startDate; + private final LocalDate endDate; + + /** + * Creates an SetShiftCommand to add the specified {@code Shift} to a {@code Person}. + */ + public SetShiftTimeCommand(Index index, Name name, String shiftDateAndSlot, LocalTime[] shiftTimes, + LocalDate startDate, LocalDate endDate) { + requireNonNull(shiftDateAndSlot); + this.index = index; + this.name = name; + String[] strings = shiftDateAndSlot.split("-"); + dayOfWeek = DayOfWeek.valueOf(strings[0].toUpperCase()); + slot = Slot.getSlotByOrder(strings[1]); + this.startTime = shiftTimes[0]; + this.endTime = shiftTimes[1]; + this.startDate = startDate; + this.endDate = endDate; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + List lastShownList = model.getFilteredPersonList(); + + Person staffToEdit; + + if (index != null) { + if (index.getZeroBased() >= lastShownList.size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + staffToEdit = lastShownList.get(index.getZeroBased()); + } else { + if (name != null) { + staffToEdit = model.findPersonByName(name); + } else { + throw new CommandException(MESSAGE_USAGE); + } + } + + if (staffToEdit == null || !model.hasPerson(staffToEdit)) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_SEARCHED); + } + + if (!endTime.isAfter(startTime)) { + throw new CommandException(MESSAGE_WRONG_TIME_ORDER); + } + + try { + model.setShiftTime(staffToEdit, dayOfWeek, slot, startTime, endTime, startDate, endDate); + } catch (InvalidShiftTimeException e) { + throw new CommandException(MESSAGE_SHIFT_TIME_OUT_OF_BOUND); + } + + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + return new CommandResult(String.format(MESSAGE_SET_SHIFT_TIME_SUCCESS, staffToEdit.getName(), dayOfWeek, slot, + startDate, endDate, startTime, endTime)); + + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof SetShiftTimeCommand)) { + return false; + } + + // state check + SetShiftTimeCommand command = (SetShiftTimeCommand) other; + return ((index == null && command.index == null) || (index != null && index.equals(command.index))) + && ((name == null && command.name == null) || (name != null && name.equals(command.name))) + && dayOfWeek.equals(command.dayOfWeek) + && slot.equals(command.slot) + && startTime.equals(command.startTime) + && endTime.equals(command.endTime) + && startDate.equals(command.startDate) + && endDate.equals(command.endDate); + } +} diff --git a/src/main/java/seedu/address/logic/commands/StaffIndividualStatisticsCommand.java b/src/main/java/seedu/address/logic/commands/StaffIndividualStatisticsCommand.java new file mode 100644 index 00000000000..3a163b47ef2 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/StaffIndividualStatisticsCommand.java @@ -0,0 +1,133 @@ +package seedu.address.logic.commands; + +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_INDEX; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_ROLE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_SALARY; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_STATUS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_TAG; + +import java.util.List; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Period; +import seedu.address.model.person.Person; +import seedu.address.model.person.predicates.PersonContainsFieldsPredicate; + +/** + * A class representing the command to obtain the working + * hours and overall salary for each staff. + */ +public class StaffIndividualStatisticsCommand extends Command { + + public static final String COMMAND_WORD = "istaff"; + public static final String DEFAULT_EXECUTION = "Staff to show for the period of %2$s:\n%1$s"; + public static final String MESSAGE_USAGE = COMMAND_WORD + ":" + + "Command to obtain salary statistics of staff\n" + + "Used by looking up the staff to display by field.\n" + + "input parameters.\n\n" + + "Parameters:\n" + + "[" + PREFIX_DASH_NAME + "NAME] " + + "[" + PREFIX_DASH_INDEX + "INDEX] " + + "[" + PREFIX_DASH_PHONE + "PHONE] " + + "[" + PREFIX_DASH_EMAIL + "EMAIL] " + + "[" + PREFIX_DASH_SALARY + "SALARY] " + + "[" + PREFIX_DASH_STATUS + "STATUS] " + + "[" + PREFIX_DASH_ROLE + "ROLE]... " + + "[" + PREFIX_DASH_TAG + "TAG]...\n\n" + + "Example:\n" + COMMAND_WORD + " " + + PREFIX_DASH_PHONE + "91234567 " + + PREFIX_DASH_EMAIL + "johndoe@example.com"; + public static final String INDIVIDUAL_STAFF_PRINT = "Stats for %1$s:\n" + + "Total work hours: %2$s\n" + + "Total salary: %3$s"; + + public static final String NO_STAFF_SATISFIES_QUERY = "No one satisfies the conditions specified"; + + private final PersonContainsFieldsPredicate predicate; + private final int index; + private final Period period; + + /** + * Constructor for the lookup command to display staff statistics. + * Staff statistics displayed are total work hours for the month. + * + * @param predicate The {@code PersonContainsFieldsPredicate} that decides + * which staff to display the statistics of. + */ + public StaffIndividualStatisticsCommand(PersonContainsFieldsPredicate predicate, Period period) { + this.predicate = predicate; + this.index = -1; + this.period = period; + } + + /** + * Constructor for the lookup command to display staff statistics by index. + * + * @param predicate The predicate to test the staff with. + * @param index The index of the staff to get. + */ + public StaffIndividualStatisticsCommand(PersonContainsFieldsPredicate predicate, Index index, Period period) { + this.predicate = predicate; + this.index = index.getZeroBased(); + this.period = period; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + if (index != -1) { + return executeIndex(model); + } + List staffs = model.getUnFilteredPersonList().filtered(predicate); + if (staffs.size() == 0) { + throw new CommandException(NO_STAFF_SATISFIES_QUERY); + } + return new CommandResult(String.format(DEFAULT_EXECUTION, result(staffs), period)); + + } + + private CommandResult executeIndex(Model model) throws CommandException { + if (index >= model.getFilteredPersonList().size()) { + throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Person staff = model.getFilteredPersonListByIndex(index); + if (!predicate.test(staff)) { + throw new CommandException(NO_STAFF_SATISFIES_QUERY); + } + return new CommandResult(String.format(DEFAULT_EXECUTION, result(List.of(staff)), period)); + } + + + private String result(List staffs) { + StringBuilder sb = new StringBuilder(); + sb.append("\n"); + for (Person staff : staffs) { + sb.append(staffSummary(staff)); + sb.append("\n"); + } + return sb.toString(); + + } + + private String staffSummary(Person staff) { + long workHours = staff + .getTotalWorkingHour(period); + double totalSalary = staff.getSalaryToBePaid(this.period); + return String.format(INDIVIDUAL_STAFF_PRINT, staff.getName(), workHours, totalSalary); + } + + @Override + public boolean equals(Object obj) { + return obj != null + && obj instanceof StaffIndividualStatisticsCommand + && ((StaffIndividualStatisticsCommand) obj).index == index + && ((StaffIndividualStatisticsCommand) obj).predicate.equals(predicate) + && ((StaffIndividualStatisticsCommand) obj).period.equals(period); + } +} diff --git a/src/main/java/seedu/address/logic/commands/StaffStatisticsCommand.java b/src/main/java/seedu/address/logic/commands/StaffStatisticsCommand.java new file mode 100644 index 00000000000..9bb2b9148f2 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/StaffStatisticsCommand.java @@ -0,0 +1,73 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.model.person.Period.getPeriodFromDateOverMonth; + +import java.time.LocalDate; +import java.util.List; + +import javafx.collections.ObservableList; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Period; +import seedu.address.model.person.Person; + +/** + * Class representing a command for obtaining information on + * staff salaries. + */ +public class StaffStatisticsCommand extends Command { + + public static final String COMMAND_WORD = "stats"; + public static final String MESSAGE_SUCCESS = "Stats for %4$s:\n" + + "Total salary: %1$s\n" + + "Total work time: %2$s\n" + + "Average work time: %3$s\n"; + public static final String EMPTY_MODEL = "There is no one to get the statistics of."; + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + //empty case + ObservableList staffList = model.getUnFilteredPersonList(); + if (staffList.isEmpty()) { + throw new CommandException(EMPTY_MODEL); + } + return new CommandResult(String.format(MESSAGE_SUCCESS, + totalSalary(staffList), totalWorkTime(staffList), + totalWorkTime(staffList) / staffList.size(), getCurrentPeriod())); + } + + /** + * Obtain the total salary of every staff in the input model. + * + * @param staffs The staffs to get the total salary from. + * @return The total salary. + */ + private double totalSalary(List staffs) { + final Period period = getCurrentPeriod(); + return staffs.stream() + .mapToDouble(person -> person.getSalaryToBePaid(period)) + .reduce(0, (x, y) -> x + y); + + } + + /** + * Obtain the total worktime of every staff in the input model. + * + * @param staffs The staffs to get the work time from. + * @return the total work time in hours. + */ + private long totalWorkTime(List staffs) { + final Period period = getCurrentPeriod(); + long result = staffs.stream() + .mapToLong(person -> person.getTotalWorkingHour(period)) + .sum(); + return result; + } + + private Period getCurrentPeriod() { + return getPeriodFromDateOverMonth(LocalDate.now()); + } + +} diff --git a/src/main/java/seedu/address/logic/commands/SwapShiftCommand.java b/src/main/java/seedu/address/logic/commands/SwapShiftCommand.java new file mode 100644 index 00000000000..4c769236021 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/SwapShiftCommand.java @@ -0,0 +1,138 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.DATE_RANGE_INPUT; +import static seedu.address.commons.core.Messages.SHIFT_PERIOD_PARSING_DEFAULT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DAY_SHIFT; +import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.util.List; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Name; +import seedu.address.model.person.Period; +import seedu.address.model.person.Person; +import seedu.address.model.person.Slot; + +/** + * Swaps shifts between 2 staffs. + */ +public class SwapShiftCommand extends Command { + public static final String COMMAND_WORD = "swapShift"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": swaps shifts between 2 staffs identified " + + "using their names. You have to input exactly 2 names and 2 shifts. The period within which the " + + "shifts are active over is optional. Date input is used to indicate the period when the shift is active. " + + SHIFT_PERIOD_PARSING_DEFAULT + "\n\n" + + "NOTE: The staff identified using the first name is associated with the first shift and the staff " + + "identified using the second name is associated with the second shift. Do take note of the order!\n\n" + + "Parameters: (2 of each)\n" + + PREFIX_DASH_NAME + " NAME\n" + + PREFIX_DAY_SHIFT + "DAYOFWEEK-SLOTNUMBER " + + DATE_RANGE_INPUT + "\n\n" + + "Examples:\n" + + COMMAND_WORD + " -n Alex Yeoh d/monday-1 -n David Li d/friday-0\n\n" + + COMMAND_WORD + " -n Alex Yeoh -n David Li d/tuesday-0 d/wednesday-1"; + + public static final String MESSAGE_SWAP_SHIFT_SUCCESS = "Successfully swapped the given shifts between %s and %s!"; + public static final String NON_UNIQUE_NAMES = "The 2 names provided are not unique!"; + public static final String NON_UNIQUE_SHIFTS = "The 2 shifts provided are not unique!"; + public static final String STAFF_NOT_FOUND = "%s is not one of your staff!"; + public static final String SHIFT_CANT_SWAP = "%s does not have the shift %s-%s"; + public static final String SHIFT_ALREADY_EXISTS = "%s already has the shift %s-%s"; + + private final Name firstStaff; + private final Name secondStaff; + private final DayOfWeek firstDayOfWeek; + private final Slot firstSlot; + private final DayOfWeek secondDayOfWeek; + private final Slot secondSlot; + private final LocalDate startDate; + private final LocalDate endDate; + private final Period period; + + /** + * Creates a SwapShiftCommand to swap shifts between 2 staffs. + * + * @param nameList List containing the names of 2 staffs + * @param shiftList List containing the shifts of 2 staffs + */ + public SwapShiftCommand(List nameList, List shiftList, LocalDate startDate, LocalDate endDate) { + firstStaff = nameList.get(0); + secondStaff = nameList.get(1); + + String[] firstStringList = shiftList.get(0).split("-"); + firstDayOfWeek = DayOfWeek.valueOf(firstStringList[0].toUpperCase()); + firstSlot = Slot.getSlotByOrder(firstStringList[1]); + + String[] secondStringList = shiftList.get(1).split("-"); + secondDayOfWeek = DayOfWeek.valueOf(secondStringList[0].toUpperCase()); + secondSlot = Slot.getSlotByOrder(secondStringList[1]); + this.startDate = startDate; + this.endDate = endDate; + this.period = new Period(startDate, endDate); + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + Person firstStaffToEdit = model.findPersonByName(firstStaff); + Person secondStaffToEdit = model.findPersonByName(secondStaff); + + // If the first staff does not exist + if (firstStaffToEdit == null) { + throw new CommandException(String.format(STAFF_NOT_FOUND, firstStaff)); + } + + // If the second staff does not exist + if (secondStaffToEdit == null) { + throw new CommandException(String.format(STAFF_NOT_FOUND, secondStaff)); + } + + // If the first staff does not have the shift to swap + if (!firstStaffToEdit.isWorking(firstDayOfWeek, firstSlot.getOrder(), period)) { + throw new CommandException(String.format(SHIFT_CANT_SWAP, firstStaff, firstDayOfWeek, firstSlot)); + } + + // If the second staff does not have the shift to swap + if (!secondStaffToEdit.isWorking(secondDayOfWeek, secondSlot.getOrder(), period)) { + throw new CommandException(String.format(SHIFT_CANT_SWAP, secondStaff, secondDayOfWeek, secondSlot)); + } + + // If the first staff already has the shift that he/she is trying to swap to + if (firstStaffToEdit.isWorking(secondDayOfWeek, secondSlot.getOrder(), period)) { + throw new CommandException(String.format(SHIFT_ALREADY_EXISTS, firstStaff, secondDayOfWeek, secondSlot)); + } + + // If the second staff already has the shift that he/she is trying to swap to + if (secondStaffToEdit.isWorking(firstDayOfWeek, firstSlot.getOrder(), period)) { + throw new CommandException(String.format(SHIFT_ALREADY_EXISTS, secondStaff, firstDayOfWeek, firstSlot)); + } + + model.deleteShift(firstStaffToEdit, firstDayOfWeek, firstSlot, startDate, endDate); + model.deleteShift(secondStaffToEdit, secondDayOfWeek, secondSlot, startDate, endDate); + model.addShift(firstStaffToEdit, secondDayOfWeek, secondSlot, startDate, endDate); + model.addShift(secondStaffToEdit, firstDayOfWeek, firstSlot, startDate, endDate); + + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + return new CommandResult(String.format(MESSAGE_SWAP_SHIFT_SUCCESS, firstStaff, secondStaff)); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SwapShiftCommand that = (SwapShiftCommand) o; + return firstStaff.equals(that.firstStaff) && secondStaff.equals(that.secondStaff) + && firstDayOfWeek == that.firstDayOfWeek && firstSlot == that.firstSlot + && secondDayOfWeek == that.secondDayOfWeek && secondSlot == that.secondSlot; + } +} diff --git a/src/main/java/seedu/address/logic/commands/SwitchTabCommand.java b/src/main/java/seedu/address/logic/commands/SwitchTabCommand.java new file mode 100644 index 00000000000..6e7ecc25f26 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/SwitchTabCommand.java @@ -0,0 +1,21 @@ +package seedu.address.logic.commands; + +import seedu.address.model.Model; + +/** + * Format full help instructions for every command for display. + */ +public class SwitchTabCommand extends Command { + + public static final String COMMAND_WORD = "tab"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Switches tab on the right window.\n" + + "Example: " + COMMAND_WORD; + + public static final String SWITCHED_TAB_MESSAGE = "Switched tab."; + + @Override + public CommandResult execute(Model model) { + return new CommandResult(SWITCHED_TAB_MESSAGE, false, false, true); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ViewShiftCommand.java b/src/main/java/seedu/address/logic/commands/ViewShiftCommand.java new file mode 100644 index 00000000000..2073f7a03b5 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ViewShiftCommand.java @@ -0,0 +1,200 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.commands.CommandUtil.checkDateForDayOfWeek; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_DAY_SHIFT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_TIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; + +import java.time.DayOfWeek; +import java.time.LocalTime; +import java.util.Set; + +import javafx.collections.ObservableList; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.person.Period; +import seedu.address.model.person.Person; +import seedu.address.model.person.Role; +import seedu.address.model.person.Shift; +import seedu.address.model.person.predicates.PersonIsWorkingPredicate; +import seedu.address.storage.RoleReqStorage; + + +/** + * Class representing the view schedule command + * which views the schedule by Person. + */ +public class ViewShiftCommand extends Command { + + public static final String COMMAND_WORD = "viewShift"; + public static final String HELP_MESSAGE = COMMAND_WORD + ": find the staff working at the specified shift. " + + "Takes in one optional date input, the shift viewed will be for the next date that shift occurs." + + "For instance, the shift viewed given a date of sunday for da/2021-11-06 (a saturday) " + + "will be for the date 2021-11-07, the next date. Date input expected in YYYY-MM-DD\n\n" + + "Parameters:\n" + + PREFIX_DASH_DAY_SHIFT + " DAYOFWEEK-SHIFT_NUMBER\n" + + PREFIX_DASH_TIME + " DAYOFWEEK-HH:mm" + " (time is in format HH:mm)\n" + + "[" + PREFIX_DATE + "DATE]" + "\n\n" + + "Examples:\n" + + COMMAND_WORD + " " + PREFIX_DASH_DAY_SHIFT + " monday-0\n" + + COMMAND_WORD + " " + PREFIX_DASH_DAY_SHIFT + " TUESDAY-1\n" + + COMMAND_WORD + " " + PREFIX_DASH_TIME + " wednesday-11:00 " + + PREFIX_DATE + "2020-01-01" + " " + + PREFIX_DATE + "2022-12-30" + "\n\n"; + + public static final int INVALID_SLOT_NUMBER = -1; + public static final int INVALID_SLOT_NUMBER_INDICATING_EMPTY_PREFIXES = -2; + private static final String NO_STAFF_WORKING = "There is currently no staff working at the specified shift."; + private static final String STAFF_LIST_EMPTY = "There are no staffs in the staff list, please add some first!"; + + private final DayOfWeek dayOfWeek; + private final int slotNum; + private final LocalTime time; + private final PersonIsWorkingPredicate isWorkingPredicate; + private final Period periodToLookAt; + + private String defaultMessage = "Staff working on shift: "; + private int[] finalRoleReqCheck = new int[]{}; + private String finalRoleReqMessage; + + /** + * Constructs a ViewShiftCommand object. + * + * @param dayOfWeek The dayOfWeek that will be checked + * @param slotNum The slot number that will be checked + * @param time The time that will be checked + */ + public ViewShiftCommand(DayOfWeek dayOfWeek, int slotNum, LocalTime time, Period period) { + this.dayOfWeek = dayOfWeek; + this.slotNum = slotNum; + this.time = time; + this.periodToLookAt = period; + this.isWorkingPredicate = new PersonIsWorkingPredicate(dayOfWeek, slotNum, time, period); + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + checkDateForDayOfWeek(periodToLookAt, dayOfWeek); + ObservableList staffs = model.getUnFilteredPersonList(); + if (staffs.size() == 0) { + throw new CommandException(STAFF_LIST_EMPTY); + } + model.updateFilteredPersonList(isWorkingPredicate); + setRoleReqMessage(model); + staffs = model.getFilteredPersonList(); + + // Assumes either time is null, or dayOfWeek and slotNum is null (as passed in by ViewShiftCommandParser) + if (time != null && dayOfWeek != null) { + this.defaultMessage += String.format("%s-%s\n", dayOfWeek, time); + return executeViewShiftByTime(staffs); + } else if (slotNum != INVALID_SLOT_NUMBER && dayOfWeek != null) { + this.defaultMessage += String.format("%s-%s\n", dayOfWeek, slotNum); + return executeViewShiftBySlot(staffs); + } else { + throw new CommandException(HELP_MESSAGE); + } + } + + /** + * Executes ViewShift by slot number, and finds the staff working at that day of the week and slot number + * + * @param staffs The staff list that will be checked + * @return The staff working at that day of the week and slot number + */ + public CommandResult executeViewShiftBySlot(ObservableList staffs) { + StringBuilder result = new StringBuilder(); + int counter = 1; + for (Person p : staffs) { + boolean hasShift = p.isWorking(dayOfWeek, slotNum, periodToLookAt); + if (hasShift) { + result.append(counter).append(". ").append(p.getName()).append("\n"); + counter++; + } + } + if (counter == 0) { + return new CommandResult(NO_STAFF_WORKING); + } else { + return new CommandResult(defaultMessage + result.toString() + finalRoleReqMessage); + } + } + + /** + * Executes ViewShift by slot number, and finds the staff working at that day of the week and time + * + * @param staffs The staff list that will be checked + * @return The staff working at that day of the week and time + */ + public CommandResult executeViewShiftByTime(ObservableList staffs) { + String result = Shift.filterListByShift(staffs, dayOfWeek, time, periodToLookAt); + if (result.equals("")) { + return new CommandResult(NO_STAFF_WORKING); + } else if (slotNum == INVALID_SLOT_NUMBER_INDICATING_EMPTY_PREFIXES) { + return new CommandResult(HELP_MESSAGE + getWorkingStaffByTime(staffs)); + } else { + return new CommandResult(defaultMessage + result + finalRoleReqMessage); + } + } + + public String getWorkingStaffByTime(ObservableList staffs) { + StringBuilder result = new StringBuilder(); + int counter = 1; + for (Person p : staffs) { + boolean hasShift = p.isWorking(dayOfWeek, time, periodToLookAt); + if (hasShift) { + result.append(counter).append(". ").append(p.getName()).append("\n"); + counter++; + } + } + return "Currently working:\n" + result; + } + + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof ViewShiftCommand + && (this.dayOfWeek == null || this.dayOfWeek.equals(((ViewShiftCommand) other).dayOfWeek)) + && (this.slotNum != INVALID_SLOT_NUMBER || this.slotNum == ((ViewShiftCommand) other).slotNum) + && (this.time == null || this.time.equals(((ViewShiftCommand) other).time))); + } + + private boolean checkRoleReq(Model model) { + int[] roleReqCheck = new int[]{0, 0, 0}; // bartender, floor, kitchen + ObservableList staffWorking = model.getFilteredPersonList(); + + for (Person p : staffWorking) { + Set pRoles = p.getRoles(); + for (Role r : pRoles) { + if (r.getValue().equals("bartender")) { + roleReqCheck[0] += 1; + } else if (r.getValue().equals("floor")) { + roleReqCheck[1] += 1; + } else if (r.getValue().equals("kitchen")) { + roleReqCheck[2] += 1; + } + } + } + + finalRoleReqCheck = roleReqCheck; + if (roleReqCheck[0] < RoleReqStorage.getMinNumBartender()) { + return false; + } else if (roleReqCheck[1] < RoleReqStorage.getMinNumFloor()) { + return false; + } else { + return roleReqCheck[2] >= RoleReqStorage.getMinNumKitchen(); + } + } + + private void setRoleReqMessage(Model model) { + String roleReqMessage = checkRoleReq(model) + ? "" + : "\n\nThere is a manpower shortage! You are supposed to have:\n" + + RoleReqStorage.getRoleReqs() + "\n\n" + + "But you currently have:\n" + + "Bartender: " + finalRoleReqCheck[0] + "\n" + + "Floor: " + finalRoleReqCheck[1] + "\n" + + "Kitchen: " + finalRoleReqCheck[2] + "\n"; + finalRoleReqMessage = roleReqMessage; + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddCommandParser.java index 3b8bfa035e8..d1be1a1cefd 100644 --- a/src/main/java/seedu/address/logic/parser/AddCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/AddCommandParser.java @@ -1,22 +1,28 @@ package seedu.address.logic.parser; import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; -import static seedu.address.logic.parser.CliSyntax.PREFIX_ADDRESS; import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; import static seedu.address.logic.parser.CliSyntax.PREFIX_NAME; import static seedu.address.logic.parser.CliSyntax.PREFIX_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ROLE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SALARY; +import static seedu.address.logic.parser.CliSyntax.PREFIX_STATUS; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; +import static seedu.address.logic.parser.ParserUtil.arePrefixesPresent; +import java.util.HashSet; import java.util.Set; -import java.util.stream.Stream; import seedu.address.logic.commands.AddCommand; import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.Address; import seedu.address.model.person.Email; import seedu.address.model.person.Name; +import seedu.address.model.person.Period; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.Role; +import seedu.address.model.person.Salary; +import seedu.address.model.person.Status; import seedu.address.model.tag.Tag; /** @@ -27,13 +33,15 @@ public class AddCommandParser implements Parser { /** * Parses the given {@code String} of arguments in the context of the AddCommand * and returns an AddCommand object for execution. + * * @throws ParseException if the user input does not conform the expected format */ public AddCommand parse(String args) throws ParseException { ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); + ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, + PREFIX_ROLE, PREFIX_SALARY, PREFIX_STATUS, PREFIX_TAG); - if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_ADDRESS, PREFIX_PHONE, PREFIX_EMAIL) + if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_SALARY) || !argMultimap.getPreamble().isEmpty()) { throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); } @@ -41,20 +49,16 @@ public AddCommand parse(String args) throws ParseException { Name name = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()); Phone phone = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get()); Email email = ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get()); - Address address = ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get()); + Set roleList = ParserUtil.parseRoles(argMultimap.getAllValues(PREFIX_ROLE)); + Salary salary = ParserUtil.parseSalary(argMultimap.getValue(PREFIX_SALARY).get()); + Status status = ParserUtil.parseStatuses(argMultimap.getAllValues(PREFIX_STATUS)); Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); + Set periods = new HashSet<>(); + Person staff = new Person(name, phone, email, roleList, salary, status, tagList, periods); - Person person = new Person(name, phone, email, address, tagList); - - return new AddCommand(person); + return new AddCommand(staff); } - /** - * 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/AddShiftCommandParser.java b/src/main/java/seedu/address/logic/parser/AddShiftCommandParser.java new file mode 100644 index 00000000000..bfe03e78398 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/AddShiftCommandParser.java @@ -0,0 +1,91 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.DATES_IN_WRONG_ORDER; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_INDEX; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DAY_SHIFT; +import static seedu.address.logic.parser.ParserUtil.extractTupleDates; + +import java.time.LocalDate; +import java.util.stream.Stream; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.DateTimeUtil; +import seedu.address.logic.commands.AddShiftCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Name; + +/** + * Parses input arguments and creates a new AddShiftCommand object. + */ +public class AddShiftCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the AddShiftCommand + * and returns an AddShiftCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format. + */ + public AddShiftCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_DASH_INDEX, PREFIX_DATE, + PREFIX_DASH_NAME, PREFIX_DAY_SHIFT); + + Index index = null; + Name name = null; + String shiftDayAndSlot; + LocalDate[] dates = DateTimeUtil.getDisplayedDateArray(); + + //PREFIX_DAY_SHIFT must exist and exactly one from PREFIX_INDEX and PREFIX_NAME must exist. + if (!arePrefixesPresent(argMultimap, PREFIX_DAY_SHIFT) + || !argMultimap.getPreamble().isEmpty() || (!arePrefixesPresent(argMultimap, PREFIX_DASH_INDEX) + && !arePrefixesPresent(argMultimap, PREFIX_DASH_NAME)) + || (arePrefixesPresent(argMultimap, PREFIX_DASH_INDEX) + && arePrefixesPresent(argMultimap, PREFIX_DASH_NAME))) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddShiftCommand.MESSAGE_USAGE)); + } + + try { + if (argMultimap.getValue(PREFIX_DASH_INDEX).isPresent()) { + if (!ParserUtil.isValidInt(argMultimap.getValue(PREFIX_DASH_INDEX).get())) { + throw new ParseException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + index = ParserUtil.parseIndex(argMultimap.getValue(PREFIX_DASH_INDEX).get()); + } + if (argMultimap.getValue(PREFIX_DASH_NAME).isPresent()) { + name = ParserUtil.parseName(argMultimap.getValue(PREFIX_DASH_NAME).get()); + } + if (argMultimap.getValue(PREFIX_DATE).isPresent()) { + dates = extractTupleDates(argMultimap); + } + shiftDayAndSlot = ParserUtil.parseDayOfWeekAndSlot(argMultimap.getValue(PREFIX_DAY_SHIFT).get()); + + } catch (ParseException pe) { + if (pe.getMessage().equals(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX)) { + throw pe; + } + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddShiftCommand.MESSAGE_USAGE), pe); + } + if (dates[0].isAfter(dates[1])) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + DATES_IN_WRONG_ORDER)); + } + + return new AddShiftCommand(index, name, shiftDayAndSlot, dates[0], dates[1]); + } + + + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } +} diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java index 1e466792b46..3db3262bea7 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.MESSAGE_INVALID_COMMAND_FORMAT; import static seedu.address.commons.core.Messages.MESSAGE_UNKNOWN_COMMAND; @@ -7,14 +8,26 @@ import java.util.regex.Pattern; import seedu.address.logic.commands.AddCommand; +import seedu.address.logic.commands.AddShiftCommand; import seedu.address.logic.commands.ClearCommand; import seedu.address.logic.commands.Command; import seedu.address.logic.commands.DeleteCommand; +import seedu.address.logic.commands.DeleteShiftCommand; import seedu.address.logic.commands.EditCommand; import seedu.address.logic.commands.ExitCommand; import seedu.address.logic.commands.FindCommand; import seedu.address.logic.commands.HelpCommand; import seedu.address.logic.commands.ListCommand; +import seedu.address.logic.commands.MarkCommand; +import seedu.address.logic.commands.RemoveMarkCommand; +import seedu.address.logic.commands.SchedulePeriodChangeCommand; +import seedu.address.logic.commands.SetRoleReqCommand; +import seedu.address.logic.commands.SetShiftTimeCommand; +import seedu.address.logic.commands.StaffIndividualStatisticsCommand; +import seedu.address.logic.commands.StaffStatisticsCommand; +import seedu.address.logic.commands.SwapShiftCommand; +import seedu.address.logic.commands.SwitchTabCommand; +import seedu.address.logic.commands.ViewShiftCommand; import seedu.address.logic.parser.exceptions.ParseException; /** @@ -53,21 +66,57 @@ public Command parseCommand(String userInput) throws ParseException { case DeleteCommand.COMMAND_WORD: return new DeleteCommandParser().parse(arguments); + case ViewShiftCommand.COMMAND_WORD: + return new ViewShiftCommandParser().parse(arguments); + + case MarkCommand.COMMAND_WORD: + return new MarkCommandParser().parse(arguments); + + case RemoveMarkCommand.COMMAND_WORD: + return new RemoveMarkCommandParser().parse(arguments); + + case SwitchTabCommand.COMMAND_WORD: + return new SwitchTabCommand(); + + case DeleteShiftCommand.COMMAND_WORD: + return new DeleteShiftCommandParser().parse(arguments); + + case StaffIndividualStatisticsCommand.COMMAND_WORD: + return new StaffIndividualStatisticsCommandParser().parse(arguments); + + case SwapShiftCommand.COMMAND_WORD: + return new SwapShiftCommandParser().parse(arguments); + case ClearCommand.COMMAND_WORD: return new ClearCommand(); case FindCommand.COMMAND_WORD: return new FindCommandParser().parse(arguments); + case AddShiftCommand.COMMAND_WORD: + return new AddShiftCommandParser().parse(arguments); + + case SetShiftTimeCommand.COMMAND_WORD: + return new SetShiftTimeCommandParser().parse(arguments); + case ListCommand.COMMAND_WORD: return new ListCommand(); + case SetRoleReqCommand.COMMAND_WORD: + return new SetRoleReqCommandParser().parse(arguments); + + case SchedulePeriodChangeCommand.COMMAND_WORD: + return new SchedulePeriodChangeCommandParser().parse(arguments); + case ExitCommand.COMMAND_WORD: return new ExitCommand(); case HelpCommand.COMMAND_WORD: return new HelpCommand(); + case StaffStatisticsCommand.COMMAND_WORD: + return new StaffStatisticsCommand(); + default: throw new ParseException(MESSAGE_UNKNOWN_COMMAND); } diff --git a/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java b/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java index 954c8e18f8e..b2512347473 100644 --- a/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java +++ b/src/main/java/seedu/address/logic/parser/ArgumentMultimap.java @@ -15,7 +15,9 @@ */ public class ArgumentMultimap { - /** Prefixes mapped to their respective arguments**/ + /** + * Prefixes mapped to their respective arguments + **/ private final Map> argMultimap = new HashMap<>(); /** @@ -57,4 +59,12 @@ public List getAllValues(Prefix prefix) { public String getPreamble() { return getValue(new Prefix("")).orElse(""); } + + /** + * Returns true if the argMultiMap is empty. + */ + public boolean isEmpty() { + return argMultimap.size() == 1; + } + } diff --git a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java b/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java index 5c9aebfa488..d946ff16e64 100644 --- a/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java +++ b/src/main/java/seedu/address/logic/parser/ArgumentTokenizer.java @@ -7,11 +7,11 @@ /** * Tokenizes arguments string of the form: {@code preamble value value ...}
                            - * e.g. {@code some preamble text t/ 11.00 t/12.00 k/ m/ July} where prefixes are {@code t/ k/ m/}.
                            + * e.g. {@code some preamble text t/ 11.00 t/12.00 k/ m/ July} where prefixes are {@code t/ k/ m/}.
                            * 1. An argument's value can be an empty string e.g. the value of {@code k/} in the above example.
                            * 2. Leading and trailing whitespaces of an argument value will be discarded.
                            * 3. An argument may be repeated and all its values will be accumulated e.g. the value of {@code t/} - * in the above example.
                            + * in the above example.
                            */ public class ArgumentTokenizer { @@ -21,7 +21,7 @@ public class ArgumentTokenizer { * * @param argsString Arguments string of the form: {@code preamble value value ...} * @param prefixes Prefixes to tokenize the arguments string with - * @return ArgumentMultimap object that maps prefixes to their arguments + * @return ArgumentMultimap object that maps prefixes to their arguments */ public static ArgumentMultimap tokenize(String argsString, Prefix... prefixes) { List positions = findAllPrefixPositions(argsString, prefixes); @@ -33,7 +33,7 @@ public static ArgumentMultimap tokenize(String argsString, Prefix... prefixes) { * * @param argsString Arguments string of the form: {@code preamble value value ...} * @param prefixes Prefixes to find in the arguments string - * @return List of zero-based prefix positions in the given arguments string + * @return List of zero-based prefix positions in the given arguments string */ private static List findAllPrefixPositions(String argsString, Prefix... prefixes) { return Arrays.stream(prefixes) @@ -62,7 +62,7 @@ private static List findPrefixPositions(String argsString, Prefi * {@code argsString} starting from index {@code fromIndex}. An occurrence * is valid if there is a whitespace before {@code prefix}. Returns -1 if no * such occurrence can be found. - * + *

                            * E.g if {@code argsString} = "e/hip/900", {@code prefix} = "p/" and * {@code fromIndex} = 0, this method returns -1 as there are no valid * occurrences of "p/" with whitespace before it. However, if @@ -82,7 +82,7 @@ private static int findPrefixPosition(String argsString, String prefix, int from * * @param argsString Arguments string of the form: {@code preamble value value ...} * @param prefixPositions Zero-based positions of all prefixes in {@code argsString} - * @return ArgumentMultimap object that maps prefixes to their arguments + * @return ArgumentMultimap object that maps prefixes to their arguments */ private static ArgumentMultimap extractArguments(String argsString, List prefixPositions) { @@ -114,8 +114,8 @@ private static ArgumentMultimap extractArguments(String argsString, List { /** * Parses the given {@code String} of arguments in the context of the DeleteCommand * and returns a DeleteCommand object for execution. + * * @throws ParseException if the user input does not conform the expected format */ public DeleteCommand parse(String args) throws ParseException { - try { - Index index = ParserUtil.parseIndex(args); + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_DASH_INDEX, PREFIX_DASH_NAME, PREFIX_DASH_ROLE, + PREFIX_DASH_STATUS); + + if (!exactlyOneAcceptedPrefix(argMultimap)) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE)); + } + + if (argMultimap.getValue(PREFIX_DASH_INDEX).isPresent()) { + if (argMultimap.getValue(PREFIX_DASH_INDEX).get() == "") { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE)); + } + + Index index = ParserUtil.parseIndex(argMultimap.getValue(PREFIX_DASH_INDEX).get()); return new DeleteCommand(index); + } + + try { + if (argMultimap.getValue(PREFIX_DASH_NAME).isPresent()) { + Name name = ParserUtil.parseName(argMultimap.getValue(PREFIX_DASH_NAME).get()); + return new DeleteCommand(name); + } else if (argMultimap.getValue(PREFIX_DASH_ROLE).isPresent()) { + Role role = ParserUtil.parseRole(argMultimap.getValue(PREFIX_DASH_ROLE).get()); + return new DeleteCommand(role); + } else if (argMultimap.getValue(PREFIX_DASH_STATUS).isPresent()) { + Status status = ParserUtil.parseStatus(argMultimap.getValue(PREFIX_DASH_STATUS).get()); + return new DeleteCommand(status); + } else { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE)); + } } catch (ParseException pe) { throw new ParseException( String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteCommand.MESSAGE_USAGE), pe); } } + private boolean exactlyOneAcceptedPrefix(ArgumentMultimap argMultimap) { + Prefix[] prefixes = {PREFIX_DASH_INDEX, PREFIX_DASH_NAME, PREFIX_DASH_ROLE, PREFIX_DASH_STATUS}; + boolean isTrueOnlyOnce = false; + for (Prefix prefix : prefixes) { + if (argMultimap.getValue(prefix).isPresent()) { + if (isTrueOnlyOnce) { + return false; + } + isTrueOnlyOnce = true; + } + } + return isTrueOnlyOnce; + } } diff --git a/src/main/java/seedu/address/logic/parser/DeleteShiftCommandParser.java b/src/main/java/seedu/address/logic/parser/DeleteShiftCommandParser.java new file mode 100644 index 00000000000..ae9094d2676 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/DeleteShiftCommandParser.java @@ -0,0 +1,92 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.DATES_IN_WRONG_ORDER; +import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_INDEX; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DAY_SHIFT; +import static seedu.address.logic.parser.ParserUtil.extractTupleDates; + +import java.time.LocalDate; +import java.util.stream.Stream; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.DateTimeUtil; +import seedu.address.logic.commands.DeleteShiftCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Name; + +/** + * Parses input arguments and creates a new DeleteShiftCommand object. + */ +public class DeleteShiftCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the DeleteShiftCommand + * and returns an DeleteShiftCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format. + */ + @Override + public DeleteShiftCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_DASH_INDEX, PREFIX_DAY_SHIFT, PREFIX_DASH_NAME, PREFIX_DATE); + + Index index = null; + Name name = null; + String shiftDayAndSlot; + LocalDate[] dates = DateTimeUtil.getDisplayedDateArray(); + //PREFIX_DAY_SHIFT must exist and exactly one from PREFIX_DASH_INDEX and PREFIX_DASH_NAME must exist. + if (!arePrefixesPresent(argMultimap, PREFIX_DAY_SHIFT) + || !argMultimap.getPreamble().isEmpty() || (!arePrefixesPresent(argMultimap, PREFIX_DASH_INDEX) + && !arePrefixesPresent(argMultimap, PREFIX_DASH_NAME)) + || (arePrefixesPresent(argMultimap, PREFIX_DASH_INDEX) + && arePrefixesPresent(argMultimap, PREFIX_DASH_NAME))) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, DeleteShiftCommand.MESSAGE_USAGE)); + } + + try { + if (argMultimap.getValue(PREFIX_DASH_INDEX).isPresent()) { + if (!ParserUtil.isValidInt(argMultimap.getValue(PREFIX_DASH_INDEX).get())) { + throw new ParseException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + index = ParserUtil.parseIndex(argMultimap.getValue(PREFIX_DASH_INDEX).get()); + } + if (argMultimap.getValue(PREFIX_DASH_NAME).isPresent()) { + name = ParserUtil.parseName(argMultimap.getValue(PREFIX_DASH_NAME).get()); + } + if (argMultimap.getValue(PREFIX_DATE).isPresent()) { + dates = extractTupleDates(argMultimap); + + } + } catch (ParseException pe) { + if (pe.getMessage().equals(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX)) { + throw pe; + } + + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + DeleteShiftCommand.MESSAGE_USAGE), pe); + } + + shiftDayAndSlot = ParserUtil.parseDayOfWeekAndSlot(argMultimap.getValue(PREFIX_DAY_SHIFT).get()); + + if (dates[0].isAfter(dates[1])) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + DATES_IN_WRONG_ORDER)); + } + + return new DeleteShiftCommand(index, name, shiftDayAndSlot, dates[0], dates[1]); + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } +} diff --git a/src/main/java/seedu/address/logic/parser/EditCommandParser.java b/src/main/java/seedu/address/logic/parser/EditCommandParser.java index 845644b7dea..ea772ab1a85 100644 --- a/src/main/java/seedu/address/logic/parser/EditCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/EditCommandParser.java @@ -2,10 +2,15 @@ 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.commons.core.Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_INDEX; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_NAME; 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_ROLE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SALARY; +import static seedu.address.logic.parser.CliSyntax.PREFIX_STATUS; import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; import java.util.Collection; @@ -17,6 +22,8 @@ import seedu.address.logic.commands.EditCommand; import seedu.address.logic.commands.EditCommand.EditPersonDescriptor; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Name; +import seedu.address.model.person.Role; import seedu.address.model.tag.Tag; /** @@ -27,22 +34,30 @@ public class EditCommandParser implements Parser { /** * Parses the given {@code String} of arguments in the context of the EditCommand * and returns an EditCommand object for execution. + * * @throws ParseException if the user input does not conform the expected format */ public EditCommand parse(String args) throws ParseException { requireNonNull(args); ArgumentMultimap argMultimap = - ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_ADDRESS, PREFIX_TAG); + ArgumentTokenizer.tokenize(args, PREFIX_DASH_INDEX, PREFIX_DASH_NAME, PREFIX_NAME, + PREFIX_PHONE, PREFIX_EMAIL, PREFIX_TAG, PREFIX_STATUS, PREFIX_SALARY, + PREFIX_ROLE); - Index index; - - try { - index = ParserUtil.parseIndex(argMultimap.getPreamble()); - } catch (ParseException pe) { - throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE), pe); + if (!argMultimap.getValue(PREFIX_DASH_INDEX).isPresent() + && !argMultimap.getValue(PREFIX_DASH_NAME).isPresent()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE)); } - EditPersonDescriptor editPersonDescriptor = new EditPersonDescriptor(); + if (argMultimap.getValue(PREFIX_STATUS).isPresent()) { + editPersonDescriptor.setStatus(ParserUtil.parseStatus(argMultimap.getValue(PREFIX_STATUS).get())); + } + if (argMultimap.getValue(PREFIX_SALARY).isPresent()) { + editPersonDescriptor.setSalary(ParserUtil.parseSalary(argMultimap.getValue(PREFIX_SALARY).get())); + } + if (argMultimap.getValue(PREFIX_ROLE).isPresent()) { + parseRolesForEdit(argMultimap.getAllValues(PREFIX_ROLE)).ifPresent(editPersonDescriptor::setRoles); + } if (argMultimap.getValue(PREFIX_NAME).isPresent()) { editPersonDescriptor.setName(ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get())); } @@ -52,16 +67,34 @@ 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_ADDRESS).isPresent()) { - editPersonDescriptor.setAddress(ParserUtil.parseAddress(argMultimap.getValue(PREFIX_ADDRESS).get())); - } + parseTagsForEdit(argMultimap.getAllValues(PREFIX_TAG)).ifPresent(editPersonDescriptor::setTags); if (!editPersonDescriptor.isAnyFieldEdited()) { throw new ParseException(EditCommand.MESSAGE_NOT_EDITED); } + try { + if (argMultimap.getValue(PREFIX_DASH_INDEX).isPresent()) { + if (!ParserUtil.isValidInt(argMultimap.getValue(PREFIX_DASH_INDEX).get())) { + throw new ParseException(MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + + Index index = ParserUtil.parseIndex(argMultimap.getValue(PREFIX_DASH_INDEX).get()); + return new EditCommand(index, editPersonDescriptor); + } + if (argMultimap.getValue(PREFIX_DASH_NAME).isPresent()) { + Name name = ParserUtil.parseName(argMultimap.getValue(PREFIX_DASH_NAME).get()); + return new EditCommand(name, editPersonDescriptor); + } + + } catch (ParseException e) { + if (e.getMessage().equals(MESSAGE_INVALID_PERSON_DISPLAYED_INDEX)) { + throw e; + } + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE), e); + } + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, EditCommand.MESSAGE_USAGE)); - return new EditCommand(index, editPersonDescriptor); } /** @@ -79,4 +112,18 @@ private Optional> parseTagsForEdit(Collection tags) throws Pars return Optional.of(ParserUtil.parseTags(tagSet)); } + /** + * Parses {@code Collection roles} into a {@code Set} if {@code roles} is non-empty. + * If {@code roles} contain only one element which is an empty string, it will be parsed into a + * {@code Set} containing zero tags. + */ + private Optional> parseRolesForEdit(Collection roles) throws ParseException { + assert roles != null; + + if (roles.isEmpty()) { + return Optional.empty(); + } + Collection roleSet = roles.size() == 1 && roles.contains("") ? Collections.emptySet() : roles; + return Optional.of(ParserUtil.parseRoles(roleSet)); + } } diff --git a/src/main/java/seedu/address/logic/parser/FindCommandParser.java b/src/main/java/seedu/address/logic/parser/FindCommandParser.java index 4fb71f23103..577b7cb1093 100644 --- a/src/main/java/seedu/address/logic/parser/FindCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/FindCommandParser.java @@ -1,12 +1,23 @@ package seedu.address.logic.parser; import static seedu.address.commons.core.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_INDEX; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_ROLE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_SALARY; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_STATUS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_TAG; +import static seedu.address.logic.parser.ParserUtil.testByAllFieldsExceptName; import java.util.Arrays; +import seedu.address.commons.core.Messages; import seedu.address.logic.commands.FindCommand; import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.address.model.person.predicates.NameContainsKeywordsPredicate; +import seedu.address.model.person.predicates.PersonContainsFieldsPredicate; /** * Parses input arguments and creates a new FindCommand object @@ -16,18 +27,45 @@ public class FindCommandParser implements Parser { /** * Parses the given {@code String} of arguments in the context of the FindCommand * and returns a FindCommand object for execution. + * * @throws ParseException if the user input does not conform the expected format */ public FindCommand parse(String args) throws ParseException { - String trimmedArgs = args.trim(); - if (trimmedArgs.isEmpty()) { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, + PREFIX_DASH_NAME, PREFIX_DASH_PHONE, PREFIX_DASH_INDEX, + PREFIX_DASH_EMAIL, PREFIX_DASH_TAG, PREFIX_DASH_STATUS, + PREFIX_DASH_ROLE, PREFIX_DASH_SALARY); + + PersonContainsFieldsPredicate predicate = testByAllFieldsExceptName(argMultimap); + + if (argMultimap.getValue(PREFIX_DASH_NAME).isPresent() + && argMultimap.getValue(PREFIX_DASH_INDEX).isPresent()) { throw new ParseException( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.FIND_COMMAND_ONLY_NAME_OR_INDEX)); } - String[] nameKeywords = trimmedArgs.split("\\s+"); + if (argMultimap.getValue(PREFIX_DASH_INDEX).isPresent()) { + if (!ParserUtil.isValidInt(argMultimap.getValue(PREFIX_DASH_INDEX).get())) { + throw new ParseException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + return new FindCommand(ParserUtil + .parseIndex(argMultimap.getValue(PREFIX_DASH_INDEX).get()).getZeroBased(), predicate); + } + if (argMultimap.getValue(PREFIX_DASH_NAME).isPresent()) { + String[] nameKeywords = argMultimap.getValue(PREFIX_DASH_NAME).get() + .trim().split("\\s+"); + if (nameKeywords[0].equals("")) { + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); + } + return new FindCommand(new NameContainsKeywordsPredicate(Arrays.asList(nameKeywords)), + predicate); - return new FindCommand(new NameContainsKeywordsPredicate(Arrays.asList(nameKeywords))); + } + if (!predicate.isEmpty()) { + return new FindCommand(predicate); + } + throw new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); } - } diff --git a/src/main/java/seedu/address/logic/parser/MarkCommandParser.java b/src/main/java/seedu/address/logic/parser/MarkCommandParser.java new file mode 100644 index 00000000000..d27bc2a29e4 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/MarkCommandParser.java @@ -0,0 +1,59 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_INDEX; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_ROLE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_SALARY; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_STATUS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_TAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.DateTimeUtil; +import seedu.address.logic.commands.MarkCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Period; +import seedu.address.model.person.predicates.PersonContainsFieldsPredicate; + + +public class MarkCommandParser implements Parser { + + private static final ParseException NO_FIELD_EXCEPTION = new ParseException(MarkCommand.MESSAGE_USAGE); + + @Override + public MarkCommand parse(String userInput) throws ParseException { + //created to test if there are any identifiers + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(userInput, PREFIX_DASH_NAME, PREFIX_DASH_PHONE, + PREFIX_DASH_INDEX, PREFIX_DATE, + PREFIX_DASH_EMAIL, PREFIX_DASH_TAG, PREFIX_DASH_STATUS, + PREFIX_DASH_ROLE, PREFIX_DASH_SALARY); + + Period period = DateTimeUtil.getDisplayedPeriod(); + if (argMultimap.getValue(PREFIX_DATE).isPresent()) { + period = ParserUtil.extractPeriodDates(argMultimap); + } + PersonContainsFieldsPredicate predicate = ParserUtil.testByAllFields(argMultimap); + //checks for index + if (argMultimap.getValue(PREFIX_DASH_INDEX).isPresent()) { + if (!ParserUtil.isValidInt(argMultimap.getValue(PREFIX_DASH_INDEX).get())) { + throw new ParseException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + Index index = ParserUtil.parseIndex(argMultimap.getValue(PREFIX_DASH_INDEX).get()); + return new MarkCommand(index, period, predicate); + } + //checks for empty + if (predicate.isEmpty()) { + throw NO_FIELD_EXCEPTION; + } + + return new MarkCommand(predicate, period); + + } + + + +} diff --git a/src/main/java/seedu/address/logic/parser/Parser.java b/src/main/java/seedu/address/logic/parser/Parser.java index d6551ad8e3f..ce644a9c6fd 100644 --- a/src/main/java/seedu/address/logic/parser/Parser.java +++ b/src/main/java/seedu/address/logic/parser/Parser.java @@ -10,6 +10,7 @@ public interface Parser { /** * Parses {@code userInput} into a command and returns it. + * * @throws ParseException if {@code userInput} does not conform the expected format */ T parse(String userInput) throws ParseException; diff --git a/src/main/java/seedu/address/logic/parser/ParserCheckedFunction.java b/src/main/java/seedu/address/logic/parser/ParserCheckedFunction.java new file mode 100644 index 00000000000..8466d5c629a --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ParserCheckedFunction.java @@ -0,0 +1,8 @@ +package seedu.address.logic.parser; + +import seedu.address.logic.parser.exceptions.ParseException; + +@FunctionalInterface +public interface ParserCheckedFunction { + public R apply(T t) throws ParseException; +} diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java index b117acb9c55..84a513cabca 100644 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java @@ -1,18 +1,42 @@ 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.commons.core.Messages.WRONG_NUMBER_OF_DATES; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_ROLE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_SALARY; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_STATUS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_TAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ROLE; +import static seedu.address.model.person.Shift.isValidDayOfWeek; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; import java.util.Collection; import java.util.HashSet; +import java.util.List; import java.util.Set; +import java.util.stream.Stream; +import seedu.address.commons.core.Messages; import seedu.address.commons.core.index.Index; import seedu.address.commons.util.StringUtil; +import seedu.address.logic.commands.SetRoleReqCommand; import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.Address; import seedu.address.model.person.Email; import seedu.address.model.person.Name; +import seedu.address.model.person.Period; import seedu.address.model.person.Phone; +import seedu.address.model.person.Role; +import seedu.address.model.person.Salary; +import seedu.address.model.person.Status; +import seedu.address.model.person.predicates.PersonContainsFieldsPredicate; import seedu.address.model.tag.Tag; /** @@ -21,10 +45,12 @@ public class ParserUtil { public static final String MESSAGE_INVALID_INDEX = "Index is not a non-zero unsigned integer."; + public static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm"); /** * Parses {@code oneBasedIndex} into an {@code Index} and returns it. Leading and trailing whitespaces will be * trimmed. + * * @throws ParseException if the specified index is invalid (not non-zero unsigned integer). */ public static Index parseIndex(String oneBasedIndex) throws ParseException { @@ -65,21 +91,6 @@ public static Phone parsePhone(String phone) throws ParseException { return new Phone(trimmedPhone); } - /** - * Parses a {@code String address} into an {@code Address}. - * Leading and trailing whitespaces will be trimmed. - * - * @throws ParseException if the given {@code address} is invalid. - */ - public static Address parseAddress(String address) throws ParseException { - requireNonNull(address); - String trimmedAddress = address.trim(); - if (!Address.isValidAddress(trimmedAddress)) { - throw new ParseException(Address.MESSAGE_CONSTRAINTS); - } - return new Address(trimmedAddress); - } - /** * Parses a {@code String email} into an {@code Email}. * Leading and trailing whitespaces will be trimmed. @@ -95,6 +106,119 @@ public static Email parseEmail(String email) throws ParseException { return new Email(trimmedEmail); } + /** + * Parses {@code Collection roles} into a {@code Set}. + */ + public static Set parseRoles(Collection roles) throws ParseException { + requireNonNull(roles); + final Set roleSet = new HashSet<>(); + for (String roleName : roles) { + roleSet.add(parseRole(roleName)); + } + return roleSet; + } + + /** + * Parses a {@code String salary} into a {@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_CONSTRAINTS); + } + return new Salary(trimmedSalary); + } + + /** + * Parses a {@code String status} into an {@code Status}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code status} is invalid. + */ + public static Status parseStatuses(List status) throws ParseException { + int length = status.size(); + if (length == 0) { + return Status.NO_STATUS; + } + String statusLast = status.get(status.size() - 1); + String trimmedStatus = statusLast.trim(); + if (!Status.isValidStatus(trimmedStatus)) { + throw new ParseException(Status.MESSAGE_CONSTRAINTS); + } + return Status.translateStringToStatus(trimmedStatus); + } + + /** + * Parses a {@code String dayOfWeek} into an {@code DayOfWeek}. + * Leading and trailing whitespaces will be trimmed. + * This parser is not case sensitive. + * + * @throws ParseException if the given {@code dayOfWeek} is invalid. + */ + public static String parseDayOfWeekAndSlot(String shiftDay) throws ParseException { + String messageConstraints = "Valid input format:\n\n dayOfWeek-slotNumber:" + " List of valid dayOfWeek: " + + "Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday. (Not case-sensitive)\n\n" + + "List of valid slotNumber: 0, 1."; + requireNonNull(shiftDay); + String trimmedStr = shiftDay.trim().toLowerCase(); + String[] strings = trimmedStr.split("-"); + if (strings.length != 2) { + throw new ParseException(messageConstraints); + } + switch (strings[0]) { + case "monday": + case "tuesday": + case "wednesday": + case "thursday": + case "friday": + case "saturday": + case "sunday": + break; + default: throw new ParseException(messageConstraints); + } + switch (strings[1]) { + case "0": + case "1": + break; + default: throw new ParseException(messageConstraints); + } + return trimmedStr; + } + + /** + * Parses a dayOfWeek-time. + * Leading and trailing whitespaces will be trimmed. + * This parser is not case sensitive. + * + * @throws ParseException if the given {@code dayOfWeek} is invalid. + */ + public static String parseDayOfWeekAndTime(String shiftDay) throws ParseException { + String messageConstraints = "Valid input format: dayOfWeek-time:" + "List of valid dayOfWeek: " + + "Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday. (Not case-sensitive)\n" + + "valid time formats: HH:mm in 24-hour format, such as 13:00"; + requireNonNull(shiftDay); + String trimmedStr = shiftDay.trim().toLowerCase(); + String[] strings = trimmedStr.split("-"); + if (strings.length != 2) { + throw new ParseException(messageConstraints); + } + if (!isValidDayOfWeek(strings[0])) { + throw new ParseException(messageConstraints); + } + + try { + LocalTime.parse(strings[1], TIME_FORMATTER); + } catch (DateTimeParseException e) { + throw new ParseException(messageConstraints); + } + return trimmedStr; + } + + /** * Parses a {@code String tag} into a {@code Tag}. * Leading and trailing whitespaces will be trimmed. @@ -110,6 +234,39 @@ public static Tag parseTag(String tag) throws ParseException { return new Tag(trimmedTag); } + /** + * Parses a {@code String status} into a {@code Status}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code status} is invalid + */ + public static Status parseStatus(String status) throws ParseException { + requireNonNull(status); + String trimmedStatus = status.trim(); + try { + return Status.translateStringToStatus(trimmedStatus); + } catch (IllegalArgumentException e) { + throw new ParseException(Status.MESSAGE_CONSTRAINTS); + } + + } + + /** + * Parses a {@code String role} into a {@code Role}. + * Leading and trailing whitespaces will be trimmed. + * + * @throws ParseException if the given {@code role} is invalid. + */ + public static Role parseRole(String role) throws ParseException { + requireNonNull(role); + String trimmedRole = role.trim(); + try { + return Role.translateStringToRole(trimmedRole); + } catch (IllegalArgumentException e) { + throw new ParseException(Role.MESSAGE_CONSTRAINTS); + } + } + /** * Parses {@code Collection tags} into a {@code Set}. */ @@ -121,4 +278,207 @@ public static Set parseTags(Collection tags) throws ParseException } return tagSet; } + + /** + * Parses {@code Collection periods} to a {@code period} from the earliest date to the + * latest date in the collection. + * @throws ParseException When the input does not have the correct format. + */ + public static Period parsePeriod(Collection periods) throws ParseException { + LocalDate start = LocalDate.MAX; + LocalDate end = LocalDate.MIN; + for (String periodName : periods) { + if (start.isAfter(LocalDate.parse(periodName))) { + start = parseLocalDate(periodName); + } + if (end.isBefore(LocalDate.parse(periodName))) { + end = parseLocalDate(periodName); + } + } + return new Period(start, end); + } + + /** + * Parses {@code String value} to a {@code LocalDate}. + */ + public static LocalDate parseLocalDate(String value) throws ParseException { + try { + return LocalDate.parse(value); + } catch (DateTimeParseException dte) { + throw new ParseException(Messages.MESSAGE_INVALID_DATE_PARSED); + } + } + /** + * Parsers {@code String shiftTimes} to a {@code LocalTime[]} which contains the a start time and end time. + * @param shiftTimes The input string. + * @return A LocalTime array containing start time and end time of the shift. + * @throws ParseException throws when the input does not have the correct format. + */ + public static LocalTime[] parseShiftTime(String shiftTimes) throws ParseException { + LocalTime startTime; + LocalTime endTime; + String[] separatedShiftTimes = shiftTimes.split("-"); + if (separatedShiftTimes.length != 2) { + throw new ParseException(Messages.MESSAGE_INVALID_SHIFT_TIME); + } + try { + startTime = LocalTime.parse(separatedShiftTimes[0], TIME_FORMATTER); + endTime = LocalTime.parse(separatedShiftTimes[1], TIME_FORMATTER); + } catch (DateTimeParseException ite) { + throw new ParseException(Messages.MESSAGE_INVALID_TIME); + } + return new LocalTime[]{startTime, endTime}; + } + + /** + * Parses {@code args} into {@code PersonContainsFieldsPredicate} which tests a person for all + * of the qualifiers of the predicate. + * @throws ParseException Throws parse exception when the input is not something needed. + */ + public static PersonContainsFieldsPredicate testByAllFields(ArgumentMultimap argMultimap) throws ParseException { + requireNonNull(argMultimap); + PersonContainsFieldsPredicate predicate = new PersonContainsFieldsPredicate(); + predicate.addFieldToTest(argMultimap.getValue(PREFIX_DASH_NAME), ParserUtil::parseName); + predicate.addFieldToTest(argMultimap.getValue(PREFIX_DASH_PHONE), ParserUtil::parsePhone); + predicate.addFieldToTest(argMultimap.getValue(PREFIX_DASH_EMAIL), ParserUtil::parseEmail); + predicate.addFieldToTest(argMultimap.getAllValues(PREFIX_DASH_TAG), ParserUtil::parseTag); + try { + predicate.addFieldToTest(argMultimap.getAllValues(PREFIX_DASH_ROLE), + Role::translateStringToRoleWithNoRole); + predicate.addFieldToTest(argMultimap.getValue(PREFIX_DASH_SALARY), Salary::new); + predicate.addFieldToTest(argMultimap.getValue(PREFIX_DASH_STATUS), Status::translateStringToStatus); + } catch (IllegalArgumentException iae) { + throw new ParseException(iae.getMessage()); + } + return predicate; + } + + /** + * Parses {@code args} into {@code PersonContainsFieldsPredicate} which tests a person for all + * of the qualifiers of the predicate except for name. + * @throws ParseException Throws parse exception when the input is not something needed. + */ + public static PersonContainsFieldsPredicate testByAllFieldsExceptName(ArgumentMultimap argMultimap) + throws ParseException { + requireNonNull(argMultimap); + PersonContainsFieldsPredicate predicate = new PersonContainsFieldsPredicate(); + predicate.addFieldToTest(argMultimap.getValue(PREFIX_DASH_PHONE), ParserUtil::parsePhone); + predicate.addFieldToTest(argMultimap.getValue(PREFIX_DASH_EMAIL), ParserUtil::parseEmail); + predicate.addFieldToTest(argMultimap.getAllValues(PREFIX_DASH_TAG), ParserUtil::parseTag); + try { + predicate.addFieldToTest(argMultimap.getAllValues(PREFIX_DASH_ROLE), + Role::translateStringToRoleWithNoRole); + predicate.addFieldToTest(argMultimap.getValue(PREFIX_DASH_SALARY), Salary::new); + predicate.addFieldToTest(argMultimap.getValue(PREFIX_DASH_STATUS), Status::translateStringToStatus); + } catch (IllegalArgumentException iae) { + throw new ParseException(iae.getMessage()); + } + return predicate; + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + public static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } + + /** + * Parses the role requirements of form "role-number". + * + * @param roles The set of roles. + * @return A parsed set of role requirements as Strings of form "role-number". + * @throws ParseException If the roles cannot be parsed. + */ + public static Set parseRoleRequirements(Collection roles) throws ParseException { + requireNonNull(roles); + final Set roleSet = new HashSet<>(); + for (String roleReq : roles) { + roleReq = roleReq.trim().replace(PREFIX_ROLE.toString(), ""); + if (!isValidRoleRequirement(roleReq)) { + throw new ParseException( + String.format(Messages.MESSAGE_INVALID_COMMAND_FORMAT, SetRoleReqCommand.getHelpMessage())); + } + roleSet.add(roleReq); + } + return roleSet; + } + + private static boolean isValidRoleRequirement(String roleReq) { + String[] roleReqSplit = roleReq.split("-"); + + if (roleReqSplit.length != 2) { + return false; + } + + if (!Role.isValidRole(roleReqSplit[0]) || roleReqSplit[0].equals("norole")) { + return false; + } + + try { + Integer.parseInt(roleReqSplit[1]); + } catch (NumberFormatException e) { + return false; + } + + return true; + } + + /** + * Extracts tuple dates. Assumes that the input has two or one dates, and outputs the result in a + * {@code LocalDate[]} of size 2. + * + * @param argMultimap The argument multimap storing the dates. + * @return The start and end date saved as a tuple. + * @throws ParseException If parsing of the dates fails. + */ + public static LocalDate[] extractTupleDates(ArgumentMultimap argMultimap) throws ParseException { + LocalDate[] dateArray = new LocalDate[2]; + List dates = argMultimap.getAllValues(PREFIX_DATE); + if (dates.size() == 2) { + dateArray[0] = ParserUtil.parseLocalDate(dates.get(0)); + dateArray[1] = ParserUtil.parseLocalDate(dates.get(1)); + } else if (dates.size() == 1) { + dateArray[0] = ParserUtil.parseLocalDate(dates.get(0)); + dateArray[1] = dateArray[0].plusDays(6); + } else { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + String.format(WRONG_NUMBER_OF_DATES, dates.size()))); + } + if (dateArray[0].isAfter(dateArray[1])) { + throw new ParseException(Messages.DATES_IN_WRONG_ORDER); + } + return dateArray; + } + + /** + * Assumes that the input has two or one dates, and outputs the result in a {@code Period}. + */ + public static Period extractPeriodDates(ArgumentMultimap argMultimap) throws ParseException { + LocalDate[] dates = extractTupleDates(argMultimap); + return new Period(dates[0], dates[1]); + } + + /** + * Returns if a string contains a valid integer. + * + * @param test The string to be tested. + * @return Whether a string contains a valid integer. + */ + public static boolean isValidInt(String test) { + test = test.trim(); + if (!test.matches("\\d+") || test.equals("")) { + return false; + } + + try { + Integer.parseInt(test); + // This exception will be caught if the integer exceeds max integer + } catch (NumberFormatException e) { + return false; + } + + return Integer.parseInt(test) > 0; + } } diff --git a/src/main/java/seedu/address/logic/parser/RemoveMarkCommandParser.java b/src/main/java/seedu/address/logic/parser/RemoveMarkCommandParser.java new file mode 100644 index 00000000000..511e6015745 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/RemoveMarkCommandParser.java @@ -0,0 +1,58 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_INDEX; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_ROLE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_SALARY; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_STATUS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_TAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.DateTimeUtil; +import seedu.address.logic.commands.RemoveMarkCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Period; +import seedu.address.model.person.predicates.PersonContainsFieldsPredicate; + +/** + * Class representing the parser for the remove mark command. + */ +public class RemoveMarkCommandParser implements Parser { + + private static final ParseException NO_FIELD_EXCEPTION = new ParseException(RemoveMarkCommand.MESSAGE_USAGE); + + + @Override + public RemoveMarkCommand parse(String userInput) throws ParseException { + //created to test if there are any identifiers + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(userInput, PREFIX_DASH_NAME, + PREFIX_DASH_PHONE, PREFIX_DASH_INDEX, PREFIX_DATE, + PREFIX_DASH_EMAIL, PREFIX_DASH_TAG, PREFIX_DASH_STATUS, + PREFIX_DASH_ROLE, PREFIX_DASH_SALARY); + //created to test if there are + Period period = DateTimeUtil.getDisplayedPeriod(); + if (argMultimap.getValue(PREFIX_DATE).isPresent()) { + period = ParserUtil.extractPeriodDates(argMultimap); + } + + PersonContainsFieldsPredicate predicate = ParserUtil.testByAllFields(argMultimap); + //checks for index + if (argMultimap.getValue(PREFIX_DASH_INDEX).isPresent()) { + if (!ParserUtil.isValidInt(argMultimap.getValue(PREFIX_DASH_INDEX).get())) { + throw new ParseException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + Index index = ParserUtil.parseIndex(argMultimap.getValue(PREFIX_DASH_INDEX).get()); + return new RemoveMarkCommand(predicate, index, period); + } + //checks for empty + if (predicate.isEmpty()) { + throw NO_FIELD_EXCEPTION; + } + return new RemoveMarkCommand(predicate, period); + } +} diff --git a/src/main/java/seedu/address/logic/parser/SchedulePeriodChangeCommandParser.java b/src/main/java/seedu/address/logic/parser/SchedulePeriodChangeCommandParser.java new file mode 100644 index 00000000000..2170b81e25e --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/SchedulePeriodChangeCommandParser.java @@ -0,0 +1,23 @@ +package seedu.address.logic.parser; + +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; + +import java.time.LocalDate; + +import seedu.address.logic.commands.SchedulePeriodChangeCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Period; + +public class SchedulePeriodChangeCommandParser implements Parser { + + @Override + public SchedulePeriodChangeCommand parse(String userInput) throws ParseException { + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(userInput, PREFIX_DATE); + if (argMultimap.getValue(PREFIX_DATE).isEmpty() + || argMultimap.getAllValues(PREFIX_DATE).size() != 1) { + throw new ParseException(SchedulePeriodChangeCommand.HELP_MESSAGE); + } + LocalDate firstDay = ParserUtil.parseLocalDate(argMultimap.getValue(PREFIX_DATE).get()); + return new SchedulePeriodChangeCommand(Period.oneWeekFrom(firstDay)); + } +} diff --git a/src/main/java/seedu/address/logic/parser/SetRoleReqCommandParser.java b/src/main/java/seedu/address/logic/parser/SetRoleReqCommandParser.java new file mode 100644 index 00000000000..57f052831e6 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/SetRoleReqCommandParser.java @@ -0,0 +1,37 @@ +package seedu.address.logic.parser; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ROLE_REQUIREMENTS; +import static seedu.address.logic.parser.ParserUtil.arePrefixesPresent; + +import java.util.Set; + +import seedu.address.commons.core.Messages; +import seedu.address.logic.commands.SetRoleReqCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +public class SetRoleReqCommandParser implements Parser { + + + /** + * Parses the given {@code String} of arguments in the context of the SetRoleReqCommand + * and returns a SetRoleReqCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format + */ + @Override + public SetRoleReqCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_ROLE_REQUIREMENTS); + + if (!arePrefixesPresent(argMultimap, PREFIX_ROLE_REQUIREMENTS) || !argMultimap.getPreamble().isEmpty()) { + throw new ParseException( + String.format(Messages.MESSAGE_INVALID_COMMAND_FORMAT, SetRoleReqCommand.getHelpMessage())); + } + + Set roleReqList = ParserUtil.parseRoleRequirements(argMultimap.getAllValues(PREFIX_ROLE_REQUIREMENTS)); + return new SetRoleReqCommand(roleReqList); + } + + +} diff --git a/src/main/java/seedu/address/logic/parser/SetShiftTimeCommandParser.java b/src/main/java/seedu/address/logic/parser/SetShiftTimeCommandParser.java new file mode 100644 index 00000000000..a0c8d947b24 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/SetShiftTimeCommandParser.java @@ -0,0 +1,85 @@ +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_DASH_INDEX; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DAY_SHIFT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_SHIFT_TIME; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.stream.Stream; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.commons.util.DateTimeUtil; +import seedu.address.logic.commands.SetShiftTimeCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Name; + +public class SetShiftTimeCommandParser implements Parser { + /** + * Parses the given {@code String} of arguments in the context of the AddShiftCommand + * and returns an SetShiftTimeCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format. + */ + public SetShiftTimeCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, + PREFIX_DASH_INDEX, PREFIX_DASH_NAME, PREFIX_DAY_SHIFT, PREFIX_SHIFT_TIME, PREFIX_DATE); + + Index index = null; + Name name = null; + String shiftDayAndSlot; + LocalTime[] shiftTimes; + + + LocalDate[] dates = DateTimeUtil.getDisplayedDateArray(); + + + //PREFIX_DAY_SHIFT must exist and exactly one from PREFIX_INDEX and PREFIX_NAME must exist. + if (!arePrefixesPresent(argMultimap, PREFIX_DAY_SHIFT, PREFIX_SHIFT_TIME) + || !argMultimap.getPreamble().isEmpty() || (!arePrefixesPresent(argMultimap, PREFIX_DASH_INDEX) + && !arePrefixesPresent(argMultimap, PREFIX_DASH_NAME)) + || (arePrefixesPresent(argMultimap, PREFIX_DASH_INDEX) + && arePrefixesPresent(argMultimap, PREFIX_DASH_NAME))) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, SetShiftTimeCommand.MESSAGE_USAGE)); + } + + try { + if (argMultimap.getValue(PREFIX_DASH_INDEX).isPresent()) { + if (!ParserUtil.isValidInt(argMultimap.getValue(PREFIX_DASH_INDEX).get())) { + throw new ParseException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + index = ParserUtil.parseIndex(argMultimap.getValue(PREFIX_DASH_INDEX).get()); + } + if (argMultimap.getValue(PREFIX_DASH_NAME).isPresent()) { + name = ParserUtil.parseName(argMultimap.getValue(PREFIX_DASH_NAME).get()); + } + if (argMultimap.getValue(PREFIX_DATE).isPresent()) { + dates = ParserUtil.extractTupleDates(argMultimap); + } + shiftDayAndSlot = ParserUtil.parseDayOfWeekAndSlot(argMultimap.getValue(PREFIX_DAY_SHIFT).get()); + shiftTimes = ParserUtil.parseShiftTime(argMultimap.getValue(PREFIX_SHIFT_TIME).get()); + + } catch (ParseException pe) { + if (pe.getMessage().equals(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX)) { + throw pe; + } + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, + SetShiftTimeCommand.MESSAGE_USAGE), pe); + } + return new SetShiftTimeCommand(index, name, shiftDayAndSlot, shiftTimes, dates[0], dates[1]); + } + + /** + * Returns true if none of the prefixes contains empty {@code Optional} values in the given + * {@code ArgumentMultimap}. + */ + private static boolean arePrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { + return Stream.of(prefixes).allMatch(prefix -> argumentMultimap.getValue(prefix).isPresent()); + } +} diff --git a/src/main/java/seedu/address/logic/parser/StaffIndividualStatisticsCommandParser.java b/src/main/java/seedu/address/logic/parser/StaffIndividualStatisticsCommandParser.java new file mode 100644 index 00000000000..c6fbf6c5ed2 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/StaffIndividualStatisticsCommandParser.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.logic.parser.CliSyntax.PREFIX_DASH_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_INDEX; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_PHONE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_ROLE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_SALARY; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_STATUS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_TAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; + +import java.time.LocalDate; + +import seedu.address.commons.core.Messages; +import seedu.address.commons.core.index.Index; +import seedu.address.logic.commands.StaffIndividualStatisticsCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Period; +import seedu.address.model.person.predicates.PersonContainsFieldsPredicate; + +public class StaffIndividualStatisticsCommandParser implements Parser { + + private static final ParseException NO_FIELD_EXCEPTION = + new ParseException( + String.format(MESSAGE_INVALID_COMMAND_FORMAT, StaffIndividualStatisticsCommand.MESSAGE_USAGE)); + + @Override + public StaffIndividualStatisticsCommand parse(String userInput) throws ParseException { + //created to test if there are any identifiers + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(userInput, PREFIX_DASH_NAME, PREFIX_DASH_PHONE, + PREFIX_DASH_INDEX, PREFIX_DATE, PREFIX_DASH_EMAIL, PREFIX_DASH_TAG, + PREFIX_DASH_STATUS, PREFIX_DASH_ROLE, PREFIX_DASH_SALARY); + + Period period = Period.getPeriodFromDateOverMonth(LocalDate.now()); + PersonContainsFieldsPredicate predicate = ParserUtil.testByAllFields(argMultimap); + if (argMultimap.getValue(PREFIX_DATE).isPresent()) { + LocalDate[] dates = ParserUtil.extractTupleDates(argMultimap); + period = new Period(dates[0], dates[1]); + } + //checks for index + if (argMultimap.getValue(PREFIX_DASH_INDEX).isPresent()) { + if (!ParserUtil.isValidInt(argMultimap.getValue(PREFIX_DASH_INDEX).get())) { + throw new ParseException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); + } + Index index = ParserUtil.parseIndex(argMultimap.getValue(PREFIX_DASH_INDEX).get()); + return new StaffIndividualStatisticsCommand(predicate, index, period); + } + //checks for empty + if (predicate.isEmpty()) { + throw NO_FIELD_EXCEPTION; + } + + + return new StaffIndividualStatisticsCommand(predicate, period); + } +} diff --git a/src/main/java/seedu/address/logic/parser/SwapShiftCommandParser.java b/src/main/java/seedu/address/logic/parser/SwapShiftCommandParser.java new file mode 100644 index 00000000000..21c24c9c9d1 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/SwapShiftCommandParser.java @@ -0,0 +1,69 @@ +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_DASH_NAME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DAY_SHIFT; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +import seedu.address.commons.util.DateTimeUtil; +import seedu.address.logic.commands.SwapShiftCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Name; + +/** + * Parses input arguments and creates a new SwapShiftCommand object. + */ +public class SwapShiftCommandParser implements Parser { + + /** + * Parses the given {@code String} of arguments in the context of the SwapShiftCommand + * and returns a SwapShiftCommand object for execution. + * + * @throws ParseException if the user input does not conform the expected format. + */ + @Override + public SwapShiftCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_DAY_SHIFT, PREFIX_DASH_NAME, PREFIX_DATE); + LocalDate[] dates = DateTimeUtil.getDisplayedDateArray(); + + // Checks if there are exactly 2 "- n" fields and exactly 2 "d/" fields + if (argMultimap.getAllValues(PREFIX_DASH_NAME).size() != 2 + || argMultimap.getAllValues(PREFIX_DAY_SHIFT).size() != 2) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, SwapShiftCommand.MESSAGE_USAGE)); + } + + // Checks if the 2 names provided are unique + if (argMultimap.getAllValues(PREFIX_DASH_NAME).get(0).equals( + argMultimap.getAllValues(PREFIX_DASH_NAME).get(1))) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, SwapShiftCommand.NON_UNIQUE_NAMES)); + } + + // Checks if the 2 shifts provided are unique + if (argMultimap.getAllValues(PREFIX_DAY_SHIFT).get(0).equals( + argMultimap.getAllValues(PREFIX_DAY_SHIFT).get(1))) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, SwapShiftCommand.NON_UNIQUE_SHIFTS)); + } + + List nameList = new ArrayList<>(); + for (String name : argMultimap.getAllValues(PREFIX_DASH_NAME)) { + nameList.add(ParserUtil.parseName(name)); + } + + List shiftList = new ArrayList<>(); + for (String shift : argMultimap.getAllValues(PREFIX_DAY_SHIFT)) { + shiftList.add(ParserUtil.parseDayOfWeekAndSlot(shift)); + } + if (argMultimap.getValue(PREFIX_DATE).isPresent()) { + dates = ParserUtil.extractTupleDates(argMultimap); + } + + return new SwapShiftCommand(nameList, shiftList, dates[0], dates[1]); + } +} diff --git a/src/main/java/seedu/address/logic/parser/ViewShiftCommandParser.java b/src/main/java/seedu/address/logic/parser/ViewShiftCommandParser.java new file mode 100644 index 00000000000..702e96dffc1 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/ViewShiftCommandParser.java @@ -0,0 +1,105 @@ +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_DASH_DAY_SHIFT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DASH_TIME; +import static seedu.address.logic.parser.CliSyntax.PREFIX_DATE; +import static seedu.address.logic.parser.ParserUtil.arePrefixesPresent; +import static seedu.address.logic.parser.ParserUtil.parseDayOfWeekAndSlot; +import static seedu.address.logic.parser.ParserUtil.parseDayOfWeekAndTime; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; + +import seedu.address.commons.util.DateTimeUtil; +import seedu.address.logic.commands.ViewShiftCommand; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Period; + + +/** + * Class representing the find schedule command parser. + */ +public class ViewShiftCommandParser implements Parser { + + public static final String INVALID_VIEW_SHIFT_COMMAND = + String.format(MESSAGE_INVALID_COMMAND_FORMAT, ViewShiftCommand.HELP_MESSAGE); + public static final ParseException INVALID_VIEW_SHIFT_COMMAND_EXCEPTION = + new ParseException(INVALID_VIEW_SHIFT_COMMAND); + private static final String INVALID_NUMBER_OF_DATES = "Wrong number of dates input. Expecting 0 or 1, " + + "received %d date inputs."; + + + private DayOfWeek currDayOfWeek = DayOfWeek.from(LocalDate.now()); + private LocalTime currTime = LocalTime.now().truncatedTo(ChronoUnit.MINUTES); + public final ViewShiftCommand errorCommand = new ViewShiftCommand(currDayOfWeek, + ViewShiftCommand.INVALID_SLOT_NUMBER_INDICATING_EMPTY_PREFIXES, currTime, + new Period(LocalDate.now())); + + + @Override + public ViewShiftCommand parse(String args) throws ParseException { + // If it is empty, return a viewShift with the current day and time + if (args.trim().equals("")) { + return errorCommand; + } + + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, PREFIX_DASH_DAY_SHIFT, PREFIX_DASH_TIME, PREFIX_DATE); + checkPrefixes(argMultimap); + + int slotNum = ViewShiftCommand.INVALID_SLOT_NUMBER; + DayOfWeek dayOfWeek = null; // should not be null when ViewShiftCommand object is created + LocalTime time = null; + LocalDate[] dates = DateTimeUtil.getDisplayedDateArray(); + try { + // remove the prefix, then parse + if (argMultimap.getValue(PREFIX_DASH_TIME).isPresent()) { + String timeInput = argMultimap.getValue(PREFIX_DASH_TIME).get(); + String parsedArg = parseDayOfWeekAndTime(timeInput); + String[] parsedArgArray = parsedArg.split("-"); + dayOfWeek = DayOfWeek.valueOf(parsedArgArray[0].toUpperCase()); + time = LocalTime.parse(parsedArgArray[1], DateTimeFormatter.ofPattern("HH:mm")); + // slotNum will remain null + } + + if (argMultimap.getValue(PREFIX_DASH_DAY_SHIFT).isPresent()) { + String shiftInput = argMultimap.getValue(PREFIX_DASH_DAY_SHIFT).get(); + String parsedArg = parseDayOfWeekAndSlot(shiftInput); // returns [day]-[slot] + String[] parsedArgArray = parsedArg.split("-"); + dayOfWeek = DayOfWeek.valueOf(parsedArgArray[0].toUpperCase()); + slotNum = Integer.parseInt(parsedArgArray[1]); + // time remains as INVALID_SLOT_NUMBER + } + if (argMultimap.getValue(PREFIX_DATE).isPresent()) { + dates = ParserUtil.extractTupleDates(argMultimap); + } + } catch (ParseException pe) { + throw INVALID_VIEW_SHIFT_COMMAND_EXCEPTION; + } + if (argMultimap.getAllValues(PREFIX_DATE).size() != 1 + && argMultimap.getAllValues(PREFIX_DATE).size() != 0) { + throw new ParseException(String.format(INVALID_NUMBER_OF_DATES, + argMultimap.getAllValues(PREFIX_DATE).size())); + } + + return new ViewShiftCommand(dayOfWeek, slotNum, time, new Period(dates[0], dates[1])); + } + + private void checkPrefixes(ArgumentMultimap argMultimap) throws ParseException { + // Exactly one of PREFIX_DASH_DAY_SHIFT or PREFIX_DASH_TIME must exist + if (!arePrefixesPresent(argMultimap, PREFIX_DASH_DAY_SHIFT) + && !arePrefixesPresent(argMultimap, PREFIX_DASH_TIME)) { + throw INVALID_VIEW_SHIFT_COMMAND_EXCEPTION; + } + if (arePrefixesPresent(argMultimap, PREFIX_DASH_TIME) + && arePrefixesPresent(argMultimap, PREFIX_DASH_DAY_SHIFT)) { + throw INVALID_VIEW_SHIFT_COMMAND_EXCEPTION; + } + } +} diff --git a/src/main/java/seedu/address/model/AddressBook.java b/src/main/java/seedu/address/model/AddressBook.java index 1a943a0781a..6aff617ac7e 100644 --- a/src/main/java/seedu/address/model/AddressBook.java +++ b/src/main/java/seedu/address/model/AddressBook.java @@ -14,7 +14,7 @@ */ public class AddressBook implements ReadOnlyAddressBook { - private final UniquePersonList persons; + private final UniquePersonList staffs; /* * The 'unusual' code block below is a non-static initialization block, sometimes used to avoid duplication @@ -24,7 +24,7 @@ public class AddressBook implements ReadOnlyAddressBook { * among constructors. */ { - persons = new UniquePersonList(); + staffs = new UniquePersonList(); } public AddressBook() {} @@ -40,11 +40,11 @@ public AddressBook(ReadOnlyAddressBook toBeCopied) { //// list overwrite operations /** - * Replaces the contents of the person list with {@code persons}. + * Replaces the contents of the person list with {@code staffs}. * {@code persons} must not contain duplicate persons. */ - public void setPersons(List persons) { - this.persons.setPersons(persons); + public void setPersons(List staffs) { + this.staffs.setPersons(staffs); } /** @@ -61,17 +61,17 @@ public void resetData(ReadOnlyAddressBook newData) { /** * Returns true if a person with the same identity as {@code person} exists in the address book. */ - public boolean hasPerson(Person person) { - requireNonNull(person); - return persons.contains(person); + public boolean hasPerson(Person staff) { + requireNonNull(staff); + return staffs.contains(staff); } /** * Adds a person to the address book. * The person must not already exist in the address book. */ - public void addPerson(Person p) { - persons.add(p); + public void addPerson(Person staff) { + staffs.add(staff); } /** @@ -79,10 +79,10 @@ public void addPerson(Person p) { * {@code target} must exist in the address book. * The person identity of {@code editedPerson} must not be the same as another existing person in the address book. */ - public void setPerson(Person target, Person editedPerson) { - requireNonNull(editedPerson); + public void setPerson(Person target, Person editedStaff) { + requireNonNull(editedStaff); - persons.setPerson(target, editedPerson); + staffs.setPerson(target, editedStaff); } /** @@ -90,31 +90,31 @@ public void setPerson(Person target, Person editedPerson) { * {@code key} must exist in the address book. */ public void removePerson(Person key) { - persons.remove(key); + staffs.remove(key); } //// util methods @Override public String toString() { - return persons.asUnmodifiableObservableList().size() + " persons"; + return staffs.asUnmodifiableObservableList().size() + " persons"; // TODO: refine later } @Override public ObservableList getPersonList() { - return persons.asUnmodifiableObservableList(); + return staffs.asUnmodifiableObservableList(); } @Override public boolean equals(Object other) { return other == this // short circuit if same object || (other instanceof AddressBook // instanceof handles nulls - && persons.equals(((AddressBook) other).persons)); + && staffs.equals(((AddressBook) other).staffs)); } @Override public int hashCode() { - return persons.hashCode(); + return staffs.hashCode(); } } diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index d54df471c1f..021944fd7aa 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -1,11 +1,19 @@ package seedu.address.model; import java.nio.file.Path; +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalTime; import java.util.function.Predicate; import javafx.collections.ObservableList; import seedu.address.commons.core.GuiSettings; +import seedu.address.commons.exceptions.InvalidShiftTimeException; +import seedu.address.model.person.Name; import seedu.address.model.person.Person; +import seedu.address.model.person.Slot; +import seedu.address.model.person.exceptions.DuplicateShiftException; +import seedu.address.model.person.exceptions.NoShiftException; /** * The API of the Model component. @@ -67,21 +75,82 @@ public interface Model { * Adds the given person. * {@code person} must not already exist in the address book. */ - void addPerson(Person person); + void addPerson(Person staff); /** * Replaces the given person {@code target} with {@code editedPerson}. * {@code target} must exist in the address book. * The person identity of {@code editedPerson} must not be the same as another existing person in the address book. */ - void setPerson(Person target, Person editedPerson); + void setPerson(Person target, Person editedStaff); + + /** + * Retrieves the Person at the index specified of the observed filtered list. + * + * @param index The index of the Person desired + * @return the Person at the index of the filtered list specified. + */ + Person getFilteredPersonListByIndex(int index); + + + /** + * Returns the person with given name. + * @param name Given name. + * @return Matched Person. + */ + Person findPersonByName(Name name); + + /** + * Add a shift to a target staff's schedule. + * {@code target} must exist in the address book. + * + * @param target The target staff. + * @param dayOfWeek of the shift. + * @param slot of the shift. + * @param startDate The startDate of the shift. + * @throws DuplicateShiftException Throws when there is already a shift at the target slot. + */ + void addShift(Person target, DayOfWeek dayOfWeek, Slot slot, + LocalDate startDate, LocalDate endDate) throws DuplicateShiftException; + + /** + * Deletes a shift from a target staff's schedule. + * {@code target} must exist in the address book. + * + * @param target The target staff. + * @param dayOfWeek of the shift. + * @param slot of the shift. + * @param endDate The date that the shift ends at. + * @throws NoShiftException throws when a user tries to delete a shift that does not exist. + */ + void deleteShift(Person target, DayOfWeek dayOfWeek, Slot slot, LocalDate startDate, + LocalDate endDate) throws NoShiftException; /** Returns an unmodifiable view of the filtered person list */ ObservableList getFilteredPersonList(); + /** Returns an unmodifiable view of the unfiltered person list */ + ObservableList getUnFilteredPersonList(); + /** * Updates the filter of the filtered person list to filter by the given {@code predicate}. * @throws NullPointerException if {@code predicate} is null. */ void updateFilteredPersonList(Predicate predicate); + + /** + * Set time for a shift from a target staff's schedule. + * {@code target} must exist in the address book. + * + * @param target The target staff. + * @param dayOfWeek of the shift. + * @param slot of the shift. + * @param startTime The start time of the shift. + * @param endTime The end time of the shift. + * @throws InvalidShiftTimeException throws when the timings of Shift are invalid. + */ + void setShiftTime(Person target, DayOfWeek dayOfWeek, Slot slot, LocalTime startTime, LocalTime endTime, + LocalDate startDate, LocalDate endDate) + throws InvalidShiftTimeException; + } diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index 0650c954f5c..eb85913f9ab 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/seedu/address/model/ModelManager.java @@ -4,14 +4,25 @@ import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; import java.nio.file.Path; +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; import java.util.function.Predicate; import java.util.logging.Logger; +import java.util.stream.Collectors; import javafx.collections.ObservableList; import javafx.collections.transformation.FilteredList; import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; +import seedu.address.commons.exceptions.InvalidShiftTimeException; +import seedu.address.model.person.Name; import seedu.address.model.person.Person; +import seedu.address.model.person.Schedule; +import seedu.address.model.person.Slot; +import seedu.address.model.person.exceptions.DuplicateShiftException; +import seedu.address.model.person.exceptions.NoShiftException; /** * Represents the in-memory model of the address book data. @@ -29,7 +40,6 @@ public class ModelManager implements Model { public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyUserPrefs userPrefs) { super(); requireAllNonNull(addressBook, userPrefs); - logger.fine("Initializing with address book: " + addressBook + " and user prefs " + userPrefs); this.addressBook = new AddressBook(addressBook); @@ -100,16 +110,26 @@ public void deletePerson(Person target) { } @Override - public void addPerson(Person person) { - addressBook.addPerson(person); + public void addPerson(Person staff) { + addressBook.addPerson(staff); updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); } @Override - public void setPerson(Person target, Person editedPerson) { - requireAllNonNull(target, editedPerson); + public void setPerson(Person target, Person editedStaff) { + requireAllNonNull(target, editedStaff); + + addressBook.setPerson(target, editedStaff); + } - addressBook.setPerson(target, editedPerson); + @Override + public Person findPersonByName(Name name) { + List results = filteredPersons.stream().filter(person -> person.getName().equals(name)) + .collect(Collectors.toList()); + if (results.size() == 0) { + return null; + } + return results.get(0); } //=========== Filtered Person List Accessors ============================================================= @@ -123,12 +143,73 @@ public ObservableList getFilteredPersonList() { return filteredPersons; } + /** + * Returns the Person from the filtered list with the corresponding index. + * + * @return the Person from the filtered list with the corresponding index. + */ + @Override + public Person getFilteredPersonListByIndex(int index) { + if (filteredPersons.size() == 0) { + return new FilteredList<>(this.addressBook.getPersonList()).get(index); + } else { + return filteredPersons.get(index); + } + } + + + /** + * Returns the unfiltered person list. + * + * @return Unfiltered Person list. + */ + @Override + public ObservableList getUnFilteredPersonList() { + return new FilteredList<>(this.addressBook.getPersonList()); + } + + /** + * Updates the filtered person list based on the predicate. + * + * @param predicate This filters the filtered person list. + */ @Override public void updateFilteredPersonList(Predicate predicate) { requireNonNull(predicate); filteredPersons.setPredicate(predicate); } + + @Override + public void addShift(Person target, DayOfWeek dayOfWeek, Slot slot, + LocalDate startDate, LocalDate endDate) throws DuplicateShiftException { + requireAllNonNull(target, dayOfWeek, slot, startDate, endDate); + Person staffToReplaceWith = Person.copy(target); + Schedule editSchedule = target.getSchedule(); + editSchedule.addShift(dayOfWeek, slot, startDate, endDate); + staffToReplaceWith.setSchedule(editSchedule); + setPerson(target, staffToReplaceWith); + } + + @Override + public void setShiftTime(Person target, DayOfWeek dayOfWeek, Slot slot, LocalTime startTime, LocalTime endTime, + LocalDate startDate, LocalDate endDate) + throws InvalidShiftTimeException { + requireAllNonNull(target, dayOfWeek, slot, startTime, endTime, startDate, endDate); + target.setShiftTime(dayOfWeek, slot, startTime, endTime, startDate, endDate); + } + + @Override + public void deleteShift(Person target, DayOfWeek dayOfWeek, Slot slot, + LocalDate startDate, LocalDate endDate) throws NoShiftException { + requireAllNonNull(target, dayOfWeek, slot); + Person staffToReplaceWith = Person.copy(target); + Schedule editSchedule = target.getSchedule(); + editSchedule.removeShift(dayOfWeek, slot, startDate, endDate); + staffToReplaceWith.setSchedule(editSchedule); + setPerson(target, staffToReplaceWith); + } + @Override public boolean equals(Object obj) { // short circuit if same object @@ -147,5 +228,4 @@ public boolean equals(Object obj) { && userPrefs.equals(other.userPrefs) && filteredPersons.equals(other.filteredPersons); } - } diff --git a/src/main/java/seedu/address/model/RecurrencePeriod.java b/src/main/java/seedu/address/model/RecurrencePeriod.java new file mode 100644 index 00000000000..0e3685aebbe --- /dev/null +++ b/src/main/java/seedu/address/model/RecurrencePeriod.java @@ -0,0 +1,142 @@ +package seedu.address.model; + +import java.time.DayOfWeek; +import java.time.Duration; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import seedu.address.commons.util.DateTimeUtil; +import seedu.address.model.person.Period; +import seedu.address.model.person.Slot; + +/** + * Utility class to represent the recurrence of a shift over a period of dates. + */ +public class RecurrencePeriod extends Period { + private final Period period; + private final LocalTime startTime; + private final LocalTime endTime; + + /** + * Creates an {@code RecurrencePeriod} using its corresponding fields. + */ + public RecurrencePeriod(Period period, LocalTime startTime, LocalTime endTime) { + super(period); + this.period = period; + assert startTime.isBefore(endTime); + this.startTime = startTime; + this.endTime = endTime; + } + + /** + * Default recurrence period for test cases. + * + */ + public RecurrencePeriod(Period period, Slot slot) { + super(period); + this.period = period; + if (slot.equals(Slot.MORNING)) { + this.startTime = DateTimeUtil.getDefaultMorningStartTime(); + this.endTime = DateTimeUtil.getDefaultMorningEndTime(); + } else if (slot.equals(Slot.AFTERNOON)) { + this.startTime = DateTimeUtil.getDefaultAfternoonStartTime(); + this.endTime = DateTimeUtil.getDefaultAfternoonEndTime(); + } else { + throw new IllegalStateException("This should not occur"); + } + + + } + + public LocalTime getStartTime() { + return this.startTime; + } + + public LocalTime getEndTime() { + return this.endTime; + } + + public Period getPeriod() { + return this.period; + } + + public long getWorkingHour(DayOfWeek day, Period period) { + long numOfTimes = this.toList().stream() + .filter(d -> period.contains(d)) + .filter(d -> d.getDayOfWeek().equals(day)).count(); + return Duration.between(this.startTime, this.endTime).toHours() * numOfTimes; + } + + /** + * Unions the period represented by this {@code RecurrencePeriod} with the {@code RecurrencePeriod} + * that have an overlapping period and the same start and end time. + */ + public Collection unionByDuration(Collection periods) { + List result = new ArrayList<>(); + Collection filteredPeriods = periods.stream() + .filter(p -> p.isSameDuration(this)) + .collect(Collectors.toList()); + //remove the periods from the input + result.addAll(periods); + result.removeAll(filteredPeriods); + Collection underlyingPeriods = filteredPeriods.stream() + .map(p -> p.getPeriod()) + .collect(Collectors.toList()); + underlyingPeriods = this.period.union(underlyingPeriods); + result.addAll(underlyingPeriods.stream() + .map(p -> new RecurrencePeriod(p, startTime, endTime)) + .collect(Collectors.toList())); + return result; + + } + + + /** + * Removes the input period from {@code this}, while retaining the information + * of the recurring shift. + */ + public Collection complementWithInformation(Period period) { + Collection periods = super.complement(period); + return periods.stream() + .map(p -> new RecurrencePeriod(p, startTime, endTime)) + .collect(Collectors.toList()); + + } + + + private boolean isSameDuration(RecurrencePeriod period) { + return getStartTime().equals(period.getStartTime()) + && getEndTime().equals(period.getEndTime()); + } + + /** + * Checks if a specified timing is within the slot period. + * + * @param time The time which will be checked against. + * @return + */ + public boolean isWithinSlotPeriod(LocalTime time) { + return time.equals(startTime) || time.equals(endTime) + || time.isBefore(endTime) && time.isAfter(startTime); + } + + + /** + * Print result for viewSchedule command. + * + */ + public String toPrintString() { + return getStartTime() + "-" + getEndTime(); + } + + @Override + public boolean equals(Object o) { + return super.equals(o) + && o instanceof RecurrencePeriod + && isSameDuration((RecurrencePeriod) o); + + } +} diff --git a/src/main/java/seedu/address/model/UserPrefs.java b/src/main/java/seedu/address/model/UserPrefs.java index 25a5fd6eab9..2d748f38c3b 100644 --- a/src/main/java/seedu/address/model/UserPrefs.java +++ b/src/main/java/seedu/address/model/UserPrefs.java @@ -14,7 +14,7 @@ public class UserPrefs implements ReadOnlyUserPrefs { private GuiSettings guiSettings = new GuiSettings(); - private Path addressBookFilePath = Paths.get("data" , "addressbook.json"); + private Path addressBookFilePath = Paths.get("data" , "staffd.json"); /** * Creates a {@code UserPrefs} with default values. diff --git a/src/main/java/seedu/address/model/person/Address.java b/src/main/java/seedu/address/model/person/Address.java deleted file mode 100644 index 60472ca22a0..00000000000 --- a/src/main/java/seedu/address/model/person/Address.java +++ /dev/null @@ -1,57 +0,0 @@ -package seedu.address.model.person; - -import static java.util.Objects.requireNonNull; -import static seedu.address.commons.util.AppUtil.checkArgument; - -/** - * Represents a Person's address in the address book. - * Guarantees: immutable; is valid as declared in {@link #isValidAddress(String)} - */ -public class Address { - - public static final String MESSAGE_CONSTRAINTS = "Addresses can take any values, and it should not be blank"; - - /* - * The first character of the address must not be a whitespace, - * otherwise " " (a blank string) becomes a valid input. - */ - public static final String VALIDATION_REGEX = "[^\\s].*"; - - public final String value; - - /** - * Constructs an {@code Address}. - * - * @param address A valid address. - */ - public Address(String address) { - requireNonNull(address); - checkArgument(isValidAddress(address), MESSAGE_CONSTRAINTS); - value = address; - } - - /** - * Returns true if a given string is a valid email. - */ - public static boolean isValidAddress(String test) { - return test.matches(VALIDATION_REGEX); - } - - @Override - public String toString() { - return value; - } - - @Override - public boolean equals(Object other) { - return other == this // short circuit if same object - || (other instanceof Address // instanceof handles nulls - && value.equals(((Address) other).value)); // state check - } - - @Override - public int hashCode() { - return value.hashCode(); - } - -} diff --git a/src/main/java/seedu/address/model/person/Email.java b/src/main/java/seedu/address/model/person/Email.java index f866e7133de..30fb03a5eb3 100644 --- a/src/main/java/seedu/address/model/person/Email.java +++ b/src/main/java/seedu/address/model/person/Email.java @@ -7,14 +7,13 @@ * Represents a Person's email in the address book. * Guarantees: immutable; is valid as declared in {@link #isValidEmail(String)} */ -public class Email { +public class Email implements Field { - private static final String SPECIAL_CHARACTERS = "+_.-"; public static final String MESSAGE_CONSTRAINTS = "Emails should be of the format local-part@domain " + "and adhere to the following constraints:\n" + "1. The local-part should only contain alphanumeric characters and these special characters, excluding " - + "the parentheses, (" + SPECIAL_CHARACTERS + "). The local-part may not start or end with any special " - + "characters.\n" + + "the parentheses, (" + displaySpecialCharacters() + "). The local-part may not start or end with " + + "any special characters.\n" + "2. This is followed by a '@' and then a domain name. The domain name is made up of domain labels " + "separated by periods.\n" + "The domain name must:\n" @@ -22,6 +21,8 @@ public class Email { + " - have each domain label start and end with alphanumeric characters\n" + " - have each domain label consist of alphanumeric characters, separated only by hyphens, if any."; // alphanumeric and special characters + + private static final String SPECIAL_CHARACTERS = "+_.-"; private static final String ALPHANUMERIC_NO_UNDERSCORE = "[^\\W_]+"; // alphanumeric characters except underscore private static final String LOCAL_PART_REGEX = "^" + ALPHANUMERIC_NO_UNDERSCORE + "([" + SPECIAL_CHARACTERS + "]" + ALPHANUMERIC_NO_UNDERSCORE + ")*"; @@ -68,4 +69,17 @@ public int hashCode() { return value.hashCode(); } + private static String displaySpecialCharacters() { + String result = ""; + for (int i = 0; i < SPECIAL_CHARACTERS.length(); i++) { + char c = SPECIAL_CHARACTERS.charAt(i); + result += c; + + if (i != (SPECIAL_CHARACTERS.length() - 1)) { + result += " "; + } + } + return result; + } + } diff --git a/src/main/java/seedu/address/model/person/EmptyShift.java b/src/main/java/seedu/address/model/person/EmptyShift.java new file mode 100644 index 00000000000..0a7d4e144fe --- /dev/null +++ b/src/main/java/seedu/address/model/person/EmptyShift.java @@ -0,0 +1,45 @@ +package seedu.address.model.person; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + +import seedu.address.model.RecurrencePeriod; + +/** + * Class representing a shift that is not being used. + */ +public class EmptyShift extends Shift { + + /** + * Creates an {@code EmptyShift} object which represents a shift where the staff is not + * working from the oldest date in its history to now and the future. + */ + public EmptyShift(DayOfWeek dayOfWeek, Slot slot) { + super(dayOfWeek, slot); + } + + @Override + public boolean isEmpty() { + return true; + } + + + @Override + public boolean isWorking(LocalTime time, Period period) { + return false; + } + + + @Override + public Shift add(LocalDate startDate, LocalDate endDate) { + return new Shift(dayOfWeek, slot, List.of(new RecurrencePeriod(new Period(startDate, endDate), slot))); + } + + @Override + public Shift remove(LocalDate startDate, LocalDate endDate) { + throw new UnsupportedOperationException("This method should not be called."); + } + +} diff --git a/src/main/java/seedu/address/model/person/Field.java b/src/main/java/seedu/address/model/person/Field.java new file mode 100644 index 00000000000..088a44f3c69 --- /dev/null +++ b/src/main/java/seedu/address/model/person/Field.java @@ -0,0 +1,20 @@ +package seedu.address.model.person; + +import java.util.Set; + +/** + * Represents a field in a staff. + */ +public interface Field { + + /** + * Add input fields to field set. + * @param fieldSet The field set to add to. + * @param fields The fields to add. + */ + static void addToFieldSet(Set fieldSet, Field... fields) { + for (Field field: fields) { + fieldSet.add(field); + } + } +} diff --git a/src/main/java/seedu/address/model/person/Name.java b/src/main/java/seedu/address/model/person/Name.java index 79244d71cf7..e6d75cca6bd 100644 --- a/src/main/java/seedu/address/model/person/Name.java +++ b/src/main/java/seedu/address/model/person/Name.java @@ -7,7 +7,7 @@ * Represents a Person's name in the address book. * Guarantees: immutable; is valid as declared in {@link #isValidName(String)} */ -public class Name { +public class Name implements Field { public static final String MESSAGE_CONSTRAINTS = "Names should only contain alphanumeric characters and spaces, and it should not be blank"; diff --git a/src/main/java/seedu/address/model/person/Period.java b/src/main/java/seedu/address/model/person/Period.java new file mode 100644 index 00000000000..5ba5d8fa688 --- /dev/null +++ b/src/main/java/seedu/address/model/person/Period.java @@ -0,0 +1,321 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; + +import java.time.LocalDate; +import java.time.Month; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.Consumer; +import java.util.stream.Collectors; + +/** + * Class representing a period of dates. + */ +public class Period { + public static final String MESSAGE_CONSTRAINTS = + "Period should be a range of two dates with a space in between\n" + + "E.g. 1999-10-09 1999-11-15"; + + private static final String DELIMITER = " "; + + private LocalDate startDate; + private LocalDate endDate; + + /** + * Constructs a {@code Period} with two {@code LocalDate}, + * order is not needed. + */ + public Period(LocalDate startDate, LocalDate endDate) { + //to swap + if (startDate.isAfter(endDate)) { + this.startDate = endDate; + this.endDate = startDate; + } else { + this.startDate = startDate; + this.endDate = endDate; + } + + } + + /** + * Constructs a {@code Period} with the same data as + * {@code period}. + */ + public Period(Period period) { + this.startDate = period.startDate; + this.endDate = period.endDate; + } + + /** + * Constructs a {@code Period} with a single {@code date}. + */ + public Period(LocalDate date) { + this.startDate = date; + this.endDate = date; + } + + /** + * Returns true if any dates within {@code period} is within + * {@code this}. + */ + public boolean isWithin(Period period) { + return this.contains(period.startDate) + || this.contains(period.endDate); + } + + /** + * Obtains a {@code Period} representing the period across the month of the input date. + * E.g. with input date 2021-10-20, the resulting period will span 2021-10-01 to + * 2021-10-31. + */ + public static Period getPeriodFromDateOverMonth(LocalDate date) { + Month month = date.getMonth(); + int year = date.getYear(); + int lastDate = month.length(year % 4 == 0); + return new Period(LocalDate.of(year, month, 1), + LocalDate.of(year, month, lastDate)); + } + + /** + * Parses a {@code period} into a Period object. + * + * @throws DateTimeParseException when the input is invalid. + * @throws IllegalArgumentException when the input cannot be split. + */ + public static Period transformStringToPeriod(String period) { + String[] splitPeriod = period.split(DELIMITER); + if (splitPeriod.length != 2) { + throw new IllegalArgumentException(MESSAGE_CONSTRAINTS); + } + return new Period(LocalDate.parse(splitPeriod[0]), + LocalDate.parse(splitPeriod[1])); + + } + + /** + * Checks if {@code date} is contained by the period inclusive of the + * {@code start} and {@code end} of the Period object. Returns true + * if it is contained and false otherwise. + */ + public boolean contains(LocalDate date) { + return withinExclusively(date) + || atDelimiters(date); + } + + /** + * Returns true if {@code this} contains {@code period}. + */ + public boolean contains(Period period) { + return this.contains(period.startDate) + && this.contains(period.endDate); + } + + /** + * Gets the complement duration of {@code period} and the + * input period. + */ + public Collection complement(Period period) { + assert period.endDate.isAfter(period.startDate) + || period.endDate.isEqual(period.startDate); + assert endDate.isAfter(startDate) + || endDate.isEqual(startDate); + if (period.contains(this)) { + return List.of(); + } + if (contains(period)) { + //when it is contained + Period period1 = new Period(startDate, period.startDate.minusDays(1)); + Period period2 = new Period(period.endDate.plusDays(1), endDate); + if (startDate.equals(period.startDate)) { + //we know that period2 must be not in period + return List.of(period2); + } + if (endDate.equals(period.endDate)) { + //we know that period1 must be not in period + return List.of(period1); + } + return List.of(period1, period2); + } + // startDate period.startDate endDate period.endDate + // startDate <-> period.startDate - 1 + if (contains(period.startDate)) { + return List.of(new Period(startDate, period.startDate.minusDays(1))); + } + // period.startDate startDate period.endDate endDate + // period.endDate + 1 <-> endDate + if (contains(period.endDate)) { + return List.of(new Period(period.endDate.plusDays(1), endDate)); + } + + //when there is no need to + return List.of(this); + } + + /** + * Obtains the periods within the input set that lie in this. + * Assumes that the input periods do not have any overlaps. + */ + public Collection intersect(Collection periods) { + return periods.stream() + .flatMap(p -> this.intersect(p).stream()) + .collect(Collectors.toList()); + } + + /** + * Finds the region in time where both {@code this} + * and {@code period} lie in. + * + */ + public Collection intersect(Period period) { + requireNonNull(period); + if (this.contains(period)) { + return List.of(period); + } + if (period.contains(this)) { + return List.of(this); + } + if (this.contains(period.startDate)) { + return List.of(new Period(period.startDate, this.endDate)); + } + if (this.contains(period.endDate)) { + return List.of(new Period(this.startDate, period.endDate)); + } + return List.of(); + + } + + /** + * Unions the input {@code Collection periods} with {@code this}. + */ + public Collection union(Collection periods) { + if (periods.stream().count() == 0) { + //nothing to union to + return List.of(this); + } + //optimisation + if (periods.stream() + .filter(p -> p.contains(this)).count() != 0) { + return periods; + } + //the list of periods to union + List toMerge = new ArrayList<>(); + Collection result = periods.stream() + .flatMap(p -> this.union(p, pe -> toMerge.add(pe)).stream()) + .collect(Collectors.toList()); + //merge the changed periods + //in theory there is only two assuming collection is unique + for (Period p : toMerge) { + result = p.union(result); + } + return result; + + } + + /** + * Unions {@code period} with {@code this}. + * The result is placed into the consumer if it is modified. + */ + private Collection union(Period period, Consumer consumer) { + requireNonNull(period); + if (this.contains(period)) { + return List.of(this); + } + if (period.contains(this)) { + return List.of(period); + } + //can lead to multiple unions + if (contains(period.startDate.minusDays(1))) { + //we know that periods endDate is not contained + Period res = new Period(startDate, period.endDate); + consumer.accept(res); + return List.of(); + } + if (contains(period.endDate.plusDays(1))) { + //we know that period startDate is not contained + Period res = new Period(period.startDate, endDate); + consumer.accept(res); + return List.of(); + } + return List.of(period, this); + } + + private boolean withinExclusively(LocalDate date) { + return (this.startDate.isBefore(date)) + && (this.endDate.isAfter(date)); + } + + private boolean atDelimiters(LocalDate date) { + return this.startDate.isEqual(date) + || this.endDate.isEqual(date); + } + + /** + * Tests if the input string is a valid string representing a period. + */ + public static boolean isValidPeriodString(String toTest) { + String[] toTestSplit = toTest.split(DELIMITER); + if (toTestSplit.length != 2) { + return false; + } + try { + LocalDate.parse(toTestSplit[0]); + LocalDate.parse(toTestSplit[1]); + } catch (DateTimeParseException dtpe) { + return false; + } + return true; + } + + /** + * Returns the first date of the period. + * + * @return LocalDate representing the first date of the period. + */ + public LocalDate getStartDate() { + return startDate; + } + + /** + * Returns a Period that represents a week that starts from the LocalDate provided. + * + * @param firstDate first date of the week. + */ + public static Period oneWeekFrom(LocalDate firstDate) { + return new Period(firstDate, firstDate.plusDays(6)); + } + + /** + * Gets an List representing an iteration over this period. + * + * @return The List. + */ + public List toList() { + return startDate + .datesUntil(endDate.plusDays(1)) //exclusive + .collect(Collectors.toList()); + } + + @Override + public final String toString() { + return this.startDate + DELIMITER + this.endDate; + } + + @Override + public boolean equals(Object o) { + return (o != null) + && (o instanceof Period) + && ((Period) o).startDate.equals(startDate) + && ((Period) o).endDate.equals(endDate); + + } + + /** + * Returns string representation of this period in a more readable format. + */ + public String toDisplayString() { + return ("[" + startDate + "] ~ [" + endDate + "]"); + } +} diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java index 8ff1d83fe89..a0a12e1ebd2 100644 --- a/src/main/java/seedu/address/model/person/Person.java +++ b/src/main/java/seedu/address/model/person/Person.java @@ -1,12 +1,21 @@ package seedu.address.model.person; +import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import static seedu.address.model.person.Field.addToFieldSet; +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalTime; import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.stream.Collectors; +import seedu.address.commons.exceptions.InvalidShiftTimeException; +import seedu.address.model.person.exceptions.DuplicateShiftException; import seedu.address.model.tag.Tag; /** @@ -21,19 +30,53 @@ public class Person { private final Email email; // Data fields - private final Address address; + private final Set roles = new HashSet<>(); + private final Salary salary; + private final Status status; private final Set tags = new HashSet<>(); + private final Set fields = new HashSet<>(); + private final Set absentDates = new HashSet<>(); + + private Schedule schedule; /** * 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(Name name, Phone phone, Email email, Set roles, + Salary salary, Status status, Set tags, Set absentDates) { + requireAllNonNull(name, phone, email, tags, roles); + this.name = name; this.phone = phone; this.email = email; - this.address = address; + if (roles.isEmpty()) { + this.roles.add(Role.NO_ROLE); + } else { + this.roles.addAll(roles); + } + this.salary = salary; + this.status = status; this.tags.addAll(tags); + this.schedule = new Schedule(); + this.fields.addAll(tags); + this.absentDates.addAll(absentDates); + this.fields.addAll(this.roles); + addToFieldSet(fields, name, phone, email, salary, status); + + } + + /** + * Returns a copy of the provided Person object. + * + * @param p Person to be copied. + * @return Person copy of p. + */ + public static Person copy(Person p) { + if (p == null) { + return null; + } + return new Person(p.getName(), p.getPhone(), p.getEmail(), p.getRoles(), p.getSalary(), + p.getStatus(), p.getTags(), p.getAbsentDates()); } public Name getName() { @@ -48,8 +91,28 @@ public Email getEmail() { return email; } - public Address getAddress() { - return address; + public Set getRoles() { + return Collections.unmodifiableSet(roles); + } + + public Salary getSalary() { + return salary; + } + + public Status getStatus() { + return status; + } + + public Schedule getSchedule() { + return schedule; + } + + public boolean isWorking(DayOfWeek dayOfWeek, int slotNum, Period period) { + return schedule.isWorking(dayOfWeek, slotNum, period); + } + + public boolean isWorking(DayOfWeek dayOfWeek, LocalTime time, Period period) { + return schedule.isWorking(dayOfWeek, time, period); } /** @@ -60,17 +123,127 @@ public Set getTags() { return Collections.unmodifiableSet(tags); } + public boolean containsFields(List fields) { + return this.fields.containsAll(fields); + } + + /** + * Marks this {@code period} when the {@code Person} was not working. + */ + public Person mark(Period period) { + Set periods = period.union(this.getAbsentDates()) + .stream() + .collect(Collectors.toUnmodifiableSet()); + Person person = new Person(name, phone, email, roles, salary, status, tags, periods); + person.setSchedule(getSchedule()); + return person; + + } + + /** + * Set time for a shift from the staff's schedule. + * + * @param dayOfWeek of the shift. + * @param slot of the shift. + * @param startTime of the shift. + * @param endTime of the shift. + * @throws InvalidShiftTimeException throws when the timings of Shift are invalid. + */ + public void setShiftTime(DayOfWeek dayOfWeek, Slot slot, LocalTime startTime, LocalTime endTime, + LocalDate startDate, LocalDate endDate) + throws InvalidShiftTimeException { + schedule.setTime(dayOfWeek, slot, startTime, endTime, startDate, endDate); + } + + + /** + * Removes the marking of {@code period} to mark that the person was working in + * this period. The input period must contain the period to remove. + * + * @return The resulting person from marking that the person was working. + */ + public Person unMark(Period period) { + requireNonNull(period); + Set result = getAbsentDates().stream() + .flatMap(p -> p.complement(period).stream()) + .collect(Collectors.toSet()); + Person person = new Person(name, phone, email, roles, salary, status, tags, result); + person.setSchedule(getSchedule()); + return person; + } + + + /** + * Returns an immutable period set, which throws {@code UnsupportedOperationException} + * if modification is attempted. + */ + public Set getAbsentDates() { + return Collections.unmodifiableSet(this.absentDates); + } + + /** + * Checks if this staff was absent on the date provided. + * + * @param checkDate The date of the shift to be checked. + * + */ + public boolean wasAbsent(LocalDate checkDate) { + for (Period period : absentDates) { + if (period.contains(checkDate)) { + return true; + } + } + return false; + } + + /** + * Add a shift to the staff's schedule. + * + * @param dayOfWeek The day of the shift. + * @param slot The time slot of the shift. + * @param startDate The date the shift starts at. + * @throws DuplicateShiftException throws when there is already a shift in the target slot. + */ + public void addShift(DayOfWeek dayOfWeek, Slot slot, + LocalDate startDate, LocalDate endDate) throws DuplicateShiftException { + schedule.addShift(dayOfWeek, slot, startDate, endDate); + } + + public void setSchedule(Schedule schedule) { + this.schedule = schedule; + } + + + /** + * A method to get the working hours of {@code this} during {@code Period period}. + * + * @param period The period to get the working hours over. + * @return The total working hours over this period. + */ + public long getTotalWorkingHour(Period period) { + return this.schedule.getTotalWorkingHour(period, getAbsentDates()); + } + + /** + * Gets the total salary that this staff has earned over {@code Period period}. + * + * @return The salary to be paid in dollars. + */ + public double getSalaryToBePaid(Period period) { + return getTotalWorkingHour(period) * salary.value / 100; + } + /** * Returns true if both persons have the same name. * This defines a weaker notion of equality between two persons. */ - public boolean isSamePerson(Person otherPerson) { - if (otherPerson == this) { + public boolean isSamePerson(Person otherStaff) { + if (otherStaff == this) { return true; } - return otherPerson != null - && otherPerson.getName().equals(getName()); + return otherStaff != null + && otherStaff.getName().equals(getName()); } /** @@ -87,37 +260,52 @@ public boolean equals(Object other) { return false; } - Person otherPerson = (Person) other; - return otherPerson.getName().equals(getName()) - && otherPerson.getPhone().equals(getPhone()) - && otherPerson.getEmail().equals(getEmail()) - && otherPerson.getAddress().equals(getAddress()) - && otherPerson.getTags().equals(getTags()); + Person otherStaff = (Person) other; + //for some odd reason, the set equals method does not work, neither does the contains all + List periods = getAbsentDates().stream().collect(Collectors.toList()); + List otherPeriods = otherStaff.getAbsentDates().stream().collect(Collectors.toList()); + + return otherStaff.getName().equals(getName()) + && otherStaff.getPhone().equals(getPhone()) + && otherStaff.getEmail().equals(getEmail()) + && otherStaff.getRoles().equals(getRoles()) + && otherStaff.getSalary().equals(getSalary()) + && otherStaff.getStatus().equals(getStatus()) + && otherStaff.getTags().equals(getTags()) + && periods.containsAll(otherPeriods) + && otherPeriods.containsAll(periods); + } @Override public int hashCode() { // use this method for custom fields hashing instead of implementing your own - return Objects.hash(name, phone, email, address, tags); + return Objects.hash(name, phone, email, tags); } @Override public String toString() { final StringBuilder builder = new StringBuilder(); - builder.append(getName()) - .append("; Phone: ") - .append(getPhone()) - .append("; Email: ") - .append(getEmail()) - .append("; Address: ") - .append(getAddress()); + builder.append(getName()).append("\n") + .append("Phone: ").append(getPhone()).append("\n") + .append("Email: ").append(getEmail()).append("\n") + .append("Salary: ").append(getSalary().convertToDollars()).append("\n") + .append("Status: ").append(getStatus()).append("\n"); + Set roles = getRoles(); + if (!roles.isEmpty()) { + builder.append("Roles: "); + for (Role r : roles) { + builder.append(r.toString()).append(" "); + } + builder.append("\n"); + } Set tags = getTags(); if (!tags.isEmpty()) { - builder.append("; Tags: "); + builder.append("Tags: "); tags.forEach(builder::append); + builder.append("\n"); } return builder.toString(); } - } diff --git a/src/main/java/seedu/address/model/person/Phone.java b/src/main/java/seedu/address/model/person/Phone.java index 872c76b382f..994e7c737b4 100644 --- a/src/main/java/seedu/address/model/person/Phone.java +++ b/src/main/java/seedu/address/model/person/Phone.java @@ -7,7 +7,7 @@ * Represents a Person's phone number in the address book. * Guarantees: immutable; is valid as declared in {@link #isValidPhone(String)} */ -public class Phone { +public class Phone implements Field { public static final String MESSAGE_CONSTRAINTS = diff --git a/src/main/java/seedu/address/model/person/Role.java b/src/main/java/seedu/address/model/person/Role.java new file mode 100644 index 00000000000..5ddc0aa7936 --- /dev/null +++ b/src/main/java/seedu/address/model/person/Role.java @@ -0,0 +1,88 @@ +package seedu.address.model.person; + +/** + * This class stands for every possible role for a staff + */ +public enum Role implements Field { + //Can add more later + KITCHEN("kitchen"), BARTENDER("bartender"), FLOOR("floor"), NO_ROLE("norole"); + + public static final String STORAGE_WRONG_ROLE_MESSAGE = + "List of valid Roles: kitchen, bartender, floor. (norole if no role is assigned). The role norole" + + " cannot be together with other roles."; + + public static final String MESSAGE_CONSTRAINTS = "List of valid Roles: kitchen, bartender, floor."; + + private final String role; + + Role(String role) { + this.role = role; + } + + /** + * Gets the value of a role. + * + * @return The value of a role. + */ + public String getValue() { + return role; + } + + /** + * Translate a string into a Role enum if the string matches any Role values except for norole. + * Trims string. + * @param string String to be translated. + * @return The translated Role if the string is valid. + * @throws IllegalArgumentException if the string is invalid. + */ + public static Role translateStringToRole(String string) throws IllegalArgumentException { + Role result = translateStringToRoleWithNoRole(string); + if (result.equals(Role.NO_ROLE)) { + throw new IllegalArgumentException("String provided does not match any of the valid roles. " + + MESSAGE_CONSTRAINTS); + } + return result; + } + + /** + * Translate a string into a Role enum if the string matches any Role values. Trims string. + * + * @param string String to be translated. + * @return The translated Role if the string is valid. + * @throws IllegalArgumentException if the string is invalid. + */ + public static Role translateStringToRoleWithNoRole(String string) throws IllegalArgumentException { + String trimmedString = string.trim(); + Role resultRole = null; + for (Role r : Role.values()) { + if (r.getValue().equalsIgnoreCase(trimmedString)) { + resultRole = r; + } + } + if (resultRole == null) { + throw new IllegalArgumentException("String provided does not match any roles. " + MESSAGE_CONSTRAINTS); + } else { + return resultRole; + } + + } + + /** + * Returns if a given string is a valid Role. + */ + public static boolean isValidRole(String test) { + String trimmedTest = test.trim(); + for (Role r : Role.values()) { + if (r.getValue().equalsIgnoreCase(trimmedTest)) { + return true; + } + } + return false; + } + + + @Override + public String toString() { + return this.role; + } +} 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 00000000000..77eaabc635e --- /dev/null +++ b/src/main/java/seedu/address/model/person/Salary.java @@ -0,0 +1,135 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; +import static seedu.address.commons.util.AppUtil.checkArgument; + +/** + * Represents a Person's email in the address book. + * Guarantees: immutable; is valid as declared in {@link #isValidSalary(String)} + */ +public class Salary implements Field { + + private static final int MAX_SALARY = 9999; + + public static final String MESSAGE_CONSTRAINTS = + "Salaries have to be a non-negative integer representing the hourly pay in dollars.\n" + + "Cents can be added by adding one or two numeric characters after a \".\".\n\n" + + "For example, \"0.1\" and \"0.10\" both represent $0.10 per hour.\n" + + "However, \"0.\" and \"0.001\" are not acceptable.\n\n" + + "Note that the salary cannot exceed $" + MAX_SALARY + ".99."; + + public final Integer value; + + /** + * Constructs an {@code Salary}. + * + * @param salary A valid salary in cents. + */ + public Salary(String salary) { + requireNonNull(salary); + checkArgument(isValidSalary(salary), MESSAGE_CONSTRAINTS); + if (salary.contains(".")) { + String[] salaryStringSplit = salary.split("\\."); + String centsString = salaryStringSplit[1]; + if (centsString.length() == 1) { + centsString += "0"; + } else if (centsString.length() > 2) { + centsString = centsString.substring(0, 2); + } + value = Integer.parseInt(salaryStringSplit[0]) * 100 + Integer.parseInt(centsString); + } else { + value = Integer.parseInt(salary) * 100; + } + } + + /** + * Returns if a given string is a valid salary. + */ + public static boolean isValidSalary(String test) { + test = test.trim(); + if (test.contains(".")) { + String[] testStringSplit = test.split("\\."); + if (testStringSplit.length != 2) { // multiple "." or empty field for dollars or cents + return false; + } + return isValidDollars(testStringSplit[0]) && isValidCents(testStringSplit[1]); + } else { + return isValidDollars(test); + } + } + + /** + * Returns if a given string represents a valid dollar value. + */ + public static boolean isValidDollars(String test) { + test = test.trim(); + if (!test.matches("\\d+") || test.equals("")) { + return false; + } + + int dollarInt; + try { + dollarInt = Integer.parseInt(test); + // This exception will be caught if the integer exceeds max integer + } catch (NumberFormatException e) { + return false; + } + + if (dollarInt > MAX_SALARY) { + return false; + } + return dollarInt >= 0; + } + + /** + * Returns if a given string represents a valid cents value. + */ + public static boolean isValidCents(String test) { + test = test.trim(); + if (!test.matches("\\d+") || test.equals("") || test.length() > 2) { + return false; + } + int dollarInt; + try { + dollarInt = Integer.parseInt(test); + // This exception will be caught if the integer exceeds max integer + } catch (NumberFormatException e) { + return false; + } + return dollarInt >= 0; + } + + @Override + public String toString() { + int dollars = value / 100; + int cents = value % 100; + String centsString; + if (cents == 0) { + centsString = "00"; + } else if (cents < 10) { + centsString = "0" + cents; + } else { + centsString = String.valueOf(cents); + } + return (dollars + "." + centsString); + } + + /** + * Returns a String representation of the value of this Salary object in dollars. + */ + public String convertToDollars() { + return ("$" + toString()); + } + + @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/Schedule.java b/src/main/java/seedu/address/model/person/Schedule.java new file mode 100644 index 00000000000..6ee6f9593cf --- /dev/null +++ b/src/main/java/seedu/address/model/person/Schedule.java @@ -0,0 +1,241 @@ +package seedu.address.model.person; + +import static java.util.Objects.requireNonNull; +import static seedu.address.model.person.Shift.isValidShift; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +import seedu.address.commons.exceptions.InvalidShiftTimeException; +import seedu.address.model.person.exceptions.DuplicateShiftException; +import seedu.address.model.person.exceptions.NoShiftException; + +/** + * Represents the schedule for the staff, which contains all the task for the staff. + */ +public class Schedule { + + public static final String MESSAGE_CONSTRAINTS = "Schedule JSON string error! Invalid format."; //todo idek if need + public static final int HOURS_PER_SLOT = 6; + + private static final int DAY_OF_WEEK = 7; + private static final int PERIOD_OF_DAY = 2; + // Set the number of hours for a slot as 4 hours + + private static final String SCHEDULE_DEFAULT = "Schedule:\n" + + "Monday: %1$s\n" + + "Tuesday: %2$s\n" + + "Wednesday: %3$s\n" + + "Thursday: %4$s\n" + + "Friday: %5$s\n" + + "Saturday: %6$s\n" + + "Sunday: %7$s\n"; + + private Shift[][] shifts; + + /** + * Initialize schedule object. + */ + public Schedule() { + this.shifts = new Shift[DAY_OF_WEEK][PERIOD_OF_DAY]; + for (int day = 0; day < DAY_OF_WEEK; day++) { + shifts[day][0] = null; + shifts[day][1] = null; + } + } + + /** + * Creates a {@code Schedule} from {@code Shift[][] shifts}. + * + */ + public Schedule(Shift[][] shifts) { + assert shifts.length == DAY_OF_WEEK; + assert shifts[0].length == PERIOD_OF_DAY; + assert shifts[1].length == PERIOD_OF_DAY; + requireNonNull(shifts); + this.shifts = shifts; + } + + public Shift[][] getShifts() { + return this.shifts; + } + + + + + /** + * Adds a new shift for a staff. + * + * @param dayOfWeek The day of the shift in a week. + * @param slot The slot of the shift located. + * @throws DuplicateShiftException throws when there is already a shift in the target slot. + */ + public void addShift(DayOfWeek dayOfWeek, Slot slot, + LocalDate startDate, LocalDate endDate) throws DuplicateShiftException { + Shift shift = new Shift(dayOfWeek, slot); + shift = shift.add(startDate, endDate); + Shift shift1 = shifts[dayOfWeek.getValue() - 1][slot.getOrder()]; + + if (shift1 == null) { + shifts[dayOfWeek.getValue() - 1][slot.getOrder()] = shift; + + return; + } + if (!shift1.isEmpty() && shift1.isWorkingExactWithin(new Period(startDate, endDate))) { + throw new DuplicateShiftException(); + } + shifts[dayOfWeek.getValue() - 1][slot.getOrder()] = shift1.add(startDate, endDate); + } + + /** + * Removes a new task for a staff. + * + * @param dayOfWeek The day of the shift in a week. + * @param slot The period of the shift. + * @param endDate The date the endDate is at. + * @throws NoShiftException throws when a user tries to delete a shift that does not exist. + */ + public void removeShift(DayOfWeek dayOfWeek, Slot slot, + LocalDate startDate, LocalDate endDate) throws NoShiftException { + Shift shift = shifts[dayOfWeek.getValue() - 1][slot.getOrder()]; + if (shift == null || shift.isEmpty()) { + throw new NoShiftException(); + } + shifts[dayOfWeek.getValue() - 1][slot.getOrder()] = shift.remove(startDate, endDate); + } + + /** + * Checks whether a staff is working in a certain period. + * + * @param dayOfWeek The day want to check. + * @param slot The period want to check. + */ + public boolean isWorking(DayOfWeek dayOfWeek, Slot slot, Period period) { + return isWorking(dayOfWeek, slot.getOrder(), period); + } + + /** + * Checks whether a staff is working in a certain period. + * + * @param dayOfWeek The day want to check. + * @param slotNum The slot number want to check. + */ + public boolean isWorking(DayOfWeek dayOfWeek, int slotNum, Period period) { + // TODO change from slots 0 and 1 to checking by a specific timing? + Shift shift = shifts[dayOfWeek.getValue() - 1][slotNum]; + return shift != null + && !shift.isEmpty() + && shift.isWorking(period); + } + + /** + * Checks whether a staff is working in a certain period. + * + * @param time The time to check if the staff is working at + */ + public boolean isWorking(DayOfWeek dayOfWeek, LocalTime time, Period period) { + for (Shift s : shifts[dayOfWeek.getValue() - 1]) { + if (s == null) { + continue; + } + if (s.isWorking(time, period)) { + return true; + } + } + return false; + } + + /** + * Set time for a shift a shift from a target staff's schedule. + * {@code target} must exist in the address book. + * + * @param dayOfWeek of the shift. + * @param slot of the shift. + * @throws NoShiftException throws when a user tries to delete a shift that does not exist. + */ + public void setTime(DayOfWeek dayOfWeek, Slot slot, LocalTime startTime, LocalTime endTime, + LocalDate startDate, LocalDate endDate) + throws InvalidShiftTimeException { + if (shifts[dayOfWeek.getValue() - 1][slot.getOrder()] == null + || shifts[dayOfWeek.getValue() - 1][slot.getOrder()].isEmpty()) { + shifts[dayOfWeek.getValue() - 1][slot.getOrder()] = new EmptyShift(dayOfWeek, slot); + shifts[dayOfWeek.getValue() - 1][slot.getOrder()] = + shifts[dayOfWeek.getValue() - 1][slot.getOrder()].add(startDate, endDate); + } + shifts[dayOfWeek.getValue() - 1][slot.getOrder()] = shifts[dayOfWeek.getValue() - 1][slot.getOrder()] + .setTime(startTime, endTime, slot.getOrder(), + startDate, endDate); + } + + /** + * Calculates the total working hours os one schedule. + * + * @return The total working hours. + */ + public int getTotalWorkingHour() { + int totalHours = 0; + for (Shift[] dayShifts : shifts) { + for (Shift shift : dayShifts) { + if (shift != null) { + totalHours += HOURS_PER_SLOT; + } + } + } + return totalHours; + } + + /** + * Calculates the total working hours over {@code Period period} + * of this schedule, while removing {@code Collection absentPeriods} from the count. + */ + public long getTotalWorkingHour(Period period, Collection absentPeriods) { + requireNonNull(period); + requireNonNull(absentPeriods); + long totalHours = 0; + List datesNotCounted = absentPeriods + .stream() + .flatMap(p -> p.toList().stream()) + .collect(Collectors.toList()); + List dates = period.toList(); + for (LocalDate date : dates) { + if (datesNotCounted.contains(date)) { + continue; + } + if (shifts[date.getDayOfWeek().getValue() - 1][Slot.MORNING.getOrder()] != null) { + totalHours += shifts[date.getDayOfWeek().getValue() - 1][Slot.MORNING.getOrder()] + .getWorkingHour(new Period(date)); + } + + + if (shifts[date.getDayOfWeek().getValue() - 1][Slot.AFTERNOON.getOrder()] != null) { + totalHours += shifts[date.getDayOfWeek().getValue() - 1][Slot.AFTERNOON.getOrder()] + .getWorkingHour(new Period(date)); + + + } + + } + return totalHours; + } + + + /** + * Returns if a given string is a valid scheduleString. + */ + public static boolean isValidSchedule(String test) { + if (test.equals("")) { + return true; + } + String[] shiftSplit = test.split("\\s+"); // splits by white space, not just single white space + for (String s : shiftSplit) { + if (!isValidShift(s)) { + return false; + } + } + return true; + } +} diff --git a/src/main/java/seedu/address/model/person/Shift.java b/src/main/java/seedu/address/model/person/Shift.java new file mode 100644 index 00000000000..309a0cb6de7 --- /dev/null +++ b/src/main/java/seedu/address/model/person/Shift.java @@ -0,0 +1,346 @@ +package seedu.address.model.person; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import javafx.collections.ObservableList; +import seedu.address.commons.exceptions.InvalidShiftTimeException; +import seedu.address.commons.util.DateTimeUtil; +import seedu.address.model.RecurrencePeriod; +import seedu.address.model.person.exceptions.NoShiftException; + +/** + * Represents a piece of work for a staff. + */ +public class Shift { + + public static final String DELIMITER = "-"; + private static final String DEFAULT_SHIFT_DISPLAY_STRING = "Day: %1$s, Slot:%2$s"; + + protected Slot slot; + protected DayOfWeek dayOfWeek; + protected List recurrences = new ArrayList<>(); + + /** + * Constructor of Task given its weekday, time, and name. + * + * @param dayOfWeek Weekday of the shift. + * @param slot The slot when the shift located. + */ + public Shift(DayOfWeek dayOfWeek, Slot slot) { + this.dayOfWeek = dayOfWeek; + this.slot = slot; + } + + /** + * Creates a shift at {@code dayOfWeek} in {@code Slot slot} at {@code LocalDate startDate} + * with a history of changes {@code Set history}. + */ + public Shift(DayOfWeek dayOfWeek, Slot slot, List recurrences) { + this.dayOfWeek = dayOfWeek; + this.slot = slot; + this.recurrences.addAll(recurrences); + } + + public Slot getSlot() { + return this.slot; + } + + public DayOfWeek getDayOfWeek() { + return this.dayOfWeek; + } + + public List getRecurrences() { + return Collections.unmodifiableList(recurrences); + } + + /** + * Returns if this is an empty shift. + * + */ + public boolean isEmpty() { + return false; + } + + /** + * Returns whether the shift is happening in the morning. + * + * @return whether the shift is happening in the morning. + */ + public boolean isInMorning() { + return this.slot.getValue().equals("morning"); + } + + /** + * Returns whether the shift is happening in the afternoon. + * + * @return whether the shift is happening in the afternoon. + */ + public boolean isInAfternoon() { + return this.slot.getValue().equals("afternoon"); + } + + /** + * Returns whether the staff is working during this {@code LocalTime time} + * Within {@code Period period.} + */ + public boolean isWorking(LocalTime time, Period period) { + List dates = period.toList() + .stream() + .filter(p -> p.getDayOfWeek().equals(dayOfWeek)) + .collect(Collectors.toList()); + long numOfRecurrences = recurrences.stream() + .filter(p -> dates.stream().filter(date -> p.contains(date)).count() != 0) + .filter(p -> p.isWithinSlotPeriod(time)) + .count(); + return numOfRecurrences != 0; + } + + /** + * Returns whether the shift + * represents that it is working during {@code period}. + */ + public boolean isWorking(Period period) { + if (this.recurrences.size() == 0) { + return false; + } + return countOfOccurrences(period) != 0; + } + + /** + * Returns whether the shift is working during {@code period} + * for all the dates in the period. False if and only if + * there is some date in the period that is not + * there. + */ + public boolean isWorkingExactWithin(Period period) { + long numOfDates = period.toList() + .stream() + .filter(p -> p.getDayOfWeek().equals(dayOfWeek)) + .count(); + return countOfOccurrences(period) == numOfDates; + } + + /** + * Returns the number of times the staff works this shift in {@code period}. + * + */ + private long countOfOccurrences(Period period) { + List dates = period.toList() + .stream() + .filter(p -> p.getDayOfWeek().equals(dayOfWeek)) + .collect(Collectors.toList()); + return recurrences.stream() + .mapToLong(p -> dates.stream() + .filter(date -> p.contains(date)) //find any date within the period + .count() //that is in recurrence + ) + .sum(); + } + + public long getWorkingHour(Period period) { + return recurrences.stream() + .mapToLong(p -> p.getWorkingHour(dayOfWeek, period)) + .sum(); + + + } + + /** + * Removes the shift that is withing the input dates. + */ + public Shift remove(LocalDate startDate, LocalDate endDate) throws NoShiftException { + assert endDate.isAfter(startDate) || endDate.isEqual(startDate); + //check if this period is already within the current set + List recurrences = removeFromRecurrencePeriods(new Period(startDate, endDate), + this.recurrences); + if (recurrences.equals(this.recurrences)) { + throw new NoShiftException(); + } + if (recurrences.size() == 0) { + return new EmptyShift(dayOfWeek, slot); + } + return new Shift(dayOfWeek, slot, recurrences); + + } + + private static List removeFromRecurrencePeriods(Period period, + Collection recurrences) { + return recurrences.stream() + .flatMap(p -> p.complementWithInformation(period).stream()) + .collect(Collectors.toList()); + } + + + /** + * Makes this shift a working shift. + */ + public Shift add(LocalDate startDate, LocalDate endDate) { + Period period = new Period(startDate, endDate); + Collection nonOverlaps = List.of(period); + for (RecurrencePeriod r : recurrences) { + Collection temp = new ArrayList<>(); + for (Period p: nonOverlaps) { + temp.addAll(p.complement(r)); + } + nonOverlaps = temp; + } + List recurrences = this.recurrences; + for (Period p : nonOverlaps) { + RecurrencePeriod recurrencePeriod = new RecurrencePeriod(p, slot); + recurrences = recurrencePeriod.unionByDuration(recurrences).stream() + .collect(Collectors.toList()); + } + + return new Shift(dayOfWeek, slot, recurrences); + } + + /** + * Returns if a given string is a valid DayOfWeek. + */ + public static boolean isValidDayOfWeek(String test) { + for (DayOfWeek d :DayOfWeek.values()) { + if (d.toString().equalsIgnoreCase(test)) { + return true; + } + } + return false; + } + + /** + * Check if the timing for a shift is valid, and then update them. + * @param startTime Start time of the shift + * @param endTime End time of the shift + * @param order Indicate the shift is in the morning slot or the afternoon slot. + * @throws InvalidShiftTimeException Throws when the timing is invalid. There are two cases, one is the startTime + * is later then the endTime, the other is that the time is out of the bound of the default one. + */ + public Shift setTime(LocalTime startTime, LocalTime endTime, int order, + LocalDate startDate, LocalDate endDate) throws InvalidShiftTimeException { + checkTimeOrder(startTime, endTime, order); + + Period period = new Period(startDate, endDate); + Collection intersects = period.intersect(recurrences); + List result = new ArrayList<>(); + result.addAll(recurrences); + for (Period intersect : intersects) { + result = removeFromRecurrencePeriods(intersect, result); + } + result.addAll(intersects.stream() + .map(p -> new RecurrencePeriod(p, startTime, endTime)) + .collect(Collectors.toList())); + + return new Shift(dayOfWeek, slot, result); + } + + /** + * Checks that the input start and end time fit the order of the shift. + * + * @param startTime The start time of the shift. + * @param endTime The end time of the shift. + * @param order The slot the shift is at in int, only accepts 1 or 0 now. + * @throws InvalidShiftTimeException + */ + public static void checkTimeOrder(LocalTime startTime, LocalTime endTime, int order) + throws InvalidShiftTimeException { + if (startTime.isAfter(endTime)) { + throw new InvalidShiftTimeException(); + } + if (order == 0) { + if (startTime.isBefore(DateTimeUtil.getDefaultMorningStartTime()) + || endTime.isAfter(DateTimeUtil.getDefaultMorningEndTime())) { + throw new InvalidShiftTimeException(); + } + } else { + if (startTime.isBefore(DateTimeUtil.getDefaultAfternoonStartTime()) + || endTime.isAfter(DateTimeUtil.getDefaultAfternoonEndTime())) { + throw new InvalidShiftTimeException(); + } + } + } + + + /** + * Returns if a given string is a valid shift. + */ + public static boolean isValidShift(String test) { + if (test.equals("")) { + return false; + } + String[] stringSplit = test.split(DELIMITER); + if (stringSplit.length != 4) { + return false; + } + String dayString = stringSplit[0]; + String slotString = stringSplit[1]; + String startTimeString = stringSplit[2]; + String endTimeString = stringSplit[3]; + return isValidDayOfWeek(dayString) + && Slot.isValidSlot(slotString) + && DateTimeUtil.isValidTime(startTimeString) + && DateTimeUtil.isValidTime(endTimeString); + } + + + /** + * Takes in a {@code Collection periods} and formats it to a string for the + * user to read. + * + */ + private static String getRecurrenceString(Collection periods) { + String resultString = ""; + + for (RecurrencePeriod rp : periods) { + resultString += rp.toPrintString() + "\n"; + } + return resultString; + } + + /** + * Returns a string of staff names that work on a specified shift. Result string is numbered and + * has each staff in a new line. + * + * @param stafflist full list of staff in Staff'd. + * @param day day of shift to be compared to. + * @param time time of shift to be compared to. + */ + public static String filterListByShift(ObservableList stafflist, DayOfWeek day, + LocalTime time, Period period) { + StringBuilder result = new StringBuilder(); + int counter = 1; + for (Person p : stafflist) { + boolean hasShift = p.isWorking(day, time, period); + if (hasShift) { + result.append(counter).append(". ").append(p.getName()).append("\n"); + counter++; + } + } + return result.toString(); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (!(other instanceof Shift)) { + return false; + } + Shift otherShift = (Shift) other; + return slot.equals(otherShift.slot) && dayOfWeek.equals(otherShift.dayOfWeek) + && this.recurrences.containsAll(otherShift.recurrences) + && otherShift.recurrences.containsAll(this.recurrences); + } + + @Override + public String toString() { + String resultString = getRecurrenceString(recurrences); + return resultString; + } +} diff --git a/src/main/java/seedu/address/model/person/Slot.java b/src/main/java/seedu/address/model/person/Slot.java new file mode 100644 index 00000000000..3862cd05244 --- /dev/null +++ b/src/main/java/seedu/address/model/person/Slot.java @@ -0,0 +1,90 @@ +package seedu.address.model.person; + +/** + * Represents possible working slot for a staff. + */ +public enum Slot { + MORNING("morning", 0), AFTERNOON("afternoon", 1); + + public static final String MESSAGE_CONSTRAINTS = + "List of valid slots: morning, afternoon."; + + private String period; + private int order; + + + private Slot(String period, int order) { + this.period = period; + this.order = order; + } + + /** + * Gets the value of a slot. + * + * @return The value of a slot. + */ + public String getValue() { + return period; + } + + /** + * Gets the order of a slot. + * + * @return The order of a slot. + */ + public int getOrder() { + return order; + } + + /** + * Translate a string into a Slot enum if the string matches any Slot values. Trims string. + * + * @param string String to be translated. + * @return The translated Slot if the string is valid, null object otherwise. + */ + public static Slot translateStringToSlot(String string) { + String trimmedString = string.trim(); + Slot resultSlot = null; + for (Slot s : Slot.values()) { + if (s.getValue().equalsIgnoreCase(trimmedString)) { + resultSlot = s; + break; + } + } + return resultSlot; + } + + public static Slot getSlotByOrder(String string) { + String trimmedString = string.trim(); + Slot resultSlot = null; + for (Slot s : Slot.values()) { + if (String.valueOf(s.getOrder()).equals(trimmedString)) { + resultSlot = s; + break; + } + } + return resultSlot; + } + + /** + * Checks if the string provided matches with any Slot enum strings. + * + * @param test String to be checked. + * @return boolean true if valid, false otherwise + */ + public static boolean isValidSlot(String test) { + String trimmedTest = test.trim(); + for (Slot s : Slot.values()) { + String sString = String.valueOf(s.getValue()); + if (sString.equals(trimmedTest)) { + return true; + } + } + return false; + } + + @Override + public String toString() { + return period; + } +} diff --git a/src/main/java/seedu/address/model/person/Status.java b/src/main/java/seedu/address/model/person/Status.java new file mode 100644 index 00000000000..a952d078109 --- /dev/null +++ b/src/main/java/seedu/address/model/person/Status.java @@ -0,0 +1,69 @@ +package seedu.address.model.person; + +/** + * This class stands for every possible status for a staff + */ +public enum Status implements Field { + //Can add more later + FULL_TIME("fulltime"), PART_TIME("parttime"), NO_STATUS("nostatus"); + + public static final String MESSAGE_CONSTRAINTS = + "List of valid Statuses: fulltime, parttime, (nostatus if no status is assigned)."; //cleanup next time + + + private final String status; + + Status(String status) { + this.status = status; + } + + /** + * Gets the value of a status. + * + * @return The value of a status. + */ + public String getValue() { + return status; + } + + /** + * Translate a string into a Status enum if the string matches any Status values. Trims string. + * + * @param string String to be translated. + * @return The translated Status if the string is valid. + * @throws IllegalArgumentException if the string is invalid. + */ + public static Status translateStringToStatus(String string) throws IllegalArgumentException { + String trimmedString = string.trim(); + Status resultStatus = null; + for (Status r : Status.values()) { + if (r.getValue().equalsIgnoreCase(trimmedString)) { + resultStatus = r; + } + } + if (resultStatus == null) { + throw new IllegalArgumentException(Status.MESSAGE_CONSTRAINTS); + } else { + return resultStatus; + } + } + + /** + * Returns if a given string is a valid Status. + */ + public static boolean isValidStatus(String test) { + String trimmedTest = test.trim(); + for (Status r : Status.values()) { + if (r.getValue().equalsIgnoreCase(trimmedTest)) { + return true; + } + } + return false; + } + + @Override + public String toString() { + return status; + } + +} diff --git a/src/main/java/seedu/address/model/person/UniquePersonList.java b/src/main/java/seedu/address/model/person/UniquePersonList.java index 0fee4fe57e6..c345d1bc0e7 100644 --- a/src/main/java/seedu/address/model/person/UniquePersonList.java +++ b/src/main/java/seedu/address/model/person/UniquePersonList.java @@ -51,21 +51,21 @@ public void add(Person toAdd) { /** * Replaces the person {@code target} in the list with {@code editedPerson}. * {@code target} must exist in the list. - * The person identity of {@code editedPerson} must not be the same as another existing person in the list. + * The person identity of {@code editedStaff} must not be the same as another existing person in the list. */ - public void setPerson(Person target, Person editedPerson) { - requireAllNonNull(target, editedPerson); + public void setPerson(Person target, Person editedStaff) { + requireAllNonNull(target, editedStaff); int index = internalList.indexOf(target); if (index == -1) { throw new PersonNotFoundException(); } - if (!target.isSamePerson(editedPerson) && contains(editedPerson)) { + if (!target.isSamePerson(editedStaff) && contains(editedStaff)) { throw new DuplicatePersonException(); } - internalList.set(index, editedPerson); + internalList.set(index, editedStaff); } /** @@ -86,15 +86,15 @@ public void setPersons(UniquePersonList replacement) { /** * Replaces the contents of this list with {@code persons}. - * {@code persons} must not contain duplicate persons. + * {@code staffs} must not contain duplicate persons. */ - public void setPersons(List persons) { - requireAllNonNull(persons); - if (!personsAreUnique(persons)) { + public void setPersons(List staffs) { + requireAllNonNull(staffs); + if (!personsAreUnique(staffs)) { throw new DuplicatePersonException(); } - internalList.setAll(persons); + internalList.setAll(staffs); } /** @@ -124,10 +124,10 @@ public int hashCode() { /** * Returns true if {@code persons} contains only unique persons. */ - private boolean personsAreUnique(List persons) { - for (int i = 0; i < persons.size() - 1; i++) { - for (int j = i + 1; j < persons.size(); j++) { - if (persons.get(i).isSamePerson(persons.get(j))) { + private boolean personsAreUnique(List staffs) { + for (int i = 0; i < staffs.size() - 1; i++) { + for (int j = i + 1; j < staffs.size(); j++) { + if (staffs.get(i).isSamePerson(staffs.get(j))) { return false; } } diff --git a/src/main/java/seedu/address/model/person/exceptions/DuplicateShiftException.java b/src/main/java/seedu/address/model/person/exceptions/DuplicateShiftException.java new file mode 100644 index 00000000000..3b87ebdad13 --- /dev/null +++ b/src/main/java/seedu/address/model/person/exceptions/DuplicateShiftException.java @@ -0,0 +1,10 @@ +package seedu.address.model.person.exceptions; + +/** + * Represents an error caused by user wants to add a duplicate shift inside a staff's schedule. + */ +public class DuplicateShiftException extends RuntimeException { + public DuplicateShiftException() { + super("Operation would result in duplicate shift for a staff."); + } +} diff --git a/src/main/java/seedu/address/model/person/exceptions/NoShiftException.java b/src/main/java/seedu/address/model/person/exceptions/NoShiftException.java new file mode 100644 index 00000000000..9119c42f972 --- /dev/null +++ b/src/main/java/seedu/address/model/person/exceptions/NoShiftException.java @@ -0,0 +1,10 @@ +package seedu.address.model.person.exceptions; + +/** + * Represents an error caused when a user wants to delete a shift that does not exist for the staff. + */ +public class NoShiftException extends RuntimeException { + public NoShiftException() { + super("Shift does not exist for the staff."); + } +} diff --git a/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java b/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java index fa764426ca7..ed586e57eec 100644 --- a/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java +++ b/src/main/java/seedu/address/model/person/exceptions/PersonNotFoundException.java @@ -1,6 +1,10 @@ package seedu.address.model.person.exceptions; /** - * Signals that the operation is unable to find the specified person. + * Signals that the operation is unable to find the specified staff in the address book. */ -public class PersonNotFoundException extends RuntimeException {} +public class PersonNotFoundException extends RuntimeException { + public PersonNotFoundException() { + super("Target person does not exist in the address book."); + } +} diff --git a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/predicates/NameContainsKeywordsPredicate.java similarity index 60% rename from src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java rename to src/main/java/seedu/address/model/person/predicates/NameContainsKeywordsPredicate.java index c9b5868427c..e94c6de1617 100644 --- a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java +++ b/src/main/java/seedu/address/model/person/predicates/NameContainsKeywordsPredicate.java @@ -1,14 +1,36 @@ -package seedu.address.model.person; +package seedu.address.model.person.predicates; import java.util.List; import java.util.function.Predicate; import seedu.address.commons.util.StringUtil; +import seedu.address.model.person.Person; /** * Tests that a {@code Person}'s {@code Name} matches any of the keywords given. */ public class NameContainsKeywordsPredicate implements Predicate { + + /** + * The empty version of NameContainsKeywordsPredicate that tests true for all input. + */ + private static class EmptyNameContainsKeywordsPredicate extends NameContainsKeywordsPredicate { + public EmptyNameContainsKeywordsPredicate() { + super(List.of("")); + } + + + @Override + public boolean test(Person person) { + return true; + } + + + } + + public static final NameContainsKeywordsPredicate EMPTY = + new EmptyNameContainsKeywordsPredicate(); + private final List keywords; public NameContainsKeywordsPredicate(List keywords) { diff --git a/src/main/java/seedu/address/model/person/predicates/PersonContainsFieldsPredicate.java b/src/main/java/seedu/address/model/person/predicates/PersonContainsFieldsPredicate.java new file mode 100644 index 00000000000..4ec1815e4e7 --- /dev/null +++ b/src/main/java/seedu/address/model/person/predicates/PersonContainsFieldsPredicate.java @@ -0,0 +1,92 @@ +package seedu.address.model.person.predicates; + +import static java.util.Objects.requireNonNull; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Predicate; + +import seedu.address.logic.parser.ParserCheckedFunction; +import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Field; +import seedu.address.model.person.Person; + +/** + * Predicate to check if the person contains the input fields. + */ +public class PersonContainsFieldsPredicate implements Predicate { + + private ArrayList fields = new ArrayList<>(); + + /** + * Constructor for a predicate to check if a person has a + * group of fields. + * @param fields The fields to check. + */ + public PersonContainsFieldsPredicate(Field... fields) { + for (Field field : fields) { + this.fields.add(field); + } + } + + /** + * Add a field to the predicate to test from a string. + * @param fieldString The field to test. + * @param func The function to create the field. + */ + public void addFieldToTest(Optional fieldString, + ParserCheckedFunction func) throws ParseException { + if (fieldString.isEmpty()) { + return; + } + this.fields.add(func.apply(fieldString.get())); + } + + /** + * Add a field to the predicate to test from a string. + * @param fieldString The field to test. + * @param func The function to create the field. + */ + public void addFieldToTest(List fieldString, + ParserCheckedFunction func) throws ParseException { + if (fieldString.isEmpty()) { + return; + } + for (String s : fieldString) { + this.fields.add(func.apply(s)); + } + } + + /** + * Adds the {@code field} to the predicate test conditions. + */ + public void addFieldToTest(Field field) { + requireNonNull(field); + this.fields.add(field); + } + + /** + * Checks if the predicate tests for anything. + * @return True if the predicate tests no fields. + */ + public boolean isEmpty() { + return this.fields.isEmpty(); + } + + @Override + public boolean test(Person person) { + if (isEmpty()) { + return true; + } + return person.containsFields(this.fields); + } + + @Override + public boolean equals(Object obj) { + return obj != null + && obj instanceof PersonContainsFieldsPredicate + && this.fields.containsAll(((PersonContainsFieldsPredicate) obj).fields) + && ((PersonContainsFieldsPredicate) obj).fields.containsAll(this.fields); //check equality + } +} diff --git a/src/main/java/seedu/address/model/person/predicates/PersonIsWorkingPredicate.java b/src/main/java/seedu/address/model/person/predicates/PersonIsWorkingPredicate.java new file mode 100644 index 00000000000..14a8a2472d4 --- /dev/null +++ b/src/main/java/seedu/address/model/person/predicates/PersonIsWorkingPredicate.java @@ -0,0 +1,55 @@ +package seedu.address.model.person.predicates; + +import java.time.DayOfWeek; +import java.time.LocalTime; +import java.util.function.Predicate; + +import seedu.address.logic.commands.ViewShiftCommand; +import seedu.address.model.person.Period; +import seedu.address.model.person.Person; + +public class PersonIsWorkingPredicate implements Predicate { + + private final DayOfWeek dayOfWeek; + private final int slotNum; + private final LocalTime time; + private final Period period; + + /** + * Constructs a PersonIsWorkingPredicate object which tests if a person is working on a specific day, at a + * specific time or slot number. + * + * @param dayOfWeek The day of week that will be checked. + * @param slotNum The slot number that will be checked. It will be {@code ViewShiftCommand.INVALID_SLOT_NUMBER} + * if the viewShift is by time. + * @param time The time that will be checked. It will be null if the viewShift is by slot number. + */ + public PersonIsWorkingPredicate(DayOfWeek dayOfWeek, int slotNum, LocalTime time, Period period) { + assert period != null; + this.dayOfWeek = dayOfWeek; + this.slotNum = slotNum; + this.time = time; + this.period = period; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof PersonIsWorkingPredicate // instanceof handles nulls + && (this.dayOfWeek.equals(((PersonIsWorkingPredicate) other).dayOfWeek)) + && (this.slotNum == ((PersonIsWorkingPredicate) other).slotNum) + && (this.time.equals(((PersonIsWorkingPredicate) other).time))); + } + + @Override + public boolean test(Person person) { + + if (time != null && dayOfWeek != null) { + return person.isWorking(dayOfWeek, time, period); + } else if (slotNum != ViewShiftCommand.INVALID_SLOT_NUMBER && dayOfWeek != null) { + return person.isWorking(dayOfWeek, slotNum, period); + } else { + return false; // can consider throwing an exception? + } + } +} diff --git a/src/main/java/seedu/address/model/person/predicates/StaffHasCorrectIndexPredicate.java b/src/main/java/seedu/address/model/person/predicates/StaffHasCorrectIndexPredicate.java new file mode 100644 index 00000000000..4a717e05e88 --- /dev/null +++ b/src/main/java/seedu/address/model/person/predicates/StaffHasCorrectIndexPredicate.java @@ -0,0 +1,51 @@ +package seedu.address.model.person.predicates; + +import java.util.function.Predicate; + +import seedu.address.model.Model; +import seedu.address.model.person.Person; + +/** + * Tests that a {@code Person}'s {@code Index} matches the index given. + * + * This class is used in FindCommand. + */ +public class StaffHasCorrectIndexPredicate implements Predicate { + private final int index; + private final Model model; + private final Person correctPerson; + + /** + * Constructs a StaffHasCorrectIndexPredicate object which returns true if the person tested against is the same as + * the person corresponding to the index inputted. + * + * @param index The index that the user inputted. + * @param model The model containing the list of Persons who will be searched from. + */ + public StaffHasCorrectIndexPredicate(int index, Model model) { + this.index = index; + this.model = model; + this.correctPerson = model.getFilteredPersonListByIndex(index); + } + + @Override + public boolean test(Person person) { + return correctPerson.isSamePerson(person); + } + + /** + * Returns the index of a StaffHasCorrectIndexPredicate object. + * + * @return the index of a StaffHasCorrectIndexPredicate object. + */ + public int getIndex() { + return this.index; + } + + @Override + public boolean equals(Object other) { + return other == this // short circuit if same object + || (other instanceof StaffHasCorrectIndexPredicate // instanceof handles nulls + && index == (((StaffHasCorrectIndexPredicate) other).getIndex())); // state check + } +} diff --git a/src/main/java/seedu/address/model/tag/Tag.java b/src/main/java/seedu/address/model/tag/Tag.java index b0ea7e7dad7..68b1be1f1a7 100644 --- a/src/main/java/seedu/address/model/tag/Tag.java +++ b/src/main/java/seedu/address/model/tag/Tag.java @@ -3,11 +3,13 @@ import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.AppUtil.checkArgument; +import seedu.address.model.person.Field; + /** * Represents a Tag in the address book. * Guarantees: immutable; name is valid as declared in {@link #isValidTagName(String)} */ -public class Tag { +public class Tag implements Field { public static final String MESSAGE_CONSTRAINTS = "Tags names should be alphanumeric"; public static final String VALIDATION_REGEX = "\\p{Alnum}+"; @@ -50,5 +52,4 @@ public int hashCode() { public String toString() { return '[' + tagName + ']'; } - } diff --git a/src/main/java/seedu/address/model/util/SampleDataUtil.java b/src/main/java/seedu/address/model/util/SampleDataUtil.java index 1806da4facf..f277067e6a8 100644 --- a/src/main/java/seedu/address/model/util/SampleDataUtil.java +++ b/src/main/java/seedu/address/model/util/SampleDataUtil.java @@ -6,11 +6,14 @@ import seedu.address.model.AddressBook; import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Address; import seedu.address.model.person.Email; import seedu.address.model.person.Name; +import seedu.address.model.person.Period; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.Role; +import seedu.address.model.person.Salary; +import seedu.address.model.person.Status; import seedu.address.model.tag.Tag; /** @@ -20,23 +23,24 @@ public class SampleDataUtil { public static Person[] getSamplePersons() { return new Person[] { new Person(new Name("Alex Yeoh"), new Phone("87438807"), new Email("alexyeoh@example.com"), - new Address("Blk 30 Geylang Street 29, #06-40"), - getTagSet("friends")), + getRoleSet("floor"), + new Salary("11"), Status.PART_TIME, + getTagSet("friends"), getPeriodSet()), 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")), + getRoleSet("kitchen"), new Salary("6"), + Status.FULL_TIME, getTagSet("colleagues", "friends"), getPeriodSet()), 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")), + getRoleSet("bartender"), new Salary("7.5"), + Status.PART_TIME, getTagSet("neighbours"), getPeriodSet()), 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")), + getRoleSet("kitchen"), new Salary("8"), + Status.FULL_TIME, getTagSet("family"), getPeriodSet()), 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")), + getRoleSet("kitchen"), new Salary("8.5"), Status.PART_TIME, + getTagSet("classmates"), getPeriodSet()), 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")) + getRoleSet("floor"), new Salary("7"), Status.PART_TIME, + getTagSet("colleagues"), getPeriodSet()) }; } @@ -57,4 +61,22 @@ public static Set getTagSet(String... strings) { .collect(Collectors.toSet()); } + /** + * Returns a period set containing the list of strings given. + */ + public static Set getPeriodSet(String ... strings) { + return Arrays.stream(strings) + .map(Period::transformStringToPeriod) + .collect(Collectors.toSet()); + } + + + /** + * Returns a role set containing the list of strings given. + */ + public static Set getRoleSet(String... strings) { + return Arrays.stream(strings) + .map(Role::translateStringToRoleWithNoRole) + .collect(Collectors.toSet()); + } } diff --git a/src/main/java/seedu/address/storage/JsonAdaptedPeriod.java b/src/main/java/seedu/address/storage/JsonAdaptedPeriod.java new file mode 100644 index 00000000000..aac6bd7c37e --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedPeriod.java @@ -0,0 +1,51 @@ +package seedu.address.storage; + +import static seedu.address.model.person.Period.isValidPeriodString; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.person.Period; + +/** + * Json friendly version of {@link Period} + */ +public class JsonAdaptedPeriod { + private final String period; + + /** + * Constructs a {@code JsonAdaptedPeriod} with the given string source. + */ + @JsonCreator + public JsonAdaptedPeriod(String period) { + this.period = period; + } + + /** + * Converts a given {@code Period} into this class for Jackson use. + */ + public JsonAdaptedPeriod(Period period) { + this.period = period.toString(); + } + + @JsonValue + public String getPeriodName() { + return period; + } + + /** + * Converts this Jackson-friendly adapted Period object into the model's {@code Period} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted tag. + */ + public Period toModelType() throws IllegalValueException { + if (!isValidPeriodString(period)) { + throw new IllegalArgumentException(Period.MESSAGE_CONSTRAINTS); + } + return Period.transformStringToPeriod(period); + } + + + +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java index a6321cec2ea..ba8bdad772f 100644 --- a/src/main/java/seedu/address/storage/JsonAdaptedPerson.java +++ b/src/main/java/seedu/address/storage/JsonAdaptedPerson.java @@ -10,13 +10,18 @@ import com.fasterxml.jackson.annotation.JsonProperty; import seedu.address.commons.exceptions.IllegalValueException; -import seedu.address.model.person.Address; import seedu.address.model.person.Email; import seedu.address.model.person.Name; +import seedu.address.model.person.Period; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; +import seedu.address.model.person.Role; +import seedu.address.model.person.Salary; +import seedu.address.model.person.Schedule; +import seedu.address.model.person.Status; import seedu.address.model.tag.Tag; + /** * Jackson-friendly version of {@link Person}. */ @@ -27,23 +32,44 @@ class JsonAdaptedPerson { private final String name; private final String phone; private final String email; - private final String address; + private final List roles = new ArrayList<>(); + private final String salary; + private final String status; + private final JsonAdaptedSchedule schedule; private final List tagged = new ArrayList<>(); + private final List absentDates = new ArrayList<>(); /** * Constructs a {@code JsonAdaptedPerson} with the given person details. */ @JsonCreator - public JsonAdaptedPerson(@JsonProperty("name") String name, @JsonProperty("phone") String phone, - @JsonProperty("email") String email, @JsonProperty("address") String address, - @JsonProperty("tagged") List tagged) { + public JsonAdaptedPerson(@JsonProperty("name") String name, + @JsonProperty("phone") String phone, @JsonProperty("email") String email, + @JsonProperty("role") List roles, @JsonProperty("salary") String salary, + @JsonProperty("status") String status, @JsonProperty("tagged") List tagged, + @JsonProperty("schedule") JsonAdaptedSchedule schedule, + @JsonProperty("absentDates") List absentDates) { this.name = name; this.phone = phone; this.email = email; - this.address = address; + if (roles != null) { + this.roles.addAll(roles); + } + this.salary = salary; + this.status = status; + if (schedule != null) { + this.schedule = schedule; + } else { + this.schedule = new JsonAdaptedSchedule(new Schedule()); + } if (tagged != null) { this.tagged.addAll(tagged); } + if (absentDates != null) { + this.absentDates.addAll(absentDates); + } + + } /** @@ -53,10 +79,18 @@ public JsonAdaptedPerson(Person source) { name = source.getName().fullName; phone = source.getPhone().value; email = source.getEmail().value; - address = source.getAddress().value; + roles.addAll(source.getRoles().stream() + .map(JsonAdaptedRole::new) + .collect(Collectors.toList())); + salary = source.getSalary().toString(); + status = source.getStatus().getValue(); tagged.addAll(source.getTags().stream() .map(JsonAdaptedTag::new) .collect(Collectors.toList())); + absentDates.addAll(source.getAbsentDates().stream() + .map(JsonAdaptedPeriod::new) + .collect(Collectors.toList())); + schedule = new JsonAdaptedSchedule(source.getSchedule()); } /** @@ -70,6 +104,20 @@ public Person toModelType() throws IllegalValueException { personTags.add(tag.toModelType()); } + final List personAbsentPeriods = new ArrayList<>(); + for (JsonAdaptedPeriod period : absentDates) { + personAbsentPeriods.add(period.toModelType()); + } + + + final Set personRoles = new HashSet<>(); + for (JsonAdaptedRole role : roles) { + personRoles.add(role.toModelType()); + } + if (personRoles.contains(Role.NO_ROLE) && personRoles.size() != 1) { + throw new IllegalValueException(Role.MESSAGE_CONSTRAINTS); + } + if (name == null) { throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Name.class.getSimpleName())); } @@ -94,16 +142,40 @@ public Person toModelType() throws IllegalValueException { } final Email modelEmail = new Email(email); - if (address == null) { - throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Address.class.getSimpleName())); + final Set modelRoles = new HashSet<>(personRoles); + + if (salary == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Salary.class.getSimpleName())); + } + if (!Salary.isValidSalary(salary)) { + throw new IllegalValueException(Salary.MESSAGE_CONSTRAINTS); + } + final Salary modelSalary = new Salary(salary); + + if (status == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, Status.class.getSimpleName())); } - if (!Address.isValidAddress(address)) { - throw new IllegalValueException(Address.MESSAGE_CONSTRAINTS); + if (!Status.isValidStatus(status)) { + throw new IllegalValueException(Status.MESSAGE_CONSTRAINTS); } - final Address modelAddress = new Address(address); + final Status modelStatus = Status.translateStringToStatus(status); final Set modelTags = new HashSet<>(personTags); - return new Person(modelName, modelPhone, modelEmail, modelAddress, modelTags); + final Set modelPeriods = new HashSet<>(personAbsentPeriods); + + if (schedule == null) { + throw new IllegalValueException(String.format(MISSING_FIELD_MESSAGE_FORMAT, + Schedule.class.getSimpleName())); + } + + + Schedule modelSchedule; + modelSchedule = schedule.toModelType(); + Person p = new Person(modelName, modelPhone, modelEmail, + modelRoles, modelSalary, modelStatus, modelTags, modelPeriods); + + p.setSchedule(modelSchedule); + return p; } } diff --git a/src/main/java/seedu/address/storage/JsonAdaptedRecurrencePeriod.java b/src/main/java/seedu/address/storage/JsonAdaptedRecurrencePeriod.java new file mode 100644 index 00000000000..e090e6544da --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedRecurrencePeriod.java @@ -0,0 +1,60 @@ +package seedu.address.storage; + +import static seedu.address.commons.util.DateTimeUtil.TIME_FORMATTER; + +import java.time.LocalTime; +import java.time.format.DateTimeParseException; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.RecurrencePeriod; + +/** + * Class representing the {@code RecurrencePeriod} object adapted for Json. + */ +public class JsonAdaptedRecurrencePeriod { + private final JsonAdaptedPeriod period; + private final String startTime; + private final String endTime; + + /** + * Constructs a {@code JsonAdaptedRecurrencePeriod} with the given RecurrencePeriod details. + */ + @JsonCreator + public JsonAdaptedRecurrencePeriod(@JsonProperty("period") JsonAdaptedPeriod period, + @JsonProperty("startTime") String startTime, + @JsonProperty("endTime") String endTime) { + this.period = period; + this.startTime = startTime; + this.endTime = endTime; + } + + /** + * Constructs a {@code JsonAdaptedRecurrencePeriod} with the given RecurrencePeriod. + * + */ + public JsonAdaptedRecurrencePeriod(RecurrencePeriod period) { + this.period = new JsonAdaptedPeriod(period.getPeriod()); + this.startTime = period.getStartTime().toString(); + this.endTime = period.getEndTime().toString(); + } + + /** + * Creates {@code RecurrencePeriod} that this is representing and + * returns it. + */ + public RecurrencePeriod toModelType() throws IllegalValueException { + try { + LocalTime modelStartTime = LocalTime.parse(startTime, TIME_FORMATTER); + LocalTime modelEndTime = LocalTime.parse(endTime, TIME_FORMATTER); + return new RecurrencePeriod(period.toModelType(), modelStartTime, modelEndTime); + } catch (DateTimeParseException e) { + throw new IllegalValueException(e.getMessage()); + } + + } + + +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedRole.java b/src/main/java/seedu/address/storage/JsonAdaptedRole.java new file mode 100644 index 00000000000..a4f26288b07 --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedRole.java @@ -0,0 +1,48 @@ +package seedu.address.storage; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.person.Role; + +/** + * Jackson-friendly version of {@link Role}. + */ +class JsonAdaptedRole { + + private final String roleName; + + /** + * Constructs a {@code JsonAdaptedRole} with the given {@code roleName}. + */ + @JsonCreator + public JsonAdaptedRole(String roleName) { + this.roleName = roleName; + } + + /** + * Converts a given {@code Role} into this class for Jackson use. + */ + public JsonAdaptedRole(Role source) { + roleName = source.toString(); + } + + @JsonValue + public String getRoleName() { + return roleName; + } + + /** + * Converts this Jackson-friendly adapted role object into the model's {@code Role} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted role. + */ + public Role toModelType() throws IllegalValueException { + if (!Role.isValidRole(roleName)) { + throw new IllegalValueException(Role.STORAGE_WRONG_ROLE_MESSAGE); + } + return Role.translateStringToRoleWithNoRole(roleName); + } + +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedSchedule.java b/src/main/java/seedu/address/storage/JsonAdaptedSchedule.java new file mode 100644 index 00000000000..6808c0ce280 --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedSchedule.java @@ -0,0 +1,63 @@ +package seedu.address.storage; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.model.person.Schedule; +import seedu.address.model.person.Shift; + +public class JsonAdaptedSchedule { + + private static final int DAY_OF_WEEK = 7; + private static final int PERIOD_OF_DAY = 2; + + private final JsonAdaptedShift[][] shifts; + + + @JsonCreator + public JsonAdaptedSchedule(@JsonProperty("shifts") JsonAdaptedShift[][] shifts) { + this.shifts = shifts; + } + + /** + * Constructs a {@code JsonAdaptedSchedule} with the given {@code Schedule} source. + */ + public JsonAdaptedSchedule(Schedule schedule) { + Shift[][] shifts = schedule.getShifts(); + this.shifts = new JsonAdaptedShift[DAY_OF_WEEK][PERIOD_OF_DAY]; + for (int i = 0; i < PERIOD_OF_DAY; i++) { + for (int j = 0; j < DAY_OF_WEEK; j++) { + if (shifts[j][i] == null) { + continue; + } + this.shifts[j][i] = new JsonAdaptedShift(shifts[j][i]); + } + } + } + + /** + * Converts this Jackson-friendly adapted role object into the model's {@code Schedule} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted role. + */ + public Schedule toModelType() throws IllegalValueException { + Shift[][] modelShifts = new Shift[DAY_OF_WEEK][PERIOD_OF_DAY]; + for (int i = 0; i < PERIOD_OF_DAY; i++) { + for (int j = 0; j < DAY_OF_WEEK; j++) { + if (this.shifts[j][i] == null) { + continue; + } + Shift shift = this.shifts[j][i].toModelType(); + int location = shift.getDayOfWeek().getValue() - 1; + int slot = shift.getSlot().getOrder(); + if (modelShifts[location][slot] != null) { + throw new IllegalValueException("Duplicate shift entry in storage."); + } + modelShifts[location][slot] = shift; + + } + } + return new Schedule(modelShifts); + } +} diff --git a/src/main/java/seedu/address/storage/JsonAdaptedShift.java b/src/main/java/seedu/address/storage/JsonAdaptedShift.java new file mode 100644 index 00000000000..8d7297a294d --- /dev/null +++ b/src/main/java/seedu/address/storage/JsonAdaptedShift.java @@ -0,0 +1,98 @@ +package seedu.address.storage; + +import static seedu.address.model.person.Slot.isValidSlot; + +import java.time.DayOfWeek; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; + +import seedu.address.commons.exceptions.IllegalValueException; +import seedu.address.commons.exceptions.InvalidShiftTimeException; +import seedu.address.model.RecurrencePeriod; +import seedu.address.model.person.EmptyShift; +import seedu.address.model.person.Shift; +import seedu.address.model.person.Slot; + +public class JsonAdaptedShift { + private static final String BOOLEAN_CONSTRAINTS = "Shift should have a true or false value to indicate if " + + "this shift is active"; + + private final String dayOfWeek; + private final String slot; + private final boolean isEmpty; + private final List history = new ArrayList<>(); + + /** + * Constructs a {@code JsonAdaptedRole} with the given Shift details. + */ + @JsonCreator + public JsonAdaptedShift(@JsonProperty("dayOfWeek") String dayOfWeek, @JsonProperty("slot") String slot, + @JsonProperty("history") List history, + @JsonProperty("isEmpty") boolean isEmpty) { + this.slot = slot; + this.dayOfWeek = dayOfWeek; + if (history != null) { + this.history.addAll(history); + } + this.isEmpty = isEmpty; + } + + /** + * Constructs a {@code JsonAdaptedShift} with the given Shift source. + */ + public JsonAdaptedShift(Shift shift) { + assert shift != null; + this.dayOfWeek = shift.getDayOfWeek().toString(); + this.slot = shift.getSlot().getValue(); + isEmpty = shift.isEmpty(); + List history = shift.getRecurrences(); + this.history.addAll(history + .stream() + .map(JsonAdaptedRecurrencePeriod::new) + .collect(Collectors.toList())); + } + + /** + * Converts this Jackson-friendly adapted role object into the model's {@code Shift} object. + * + * @throws IllegalValueException if there were any data constraints violated in the adapted role. + */ + public Shift toModelType() throws IllegalValueException { + if (!isValidSlot(slot)) { + throw new IllegalValueException(Slot.MESSAGE_CONSTRAINTS); + } + Slot modelSlot = Slot.translateStringToSlot(slot); + DayOfWeek modelDayOfWeek = toModelTypeDayOfWeek(); + List periods = new ArrayList<>(); + for (JsonAdaptedRecurrencePeriod period : history) { + RecurrencePeriod toAdd = period.toModelType(); + try { + Shift.checkTimeOrder(toAdd.getStartTime(), toAdd.getEndTime(), modelSlot.getOrder()); + } catch (InvalidShiftTimeException e) { + throw new IllegalValueException(e.getMessage()); + } + periods.add(toAdd); + } + + //empty case + if (isEmpty) { + //create empty + return new EmptyShift(modelDayOfWeek, modelSlot); + } + Shift result = new Shift(modelDayOfWeek, modelSlot, periods); + return result; + } + + private DayOfWeek toModelTypeDayOfWeek() throws IllegalValueException { + try { + return DayOfWeek.valueOf(dayOfWeek); + } catch (IllegalArgumentException re) { + throw new IllegalValueException(re.getMessage()); + } + } + +} diff --git a/src/main/java/seedu/address/storage/RoleReqStorage.java b/src/main/java/seedu/address/storage/RoleReqStorage.java new file mode 100644 index 00000000000..9d01a81e931 --- /dev/null +++ b/src/main/java/seedu/address/storage/RoleReqStorage.java @@ -0,0 +1,165 @@ +package seedu.address.storage; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.Arrays; +import java.util.Scanner; + +public class RoleReqStorage { + + public static final String FILEPATH = "data/RoleReq.txt"; + private static File file = new File(FILEPATH); + + // The role name and the requirements have corresponding indexes. + // For example, "bartender" is in roleNames[0], and its role requirements corresponds to requirements[0] + private static final String[] roleNames = new String[]{"bartender", "floor", "kitchen"}; + private static final int[] DEFAULT_REQUIREMENTS = new int[]{0, 0, 0}; + private static int[] requirements = new int[]{0, 0, 0}; + + /** + * Loads the existing file for the list of requirements if it exists. + * + * @return an int[] of requirements. + * @throws IOException If there are errors processing the file. + */ + public static int[] load() throws IOException { + + if (!file.exists()) { + // Create the data folder if it does not exist. + if (!file.getParentFile().exists()) { + file.getParentFile().mkdir(); + } + file.createNewFile(); // Create the RoleReq.txt file. + update(); + return requirements; // default should be 0, 0, 0 + } + + if (file.length() == 0) { + update(); + return requirements; // default should be 0, 0, 0 + } + + return readFileModifyRequirements(); + } + + /** + * Reads the saved file and adds tasks to the tasklist. + * + * @return An int[] of tasks read from the save file. + * @throws FileNotFoundException If saved file cannot be found. + */ + private static int[] readFileModifyRequirements() throws FileNotFoundException { + Scanner fileSc = new Scanner(file); + int i = 0; + + while (fileSc.hasNext() && i < 3) { + String nextLine = fileSc.nextLine(); + requirements[i] = Integer.parseInt(nextLine); + i++; + } + + fileSc.close(); + return requirements; + } + + /** + * Updates the file. + * + * @throws IOException If the file cannot be created or opened. + */ + private static void update() throws IOException { + if (!file.exists()) { + // Create the data folder if it does not exist. + if (!file.getParentFile().exists()) { + file.getParentFile().mkdir(); + } + file.createNewFile(); // Create the RoleReq.txt file. + } + + StringBuilder txt = new StringBuilder(); + for (int n : requirements) { + txt.append(n).append("\n"); + } + + PrintWriter pw = new PrintWriter(file); + pw.append(txt.toString()); + pw.flush(); + } + + /** + * Updates the save file with the new minimum number of staff required for a specific role. + * + * @param role The role which minimum requirement will be updated. + * @param numMinStaff The minimum number of staff required for that role. + */ + public static void update(String role, int numMinStaff) throws IOException { + if (!file.exists()) { + // Create the data folder if it does not exist. + if (!file.getParentFile().exists()) { + file.getParentFile().mkdir(); + } + file.createNewFile(); // Create the RoleReq.txt file. + } + + int roleNum = getNumFromRole(role.toLowerCase()); + requirements[roleNum] = numMinStaff; + update(); + } + + private static int getNumFromRole(String role) { + return Arrays.asList(roleNames).indexOf(role); + } + + /** + * Returns the minimum of bartenders required for each shift. + * + * @return the minimum of bartenders required for each shift. + */ + public static int getMinNumBartender() { + return requirements[0]; + } + + /** + * Returns the minimum of floor staff required for each shift. + * + * @return the minimum of floor staff required for each shift. + */ + public static int getMinNumFloor() { + return requirements[1]; + } + + /** + * Returns the minimum of kitchen staff required for each shift. + * + * @return the minimum of kitchen staff required for each shift. + */ + public static int getMinNumKitchen() { + return requirements[2]; + } + + /** + * Returns a String representing the current Role Requirements. + * + * @return String representation of the current Role Requirements. + */ + public static String getRoleReqs() { + StringBuilder result = new StringBuilder(); + for (int i = 0; i < roleNames.length; i++) { + result.append(roleNames[i] + ": " + requirements[i] + "\n"); + } + return result.toString(); + } + + /** + * Resets the timings to the default timings. + * + * @throws IOException If the file cannot be created or opened. + */ + public static void reset() throws IOException { + requirements = DEFAULT_REQUIREMENTS; + update(); + } +} + diff --git a/src/main/java/seedu/address/ui/DayCard.java b/src/main/java/seedu/address/ui/DayCard.java new file mode 100644 index 00000000000..cf87aafd9c1 --- /dev/null +++ b/src/main/java/seedu/address/ui/DayCard.java @@ -0,0 +1,82 @@ +package seedu.address.ui; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.TextStyle; +import java.util.Locale; +import java.util.logging.Logger; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import seedu.address.commons.core.LogsCenter; +import seedu.address.model.person.Period; +import seedu.address.model.person.Person; +import seedu.address.model.person.Slot; + +/** + * An UI component that displays information of a {@code staffList} + * of the staff who work in a given date. + */ +public class DayCard extends UiPart { + + private static final String FXML = "DayCard.fxml"; + private final Logger logger = LogsCenter.getLogger(DayCard.class); + + /** + * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. + * As a consequence, UI elements' variable names cannot be set to such keywords + * or an exception will be thrown by JavaFX during runtime. + * + * @see The issue on AddressBook level 4 + */ + + private final int dayCardNumber; + + @FXML + private VBox slotPane; + @FXML + private Label dayDateLabel; + + /** + * Creates a {@code DayCard} with the given {@code LocalDate firstDate} + * and {@code ObservableList staffList} to display. + */ + public DayCard(LocalDate firstDate, int dayCardNumber, ObservableList stafflist, Period currentPeriod) { + super(FXML); + this.dayCardNumber = dayCardNumber; + + DayOfWeek day = firstDate.getDayOfWeek().plus(dayCardNumber); + LocalDate date = firstDate.plusDays(dayCardNumber); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yy"); + String text = day.getDisplayName(TextStyle.SHORT, Locale.getDefault()) + ", " + + date.format(formatter); + dayDateLabel.setText(text); + SlotCard morningSlotCard = new SlotCard(day, Slot.MORNING, stafflist, currentPeriod, date); + SlotCard afternoonSlotCard = new SlotCard(day, Slot.AFTERNOON, stafflist, currentPeriod, date); + slotPane.getChildren().addAll(morningSlotCard.getRoot(), afternoonSlotCard.getRoot()); + VBox.setVgrow(morningSlotCard.getRoot(), Priority.ALWAYS); + VBox.setVgrow(afternoonSlotCard.getRoot(), Priority.ALWAYS); + } + + @Override + public boolean equals(Object other) { + // short circuit if same object + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof DayCard)) { + return false; + } + + // state check + DayCard card = (DayCard) other; + return dayCardNumber == card.dayCardNumber; + } +} diff --git a/src/main/java/seedu/address/ui/HelpWindow.java b/src/main/java/seedu/address/ui/HelpWindow.java index 9a665915949..1d924184fc6 100644 --- a/src/main/java/seedu/address/ui/HelpWindow.java +++ b/src/main/java/seedu/address/ui/HelpWindow.java @@ -15,7 +15,7 @@ */ public class HelpWindow extends UiPart { - public static final String USERGUIDE_URL = "https://se-education.org/addressbook-level3/UserGuide.html"; + public static final String USERGUIDE_URL = "https://ay2122s1-cs2103t-w11-2.github.io/tp/UserGuide.html"; public static final String HELP_MESSAGE = "Refer to the user guide: " + USERGUIDE_URL; private static final Logger logger = LogsCenter.getLogger(HelpWindow.class); diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java index 9106c3aa6e5..97d087180d9 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/address/ui/MainWindow.java @@ -5,6 +5,7 @@ import javafx.event.ActionEvent; import javafx.fxml.FXML; import javafx.scene.control.MenuItem; +import javafx.scene.control.TabPane; import javafx.scene.control.TextInputControl; import javafx.scene.input.KeyCombination; import javafx.scene.input.KeyEvent; @@ -16,6 +17,7 @@ import seedu.address.logic.commands.CommandResult; import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; +import seedu.address.model.person.Period; /** * The Main Window. Provides the basic application layout containing @@ -32,23 +34,22 @@ public class MainWindow extends UiPart { // Independent Ui parts residing in this Ui container private PersonListPanel personListPanel; + private WeekShiftsPane schedulePanel; private ResultDisplay resultDisplay; private HelpWindow helpWindow; @FXML private StackPane commandBoxPlaceholder; - @FXML private MenuItem helpMenuItem; - @FXML private StackPane personListPanelPlaceholder; - + @FXML + private StackPane schedulePanelPlaceholder; @FXML private StackPane resultDisplayPlaceholder; - @FXML - private StackPane statusbarPlaceholder; + private TabPane tabPane; /** * Creates a {@code MainWindow} with the given {@code Stage} and {@code Logic}. @@ -113,12 +114,13 @@ void fillInnerParts() { personListPanel = new PersonListPanel(logic.getFilteredPersonList()); personListPanelPlaceholder.getChildren().add(personListPanel.getRoot()); + schedulePanel = new WeekShiftsPane(logic.getFilteredPersonList()); + + schedulePanelPlaceholder.getChildren().add(schedulePanel.getRoot()); + resultDisplay = new ResultDisplay(); resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot()); - StatusBarFooter statusBarFooter = new StatusBarFooter(logic.getAddressBookFilePath()); - statusbarPlaceholder.getChildren().add(statusBarFooter.getRoot()); - CommandBox commandBox = new CommandBox(this::executeCommand); commandBoxPlaceholder.getChildren().add(commandBox.getRoot()); } @@ -163,8 +165,20 @@ private void handleExit() { primaryStage.hide(); } - public PersonListPanel getPersonListPanel() { - return personListPanel; + /** + * Switches the tab showing. + */ + @FXML + private void handleSwitchTab() { + if (tabPane.getSelectionModel().isSelected(1)) { + tabPane.getSelectionModel().selectFirst(); + } else { + tabPane.getSelectionModel().selectNext(); + } + } + + public void setPeriod(Period period) { + this.schedulePanel.setChildren(logic.getFilteredPersonList(), period); } /** @@ -186,6 +200,14 @@ private CommandResult executeCommand(String commandText) throws CommandException handleExit(); } + if (commandResult.isSwitchTab()) { + handleSwitchTab(); + } + + if (commandResult.isChangeSchedule()) { + setPeriod(commandResult.getPeriod()); + } + return commandResult; } catch (CommandException | ParseException e) { logger.info("Invalid command: " + commandText); diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java index 7fc927bc5d9..c221c86fde4 100644 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ b/src/main/java/seedu/address/ui/PersonCard.java @@ -2,12 +2,16 @@ import java.util.Comparator; +import javafx.collections.FXCollections; import javafx.fxml.FXML; import javafx.scene.control.Label; +import javafx.scene.control.ListView; import javafx.scene.layout.FlowPane; import javafx.scene.layout.HBox; import javafx.scene.layout.Region; +import seedu.address.model.person.Period; import seedu.address.model.person.Person; +import seedu.address.model.person.Role; /** * An UI component that displays information of a {@code Person}. @@ -24,37 +28,58 @@ public class PersonCard extends UiPart { * @see The issue on AddressBook level 4 */ - public final Person person; + public final Person staff; + private int displayedIndex; @FXML private HBox cardPane; @FXML - private Label name; + private Label index; @FXML - private Label id; + private Label name; @FXML private Label phone; @FXML - private Label address; + private ListView periodListView; @FXML private Label email; @FXML - private FlowPane tags; + private FlowPane roles; + @FXML + private Label salary; + @FXML + private Label status; + @FXML + private ListView tagsListView; /** * Creates a {@code PersonCode} with the given {@code Person} and index to display. */ - public PersonCard(Person person, int displayedIndex) { + public PersonCard(Person staff, int displayedIndex) { super(FXML); - this.person = person; - id.setText(displayedIndex + ". "); - name.setText(person.getName().fullName); - phone.setText(person.getPhone().value); - address.setText(person.getAddress().value); - email.setText(person.getEmail().value); - person.getTags().stream() - .sorted(Comparator.comparing(tag -> tag.tagName)) - .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); + this.staff = staff; + this.displayedIndex = displayedIndex; + index.setText(String.valueOf(displayedIndex)); + name.setText(staff.getName().fullName); + phone.setText(staff.getPhone().value); + email.setText(staff.getEmail().value); + salary.setText(staff.getSalary().convertToDollars()); + status.setText(staff.getStatus().getValue()); + + String[] periodArray = + staff.getAbsentDates().stream().map(Period::toDisplayString).sorted().toArray(String[]::new); + periodListView.setItems(FXCollections.observableArrayList(periodArray)); + + staff.getRoles().stream() + .sorted(Comparator.comparing(Role::toString)) + .forEach(role -> roles.getChildren().add(new Label(role.toString()))); + + String[] tagArray = staff.getTags().stream().map(tag -> tag.tagName).sorted().toArray(String[]::new); + tagsListView.setItems(FXCollections.observableArrayList(tagArray)); + } + + public int getDisplayedIndex() { + return displayedIndex; } @Override @@ -71,7 +96,7 @@ public boolean equals(Object other) { // state check PersonCard card = (PersonCard) other; - return id.getText().equals(card.id.getText()) - && person.equals(card.person); + return displayedIndex == card.getDisplayedIndex() + && staff.equals(card.staff); } } diff --git a/src/main/java/seedu/address/ui/PersonListPanel.java b/src/main/java/seedu/address/ui/PersonListPanel.java index f4c501a897b..308dcbe557d 100644 --- a/src/main/java/seedu/address/ui/PersonListPanel.java +++ b/src/main/java/seedu/address/ui/PersonListPanel.java @@ -23,9 +23,9 @@ public class PersonListPanel extends UiPart { /** * Creates a {@code PersonListPanel} with the given {@code ObservableList}. */ - public PersonListPanel(ObservableList personList) { + public PersonListPanel(ObservableList staffList) { super(FXML); - personListView.setItems(personList); + personListView.setItems(staffList); personListView.setCellFactory(listView -> new PersonListViewCell()); } @@ -34,16 +34,15 @@ public PersonListPanel(ObservableList personList) { */ class PersonListViewCell extends ListCell { @Override - protected void updateItem(Person person, boolean empty) { - super.updateItem(person, empty); + protected void updateItem(Person staff, boolean empty) { + super.updateItem(staff, empty); - if (empty || person == null) { + if (empty || staff == null) { setGraphic(null); setText(null); } else { - setGraphic(new PersonCard(person, getIndex() + 1).getRoot()); + setGraphic(new PersonCard(staff, getIndex() + 1).getRoot()); } } } - } diff --git a/src/main/java/seedu/address/ui/SlotCard.java b/src/main/java/seedu/address/ui/SlotCard.java new file mode 100644 index 00000000000..27152c1bb30 --- /dev/null +++ b/src/main/java/seedu/address/ui/SlotCard.java @@ -0,0 +1,86 @@ +package seedu.address.ui; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.util.logging.Logger; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import seedu.address.commons.core.LogsCenter; +import seedu.address.model.person.Period; +import seedu.address.model.person.Person; +import seedu.address.model.person.Slot; + +/** + * An UI component that displays information of a {@code SlotCard}. + */ +public class SlotCard extends UiPart { + + private static final String FXML = "SlotCard.fxml"; + private final Logger logger = LogsCenter.getLogger(SlotCard.class); + + /** + * Note: Certain keywords such as "location" and "resources" are reserved keywords in JavaFX. + * As a consequence, UI elements' variable names cannot be set to such keywords + * or an exception will be thrown by JavaFX during runtime. + * + * @see The issue on AddressBook level 4 + */ + + private final DayOfWeek day; + private final LocalDate date; + private final Slot slot; + private ObservableList stafflist; + + @FXML + private VBox slotCard; + @FXML + private Label shiftName; + @FXML + private ListView staffWorkingList; + + + /** + * Creates a {@code SlotCard} with the given {@code ObservableList staffList} + * and index to display. + */ + public SlotCard(DayOfWeek day, Slot slot, ObservableList stafflist, Period period, LocalDate date) { + super(FXML); + this.day = day; + this.date = date; + this.slot = slot; + this.stafflist = stafflist; + shiftName.setText(slot.toString()); + ObservableList filteredList = + stafflist.filtered(p -> p.isWorking(day, slot.getOrder(), period)); + staffWorkingList.setItems(filteredList); + staffWorkingList.refresh(); + staffWorkingList.setCellFactory(listView -> new PersonNameCell()); + } + + /** + * Custom {@code ListCell} that displays the graphics of a {@code Person} with just the name. + */ + class PersonNameCell extends ListCell { + @Override + protected void updateItem(Person staff, boolean empty) { + super.updateItem(staff, empty); + if (empty || staff == null) { + setText(null); + setStyle(""); + } else { + setText(staff.getName().toString()); + if (staff.wasAbsent(date)) { + setStyle("-fx-background-color: #FF7F7F; -fx-text-fill: black"); + } else { + setStyle(""); + } + } + } + } +} diff --git a/src/main/java/seedu/address/ui/StatusBarFooter.java b/src/main/java/seedu/address/ui/StatusBarFooter.java deleted file mode 100644 index b577f829423..00000000000 --- a/src/main/java/seedu/address/ui/StatusBarFooter.java +++ /dev/null @@ -1,28 +0,0 @@ -package seedu.address.ui; - -import java.nio.file.Path; -import java.nio.file.Paths; - -import javafx.fxml.FXML; -import javafx.scene.control.Label; -import javafx.scene.layout.Region; - -/** - * A ui for the status bar that is displayed at the footer of the application. - */ -public class StatusBarFooter extends UiPart { - - private static final String FXML = "StatusBarFooter.fxml"; - - @FXML - private Label saveLocationStatus; - - /** - * Creates a {@code StatusBarFooter} with the given {@code Path}. - */ - public StatusBarFooter(Path saveLocation) { - super(FXML); - saveLocationStatus.setText(Paths.get(".").resolve(saveLocation).toString()); - } - -} diff --git a/src/main/java/seedu/address/ui/UiManager.java b/src/main/java/seedu/address/ui/UiManager.java index 882027e4537..b0ad527e360 100644 --- a/src/main/java/seedu/address/ui/UiManager.java +++ b/src/main/java/seedu/address/ui/UiManager.java @@ -20,7 +20,9 @@ public class UiManager implements Ui { public static final String ALERT_DIALOG_PANE_FIELD_ID = "alertDialogPane"; private static final Logger logger = LogsCenter.getLogger(UiManager.class); - private static final String ICON_APPLICATION = "/images/address_book_32.png"; + // private static final String ICON_APPLICATION = "/images/ + // found under build/resources/main/images + private static final String ICON_APPLICATION = "/images/staffd_icon.jpg"; private Logic logic; private MainWindow mainWindow; diff --git a/src/main/java/seedu/address/ui/WeekShiftsPane.java b/src/main/java/seedu/address/ui/WeekShiftsPane.java new file mode 100644 index 00000000000..652fdc3082a --- /dev/null +++ b/src/main/java/seedu/address/ui/WeekShiftsPane.java @@ -0,0 +1,42 @@ +package seedu.address.ui; + +import java.time.LocalDate; +import java.util.logging.Logger; + +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import seedu.address.commons.core.LogsCenter; +import seedu.address.model.person.Period; +import seedu.address.model.person.Person; + +/** + * Panel containing the list of persons. + */ +public class WeekShiftsPane extends UiPart { + private static final String FXML = "WeekShiftsPane.fxml"; + private final Logger logger = LogsCenter.getLogger(WeekShiftsPane.class); + + @FXML + private HBox weekShiftsPane; + + /** + * Creates a {@code PersonListPanel} with the given {@code ObservableList}. + */ + public WeekShiftsPane(ObservableList staffList) { + super(FXML); + setChildren(staffList, Period.oneWeekFrom(LocalDate.now())); + } + + public void setChildren(ObservableList staffList, Period period) { + LocalDate firstDay = period.getStartDate(); + weekShiftsPane.getChildren().clear(); + for (int i = 0; i < 7; i++) { + DayCard nextDayCard = new DayCard(firstDay, i, staffList, period); + weekShiftsPane.getChildren().add(nextDayCard.getRoot()); + HBox.setHgrow(nextDayCard.getRoot(), Priority.ALWAYS); + } + } +} diff --git a/src/main/resources/images/staffd_icon.jpg b/src/main/resources/images/staffd_icon.jpg new file mode 100644 index 00000000000..ffd1a484946 Binary files /dev/null and b/src/main/resources/images/staffd_icon.jpg differ diff --git a/src/main/resources/view/CommandBox.fxml b/src/main/resources/view/CommandBox.fxml index 09f6d6fe9e4..f312c73fa24 100644 --- a/src/main/resources/view/CommandBox.fxml +++ b/src/main/resources/view/CommandBox.fxml @@ -3,7 +3,7 @@ - + diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index 9ce9bcfb569..b683c96fe21 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -37,6 +37,20 @@ -fx-padding: 0 0 0 0; -fx-min-height: 0; -fx-max-height: 0; + -fx-background-color: derive(#1d1d1d, 20%); +} + +.tab-pane .tab-header-area .tab-header-background { + -fx-background-color: -fx-outer-border, -fx-text-box-border, grey; +} + +.tab.dark { + -fx-background-color: derive(#1d1d1d, 20%); +} + +.tab.dark .tab-label { + -fx-text-fill: white; + } .table-view { @@ -93,10 +107,20 @@ -fx-background-color: derive(#1d1d1d, 20%); } -.list-cell { +#staffWorkingList .list-cell { -fx-label-padding: 0 0 0 0; -fx-graphic-text-gap : 0; -fx-padding: 0 0 0 0; + -fx-text-fill: white; + -fx-font-size: 14px; +} + +#personListView .list-cell { + -fx-label-padding: 0 0 0 0; + -fx-graphic-text-gap : 0; + -fx-padding: 0 0 0 0; + -fx-text-fill: white; + -fx-alignment: center; } .list-cell:filled:even { @@ -126,12 +150,26 @@ -fx-text-fill: #010504; } +.cell_big_label_centered { + -fx-font-family: "Open Sans Semibold"; + -fx-font-size: 16px; + -fx-text-fill: white; + -fx-text-alignment: center; +} + .cell_small_label { -fx-font-family: "Open Sans Regular"; -fx-font-size: 13px; -fx-text-fill: #010504; } +.cell_small_label_centered { + -fx-font-family: "Open Sans Regular"; + -fx-font-size: 13px; + -fx-text-fill: #010504; + -fx-text-alignment: center; +} + .stack-pane { -fx-background-color: derive(#1d1d1d, 20%); } @@ -307,6 +345,18 @@ -fx-padding: 8 1 8 1; } +.slotCard { + -fx-background-color: grey; + -fx-border-width: 2; + -fx-border-color: white; +} + +.big_label_white { + -fx-font-family: "Open Sans Semibold"; + -fx-font-size: 16px; + -fx-text-fill: #ffffff; +} + #cardPane { -fx-background-color: transparent; -fx-border-width: 0; @@ -342,11 +392,11 @@ -fx-vgap: 3; } -#tags .label { +#roles .label { -fx-text-fill: white; -fx-background-color: #3e7b91; -fx-padding: 1 3 1 3; -fx-border-radius: 2; -fx-background-radius: 2; - -fx-font-size: 11; + -fx-font-size: 12; } diff --git a/src/main/resources/view/DayCard.fxml b/src/main/resources/view/DayCard.fxml new file mode 100644 index 00000000000..25b7086e2ac --- /dev/null +++ b/src/main/resources/view/DayCard.fxml @@ -0,0 +1,14 @@ + + + + + + + + +

                            +
                            + + + diff --git a/src/main/resources/view/MainWindow.fxml b/src/main/resources/view/MainWindow.fxml index 32bcf2c8e70..1dcb9948a30 100644 --- a/src/main/resources/view/MainWindow.fxml +++ b/src/main/resources/view/MainWindow.fxml @@ -6,56 +6,82 @@ - + + + + + - + - + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/view/PersonListCard.fxml b/src/main/resources/view/PersonListCard.fxml index f08ea32ad55..e80731c2464 100644 --- a/src/main/resources/view/PersonListCard.fxml +++ b/src/main/resources/view/PersonListCard.fxml @@ -1,36 +1,40 @@ - - - - - - - - + + + - - - - - - - - - - - - - - diff --git a/src/main/resources/view/PersonListPanel.fxml b/src/main/resources/view/PersonListPanel.fxml index 8836d323cc5..399636b9409 100644 --- a/src/main/resources/view/PersonListPanel.fxml +++ b/src/main/resources/view/PersonListPanel.fxml @@ -1,8 +1,43 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - diff --git a/src/main/resources/view/ResultDisplay.fxml b/src/main/resources/view/ResultDisplay.fxml index 58d5ad3dc56..d7a774a058d 100644 --- a/src/main/resources/view/ResultDisplay.fxml +++ b/src/main/resources/view/ResultDisplay.fxml @@ -3,7 +3,7 @@ - -