diff --git a/docs/UserGuide.md b/docs/UserGuide.md index 6c7f6d2ee01..d1dddca70d1 100644 --- a/docs/UserGuide.md +++ b/docs/UserGuide.md @@ -81,7 +81,7 @@ Adds a contact to OutBook. Format: `addc n/NAME p/PHONE_NUMBER e/EMAIL l/LAST_CONTACTED_TIME s/STATUS [t/TAG]…​` -* NAME, PHONE_NUMBER, EMAIL and LAST_CONTACTED_TIME are compulsory fields. STATUS and TAG are optional. +* NAME, PHONE_NUMBER, and EMAIL are compulsory fields. STATUS, TAG and LAST_CONTACTED_TIME are optional. * PHONE_NUMBER must contain only numbers, and be at least 3 digits long. * EMAIL must be of the format local-part@domain and adhere to the following constraints: 1. The local-part should only contain alphanumeric characters and these special characters, excluding the parentheses, (+_.-). @@ -174,23 +174,28 @@ Examples: * `viewc 2` Displays detailed information related to the 2nd contact on the list. -### Locating persons by name: `findc` +### Search for persons using contact fields: `findc` +Find persons whose contact details match the keywords specified for at least 1 of these fields: name, phone, email, status, tag -Find contacts whose names contain any of the given keywords. +Format: `findc [n/KEYWORDS] [p/KEYWORDS] [e/KEYWORDS] [l/DATETIME] [s/KEYWORDS] [t/KEYWORDS]` -Format: `findc KEYWORD [MORE_KEYWORDS]` - -* The search is case-insensitive. e.g `hans` will match `Hans` -* The order of the keywords does not matter. e.g. `Hans Bo` will match `Bo Hans` -* Only the name is searched. -* Only full words will be matched e.g. `Han` will not match `Hans` -* Persons matching at least one keyword will be returned (i.e. `OR` search). - e.g. `Hans Bo` will return `Hans Gruber`, `Bo Yang` +* The search is case-insensitive. e.g `shop` will match `SHOP` +* The order of the keywords does not matter. e.g. `Shop Meet` will match `Meet Shop` +* For name, status and tags, only full words will be matched e.g. `Meet` will not match `Meeting` +* For email, any characters (alphanumeric, special characters) will be matched e.g. `_` will match `m_e@gmail.com` +* For phone, the entire length of the input digits will be matched e.g. `913` will match `90091300` but not `90103000` +* For last contacted time, the input must adhere to the dd.MM.yyyy HHmm format e.g. 9th October 2023 10.30am will be `09.10.2023 1030` +* For a single field, a Person must match at least one keyword to be returned as a result (i.e. `OR` search). + e.g. `John Doe` will return `John Lee`, `James Doe` +* If there are multiple fields specified, the Person must match at least one keyword in each field to be returned as a result (i.e. `AND` search). + e.g. `m/Shop Meet a/Mall` will return `Meeting: Shop at mall, Location:Mall` Examples: -* `findc John` returns `john` and `John Doe` -* `findc alex david` returns `Alex Yeoh`, `David Li`
- ![result for 'find alex david'](images/findAlexDavidResult.png) +* `findc n/alice` returns `Alice` and `alice tan` +* `findc p/51` returns `95163890` and `40351` +* `findc e/_@GMAIL` returns `alice_@gmail.com` +* `findc p/9 s/inactive claimant t/friend` returns persons with a `9` in their phone number, whose status is either `inactive` or `claimant`, and has a `friend` tag + ![result for 'findContact'](images/findContactResult.png) ## Meeting commands diff --git a/docs/images/findContactResult.png b/docs/images/findContactResult.png new file mode 100644 index 00000000000..ffb5f6ad841 Binary files /dev/null and b/docs/images/findContactResult.png differ diff --git a/docs/team/qz1004.md b/docs/team/qz1004.md index 2630c06a5d9..1a3f80b1d99 100644 --- a/docs/team/qz1004.md +++ b/docs/team/qz1004.md @@ -20,10 +20,11 @@ My contributions to the project are listed below. - **Enhancements to existing features**: - Add Status field to Person + - Improve `findc` to search for phone, email, status and tags in addition to name - **Documentation**: - - Update Developer Guide with the features that I implemented - - Update User Guide with the features that I implemented + - "Add attendee" feature in Developer Guide + - Modified the User Guide for `addc`, `findc` and `addmc` - **Community**: diff --git a/src/main/java/seedu/address/logic/commands/AddCommand.java b/src/main/java/seedu/address/logic/commands/AddCommand.java index 3f4cdb50bc4..ee0e98c0d1f 100644 --- a/src/main/java/seedu/address/logic/commands/AddCommand.java +++ b/src/main/java/seedu/address/logic/commands/AddCommand.java @@ -27,7 +27,7 @@ public class AddCommand extends Command { + PREFIX_NAME + "NAME " + PREFIX_PHONE + "PHONE " + PREFIX_EMAIL + "EMAIL " - + PREFIX_LASTTIME + "LAST CONTACTED TIME " + + "[" + PREFIX_LASTTIME + "LAST CONTACTED TIME] " + "[" + PREFIX_STATUS + "STATUS] " + "[" + PREFIX_REMARK + "REMARK] " + "[" + PREFIX_TAG + "TAG]...\n" diff --git a/src/main/java/seedu/address/logic/commands/FindCommand.java b/src/main/java/seedu/address/logic/commands/FindCommand.java index e471daf0e2e..386c3495f47 100644 --- a/src/main/java/seedu/address/logic/commands/FindCommand.java +++ b/src/main/java/seedu/address/logic/commands/FindCommand.java @@ -5,7 +5,7 @@ import seedu.address.commons.util.ToStringBuilder; import seedu.address.logic.Messages; import seedu.address.model.Model; -import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.address.model.person.GeneralPersonPredicate; /** * Finds and lists all persons in address book whose name contains any of the argument keywords. @@ -15,14 +15,20 @@ public class FindCommand extends Command { public static final String COMMAND_WORD = "findc"; - public static final String MESSAGE_USAGE = COMMAND_WORD + ": Finds all persons whose names contain any of " + public static final String MESSAGE_USAGE = COMMAND_WORD + + ": Finds all contacts whose specified fields (except remarks) contain any of " + "the specified keywords (case-insensitive) and displays them as a list with index numbers.\n" - + "Parameters: KEYWORD [MORE_KEYWORDS]...\n" - + "Example: " + COMMAND_WORD + " alice bob charlie"; + + "Parameters: n/KEYWORDS p/KEYWORDS e/KEYWORDS l/LOCALDATETIME s/KEYWORDS t/KEYWORDS\n" + + "Example: " + COMMAND_WORD + " n/alice l/10.10.2023 0900 s/Prospective t/Health"; - private final NameContainsKeywordsPredicate predicate; + private final GeneralPersonPredicate predicate; - public FindCommand(NameContainsKeywordsPredicate predicate) { + /** + * Constructs a FindCommand object. + * @param predicate The predicate that will be used by the FindCommand object. + */ + public FindCommand(GeneralPersonPredicate predicate) { + requireNonNull(predicate); this.predicate = predicate; } diff --git a/src/main/java/seedu/address/logic/parser/AddCommandParser.java b/src/main/java/seedu/address/logic/parser/AddCommandParser.java index 1161258301f..917fdf54a70 100644 --- a/src/main/java/seedu/address/logic/parser/AddCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/AddCommandParser.java @@ -16,7 +16,6 @@ import seedu.address.logic.commands.AddCommand; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.person.Email; -import seedu.address.model.person.LastContactedTime; import seedu.address.model.person.Name; import seedu.address.model.person.Person; import seedu.address.model.person.Phone; @@ -39,7 +38,7 @@ public AddCommand parse(String args) throws ParseException { ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_LASTTIME, PREFIX_STATUS, PREFIX_REMARK, PREFIX_TAG); - if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, PREFIX_LASTTIME) + if (!arePrefixesPresent(argMultimap, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL) || !argMultimap.getPreamble().isEmpty()) { throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, AddCommand.MESSAGE_USAGE)); } @@ -49,10 +48,7 @@ public AddCommand parse(String args) throws ParseException { Name name = ParserUtil.parseName(argMultimap.getValue(PREFIX_NAME).get()); Phone phone = ParserUtil.parsePhone(argMultimap.getValue(PREFIX_PHONE).get()); Email email = ParserUtil.parseEmail(argMultimap.getValue(PREFIX_EMAIL).get()); - LocalDateTime lastContactedTime = ParserUtil.parseContactTime(argMultimap.getValue(PREFIX_LASTTIME).get()); - if (!LastContactedTime.isValidLastContactedTime(lastContactedTime)) { - throw new ParseException(LastContactedTime.MESSAGE_CONSTRAINTS); - } + LocalDateTime lastContactedTime = ParserUtil.parseContactTime(argMultimap.getValue(PREFIX_LASTTIME).orElse("")); Status status = ParserUtil.parseStatus(argMultimap.getValue(PREFIX_STATUS).orElse("")); Remark remark = ParserUtil.parseRemark(argMultimap.getValue(PREFIX_REMARK).orElse("")); Set tagList = ParserUtil.parseTags(argMultimap.getAllValues(PREFIX_TAG)); diff --git a/src/main/java/seedu/address/logic/parser/FindCommandParser.java b/src/main/java/seedu/address/logic/parser/FindCommandParser.java index 2867bde857b..56829bbca15 100644 --- a/src/main/java/seedu/address/logic/parser/FindCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/FindCommandParser.java @@ -1,17 +1,28 @@ package seedu.address.logic.parser; import static seedu.address.logic.Messages.MESSAGE_INVALID_COMMAND_FORMAT; +import static seedu.address.logic.parser.CliSyntax.PREFIX_EMAIL; +import static seedu.address.logic.parser.CliSyntax.PREFIX_LASTTIME; +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_STATUS; +import static seedu.address.logic.parser.CliSyntax.PREFIX_TAG; -import java.util.Arrays; +import java.time.LocalDateTime; +import java.util.logging.Logger; +import seedu.address.Main; +import seedu.address.commons.core.LogsCenter; import seedu.address.logic.commands.FindCommand; import seedu.address.logic.parser.exceptions.ParseException; -import seedu.address.model.person.NameContainsKeywordsPredicate; +import seedu.address.model.person.GeneralPersonPredicate; +import seedu.address.model.person.LastContactedTime; /** * Parses input arguments and creates a new FindCommand object */ public class FindCommandParser implements Parser { + private static Logger logger = LogsCenter.getLogger(Main.class); /** * Parses the given {@code String} of arguments in the context of the FindCommand @@ -19,15 +30,43 @@ public class FindCommandParser implements Parser { * @throws ParseException if the user input does not conform the expected format */ public FindCommand parse(String args) throws ParseException { - String trimmedArgs = args.trim(); - if (trimmedArgs.isEmpty()) { - throw new ParseException( - String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); + logger.info("Begin FindCommand parse"); + assert args != null; + + ArgumentMultimap argMultimap = ArgumentTokenizer.tokenize(args, PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, + PREFIX_LASTTIME, PREFIX_STATUS, PREFIX_TAG); + if (!argMultimap.getPreamble().isEmpty()) { + throw new ParseException(String.format(MESSAGE_INVALID_COMMAND_FORMAT, FindCommand.MESSAGE_USAGE)); } + argMultimap.verifyNoDuplicatePrefixesFor(PREFIX_NAME, PREFIX_PHONE, PREFIX_EMAIL, + PREFIX_LASTTIME, PREFIX_STATUS, PREFIX_TAG); + + logger.info("Begin creation of Meeting predicates"); + String[] nameKeyWords = argMultimap.getValue(PREFIX_NAME).orElse("").split("\\s+"); + String[] phoneValues = argMultimap.getValue(PREFIX_PHONE).orElse("").split("\\s+"); + String[] emailKeyWords = argMultimap.getValue(PREFIX_EMAIL).orElse("").split("\\s+"); + String[] statusKeyWords = argMultimap.getValue(PREFIX_STATUS).orElse("").split("\\s+"); + String[] tagKeyWords = argMultimap.getValue(PREFIX_TAG).orElse("").split("\\s+"); + + LocalDateTime lastContacted = LocalDateTime.MIN; + if (argMultimap.getValue(PREFIX_LASTTIME).isPresent()) { + lastContacted = ParserUtil.parseContactTime(argMultimap.getValue(PREFIX_LASTTIME).get()); + if (!LastContactedTime.isValidLastContactedTime(lastContacted)) { + throw new ParseException(LastContactedTime.MESSAGE_CONSTRAINTS); + } + } + + GeneralPersonPredicate generalPersonPredicate = new GeneralPersonPredicate( + nameKeyWords, + phoneValues, + emailKeyWords, + lastContacted, + statusKeyWords, + tagKeyWords); - String[] nameKeywords = trimmedArgs.split("\\s+"); + logger.info("All Person predicates created"); - return new FindCommand(new NameContainsKeywordsPredicate(Arrays.asList(nameKeywords))); + return new FindCommand(generalPersonPredicate); } } diff --git a/src/main/java/seedu/address/logic/parser/FindMeetingCommandParser.java b/src/main/java/seedu/address/logic/parser/FindMeetingCommandParser.java index d2eefb72bfe..627ea6cff38 100644 --- a/src/main/java/seedu/address/logic/parser/FindMeetingCommandParser.java +++ b/src/main/java/seedu/address/logic/parser/FindMeetingCommandParser.java @@ -59,7 +59,6 @@ public FindMeetingCommand parse(String args) throws ParseException { String[] attendeeKeyWords = argMultimap.getValue(PREFIX_NAME).orElse("").split("\\s+"); String[] tagKeyWords = argMultimap.getValue(PREFIX_TAG).orElse("").split("\\s+"); - GeneralMeetingPredicate generalMeetingPredicate = new GeneralMeetingPredicate( titleKeyWords, locationKeyWords, diff --git a/src/main/java/seedu/address/logic/parser/ParserUtil.java b/src/main/java/seedu/address/logic/parser/ParserUtil.java index 2607a071c0e..cb16e8339e9 100644 --- a/src/main/java/seedu/address/logic/parser/ParserUtil.java +++ b/src/main/java/seedu/address/logic/parser/ParserUtil.java @@ -155,8 +155,16 @@ public static Email parseEmail(String email) throws ParseException { public static LocalDateTime parseContactTime(String time) throws ParseException { requireNonNull(time); String trimmedStart = time.trim(); + //set last contacted to LocalDateTime.MIN if last contacted field is not specified + if (trimmedStart.isEmpty()) { + return LocalDateTime.MIN; + } try { - return LocalDateTime.parse(trimmedStart, FORMAT); + LocalDateTime preppedTime = LocalDateTime.parse(trimmedStart, FORMAT); + if (!LastContactedTime.isValidLastContactedTime(preppedTime)) { + throw new ParseException(LastContactedTime.MESSAGE_CONSTRAINTS); + } + return preppedTime; } catch (DateTimeParseException e) { throw new ParseException(LastContactedTime.MESSAGE_CONSTRAINTS); } diff --git a/src/main/java/seedu/address/model/meeting/GeneralMeetingPredicate.java b/src/main/java/seedu/address/model/meeting/GeneralMeetingPredicate.java index b8b6086bff9..f8962347086 100644 --- a/src/main/java/seedu/address/model/meeting/GeneralMeetingPredicate.java +++ b/src/main/java/seedu/address/model/meeting/GeneralMeetingPredicate.java @@ -4,18 +4,15 @@ import java.util.Arrays; import java.util.function.Predicate; -import seedu.address.model.tag.TagContainsKeywordsPredicate; - - /** - * The predicate class that brings together of all the other predicate class. + * The predicate class that brings together of all the other predicate class for Meeting. */ public class GeneralMeetingPredicate implements Predicate { private final TitleContainsKeywordsPredicate titlePredicate; private final LocationContainsKeywordsPredicate locationPredicate; private final MeetingTimeContainsPredicate meetingTimePredicate; private final AttendeeContainsKeywordsPredicate attendeePredicate; - private final TagContainsKeywordsPredicate tagPredicate; + private final MeetingTagContainsKeywordsPredicate tagPredicate; /** * Constructs a predicate class that fulfills all the argument predicates @@ -29,7 +26,7 @@ public GeneralMeetingPredicate(TitleContainsKeywordsPredicate titlePredicate, LocationContainsKeywordsPredicate locationPredicate, MeetingTimeContainsPredicate meetingTimePredicate, AttendeeContainsKeywordsPredicate attendeePredicate, - TagContainsKeywordsPredicate tagPredicate) { + MeetingTagContainsKeywordsPredicate tagPredicate) { this.titlePredicate = titlePredicate; this.locationPredicate = locationPredicate; this.meetingTimePredicate = meetingTimePredicate; @@ -52,7 +49,7 @@ public GeneralMeetingPredicate(String[] titleKeyWords, String[] locationKeyWords this.locationPredicate = new LocationContainsKeywordsPredicate(Arrays.asList(locationKeyWords)); this.meetingTimePredicate = new MeetingTimeContainsPredicate(start, end); this.attendeePredicate = new AttendeeContainsKeywordsPredicate(Arrays.asList(attendeeKeyWords)); - this.tagPredicate = new TagContainsKeywordsPredicate(Arrays.asList(tagKeyWords)); + this.tagPredicate = new MeetingTagContainsKeywordsPredicate(Arrays.asList(tagKeyWords)); } @Override diff --git a/src/main/java/seedu/address/model/tag/TagContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/meeting/MeetingTagContainsKeywordsPredicate.java similarity index 66% rename from src/main/java/seedu/address/model/tag/TagContainsKeywordsPredicate.java rename to src/main/java/seedu/address/model/meeting/MeetingTagContainsKeywordsPredicate.java index 1c6499f13fb..9e9a06424d1 100644 --- a/src/main/java/seedu/address/model/tag/TagContainsKeywordsPredicate.java +++ b/src/main/java/seedu/address/model/meeting/MeetingTagContainsKeywordsPredicate.java @@ -1,19 +1,18 @@ -package seedu.address.model.tag; +package seedu.address.model.meeting; import java.util.List; import java.util.function.Predicate; import seedu.address.commons.util.StringUtil; import seedu.address.commons.util.ToStringBuilder; -import seedu.address.model.meeting.Meeting; /** - * Tests that a {@code Meetings}'s {@code Title} matches any of the keywords given. + * Tests that a {@code Meeting}'s {@code Tag} matches any of the keywords given. */ -public class TagContainsKeywordsPredicate implements Predicate { +public class MeetingTagContainsKeywordsPredicate implements Predicate { private final List keywords; - public TagContainsKeywordsPredicate(List keywords) { + public MeetingTagContainsKeywordsPredicate(List keywords) { this.keywords = keywords; } @@ -31,11 +30,12 @@ public boolean equals(Object other) { } // instanceof handles nulls - if (!(other instanceof TagContainsKeywordsPredicate)) { + if (!(other instanceof MeetingTagContainsKeywordsPredicate)) { return false; } - TagContainsKeywordsPredicate otherTagContainsKeywordsPredicate = (TagContainsKeywordsPredicate) other; + MeetingTagContainsKeywordsPredicate otherTagContainsKeywordsPredicate = + (MeetingTagContainsKeywordsPredicate) other; return keywords.equals(otherTagContainsKeywordsPredicate.keywords); } diff --git a/src/main/java/seedu/address/model/meeting/MeetingTimeContainsPredicate.java b/src/main/java/seedu/address/model/meeting/MeetingTimeContainsPredicate.java index b5aa1c091fd..b6f60fb7772 100644 --- a/src/main/java/seedu/address/model/meeting/MeetingTimeContainsPredicate.java +++ b/src/main/java/seedu/address/model/meeting/MeetingTimeContainsPredicate.java @@ -8,7 +8,7 @@ import seedu.address.commons.util.ToStringBuilder; /** - * Tests that a {@code Meetings}'s {@code MeetingTime} duration within the given start and end. + * Tests that a {@code Meeting}'s {@code MeetingTime} duration within the given start and end. */ public class MeetingTimeContainsPredicate implements Predicate { private final LocalDateTime start; diff --git a/src/main/java/seedu/address/model/person/EmailContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/EmailContainsKeywordsPredicate.java new file mode 100644 index 00000000000..77488cba4f6 --- /dev/null +++ b/src/main/java/seedu/address/model/person/EmailContainsKeywordsPredicate.java @@ -0,0 +1,47 @@ +package seedu.address.model.person; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.ToStringBuilder; + +/** + * Tests that a {@code Person}'s {@code Email} matches any of the keywords given. + */ +public class EmailContainsKeywordsPredicate implements Predicate { + private final List keywords; + + public EmailContainsKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Person person) { + String email = person.getEmail().toString(); + Boolean result = false; + for (String keyword : keywords) { + result |= email.contains(keyword.toLowerCase()); + } + return result; + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof EmailContainsKeywordsPredicate)) { + return false; + } + + EmailContainsKeywordsPredicate otherEmailContainsKeywordsPredicate = (EmailContainsKeywordsPredicate) other; + return keywords.equals(otherEmailContainsKeywordsPredicate.keywords); + } + + @Override + public String toString() { + return new ToStringBuilder(this).add("keywords", keywords).toString(); + } +} diff --git a/src/main/java/seedu/address/model/person/GeneralPersonPredicate.java b/src/main/java/seedu/address/model/person/GeneralPersonPredicate.java new file mode 100644 index 00000000000..aaf660d263e --- /dev/null +++ b/src/main/java/seedu/address/model/person/GeneralPersonPredicate.java @@ -0,0 +1,89 @@ +package seedu.address.model.person; + +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.function.Predicate; + +/** + * The predicate class that brings together of all the other predicate class for Person. + */ +public class GeneralPersonPredicate implements Predicate { + private final NameContainsKeywordsPredicate namePredicate; + private final PhoneContainsPredicate phonePredicate; + private final EmailContainsKeywordsPredicate emailPredicate; + private final LastContactTimeContainsPredicate lastContactTimePredicate; + private final StatusContainsKeywordsPredicate statusPredicate; + private final PersonTagContainsKeywordsPredicate tagPredicate; + + /** + * Constructs a predicate class that fulfills all the argument predicates + * @param namePredicate A predicate that test a person's name. + * @param phonePredicate A predicate that test a person's phone. + * @param emailPredicate A predicate that test a person's email. + * @param lastContactTimePredicate A predicate that test a person's last contacted time. + * @param statusPredicate A predicate that test a person's status. + * @param tagPredicate A predicate that test a person's tags. + */ + public GeneralPersonPredicate(NameContainsKeywordsPredicate namePredicate, + PhoneContainsPredicate phonePredicate, + EmailContainsKeywordsPredicate emailPredicate, + LastContactTimeContainsPredicate lastContactTimePredicate, + StatusContainsKeywordsPredicate statusPredicate, + PersonTagContainsKeywordsPredicate tagPredicate) { + this.namePredicate = namePredicate; + this.phonePredicate = phonePredicate; + this.emailPredicate = emailPredicate; + this.lastContactTimePredicate = lastContactTimePredicate; + this.statusPredicate = statusPredicate; + this.tagPredicate = tagPredicate; + } + + /** + * Constructs a predicate class that fulfills all the argument predicates + * @param nameKeyWords String array that will be used to construct NameContainsKeywordsPredicate + * @param phoneValues String array that will be used to construct PhoneContainsPredicate + * @param emailKeyWords String array that will be used to construct EmailContainsKeywordsPredicate + * @param lastContacted A predicate that wil be used to construct LastContactTimeContainsPredicate. + * @param statusKeyWords String array that will be used to construct StatusContainsKeywordsPredicate + * @param tagKeyWords String array that will be used to construct PersonTagContainsKeywordsPredicate + */ + public GeneralPersonPredicate(String[] nameKeyWords, String[] phoneValues, String[] emailKeyWords, + LocalDateTime lastContacted, String[] statusKeyWords, String[] tagKeyWords) { + this.namePredicate = new NameContainsKeywordsPredicate(Arrays.asList(nameKeyWords)); + this.phonePredicate = new PhoneContainsPredicate(Arrays.asList(phoneValues)); + this.emailPredicate = new EmailContainsKeywordsPredicate(Arrays.asList(emailKeyWords)); + this.lastContactTimePredicate = new LastContactTimeContainsPredicate(lastContacted); + this.statusPredicate = new StatusContainsKeywordsPredicate(Arrays.asList(statusKeyWords)); + this.tagPredicate = new PersonTagContainsKeywordsPredicate(Arrays.asList(tagKeyWords)); + } + + @Override + public boolean test(Person person) { + return namePredicate.test(person) + && phonePredicate.test(person) + && emailPredicate.test(person) + && lastContactTimePredicate.test(person) + && statusPredicate.test(person) + && tagPredicate.test(person); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof GeneralPersonPredicate)) { + return false; + } + + GeneralPersonPredicate otherGeneralPersonPredicate = (GeneralPersonPredicate) other; + return namePredicate.equals(otherGeneralPersonPredicate.namePredicate) + && phonePredicate.equals(otherGeneralPersonPredicate.phonePredicate) + && emailPredicate.equals(otherGeneralPersonPredicate.emailPredicate) + && lastContactTimePredicate.equals(otherGeneralPersonPredicate.lastContactTimePredicate) + && statusPredicate.equals(otherGeneralPersonPredicate.statusPredicate) + && tagPredicate.equals(otherGeneralPersonPredicate.tagPredicate); + } +} diff --git a/src/main/java/seedu/address/model/person/LastContactTimeContainsPredicate.java b/src/main/java/seedu/address/model/person/LastContactTimeContainsPredicate.java new file mode 100644 index 00000000000..e8c8d958bbd --- /dev/null +++ b/src/main/java/seedu/address/model/person/LastContactTimeContainsPredicate.java @@ -0,0 +1,54 @@ +package seedu.address.model.person; + +import static seedu.address.logic.parser.ParserUtil.FORMAT; + +import java.time.LocalDateTime; +import java.util.function.Predicate; + +import seedu.address.commons.util.ToStringBuilder; + +/** + * Tests that a {@code Person}'s {@code LastContactedTime} matches the given time. + */ +public class LastContactTimeContainsPredicate implements Predicate { + private final LocalDateTime time; + + /** + * Constructs a predicate with the given time. + * @param time time to be checked. + */ + public LastContactTimeContainsPredicate(LocalDateTime time) { + this.time = time; + this.time.format(FORMAT); + LastContactedTime.isValidLastContactedTime(time); + } + + @Override + public boolean test(Person person) { + if (time.equals(LocalDateTime.MIN)) { + return true; + } + return person.getLastContactedTime().equals(time); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof LastContactTimeContainsPredicate)) { + return false; + } + + LastContactTimeContainsPredicate otherLastContactTimeContainsPredicate = + (LastContactTimeContainsPredicate) other; + return this.time.equals(otherLastContactTimeContainsPredicate.time); + } + + @Override + public String toString() { + return new ToStringBuilder(this).add("time", time).toString(); + } +} diff --git a/src/main/java/seedu/address/model/person/LastContactedTime.java b/src/main/java/seedu/address/model/person/LastContactedTime.java index 5f2c9f4f7cd..cccd4273b12 100644 --- a/src/main/java/seedu/address/model/person/LastContactedTime.java +++ b/src/main/java/seedu/address/model/person/LastContactedTime.java @@ -40,10 +40,8 @@ public static String toDisplayFormat(LastContactedTime dateTime) { * Returns true if a given LocalDateTime input is valid. */ public static boolean isValidLastContactedTime(LocalDateTime input) { - if (input == null) { - return false; - } - return true; + return input.isEqual(LocalDateTime.MIN) + || input.isAfter(LocalDateTime.MIN) && input.isBefore(LocalDateTime.MAX); } @Override diff --git a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java index 62d19be2977..a7775f9a3be 100644 --- a/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java +++ b/src/main/java/seedu/address/model/person/NameContainsKeywordsPredicate.java @@ -19,7 +19,8 @@ public NameContainsKeywordsPredicate(List keywords) { @Override public boolean test(Person person) { return keywords.stream() - .anyMatch(keyword -> StringUtil.containsWordIgnoreCase(person.getName().fullName, keyword)); + .anyMatch(keyword -> keyword.isEmpty() + || StringUtil.containsWordIgnoreCase(person.getName().toString(), keyword)); } @Override diff --git a/src/main/java/seedu/address/model/person/Person.java b/src/main/java/seedu/address/model/person/Person.java index 8267c6dd1af..4681118138e 100644 --- a/src/main/java/seedu/address/model/person/Person.java +++ b/src/main/java/seedu/address/model/person/Person.java @@ -33,7 +33,7 @@ public class Person { */ public Person(Name name, Phone phone, Email email, LocalDateTime time, Status status, Remark remark, Set tags) { - requireAllNonNull(name, phone, email, status, remark, tags); + requireAllNonNull(name, phone, email, time, status, remark, tags); this.name = name; this.phone = phone; this.email = email; diff --git a/src/main/java/seedu/address/model/person/PersonTagContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/PersonTagContainsKeywordsPredicate.java new file mode 100644 index 00000000000..9127aa2e7e7 --- /dev/null +++ b/src/main/java/seedu/address/model/person/PersonTagContainsKeywordsPredicate.java @@ -0,0 +1,46 @@ +package seedu.address.model.person; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.StringUtil; +import seedu.address.commons.util.ToStringBuilder; + +/** + * Tests that a {@code Person}'s {@code Tag} matches any of the keywords given. + */ +public class PersonTagContainsKeywordsPredicate implements Predicate { + private final List keywords; + + public PersonTagContainsKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Person person) { + return keywords.stream() + .anyMatch(keyword -> keyword.isEmpty() || person.getTags().stream() + .anyMatch(tag -> StringUtil.containsWordIgnoreCase(tag.tagName, keyword))); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof PersonTagContainsKeywordsPredicate)) { + return false; + } + + PersonTagContainsKeywordsPredicate otherTagContainsKeywordsPredicate = + (PersonTagContainsKeywordsPredicate) other; + return keywords.equals(otherTagContainsKeywordsPredicate.keywords); + } + + @Override + public String toString() { + return new ToStringBuilder(this).add("keywords", keywords).toString(); + } +} diff --git a/src/main/java/seedu/address/model/person/PhoneContainsPredicate.java b/src/main/java/seedu/address/model/person/PhoneContainsPredicate.java new file mode 100644 index 00000000000..34d66a69ae2 --- /dev/null +++ b/src/main/java/seedu/address/model/person/PhoneContainsPredicate.java @@ -0,0 +1,44 @@ +package seedu.address.model.person; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.ToStringBuilder; + +/** + * Tests that a {@code Person}'s {@code Phone} matches any of the values given. + */ +public class PhoneContainsPredicate implements Predicate { + private final List values; + + public PhoneContainsPredicate(List values) { + this.values = values; + } + + @Override + public boolean test(Person person) { + return values.stream() + .anyMatch(value -> value.isEmpty() + || person.getPhone().toString().contains(value)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof PhoneContainsPredicate)) { + return false; + } + + PhoneContainsPredicate otherPhoneContainsPredicate = (PhoneContainsPredicate) other; + return values.equals(otherPhoneContainsPredicate.values); + } + + @Override + public String toString() { + return new ToStringBuilder(this).add("values", values).toString(); + } +} diff --git a/src/main/java/seedu/address/model/person/StatusContainsKeywordsPredicate.java b/src/main/java/seedu/address/model/person/StatusContainsKeywordsPredicate.java new file mode 100644 index 00000000000..cf6c7499e2f --- /dev/null +++ b/src/main/java/seedu/address/model/person/StatusContainsKeywordsPredicate.java @@ -0,0 +1,46 @@ +package seedu.address.model.person; + +import java.util.List; +import java.util.function.Predicate; + +import seedu.address.commons.util.StringUtil; +import seedu.address.commons.util.ToStringBuilder; + +/** + * Tests that a {@code Person}'s {@code Status} duration within the given start and end. + */ +public class StatusContainsKeywordsPredicate implements Predicate { + private final List keywords; + + public StatusContainsKeywordsPredicate(List keywords) { + this.keywords = keywords; + } + + @Override + public boolean test(Person person) { + return keywords.stream() + .anyMatch(keyword -> keyword.isEmpty() + || StringUtil.containsWordIgnoreCase(person.getStatus().toString(), keyword)); + } + + @Override + public boolean equals(Object other) { + if (other == this) { + return true; + } + + // instanceof handles nulls + if (!(other instanceof StatusContainsKeywordsPredicate)) { + return false; + } + + StatusContainsKeywordsPredicate otherStatusContainsKeywordsPredicate = (StatusContainsKeywordsPredicate) other; + return keywords.equals(otherStatusContainsKeywordsPredicate.keywords); + } + + @Override + public String toString() { + return new ToStringBuilder(this).add("keywords", keywords).toString(); + } + +} diff --git a/src/main/java/seedu/address/ui/PersonCard.java b/src/main/java/seedu/address/ui/PersonCard.java index c7dcad41e27..b7410a80be0 100644 --- a/src/main/java/seedu/address/ui/PersonCard.java +++ b/src/main/java/seedu/address/ui/PersonCard.java @@ -39,6 +39,8 @@ public class PersonCard extends UiPart { @FXML private Label email; @FXML + private Label status; + @FXML private Label lastContactedTime; @FXML private FlowPane tags; @@ -54,7 +56,10 @@ public PersonCard(Person person, int displayedIndex) { phone.setText(person.getPhone().value); email.setText(person.getEmail().value); LocalDateTime time = person.getLastContactedTime(); - lastContactedTime.setText(time.format(DateTimeFormatter.ofPattern("dd.MM.yyyy HHmm"))); + lastContactedTime.setText(time.isEqual(LocalDateTime.MIN) + ? "Not contacted yet" + : time.format(DateTimeFormatter.ofPattern("dd.MM.yyyy HHmm"))); + status.setText(person.getStatus().value); person.getTags().stream() .sorted(Comparator.comparing(tag -> tag.tagName)) .forEach(tag -> tags.getChildren().add(new Label(tag.tagName))); diff --git a/src/main/resources/view/DarkTheme.css b/src/main/resources/view/DarkTheme.css index b2bc9a2bca6..e35618c2c2d 100644 --- a/src/main/resources/view/DarkTheme.css +++ b/src/main/resources/view/DarkTheme.css @@ -132,6 +132,15 @@ -fx-text-fill: #010504; } +.cell_status_label { + -fx-text-fill: white; + -fx-background-color: #70798c; + -fx-padding: 1 3 1 3; + -fx-border-radius: 2; + -fx-background-radius: 2; + -fx-font-size: 11; +} + .stack-pane { -fx-background-color: derive(#1d1d1d, 20%); } diff --git a/src/main/resources/view/PersonListCard.fxml b/src/main/resources/view/PersonListCard.fxml index 5c4cb11a5f0..948411f6ea5 100644 --- a/src/main/resources/view/PersonListCard.fxml +++ b/src/main/resources/view/PersonListCard.fxml @@ -29,6 +29,7 @@