Skip to content

Commit

Permalink
Merge pull request #42 from Robert-M-Lucas/dev-SCRUM-69
Browse files Browse the repository at this point in the history
Dev scrum 69
  • Loading branch information
Robert-M-Lucas authored Apr 28, 2024
2 parents 7ee05df + 5729c8d commit 6994a67
Show file tree
Hide file tree
Showing 19 changed files with 294 additions and 30 deletions.
10 changes: 10 additions & 0 deletions src/components/transactions/Tranasction.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {describe, expect, test} from "vitest";
import {emojis} from "./Transaction.ts";
import {TransactionCategories} from "../../utils/transaction.ts";
import _ from "lodash";

describe("Input Transaction Test", () => {
test("Category Integrity Test", () => {
expect(_.isEqual(new Set(Object.keys(emojis)), TransactionCategories), "Emoji keys do not match transaction categories").toBeTruthy();
});
});
14 changes: 5 additions & 9 deletions src/components/transactions/Transaction.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Transaction as TransactionDocument } from "../../utils/transaction.ts";
import {Transaction as TransactionDocument, TransactionCategories} from "../../utils/transaction.ts";
import strftime from "strftime";

export const emojis: { [index: string]: string } = {
"Income": "💸",
Expand All @@ -20,7 +21,6 @@ export const emojis: { [index: string]: string } = {
"General": "🎒"
}

export const categories = new Set(Object.keys(emojis));

export class Transaction {
isValid: boolean;
Expand Down Expand Up @@ -100,7 +100,7 @@ export class Transaction {
}

setCategory(category?: string) {
if (!this.isNotEmpty(category) || !categories.has(category)) {
if (!this.isNotEmpty(category) || !TransactionCategories.has(category)) {
this.isValid = false;
this.invalidField = "category";
} else {
Expand Down Expand Up @@ -173,13 +173,9 @@ export class Transaction {
// utility functions for transactions

export function formatDate(date: Date):string {
return `${padding(date.getDate())}/${padding(date.getMonth()+1)}/${date.getFullYear()}`;
return strftime("%d/%m/%Y", date);
}

export function formatTime(date: Date):string {
return `${padding(date.getHours())}:${padding(date.getMinutes())}:${padding(date.getSeconds())}`;
}

function padding(n: number): string {
return n.toString().padStart(2, "0");
return strftime("%H:%M:%S", date);
}
31 changes: 24 additions & 7 deletions src/pages/dashboard/DashboardPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,25 +14,34 @@ import {signInWithGoogle} from "../../utils/authentication.ts";
import totalTile from "./total tile/TotalTile.tsx";
import {getUserPrefs, UserPrefs} from "../../utils/user_prefs.ts";
import {User} from "firebase/auth";
import goalsTile from "./goals tile/GoalsTile.tsx";
import goalSettingTile from "./goal setting tile/GoalSettingTile.tsx";
import {RenderTileFunction, TilesContainer} from "react-tiles-dnd";
import goalTracking from "./goal tracking tile/GoalTrackingTile.tsx";
import motivationTile from "./motivation tile/MotivationTile.tsx";
import AddTransactionTile from "./add transaction tile/AddTransactionTile.tsx";

export default function Dashboard() {
// const [balance, setBalance] = useState(0);
const [transactionPoints, setPoints] = useState<finalGraphData | null>(null);
const [transactions, setTransactions] = useState<Transaction[]>([]);
const [authResolved, setAuthResolved] = useState<Boolean>(false);
const [authResolved, setAuthResolved] = useState(false);
const [userPrefs, setUserPrefs] = useState<UserPrefs | null>(null);
// const draggable = useRef(true);
// const [showCSVModal, setShowCSVModal] = useState(false);
// const [showTransactionModal, setShowTransactionModal] = useState(false);
const [update, setUpdate] = useState(0)

const forceUpdate = () => {
const forceUpdatePrefs = () => {
setUpdate(update + 1);
setUserPrefs(null);
};

const forceUpdateTransactions = () => {
setUpdate(update + 1);
setPoints(null);
setTransactions([]);
};

const fetchTransactions = async (user: User) => {
const transactions = await getTransactionsFilterOrderBy(user, orderBy("dateTime", "desc"));
setTransactions(transactions);
Expand All @@ -45,10 +54,15 @@ export default function Dashboard() {
// Transaction Loading and Handling
useEffect(() => {
if (auth.currentUser !== null) {
fetchTransactions(auth.currentUser).then();
getUserPrefs(auth.currentUser).then((prefs) => setUserPrefs(prefs));
if (transactionPoints === null) {
fetchTransactions(auth.currentUser).then();
}
if (userPrefs === null) {
getUserPrefs(auth.currentUser).then((prefs) => setUserPrefs(prefs));
}
}
},[auth.currentUser, update]);
// eslint-disable-next-line
},[update]);

if (!authResolved) {
auth.authStateReady().then(() => setAuthResolved(true));
Expand Down Expand Up @@ -92,7 +106,10 @@ export default function Dashboard() {

const transactionTiles: TileElement[] = [
TileElement.newTSX(() => totalTile(transactions), 2, 1, columns),
TileElement.newTSX(() => (goalsTile(userPrefs, forceUpdate)), 2, 2, columns),
TileElement.newTSX(() => (goalSettingTile(userPrefs, forceUpdatePrefs)), 2, 2, columns),
TileElement.newTSX(() => goalTracking(transactions, userPrefs), 3, 1, columns),
TileElement.newTSX(() => motivationTile(transactions, userPrefs), 1, 1, columns),
TileElement.newTSX(() => AddTransactionTile(forceUpdateTransactions), 1, 1, columns),
TileElement.newGraph(transactionPoints.raw, 3, 2, columns),
TileElement.newGraph(transactionPoints.in, 3, 2, columns),
TileElement.newGraph(transactionPoints.out, 3, 2, columns),
Expand Down
25 changes: 25 additions & 0 deletions src/pages/dashboard/add transaction tile/AddTransactionTile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {ReactNode, useState} from "react";
import {CSVUpload} from "../../../components/transactions/CSVUpload.tsx";
import {InputTransaction} from "../../../components/transactions/InputTransaction.tsx";

export default function AddTransactionTile(updateTransactions: () => void): ReactNode {
const [showCSVModal, setShowCSVModal] = useState(false);
const [showTransactionModal, setShowTransactionModal] = useState(false);

const onCSVModalClose = () => {
setShowCSVModal(false);
updateTransactions();
};

const onTransactionModalClose = () => {
setShowTransactionModal(false);
updateTransactions();
}

return <>
<CSVUpload show={showCSVModal} closeModal={onCSVModalClose}/>
<InputTransaction show={showTransactionModal} closeModal={onTransactionModalClose}/>
<button onClick={() => setShowCSVModal(true)}>Upload CSV</button>
<button onClick={() => setShowTransactionModal(true)}>Add Transaction</button>
</>
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import React, {ReactNode, useState} from "react";
import {setUserPrefs, UserPrefs} from "../../../utils/user_prefs.ts";
import MultiRangeSlider, {ChangeResult} from "multi-range-slider-react";
import "./GoalsTile.scss";
import "./GoalsSettingTile.scss";
import {auth} from "../../../utils/firebase.ts";

export default function goalsTile(userPrefs: UserPrefs, forceUpdate: () => void): ReactNode {
export default function goalSettingTile(userPrefs: UserPrefs, forceUpdate: () => void): ReactNode {
const [minValue, setMinValue] = useState(
Math.round(userPrefs.getNeedsBudget() * 100)
);
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

85 changes: 85 additions & 0 deletions src/pages/dashboard/goal tracking tile/GoalTrackingTile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import {Transaction} from "../../../utils/transaction.ts";
import {ReactNode} from "react";
import {max} from "lodash";
import {UserPrefs} from "../../../utils/user_prefs.ts";
import {nanDisplay, roundFix} from "../../../utils/numbers.ts";

export default function goalTracking(transactions: Transaction[], userPrefs: UserPrefs): ReactNode {
const balance = transactions.reduce((prev, curr): number => prev + curr.amount, 0);
const income = transactions.reduce((prev, curr): number => prev + max([curr.amount, 0])!, 0);
const needs = transactions.reduce((prev, curr): number => {
if (curr.getCategoryCategory() === "Needs" && curr.amount < 0) {
return prev - curr.amount;
}
return prev;
}, 0);
const wants = transactions.reduce((prev, curr): number => {
if (curr.getCategoryCategory() === "Wants" && curr.amount < 0) {
return prev - curr.amount;
}
return prev;
}, 0);
const savings = transactions.reduce((prev, curr): number => {
if (curr.getCategoryCategory() === "Savings" && curr.amount < 0) {
return prev - curr.amount;
}
return prev;
}, 0);

const needsTarget = income * userPrefs.getNeedsBudget();
const wantsTarget = income * userPrefs.getWantsBudget();
const savingsTarget = income * userPrefs.getSavingsBudget();

const needsOffsetPercentage = Math.round((needs - needsTarget) / income * 100);
const wantsOffsetPercentage = Math.round((wants - wantsTarget) / income * 100);
const savingsBalanceOffsetPercentage = Math.round(((savings + balance) - savingsTarget) / income * 100);

return <table className="table h-100 mt-4 mb-4">
<thead style={{height: "10px!important"}}>
<tr>
<th scope="col" className={"p-0"}>Category</th>
<th scope="col" className={"p-0"}>Actual</th>
<th scope="col" className={"p-0"}>Target</th>
<th scope="col" className={"p-0"}>Difference</th>
</tr>
</thead>
<tbody>
<tr>
<td className={"text-center"} scope="row">Needs</td>
{needsOffsetPercentage > 0 ?
<td className={"text-center"} style={{color: "#ca0f0f"}}>£{roundFix(needs)}</td> :
<td className={"text-center"} style={{color: "green"}}>£{roundFix(needs)}</td>
}
<td className={"text-center"}>£{roundFix(needsTarget)}</td>
{needsOffsetPercentage > 0 ?
<td className={"text-center"} style={{color: "#ca0f0f"}}>{nanDisplay(needsOffsetPercentage)}%</td> :
<td className={"text-center"} style={{color: "green"}}>{nanDisplay(needsOffsetPercentage)}%</td>
}
</tr>
<tr>
<td className={"text-center"} scope="row">Wants</td>
{wantsOffsetPercentage > 0 ?
<td className={"text-center"} style={{color: "#ca0f0f"}}>£{roundFix(wants)}</td> :
<td className={"text-center"} style={{color: "green"}}>£{roundFix(wants)}</td>
}
<td className={"text-center"}>£{roundFix(wantsTarget)}</td>
{wantsOffsetPercentage > 0 ?
<td className={"text-center"} style={{color: "#ca0f0f"}}>{nanDisplay(wantsOffsetPercentage)}%</td> :
<td className={"text-center"} style={{color: "green"}}>{nanDisplay(wantsOffsetPercentage)}%</td>
}
</tr>
<tr style={{borderBottom: "rgba(0, 0, 0, 0)"}}>
<td className={"text-center"} scope="row">Savings + Balance</td>
{savingsBalanceOffsetPercentage < 0 ?
<td className={"text-center"} style={{color: "#ca0f0f"}}>£{roundFix(savings + balance)}</td> :
<td className={"text-center"} style={{color: "green"}}>£{roundFix(savings + balance)}</td>
}
<td className={"text-center"}>£{roundFix(savingsTarget)}</td>
{savingsBalanceOffsetPercentage < 0 ?
<td className={"text-center"} style={{color: "#ca0f0f"}}>{nanDisplay(savingsBalanceOffsetPercentage)}%</td> :
<td className={"text-center"} style={{color: "green"}}>{nanDisplay(savingsBalanceOffsetPercentage)}%</td>
}
</tr>
</tbody>
</table>;
}
1 change: 0 additions & 1 deletion src/pages/dashboard/goals tile/GoalsTile.css.map

This file was deleted.

59 changes: 59 additions & 0 deletions src/pages/dashboard/motivation tile/MotivationTile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {Transaction} from "../../../utils/transaction.ts";
import {ReactNode} from "react";
import {max} from "lodash";
import {UserPrefs} from "../../../utils/user_prefs.ts";
import {nanDisplay} from "../../../utils/numbers.ts";

export default function motivationTile(transactions: Transaction[], userPrefs: UserPrefs): ReactNode {
const balance = transactions.reduce((prev, curr): number => prev + curr.amount, 0);
const income = transactions.reduce((prev, curr): number => prev + max([curr.amount, 0])!, 0);
const needs = transactions.reduce((prev, curr): number => {
if (curr.getCategoryCategory() === "Needs" && curr.amount < 0) {
return prev - curr.amount;
}
return prev;
}, 0);
const wants = transactions.reduce((prev, curr): number => {
if (curr.getCategoryCategory() === "Wants" && curr.amount < 0) {
return prev - curr.amount;
}
return prev;
}, 0);
const savings = transactions.reduce((prev, curr): number => {
if (curr.getCategoryCategory() === "Savings" && curr.amount < 0) {
return prev - curr.amount;
}
return prev;
}, 0);

const needsTarget = income * userPrefs.getNeedsBudget();
const wantsTarget = income * userPrefs.getWantsBudget();
const savingsTarget = income * userPrefs.getSavingsBudget();

const needsOffsetPercentage = Math.round((needs - needsTarget) / income * 100);
const wantsOffsetPercentage = Math.round((wants - wantsTarget) / income * 100);
const savingsBalanceOffsetPercentage = Math.round(((savings + balance) - savingsTarget) / income * 100);

const finalOffsetPercentage = (-needsOffsetPercentage) + (-wantsOffsetPercentage) + savingsBalanceOffsetPercentage;

return <>
<div style={{height: "5%"}}></div>
<div className={"w-100 text-center d-flex align-items-center justify-content-center"} style={{height: "20%"}}>
<span>Your score is:</span>
</div>
<div className={"w-100 text-center d-flex align-items-center justify-content-center"}
style={{height: "50%", lineHeight: "100%"}}>
{finalOffsetPercentage < 0 ?
<span style={{fontSize: "60px", color: "#ca0f0f"}}>{nanDisplay(finalOffsetPercentage)}</span> :
<span style={{fontSize: "60px", color: "green"}}>{nanDisplay(finalOffsetPercentage)}</span>
}
</div>
<div className={"w-100 text-center d-flex align-items-center justify-content-center"} style={{height: "20%"}}>
{finalOffsetPercentage < 0 ?
<span>Keep Trying!</span> :
<span>Good Job!</span>
}
</div>
<div style={{height: "5%"}}></div>
</>;
}
9 changes: 5 additions & 4 deletions src/pages/dashboard/total tile/TotalTile.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {ReactNode} from "react";
import { Transaction } from "../../../utils/transaction.ts";
import {max, min} from "lodash";
import {roundFix} from "../../../utils/numbers.ts";

export default function totalTile(transactions: Transaction[]): ReactNode {
const balance = transactions.reduce((prev, curr): number => prev + curr.amount, 0);
Expand All @@ -13,25 +14,25 @@ export default function totalTile(transactions: Transaction[]): ReactNode {
<div className="col d-flex justify-content-center align-items-center">Balance:</div>
{balance > 0 ? <>
<div className="col d-flex justify-content-center align-items-center" style={{color: "green"}}>£</div>
<div className="col d-flex justify-content-center align-items-center" style={{color: "green"}}>{balance.toFixed(2)}</div>
<div className="col d-flex justify-content-center align-items-center" style={{color: "green"}}>{roundFix(balance)}</div>
</> : <>
<div className="col d-flex justify-content-center align-items-center" style={{color: "#ca0f0f"}}>£</div>
<div className="col d-flex justify-content-center align-items-center" style={{color: "#ca0f0f"}}>{balance.toFixed(2)}</div>
<div className="col d-flex justify-content-center align-items-center" style={{color: "#ca0f0f"}}>{roundFix(balance)}</div>
</>}
</div>
</li>
<li className="list-group-item" style={{height: "33%"}}>
<div className="row h-100">
<div className="col d-flex justify-content-center align-items-center">Income:</div>
<div className="col d-flex justify-content-center align-items-center" style={{color: "green"}}>£</div>
<div className="col d-flex justify-content-center align-items-center" style={{color: "green"}}>{income.toFixed(2)}</div>
<div className="col d-flex justify-content-center align-items-center" style={{color: "green"}}>{roundFix(income)}</div>
</div>
</li>
<li className="list-group-item" style={{height: "33%"}}>
<div className="row h-100">
<div className="col d-flex justify-content-center align-items-center">Expenses:</div>
<div className="col d-flex justify-content-center align-items-center" style={{color: "#ca0f0f"}}>£</div>
<div className="col d-flex justify-content-center align-items-center" style={{color: "#ca0f0f"}}>{expenses.toFixed(2)}</div>
<div className="col d-flex justify-content-center align-items-center" style={{color: "#ca0f0f"}}>{roundFix(expenses)}</div>
</div>
</li>
</ul>;
Expand Down
Loading

0 comments on commit 6994a67

Please sign in to comment.