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).
local-part@domain
. local-part
must only contain alphanumeric characters, excluding and the special characters "+ _ . -
", but cannot start or end with any special character. domain
is made up of domain labels
separated by periods. It must end with a domain label
which is at least 2 characters long. 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}floor
, kitchen
, or bartender
. fulltime
, partime
or nostatus
.{:/}
+$/|-$|Salary (per hour)|{::nomarkdown}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
.TUESDAY-0
and wednesday-1
are valid shifts.{:/}
+rr/|NA|Role Requirements|{::nomarkdown}ROLE-REQUIRED_NUMBER
. YYYY-MM-DD
when provided.DAYOFWEEK-TIME
HH:mm
, and follows the 24hr format.{:/}
+
+:exclamation: Note: "NA" means that the tag does not exist.
+
+
* 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 + */ ObservableListgetFilteredPersonList(); /** 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 extends Person> 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 extends Person> 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 preamblevalue 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 extends Period> 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 extends String> 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