Skip to content

Commit

Permalink
Merge branch 'refs/heads/dev-SCRUM-66' into dev-SCRUM-55
Browse files Browse the repository at this point in the history
# Conflicts:
#	src/pages/dashboard/DashboardPage.tsx
#	src/pages/dashboard/GraphUtils.ts
#	src/pages/dashboard/Graphs.tsx
  • Loading branch information
Robert-M-Lucas committed Apr 27, 2024
2 parents ccafd9c + 5c2834e commit 804c39a
Show file tree
Hide file tree
Showing 19 changed files with 554 additions and 259 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ Start Firebase emulator(s): `firebase emulators:start`
### Test Deployment

Creating a test deployment to Firebase:
Creating a tiles deployment to Firebase:
```
npm run build
firebase hosting:channel:deploy [Test Deployment Name]
Expand All @@ -51,7 +51,7 @@ firebase hosting:channel:deploy [Test Deployment Name]
### Production Deployment

Making a [pull request](https://github.com/Robert-M-Lucas/budget-19/compare)
will automatically create a test deployment to Firebase (i.e. not
will automatically create a tiles deployment to Firebase (i.e. not
overwrite the production app). You should see a message below your new pull
request notifying you that your changes are being deployed. Click on
the link shown when this is complete to view your changes. The pull
Expand Down
17 changes: 16 additions & 1 deletion package-lock.json

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

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
},
"dependencies": {
"@types/papaparse": "^5.3.14",
"@types/strftime": "^0.9.8",
"bootstrap": "^5.3.3",
"firebase": "^10.11.0",
"lodash": "^4.17.21",
Expand All @@ -23,7 +24,8 @@
"react-router-dom": "^6.23.0",
"react-tiles-dnd": "^0.1.2",
"recharts": "^2.12.5",
"sass": "^1.72.0"
"sass": "^1.72.0",
"strftime": "^0.10.2"
},
"devDependencies": {
"@faker-js/faker": "^8.4.1",
Expand Down
2 changes: 1 addition & 1 deletion src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export function Header() {
{/* Links to transactions page with table of expenses */}
<li><Link to="/transactions" className={`header-item ${isLoggedIn ? "header-item" : "text-muted bg-transparent"}`}>Transactions</Link></li>

<li><Link to="/test" className="header-item">Firestore Test</Link></li>
<li><Link to="/tiles" className="header-item">Firestore Test</Link></li>

{displayName && <li><span className="username">{displayName}</span></li>}

Expand Down
159 changes: 51 additions & 108 deletions src/pages/dashboard/DashboardPage.tsx
Original file line number Diff line number Diff line change
@@ -1,171 +1,114 @@
import "./styles.css";
import "react-tiles-dnd/esm/index.css";
import {TilesContainer, RenderTileFunction} from "react-tiles-dnd";
import { TilesContainer, RenderTileFunction } from "react-tiles-dnd";
import useWindowDimensions from "../../hooks/WindowDimensionsHook.tsx";
import {ReactNode, useEffect, useState} from "react";
import {Header} from "../../components/Header.tsx";
import {getTransactionsFilterOrderBy} from "../../utils/transaction.ts"
import {getCurrentBalance} from "../../utils/transaction_utils.ts";
import {getUserPrefs, UserPrefs} from "../../utils/user_prefs.ts";
import {readTransactions} from "./GraphUtils.ts";
import {useEffect, useState} from "react";
import {Transaction, getTransactionsFilterOrderBy } from "../../utils/transaction.ts"
import {auth} from "../../utils/firebase.ts";
import {orderBy} from "firebase/firestore";
import { User } from "firebase/auth";
import {FullscreenCenter} from "../../components/FullscreenCenter.tsx";
import {Button} from "react-bootstrap";
import {CSVUpload} from "../../components/transactions/CSVUpload.tsx";
import {InputTransaction} from "../../components/transactions/InputTransaction.tsx";
import Graphs from "./Graphs.tsx"
import test from "./test.tsx"

type transactionPoint = { date: string; amount: number }
type tsxContents = ReactNode;

class TileElement {
private graph?: transactionPoint[];
private TSX?: () => tsxContents;

constructor(graph: transactionPoint[] | undefined, TSX: (() => tsxContents) | undefined) {
this.graph = graph;
this.TSX = TSX;
}

static newGraph(graph: transactionPoint[]): TileElement {
return new TileElement(graph, undefined);
}
static newTSX(TSX: () => tsxContents): TileElement {
return new TileElement(undefined, TSX);
}

isGraph(): boolean {
return typeof this.graph !== "undefined";
}

forceGetGraph(): transactionPoint[] {
return this.graph!;
}
forceGetTSX(): () => tsxContents {
return this.TSX!;
}
}
import {getTileSize, TileElement} from "./TileUtils.ts";
import {finalGraphData, readTransactions} from "./GraphUtils.ts";
import {signInWithGoogle} from "../../utils/authentication.ts";
import totalTile from "./TotalTile.tsx";
import {getUserPrefs, UserPrefs} from "../../utils/user_prefs.ts";
import {User} from "firebase/auth";
import goalsTile from "./GoalsTile.tsx";

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

const fetchTransactions = async (user: User, goal: number) => {
try {
const transactions = await getTransactionsFilterOrderBy(user, orderBy("dateTime", "asc"))
console.log("Check 0", transactions)
await readTransactions(transactions, goal).then((t) => {
console.log("Check 1", t)
setPoints(t)
})
setFetchResolved(true);
} catch (error) { console.error("Error in fetchTransactions", error) }
}
const fetchUserPrefs = async (user: User): Promise<UserPrefs | undefined> => {
try {
const u = await getUserPrefs(user);
setUserResolved(true);
return u
} catch (error) { console.error("Error in fetchUserPrefs", error) }
const fetchTransactions = async (user: User) => {
const transactions = await getTransactionsFilterOrderBy(user, orderBy("dateTime", "desc"));
setTransactions(transactions);
setPoints(readTransactions(transactions));
}

const {width} = useWindowDimensions();
const columns = Math.max(Math.floor(width / 200), 1);
const columns = Math.max(Math.floor(width / 180), 1);

// Transaction Loading and Handling
useEffect(() => {
if (auth.currentUser !== null) {
getCurrentBalance(auth.currentUser).then((b) => setBalance(b));
fetchUserPrefs(auth.currentUser).then((u) => {
console.log("Fetched UserPrefs", u)
if (u) {
fetchTransactions(auth.currentUser!, u!.goal).then(() => {
console.log("Fetched Transactions", transactionPoints)
});
}
})

fetchTransactions(auth.currentUser).then();
getUserPrefs(auth.currentUser).then((prefs) => setUserPrefs(prefs));
}
},[auth.currentUser])

// Forces user to wait till everything has loaded
if (!authResolved) {
auth.authStateReady().then(() => setAuthResolved(true));
return <>
<Header/>
<FullscreenCenter>
<div className="text-center">
<h1>Waiting for Authentication</h1>
<h1>Waiting for Auth</h1>
</div>
</FullscreenCenter>
</>;
}
if (!userResolved) {

if (auth.currentUser === null) {
auth.onAuthStateChanged(() => {
setUpdate(update + 1);
});
return <>
<Header/>
<FullscreenCenter>
<div className="text-center">
<h1>Fetching User Data</h1>
<h1>Not Logged In</h1>
<button type="button" className="login-with-google-btn" onClick={signInWithGoogle}>
Sign in with Google
</button>
</div>
</FullscreenCenter>
</>;
}
if (!fetchResolved) {

if (!transactionPoints || !userPrefs) {
return <>
<Header/>
<FullscreenCenter>
<div className="text-center">
<h1>Fetching User Transactions</h1>
<h1>Fetching</h1>
</div>
</FullscreenCenter>
</>;
}

// Tiles
const tileSize = (tile: typeof transactionTiles[0]) => ({
colSpan: tile.cols,
rowSpan: tile.rows
});
const transactionTiles = [
{d: TileElement.newGraph(transactionPoints[0]), cols:5, rows:2},
{d: TileElement.newGraph(transactionPoints[1]), cols:5, rows:2},
{d: TileElement.newGraph(transactionPoints[2]), cols:5, rows:2},
{d: TileElement.newTSX(test), cols:1, rows:1}
const transactionTiles: TileElement[] = [
TileElement.newTSX(() => totalTile(transactions), 2, 1, columns),
TileElement.newTSX(() => (goalsTile(userPrefs)), 2, 1, columns),
TileElement.newGraph(transactionPoints.raw, 3, 2, columns),
TileElement.newGraph(transactionPoints.in, 3, 2, columns),
TileElement.newGraph(transactionPoints.out, 3, 2, columns),
];
const renderFirebase: RenderTileFunction<typeof transactionTiles[0]> = ({ data, isDragging }) => (

const renderTile: RenderTileFunction<typeof transactionTiles[0]> = ({ data, isDragging }) => (
<div style={{padding: ".75rem", width: "100%"}}>
<div className={`tile card ${isDragging ? "dragging" : ""}`}
style={{width: "100%", height: "100%"}}>
{data.d.isGraph() ? <Graphs data={data.d.forceGetGraph()}/> : data.d.forceGetTSX()()}
{data.isGraph() ? <Graphs data={data.forceGetGraph()}/> : data.forceGetTSX()()}
</div>
</div>
);

return (
<div className="vh-100 d-flex flex-column">
<Header/>
<div>
<Button variant="primary" onClick={() => setShowCSVModal(true)}>Upload CSV</Button>
<CSVUpload show={showCSVModal} setShow={setShowCSVModal}/>

<Button variant="primary" onClick={() => setShowTransactionModal(true)}>Add Transaction</Button>
<InputTransaction show={showTransactionModal} setShow={setShowTransactionModal}/>
</div>
<div className="App ps-5 pe-5 mt-3">
<button onClick={() => {
console.log(transactionPoints)
}}>Console Log Transactions
</button>
{balance}
<TilesContainer
data={transactionTiles}
renderTile={renderFirebase}
tileSize={tileSize}
renderTile={renderTile}
tileSize={getTileSize}
ratio={1}
columns={columns}
></TilesContainer>
Expand Down
7 changes: 7 additions & 0 deletions src/pages/dashboard/GoalsTile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {ReactNode} from "react";
import {UserPrefs} from "../../utils/user_prefs.ts";

export default function goalsTile(userPrefs: UserPrefs): ReactNode {

return <p>Goal: {userPrefs.getNeedsBudget()} | {userPrefs.getWantsBudget()} | {userPrefs.getSavingsBudget()}</p>;
}
40 changes: 19 additions & 21 deletions src/pages/dashboard/GraphUtils.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,23 @@
import {Transaction} from "../../utils/transaction.ts";
import strftime from "strftime";

type transactionPoint = { date: string; amount: number; goal: number }
export type transactionPoint = { date: string; amount: number }
export type finalGraphData = {raw: transactionPoint[], in: transactionPoint[], out: transactionPoint[]};

export const cumulateTransactions = (points: transactionPoint[]): transactionPoint[] => {
function cumulateTransactions(points: transactionPoint[]): transactionPoint[] {
let total = 0;
return points.map(value => {
total += value.amount;
return {date: value.date, amount: total, goal: value.goal};
value.amount = total;
return value;
})
}
const getDateString = (timestamp: number): string => {
const date = new Date(timestamp)
const day = date.getDate().toString().padStart(2, '0'); // Ensures two digits
const month = (date.getMonth() + 1).toString().padStart(2, '0'); // Month is 0-indexed, add 1
const year = date.getFullYear();
return `${day}/${month}/${year}`;
}
export const readTransactions = async (data: Transaction[], goal: number): Promise<transactionPoint[][]> => {
const result: transactionPoint[] = []
data.forEach(t => {
result.push({amount: t.amount, date: getDateString(t.dateTime), goal: 0})
})
// Change the last bit of data to set the goal
const lastElem = result[result.length - 1]
result[result.length - 1] = {amount: lastElem.amount, date: lastElem.date, goal: goal}

return splitTransactions(result)
function getDateString(timestamp: number): string {
return strftime("%d/%m/%y", new Date(timestamp))
}
export const splitTransactions = (data: transactionPoint[]): transactionPoint[][] => {

function splitTransactions (data: transactionPoint[]): finalGraphData {
const moneyIn: transactionPoint[] = []
const moneyOut: transactionPoint[] = []
data.forEach(t => {
Expand All @@ -37,5 +27,13 @@ export const splitTransactions = (data: transactionPoint[]): transactionPoint[][
moneyOut.push(t)
}
})
return ([cumulateTransactions(data), cumulateTransactions(moneyIn), cumulateTransactions(moneyOut)])
return {raw: cumulateTransactions(data), in: cumulateTransactions(moneyIn), out: cumulateTransactions(moneyOut)};
}

export function readTransactions(data: Transaction[]): finalGraphData {
return splitTransactions(
data.map((t) => {
return {date: getDateString(t.dateTime), amount: t.amount}
})
);
}
3 changes: 3 additions & 0 deletions src/pages/dashboard/Graphs.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {Line, LineChart, ResponsiveContainer, XAxis, YAxis} from "recharts";

type transactionPoint = { date: string; amount: number; goal: number }
import {Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis} from "recharts";
import {transactionPoint} from "./GraphUtils.ts";

interface Props {
data: transactionPoint[];
Expand All @@ -12,6 +14,7 @@ export default function Graphs({data}: Props) {
<LineChart data={data}>
<XAxis dataKey="date"/>
<YAxis/>
<Tooltip/>
<Line type="monotone" dataKey="amount" stroke="#8884d8"/>
<Line type="monotone" dataKey="goal" stroke="#000000"/>
</LineChart>
Expand Down
Loading

0 comments on commit 804c39a

Please sign in to comment.