diff --git a/README.md b/README.md index 8e2693d..430c656 100644 --- a/README.md +++ b/README.md @@ -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] @@ -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 diff --git a/package-lock.json b/package-lock.json index aa0a8a9..c65656a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "@types/papaparse": "^5.3.14", + "@types/strftime": "^0.9.8", "bootstrap": "^5.3.3", "firebase": "^10.11.0", "lodash": "^4.17.21", @@ -19,7 +20,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", @@ -2584,6 +2586,11 @@ "@types/send": "*" } }, + "node_modules/@types/strftime": { + "version": "0.9.8", + "resolved": "https://registry.npmjs.org/@types/strftime/-/strftime-0.9.8.tgz", + "integrity": "sha512-QIvDlGAKyF3YJbT3QZnfC+RIvV5noyDbi+ZJ5rkaSRqxCGrYJefgXm3leZAjtoQOutZe1hCXbAg+p89/Vj4HlQ==" + }, "node_modules/@types/tough-cookie": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", @@ -6705,6 +6712,14 @@ "dev": true, "optional": true }, + "node_modules/strftime": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/strftime/-/strftime-0.10.2.tgz", + "integrity": "sha512-Y6IZaTVM80chcMe7j65Gl/0nmlNdtt+KWPle5YeCAjmsBfw+id2qdaJ5MDrxUq+OmHKab+jHe7mUjU/aNMSZZg==", + "engines": { + "node": ">=0.2.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", diff --git a/package.json b/package.json index caaae65..4dce34f 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", diff --git a/src/components/Header.tsx b/src/components/Header.tsx index fd668e9..ec298ff 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -42,7 +42,7 @@ export function Header() { {/* Links to transactions page with table of expenses */}
  • Transactions
  • -
  • Firestore Test
  • +
  • Firestore Test
  • {displayName &&
  • {displayName}
  • } diff --git a/src/pages/dashboard/DashboardPage.tsx b/src/pages/dashboard/DashboardPage.tsx index fd2714c..7004cbf 100644 --- a/src/pages/dashboard/DashboardPage.tsx +++ b/src/pages/dashboard/DashboardPage.tsx @@ -1,147 +1,102 @@ 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([[]]); - const [userResolved, setUserResolved] = useState(false); + // const [balance, setBalance] = useState(0); + const [transactionPoints, setPoints] = useState(null); + const [transactions, setTransactions] = useState([]); const [authResolved, setAuthResolved] = useState(false); - const [fetchResolved, setFetchResolved] = useState(false); - const [showCSVModal, setShowCSVModal] = useState(false); - const [showTransactionModal, setShowTransactionModal] = useState(false); + const [userPrefs, setUserPrefs] = useState(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 => { - 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 <> +
    -

    Waiting for Authentication

    +

    Waiting for Auth

    ; } - if (!userResolved) { + + if (auth.currentUser === null) { + auth.onAuthStateChanged(() => { + setUpdate(update + 1); + }); return <> +
    -

    Fetching User Data

    +

    Not Logged In

    +
    ; } - if (!fetchResolved) { + + if (!transactionPoints || !userPrefs) { return <> +
    -

    Fetching User Transactions

    +

    Fetching

    ; } - // 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 = ({ data, isDragging }) => ( + + const renderTile: RenderTileFunction = ({ data, isDragging }) => (
    - {data.d.isGraph() ? : data.d.forceGetTSX()()} + {data.isGraph() ? : data.forceGetTSX()()}
    ); @@ -149,23 +104,11 @@ export default function Dashboard() { return (
    -
    - - - - - -
    - - {balance} diff --git a/src/pages/dashboard/GoalsTile.tsx b/src/pages/dashboard/GoalsTile.tsx new file mode 100644 index 0000000..8232909 --- /dev/null +++ b/src/pages/dashboard/GoalsTile.tsx @@ -0,0 +1,7 @@ +import {ReactNode} from "react"; +import {UserPrefs} from "../../utils/user_prefs.ts"; + +export default function goalsTile(userPrefs: UserPrefs): ReactNode { + + return

    Goal: {userPrefs.getNeedsBudget()} | {userPrefs.getWantsBudget()} | {userPrefs.getSavingsBudget()}

    ; +} \ No newline at end of file diff --git a/src/pages/dashboard/GraphUtils.ts b/src/pages/dashboard/GraphUtils.ts index 75fc0e2..cad8f00 100644 --- a/src/pages/dashboard/GraphUtils.ts +++ b/src/pages/dashboard/GraphUtils.ts @@ -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 => { - 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 => { @@ -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} + }) + ); } \ No newline at end of file diff --git a/src/pages/dashboard/Graphs.tsx b/src/pages/dashboard/Graphs.tsx index 50682d9..ebd2a4f 100644 --- a/src/pages/dashboard/Graphs.tsx +++ b/src/pages/dashboard/Graphs.tsx @@ -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[]; @@ -12,6 +14,7 @@ export default function Graphs({data}: Props) { + diff --git a/src/pages/dashboard/TileUtils.ts b/src/pages/dashboard/TileUtils.ts new file mode 100644 index 0000000..3fbee2b --- /dev/null +++ b/src/pages/dashboard/TileUtils.ts @@ -0,0 +1,43 @@ +import {ReactNode} from "react"; +import {transactionPoint} from "./GraphUtils.ts"; +import {min} from "lodash"; + +type tsxContents = ReactNode; + +export class TileElement { + private readonly graph?: transactionPoint[]; + private readonly TSX?: () => tsxContents; + public readonly cols: number; + public readonly rows: number; + + private constructor(graph: transactionPoint[] | undefined, TSX: (() => tsxContents) | undefined, cols: number, rows: number) { + this.graph = graph; + this.TSX = TSX; + this.cols = cols; + this.rows = rows; + } + + static newGraph(graph: transactionPoint[], cols: number, rows: number, maxCol: number): TileElement { + return new TileElement(graph, undefined, min([cols, maxCol])!, rows); + } + static newTSX(TSX: () => tsxContents, cols: number, rows: number, maxCol: number): TileElement { + return new TileElement(undefined, TSX, min([cols, maxCol])!, rows); + } + + isGraph(): boolean { + return typeof this.graph !== "undefined"; + } + + forceGetGraph(): transactionPoint[] { + return this.graph!; + } + forceGetTSX(): () => tsxContents { + return this.TSX!; + } +} + +export type tileData = { d: TileElement; rows: number; cols: number }; + +export function getTileSize (tile: TileElement) { + return {colSpan: tile.cols, rowSpan: tile.rows}; +} \ No newline at end of file diff --git a/src/pages/dashboard/TotalTile.tsx b/src/pages/dashboard/TotalTile.tsx new file mode 100644 index 0000000..6bad195 --- /dev/null +++ b/src/pages/dashboard/TotalTile.tsx @@ -0,0 +1,38 @@ +import {ReactNode} from "react"; +import { Transaction } from "../../utils/transaction"; +import {max, min} from "lodash"; + +export default function totalTile(transactions: Transaction[]): 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 expenses = transactions.reduce((prev, curr): number => prev + min([curr.amount, 0])!, 0); + + return
      +
    • +
      +
      Balance:
      + {balance > 0 ? <> +
      £
      +
      {balance.toFixed(2)}
      + : <> +
      £
      +
      {balance.toFixed(2)}
      + } +
      +
    • +
    • +
      +
      Income:
      +
      £
      +
      {income.toFixed(2)}
      +
      +
    • +
    • +
      +
      Expenses:
      +
      £
      +
      {expenses.toFixed(2)}
      +
      +
    • +
    ; +} \ No newline at end of file diff --git a/src/pages/dashboard/test.tsx b/src/pages/dashboard/test.tsx deleted file mode 100644 index 13cba03..0000000 --- a/src/pages/dashboard/test.tsx +++ /dev/null @@ -1,7 +0,0 @@ -export default function test() { - return ( - <> - Hi - - ); -} \ No newline at end of file diff --git a/src/pages/test firestore/TestFirestore.tsx b/src/pages/test firestore/TestFirestore.tsx index 2655ca2..899c941 100644 --- a/src/pages/test firestore/TestFirestore.tsx +++ b/src/pages/test firestore/TestFirestore.tsx @@ -15,6 +15,7 @@ import {Header} from "../../components/Header.tsx"; import { orderBy } from "firebase/firestore"; import {User} from "firebase/auth"; import {getUserPrefs, setUserPrefs, UserPrefs} from "../../utils/user_prefs.ts"; +import {round} from "lodash"; function writeSampleData() { if (auth.currentUser === null) { @@ -95,11 +96,14 @@ export function TestFirestorePage() {

    UserPrefs

    { userPrefs ? <> -

    Goal: {userPrefs.goal}

    +

    Goal: {userPrefs.getNeedsBudget()} | {userPrefs.getWantsBudget()} | {userPrefs.getSavingsBudget()}

    :

    Loading

    } diff --git a/src/pages/transactions/TransactionPage.tsx b/src/pages/transactions/TransactionPage.tsx new file mode 100644 index 0000000..f7ecfde --- /dev/null +++ b/src/pages/transactions/TransactionPage.tsx @@ -0,0 +1,177 @@ +import {useState} from "react"; +import {auth} from "../../utils/firebase.ts"; +import {getTransactionsFilterOrderBy, Transaction} from "../../utils/transaction.ts"; +import {FullscreenCenter} from "../../components/FullscreenCenter.tsx"; +import {User} from "firebase/auth"; +import {Header} from "../../components/Header.tsx"; +import {signInWithGoogle} from "../../utils/authentication.ts"; +import {limit, orderBy, startAfter} from "firebase/firestore"; +import strftime from "strftime"; + +// CSS +import "./transactionsTable.scss"; + +export function TransactionPage() { + const [transactions, setTransactions] = useState(null); + const [currentPage, setCurrentPage] = useState(0); + const [authResolved, setAuthResolved] = useState(false); + const [update, setUpdate] = useState(0); + const [pageStarts, setPageStarts] = useState([Infinity]); + const itemsPerPage = 14; + + + if (!authResolved) { + auth.authStateReady().then(() => setAuthResolved(true)); + return <> + +
    +

    Waiting for Auth

    +
    +
    + ; + } + + if (auth.currentUser === null) { + auth.onAuthStateChanged((new_user: User | null) => { + if (new_user !== null) { + setUpdate(update + 1); + } + }); + return <> +
    + +
    +

    Not Logged In

    + +
    +
    + ; + } + + if (!transactions) { + getTransactionsFilterOrderBy(auth.currentUser, orderBy("dateTime", "desc"), limit(itemsPerPage), startAfter(pageStarts[pageStarts.length - 1])) + .then((pageTransactions) => { + setTransactions(pageTransactions); + }); + + return <> +
    + + + + + + + + + + + + + + + + + + + + + +
    NameCategoryEmojiDateAmountNotes
    Fetching...Fetching...Fetching...Fetching...Fetching...Fetching...
    + ; + } + + + // adjusted dylan.s code to use getTransactionPage instead of getTransactions + // useEffect(() => { + // getTransactionsPage(auth.currentUser, itemsPerPage, currentPage) + // .then((pageTransactions) => { + // console.log("Fetched transactions:"); + // console.log(pageTransactions); + // setTransactions(pageTransactions); + // }); + // }, [currentPage]); + + return <> +
    +
    + + + + + + + + + + + + + {transactions.map((transaction) => ( + + // maps every transaction as a row in the table + ))} + +
    NameCategoryEmojiDateAmountDescription
    + { transactions.length == 0 && +

    [No More Data]

    + } +
    + {/* conditional previous page button, only displayed if page number > 1 */} + {currentPage > 0 && ( + + )} + {/* spacers to push buttons to the edges */} +
    + Page {currentPage + 1} +
    + {transactions.length === itemsPerPage && + + } +
    + +
    + ; +} + +function TransactionItem({data}: { data: Transaction }) { + return ( + + {data.name} + {data.category} + {data.emoji} + {strftime("%d/%m/%y - %H:%M", new Date(data.dateTime))} + {data.amount > 0 ? + +
    + £ + {data.amount.toFixed(2)} +
    + : + +
    + £ + {data.amount.toFixed(2)} +
    + + } + {data.description} + + ); +} \ No newline at end of file diff --git a/src/pages/transactions/transactionsTable.css b/src/pages/transactions/transactionsTable.css new file mode 100644 index 0000000..2eb9216 --- /dev/null +++ b/src/pages/transactions/transactionsTable.css @@ -0,0 +1,69 @@ +/* page background */ +body { + background-color: #e6f2ff; +} + +/* table */ +table { + width: 100%; + border-collapse: separate; + border-spacing: 0; + border-radius: 10px; /* rounded borders */ + background-color: #ffffff; + overflow: hidden; /* hide overflow from rounded borders */ +} + +th { + padding: 12px 15px; + background-color: #f2f2f2; + border-bottom: 1px solid transparent; /* transparent border for rows */ +} + +td { + padding: 12px 15px; + text-align: left; + border-bottom: 1px solid transparent; /* transparent border for rows */ +} + +/* highlight on hover for table rows */ +tbody tr:hover { + background-color: #f5f5f5; +} + +/* Pagination */ +.pagination { + margin-top: 20px; + display: flex; + align-items: center; /* align items vertically */ +} + +.pagination button { + padding: 8px 12px; + border: 1px solid #007bff; + background-color: #007bff; + color: white; + cursor: pointer; + border-radius: 5px; +} + +.pagination .spacer { + flex-grow: 1; /* grow to fill available space */ +} + +.page-counter { + margin: 0 10px; /* add margin to separate from buttons */ +} + +/* hover effect for buttons */ +.pagination button:hover { + background-color: #0056b3; +} + +/* disabled state for buttons */ +.pagination button:disabled { + background-color: #cccccc; + border-color: #cccccc; + cursor: not-allowed; +} + +/*# sourceMappingURL=transactionsTable.css.map */ diff --git a/src/pages/transactions/transactionsTable.css.map b/src/pages/transactions/transactionsTable.css.map new file mode 100644 index 0000000..30fecee --- /dev/null +++ b/src/pages/transactions/transactionsTable.css.map @@ -0,0 +1 @@ +{"version":3,"sourceRoot":"","sources":["transactionsTable.scss"],"names":[],"mappings":"AAAA;AACA;EACE;;;AAGF;AACA;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;EACA;EACA;;;AAKF;EACE;EACA;EACA;;;AAGF;AACA;EACE;;;AAGF;AACA;EACE;EACA;EACA;;;AAGF;EACE;EACA;EACA;EACA;EACA;EACA;;;AAGF;EACE;;;AAGF;EACE;;;AAGF;AACA;EACE;;;AAGF;AACA;EACE;EACA;EACA","file":"transactionsTable.css"} \ No newline at end of file diff --git a/src/pages/transactions/transactionsTable.scss b/src/pages/transactions/transactionsTable.scss new file mode 100644 index 0000000..b8e0aeb --- /dev/null +++ b/src/pages/transactions/transactionsTable.scss @@ -0,0 +1,69 @@ +/* page background */ +body { + background-color: #e6f2ff; +} + +/* table */ +table { + width: 100%; + border-collapse: separate; + border-spacing: 0; + border-radius: 10px; /* rounded borders */ + background-color: #ffffff; + overflow: hidden; /* hide overflow from rounded borders */ +} + +th { + padding: 12px 15px; + background-color: #f2f2f2; + border-bottom: 1px solid transparent; /* transparent border for rows */ + //border-top-left-radius: 10px; /* rounded top-left corner */ + //border-top-right-radius: 10px; /* rounded top-right corner */ +} + +td { + padding: 12px 15px; + text-align: left; + border-bottom: 1px solid transparent; /* transparent border for rows */ +} + +/* highlight on hover for table rows */ +tbody tr:hover { + background-color: #f5f5f5; +} + +/* Pagination */ +.pagination { + margin-top: 20px; + display: flex; + align-items: center; /* align items vertically */ +} + +.pagination button { + padding: 8px 12px; + border: 1px solid #007bff; + background-color: #007bff; + color: white; + cursor: pointer; + border-radius: 5px; +} + +.pagination .spacer { + flex-grow: 1; /* grow to fill available space */ +} + +.page-counter { + margin: 0 10px; /* add margin to separate from buttons */ +} + +/* hover effect for buttons */ +.pagination button:hover { + background-color: #0056b3; +} + +/* disabled state for buttons */ +.pagination button:disabled { + background-color: #cccccc; + border-color: #cccccc; + cursor: not-allowed; +} \ No newline at end of file diff --git a/src/router.tsx b/src/router.tsx index 283b71b..f5b2c09 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -7,93 +7,10 @@ import {SampleSidebarHeader} from "./pages/samples/sidebar_header/SampleSidebarH import {SampleModal} from "./pages/samples/modal/SampleModal.tsx"; import GraphDashboard from "./pages/dashboard/DashboardPage.tsx"; import {TestFirestorePage} from "./pages/test firestore/TestFirestore.tsx"; +import {TransactionPage} from "./pages/transactions/TransactionPage.tsx"; // ? Routing - see https://reactrouter.com/en/main -// const tileset_defult: Array<{ text: string; rows: number; cols: number }> = [ -// { text: "Tile 1", cols: 1, rows: 1 }, -// { text: "Tile 2", cols: 1, rows: 1 }, -// { text: "Tile 3", cols: 2, rows: 2 }, -// { text: "Tile 4", cols: 2, rows: 2 }, -// { text: "Tile 5", cols: 1, rows: 1 }, -// { text: "Tile 6", cols: 1, rows: 1 }, -// { text: "Tile 7", cols: 1, rows: 1 }, -// { text: "Tile 8", cols: 1, rows: 1 }, -// { text: "Tile 9", cols: 2, rows: 1 }, -// ]; - -const tileset_column: Array<{ text: string; rows: number; cols: number }> = [ - { text: "Tile 1", cols: 1, rows: 1 }, - { text: "Tile 2", cols: 1, rows: 2 }, - { text: "Tile 3", cols: 2, rows: 2 }, - { text: "Tile 4", cols: 2, rows: 2 }, - { text: "Tile 5", cols: 1, rows: 1 }, - { text: "Tile 6", cols: 1, rows: 1 }, - { text: "Tile 7", cols: 1, rows: 1 }, - { text: "Tile 8", cols: 1, rows: 1 }, - { text: "Tile 9", cols: 2, rows: 1 }, -]; - -const tileset_many: Array<{ text: string; rows: number; cols: number }> = [ - { text: "Tile 1", cols: 1, rows: 1 }, - { text: "Tile 2", cols: 2, rows: 1 }, - { text: "Tile 3", cols: 2, rows: 2 }, - { text: "Tile 4", cols: 2, rows: 2 }, - { text: "Tile 5", cols: 1, rows: 1 }, - { text: "Tile 6", cols: 1, rows: 1 }, - { text: "Tile 7", cols: 1, rows: 1 }, - { text: "Tile 8", cols: 1, rows: 1 }, - { text: "Tile 9", cols: 2, rows: 1 }, - { text: "Tile 11", cols: 1, rows: 1 }, - { text: "Tile 12", cols: 2, rows: 1 }, - { text: "Tile 13", cols: 2, rows: 2 }, - { text: "Tile 14", cols: 2, rows: 2 }, - { text: "Tile 15", cols: 1, rows: 1 }, - {text: "Tile 16", cols: 1, rows:1 }, - { text: "Tile 17", cols: 1, rows: 1 }, - { text: "Tile 18", cols: 1, rows: 1 }, - { text: "Tile 19", cols: 2, rows: 1 }, - { text: "Tile 21", cols: 1, rows: 1 }, - { text: "Tile 22", cols: 2, rows: 1 }, - { text: "Tile 23", cols: 2, rows: 2 }, - { text: "Tile 24", cols: 2, rows: 2 }, - { text: "Tile 25", cols: 1, rows: 1 }, - { text: "Tile 26", cols: 1, rows: 1 }, - { text: "Tile 27", cols: 1, rows: 1 }, - { text: "Tile 28", cols: 1, rows: 1 }, - { text: "Tile 29", cols: 2, rows: 1 }, - { text: "Tile 31", cols: 1, rows: 1 }, - { text: "Tile 32", cols: 2, rows: 1 }, - { text: "Tile 33", cols: 2, rows: 2 }, - { text: "Tile 34", cols: 2, rows: 2 }, - { text: "Tile 35", cols: 1, rows: 1 }, - { text: "Tile 36", cols: 1, rows: 1 }, - { text: "Tile 37", cols: 1, rows: 1 }, - { text: "Tile 38", cols: 1, rows: 1 }, - { text: "Tile 39", cols: 2, rows: 1 }, -]; - -const tileset_weird: Array<{ text: string; rows: number; cols: number }> = [ - { text: "Tile 1", cols: 1, rows: 3 }, - { text: "Tile 2", cols: 2, rows: 3 }, - { text: "Tile 3", cols: 3, rows: 2 }, - { text: "Tile 4", cols: 3, rows: 2 }, - { text: "Tile 5", cols: 3, rows: 1 }, - { text: "Tile 6", cols: 1, rows: 1 }, - { text: "Tile 7", cols: 1, rows: 1 }, - { text: "Tile 8", cols: 1, rows: 1 }, - { text: "Tile 9", cols: 2, rows: 1 }, - { text: "Tile 11", cols: 1, rows: 1 }, - { text: "Tile 12", cols: 2, rows: 1 }, - { text: "Tile 13", cols: 2, rows: 2 }, - { text: "Tile 14", cols: 2, rows: 2 }, - { text: "Tile 15", cols: 1, rows: 1 }, - { text: "Tile 16", cols: 1, rows: 1 }, - { text: "Tile 17", cols: 3, rows: 3 }, - { text: "Tile 18", cols: 1, rows: 1 }, - { text: "Tile 19", cols: 2, rows: 1 }, -]; - export const router = createBrowserRouter([ { path: "/", @@ -102,17 +19,14 @@ export const router = createBrowserRouter([ }, { path: "/dash", - element: , + element: , }, { path: "/transactions", - element: , + element: , }, - // --> - - { - path: "/user-test", + path: "/user-tiles", element: , errorElement:<_404Page/> }, @@ -120,18 +34,6 @@ export const router = createBrowserRouter([ path: "/graphs", element: , }, - { - path: "/dash-2", - element: , - }, - { - path: "/dash-3", - element: , - }, - { - path: "/dash-4", - element: , - }, { path: "/sample_sidebar", element: , @@ -145,7 +47,7 @@ export const router = createBrowserRouter([ element: , }, { - path: "/test", + path: "/tiles", element: , }, ]); \ No newline at end of file diff --git a/src/utils/transaction_utils.ts b/src/utils/transaction_utils.ts index 53dd236..69fa01d 100644 --- a/src/utils/transaction_utils.ts +++ b/src/utils/transaction_utils.ts @@ -1,6 +1,6 @@ import {User} from "firebase/auth"; import {getTransactions, getTransactionsFilterOrderBy, Transaction} from "./transaction.ts"; -import {limit, startAfter, where} from "firebase/firestore"; +import {where} from "firebase/firestore"; export async function getCurrentBalance(user: User): Promise { const transactions = await getTransactions(user); @@ -24,9 +24,4 @@ const DAY_MILLIS = 8.64e+7; export async function getLastDayTransaction(user: User): Promise { return await getTransactionsFilterOrderBy(user, where("dateTime", ">", Date.now() - DAY_MILLIS)); -} - -// Returns `pageSize` transactions for the given `user` with the `docName` attribute set -export async function getTransactionsPage(user: User, pageSize: number, page: number): Promise { - return await getTransactionsFilterOrderBy(user, startAfter(page * pageSize), limit(pageSize)); } \ No newline at end of file diff --git a/src/utils/user_prefs.ts b/src/utils/user_prefs.ts index 2aa65b5..24ebba6 100644 --- a/src/utils/user_prefs.ts +++ b/src/utils/user_prefs.ts @@ -1,17 +1,52 @@ import {collection, doc, DocumentSnapshot, getDoc, setDoc, SnapshotOptions} from "firebase/firestore"; import {User} from "firebase/auth"; import {db} from "./firebase.ts"; +import {round} from "lodash"; export class UserPrefs { - public goal: number; + private readonly needsBudget: number; + private readonly wantsBudget: number; - constructor(goal: number) { - this.goal = goal; + private constructor(needsBudget: number, wantsBudget: number) { + if (needsBudget > 1) { + needsBudget = 1; + } + + if (needsBudget + wantsBudget > 1) { + wantsBudget = 1 - needsBudget; + } + + this.needsBudget = round(needsBudget, 2); + this.wantsBudget = round(wantsBudget, 2); + } + + static newChecked(needsBudget: number, wantsBudget: number): UserPrefs | Error { + if (needsBudget > 1) { + return new Error("needsBudget > 1!"); + } + + if (needsBudget + wantsBudget > 1) { + return new Error("needsBudget + wantsBudget > 1!"); + } + + return new UserPrefs(needsBudget, wantsBudget); } static default(): UserPrefs { - return new UserPrefs(100); + return new UserPrefs(0.5, 0.3); + } + + getNeedsBudget(): number { + return this.needsBudget; + } + + getWantsBudget(): number { + return this.wantsBudget; + } + + getSavingsBudget(): number { + return round(1 - this.wantsBudget - this.needsBudget, 2); } // Utility method for creating `Transactions` @@ -20,7 +55,8 @@ export class UserPrefs { if (!data) { throw Error("No data returned for snapshot!"); } - return new UserPrefs(data.goal); + + return new UserPrefs(round(data.needsBudget, 2), round(data.wantsBudget, 2)); } toSendObject(): object {