diff --git a/cypress/e2e/Dashboard.cy.js b/cypress/e2e/Dashboard.cy.js new file mode 100644 index 00000000..4246fbaf --- /dev/null +++ b/cypress/e2e/Dashboard.cy.js @@ -0,0 +1,59 @@ +import BasicSelect from "../../src/components/CashFlow/BasicSelect"; + +beforeEach(() => { + cy.intercept("POST", "http://localhost:3000/graphql", { + statusCode: 401, + fixture: "data" + }); + cy.visit("http://localhost:3003"); +}) + +describe("Dashboard", () => { + it("Checks for text on left sidebar", () => { + cy.contains("Powdered Toast Man"); + cy.contains("Dashboard"); + cy.contains("Activity"); + cy.contains("Accounts"); + cy.contains("Settings"); + }) + + it("should have a main container", () => { + cy.get(".cashflow").should("exist"); + }); + + it("should have a Cash Flow header", () => { + cy.get(".cashflow-header").should("exist"); + }); + + it("should have a Cash Flow title", () => { + cy.get(".cashflow-h1").should("exist").and("have.text", "Cash Flow"); + }); + + it("should have an Income header", () => { + cy.get(".cashflow-header-income").contains(".cashflow-header-text", "Income").should("exist"); + }); + + it("should have an Expense header", () => { + cy.get(".cashflow-header-income").contains(".cashflow-header-text", "Expenses").should("exist"); + }); + + it('should have a BasicSelect dropdown', () => { + cy.get(".MuiBox-root").should("be.visible").click() + + cy.get('li').first().invoke("attr", "data-value").should("eq", "2022"); + cy.get('li').last().invoke("attr", "data-value").should("eq", "2024"); + }); + + it("should have bar chart elements", () => { + cy.get(".css-16ktrx5-MuiResponsiveChart-container").should("exist"); + cy.get(".css-13aj3tc-MuiChartsSurface-root").should("exist"); + cy.get(".MuiChartsAxis-directionX").should("be.visible"); + cy.get(".MuiChartsAxis-directionY").should("be.visible"); + }); +}) + +//example commands to use when writing tests + // cy.contains('type').click() + // cy.url().should('include', '/commands/actions') + // cy.get('.action-email').type('fake@email.com') + // cy.get('.action-email').should('have.value', 'fake@email.com') \ No newline at end of file diff --git a/cypress/fixtures/data.json b/cypress/fixtures/data.json new file mode 100644 index 00000000..08264f64 --- /dev/null +++ b/cypress/fixtures/data.json @@ -0,0 +1,45 @@ +{ + "data": { + "user": { + "id": "1", + "cashFlows": [ + { + "month": "February", + "year": 2023, + "totalIncome": 12500.0, + "totalExpense": 0.0 + }, +{ + "month": "April", + "year": 2024, + "totalIncome": 2000.0, + "totalExpense": 0.0 + }, + { + "month": "February", + "year": 2024, + "totalIncome": 3070000.0, + "totalExpense": 6960.0 + }, + { + "month": "January", + "year": 2024, + "totalIncome": 5000.0, + "totalExpense": 0.0 + }, + { + "month": "March", + "year": 2024, + "totalIncome": 3000.0, + "totalExpense": 0.0 + }, + { + "month": "May", + "year": 2024, + "totalIncome": 1000.0, + "totalExpense": 0.0 + } + ] + } + } +} \ No newline at end of file diff --git a/src/components/AddExpense/AddExpense.js b/src/components/AddExpense/AddExpense.js index f37f01fc..2958c132 100644 --- a/src/components/AddExpense/AddExpense.js +++ b/src/components/AddExpense/AddExpense.js @@ -26,7 +26,7 @@ const AddExpense = ({ totalExpenses, setTotalExpenses, setTransactions }) => { const [amount, setAmount] = useState(0); const [date, setDate] = useState(""); // Bilbo's UID - const userId = "2" + const userId = 2 const { createExpense } = useCreateExpense(); @@ -43,7 +43,7 @@ const AddExpense = ({ totalExpenses, setTotalExpenses, setTransactions }) => { userId, vendor, category, - amount: amountCents, + amount: amountCents / 100, date, }, }); diff --git a/src/components/AddIncome/AddIncome.js b/src/components/AddIncome/AddIncome.js index 6cf0de7f..d2526008 100644 --- a/src/components/AddIncome/AddIncome.js +++ b/src/components/AddIncome/AddIncome.js @@ -26,7 +26,7 @@ const AddIncome = ({ totalIncome, setTotalIncome, setTransactions }) => { const [date, setDate] = useState(""); // Bilbo's UID - const userId = "2" + const userId = 2 const { createIncome } = useCreateIncome(); @@ -51,7 +51,7 @@ const AddIncome = ({ totalIncome, setTotalIncome, setTransactions }) => { id: data.createIncome.id, vendor: source, date, - amount: amountCents, + amount: amountCents / 100, status: "credited", }; diff --git a/src/components/App/App.js b/src/components/App/App.js index d5ce3331..dfd9ef68 100644 --- a/src/components/App/App.js +++ b/src/components/App/App.js @@ -5,27 +5,33 @@ import Dashboard from '../Dashboard/Dashboard' // import transactionsFixtureData from "../sample-data/TransactionsData.json" // import incomeData from "../sample-data/IncomeData.json" // import expensesData from "../sample-data/ExpensesData.json" +// import budgetsData from "../sample-data/BudgetsData.json" import './App.css'; import { useGetIncomes } from '../apollo-client/queries/getIncomes'; import { useGetExpenses } from '../apollo-client/queries/getExpenses'; import { useGetTransactions } from '../apollo-client/queries/getTransactions'; import { useGetCashFlow } from '../apollo-client/queries/getCashFlow'; - +import { useGetBudgetsByParams } from '../apollo-client/queries/getBudgetsByParams'; const App = () => { const [transactions, setTransactions] = useState([]); const [totalIncome, setTotalIncome] = useState(0); const [totalExpenses, setTotalExpenses] = useState(0); const [cashFlow, setCashFlow] = useState(null); + const [budgets, setBudgets] = useState(null); // Hardcoded user, will pull from getUser endpoint soon + const month = "2024-02"; + // const category = "Travel"; const userName = "Powdered Toast Man"; const email = "moneybaggins@bigbanktakelilbank.doge" - + localStorage.setItem('email', email); + const { totalIncomeData } = useGetIncomes(email); const { totalExpensesData } = useGetExpenses(email); const { transactionsData } = useGetTransactions(email); const { cashFlowData } = useGetCashFlow(email); + // const { budgetsData } = useGetBudgetsByParams(month, category, email); useEffect(() => { if (totalIncomeData) { @@ -39,7 +45,8 @@ const App = () => { if (totalExpensesData) setTotalExpenses(totalExpensesData); if (transactionsData) setTransactions(transactionsData); if (cashFlowData) setCashFlow(cashFlowData); - }, [totalIncomeData, totalExpensesData, transactionsData, cashFlowData]); + // if (budgetsData) setBudgets(budgetsData); + }, [totalIncomeData, totalExpensesData, transactionsData, cashFlowData]); return (
diff --git a/src/components/Budget/BasicPie.js b/src/components/Budget/BasicPie.js index 7ef9301c..fece9a12 100644 --- a/src/components/Budget/BasicPie.js +++ b/src/components/Budget/BasicPie.js @@ -1,27 +1,34 @@ import * as React from 'react'; import { PieChart } from '@mui/x-charts/PieChart'; -const data = [ +// Default data for fallback +const defaultData = [ { id: 0, value: 25 }, - { id: 1, value: 15 }, + { id: 1, value: 75 }, ]; -export default function PieActiveArc() { +export default function BasicPie({ data }) { + // Validate incoming data - simple example + const isValidData = data && Array.isArray(data) && data.length > 0 && data.every(d => d.hasOwnProperty('value') && typeof d.value === 'number'); + + // Use incoming data if valid, otherwise use default + const pieData = isValidData ? data : defaultData; + return ( - + ); } \ No newline at end of file diff --git a/src/components/Budget/Budget.js b/src/components/Budget/Budget.js index db8f3d85..362599f2 100644 --- a/src/components/Budget/Budget.js +++ b/src/components/Budget/Budget.js @@ -1,48 +1,148 @@ -import React from 'react' +import React, { useState, useRef, useEffect, useMemo } from 'react' import './Budget.css' import BasicPie from './BasicPie' +import './budgetSelectModal.css' import DropDownIcon from '../../assets/icons/dropdown-icon.svg' import EllipsePurple from '../../assets/icons/Ellipse-purple.svg' import EllipseBlue from '../../assets/icons/Ellipse-blue.svg' import PlusIcon from '../../assets/icons/plus-icon.svg' +import { useGetBudgetsByParams } from "../apollo-client/queries/getBudgetsByParams"; +import { useGetBudgetCategories } from "../apollo-client/queries/getBudgetCategories"; const Budget = () => { - return ( + const email = "moneybaggins@bigbanktakelilbank.doge"; + const { loading: loadingCategories, error: errorCategories, budgetCategoriesData } = useGetBudgetCategories(email); + console.log("Fetched budgetCategoriesData:", budgetCategoriesData); + const categories = loadingCategories || errorCategories ? [] : budgetCategoriesData || []; + + const [category, setCategory] = useState(); + const [month, setMonth] = useState(getCurrentMonth()); + const { loading, error, budgetsData } = useGetBudgetsByParams(month, category, email); + // debugger; + if (error) { + console.error("Error fetching data:", error); + } + console.log("Fetched budgetData:", budgetsData); + const pctRemaining = Math.round(budgetsData?.budgets[0]?.pctRemaining) || 'Loading...'; + const amount = budgetsData?.budgets[0]?.amount || 'Loading...'; + const amountRemaining = Math.round(budgetsData?.budgets[0]?.amountRemaining) || 'Loading...'; + + // State to manage dropdown visibility + const [isDropdownVisible, setIsDropdownVisible] = useState(false); + const [dropDownStyle, setDropdownStyle] = useState({}); // State to hold modal's dynamic style + // references + const dropdownRef = useRef(null); // Ref for the dropdown icon to position the modal + const modalRef = useRef(null); // Add a ref for the modal + + // Handler functions for updating state + const handleCategoryChange = (selectedCategory) => { + setCategory(selectedCategory); + setIsDropdownVisible(false); // Hide the modal + }; + // Handler to toggle dropdown visibility + const toggleDropdownVisibility = () => { + setIsDropdownVisible(!isDropdownVisible); + + if (dropdownRef.current) { + const { bottom, right } = dropdownRef.current.getBoundingClientRect(); + const rightOffset = window.innerWidth - right; // Calculate the right offset from the viewport + + setDropdownStyle({ + position: 'absolute', + top: `${bottom}px`, + right: `${rightOffset}px`, + // Adjustments might be needed based on actual layout and styling + }); + } + }; + const handleMonthChange = (event) => { + setMonth(event.target.value); + }; + // Utility function to get current month, implementation depends on your needs + function getCurrentMonth() { + const date = new Date(); + const year = date.getFullYear(); // Get current year + let month = date.getMonth() + 1; // Get current month (0-11, hence +1) + month = month < 10 ? `0${month}` : month; // Ensure month is in two digits + return `${year}-${month}`; // Concatenate to get "YYYY-MM" format + } + + useEffect(() => { + if (categories.length > 0) { + setCategory(categories[0]); + } + }, [categories]); + + useEffect(() => { + const handleClickOutside = (event) => { + if (modalRef.current && !modalRef.current.contains(event.target) && + dropdownRef.current && !dropdownRef.current.contains(event.target)) { + setIsDropdownVisible(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, []); // This effect does not depend on `categories` + + return ( - ) -} - -export default Budget \ No newline at end of file + ) + } + export default Budget diff --git a/src/components/Budget/budgetSelectModal.css b/src/components/Budget/budgetSelectModal.css new file mode 100644 index 00000000..123e51a1 --- /dev/null +++ b/src/components/Budget/budgetSelectModal.css @@ -0,0 +1,20 @@ +.select-modal { + cursor: pointer; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + background-color: rgba(255, 255, 255, 0.77); + padding: 10px 0; + border-radius: 4px; + z-index: 100; /* Ensure it sits above other content */ +} + +.select-modal div { + padding: 7px 7px; /* Apply horizontal padding here for content alignment */ + margin: 0; /* Remove margin to allow hover to fill from edge to edge */ + width: 100%; /* Adjust width to account for padding */ + box-sizing: border-box; /* Ensure padding is included in the width calculation */ +} + +.select-modal div:hover { + background-color: rgba(86, 77, 201, 0.7); /* Updated color for visibility */ + /* Ensures hover effect extends to full width of each option */ +} \ No newline at end of file diff --git a/src/components/Dashboard/Dashboard.js b/src/components/Dashboard/Dashboard.js index da59757e..c65b6df0 100644 --- a/src/components/Dashboard/Dashboard.js +++ b/src/components/Dashboard/Dashboard.js @@ -14,6 +14,11 @@ const Dashboard = ({ setTotalIncome, totalExpenses, setTotalExpenses, + incomeTransactions, + setIncomeTransactions, + expensesTransactions, + setExpensesTransactions, + budgets }) => { diff --git a/src/components/apollo-client/apollo-client.js b/src/components/apollo-client/apollo-client.js index 84c2d712..f0d37fb2 100644 --- a/src/components/apollo-client/apollo-client.js +++ b/src/components/apollo-client/apollo-client.js @@ -2,8 +2,8 @@ import { ApolloClient, InMemoryCache, createHttpLink } from "@apollo/client"; import { setContext } from "@apollo/client/link/context"; const httpLink = createHttpLink({ - // uri: "https://doughfin-be.onrender.com/graphql", - uri: "http://localhost:3000/graphql" + uri: "https://doughfin-be.onrender.com/graphql", + // uri: "http://localhost:3000/graphql" }); const customHeaders = setContext((_, { headers }) => { diff --git a/src/components/apollo-client/queries/getBudgetCategories.js b/src/components/apollo-client/queries/getBudgetCategories.js new file mode 100644 index 00000000..0f22855d --- /dev/null +++ b/src/components/apollo-client/queries/getBudgetCategories.js @@ -0,0 +1,21 @@ +import { gql, useQuery } from "@apollo/client"; + +export const GET_BUDGET_CATEGORIES = gql` +query GetBudgetCategories($email: String!) { + user(email: $email) { + budgetCategories + } +}` + +export const useGetBudgetCategories= (email) => { + const { loading, error, data } = useQuery(GET_BUDGET_CATEGORIES, { + variables: { email: email }, + fetchPolicy: "no-cache" + }); + let budgetCategoriesData = null; + if (!loading && data) { + budgetCategoriesData = data?.user?.budgetCategories || []; + } + + return { loading, error, budgetCategoriesData }; +}; \ No newline at end of file diff --git a/src/components/apollo-client/queries/getBudgetsByParams.js b/src/components/apollo-client/queries/getBudgetsByParams.js new file mode 100644 index 00000000..b7f05c9c --- /dev/null +++ b/src/components/apollo-client/queries/getBudgetsByParams.js @@ -0,0 +1,39 @@ +import { gql, useQuery } from "@apollo/client"; +import { useState, useEffect } from 'react'; + +export const GET_BUDGETS_BY_PARAMS = gql` +query GetBudgetsByParams($month: String!, $category: String!, $email: String!) { + user(email: $email) { + id + budgets(month: $month, category: $category) { + id + month + category + amount + pctRemaining + amountRemaining + } + expenses(category: $category, month: $month) { + id + amount + date + category + } + } +}` + +export const useGetBudgetsByParams = ( month, category, email ) => { + const { loading, error, data } = useQuery(GET_BUDGETS_BY_PARAMS, { + variables: { month: month, category: category, email: email }, + }); + + let budgetsData = null; + if (!loading && data) { + budgetsData = { + budgets: data.user?.budgets, + expenses: data.user?.expenses, + }; + } + + return { loading, error, budgetsData }; +}; \ No newline at end of file diff --git a/src/components/apollo-client/queries/getExpenses.js b/src/components/apollo-client/queries/getExpenses.js index 9a368068..dd1c33d3 100644 --- a/src/components/apollo-client/queries/getExpenses.js +++ b/src/components/apollo-client/queries/getExpenses.js @@ -13,10 +13,12 @@ query GetExpenses($email: String!) { export const useGetExpenses = (email) => { const { loading, error, data } = useQuery(GET_EXPENSES, { variables: { email: email }, + fetchPolicy: "no-cache", }); let totalExpensesData = null; if (!loading && data) { totalExpensesData = data?.user?.currentExpenses?.amount; } + console.log("totalExpensesData", totalExpensesData); return { loading, error, totalExpensesData }; }; \ No newline at end of file diff --git a/src/components/sample-data/BudgetsData.json b/src/components/sample-data/BudgetsData.json new file mode 100644 index 00000000..fff00c4c --- /dev/null +++ b/src/components/sample-data/BudgetsData.json @@ -0,0 +1,31 @@ +{ + "data": { + "user": { + "id": "2", + "budgets": [ + { + "id": "27", + "month": "2024-02", + "category": "Groceries", + "amount": 350.00, + "pctRemaining": 33.2, + "amountRemaining": 99.50 + } + ], + "expenses": [ + { + "id": "1", + "amount": 75.00, + "date": "2024-02-07", + "category": "Groceries" + }, + { + "id": "2", + "amount": 125.50, + "date": "2024-02-15", + "category": "Groceries" + } + ] + } + } +} \ No newline at end of file diff --git a/src/fetchCalls.js b/src/fetchCalls.js index 9f0ba248..d71c080f 100644 --- a/src/fetchCalls.js +++ b/src/fetchCalls.js @@ -4,6 +4,7 @@ import { GET_EXPENSES } from "./queries/getExpenses"; import { GET_INCOMES } from "./queries/getIncomes"; import { GET_TRANSACTIONS } from "./queries/getTransactions"; import { GET_USER_CASH_FLOW } from "./queries/getUserCashFlow"; +import { GET_BUDGETS_BY_PARAMS } from "./queries/getBudgetsByParams"; function GetUser({ email }) { const { loading, error, data } = useQuery(GET_USER, { @@ -92,10 +93,52 @@ function GetUserCashFlow({ userId }) { ); } +function GetBudgetsByParams({ month, category, email }) { + const { loading, error, data } = useQuery(GET_BUDGETS_BY_PARAMS, { + variables: { month, category, email }, + }); + if (loading) return null; + if (error) return `Error! ${error}`; + + const { user } = data; + const budgets = user?.budgets || []; + const expenses = user?.expenses || []; + + console.log(budgets); + return ( +
+

Budgets and Expenses

+
+

Budgets:

+ {budgets.map((budget) => ( +
+

Month: {budget.month}

+

Category: {budget.category}

+

Amount: {budget.amount}

+

Remaining (%): {budget.pctRemaining}

+

Amount Remaining: {budget.amountRemaining}

+
+ ))} +
+
+

Expenses:

+ {expenses.map((expense) => ( +
+

Date: {expense.date}

+

Amount: {expense.amount}

+

Category: {expense.category}

+
+ ))} +
+
+); +} + export default { GetUser, GetExpenses, GetIncomes, GetTransactions, GetUserCashFlow, + GetBudgetsByParams };