diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 00000000000..109efdf7bbb
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -0,0 +1,25 @@
+name: MarkBind Action
+
+on:
+ push:
+ branches:
+ - master
+
+jobs:
+ build_and_deploy:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Install Graphviz
+ run: sudo apt-get install graphviz
+ - name: Install Java
+ uses: actions/setup-java@v3
+ with:
+ java-version: '17'
+ distribution: 'zulu'
+ - name: Build & Deploy MarkBind site
+ uses: MarkBind/markbind-action@v2
+ with:
+ token: ${{ secrets.GITHUB_TOKEN }}
+ rootDirectory: './docs'
+ baseUrl: '/tp' # assuming your repo name is tp
+ version: '^5.5.2'
diff --git a/.gitignore b/.gitignore
index 284c4ca7cd9..b3c0e257958 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,8 @@ src/main/resources/docs/
/.idea/
/out/
/*.iml
+/bin/
+/.vscode/
# Storage/log files
/data/
@@ -21,3 +23,7 @@ src/test/data/sandbox/
# MacOS custom attributes files created by Finder
.DS_Store
docs/_site/
+docs/_markbind/logs/
+
+# Node.js
+node_modules/
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 00000000000..050505ce79e
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+ "java.configuration.updateBuildConfiguration": "interactive"
+}
diff --git a/README.md b/README.md
index 16208adb9b6..41d319663d3 100644
--- a/README.md
+++ b/README.md
@@ -1,14 +1,10 @@
-[](https://github.com/se-edu/addressbook-level3/actions)
+[](https://github.com/AY2425S2-CS2103T-F12-2/tp/actions)

-* 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/#contributing-to-se-edu) for more info.
+- **EasyWeds** is developed as part of NUS CS2103T Module Project
+- EasyWeds is a desktop application for streamlining wedding planning by centralising vendor and client information, ensuring planners can quickly access contacts, track event schedules, and manage multiple weddings efficiently.
+ - It is **written in OOP fashion**. It provides a **well-written** code base (around 9 KLoC).
+ - It comes with a **high level of user and developer documentation**.
+- For the detailed documentation of this project, see the **[EasyWeds Product Website](https://ay2425s2-cs2103t-f12-2.github.io/tp)**.
+- 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 0db3743584e..f606375b85f 100644
--- a/build.gradle
+++ b/build.gradle
@@ -20,6 +20,10 @@ checkstyle {
toolVersion = '10.2'
}
+run {
+ enableAssertions = true
+}
+
test {
useJUnitPlatform()
finalizedBy jacocoTestReport
@@ -61,12 +65,15 @@ dependencies {
implementation group: 'com.fasterxml.jackson.datatype', name: 'jackson-datatype-jsr310', version: '2.7.4'
testImplementation group: 'org.junit.jupiter', name: 'junit-jupiter-api', version: jUnitVersion
+ testImplementation 'org.testfx:testfx-core:4.0.16-alpha'
+ testImplementation 'org.testfx:testfx-junit5:4.0.16-alpha'
+ testImplementation 'org.testfx:openjfx-monocle:jdk-12.0.1+2'
testRuntimeOnly group: 'org.junit.jupiter', name: 'junit-jupiter-engine', version: jUnitVersion
}
shadowJar {
- archiveFileName = 'addressbook.jar'
+ archiveFileName = 'easyweds.jar'
}
defaultTasks 'clean', 'test'
diff --git a/docs/.gitignore b/docs/.gitignore
new file mode 100644
index 00000000000..1748e487fbd
--- /dev/null
+++ b/docs/.gitignore
@@ -0,0 +1,23 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+_markbind/logs/
+
+# Dependency directories
+node_modules/
+
+# Production build files (change if you output the build to a different directory)
+_site/
+
+# Env
+.env
+.env.local
+
+# IDE configs
+.vscode/
+.idea/*
+*.iml
diff --git a/docs/AboutUs.md b/docs/AboutUs.md
index ff3f04abd02..c40758acf8b 100644
--- a/docs/AboutUs.md
+++ b/docs/AboutUs.md
@@ -1,59 +1,61 @@
---
-layout: page
-title: About Us
+layout: default.md
+title: "About Us"
---
-We are a team based in the [School of Computing, National University of Singapore](https://www.comp.nus.edu.sg).
+# 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
+### Geng Yudong
-
+
-[[homepage](http://www.comp.nus.edu.sg/~damithch)]
-[[github](https://github.com/johndoe)]
-[[portfolio](team/johndoe.md)]
+[[Github](http://github.com/gengyudong)]
+[[Portfolio](team/gengyudong.md)]
-* Role: Project Advisor
+* Role: Developer (Team Lead)
+* Responsibilities: Feature - Delete Contacts
-### Jane Doe
+### Glenn Liew Zi Yi
-
+
-[[github](http://github.com/johndoe)]
-[[portfolio](team/johndoe.md)]
+[[Github](http://github.com/glennliew)]
+[[Portfolio](team/glennliew.md)]
-* Role: Team Lead
+* Role: Developer (Documentation)
* Responsibilities: UI
-### Johnny Doe
+### Hu Junjie
-
+
-[[github](http://github.com/johndoe)] [[portfolio](team/johndoe.md)]
+[[Github](http://github.com/grassheadd)] [[Portfolio](team/junjie.md)]
-* Role: Developer
+* Role: Developer (Code Quality)
* Responsibilities: Data
-### Jean Doe
+### Arulanandam James Beryl
-
+
-[[github](http://github.com/johndoe)]
-[[portfolio](team/johndoe.md)]
+[[Github](https://github.com/jamesberyl)]
+[[Portfolio](team/jamesberyl.md)]
-* Role: Developer
-* Responsibilities: Dev Ops + Threading
+* Role: Developer (Testing)
+* Responsibilities: Feature - Filter Contacts
-### James Doe
+### Wei Yan Min Oo
-
+
-[[github](http://github.com/johndoe)]
-[[portfolio](team/johndoe.md)]
+[[Github](http://github.com/weiyanminoo)]
+[[Portfolio](team/weiyanminoo.md)]
-* Role: Developer
-* Responsibilities: UI
+* Role: Developer (Integration)
+* Responsibilities: Feature - Add Contacts
diff --git a/docs/Configuration.md b/docs/Configuration.md
index 13cf0faea16..32f6255f3b9 100644
--- a/docs/Configuration.md
+++ b/docs/Configuration.md
@@ -1,6 +1,8 @@
---
-layout: page
-title: Configuration guide
+ layout: default.md
+ title: "Configuration guide"
---
+# Configuration guide
+
Certain properties of the application can be controlled (e.g user preferences file location, logging level) through the configuration file (default: `config.json`).
diff --git a/docs/DevOps.md b/docs/DevOps.md
index d2fd91a6001..8228c845e86 100644
--- a/docs/DevOps.md
+++ b/docs/DevOps.md
@@ -1,12 +1,15 @@
---
-layout: page
-title: DevOps guide
+ layout: default.md
+ title: "DevOps guide"
+ pageNav: 3
---
-* Table of Contents
-{:toc}
+# DevOps guide
---------------------------------------------------------------------------------------------------------------------
+
+
+
+
+
+
+
+**API** : [`Model.java`](https://github.com/AY2425S2-CS2103T-F12-2/tp/blob/master/src/main/java/seedu/address/model/Model.java)
+[Model is the interface for the model component. It encapsulates the application's data structures and business logic, providing methods to access and modify contact, wedding and task data.]
+
+**Note:** An alternative (arguably, a more OOP) model is given below. It has a `Tag` list in the `AddressBook`, which `Person` references. This allows `AddressBook` to only require one `Tag` object per unique tag, instead of each `Person` needing their own `Tag` objects.
+
+
-### Adding a person: `add`
+This method first checks if the person already exists in the address book. + * If a duplicate is detected and this is not a force add, it returns a {@code CommandResult} + * containing a duplicate warning message along with a flag indicating that confirmation is required. + * The UI should then prompt the user and, upon confirmation, re-execute this command in force mode.
+ * + * @param model {@code Model} which the command should operate on. + * @return the result of command execution. + * @throws CommandException if an error occurs during execution. + */ @Override public CommandResult execute(Model model) throws CommandException { requireNonNull(model); - if (model.hasPerson(toAdd)) { - throw new CommandException(MESSAGE_DUPLICATE_PERSON); + if (!isForced && model.hasPerson(toAdd)) { + // In normal mode, if a duplicate exists, store this command and signal confirmation required. + ConfirmationManager.getInstance().setPendingCommand(this); + return new CommandResult(MESSAGE_DUPLICATE_PERSON, false, false, true); } - model.addPerson(toAdd); + if (isForced) { + // Force mode: bypass duplicate check. + model.forceAddPerson(toAdd); + } else { + // Normal mode: add person normally. + model.addPerson(toAdd); + } return new CommandResult(String.format(MESSAGE_SUCCESS, Messages.format(toAdd))); } + @Override + public ForceableCommand createForceCommand() { + return new AddCommand(toAdd, true); + } + @Override public boolean equals(Object other) { if (other == this) { @@ -72,13 +115,14 @@ public boolean equals(Object other) { } AddCommand otherAddCommand = (AddCommand) other; - return toAdd.equals(otherAddCommand.toAdd); + return toAdd.equals(otherAddCommand.toAdd) && isForced == otherAddCommand.isForced; } @Override public String toString() { return new ToStringBuilder(this) .add("toAdd", toAdd) + .add("isForced", isForced) .toString(); } } diff --git a/src/main/java/seedu/address/logic/commands/AddTaskCommand.java b/src/main/java/seedu/address/logic/commands/AddTaskCommand.java new file mode 100644 index 00000000000..22080a1f59a --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AddTaskCommand.java @@ -0,0 +1,65 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.wedding.Wedding; +import seedu.address.model.wedding.WeddingId; +import seedu.address.model.wedding.WeddingTask; + +/** + * Adds a new task to a specific wedding identified by a Wedding ID. + */ +public class AddTaskCommand extends Command { + + public static final String COMMAND_WORD = "addTask"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a task to a specified wedding.\n" + + "Parameters: w/WEDDING_ID desc/TASK_DESCRIPTION\n" + + "Example: " + COMMAND_WORD + " w/W1 desc/Book photographer"; + + public static final String MESSAGE_SUCCESS = "New task added to wedding %1$s:\n%2$s"; + public static final String MESSAGE_INVALID_FORMAT = "Invalid command format. " + MESSAGE_USAGE; + + private final WeddingId weddingId; + private final String taskDescription; + + /** + * Creates an AddTaskCommand to add a task with the specified description to the specified wedding ID. + */ + public AddTaskCommand(WeddingId weddingId, String taskDescription) { + requireNonNull(weddingId); + requireNonNull(taskDescription); + this.weddingId = weddingId; + this.taskDescription = taskDescription; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + Wedding wedding = model.getFilteredWeddingList().stream() + .filter(w -> w.getWeddingId().equals(weddingId)) + .findFirst() + .orElseThrow(() -> new CommandException(String.format(Messages.MESSAGE_WEDDING_NOT_FOUND, + weddingId.value))); + + WeddingTask newTask = new WeddingTask(taskDescription); + wedding.addTask(newTask); + return new CommandResult(String.format(MESSAGE_SUCCESS, weddingId.value, newTask), false, false, false, true); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (!(other instanceof AddTaskCommand)) { + return false; + } + AddTaskCommand c = (AddTaskCommand) other; + return weddingId.equals(c.weddingId) && taskDescription.equals(c.taskDescription); + } +} diff --git a/src/main/java/seedu/address/logic/commands/AddWeddingCommand.java b/src/main/java/seedu/address/logic/commands/AddWeddingCommand.java new file mode 100644 index 00000000000..762f127783f --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/AddWeddingCommand.java @@ -0,0 +1,53 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.wedding.Wedding; + +/** + * Adds a Wedding event to the contact book. + */ +public class AddWeddingCommand extends Command { + + public static final String COMMAND_WORD = "addWedding"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Adds a wedding to the contact book. " + + "Parameters: " + + "n/NAME d/DATE l/LOCATION\n" + + "Example: " + COMMAND_WORD + " n/John & Jane's Wedding d/20-Feb-2026 l/Grand Ballroom"; + + public static final String MESSAGE_SUCCESS = "New wedding added: %1$s"; + + public static final String MESSAGE_DUPLICATE_WEDDING = "This wedding already exists in the contact book"; + + private final Wedding toAdd; + + /** + * Creates an AddWeddingCommand to add the specified {@code Wedding}. + */ + public AddWeddingCommand(Wedding wedding) { + requireNonNull(wedding); + toAdd = wedding; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + if (model.hasWedding(toAdd)) { + throw new CommandException(MESSAGE_DUPLICATE_WEDDING); + } + + model.addWedding(toAdd); + return new CommandResult(String.format(MESSAGE_SUCCESS, toAdd)); + } + + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof AddWeddingCommand + && toAdd.equals(((AddWeddingCommand) other).toAdd)); + } +} diff --git a/src/main/java/seedu/address/logic/commands/ClearCommand.java b/src/main/java/seedu/address/logic/commands/ClearCommand.java index 9c86b1fa6e4..6d5165c6121 100644 --- a/src/main/java/seedu/address/logic/commands/ClearCommand.java +++ b/src/main/java/seedu/address/logic/commands/ClearCommand.java @@ -6,18 +6,90 @@ import seedu.address.model.Model; /** - * Clears the address book. + * Clears the contact book. + *+ * In normal mode, if the address book is not empty, the command will signal that confirmation is required + * to avoid accidental clearing. The command is stored in the ConfirmationManager so that it can be re-executed + * in force mode if the user confirms. + *
*/ -public class ClearCommand extends Command { +public class ClearCommand extends Command implements ForceableCommand { public static final String COMMAND_WORD = "clear"; - public static final String MESSAGE_SUCCESS = "Address book has been cleared!"; + public static final String MESSAGE_SUCCESS = "Contact book has been cleared!"; + public static final String MESSAGE_CONFIRMATION_REQUIRED = + "WARNING: You are about to clear the contact book.\n" + + "If you wish to proceed, use 'Ctrl / Command + A' and press 'Delete / Backspace' to clear the input box\n" + + "and input 'y' to confirm.\n" + + "Else, edit your input directly and press 'Enter'. "; + private final boolean isForced; + /** + * Creates a ClearCommand in normal mode. + */ + public ClearCommand() { + this(false); + } + + /** + * Creates a ClearCommand with the specified mode. + * + * @param isForced If true, the confirmation requirement is bypassed. + */ + public ClearCommand(boolean isForced) { + requireNonNull(isForced); + this.isForced = isForced; + } + + /** + * Executes the clear command. + *+ * In normal mode, if the address book is not empty, this method stores this command in the ConfirmationManager + * and returns a CommandResult indicating that confirmation is required. + * In force mode (or if the address book is already empty), it clears the address book immediately. + *
+ * + * @param model {@code Model} which the command should operate on. + * @return the result of command execution. + */ @Override public CommandResult execute(Model model) { requireNonNull(model); + // Check if the address book is not empty + if (!isForced && !model.getAddressBook().getPersonList().isEmpty()) { + ConfirmationManager.getInstance().setPendingCommand(this); + return new CommandResult(MESSAGE_CONFIRMATION_REQUIRED, false, false, true); + } + // If force mode or if the address book is empty, proceed to clear it. model.setAddressBook(new AddressBook()); return new CommandResult(MESSAGE_SUCCESS); } + + /** + * Creates and returns a forced version of this command. + * + * @return a new ClearCommand with the force flag set to true. + */ + @Override + public ForceableCommand createForceCommand() { + return new ClearCommand(true); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (!(other instanceof ClearCommand)) { + return false; + } + ClearCommand otherClear = (ClearCommand) other; + return isForced == otherClear.isForced; + } + + @Override + public String toString() { + return getClass().getCanonicalName() + "{isForced=" + isForced + "}"; + } } diff --git a/src/main/java/seedu/address/logic/commands/CommandResult.java b/src/main/java/seedu/address/logic/commands/CommandResult.java index 249b6072d0d..3d20ffa3d59 100644 --- a/src/main/java/seedu/address/logic/commands/CommandResult.java +++ b/src/main/java/seedu/address/logic/commands/CommandResult.java @@ -19,13 +19,35 @@ public class CommandResult { /** The application should exit. */ private final boolean exit; + private boolean refreshUI; + + /** + * Indicates whether this command result requires further user confirmation. + * For example, this flag can be set when a duplicate person is detected, + * and the UI must prompt the user to confirm the operation. + */ + private final boolean requiresConfirmation; + /** * 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 requiresConfirmation) { + this.feedbackToUser = requireNonNull(feedbackToUser); + this.showHelp = showHelp; + this.exit = exit; + this.requiresConfirmation = requiresConfirmation; + } + + /** + * Constructs a {@code CommandResult} with the specified fields. + */ + public CommandResult(String feedbackToUser, boolean showHelp, + boolean exit, boolean requiresConfirmation, boolean refreshUI) { this.feedbackToUser = requireNonNull(feedbackToUser); this.showHelp = showHelp; this.exit = exit; + this.requiresConfirmation = requiresConfirmation; + this.refreshUI = refreshUI; } /** @@ -33,7 +55,7 @@ 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); } public String getFeedbackToUser() { @@ -48,6 +70,23 @@ public boolean isExit() { return exit; } + /** + * Returns true if this CommandResult indicates that further user confirmation is required. + * + * @return {@code true} if user confirmation is required; {@code false} otherwise. + */ + public boolean isRequiresConfirmation() { + return requiresConfirmation; + } + + /** + * Returns if this CommandResult indicates that the UI should be refreshed. + * @return {@code true} if UI should be refreshed; {@code false} otherwise. + */ + public boolean isRefreshUI() { + return refreshUI; + } + @Override public boolean equals(Object other) { if (other == this) { @@ -62,12 +101,13 @@ public boolean equals(Object other) { CommandResult otherCommandResult = (CommandResult) other; return feedbackToUser.equals(otherCommandResult.feedbackToUser) && showHelp == otherCommandResult.showHelp - && exit == otherCommandResult.exit; + && exit == otherCommandResult.exit + && requiresConfirmation == otherCommandResult.requiresConfirmation; } @Override public int hashCode() { - return Objects.hash(feedbackToUser, showHelp, exit); + return Objects.hash(feedbackToUser, showHelp, exit, requiresConfirmation); } @Override @@ -76,7 +116,7 @@ public String toString() { .add("feedbackToUser", feedbackToUser) .add("showHelp", showHelp) .add("exit", exit) + .add("requiresConfirmation", requiresConfirmation) .toString(); } - } diff --git a/src/main/java/seedu/address/logic/commands/ConfirmCommand.java b/src/main/java/seedu/address/logic/commands/ConfirmCommand.java new file mode 100644 index 00000000000..c128c15c0d6 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ConfirmCommand.java @@ -0,0 +1,33 @@ +package seedu.address.logic.commands; + +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; + +/** + * Re-executes the pending command in force mode after user confirmation. + */ +public class ConfirmCommand extends Command { + + public static final String COMMAND_WORD = "y"; + + @Override + public CommandResult execute(Model model) throws CommandException { + // Retrieve the pending command that required confirmation. + Command pendingCommand = ConfirmationManager.getInstance().getPendingCommand(); + if (pendingCommand == null) { + throw new CommandException("No pending command to confirm."); + } + if (!(pendingCommand instanceof ForceableCommand)) { + throw new CommandException("Pending command cannot be forced."); + } + ForceableCommand forceable = (ForceableCommand) pendingCommand; + // Clear the pending command so that it's not executed twice. + ConfirmationManager.getInstance().clearPendingCommand(); + return ((Command) forceable.createForceCommand()).execute(model); + } + + @Override + public boolean equals(Object other) { + return other instanceof ConfirmCommand; + } +} diff --git a/src/main/java/seedu/address/logic/commands/ConfirmationManager.java b/src/main/java/seedu/address/logic/commands/ConfirmationManager.java new file mode 100644 index 00000000000..3959462dbf2 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/ConfirmationManager.java @@ -0,0 +1,48 @@ +package seedu.address.logic.commands; + +/** + * A singleton that manages the pending command requiring confirmation. + */ +public class ConfirmationManager { + private static ConfirmationManager instance; + private Command pendingCommand; + + private ConfirmationManager() {} + + /** + * Returns the singleton instance of ConfirmationManager. + * + * @return the ConfirmationManager instance. + */ + public static ConfirmationManager getInstance() { + if (instance == null) { + instance = new ConfirmationManager(); + } + return instance; + } + + /** + * Sets the pending command that requires user confirmation. + * + * @param command The command to store. + */ + public void setPendingCommand(Command command) { + this.pendingCommand = command; + } + + /** + * Returns the currently pending command. + * + * @return the pending command, or null if none. + */ + public Command getPendingCommand() { + return pendingCommand; + } + + /** + * Clears the stored pending command. + */ + public void clearPendingCommand() { + this.pendingCommand = null; + } +} diff --git a/src/main/java/seedu/address/logic/commands/DeleteCommand.java b/src/main/java/seedu/address/logic/commands/DeleteCommand.java index 1135ac19b74..ce2bf78cbf0 100644 --- a/src/main/java/seedu/address/logic/commands/DeleteCommand.java +++ b/src/main/java/seedu/address/logic/commands/DeleteCommand.java @@ -12,7 +12,7 @@ import seedu.address.model.person.Person; /** - * Deletes a person identified using it's displayed index from the address book. + * Deletes a person identified using it's displayed index from the contact book. */ public class DeleteCommand extends Command { diff --git a/src/main/java/seedu/address/logic/commands/DeleteTaskCommand.java b/src/main/java/seedu/address/logic/commands/DeleteTaskCommand.java new file mode 100644 index 00000000000..570cf0dffcd --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/DeleteTaskCommand.java @@ -0,0 +1,70 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.wedding.Wedding; +import seedu.address.model.wedding.WeddingId; +import seedu.address.model.wedding.WeddingTask; + +/** + * Deletes a task to a specific wedding identified by a Wedding ID. + */ +public class DeleteTaskCommand extends Command { + + public static final String COMMAND_WORD = "deleteTask"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Deletes a task from a specified wedding.\n" + + "Parameters: w/WEDDING_ID i/TASK_INDEX\n" + + "Example: " + COMMAND_WORD + " w/W1 i/2"; + + public static final String MESSAGE_SUCCESS = "Deleted task from wedding %1$s:\n%2$s"; + public static final String MESSAGE_INVALID_TASK_INDEX = "Invalid task index for wedding %1$s."; + + private final WeddingId weddingId; + private final int taskIndex; + + /** + * Constructs a DeleteTaskCommand to remove the specified task from the given wedding. + * + * @param weddingId The ID of the wedding from which the task will be deleted. + * @param taskIndex The 1-based index of the task to delete. + */ + public DeleteTaskCommand(WeddingId weddingId, int taskIndex) { + this.weddingId = weddingId; + this.taskIndex = taskIndex; + } + + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + Wedding wedding = model.getFilteredWeddingList().stream() + .filter(w -> w.getWeddingId().equals(weddingId)) + .findFirst() + .orElseThrow(() -> new CommandException(String.format(Messages.MESSAGE_WEDDING_NOT_FOUND, + weddingId.value))); + + try { + WeddingTask removed = wedding.removeTask(taskIndex - 1); + return new CommandResult(String.format(MESSAGE_SUCCESS, weddingId.value, removed), + false, false, false, true); + } catch (IndexOutOfBoundsException e) { + throw new CommandException(String.format(MESSAGE_INVALID_TASK_INDEX, weddingId.value)); + } + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + if (!(other instanceof DeleteTaskCommand)) { + return false; + } + DeleteTaskCommand c = (DeleteTaskCommand) other; + return weddingId.equals(c.weddingId) && taskIndex == c.taskIndex; + } +} diff --git a/src/main/java/seedu/address/logic/commands/DeleteWeddingCommand.java b/src/main/java/seedu/address/logic/commands/DeleteWeddingCommand.java new file mode 100644 index 00000000000..60b47fb9681 --- /dev/null +++ b/src/main/java/seedu/address/logic/commands/DeleteWeddingCommand.java @@ -0,0 +1,83 @@ +package seedu.address.logic.commands; + +import static java.util.Objects.requireNonNull; + +import seedu.address.logic.Messages; +import seedu.address.logic.commands.exceptions.CommandException; +import seedu.address.model.Model; +import seedu.address.model.tag.Tag; +import seedu.address.model.wedding.Wedding; +import seedu.address.model.wedding.WeddingId; + + + +/** + * Deletes a Wedding event from the contact book. + */ +public class DeleteWeddingCommand extends Command { + + public static final String COMMAND_WORD = "deleteWedding"; + + public static final String MESSAGE_USAGE = COMMAND_WORD + ": Deletes a wedding from the contact book. " + + "Parameters: WEDDING_ID\n" + + "Example: " + COMMAND_WORD + " W1"; + + public static final String MESSAGE_SUCCESS = "Deleted wedding: %1$s"; + + private final WeddingId weddingId; + + /** + * Creates a DeleteWeddingCommand to delete the specified {@code Wedding}. + */ + public DeleteWeddingCommand(WeddingId weddingId) { + requireNonNull(weddingId); + this.weddingId = weddingId; + } + + /** + * Executes the command and deletes the specified {@code Wedding}. + * + * @param model {@code Model} which the command should operate on. + * @return A {@code CommandResult} representing the result of the deletion. + * @throws CommandException If the specified {@code Wedding} does not exist. + */ + @Override + public CommandResult execute(Model model) throws CommandException { + requireNonNull(model); + + Wedding weddingToDelete = model.getWeddingById(weddingId); + + if (weddingToDelete == null) { + throw new CommandException(String.format(Messages.MESSAGE_WEDDING_NOT_FOUND, weddingId.value)); + } + + boolean isFiltered = model.getFilteredWeddingList().size() < model.getAddressBook().getWeddingList().size(); + + model.deleteWedding(weddingToDelete); + + Tag tagToDelete = new Tag(weddingId); + model.removeTagFromAllContacts(tagToDelete); + + // If a filter was applied, show blank lists, otherwise, show full lists. + if (isFiltered) { + model.updateFilteredWeddingList(w -> false); + model.updateFilteredPersonList(p -> false); + } else { + model.updateFilteredWeddingList(w -> true); + model.updateFilteredPersonList(p -> true); + } + + return new CommandResult(String.format(MESSAGE_SUCCESS, weddingToDelete)); + } + + /** + * Returns true if both DeleteWeddingCommands have the same wedding id. + * @param other The other object to compare to. + */ + @Override + public boolean equals(Object other) { + return other == this + || (other instanceof DeleteWeddingCommand + && weddingId.equals(((DeleteWeddingCommand) other).weddingId)); + } +} diff --git a/src/main/java/seedu/address/logic/commands/EditCommand.java b/src/main/java/seedu/address/logic/commands/EditCommand.java index 4b581c7331e..6b0e4ef95d8 100644 --- a/src/main/java/seedu/address/logic/commands/EditCommand.java +++ b/src/main/java/seedu/address/logic/commands/EditCommand.java @@ -5,15 +5,12 @@ 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_TAG; +import static seedu.address.logic.parser.CliSyntax.PREFIX_ROLE; import static seedu.address.model.Model.PREDICATE_SHOW_ALL_PERSONS; -import java.util.Collections; -import java.util.HashSet; import java.util.List; import java.util.Objects; import java.util.Optional; -import java.util.Set; import seedu.address.commons.core.index.Index; import seedu.address.commons.util.CollectionUtil; @@ -26,12 +23,12 @@ import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; -import seedu.address.model.tag.Tag; +import seedu.address.model.person.Role; /** - * Edits the details of an existing person in the address book. + * Edits the details of an existing person in the contact book. */ -public class EditCommand extends Command { +public class EditCommand extends Command implements ForceableCommand { public static final String COMMAND_WORD = "edit"; @@ -42,29 +39,47 @@ public class EditCommand extends Command { + "[" + PREFIX_NAME + "NAME] " + "[" + PREFIX_PHONE + "PHONE] " + "[" + PREFIX_EMAIL + "EMAIL] " + + "[" + PREFIX_ROLE + "ROLE] " + "[" + PREFIX_ADDRESS + "ADDRESS] " - + "[" + PREFIX_TAG + "TAG]...\n" + "Example: " + COMMAND_WORD + " 1 " + PREFIX_PHONE + "91234567 " + PREFIX_EMAIL + "johndoe@example.com"; public static final String MESSAGE_EDIT_PERSON_SUCCESS = "Edited Person: %1$s"; public static final String MESSAGE_NOT_EDITED = "At least one field to edit must be provided."; - public static final String MESSAGE_DUPLICATE_PERSON = "This person already exists in the address book."; + public static final String MESSAGE_NO_CHANGES = + "No changes detected. Your input is exactly the same as the existing values."; + public static final String MESSAGE_DUPLICATE_PERSON = + "WARNING: This person may already exist in the contact book.\n" + + "If you wish to proceed, use 'Ctrl / Command + A' and press 'Delete / Backspace' to clear the input box\n" + + "and input 'y' to confirm.\n" + + "Else, edit your input directly and press 'Enter'. "; private final Index index; private final EditPersonDescriptor editPersonDescriptor; + private final boolean isForced; /** + * Constructs an EditCommand in normal mode. + */ + public EditCommand(Index index, EditPersonDescriptor editPersonDescriptor) { + this(index, editPersonDescriptor, false); + } + + /** + * Constructs an EditCommand with the specified mode. + * * @param index of the person in the filtered person list to edit * @param editPersonDescriptor details to edit the person with + * @param isForced flag indicating that duplicate checks are bypassed (force mode) */ - public EditCommand(Index index, EditPersonDescriptor editPersonDescriptor) { + public EditCommand(Index index, EditPersonDescriptor editPersonDescriptor, boolean isForced) { requireNonNull(index); requireNonNull(editPersonDescriptor); this.index = index; this.editPersonDescriptor = new EditPersonDescriptor(editPersonDescriptor); + this.isForced = isForced; } @Override @@ -79,8 +94,22 @@ public CommandResult execute(Model model) throws CommandException { Person personToEdit = lastShownList.get(index.getZeroBased()); Person editedPerson = createEditedPerson(personToEdit, editPersonDescriptor); + // If no effective changes are made, signal that the input is exactly the same. + if (personToEdit.equals(editedPerson)) { + throw new CommandException(MESSAGE_NO_CHANGES); + } + + // If the edited person is not the same as the original and a duplicate exists: if (!personToEdit.isSamePerson(editedPerson) && model.hasPerson(editedPerson)) { - throw new CommandException(MESSAGE_DUPLICATE_PERSON); + if (!isForced) { + ConfirmationManager.getInstance().setPendingCommand(this); + return new CommandResult(MESSAGE_DUPLICATE_PERSON, false, false, true); + } else { + // Force mode: bypass duplicate check by calling forceSetPerson. + model.forceSetPerson(personToEdit, editedPerson); + model.updateFilteredPersonList(PREDICATE_SHOW_ALL_PERSONS); + return new CommandResult(String.format(MESSAGE_EDIT_PERSON_SUCCESS, Messages.format(editedPerson))); + } } model.setPerson(personToEdit, editedPerson); @@ -98,10 +127,15 @@ private static Person createEditedPerson(Person personToEdit, EditPersonDescript Name updatedName = editPersonDescriptor.getName().orElse(personToEdit.getName()); Phone updatedPhone = editPersonDescriptor.getPhone().orElse(personToEdit.getPhone()); Email updatedEmail = editPersonDescriptor.getEmail().orElse(personToEdit.getEmail()); + Role updatedRole = editPersonDescriptor.getRole().orElse(personToEdit.getRole()); Address updatedAddress = editPersonDescriptor.getAddress().orElse(personToEdit.getAddress()); - SetThis updated version supports escaping prefixes. For instance, if a user inputs:
+ *
+ * The ConfirmCommand does not accept any arguments. If the user input is not empty,
+ * a ParseException is thrown.
+ *
+ * When loading data, forced duplicates are allowed so that previously forced duplicate entries are preserved.
+ *
+ * This method bypasses duplicate checks. It locates the person in the internal list and
+ * replaces the record with {@code editedPerson}. If the target is not found, a
+ * {@code PersonNotFoundException} is thrown.
+ *
+ * The command text is executed and the resulting CommandResult is checked.
+ * If the command does not require confirmation, the input field is cleared.
+ * Otherwise, the user's input is preserved so that they can edit or confirm.
+ * addWedding n/John & Jane \d/ Wedding d/30-Apr-2026 l/MBS
+ *
then the "\d/" will be treated as literal text (producing "d/") and will not be interpreted as a new prefix.
*/
public class ArgumentTokenizer {
@@ -60,19 +64,26 @@ private static List