diff --git a/src/main/java/seedu/address/logic/Logic.java b/src/main/java/seedu/address/logic/Logic.java index 73a95c1bca9..45e8f58fbc0 100644 --- a/src/main/java/seedu/address/logic/Logic.java +++ b/src/main/java/seedu/address/logic/Logic.java @@ -10,7 +10,7 @@ import seedu.address.logic.commands.exceptions.CommandException; import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Analytics; +import seedu.address.model.analytics.DashboardData; import seedu.address.model.person.Loan; import seedu.address.model.person.Person; @@ -67,5 +67,5 @@ public interface Logic { void setToPersonTab(); - ObjectProperty getAnalytics(); + ObjectProperty getAnalytics(); } diff --git a/src/main/java/seedu/address/logic/LogicManager.java b/src/main/java/seedu/address/logic/LogicManager.java index f97a2eb7efe..66dfdeff5ff 100644 --- a/src/main/java/seedu/address/logic/LogicManager.java +++ b/src/main/java/seedu/address/logic/LogicManager.java @@ -17,7 +17,7 @@ import seedu.address.logic.parser.exceptions.ParseException; import seedu.address.model.Model; import seedu.address.model.ReadOnlyAddressBook; -import seedu.address.model.person.Analytics; +import seedu.address.model.analytics.DashboardData; import seedu.address.model.person.Loan; import seedu.address.model.person.Person; import seedu.address.storage.Storage; @@ -106,8 +106,8 @@ public BooleanProperty getIsLoansTab() { } @Override - public ObjectProperty getAnalytics() { - return model.getAnalytics(); + public ObjectProperty getAnalytics() { + return model.getDashboardData(); } @Override diff --git a/src/main/java/seedu/address/logic/commands/AnalyticsCommand.java b/src/main/java/seedu/address/logic/commands/AnalyticsCommand.java index e9d0c199962..f88d7739bbf 100644 --- a/src/main/java/seedu/address/logic/commands/AnalyticsCommand.java +++ b/src/main/java/seedu/address/logic/commands/AnalyticsCommand.java @@ -35,12 +35,13 @@ public CommandResult execute(Model model) throws CommandException { } Person targetPerson = lastShownList.get(targetIndex.getZeroBased()); - model.updateFilteredLoanList(loan -> loan.isAssignedTo(targetPerson) && loan.isActive()); + model.updateFilteredLoanList(loan -> loan.isAssignedTo(targetPerson)); Analytics targetAnalytics = Analytics.getAnalytics(model.getSortedLoanList()); - // TODO: Implement analytics GUI display logic - model.setAnalytics(targetAnalytics); + + model.generateDashboardData(targetAnalytics); model.setIsAnalyticsTab(true); - return new CommandResult(MESSAGE_SUCCESS + model.getAnalytics().getValue(), + + return new CommandResult(MESSAGE_SUCCESS + " for " + targetPerson.getName() + " ", false, false, false); } diff --git a/src/main/java/seedu/address/model/Model.java b/src/main/java/seedu/address/model/Model.java index 4c7541b96d6..aa7c614ca48 100644 --- a/src/main/java/seedu/address/model/Model.java +++ b/src/main/java/seedu/address/model/Model.java @@ -9,6 +9,7 @@ import javafx.collections.ObservableList; import seedu.address.commons.core.GuiSettings; import seedu.address.logic.commands.LinkLoanCommand; +import seedu.address.model.analytics.DashboardData; import seedu.address.model.person.Analytics; import seedu.address.model.person.Loan; import seedu.address.model.person.Person; @@ -150,11 +151,9 @@ public interface Model { void markLoan(Loan loanToMark); - void unmarkLoan(Loan loanToUnmark); - - void setAnalytics(Analytics analytics); - - ObjectProperty getAnalytics(); + void generateDashboardData(Analytics analytics); + void unmarkLoan(Loan loanToUnmark); + ObjectProperty getDashboardData(); } diff --git a/src/main/java/seedu/address/model/ModelManager.java b/src/main/java/seedu/address/model/ModelManager.java index 3139f9f80a0..c41c5d4f517 100644 --- a/src/main/java/seedu/address/model/ModelManager.java +++ b/src/main/java/seedu/address/model/ModelManager.java @@ -4,6 +4,7 @@ import static seedu.address.commons.util.CollectionUtil.requireAllNonNull; import java.nio.file.Path; +import java.util.Date; import java.util.List; import java.util.function.Predicate; import java.util.logging.Logger; @@ -18,6 +19,7 @@ import seedu.address.commons.core.GuiSettings; import seedu.address.commons.core.LogsCenter; import seedu.address.logic.commands.LinkLoanCommand; +import seedu.address.model.analytics.DashboardData; import seedu.address.model.person.Analytics; import seedu.address.model.person.Loan; import seedu.address.model.person.Person; @@ -35,7 +37,7 @@ public class ModelManager implements Model { private final SortedList sortedLoans; private final BooleanProperty isLoansTab = new SimpleBooleanProperty(false); private final BooleanProperty isAnalyticsTab = new SimpleBooleanProperty(false); - private final ObjectProperty targetAnalytics = new SimpleObjectProperty<>(); + private final ObjectProperty dashboardData = new SimpleObjectProperty<>(); /** * Initializes a ModelManager with the given addressBook and userPrefs. @@ -50,7 +52,7 @@ public ModelManager(ReadOnlyAddressBook addressBook, ReadOnlyUserPrefs userPrefs filteredPersons = new FilteredList<>(this.addressBook.getPersonList()); filteredLoans = new FilteredList<>(this.addressBook.getLoanList()); sortedLoans = new SortedList<>(filteredLoans, Loan::compareTo); - targetAnalytics.setValue(null); + dashboardData.setValue(null); } public ModelManager() { @@ -244,13 +246,14 @@ public void setIsAnalyticsTab(Boolean isAnalyticsTab) { } @Override - public ObjectProperty getAnalytics() { - return targetAnalytics; + public ObjectProperty getDashboardData() { + return dashboardData; } @Override - public void setAnalytics(Analytics analytics) { - targetAnalytics.setValue(analytics); + public void generateDashboardData(Analytics analytics) { + float impactBenchmark = this.addressBook.getUniqueLoanList().getMaxLoanValue(); + Date urgencyBenchmark = this.addressBook.getUniqueLoanList().getEarliestReturnDate(); + dashboardData.setValue(new DashboardData(analytics, impactBenchmark, urgencyBenchmark)); } - } diff --git a/src/main/java/seedu/address/model/analytics/DashboardData.java b/src/main/java/seedu/address/model/analytics/DashboardData.java new file mode 100644 index 00000000000..604205d7abc --- /dev/null +++ b/src/main/java/seedu/address/model/analytics/DashboardData.java @@ -0,0 +1,77 @@ +package seedu.address.model.analytics; + +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.Date; + +import seedu.address.model.person.Analytics; + + +/** + * Represents the analytics data of the dashboard with 3 values + * - Analytics object to be displayed + * - Max loan value + * - Earliest return date + */ +public class DashboardData { + private Analytics analytics; + private float maxLoanValue; + private Date earliestReturnDate; + + /** + * Creates a DashboardData object with the given analytics, max loan value and earliest return date + * + * @param analytics analytics object to be displayed + * @param maxLoanValue maximum loan value of all loans + * @param earliestReturnDate earliest return date of all loans (not returned and not overdue) + */ + public DashboardData(Analytics analytics, float maxLoanValue, Date earliestReturnDate) { + this.analytics = analytics; + this.maxLoanValue = maxLoanValue; + this.earliestReturnDate = earliestReturnDate; + } + + public Analytics getAnalytics() { + return analytics; + } + + public float getMaxLoanValue() { + return maxLoanValue; + } + + /** + * Calculates the impact index of the dashboard data + * Impact index is calculated as the ratio of the average loan value to the maximum loan value + * + * @return impact index between 0 and 1 + */ + public float getImpactIndex() { + return analytics.getAverageLoanValue() / maxLoanValue; + } + + /** + * Calculates the urgency index of the dashboard data + * Urgency index is calculated as the ratio of the number of days between the earliest return date and the current + * to the number of days between the earliest return date and the benchmark date + * + * @return urgency index between 0 and 1 + */ + public Float getUrgencyIndex() { + // Should take extra measures to ensure no overdue loans are used for calculations + if (analytics.getEarliestReturnDate() == null || earliestReturnDate == null) { + return null; + } + LocalDate target = analytics.getEarliestReturnDate().toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + LocalDate benchmark = this.earliestReturnDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + LocalDate now = LocalDate.now(); + long dayDiffBenchmark = benchmark.toEpochDay() - now.toEpochDay(); + long dayDiffTarget = target.toEpochDay() - now.toEpochDay(); + return (float) dayDiffBenchmark / dayDiffTarget; + } + + @Override + public String toString() { + return "Analytics: " + analytics + ", Max Loan Value: " + maxLoanValue + ", Earliest Return Date: " + + earliestReturnDate; + } +} diff --git a/src/main/java/seedu/address/model/person/UniqueLoanList.java b/src/main/java/seedu/address/model/person/UniqueLoanList.java index c666a28189e..86726bea839 100644 --- a/src/main/java/seedu/address/model/person/UniqueLoanList.java +++ b/src/main/java/seedu/address/model/person/UniqueLoanList.java @@ -40,6 +40,7 @@ public boolean contains(Loan toCheck) { /** * Adds a loan to the list of loans. + * * @param loan A valid loan. */ public void addLoan(Loan loan) { @@ -50,6 +51,7 @@ public void addLoan(Loan loan) { /** * Adds a loan to the list of loans. + * * @param value A valid value. * @param startDate A valid start date. * @param returnDate A valid return date. @@ -62,6 +64,7 @@ public Loan addLoan(float value, Date startDate, Date returnDate, Person assigne /** * Adds a loan to the list of loans. + * * @param loanDescription A valid LinkLoanDescriptor, which contains details about the loan to be added. */ public Loan addLoan(LinkLoanDescriptor loanDescription, Person assignee) { @@ -73,6 +76,7 @@ public Loan addLoan(LinkLoanDescriptor loanDescription, Person assignee) { /** * Adds a loan to the list of loans. + * * @param value A valid value. * @param startDate A valid start date. * @param returnDate A valid return date. @@ -91,6 +95,7 @@ public void addLoan(float value, String startDate, String returnDate, Person ass /** * Removes a loan from the list of loans. + * * @param toRemove A valid loan. */ public void removeLoan(Loan toRemove) { @@ -147,6 +152,7 @@ public Loan getLoanById(int id) { /** * Marks a loan as returned. + * * @param idx A valid index. */ public void markLoanAsReturned(int idx) { @@ -155,6 +161,7 @@ public void markLoanAsReturned(int idx) { /** * Marks a loan as returned. + * * @param id A valid id. */ public void markLoanAsReturnedById(int id) { @@ -166,6 +173,7 @@ public void markLoanAsReturnedById(int id) { /** * Marks a loan as not returned. + * * @param loanToMark A valid loan. */ public void markLoan(Loan loanToMark) { @@ -235,6 +243,7 @@ public void unmarkLoan(int idx) { public int size() { return internalList.size(); } + @Override public String toString() { String output = "Loans:\n"; @@ -291,6 +300,7 @@ private boolean loansAreUnique(List loans) { /** * Removes all loans attached to a person. + * * @param key A valid person. */ public void removeLoansAttachedTo(Person key) { @@ -300,6 +310,7 @@ public void removeLoansAttachedTo(Person key) { /** * Modifies the assignee of all loans attached to a person. + * * @param target A valid person. * @param editedPerson A valid person. */ @@ -315,4 +326,36 @@ public void modifyLoanAssignee(Person target, Person editedPerson) { internalList.set(0, internalList.get(0)); } } + + /** + * Returns the maximum loan value of all loans. + * + * @return The maximum loan value of all loans. + */ + public int getMaxLoanValue() { + int maxLoanValue = 0; + for (Loan loan : internalList) { + if (loan.getValue() > maxLoanValue) { + maxLoanValue = (int) loan.getValue(); + } + } + return maxLoanValue; + } + + /** + * Returns the earliest return date of all loans. + * The loan must not be overdue and must not have been returned. + * + * @return The earliest return date of all loans. Returns null if there are no loans that meet the criteria. + */ + public Date getEarliestReturnDate() { + Date earliestReturnDate = null; + for (Loan loan : internalList) { + if ((earliestReturnDate == null || loan.getReturnDate().before(earliestReturnDate)) + && !loan.getReturnDate().before(new Date()) && !loan.isReturned()) { + earliestReturnDate = loan.getReturnDate(); + } + } + return earliestReturnDate; + } } diff --git a/src/main/java/seedu/address/ui/AnalyticsPanel.java b/src/main/java/seedu/address/ui/AnalyticsPanel.java index b1c2d96ba24..a25810b07b3 100644 --- a/src/main/java/seedu/address/ui/AnalyticsPanel.java +++ b/src/main/java/seedu/address/ui/AnalyticsPanel.java @@ -5,9 +5,12 @@ import javafx.collections.ObservableList; import javafx.fxml.FXML; import javafx.scene.chart.PieChart; +import javafx.scene.control.Label; import javafx.scene.layout.Region; +import seedu.address.model.analytics.DashboardData; import seedu.address.model.person.Analytics; + /** * Panel containing the analytics of the loan records. */ @@ -15,24 +18,86 @@ public class AnalyticsPanel extends UiPart { private static final String FXML = "AnalyticsPanel.fxml"; @FXML - private PieChart pieChart; + private PieChart reliabilityChart; + + @FXML + private PieChart impactChart; + + @FXML + private PieChart urgencyChart; + + @FXML + private Label reliabilityIndex; + + @FXML + private Label impactIndex; + + @FXML + private Label urgencyIndex; /** * Creates a {@code AnalyticsPanel} with the given {@code ObjectProperty}. */ - public AnalyticsPanel(ObjectProperty analytics) { + public AnalyticsPanel(ObjectProperty dashboardData) { super(FXML); - pieChart.setData(FXCollections.observableArrayList()); - analytics.addListener((observable, oldValue, newValue) -> { + initializeCharts(); + reliabilityChart.setData(FXCollections.observableArrayList()); + dashboardData.addListener((observable, oldValue, newValue) -> { updateChart(newValue); }); } - private void updateChart(Analytics analytics) { - ObservableList pieChartData = FXCollections.observableArrayList( - new PieChart.Data("Active Loans", analytics.getNumActiveLoans()), - new PieChart.Data("Overdue Loans", analytics.getNumOverdueLoans()) - ); - pieChart.setData(pieChartData); + private void initializeCharts() { + reliabilityChart.setStartAngle(90); + impactChart.setStartAngle(90); + urgencyChart.setStartAngle(90); + reliabilityChart.setLabelsVisible(false); + impactChart.setLabelsVisible(false); + urgencyChart.setLabelsVisible(false); + reliabilityChart.setLegendVisible(false); + impactChart.setLegendVisible(false); + urgencyChart.setLegendVisible(false); + } + + private void updateChart(DashboardData data) { + Analytics analytics = data.getAnalytics(); + if (analytics.getNumActiveLoans() == 0) { + reliabilityIndex.setText("No active loans to analyze"); + reliabilityChart.setVisible(false); + } else { + reliabilityChart.setVisible(true); + ObservableList reliabilityData = FXCollections.observableArrayList( + new PieChart.Data("Reliability Index", analytics.getPropOverdueLoans()), + new PieChart.Data("", 1 - analytics.getPropOverdueLoans()) + ); + reliabilityChart.setData(reliabilityData); + reliabilityIndex.setText(String.format("%.2f", (1 - analytics.getPropOverdueLoans()) * 100) + "%"); + } + + + if (data.getMaxLoanValue() == 0) { + impactIndex.setText("No loans to analyze"); + impactChart.setVisible(false); + } else { + impactChart.setVisible(true); + ObservableList impactData = FXCollections.observableArrayList( + new PieChart.Data("Impact Index", data.getImpactIndex()), + new PieChart.Data("", 1 - data.getImpactIndex()) + ); + impactChart.setData(impactData); + impactIndex.setText(String.format("%.2f", data.getImpactIndex() * 100) + "%"); + } + if (data.getUrgencyIndex() == null) { + urgencyIndex.setText("No due loans to analyze"); + urgencyChart.setVisible(false); + } else { + urgencyChart.setVisible(true); + ObservableList urgencyData = FXCollections.observableArrayList( + new PieChart.Data("Urgency Index", data.getUrgencyIndex()), + new PieChart.Data("", 1 - data.getUrgencyIndex()) + ); + urgencyChart.setData(urgencyData); + urgencyIndex.setText(String.format("%.2f", data.getUrgencyIndex() * 100) + "%"); + } } } diff --git a/src/main/resources/view/AnalyticsPanel.fxml b/src/main/resources/view/AnalyticsPanel.fxml index 5cf6aa5b7d9..a27022cdd9f 100644 --- a/src/main/resources/view/AnalyticsPanel.fxml +++ b/src/main/resources/view/AnalyticsPanel.fxml @@ -2,8 +2,31 @@ + + - + + + + + + + + + + + + + + + + diff --git a/src/test/java/seedu/address/logic/commands/AddCommandTest.java b/src/test/java/seedu/address/logic/commands/AddCommandTest.java index ec4363c0b48..9074afbbbd5 100644 --- a/src/test/java/seedu/address/logic/commands/AddCommandTest.java +++ b/src/test/java/seedu/address/logic/commands/AddCommandTest.java @@ -25,6 +25,7 @@ import seedu.address.model.Model; import seedu.address.model.ReadOnlyAddressBook; import seedu.address.model.ReadOnlyUserPrefs; +import seedu.address.model.analytics.DashboardData; import seedu.address.model.person.Analytics; import seedu.address.model.person.Loan; import seedu.address.model.person.Person; @@ -203,12 +204,7 @@ public void unmarkLoan(Loan loanToUnmark) { } @Override - public void setAnalytics(Analytics analytics) { - throw new AssertionError("This method should not be called."); - } - - @Override - public ObjectProperty getAnalytics() { + public ObjectProperty getDashboardData() { throw new AssertionError("This method should not be called."); } @@ -242,6 +238,11 @@ public void setToPersonTab() { throw new AssertionError("This method should not be called."); } + @Override + public void generateDashboardData(Analytics analytics) { + throw new AssertionError("This method should not be called."); + } + } /**