= ({
return (
<>
-
+
{paidOffDiff().years > 0 && paidOffDiff().months > 0 && (
<>
{explainer}
@@ -83,14 +83,14 @@ const LoanRepaymentNarrative: NextPage = ({
{periodComplete.periodDate.toFormat("LLLL yyyy")}
.
-
+
There is{" "}
- {currencyFormatter().format(periodCompleteProjection.totalPaid)}
+ {currencyFormatter.format(periodCompleteProjection.totalPaid)}
{" "}
remaining to pay, with{" "}
- {currencyFormatter().format(
+ {currencyFormatter.format(
periodCompleteProjection.totalInterestApplied
)}
{" "}
diff --git a/components/ui/molecules/MonthLoanTable.tsx b/components/ui/molecules/MonthLoanTable.tsx
new file mode 100644
index 0000000..e66e6e5
--- /dev/null
+++ b/components/ui/molecules/MonthLoanTable.tsx
@@ -0,0 +1,352 @@
+import type { NextPage } from "next";
+import React, { useCallback } from "react";
+import LoanType from "../../../models/loanType";
+import { Results } from "../../../api/models/results";
+import {
+ currencyFormatter,
+ currencyFormatterNoFraction,
+} from "../../../utils/currencyFormatter";
+import Result from "../../../api/models/result";
+import classNames from "classnames";
+import {
+ ChevronDoubleLeft,
+ ChevronDoubleRight,
+ ChevronLeft,
+ ChevronRight,
+} from "react-bootstrap-icons";
+
+interface MonthLoanTableProps {
+ results: Results;
+ loanType: LoanType;
+}
+
+export const MonthLoanTable: NextPage = ({
+ results,
+ loanType,
+}) => {
+ const periodCompleteIndex = results.results.findIndex((r) =>
+ r.projections.find(
+ (p) =>
+ p.loanType == loanType &&
+ (p.repaymentStatus == "WrittenOff" || p.repaymentStatus == "PaidOff")
+ )
+ )!;
+
+ const completedResults = results.results.slice(0, periodCompleteIndex);
+ const paging = usePager(completedResults);
+
+ return (
+
+
+
+
+
+
+ Month
+ |
+
+ Paid
+ |
+
+ Interest added
+ |
+
+ Debt remaining
+ |
+
+ Interest rate
+ |
+
+ Repayment threshold
+ |
+
+ Income
+ |
+
+
+
+ {paging.pageResults.map((result, index) => {
+ const projection = result.projections.find(
+ (x) => x.loanType == loanType
+ )!;
+ const prevResult = results.results.find(
+ (x) => x.period == result.period - 1
+ );
+ const prevPrevResult = results.results.find(
+ (x) => x.period == result.period - 2
+ );
+ const prevProjection = prevResult?.projections.find(
+ (x) => x.loanType == loanType
+ )!;
+ const prevPrevProjection = prevPrevResult?.projections.find(
+ (x) => x.loanType == loanType
+ )!;
+
+ const salaryChange =
+ !prevResult || prevResult.salary == result.salary
+ ? null
+ : prevResult.salary < result.salary
+ ? "up"
+ : "down";
+ const thresholdChange =
+ !prevProjection ||
+ prevProjection.threshold == projection.threshold
+ ? null
+ : prevProjection.threshold < projection.threshold
+ ? "up"
+ : "down";
+
+ const debtDecreasedFirstTime =
+ prevPrevProjection?.debtRemaining <
+ prevProjection?.debtRemaining &&
+ prevProjection?.debtRemaining > projection.debtRemaining;
+
+ const increasedFirstTime =
+ prevPrevProjection?.debtRemaining >
+ prevProjection?.debtRemaining &&
+ prevProjection?.debtRemaining < projection.debtRemaining;
+
+ return (
+
+
+ {result.periodDate.toFormat("LLLL yyyy")}
+ |
+
+ {currencyFormatter.format(projection.paid)}
+ |
+
+ {currencyFormatter.format(projection.interestApplied)}
+ |
+
+ {currencyFormatter.format(projection.debtRemaining)}
+ {debtDecreasedFirstTime && (
+
+ ↓
+
+ )}
+ {increasedFirstTime && (
+
+ ↑
+
+ )}
+ |
+
+ {projection.interestRate * 100}%
+ |
+
+ {currencyFormatterNoFraction.format(projection.threshold)}
+ {thresholdChange && (
+
+ {thresholdChange == "up" ? "↑" : "↓"}
+
+ )}
+ |
+
+ {currencyFormatter.format(result.salary)}
+ {salaryChange && (
+
+ {salaryChange == "up" ? "↑" : "↓"}
+
+ )}
+ |
+
+ );
+ })}
+
+
+
+
+
+ );
+};
+
+const usePager = (results: Result[]) => {
+ const [currentPage, setCurrentPage] = React.useState(1);
+ const [itemsPerPage, setItemsPerPage] = React.useState(10);
+
+ const updateItemsPerPage = useCallback(
+ (itemsPerPage: number) => {
+ setItemsPerPage(itemsPerPage);
+ const newLastPage = Math.ceil(results.length / itemsPerPage);
+
+ if (currentPage > newLastPage) {
+ setCurrentPage(newLastPage);
+ }
+ },
+ [currentPage, results.length]
+ );
+
+ const pageResults = results.slice(
+ (currentPage - 1) * itemsPerPage,
+ (currentPage - 1) * itemsPerPage + itemsPerPage
+ );
+
+ return {
+ pageResults,
+ currentPage,
+ itemsPerPage,
+ setItemsPerPage: updateItemsPerPage,
+ setCurrentPage,
+ };
+};
+
+type PaginationProps = {
+ total: number;
+ currentPage: number;
+ setCurrentPage: (page: number) => void;
+ setItemsPerPage: (itemsPerPage: number) => void;
+ itemsPerPage: number;
+ itemsPerPageOptions?: number[];
+};
+
+const Pagination = ({
+ total,
+ currentPage,
+ setCurrentPage,
+ itemsPerPage,
+ setItemsPerPage,
+ itemsPerPageOptions = [10, 20, 30, 40, 50],
+}: PaginationProps) => {
+ const pageCount = Math.ceil(total / itemsPerPage);
+ const canGoBack = currentPage > 1;
+ const canGoForward = currentPage < pageCount;
+
+ let pagesToDistribute = 4;
+ const pages = [currentPage];
+
+ let i = 1;
+ while (pagesToDistribute > 0) {
+ let currentPagesToDistribute = pagesToDistribute;
+ if (currentPage - i > 0) {
+ pages.push(currentPage - i);
+ pagesToDistribute--;
+ }
+
+ if (currentPage + i <= pageCount) {
+ pages.push(currentPage + i);
+ pagesToDistribute--;
+ }
+
+ if (pagesToDistribute == currentPagesToDistribute) {
+ break;
+ }
+
+ i++;
+ }
+
+ pages.sort((a, b) => a - b);
+
+ return (
+
+
+
+
+
+
+
+
+ {currentPage * itemsPerPage - itemsPerPage + 1}-
+ {currentPage * itemsPerPage} of {total}
+
+
+
+
+
+
+
+
+ {pages.map((number) => (
+
+ ))}
+
+
+
+
+
+
+ );
+};
diff --git a/components/ui/molecules/totalsGraph.tsx b/components/ui/molecules/TotalsGraph.tsx
similarity index 92%
rename from components/ui/molecules/totalsGraph.tsx
rename to components/ui/molecules/TotalsGraph.tsx
index 308bd61..892241c 100644
--- a/components/ui/molecules/totalsGraph.tsx
+++ b/components/ui/molecules/TotalsGraph.tsx
@@ -20,7 +20,7 @@ import {
getLabelsForGroupedDataCallback,
groupDataEveryNthPeriod,
} from "./graphUtils";
-import currencyFormatter from "../../../utils/currencyFormatter";
+import { currencyFormatter } from "../../../utils/currencyFormatter";
import { DateTime } from "luxon";
ChartJS.register(
@@ -30,7 +30,7 @@ ChartJS.register(
LineElement,
Title,
Tooltip,
- Legend,
+ Legend
);
interface TotalsGraphProps {
@@ -49,11 +49,11 @@ const TotalsGraph = (props: TotalsGraphProps) => {
this: Scale,
tickValue: string | number,
index: number,
- ticks: Tick[],
+ ticks: Tick[]
): string {
return getLabelsForGroupedDataCallback(
props.results.results,
- this.getLabelForValue(index),
+ this.getLabelForValue(index)
);
},
autoSkip: false,
@@ -77,7 +77,7 @@ const TotalsGraph = (props: TotalsGraphProps) => {
}
if (context.parsed.y !== null) {
- label += currencyFormatter().format(context.parsed.y);
+ label += currencyFormatter.format(context.parsed.y);
}
return label;
diff --git a/components/ui/molecules/graphUtils.ts b/components/ui/molecules/graphUtils.ts
index f389bea..d1341d4 100644
--- a/components/ui/molecules/graphUtils.ts
+++ b/components/ui/molecules/graphUtils.ts
@@ -35,7 +35,7 @@ export function groupDataEveryNthPeriod(results: Result[]) {
export function getLabelsForGroupedDataCallback(
results: Result[],
- label: string,
+ label: string
) {
var date = DateTime.fromISO(label);
diff --git a/components/ui/organisms/assumptions-input.tsx b/components/ui/organisms/AssumptionsInput.tsx
similarity index 97%
rename from components/ui/organisms/assumptions-input.tsx
rename to components/ui/organisms/AssumptionsInput.tsx
index 68599c3..778e3bd 100644
--- a/components/ui/organisms/assumptions-input.tsx
+++ b/components/ui/organisms/AssumptionsInput.tsx
@@ -1,6 +1,6 @@
import { NextPage } from "next";
import { ChangeEvent } from "react";
-import InputGroup from "../atoms/input-group";
+import InputGroup from "../atoms/InputGroup";
interface AssumptionsInputProps {
salaryGrowth: number;
diff --git a/components/ui/organisms/details-input.tsx b/components/ui/organisms/DetailsInput.tsx
similarity index 98%
rename from components/ui/organisms/details-input.tsx
rename to components/ui/organisms/DetailsInput.tsx
index 510d332..9d483eb 100644
--- a/components/ui/organisms/details-input.tsx
+++ b/components/ui/organisms/DetailsInput.tsx
@@ -1,17 +1,17 @@
import { NextPage } from "next";
import { RefObject, useEffect, useState } from "react";
-import InputGroup from "../atoms/input-group";
import { Loan } from "../../../models/loan";
+import InputGroup from "../atoms/InputGroup";
import LoanType from "../../../models/loanType";
import { DateTime } from "luxon";
import { Details } from "../../../models/details";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
-import Input from "../atoms/input";
-import Button from "../atoms/button";
+import Input from "../atoms/Input";
+import Button from "../atoms/Button";
import { Trash } from "react-bootstrap-icons";
-import DatePicker from "../atoms/date-picker";
+import DatePicker from "../atoms/DatePicker";
import classNames from "classnames";
let nextMonth = () => {
diff --git a/components/ui/organisms/footer.tsx b/components/ui/organisms/Footer.tsx
similarity index 100%
rename from components/ui/organisms/footer.tsx
rename to components/ui/organisms/Footer.tsx
diff --git a/components/ui/organisms/header.tsx b/components/ui/organisms/Header.tsx
similarity index 98%
rename from components/ui/organisms/header.tsx
rename to components/ui/organisms/Header.tsx
index ca65b36..13c6687 100644
--- a/components/ui/organisms/header.tsx
+++ b/components/ui/organisms/Header.tsx
@@ -43,7 +43,7 @@ const Header = () => {
{isNavOpen && (
-
+
= ({ results, loanType }) => {
+ const [resultType, setResultType] = React.useState(
+ ResultType.Chart
+ );
+
+ return (
+
+
+ Your{" "}
+
+ {LoanTypeToDescription(loanType)}
+ {" "}
+ results
+
+
+
+
+
+
+
+
+
+
+ {resultType == ResultType.Chart && (
+
+
+
+
+ )}
+
+ {resultType == ResultType.Table && (
+
+
+
+ )}
+
+ );
+};
+
+export default LoanBreakdown;
diff --git a/components/ui/organisms/loan-input.tsx b/components/ui/organisms/LoanInput.tsx
similarity index 97%
rename from components/ui/organisms/loan-input.tsx
rename to components/ui/organisms/LoanInput.tsx
index 1aa80af..e402ebe 100644
--- a/components/ui/organisms/loan-input.tsx
+++ b/components/ui/organisms/LoanInput.tsx
@@ -2,11 +2,11 @@ import { NextPage } from "next";
import React from "react";
import { Loan, NewLoan } from "../../../models/loan";
import LoanType from "../../../models/loanType";
-import Button from "../atoms/button";
-import Checkbox from "../atoms/checkbox";
-import Input from "../atoms/input";
-import InputGroup from "../atoms/input-group";
-import Select from "../atoms/select";
+import Button from "../atoms/Button";
+import Checkbox from "../atoms/Checkbox";
+import Input from "../atoms/Input";
+import InputGroup from "../atoms/InputGroup";
+import Select from "../atoms/Select";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
@@ -261,7 +261,7 @@ const LoanInput: NextPage = ({