diff --git a/docs/DeveloperGuide.adoc b/docs/DeveloperGuide.adoc index a8b60ff8702..8f12d1cf3df 100644 --- a/docs/DeveloperGuide.adoc +++ b/docs/DeveloperGuide.adoc @@ -201,6 +201,101 @@ Similarly, calling `free` command will: // end::task-scheduling[] +// tag::task-feature[] +=== Task Feature (E.g. Add Delivery Task) +==== Implementation +The *Add Delivery Task* feature adds a new task into a task list. + +It uses the `AddTaskCommand`, which extends `Command`, to add a `Task` into the `TaskManager`. +`AddTaskCommandParser` is also utilised to parse and validate the user inputs before sending it to `AddTaskCommand` to execute. +'AddTaskCommand' requires the following fields: `Task`, `customerId`. +The attributes of Task is as follows: + +.Class Diagram of Task class. +image::Task.png[] + +As seen in the above class diagram, `driver` and `eventTime` are optional fields that are not mandatory when adding a task. +They will be assigned subsequently using `assign` command. (Refer to Assign feature) +The mandatory fields for users are: 'description', 'date' and 'Customer'. +After the validation is completed, `AddTaskCommand` will fetch `Customer` using the `customerId` through the `CustomerManager`. +A unique id will also be allocated to the task for differentiation. + +The following sequence diagrams show how the add task operation works: + +.Sequence Diagram of adding a task. +image::AddTaskCommand.png[] +.Sequence Diagram of Model interaction with the CustomerManager and TaskManager for adding a task. +image::ModelInteractWithManagers.png[] + +[NOTE] +The flow of how the task is being accessed and managed as shown above is the same for other task related command +such as edit task command (`editT`) and delete task command (`del`). + +==== Design Considerations + +===== Aspect: Coupling of Task and other entities (Driver and Customer) + +* **Alternative 1 (current choice):** Task class contains Driver and Customer classes as attributes. +** Pros: Centralised Task class that encapsulates all the information, which makes it easy to manage task. +** Cons: Task will have to depend on Driver and Customer. Decreases testability. +* **Alternative 2:** Driver and Customer classes have Task class as attribute. +** Pros: Easy to access tasks through the respective classes. (Driver and Customer classes) +** Cons: Having 2 classes depend on Task class. Decreases testability. +// end::task-feature[] + + + +// tag:generate-pdf[] +=== Generate PDF Task Summary Feature +==== Implementation +The *generate PDF Task Summary* feature creates a task summary in a user-friendly layout in PDF format for *user reference* and *archive* usage. +`PdfCreator` class creates and saves the PDF document as well as formatting its layout. +It is implemented with the help of an external library, https://github.com/itext/itext7[iText7]. + +[NOTE] +Regarding iText's license, it can be used for free in situations where you distribute your software for free. +It is a Affero General Public License (AGPL) library. + +Information updated as of 6 November 2019. +For more information, please visit the https://itextpdf.com/en[iText official website]. + +The following sequence diagram shows how the user command `savepdf` is being executed and handled. + +.Sequence Diagram of how PDF task summary is saved. +image::SavePdfCommand.png[] + +NOTE: The lifeline for `SavePdfCommandParser` should end at the destroy marker (X) but due to a limitation of PlantUML, the lifeline reaches the end of diagram. + +Notice that only the `filepath` and the `date of delivery` is needed when calling `saveDriverTaskPdf`. +This is because only the saving location of the PDF file and the date, where the task summary will be based on, are the only fields needed for the `PdfCreator`. +The rest of the components, such as fetching of the tasks, will be handled by the `Model` while the formatting will be handled by `PdfCreator`. + +The following sequence diagram shows how the model interact with `PdfCreator` to generate the PDF task summary. + +.Sequence Diagram of how the model generates the PDF task summary. +image::GeneratePdfSequenceDiagram.png[] + +The `PdfWrapperLayout` provides a outer canvas to encapsulates all the layouts. +The following layouts are mainly what makes up the task summary: + +* `PdfDriverLayout` class - wraps driver details. +* `PdfCustomerLayout` class - wraps customer details. +* `PdfTaskLayout` class - wraps task details. + +The following activity diagram shows what happens when a user executes the `savepdf` command: + +.Activity Diagram of how a PDF task summary is generated. +image::GeneratePdfActivityDiagram.png[] + +==== Design Considerations + +===== Aspect: + +* **Alternative 1 (current choice)**: Abstract the layout of each part of the task summary. +** Pros: Encourages reuse and easier to manage and add on. +** Cons: Harder to implement. +* **Alternative 2**: Do the whole task summary layout in 1 class. +** Pros: Easy to implement. +** Cons: Harder to manage. +// end::generate-pdf[] @@ -313,44 +408,6 @@ We are using `java.util.logging` package for logging. The `LogsCenter` class is Certain properties of the application can be controlled (e.g user prefs file location, logging level) through the configuration file (default: `config.json`). -=== Task Feature (E.g. Add Delivery Task) -==== Implementation -The *Add Delivery Task* feature adds a new task into a task list. + -It uses the `AddTaskCommand`, which extends `Command`, to add a `Task` into the `TaskManager`. -`AddTaskCommandParser` is also utilised to parse and validate the user inputs before sending it to `AddTaskCommand` to execute. -'AddTaskCommand' requires the following fields: `Task`, `customerId`. -The attributes of Task is as follows: - -.Class Diagram of Task class. -image::Task.png[] - -As seen in the above class diagram, `driver` and `eventTime` are optional fields that are not mandatory when adding a task. -They will be assigned subsequently using `assign` command. (Refer to Assign feature) -The mandatory fields for users are: 'description', 'date' and 'Customer'. -After the validation is completed, `AddTaskCommand` will fetch `Customer` using the `customerId` through the `CustomerManager`. -A unique id will also be allocated to the task for differentiation. - -The following sequence diagrams show how the add task operation works: - -.Sequence Diagram of adding a task. -image::AddTaskCommand.png[] -.Sequence Diagram of Model interaction with the CustomerManager and TaskManager for adding a task. -image::ModelInteractWithManagers.png[] - -[NOTE] -The flow of how the task is being accessed and managed as shown above is the same for other task related command such as Edit and Delete. - -==== Design Considerations - -===== Aspect: Couping of Task and other entities (Driver and Customer) - -* **Alternative 1 (current choice):** Task class contains Driver and Customer classes as attributes. -** Pros: Centralised Task class that encapsulates all the information, which makes it easy to manage task. -** Cons: Task will have to depend on Driver and Customer. Decreases testability. -* **Alternative 2:** Driver and Customer classes have Task class as attribute. -** Pros: Easy to access tasks through the respective classes. (Driver and Customer classes) -** Cons: Having 2 classes depend on Task class. Decreases testability. - === Customer Feature (E.g. Add Customer) ==== Implementation The *Add Customer* feature adds a new Customer into a Customer list. + diff --git a/docs/UserGuide.adoc b/docs/UserGuide.adoc index e9518c287eb..deeae994183 100644 --- a/docs/UserGuide.adoc +++ b/docs/UserGuide.adoc @@ -195,15 +195,17 @@ Deletes task (Task ID: 1) from the task manager. * `del d/2` + Deletes driver (Driver ID: 2) from the driver manager. -==== Saves assigned delivery tasks into PDF document: `savepdf` -The PDF document is arranged in a table format to allow easy reference of the delivery tasks that is assigned to each drivers for the date. + +==== Saves assigned delivery tasks for a specific date into PDF document: `savepdf` +The PDF document is arranged in a table format to allow easy reference of the delivery tasks that is assigned to each drivers for the date. +Its purpose is for user reference and archive. + +Refer to <> for sample. + Format: `savepdf [DATE]` **** * `DATE` format is dd/mm/yyy. * `DATE` field is OPTIONAL. If date field is not declared, it will take the date of today. * PDF document will be saved under `data` folder which is the same directory as where you put the deliveria.jar. -* Name of the PDF document will be DeliveryTasks (DATE).pdf . +* Name of the PDF document will be DeliveryTasks [DATE].pdf . **** Examples: @@ -263,3 +265,6 @@ There is no need to save manually. *Q*: How do I know the list of commands? + *A*: The `help` command will give a list of available commands. +== Appendix +.PDF Document generated by `savepdf` command +image::/images/DeliveryTasks_Pdf_Layout.png[id="PdfLayout", Delivery Tasks PDF] diff --git a/docs/diagrams/AssignActivityDiagram.puml b/docs/diagrams/AssignActivityDiagram.puml index 6a487016eeb..f83c4c99bb6 100644 --- a/docs/diagrams/AssignActivityDiagram.puml +++ b/docs/diagrams/AssignActivityDiagram.puml @@ -5,22 +5,31 @@ start 'Since the beta syntax does not support placing the condition outside the 'diamond we place it as the true branch instead. -if (Is force assign?) then ([yes]) +if (Are the driver and task valid?) then ([yes]) else ([no]) - : Check against current schedule; - if (The most optimal time slot?) then ([yes]) + stop +endif + +if (Is the task already assigned?) then ([yes]) + +else ([yes]) + if (Is force assign?) then ([yes]) + : Free that task; else ([no]) stop endif endif -if (Within working hours?) then ([yes]) -else ([no]) + +: Check against the driver's schedule; +if (Is the target driver available during the proposed time) then ([yes]) + : assign; stop -endif -if (No schedule clash?) then ([yes]) +else ([no]) + if (Was the task changed?) then ([yes]) + : Restore that task; else ([no]) - stop endif stop + @enduml diff --git a/docs/diagrams/GeneratePdfActivityDiagram.puml b/docs/diagrams/GeneratePdfActivityDiagram.puml new file mode 100644 index 00000000000..8a60ed91440 --- /dev/null +++ b/docs/diagrams/GeneratePdfActivityDiagram.puml @@ -0,0 +1,28 @@ +@startuml +start +:User executes `savepdf` command; + +:Checks if there is tasks +assigned for the day; + +'Since the beta syntax does not support placing the condition outside the +'diamond we place it as the true branch instead. + +if () then ([has tasks assigned for the day]) + if () then ([file path does + not exists]) + :Create file path; + else ([else]) + endif + :Generates a task summary + in PDF format; + :Save PDF document + in the file path; +else ([else]) + :Notify user that no tasks + are assigned for the day; + stop +endif + +stop +@enduml diff --git a/docs/diagrams/GeneratePdfSequenceDiagram.puml b/docs/diagrams/GeneratePdfSequenceDiagram.puml new file mode 100644 index 00000000000..5424a84b37d --- /dev/null +++ b/docs/diagrams/GeneratePdfSequenceDiagram.puml @@ -0,0 +1,67 @@ +@startuml +!include style.puml + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +participant ":PdfCreator" as PdfCreator MODEL_COLOR +participant ":TaskManager" as TaskManager MODEL_COLOR +participant ":PdfWrapperLayout" as PdfWrapperLayout MODEL_COLOR +end box + + +[-> Model : saveDriverTaskPdf( \n FILEPATH*, 5/11/2019) +note right: FILEPATH*: "./data/DeliveryTasks 2019-11-5".pdf" +activate Model + +create PdfCreator +Model -> PdfCreator : new PdfCreator(FILEPATH*) +activate PdfCreator + +PdfCreator --> Model +deactivate PdfCreator + +Model -> TaskManager : getList() +activate TaskManager + +TaskManager --> Model: tasks +deactivate TaskManager + +Model -> TaskManager : getDriversFromTasks() +activate TaskManager + +TaskManager --> Model: drivers +deactivate TaskManager + +Model -> PdfCreator : saveDriverTaskPdf(tasks, drivers, 5/11/2019) +activate PdfCreator + +PdfCreator -> PdfCreator ++: createDocument() +PdfCreator --> PdfCreator --: document + +PdfCreator -> PdfCreator ++: insertCoverPage(document, 5/11/2019) +PdfCreator --> PdfCreator -- + +PdfCreator -> PdfCreator ++: insertDriverTask(document, 5/11/2019) + +create PdfWrapperLayout +PdfCreator -> PdfWrapperLayout : new PdfWrapperLayout(document) +activate PdfWrapperLayout + +PdfWrapperLayout --> PdfCreator +deactivate PdfWrapperLayout + +PdfCreator -> PdfWrapperLayout : populateDocumentWithTasks(tasks, 5/11/2019); +activate PdfWrapperLayout + +PdfWrapperLayout --> PdfCreator +deactivate PdfWrapperLayout + +PdfCreator --> PdfCreator -- + +PdfCreator --> Model +deactivate PdfCreator + +[<-- Model +deactivate Model + +@enduml diff --git a/docs/diagrams/SavePdfCommand.puml b/docs/diagrams/SavePdfCommand.puml new file mode 100644 index 00000000000..397221a6758 --- /dev/null +++ b/docs/diagrams/SavePdfCommand.puml @@ -0,0 +1,73 @@ +@startuml +!include style.puml + +box Logic LOGIC_COLOR_T1 +participant ":LogicManager" as LogicManager LOGIC_COLOR +participant ":AddressBookParser" as AddressBookParser LOGIC_COLOR +participant ":SavePdfCommandParser" as SavePdfCommandParser LOGIC_COLOR +participant "p:SavePdfCommand" as SavePdfCommand LOGIC_COLOR +participant ":CommandResult" as CommandResult LOGIC_COLOR +end box + +box Model MODEL_COLOR_T1 +participant ":Model" as Model MODEL_COLOR +end box + +[-> LogicManager : execute(\n"savepdf 5/11/2019") +activate LogicManager + +LogicManager -> AddressBookParser : parseCommand(\n"savepdf 5/11/2019") +activate AddressBookParser + +create SavePdfCommandParser +AddressBookParser -> SavePdfCommandParser : SavePdfCommandParser() +activate SavePdfCommandParser + +SavePdfCommandParser --> AddressBookParser +deactivate SavePdfCommandParser + +AddressBookParser -> SavePdfCommandParser : parse("savepdf 5/11/2019") +activate SavePdfCommandParser + +create SavePdfCommand +SavePdfCommandParser -> SavePdfCommand : SavePdfCommand(5/11/2019) +activate SavePdfCommand + +SavePdfCommand --> SavePdfCommandParser : p +deactivate SavePdfCommand + +SavePdfCommandParser --> AddressBookParser : p +deactivate SavePdfCommandParser + +'Hidden arrow to position the destroy marker below the end of the activation bar. +SavePdfCommandParser -[hidden]-> AddressBookParser +destroy SavePdfCommandParser + +AddressBookParser --> LogicManager : p +deactivate AddressBookParser + +LogicManager -> SavePdfCommand : execute() +activate SavePdfCommand + + +note right : FILEPATH*: "./data/DeliveryTasks 2019-11-05.pdf" +SavePdfCommand -> Model : saveDriverTaskPdf(FILEPATH*, 5/11/2019) +activate Model + +Model --> SavePdfCommand +deactivate Model + +create CommandResult +SavePdfCommand -> CommandResult +activate CommandResult + +CommandResult --> SavePdfCommand +deactivate CommandResult + +SavePdfCommand --> LogicManager : result +deactivate SavePdfCommand + +[<--LogicManager +deactivate LogicManager + +@enduml diff --git a/docs/images/AssignActivityDiagram.png b/docs/images/AssignActivityDiagram.png index 4329c624972..628c5ecc383 100644 Binary files a/docs/images/AssignActivityDiagram.png and b/docs/images/AssignActivityDiagram.png differ diff --git a/docs/images/DeliveryTasks_Pdf_Layout.png b/docs/images/DeliveryTasks_Pdf_Layout.png new file mode 100644 index 00000000000..acfcc03ff26 Binary files /dev/null and b/docs/images/DeliveryTasks_Pdf_Layout.png differ diff --git a/docs/images/GeneratePdfActivityDiagram.png b/docs/images/GeneratePdfActivityDiagram.png new file mode 100644 index 00000000000..26b58329c12 Binary files /dev/null and b/docs/images/GeneratePdfActivityDiagram.png differ diff --git a/docs/images/GeneratePdfSequenceDiagram.png b/docs/images/GeneratePdfSequenceDiagram.png new file mode 100644 index 00000000000..4e35ee0b47d Binary files /dev/null and b/docs/images/GeneratePdfSequenceDiagram.png differ diff --git a/docs/images/SavePdfCommand.png b/docs/images/SavePdfCommand.png new file mode 100644 index 00000000000..28de4b66846 Binary files /dev/null and b/docs/images/SavePdfCommand.png differ diff --git a/src/main/java/seedu/address/MainApp.java b/src/main/java/seedu/address/MainApp.java index 91f32f430d1..bcd6f7fbf88 100644 --- a/src/main/java/seedu/address/MainApp.java +++ b/src/main/java/seedu/address/MainApp.java @@ -89,15 +89,15 @@ private Model initModelManager(Storage storage, ReadOnlyUserPrefs userPrefs) { try { centralManagerOptional = storage.readManager(); if (!centralManagerOptional.isPresent()) { - logger.info("Data file not found. Will be starting with a sample AddressBook"); + logger.info("Data file not found. Will be starting with a sample Central Manager"); } initialManagerData = centralManagerOptional.orElseGet(SampleDataUtil::getSampleCentralManager); } catch (DataConversionException e) { - logger.warning("Data file not in the correct format. Will be starting with an empty AddressBook"); + logger.warning("Data file not in the correct format. Will be starting with an empty Central Manager"); initialManagerData = new CentralManager(); } catch (IOException e) { - logger.warning("Problem while reading from the file. Will be starting with an empty AddressBook"); + logger.warning("Problem while reading from the file. Will be starting with an empty Central Manager"); initialManagerData = new CentralManager(); } diff --git a/src/main/java/seedu/address/commons/core/Messages.java b/src/main/java/seedu/address/commons/core/Messages.java index 4d1957e655b..1c67599d3cc 100644 --- a/src/main/java/seedu/address/commons/core/Messages.java +++ b/src/main/java/seedu/address/commons/core/Messages.java @@ -1,5 +1,8 @@ package seedu.address.commons.core; +import seedu.address.logic.GlobalClock; +import seedu.address.model.task.Task; + /** * Container for user visible messages. */ @@ -13,4 +16,14 @@ public class Messages { public static final String MESSAGE_PERSONS_LISTED_OVERVIEW = "%1$d persons listed!"; public static final String MESSAGE_CUSTOMERS_LISTED_OVERVIEW = "%1$d customers listed!"; public static final String MESSAGE_DRIVERS_LISTED_OVERVIEW = "%1$d drivers listed!"; + + public static final String MESSAGE_DATA_START_NEW = "Starting with a empty manager. \n" + + "If you had data previously, this means that your data file is corrupted"; + + public static final String MESSAGE_ASSIGN_SUCCESS = "Assigned #%1$d to %2$s at %3$s"; + public static final String MESSAGE_ALREADY_ASSIGNED = "This task is already scheduled. "; + public static final String MESSAGE_ALREADY_COMPLETED = "This task is completed. "; + public static final String MESSAGE_NOT_TODAY = "The task is not scheduled for today. " + "\n" + + String.format("Only tasks scheduled for today can be assigned. Today is %s.", + GlobalClock.dateToday().format(Task.DATE_FORMAT_FOR_PRINT)); } diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java index 34dc28f6c73..8115e956483 100644 --- a/src/main/java/seedu/address/logic/Logic.java +++ b/src/main/java/seedu/address/logic/Logic.java @@ -51,6 +51,8 @@ public interface Logic { /** Returns an unmodifiable view of the filtered list of customers */ ObservableList getFilteredCustomerList(); + boolean isStartAfresh(); + /** * Returns a list of incomplete tasks from previous days */ diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java index d49e1474565..c1addee9847 100644 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ b/src/main/java/seedu/address/logic/LogicManager.java @@ -101,6 +101,10 @@ public void refreshFilteredTaskList() { model.refreshFilteredTaskList(); } + public boolean isStartAfresh() { + return model.isStartAfresh(); + } + @Override public Path getAddressBookFilePath() { return model.getAddressBookFilePath(); diff --git a/src/main/java/seedu/address/logic/commands/AssignCommand.java b/src/main/java/seedu/address/logic/commands/AssignCommand.java index a630e10ad06..cccfe156c91 100644 --- a/src/main/java/seedu/address/logic/commands/AssignCommand.java +++ b/src/main/java/seedu/address/logic/commands/AssignCommand.java @@ -1,6 +1,10 @@ package seedu.address.logic.commands; import static java.util.Objects.requireNonNull; +import static seedu.address.commons.core.Messages.MESSAGE_ALREADY_ASSIGNED; +import static seedu.address.commons.core.Messages.MESSAGE_ALREADY_COMPLETED; +import static seedu.address.commons.core.Messages.MESSAGE_ASSIGN_SUCCESS; +import static seedu.address.commons.core.Messages.MESSAGE_NOT_TODAY; import static seedu.address.logic.parser.CliSyntax.PREFIX_DRIVER; import static seedu.address.logic.parser.CliSyntax.PREFIX_EVENT_TIME; import static seedu.address.logic.parser.CliSyntax.PREFIX_TASK; @@ -15,21 +19,17 @@ import seedu.address.model.Model; import seedu.address.model.person.Driver; import seedu.address.model.person.Schedule; +import seedu.address.model.person.SchedulingSuggestion; import seedu.address.model.person.exceptions.SchedulingException; import seedu.address.model.task.Task; import seedu.address.model.task.TaskStatus; /** - * Edits the details of an existing person in the address book. + * Assigns a task with a Driver and a valid EventTime. */ public class AssignCommand extends Command { public static final String COMMAND_WORD = "assign"; - public static final String MESSAGE_ASSIGN_SUCCESS = "Assigned #%1$d to %2$s at %3$s"; - public static final String MESSAGE_ALREADY_ASSIGNED = "This task is already scheduled. "; - public static final String MESSAGE_NOT_TODAY = "The task is not scheduled for today. " + "\n" - + String.format("Only tasks scheduled for today can be assigned. Today is %s.", - GlobalClock.dateToday().format(Task.DATE_FORMAT_FOR_PRINT)); - public static final String MESSAGE_PROMPT_FORCE = "Use 'assign force' to override the suggestion."; + public static final String MESSAGE_PROMPT_FORCE = "Use 'assign force' to overwrite the current assignment."; public static final String MESSAGE_USAGE = COMMAND_WORD + ": Assign a driver the specified task, with a proposed " + "start and end time. " + "\n" @@ -62,10 +62,49 @@ public AssignCommand(int driverId, int taskId, EventTime eventTime, boolean isFo this.isForceAssign = isForceAssign; } + /** + * Assign the task at the given time to the specified driver, without checking the driver's schedule. + * The operation is atomic. + * + * @param driver driver + * @param task task + * @param eventTime the time which the task is happening + * @throws SchedulingException when the proposed time conflicts with the driver's schedule + */ + public static void forceAssign(Driver driver, Task task, EventTime eventTime) throws CommandException { + try { + driver.assign(eventTime); + } catch (SchedulingException e) { + throw new CommandException(e.getMessage()); + } + + task.setDriverAndEventTime(Optional.of(driver), Optional.of(eventTime)); + } + + /** + * Builds a String when a command is successful. + * + * @param suggestion the suggestion given by Schedule + * @param task the assigned task + * @param driver the driver assigned + * @param eventTime the time to happen + * @return the string that used to return CommandResult + */ + public static String buildSuccessfulResponse(SchedulingSuggestion suggestion, Task task, Driver driver, + EventTime eventTime) { + String additionalSuggestion = suggestion.isEmpty() ? "" : "\n" + suggestion; + String succeedResponse = String.format(MESSAGE_ASSIGN_SUCCESS, + task.getId(), + driver.getName().fullName, + eventTime.toString()) + additionalSuggestion; + return succeedResponse; + } + @Override public CommandResult execute(Model model) throws CommandException { requireNonNull(model); + // check existence of task and driver if (!model.hasTask(taskId)) { throw new CommandException(Task.MESSAGE_INVALID_ID); } @@ -73,20 +112,30 @@ public CommandResult execute(Model model) throws CommandException { throw new CommandException(Messages.MESSAGE_INVALID_PERSON_DISPLAYED_INDEX); } + Driver driver = model.getDriver(driverId); Task task = model.getTask(taskId); + Optional existingEventTime = task.getEventTime(); + // check whether the task is scheduled for today if (!task.getDate().equals(GlobalClock.dateToday())) { throw new CommandException(MESSAGE_NOT_TODAY); } - - if (task.getStatus() != TaskStatus.INCOMPLETE || task.getDriver().isPresent() - || task.getEventTime().isPresent()) { - throw new CommandException(MESSAGE_ALREADY_ASSIGNED); + // check if the task is already assigned + if (task.getStatus().equals(TaskStatus.COMPLETED)) { + throw new CommandException(MESSAGE_ALREADY_COMPLETED); } + boolean isAlreadyAssigned = task.getStatus() != TaskStatus.INCOMPLETE || task.getDriver().isPresent() + || task.getEventTime().isPresent(); - Driver driver = model.getDriver(driverId); + if (isAlreadyAssigned) { + if (isForceAssign) { + FreeCommand.freeDriverFromTask(task.getDriver().get(), task); + } else { + throw new CommandException(MESSAGE_ALREADY_ASSIGNED + MESSAGE_PROMPT_FORCE); + } + } // check current time against system time if (eventTime.getStart().compareTo(GlobalClock.timeNow()) < 0) { @@ -95,36 +144,19 @@ public CommandResult execute(Model model) throws CommandException { } - String suggestion = driver.suggestTime(eventTime, GlobalClock.timeNow()); - if (!suggestion.isEmpty() && !isForceAssign) { - throw new CommandException(suggestion); + SchedulingSuggestion suggestion = driver.suggestTime(eventTime, GlobalClock.timeNow()); + if (suggestion.isFatal()) { + if (isAlreadyAssigned && isForceAssign) { + // restore task + forceAssign(driver, task, existingEventTime.get()); + } + throw new CommandException(suggestion.toString()); } forceAssign(driver, task, eventTime); - model.refreshAllFilteredList(); - return new CommandResult(String.format(MESSAGE_ASSIGN_SUCCESS, - task.getId(), driver.getName().fullName, eventTime.toString())); - } - - /** - * Assign the task at the given time to the specified driver, without checking the driver's schedule. - * The operation is atomic. - * - * @param driver driver - * @param task task - * @param eventTime the time which the task is happening - * @throws SchedulingException when the proposed time conflicts with the driver's schedule - */ - private void forceAssign(Driver driver, Task task, EventTime eventTime) throws CommandException { - try { - driver.assign(eventTime); - } catch (SchedulingException e) { - throw new CommandException(e.getMessage()); - } - - task.setDriverAndEventTime(Optional.of(driver), Optional.of(eventTime)); + return new CommandResult(buildSuccessfulResponse(suggestion, task, driver, eventTime)); } @Override diff --git a/src/main/java/seedu/address/logic/commands/FreeCommand.java b/src/main/java/seedu/address/logic/commands/FreeCommand.java index 9b2f9fef404..8170684a662 100644 --- a/src/main/java/seedu/address/logic/commands/FreeCommand.java +++ b/src/main/java/seedu/address/logic/commands/FreeCommand.java @@ -12,7 +12,7 @@ import seedu.address.model.task.Task; /** - * Edits the details of an existing person in the address book. + * Removes a driver from a task. */ public class FreeCommand extends Command { public static final String COMMAND_WORD = "free"; @@ -38,7 +38,7 @@ public FreeCommand(int taskId) { } /** - * Remove a driver from a task, and set the driver free during the corresponding time in the task. + * Removes a driver from a task, and set the driver free during the corresponding time in the task. * The method will fail if the Task contains no EventTime, or the Driver's Schedule doesn't contain * the EventTime. * diff --git a/src/main/java/seedu/address/logic/commands/ListCommand.java b/src/main/java/seedu/address/logic/commands/ListCommand.java index 8e1fdf5ab5c..5c9dd84d54a 100644 --- a/src/main/java/seedu/address/logic/commands/ListCommand.java +++ b/src/main/java/seedu/address/logic/commands/ListCommand.java @@ -11,7 +11,8 @@ public class ListCommand extends Command { public static final String COMMAND_WORD = "list"; - public static final String MESSAGE_SUCCESS = "Resets all the Task's / Driver's / Customer's list to original view."; + public static final String MESSAGE_SUCCESS = "Returned all the Task's / Driver's / Customer's list " + + "to its original view."; @Override diff --git a/src/main/java/seedu/address/logic/commands/SavePdfCommand.java b/src/main/java/seedu/address/logic/commands/SavePdfCommand.java index 32070c3ace3..2aa75dff8e2 100644 --- a/src/main/java/seedu/address/logic/commands/SavePdfCommand.java +++ b/src/main/java/seedu/address/logic/commands/SavePdfCommand.java @@ -48,8 +48,10 @@ public CommandResult execute(Model model) throws CommandException { LocalDate dateOfDelivery = date.get(); + String filePathWithDate = String.format(FILE_PATH_FOR_PDF, dateOfDelivery); + try { - model.saveDriverTaskPdf(FILE_PATH_FOR_PDF, dateOfDelivery); + model.saveDriverTaskPdf(filePathWithDate, dateOfDelivery); } catch (IOException | PdfNoTaskToDisplayException e) { throw new CommandException(e.getMessage()); } diff --git a/src/main/java/seedu/address/logic/commands/SuggestCommand.java b/src/main/java/seedu/address/logic/commands/SuggestCommand.java new file mode 100644 index 00000000000..08e243bf4fc --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/SuggestCommand.java @@ -0,0 +1,84 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; +import static seedu.address.logic.commands.AssignCommand.forceAssign; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TASK; + +import java.time.Duration; + +import seedu.address.commons.core.Messages; +import seedu.address.logic.GlobalClock; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.logic.optimizer.Candidate; +import seedu.address.logic.optimizer.ScheduleOptimizer; +import seedu.address.model.Model; +import seedu.address.model.task.Task; +import seedu.address.model.task.TaskStatus; + +/** + * Suggests a most optimum driver for an incoming task. + */ +public class SuggestCommand extends Command { + public static final String COMMAND_WORD = "suggest"; + public static final String MESSAGE_NO_DRIVER_AVAILABLE = "No driver is available for this duration. "; + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Suggest and assign the most optimum driver for the " + + "task" + + "\n" + + "Parameters: [NUMBER_OF_HOURS] " + + "[" + PREFIX_TASK + "TASK_ID] " + "\n" + + "Example: " + COMMAND_WORD + " " + + "1.5" + " " + + PREFIX_TASK + "3 "; + + private Duration duration; + private int taskId; + + /** + * @param taskId task's ID + * @param duration duration + */ + public SuggestCommand(int taskId, Duration duration) { + requireNonNull(duration); + + this.taskId = taskId; + this.duration = duration; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + // fails when task cannot be found + if (!model.hasTask(taskId)) { + throw new CommandException(Task.MESSAGE_INVALID_ID); + } + + + Task task = model.getTask(taskId); + // check whether the task is scheduled for today + if (!task.getDate().equals(GlobalClock.dateToday())) { + throw new CommandException(Messages.MESSAGE_NOT_TODAY); + } + + + // check whether the task is already scheduled + if (task.getStatus() != TaskStatus.INCOMPLETE || task.getDriver().isPresent() + || task.getEventTime().isPresent()) { + throw new CommandException(Messages.MESSAGE_ALREADY_ASSIGNED); + } + + + ScheduleOptimizer optimizer = new ScheduleOptimizer(model, task, duration); + Candidate result = optimizer.start() + .orElseThrow(() -> new CommandException(MESSAGE_NO_DRIVER_AVAILABLE)); + + + forceAssign(result.getKey(), task, result.getValue().get()); + + model.refreshAllFilteredList(); + + return new CommandResult(String.format(Messages.MESSAGE_ASSIGN_SUCCESS, + task.getId(), result.getKey().getName().fullName, result.getValue().get().toString())); + } + +} diff --git a/src/main/java/seedu/address/logic/optimizer/Candidate.java b/src/main/java/seedu/address/logic/optimizer/Candidate.java new file mode 100644 index 00000000000..879dce39e5b --- /dev/null +++ b/src/main/java/seedu/address/logic/optimizer/Candidate.java @@ -0,0 +1,46 @@ +package seedu.address.logic.optimizer; + +import java.util.Comparator; +import java.util.Optional; + +import javafx.util.Pair; +import seedu.address.model.EventTime; +import seedu.address.model.person.Driver; + +/** + * A convenient representation for a Driver-EventTime pair. + */ +public class Candidate extends Pair> { + + /** + * Creates a new pair. + * + * @param key The key for this pair + * @param value The value to use for this pair + */ + public Candidate(Driver key, Optional value) { + super(key, value); + } + + /** + * Gets a comparator between the EventTimes in the pair. + * + * @return the comparator + */ + public static Comparator comparator() { + return (o1, o2) -> { + // unpack + Optional t1 = o1.getValue(); + Optional t2 = o2.getValue(); + + if (t1.isPresent() && t2.isPresent()) { + return t1.get().compareTo(t2.get()); + } else if (t1.isEmpty()) { + return 1; + } else { + return -1; + } + }; + } +} + diff --git a/src/main/java/seedu/address/logic/optimizer/ScheduleOptimizer.java b/src/main/java/seedu/address/logic/optimizer/ScheduleOptimizer.java new file mode 100644 index 00000000000..efedd6733cb --- /dev/null +++ b/src/main/java/seedu/address/logic/optimizer/ScheduleOptimizer.java @@ -0,0 +1,93 @@ +package seedu.address.logic.optimizer; + +import java.time.Duration; +import java.util.List; +import java.util.Optional; + +import seedu.address.logic.GlobalClock; +import seedu.address.model.Model; +import seedu.address.model.person.Driver; +import seedu.address.model.task.Task; + +/** + * An optimizer to suggest the best driver and best time for a task, based on rules and their priorities. + * The optimizer is guaranteed not modifying the model, and is not designed to persist. + *

+ * It exposes a convenient {@code ScheduleOptimizer#start} method to apply the rules and return the result. + */ +public class ScheduleOptimizer { + private Model model; + private Task task; + private Duration duration; + + /** + * Constructs the optimizer object with the needed information. + * + * @param model the model which contains the state of the app + * @param task the task to assign + * @param duration the proposed duration of the task + */ + public ScheduleOptimizer(Model model, Task task, Duration duration) { + this.model = model; + this.task = task; + this.duration = duration; + } + + /** + * Finds the global optimum Driver-EventTime pair, based on the rules and priorities. + * + * @return an Optional of the pair if a suitable driver can be found; otherwise, an empty Optional + */ + public Optional start() { + return this + // preferred + .prioritizeSameCustomer() + // fall back + .or(this::driverEarliestFit) + // in case other methods didn't check for this + .filter(candidate -> candidate.getValue().isPresent()); + } + + /** + * Finds the driver who has an earliest time slot to fit the proposed task, and the time slot as well. + * + * @return an Optional of the pair if a suitable driver can be found; otherwise, an empty Optional + */ + private Optional driverEarliestFit() { + List driverList = model.getDriverManager().getDriverList(); + return driverList.stream() + // for every driver, find whether he is able to block this duration + .map(driver -> new Candidate(driver, driver.suggestTime(duration, GlobalClock.timeNow()))) + + // filter out the candidate who has no available time + .filter(candidate -> candidate.getValue().isPresent()) + + // find the earliest driver-time pair + .min(Candidate.comparator()); + } + + /** + * Finds the driver who is already delivering to the same customer as the incoming task, and checks whether + * he can take up the task. + * + * @return an Optional of the pair if a suitable driver can be found; otherwise, an empty Optional + */ + private Optional prioritizeSameCustomer() { + List taskList = model.getTaskManager().getList(); + + return taskList.stream() + // find all the task whose customer is the same as the requested task + .filter(t -> t.getCustomer().equals(task.getCustomer())) + + // get a distinct stream of drivers of the above tasks + .map(Task::getDriver) + .flatMap(Optional::stream) // get the list of drivers + .distinct() + + // same as driverEarliestFit + .map(driver -> new Candidate(driver, driver.suggestTime(duration, GlobalClock.timeNow()))) + .filter(candidate -> candidate.getValue().isPresent()) + .min(Candidate.comparator()); + } + +} diff --git a/src/main/java/seedu/address/logic/parser/AddressBookParser.java b/src/main/java/seedu/address/logic/parser/AddressBookParser.java index 82013ec2c39..c2437e211f5 100644 --- a/src/main/java/seedu/address/logic/parser/AddressBookParser.java +++ b/src/main/java/seedu/address/logic/parser/AddressBookParser.java @@ -25,6 +25,7 @@ import seedu.address.logic.commands.ListCommand; import seedu.address.logic.commands.ReadIdCommand; import seedu.address.logic.commands.SavePdfCommand; +import seedu.address.logic.commands.SuggestCommand; import seedu.address.logic.parser.exceptions.ParseException; /** @@ -75,6 +76,9 @@ public Command parseCommand(String userInput) throws ParseException { case AssignCommand.COMMAND_WORD: return new AssignCommandParser().parse(arguments); + case SuggestCommand.COMMAND_WORD: + return new SuggestCommandParser().parse(arguments); + case FreeCommand.COMMAND_WORD: return new FreeCommandParser().parse(arguments); diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java index 3d61b43ec84..695bcbb238d 100644 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java @@ -4,12 +4,14 @@ import static seedu.address.model.task.Task.DATE_FORMAT; import static seedu.address.model.task.Task.DATE_FORMATTER_FOR_USER_INPUT; +import java.time.Duration; import java.time.LocalDate; import java.time.format.DateTimeParseException; import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.regex.Pattern; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -31,12 +33,16 @@ public class ParserUtil { public static final String MESSAGE_INVALID_INDEX = "Index is not a non-zero unsigned integer."; + public static final int MINUTES_IN_AN_HOUR = 60; + public static final String HHMM_REGEX = "([0-9]{2}):[0-5][0-9]"; public static final String MESSAGE_INVALID_DATE_FORMAT = "Invalid Date format. Date format should be " + DATE_FORMAT + ". " + "Chosen date should be from today onwards."; public static final String MESSAGE_INVALID_ID = "ID should be a integer number and more than 0."; + public static final String MESSAGE_INVALID_DURATION = "The format of the number of hours is either a decimal " + + "number (e.g. 1.5) or in hh:MM (e.g. 1:30)"; /** * Parses {@code oneBasedIndex} into an {@code Index} and returns it. Leading and trailing whitespaces will be @@ -239,6 +245,41 @@ public static EventTime parseEventTime(String duration) throws ParseException { } + /** + * Parses a duration in hours, either 1.5 (a decimal), or 1:30 (in hh:MM) format. + * @param input the string to be parsed + * @return the parsed Duration + * @throws ParseException when the String doesn't fit the required format + */ + public static Duration parseDuration(String input) throws ParseException { + input = input.trim(); + try { + // either 1.5 or 1:30 + if (Pattern.matches(HHMM_REGEX, input)) { + String[] hourMinute = input.split(":"); + long hour = Long.parseLong(hourMinute[0]); + long minute = Long.parseLong(hourMinute[1]); + + if ((minute < 0 || hour < 0) || (minute == 0 && hour == 0)) { + throw new ParseException(MESSAGE_INVALID_DURATION); + } + + return Duration.ofMinutes(hour * MINUTES_IN_AN_HOUR + minute); + } else { + long minutes = Math.round(Double.parseDouble(input) * MINUTES_IN_AN_HOUR); + + if (minutes <= 0) { + throw new ParseException(MESSAGE_INVALID_DURATION); + } + + return Duration.ofMinutes(minutes); + } + } catch (NumberFormatException e) { + throw new ParseException(MESSAGE_INVALID_DURATION); + } + } + + public static int getNoOfPrefixesPresent(ArgumentMultimap argumentMultimap, Prefix... prefixes) { return (int) Stream.of(prefixes).filter(prefix -> argumentMultimap.getValue(prefix).isPresent()).count(); } diff --git a/src/main/java/seedu/address/logic/parser/SuggestCommandParser.java b/src/main/java/seedu/address/logic/parser/SuggestCommandParser.java new file mode 100644 index 00000000000..b3f2ab69112 --- /dev/null +++ b/src/main/java/seedu/address/logic/parser/SuggestCommandParser.java @@ -0,0 +1,53 @@ +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_TASK; +import static seedu.address.logic.parser.CliSyntax.getAllPrefixes; +import static seedu.address.logic.parser.ParserUtil.arePrefixesPresent; +import static seedu.address.logic.parser.ParserUtil.parseDuration; + +import java.time.Duration; + +import seedu.address.logic.commands.SuggestCommand; +import seedu.address.logic.parser.exceptions.ParseException; + +/** + * Parses input arguments and creates a new EditCommand object + */ +public class SuggestCommandParser implements Parser { + + + private static ParseException getWrongFormatException() { + return new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, SuggestCommand.MESSAGE_USAGE)); + } + + /** + * Parses the given {@code String} of arguments in the context of the EditCommand + * and returns an EditCommand object for execution. + * + * @return the parsed command + * @throws ParseException if the user input does not conform the expected format + */ + public SuggestCommand parse(String args) throws ParseException { + requireNonNull(args); + ArgumentMultimap argMultimap = + ArgumentTokenizer.tokenize(args, getAllPrefixes()); + + if (!arePrefixesPresent(argMultimap, PREFIX_TASK) || argMultimap.getPreamble().isEmpty()) { + throw getWrongFormatException(); + } + + String task = argMultimap.getValue(PREFIX_TASK).orElseThrow(SuggestCommandParser::getWrongFormatException); + int taskId = ParserUtil.parseId(task); + + String duration = argMultimap.getPreamble(); + + + Duration proposed = parseDuration(duration); + + + return new SuggestCommand(taskId, proposed); + } + +} diff --git a/src/main/java/seedu/address/model/DriverManager.java b/src/main/java/seedu/address/model/DriverManager.java index 0bf43ebbc15..3ff718f9c13 100644 --- a/src/main/java/seedu/address/model/DriverManager.java +++ b/src/main/java/seedu/address/model/DriverManager.java @@ -1,5 +1,7 @@ package seedu.address.model; +import java.util.Comparator; +import java.util.List; import java.util.Optional; import javafx.collections.ObservableList; @@ -60,6 +62,14 @@ public Driver getDriver(int driverId) { .orElseThrow(PersonNotFoundException::new); } + /** + * Sorts driver list accordingly to the comparator provided. + */ + public static List getSortedDriverList(List drivers, Comparator comparator) { + drivers.sort(comparator); + return drivers; + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index cee96bebb6a..27a2f8ce212 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -30,7 +30,6 @@ public interface Model { Predicate PREDICATE_SHOW_ALL_PERSONS = unused -> true; Predicate PREDICATE_SHOW_ALL_CUSTOMERS = unused -> true; Predicate PREDICATE_SHOW_ALL_DRIVERS = unused -> true; - Predicate PREDICATE_SHOW_ALL_TASKS = unused -> true; /** * {@code Predicate} that always evaluate to false @@ -260,5 +259,7 @@ public interface Model { IdManager getIdManager(); + boolean isStartAfresh(); + void saveDriverTaskPdf(String filePathForPdf, LocalDate date) throws IOException, PdfNoTaskToDisplayException; } diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index 344beed2644..8089402a1d8 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/seedu/address/model/ModelManager.java @@ -6,6 +6,8 @@ import java.io.IOException; import java.nio.file.Path; import java.time.LocalDate; +import java.util.Comparator; +import java.util.List; import java.util.function.Predicate; import java.util.logging.Logger; @@ -25,12 +27,16 @@ import seedu.address.model.task.Task; import seedu.address.model.task.TaskList; import seedu.address.model.task.TaskManager; +import seedu.address.model.task.TaskStatus; import seedu.address.storage.CentralManager; /** * Represents the in-memory model of the address book data. */ public class ModelManager implements Model { + + public static final String MESSAGE_NO_ASSIGNED_TASK_FOR_THE_DATE = "There's no assigned tasks for %1$s."; + private static final Logger logger = LogsCenter.getLogger(ModelManager.class); private final AddressBook addressBook; @@ -328,6 +334,10 @@ public IdManager getIdManager() { return idManager; } + public boolean isStartAfresh() { + return idManager.isStartAfresh(); + } + // ========= PdfCreator ========================================================================= /** @@ -336,12 +346,55 @@ public IdManager getIdManager() { * @param filePath directory to save the PDF file. * @param dateOfDelivery date of delivery. * @throws IOException if directory is not found. + * @throws PdfNoTaskToDisplayException if there is no assigned task on the day. */ public void saveDriverTaskPdf(String filePath, LocalDate dateOfDelivery) throws IOException, PdfNoTaskToDisplayException { requireAllNonNull(filePath, dateOfDelivery); + + List assignedTaskOnDateList = getOnlyAssignedTaskOnDate(taskManager.getList(), dateOfDelivery); + List sortedByEventTimeTasks = getSortedByEventTimeTasks(assignedTaskOnDateList); + + if (assignedTaskOnDateList.size() == 0) { + throw new PdfNoTaskToDisplayException(String.format(MESSAGE_NO_ASSIGNED_TASK_FOR_THE_DATE, dateOfDelivery)); + } + + List drivers = getDriversFromTasks(assignedTaskOnDateList); + List sortedByNameDrivers = getSortedByNameDrivers(drivers); + PdfCreator pdfCreator = new PdfCreator(filePath); - pdfCreator.saveDriverTaskPdf(taskManager.getList(), dateOfDelivery); + pdfCreator.saveDriverTaskPdf(sortedByEventTimeTasks, sortedByNameDrivers, dateOfDelivery); + } + + public List getOnlyAssignedTaskOnDate(List tasks, LocalDate dateOfDelivery) { + Predicate assignedTaskOnDatePredicate = task -> task.getDate().equals(dateOfDelivery) + && !task.getStatus().equals(TaskStatus.INCOMPLETE); + List assignedTaskOnDateList = TaskManager.getFilteredList(tasks, assignedTaskOnDatePredicate); + + return assignedTaskOnDateList; + } + + public List getSortedByEventTimeTasks(List tasks) { + Comparator ascendingEventTimeComparator = Comparator.comparing(t -> { + //uses filtered assigned tasks, so eventTime must be present + assert t.getEventTime().isPresent(); + return t.getEventTime().get(); + }); + + List sortedList = TaskManager.getSortedList(tasks, ascendingEventTimeComparator); + + return sortedList; + } + + public List getDriversFromTasks(List tasks) { + return TaskManager.getDriversFromTasks(tasks); + } + + public List getSortedByNameDrivers(List drivers) { + Comparator sortByNameComparator = Comparator.comparing(driver -> driver.getName().toString()); + List sortedByNameDrivers = DriverManager.getSortedDriverList(drivers, sortByNameComparator); + + return sortedByNameDrivers; } // =========== Filtered Person List Accessors ============================================================= diff --git a/src/main/java/seedu/address/model/id/IdManager.java b/src/main/java/seedu/address/model/id/IdManager.java index fd271098e0d..b77959c0656 100644 --- a/src/main/java/seedu/address/model/id/IdManager.java +++ b/src/main/java/seedu/address/model/id/IdManager.java @@ -72,6 +72,10 @@ public void lastDriverIdPlusOne() { lastDriverId++; } + public boolean isStartAfresh() { + return getLastTaskId() == 0 && getLastCustomerId() == 0 && getLastDriverId() == 0; + } + /** * Resets all the last id counters for all managers to zero. */ diff --git a/src/main/java/seedu/address/model/pdfmanager/PdfCoverPageLayout.java b/src/main/java/seedu/address/model/pdfmanager/PdfCoverPageLayout.java index 257012c9cb1..69d6fd610bd 100644 --- a/src/main/java/seedu/address/model/pdfmanager/PdfCoverPageLayout.java +++ b/src/main/java/seedu/address/model/pdfmanager/PdfCoverPageLayout.java @@ -9,7 +9,7 @@ /** * Represents the cover page in the PDF document. */ -public class PdfCoverPageLayout extends PdfLayout { +public class PdfCoverPageLayout { private Document document; @@ -26,8 +26,8 @@ public PdfCoverPageLayout(Document document) { public void addCoverPage(String title, String subTitle) { Paragraph titleParagraph = createTitle(title); Paragraph subTitleParagraph = createSubTitle(subTitle); - document.add(alignParagraphMiddle(titleParagraph)); - document.add(alignParagraphMiddle(subTitleParagraph)); + document.add(PdfLayout.alignParagraphMiddle(titleParagraph)); + document.add(PdfLayout.alignParagraphMiddle(subTitleParagraph)); Table sampleTable = createSampleTable(); document.add(sampleTable); diff --git a/src/main/java/seedu/address/model/pdfmanager/PdfCreator.java b/src/main/java/seedu/address/model/pdfmanager/PdfCreator.java index 5ab968a185c..ed1e715336c 100644 --- a/src/main/java/seedu/address/model/pdfmanager/PdfCreator.java +++ b/src/main/java/seedu/address/model/pdfmanager/PdfCreator.java @@ -4,59 +4,45 @@ import java.nio.file.Paths; import java.time.LocalDate; import java.util.List; -import java.util.stream.Collectors; import com.itextpdf.kernel.pdf.PdfDocument; import com.itextpdf.kernel.pdf.PdfWriter; import com.itextpdf.layout.Document; import seedu.address.commons.util.FileUtil; -import seedu.address.model.pdfmanager.exceptions.PdfNoTaskToDisplayException; +import seedu.address.model.person.Driver; import seedu.address.model.task.Task; -import seedu.address.model.task.TaskStatus; /** * Creates and saves details provided into a PDF file. */ public class PdfCreator { - public static final String MESSAGE_NO_ASSIGNED_TASK_FOR_THE_DATE = "There's no assigned tasks for %1$s."; - public final String filePath; public PdfCreator(String filePath) { this.filePath = filePath; } - public String getFilePathWithDate(LocalDate date) { - return String.format(filePath, "(" + date + ")"); - } - /** * Saves drivers` tasks for a specific date into a PDF file. * * @param tasks tasks list. * @param dateOfDelivery date of delivery. * @throws IOException if directory used for saving is not found. - * @throws PdfNoTaskToDisplayException if there is no assigned task on the day. */ - public void saveDriverTaskPdf(List tasks, LocalDate dateOfDelivery) - throws IOException, PdfNoTaskToDisplayException { - if (!hasAssignedTasks(tasks, dateOfDelivery)) { - throw new PdfNoTaskToDisplayException(String.format(MESSAGE_NO_ASSIGNED_TASK_FOR_THE_DATE, dateOfDelivery)); - } - - Document document = createDocument(dateOfDelivery); + public void saveDriverTaskPdf(List tasks, List drivers, LocalDate dateOfDelivery) + throws IOException { + Document document = createDocument(); insertCoverPage(document, dateOfDelivery); - insertDriverTask(document, tasks, dateOfDelivery); + insertDriverTask(document, tasks, drivers, dateOfDelivery); //close to save document.close(); } - private void createFileIfMissing(LocalDate dateOfDelivery) throws IOException { - String filePathWithDate = getFilePathWithDate(dateOfDelivery); - FileUtil.createIfMissing(Paths.get(filePathWithDate)); + private void createFileIfMissing() throws IOException { + FileUtil.createIfMissing(Paths.get(filePath)); } /** @@ -65,11 +51,10 @@ private void createFileIfMissing(LocalDate dateOfDelivery) throws IOException { * @return PDF document ready to be filled with content. * @throws IOException if file path is not created or found. */ - private Document createDocument(LocalDate dateOfDelivery) throws IOException { - createFileIfMissing(dateOfDelivery); + private Document createDocument() throws IOException { + createFileIfMissing(); - String filePathWithDate = getFilePathWithDate(dateOfDelivery); - PdfDocument pdf = new PdfDocument(new PdfWriter(filePathWithDate)); + PdfDocument pdf = new PdfDocument(new PdfWriter(filePath)); Document newDocument = new Document(pdf); newDocument.setMargins(30, 30, 30, 30); @@ -91,26 +76,8 @@ private void insertCoverPage(Document document, LocalDate dateOfDelivery) { coverPageLayout.addCoverPage(title, subTitle); } - private void insertDriverTask(Document document, List tasks, LocalDate dateOfDelivery) - throws PdfNoTaskToDisplayException { + private void insertDriverTask(Document document, List tasks, List drivers, LocalDate dateOfDelivery) { PdfWrapperLayout wrapperLayout = new PdfWrapperLayout(document); - wrapperLayout.populateDocumentWithTasks(tasks, dateOfDelivery); - } - - /** - * Checks if the task list contains assigned tasks for the specified date. - * - * @param tasks task list. - * @param date date of delivery. - * @return true if there are assigned tasks for the specified date. - */ - private boolean hasAssignedTasks(List tasks, LocalDate date) { - List filteredTasks = tasks - .stream() - .filter(task -> task.getDate().equals(date) - && !task.getStatus().equals(TaskStatus.INCOMPLETE)) - .collect(Collectors.toList()); - - return (filteredTasks.size() != 0); + wrapperLayout.populateDocumentWithTasks(tasks, drivers, dateOfDelivery); } } diff --git a/src/main/java/seedu/address/model/pdfmanager/PdfCustomerLayout.java b/src/main/java/seedu/address/model/pdfmanager/PdfCustomerLayout.java index d3294c24283..76b4fc9785c 100644 --- a/src/main/java/seedu/address/model/pdfmanager/PdfCustomerLayout.java +++ b/src/main/java/seedu/address/model/pdfmanager/PdfCustomerLayout.java @@ -11,7 +11,7 @@ /** * Represents a customer's information in a table format in the PDF document. */ -public class PdfCustomerLayout extends PdfLayout { +public class PdfCustomerLayout { private Customer customer; @@ -42,22 +42,22 @@ public Table createTable() { private Cell getCustomerIdCell(int customerId) { String idStr = "Customer ID \n" + customerId; - return createCell(1, 2, idStr); + return PdfLayout.createCell(1, 2, idStr); } private Cell getNameCell(Name name) { String nameStr = "Customer\n" + name; - return createCell(1, 6, nameStr); + return PdfLayout.createCell(1, 6, nameStr); } private Cell getPhoneNumberCell(Phone phone) { String phoneNumberStr = "Contact No \n" + phone; - return createCell(1, 3, phoneNumberStr); + return PdfLayout.createCell(1, 3, phoneNumberStr); } private Cell getAddressCell(Address address) { String addressStr = "Address: " + address; - return createCell(1, 10, addressStr); + return PdfLayout.createCell(1, 10, addressStr); } private Table designTable(Table customerTable) { diff --git a/src/main/java/seedu/address/model/pdfmanager/PdfDriverLayout.java b/src/main/java/seedu/address/model/pdfmanager/PdfDriverLayout.java index 387b732c6f8..f69bc08ce09 100644 --- a/src/main/java/seedu/address/model/pdfmanager/PdfDriverLayout.java +++ b/src/main/java/seedu/address/model/pdfmanager/PdfDriverLayout.java @@ -12,7 +12,7 @@ /** * Represents a driver's information in a table format in the PDF document. */ -public class PdfDriverLayout extends PdfLayout { +public class PdfDriverLayout { private Driver driver; private Document document; @@ -42,21 +42,21 @@ public void createDriverDetails() { private Paragraph getDriverIdCell(int id) { String idStr = "Driver ID: " + id; - return createParagraph(idStr); + return PdfLayout.createParagraph(idStr); } private Paragraph getNameCell(Name name) { String nameStr = "Driver: " + name; - return createParagraph(nameStr); + return PdfLayout.createParagraph(nameStr); } private Paragraph getPhoneNumberCell(Phone phone) { String phoneStr = "Contact No: " + phone; - return createParagraph(phoneStr); + return PdfLayout.createParagraph(phoneStr); } private Paragraph getDateOfDelivery(LocalDate date) { String dateOfDelivery = "Date of Delivery: " + date; - return createParagraph(dateOfDelivery); + return PdfLayout.createParagraph(dateOfDelivery); } } diff --git a/src/main/java/seedu/address/model/pdfmanager/PdfLayout.java b/src/main/java/seedu/address/model/pdfmanager/PdfLayout.java index 8e775c5e733..8d087bd5216 100644 --- a/src/main/java/seedu/address/model/pdfmanager/PdfLayout.java +++ b/src/main/java/seedu/address/model/pdfmanager/PdfLayout.java @@ -7,14 +7,10 @@ import com.itextpdf.layout.property.VerticalAlignment; /** - * Represents a general layout which consists of functions to aid creation and formatting of the content in a layout. + * Consists of functions to aid creation and formatting of the content in a layout. */ public class PdfLayout { - public PdfLayout() { - - } - /** * Creates a {@code Cell} object with the message and the measurement specified. * @@ -23,7 +19,7 @@ public PdfLayout() { * @param message message to be displayed in the cell. * @return populated cell with the message. */ - public Cell createCell(int rowSpan, int colSpan, String message) { + public static Cell createCell(int rowSpan, int colSpan, String message) { Cell newCell = new Cell(rowSpan, colSpan).add(new Paragraph(message)); Cell designedCell = alignCellMiddle(newCell); return designedCell; @@ -34,7 +30,7 @@ public Cell createCell(int rowSpan, int colSpan, String message) { * * @param str words to be insert. */ - public Paragraph createParagraph(String str) { + public static Paragraph createParagraph(String str) { Paragraph paragraph = new Paragraph(str); return paragraph; } @@ -44,7 +40,7 @@ public Paragraph createParagraph(String str) { * * @param paragraph a block of words. */ - public Paragraph alignParagraphMiddle(Paragraph paragraph) { + public static Paragraph alignParagraphMiddle(Paragraph paragraph) { paragraph.setTextAlignment(TextAlignment.CENTER); paragraph.setVerticalAlignment(VerticalAlignment.MIDDLE); paragraph.setHorizontalAlignment(HorizontalAlignment.CENTER); @@ -57,7 +53,7 @@ public Paragraph alignParagraphMiddle(Paragraph paragraph) { * * @param cell Cell of a table in the PDF document. */ - public Cell alignCellMiddle(Cell cell) { + public static Cell alignCellMiddle(Cell cell) { cell.setTextAlignment(TextAlignment.CENTER); cell.setHorizontalAlignment(HorizontalAlignment.CENTER); cell.setVerticalAlignment(VerticalAlignment.MIDDLE); @@ -71,7 +67,7 @@ public Cell alignCellMiddle(Cell cell) { * * @param cell Cell of a table in the PDF document. */ - public Cell boldCell(Cell cell) { + public static Cell boldCell(Cell cell) { cell.setBold(); return cell; } @@ -81,7 +77,7 @@ public Cell boldCell(Cell cell) { * * @param cell Cell of a table in the PDF document. */ - public Cell insertPadding(Cell cell) { + public static Cell insertPadding(Cell cell) { cell.setPadding(20); return cell; } diff --git a/src/main/java/seedu/address/model/pdfmanager/PdfPageHeaderLayout.java b/src/main/java/seedu/address/model/pdfmanager/PdfPageHeaderLayout.java index cc9ffdf7a52..05c061de1a0 100644 --- a/src/main/java/seedu/address/model/pdfmanager/PdfPageHeaderLayout.java +++ b/src/main/java/seedu/address/model/pdfmanager/PdfPageHeaderLayout.java @@ -7,7 +7,7 @@ /** * Represents the page header details in the PDF document. */ -public class PdfPageHeaderLayout extends PdfLayout { +public class PdfPageHeaderLayout { private Document document; @@ -24,7 +24,7 @@ public void createPageHeader() { } private Paragraph getPageHeader() { - return createParagraph("Deliveria"); + return PdfLayout.createParagraph("Deliveria"); } /** diff --git a/src/main/java/seedu/address/model/pdfmanager/PdfSampleLayout.java b/src/main/java/seedu/address/model/pdfmanager/PdfSampleLayout.java index c32430f37a7..e208413cac1 100644 --- a/src/main/java/seedu/address/model/pdfmanager/PdfSampleLayout.java +++ b/src/main/java/seedu/address/model/pdfmanager/PdfSampleLayout.java @@ -6,7 +6,7 @@ /** * Represents a sample layout to show how the information in the PDF document is represented. */ -public class PdfSampleLayout extends PdfLayout { +public class PdfSampleLayout { public PdfSampleLayout() { @@ -18,17 +18,17 @@ public PdfSampleLayout() { public Table createTable() { Table sampleTable = new Table(10).useAllAvailableWidth().setFixedLayout(); - Cell titleCell = createCell(1, 10, "SAMPLE DRIVER'S TASK."); + Cell titleCell = PdfLayout.createCell(1, 10, "SAMPLE DRIVER'S TASK."); titleCell.setBold(); - Cell eventTimeCell = createCell(2, 2, "Duration of Delivery"); - Cell taskDetailCell = createCell(1, 8, "Task's Information\n"); - Cell customerDetailCell = createCell(1, 8, "Customer's Information\n"); + Cell eventTimeCell = PdfLayout.createCell(2, 2, "Duration of Delivery"); + Cell taskDetailCell = PdfLayout.createCell(1, 8, "Task's Information\n"); + Cell customerDetailCell = PdfLayout.createCell(1, 8, "Customer's Information\n"); sampleTable.addCell(titleCell); - sampleTable.addCell(insertPadding(eventTimeCell)); - sampleTable.addCell(insertPadding(taskDetailCell)); - sampleTable.addCell(insertPadding(customerDetailCell)); + sampleTable.addCell(PdfLayout.insertPadding(eventTimeCell)); + sampleTable.addCell(PdfLayout.insertPadding(taskDetailCell)); + sampleTable.addCell(PdfLayout.insertPadding(customerDetailCell)); return sampleTable; } diff --git a/src/main/java/seedu/address/model/pdfmanager/PdfTableHeaderLayout.java b/src/main/java/seedu/address/model/pdfmanager/PdfTableHeaderLayout.java index 6edf5c9b1ea..9d980b3f077 100644 --- a/src/main/java/seedu/address/model/pdfmanager/PdfTableHeaderLayout.java +++ b/src/main/java/seedu/address/model/pdfmanager/PdfTableHeaderLayout.java @@ -7,7 +7,7 @@ /** * Represents a table header's details in a table format in the PDF document. */ -public class PdfTableHeaderLayout extends PdfLayout { +public class PdfTableHeaderLayout { public PdfTableHeaderLayout() { } @@ -21,18 +21,18 @@ public Table createTable() { Cell eventTimeTitle = getEventTimeTitle(); Cell taskTitle = getTaskTitle(); - headerTable.addCell(boldCell(eventTimeTitle)); - headerTable.addCell(boldCell(taskTitle)); + headerTable.addCell(PdfLayout.boldCell(eventTimeTitle)); + headerTable.addCell(PdfLayout.boldCell(taskTitle)); return designTable(headerTable); } private Cell getEventTimeTitle() { - return createCell(1, 2, "Duration"); + return PdfLayout.createCell(1, 2, "Duration"); } private Cell getTaskTitle() { - return createCell(1, 8, "Task's Details"); + return PdfLayout.createCell(1, 8, "Task's Details"); } private Table designTable(Table headerTable) { diff --git a/src/main/java/seedu/address/model/pdfmanager/PdfTaskLayout.java b/src/main/java/seedu/address/model/pdfmanager/PdfTaskLayout.java index 8ed71c62c7d..1fccf85ee28 100644 --- a/src/main/java/seedu/address/model/pdfmanager/PdfTaskLayout.java +++ b/src/main/java/seedu/address/model/pdfmanager/PdfTaskLayout.java @@ -16,7 +16,7 @@ /** * Represents a task's information in a table format in the PDF document. */ -public class PdfTaskLayout extends PdfLayout { +public class PdfTaskLayout { private static boolean toggleColor = false; @@ -42,8 +42,8 @@ public Table createTable() { assert optionalEventTime.isPresent(); Cell eventTimeCell = getEventTimeCell(optionalEventTime.get()); - taskTable.addCell(alignCellMiddle(eventTimeCell)); - taskTable.addCell(alignCellMiddle(taskIdCell)); + taskTable.addCell(PdfLayout.alignCellMiddle(eventTimeCell)); + taskTable.addCell(PdfLayout.alignCellMiddle(taskIdCell)); taskTable.addCell(descriptionCell); taskTable.addCell(statusCell); @@ -62,17 +62,17 @@ public Table createTable() { public Cell getTaskIdCell(int taskId) { String idStr = "Task ID\n" + taskId; - return createCell(1, 1, idStr); + return PdfLayout.createCell(1, 1, idStr); } public Cell getDescriptionCell(Description description) { String descriptionStr = "Goods\n" + description; - return createCell(1, 5, descriptionStr); + return PdfLayout.createCell(1, 5, descriptionStr); } public Cell getStatusCell(TaskStatus status) { String statusStr = "Status\n" + status; - return createCell(1, 2, statusStr) + return PdfLayout.createCell(1, 2, statusStr) .setFontColor((status.equals(TaskStatus.ON_GOING) ? ColorConstants.RED : ColorConstants.GREEN)); @@ -80,7 +80,7 @@ public Cell getStatusCell(TaskStatus status) { public Cell getEventTimeCell(EventTime eventTime) { String eventTimeStr = eventTime.toString(); - return createCell(2, 2, eventTimeStr); + return PdfLayout.createCell(2, 2, eventTimeStr); } /** diff --git a/src/main/java/seedu/address/model/pdfmanager/PdfWrapperLayout.java b/src/main/java/seedu/address/model/pdfmanager/PdfWrapperLayout.java index 5368516da40..cf0dd0881c7 100644 --- a/src/main/java/seedu/address/model/pdfmanager/PdfWrapperLayout.java +++ b/src/main/java/seedu/address/model/pdfmanager/PdfWrapperLayout.java @@ -1,10 +1,7 @@ package seedu.address.model.pdfmanager; import java.time.LocalDate; -import java.util.ArrayList; -import java.util.Comparator; import java.util.List; -import java.util.stream.Collectors; import com.itextpdf.layout.Document; import com.itextpdf.layout.element.AreaBreak; @@ -13,12 +10,11 @@ import seedu.address.model.person.Driver; import seedu.address.model.task.Task; -import seedu.address.model.task.TaskStatus; /** * A outer layout that encapsulates all the layouts used. */ -public class PdfWrapperLayout extends PdfLayout { +public class PdfWrapperLayout { private Document document; @@ -32,15 +28,9 @@ public PdfWrapperLayout(Document document) { * @param tasks task list. * @param dateOfDelivery date of delivery. */ - public void populateDocumentWithTasks(List tasks, LocalDate dateOfDelivery) { - List tasksOnDate = filterTasksBasedOnDate(tasks, dateOfDelivery); - List sortedTasks = sortTaskByEventTime(tasksOnDate); - - List driversOnDate = extractDriversFromTaskList(sortedTasks); - List sortedDrivers = sortDriverByName(driversOnDate); - + public void populateDocumentWithTasks(List tasks, List drivers, LocalDate dateOfDelivery) { //initialise outerlayout - insertDriverTasksIntoDocument(sortedDrivers, sortedTasks, dateOfDelivery); + insertDriverTasksIntoDocument(drivers, tasks, dateOfDelivery); } /** @@ -96,62 +86,4 @@ private void addTaskUnderDriver(Driver driver, Task task) { } } - /** - * Sorts driver list accordingly to driver's name in ascending order. - */ - private List sortDriverByName(List drivers) { - Comparator driverComparator = Comparator.comparing(d -> d.getName().toString()); - drivers.sort(driverComparator); - - return drivers; - } - - /** - * Creates a driver list out of task list. - * The driver list contains the drivers working on the specific date. - * - * @param tasks task list for a specific date. - * @return driver list that contains only drivers that is working on the specific date. - */ - private List extractDriversFromTaskList(List tasks) { - List driverList = new ArrayList<>(); - for (Task task : tasks) { - Driver driver = task.getDriver().get(); - if (!driverList.contains(driver)) { - driverList.add(driver); - } - } - - return driverList; - } - - /** - * Filter task list to get tasks on a specific date. Only account for ONGOING and COMPLETE tasks. - * INCOMPLETE tasks are not needed as they are not assigned to any drivers. - * - * @param tasks task list. - * @param date date of delivery. - * @return filtered task list that contains only tasks on a specific date and are ONGOING or COMPLETE. - */ - private List filterTasksBasedOnDate(List tasks, LocalDate date) { - List filteredTasks = tasks - .stream() - .filter(task -> task.getDate().equals(date) - && !task.getStatus().equals(TaskStatus.INCOMPLETE)) - .collect(Collectors.toList()); - - return filteredTasks; - } - - /** - * Sort tasks by ascending time of delivery. - * All the tasks must be assigned tasks with eventTime. - * Uses {@code filterTasksBasedOnDate} to get only assigned tasks for a specific date. - */ - private List sortTaskByEventTime(List tasksToSort) { - //list has been filtered by assigned tasks only so eventTime must exist. - Comparator taskComparator = Comparator.comparing(t -> t.getEventTime().get()); - tasksToSort.sort(taskComparator); - return tasksToSort; - } } diff --git a/src/main/java/seedu/address/model/person/Driver.java b/src/main/java/seedu/address/model/person/Driver.java index 2dcef1901a0..832e54aa5cb 100644 --- a/src/main/java/seedu/address/model/person/Driver.java +++ b/src/main/java/seedu/address/model/person/Driver.java @@ -1,6 +1,8 @@ package seedu.address.model.person; +import java.time.Duration; import java.time.LocalTime; +import java.util.Optional; import java.util.Set; import seedu.address.model.EventTime; @@ -25,7 +27,7 @@ public class Driver extends Person { /** * Every field must be present and not null. */ - public Driver (int id, Name name, Phone phone, Email email, Address address, Set tags) { + public Driver(int id, Name name, Phone phone, Email email, Address address, Set tags) { super(name, phone, email, address, tags); schedule = new Schedule(); this.id = id; @@ -54,10 +56,14 @@ public boolean isScheduleAvailable(EventTime durationToAdd) { return schedule.isAvailable(durationToAdd); } - public String suggestTime(EventTime eventTime, LocalTime timeNow) { + public SchedulingSuggestion suggestTime(EventTime eventTime, LocalTime timeNow) { return this.schedule.getSchedulingSuggestion(eventTime, timeNow); } + public Optional suggestTime(Duration duration, LocalTime timeNow) { + return this.schedule.findFirstAvailableSlot(timeNow, duration); + } + public void assign(EventTime eventTime) throws SchedulingException { this.schedule.add(eventTime); } @@ -105,9 +111,9 @@ public boolean equals(Object other) { public String toString() { StringBuilder driverBuilder = new StringBuilder(); driverBuilder.append(" Driver stats: \n") - .append(" id: ") - .append(getId()) - .append(super.toString()); + .append(" id: ") + .append(getId()) + .append(super.toString()); return driverBuilder.toString(); } diff --git a/src/main/java/seedu/address/model/person/Schedule.java b/src/main/java/seedu/address/model/person/Schedule.java index 34bfeeca5b5..7bfbc80348b 100644 --- a/src/main/java/seedu/address/model/person/Schedule.java +++ b/src/main/java/seedu/address/model/person/Schedule.java @@ -8,7 +8,6 @@ import java.util.Optional; import java.util.TreeSet; -import seedu.address.logic.commands.AssignCommand; import seedu.address.model.EventTime; import seedu.address.model.person.exceptions.SchedulingException; @@ -47,30 +46,18 @@ public Schedule() { } - public String getSchedulingSuggestion(EventTime eventTime, LocalTime timeNow) { - String suggested = findFirstAvailableSlot(eventTime, timeNow) - .filter(x -> !x.equals(eventTime)) // check if the suggested time is different from proposed - .map(x -> String.format(MESSAGE_SUGGEST_TIME_FORMAT, x.toString())) - .orElse(""); - - String returnSuggestion = suggested.isEmpty() ? "" : "\n" + suggested; - + public SchedulingSuggestion getSchedulingSuggestion(EventTime eventTime, LocalTime timeNow) { + Optional suggestedEventTime = findFirstAvailableSlot(timeNow, eventTime.getDuration()); if (isOutsideWorkingHours(eventTime)) { - return MESSAGE_OUTSIDE_WORKING_HOURS + returnSuggestion; + return new SchedulingSuggestion(MESSAGE_OUTSIDE_WORKING_HOURS, suggestedEventTime, eventTime); } if (!isAvailable(eventTime)) { - return MESSAGE_SCHEDULE_CONFLICT + returnSuggestion; + return new SchedulingSuggestion(MESSAGE_SCHEDULE_CONFLICT, suggestedEventTime, eventTime); } - if (suggested.isEmpty()) { - // no suggestion, the command is good - return suggested; - } - - // has suggestion but dismissible - return MESSAGE_EARLIER_AVAILABLE + suggested + "\n" + AssignCommand.MESSAGE_PROMPT_FORCE; + return new SchedulingSuggestion("", suggestedEventTime, eventTime); } /** @@ -88,23 +75,23 @@ public void add(EventTime eventTime) throws SchedulingException { } if (!schedule.add(eventTime)) { - throw new SchedulingException("An unknown error has occurred."); + // this operation should always succeed, and this line shouldn't be called + throw new SchedulingException("An unknown error has occurred. The schedule is unable" + + " to add the EventTime"); } } /** - * Finds the earliest available EventTime has the same length of proposed, and fits in the schedule. - * This method will check against the current time. + * Finds the earliest available EventTime has the length of proposed, and fits in the schedule. + * This method will check against the input current time. * - * @param proposed a proposed time slot * @param timeNow time now + * @param proposed a proposed duration * @return Optional of the earliest EventTime that can fit in the schedule; if the proposed time is already the * earliest, return an Optional of the proposed time; if no slot available, return an empty Optional. */ - public Optional findFirstAvailableSlot(EventTime proposed, LocalTime timeNow) { - Duration length = proposed.getDuration(); - + public Optional findFirstAvailableSlot(LocalTime timeNow, Duration proposed) { // get a view of the schedule, from system time to the last EventTime in the schedule EventTime lastCandidate = schedule.last(); @@ -122,11 +109,11 @@ public Optional findFirstAvailableSlot(EventTime proposed, LocalTime while (iter.hasNext()) { EventTime head = iter.next(); - boolean canFit = Duration.between(prev.getEnd(), head.getStart()).compareTo(length) >= 0; + boolean canFit = Duration.between(prev.getEnd(), head.getStart()).compareTo(proposed) >= 0; if (canFit) { schedule.remove(now); - return Optional.of(new EventTime(prev.getEnd(), length)); + return Optional.of(new EventTime(prev.getEnd(), proposed)); } prev = head; diff --git a/src/main/java/seedu/address/model/person/SchedulingSuggestion.java b/src/main/java/seedu/address/model/person/SchedulingSuggestion.java new file mode 100644 index 00000000000..78b96ebfa11 --- /dev/null +++ b/src/main/java/seedu/address/model/person/SchedulingSuggestion.java @@ -0,0 +1,59 @@ +package seedu.address.model.person; + +import static seedu.address.logic.commands.AssignCommand.MESSAGE_PROMPT_FORCE; +import static seedu.address.model.person.Schedule.MESSAGE_EARLIER_AVAILABLE; +import static seedu.address.model.person.Schedule.MESSAGE_SUGGEST_TIME_FORMAT; + +import java.util.Optional; + +import seedu.address.model.EventTime; + +/** + * A wrapper around the suggestion returned by Schedule. It has a convenient {@code toString} method for printing. + */ +public class SchedulingSuggestion { + private String errorMessage; + private Optional suggestedTime; + + public SchedulingSuggestion(String errorMessage, Optional suggestedTime, EventTime requestedTime) { + this.errorMessage = errorMessage; + if (suggestedTime.isPresent() && suggestedTime.get().equals(requestedTime)) { + // requested time is the same as the suggested time + // no better time slot exists + this.suggestedTime = Optional.empty(); + } else { + this.suggestedTime = suggestedTime; + } + } + + public SchedulingSuggestion(String errorMessage, Optional suggestedTime) { + this.errorMessage = errorMessage; + this.suggestedTime = suggestedTime; + } + + public boolean isFatal() { + return !errorMessage.isEmpty(); + } + + public boolean isEmpty() { + return suggestedTime.isEmpty() && errorMessage.isEmpty(); + } + + + public Optional getSuggestedTime() { + return suggestedTime; + } + + @Override + public String toString() { + if (isFatal()) { + return errorMessage + getSuggestedTime() + .map(x -> "\n" + String.format(MESSAGE_SUGGEST_TIME_FORMAT, x.toString())) + .orElse(""); + } else if (suggestedTime.isPresent()) { + return MESSAGE_EARLIER_AVAILABLE + suggestedTime.get() + "\n" + MESSAGE_PROMPT_FORCE; + } else { + return ""; // no suggestion, command is good + } + } +} diff --git a/src/main/java/seedu/address/model/task/TaskList.java b/src/main/java/seedu/address/model/task/TaskList.java index 9bb7f7922f3..e8344e92d32 100644 --- a/src/main/java/seedu/address/model/task/TaskList.java +++ b/src/main/java/seedu/address/model/task/TaskList.java @@ -3,7 +3,9 @@ import static java.util.Objects.requireNonNull; import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; +import java.util.ArrayList; import java.util.Comparator; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Optional; @@ -12,6 +14,7 @@ import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import seedu.address.model.person.Driver; import seedu.address.model.task.exceptions.TaskNotFoundException; /** @@ -114,20 +117,39 @@ public ObservableList getList() { return tasksUnmodifiable; } - public List getSortedList(Comparator comparator) { - return tasks - .stream() - .sorted(comparator) - .collect(Collectors.toList()); + public static List getSortedList(List tasks, Comparator comparator) { + tasks.sort(comparator); + return tasks; } - public List getFilteredList(Predicate predicate) { + public static List getFilteredList(List tasks, Predicate predicate) { return tasks .stream() .filter(predicate) .collect(Collectors.toList()); } + /** + * Creates a driver list out of task list. + * NOTE: task list must be filtered by assigned tasks so that all the tasks contains a driver. + * + * @param assignedTasks list of task that is assigned to drivers. + * @return driver list that contains only drivers with assigned tasks. + */ + public static List getDriversFromTasks(List assignedTasks) { + HashSet driverSet = new HashSet<>(); + for (Task task : assignedTasks) { + assert task.getDriver().isPresent(); + + Driver driver = task.getDriver().get(); + driverSet.add(driver); + } + + List driverList = new ArrayList<>(driverSet); + + return driverList; + } + public Iterator getIterator() { return tasks.iterator(); } diff --git a/src/main/java/seedu/address/model/task/TaskManager.java b/src/main/java/seedu/address/model/task/TaskManager.java index fc1d44837b7..9fb7294430d 100644 --- a/src/main/java/seedu/address/model/task/TaskManager.java +++ b/src/main/java/seedu/address/model/task/TaskManager.java @@ -1,6 +1,11 @@ package seedu.address.model.task; +import java.util.Comparator; +import java.util.List; +import java.util.function.Predicate; + import javafx.collections.ObservableList; +import seedu.address.model.person.Driver; /** * Manages the task list. @@ -53,6 +58,18 @@ public void setTaskList(TaskList taskList) { tasks.setTaskList(taskList.getList()); } + public static List getSortedList(List tasks, Comparator comparator) { + return TaskList.getSortedList(tasks, comparator); + } + + public static List getFilteredList(List tasks, Predicate predicate) { + return TaskList.getFilteredList(tasks, predicate); + } + + public static List getDriversFromTasks(List assignedTasks) { + return TaskList.getDriversFromTasks(assignedTasks); + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/src/main/java/seedu/address/ui/MainWindow.java b/src/main/java/seedu/address/ui/MainWindow.java index 9e0b331409c..f2350537a99 100644 --- a/src/main/java/seedu/address/ui/MainWindow.java +++ b/src/main/java/seedu/address/ui/MainWindow.java @@ -1,5 +1,7 @@ package seedu.address.ui; +import static seedu.address.commons.core.Messages.MESSAGE_DATA_START_NEW; + import java.util.logging.Logger; import javafx.event.ActionEvent; @@ -137,6 +139,10 @@ void fillInnerParts() { resultDisplay = new ResultDisplay(); resultDisplayPlaceholder.getChildren().add(resultDisplay.getRoot()); + if (logic.isStartAfresh()) { + resultDisplay.setFeedbackToUser(MESSAGE_DATA_START_NEW); + } + StatusBarFooter statusBarFooter = new StatusBarFooter(logic.getAddressBookFilePath()); statusbarPlaceholder.getChildren().add(statusBarFooter.getRoot()); diff --git a/src/test/java/seedu/address/logic/commands/AddCommandTest.java b/src/test/java/seedu/address/logic/commands/AddCommandTest.java index a8d7a914cad..414ed0d59cc 100644 --- a/src/test/java/seedu/address/logic/commands/AddCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/AddCommandTest.java @@ -261,7 +261,7 @@ public Customer getCustomer(int customerId) { @Override public void setCustomer(Customer customerToEdit, Customer editedCustomer) { - + throw new AssertionError("This method should not be called."); } @Override @@ -360,6 +360,11 @@ public IdManager getIdManager() { throw new AssertionError("This method should not be called."); }; + @Override + public boolean isStartAfresh() { + throw new AssertionError("This method should not be called."); + } + @Override public void viewDriverTask(Person driverToView) { throw new AssertionError("This method should not be called."); diff --git a/src/test/java/seedu/address/logic/commands/AssignCommandTest.java b/src/test/java/seedu/address/logic/commands/AssignCommandTest.java index f12b1dc4394..9bddb53cd13 100644 --- a/src/test/java/seedu/address/logic/commands/AssignCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/AssignCommandTest.java @@ -1,10 +1,11 @@ package seedu.address.logic.commands; -import static seedu.address.logic.commands.AssignCommand.MESSAGE_ASSIGN_SUCCESS; +import static org.junit.jupiter.api.Assertions.assertEquals; import static seedu.address.logic.commands.CommandTestUtil.assertCommandFailure; import static seedu.address.logic.commands.CommandTestUtil.assertCommandSuccess; -import static seedu.address.model.person.Schedule.MESSAGE_EARLIER_AVAILABLE; import static seedu.address.model.person.Schedule.MESSAGE_EVENT_START_BEFORE_NOW_FORMAT; +import static seedu.address.model.person.Schedule.MESSAGE_SCHEDULE_CONFLICT; +import static seedu.address.testutil.SampleEntity.THIRD_VALID_TASK_ID; import static seedu.address.testutil.SampleEntity.VALID_DRIVER; import static seedu.address.testutil.SampleEntity.VALID_TASK_ID; import static seedu.address.testutil.SampleEntity.getSampleFreshModel; @@ -19,7 +20,9 @@ import seedu.address.logic.GlobalClock; import seedu.address.model.EventTime; import seedu.address.model.Model; -import seedu.address.model.person.Schedule; +import seedu.address.model.person.Driver; +import seedu.address.model.person.SchedulingSuggestion; +import seedu.address.model.task.Task; class AssignCommandTest { private Model model; @@ -49,40 +52,45 @@ void execute_addTaskNow_shouldSucceed() { // construct expected by setting both driver and task Model expectedModel = getSampleFreshModel(); - expectedModel.getDriver(VALID_DRIVER.getId()).getSchedule().add(proposed); - expectedModel.getTask(VALID_TASK_ID) - .setDriverAndEventTime(Optional.of(expectedModel.getDriver(VALID_DRIVER.getId())), - Optional.of(proposed)); + Driver targetDriver = expectedModel.getDriver(VALID_DRIVER.getId()); + Task targetTask = expectedModel.getTask(VALID_TASK_ID); - assertCommandSuccess(cmd, model, new CommandResult(String.format(MESSAGE_ASSIGN_SUCCESS, VALID_TASK_ID, - VALID_DRIVER.getName().fullName, proposed)), expectedModel); - } + targetDriver.getSchedule().add(proposed); + targetTask.setDriverAndEventTime( + Optional.of(expectedModel.getDriver(VALID_DRIVER.getId())), + Optional.of(proposed)); - @Test - void execute_addLateTime_shouldSuggestsTimeThrowsException() { - EventTime proposed = EventTime.parse("1600", "1700"); - AssignCommand cmd = new AssignCommand(VALID_DRIVER.getId(), VALID_TASK_ID, proposed, false); + String response = AssignCommand.buildSuccessfulResponse( + new SchedulingSuggestion("", Optional.empty(), proposed), + targetTask, + targetDriver, + proposed); - assertCommandFailure(cmd, model, - MESSAGE_EARLIER_AVAILABLE + String.format(Schedule.MESSAGE_SUGGEST_TIME_FORMAT, - EventTime.parse("1400", "1500").toString()) + "\n" + AssignCommand.MESSAGE_PROMPT_FORCE); + assertCommandSuccess(cmd, model, new CommandResult(response), expectedModel); } - @Test - void executeForce_addLateTime_shouldSucceed() { + void execute_addLateTime_shouldSucceed() { EventTime proposed = EventTime.parse("1600", "1700"); - AssignCommand cmd = new AssignCommand(VALID_DRIVER.getId(), VALID_TASK_ID, proposed, true); + AssignCommand cmd = new AssignCommand(VALID_DRIVER.getId(), VALID_TASK_ID, proposed, false); // construct expected by setting both driver and task Model expectedModel = getSampleFreshModel(); - expectedModel.getDriver(VALID_DRIVER.getId()).getSchedule().add(proposed); - expectedModel.getTask(VALID_TASK_ID) - .setDriverAndEventTime(Optional.of(expectedModel.getDriver(VALID_DRIVER.getId())), - Optional.of(proposed)); + Driver targetDriver = expectedModel.getDriver(VALID_DRIVER.getId()); + Task targetTask = expectedModel.getTask(VALID_TASK_ID); + + targetDriver.getSchedule().add(proposed); + targetTask.setDriverAndEventTime( + Optional.of(expectedModel.getDriver(VALID_DRIVER.getId())), + Optional.of(proposed)); - assertCommandSuccess(cmd, model, new CommandResult(String.format(MESSAGE_ASSIGN_SUCCESS, VALID_TASK_ID, - VALID_DRIVER.getName().fullName, proposed)), expectedModel); + String response = AssignCommand.buildSuccessfulResponse( + new SchedulingSuggestion("", Optional.of(EventTime.parse("1400", "1500"))), + targetTask, + targetDriver, + proposed); + + assertCommandSuccess(cmd, model, new CommandResult(response), expectedModel); } @Test @@ -93,5 +101,89 @@ void execute_addPastTime_throwsException() { GlobalClock.timeNow().format(EventTime.DISPLAY_TIME_FORMAT))); } + @Test + void execute_addConflictingTime_throwsException() { + EventTime existing = EventTime.parse("1400", "1500"); + AssignCommand addExisting = new AssignCommand(VALID_DRIVER.getId(), VALID_TASK_ID, existing, false); + try { + addExisting.execute(model); + } catch (Exception e) { + e.printStackTrace(); + } + + EventTime proposed = EventTime.parse("1430", "1600"); + AssignCommand addProposed = new AssignCommand(VALID_DRIVER.getId(), THIRD_VALID_TASK_ID, proposed, false); + + assertCommandFailure(addProposed, model, new SchedulingSuggestion( + MESSAGE_SCHEDULE_CONFLICT, + Optional.of(EventTime.parse("1500", "1630"))).toString()); + } + + @Test + void executeForce_taskWithDriver_shouldSucceed() { + EventTime existing = EventTime.parse("1400", "1500"); + AssignCommand addExisting = new AssignCommand(VALID_DRIVER.getId(), VALID_TASK_ID, existing, false); + try { + addExisting.execute(model); + } catch (Exception e) { + e.printStackTrace(); + } + + EventTime proposed = EventTime.parse("1430", "1600"); + AssignCommand addProposed = new AssignCommand(VALID_DRIVER.getId(), VALID_TASK_ID, proposed, true); + + + // construct expected by setting both driver and task + Model expectedModel = getSampleFreshModel(); + Driver targetDriver = expectedModel.getDriver(VALID_DRIVER.getId()); + Task targetTask = expectedModel.getTask(VALID_TASK_ID); + + targetDriver.getSchedule().add(proposed); + targetTask.setDriverAndEventTime( + Optional.of(expectedModel.getDriver(VALID_DRIVER.getId())), + Optional.of(proposed)); + + String response = AssignCommand.buildSuccessfulResponse( + new SchedulingSuggestion("", Optional.of(EventTime.parse("1400", "1530"))), + targetTask, + targetDriver, + proposed); + + assertCommandSuccess(addProposed, model, new CommandResult(response), expectedModel); + } + + + @Test + void executeForce_taskWithUnavailableDriver_throwsExceptionAndModelNotChanged() { + EventTime existing1 = EventTime.parse("1400", "1500"); + EventTime existing2 = EventTime.parse("1500", "1600"); + + AssignCommand cmd1 = new AssignCommand(VALID_DRIVER.getId(), VALID_TASK_ID, existing1, false); + AssignCommand cmd2 = new AssignCommand(VALID_DRIVER.getId(), THIRD_VALID_TASK_ID, existing2, false); + try { + cmd1.execute(model); + cmd2.execute(model); + } catch (Exception e) { + e.printStackTrace(); + } + + EventTime proposed = EventTime.parse("1430", "1600"); + AssignCommand addProposed = new AssignCommand(VALID_DRIVER.getId(), VALID_TASK_ID, proposed, true); + + // construct expected by setting both driver and task + Model expectedModel = getSampleFreshModel(); + try { + cmd1.execute(expectedModel); + cmd2.execute(expectedModel); + } catch (Exception e) { + e.printStackTrace(); + } + + assertCommandFailure(addProposed, model, new SchedulingSuggestion( + MESSAGE_SCHEDULE_CONFLICT, + Optional.of(EventTime.parse("1600", "1730"))).toString()); + + assertEquals(expectedModel, model); + } } diff --git a/src/test/java/seedu/address/model/person/ScheduleTest.java b/src/test/java/seedu/address/model/person/ScheduleTest.java index fd6a79952c2..e4abc2a9fdd 100644 --- a/src/test/java/seedu/address/model/person/ScheduleTest.java +++ b/src/test/java/seedu/address/model/person/ScheduleTest.java @@ -65,7 +65,7 @@ void findFirstAvailableSlot_lateButAvail_returnsEarlySlot() { Schedule sample = sampleSchedule(); EventTime expected = EventTime.parse("1000", "1100"); EventTime oneHourTask = EventTime.parse("1500", "1600"); - assertEquals(expected, sample.findFirstAvailableSlot(oneHourTask, expected.getStart()).get()); + assertEquals(expected, sample.findFirstAvailableSlot(expected.getStart(), oneHourTask.getDuration()).get()); } @Test @@ -73,21 +73,21 @@ void findFirstAvailableSlot_schedulingConflict_returnsAvailableSlot() { Schedule sample = sampleSchedule(); EventTime oneHourTask = EventTime.parse("1400", "1500"); EventTime expected = EventTime.parse("1000", "1100"); - assertEquals(expected, sample.findFirstAvailableSlot(oneHourTask, expected.getStart()).get()); + assertEquals(expected, sample.findFirstAvailableSlot(expected.getStart(), oneHourTask.getDuration()).get()); } @Test void findFirstAvailableSlot_alreadyEarliest_returnsItself() { Schedule sample = sampleSchedule(); EventTime threeHourTask = EventTime.parse("1500", "1800"); - assertEquals(threeHourTask, sample.findFirstAvailableSlot(threeHourTask, TEN_AM).get()); + assertEquals(threeHourTask, sample.findFirstAvailableSlot(TEN_AM, threeHourTask.getDuration()).get()); } @Test void findFirstAvailableSlot_notAvailable_returnsEmpty() { Schedule sample = sampleSchedule(); EventTime fourHourTask = EventTime.parse("1400", "1800"); - assertTrue(sample.findFirstAvailableSlot(fourHourTask, TEN_AM).isEmpty()); + assertTrue(sample.findFirstAvailableSlot(TEN_AM, fourHourTask.getDuration()).isEmpty()); } diff --git a/src/test/java/seedu/address/testutil/SampleEntity.java b/src/test/java/seedu/address/testutil/SampleEntity.java index 367913782b6..f35e85715a4 100644 --- a/src/test/java/seedu/address/testutil/SampleEntity.java +++ b/src/test/java/seedu/address/testutil/SampleEntity.java @@ -39,6 +39,10 @@ public class SampleEntity { public static final LocalDate SECOND_VALID_LOCAL_DATE = Task.getDateFromString("13/11/2019"); public static final EventTime SECOND_VALID_EVENT_TIME = EventTime.parse("1200 - 1430"); + public static final int THIRD_VALID_TASK_ID = 3; + public static final Description THIRD_VALID_DESCRIPTION = new Description("10 boxes of Blood Oranges"); + public static final LocalDate THIRD_VALID_LOCAL_DATE = GlobalClock.getStaticDate(); + public static final Customer VALID_CUSTOMER = new Customer(1, new Name("Alex Yeoh"), new Phone("87438807"), new Email("alexyeoh@gmail.com"), new Address("Blk 30 Geylang Street 29, #06-40"), @@ -130,6 +134,8 @@ public static CentralManager getSampleCentralManager() { taskManager.addTask(getUnassignedTask(VALID_TASK_ID, VALID_DESCRIPTION, VALID_LOCAL_DATE, VALID_CUSTOMER)); taskManager.addTask(getUnassignedTask(SECOND_VALID_TASK_ID, SECOND_VALID_DESCRIPTION, SECOND_VALID_LOCAL_DATE, SECOND_VALID_CUSTOMER)); + taskManager.addTask(getUnassignedTask(THIRD_VALID_TASK_ID, THIRD_VALID_DESCRIPTION, THIRD_VALID_LOCAL_DATE, + SECOND_VALID_CUSTOMER)); return new CentralManager(customerManager, driverManager, taskManager, new IdManager(