From 91c3f7776c9b9419c856956ac9b7c593850ac682 Mon Sep 17 00:00:00 2001 From: Jane Kamata Date: Sun, 22 Mar 2026 16:47:31 -0400 Subject: [PATCH 1/4] Adding basic backend connections - need to update with actual endpoints --- frontend/src/external/bcanSatchel/actions.ts | 10 + frontend/src/external/bcanSatchel/mutators.ts | 12 + frontend/src/external/bcanSatchel/store.ts | 8 +- .../cash-flow/processCashflowData.ts | 57 +++++ .../cash-flow/processCashflowEditSave.ts | 211 ++++++++++++++++++ 5 files changed, 294 insertions(+), 4 deletions(-) create mode 100644 frontend/src/main-page/cash-flow/processCashflowData.ts create mode 100644 frontend/src/main-page/cash-flow/processCashflowEditSave.ts diff --git a/frontend/src/external/bcanSatchel/actions.ts b/frontend/src/external/bcanSatchel/actions.ts index d7be555a..3c4f28a3 100644 --- a/frontend/src/external/bcanSatchel/actions.ts +++ b/frontend/src/external/bcanSatchel/actions.ts @@ -3,6 +3,8 @@ import { Grant } from '../../../../middle-layer/types/Grant' import { User } from '../../../../middle-layer/types/User' import { Status } from '../../../../middle-layer/types/Status' import { Notification } from '../../../../middle-layer/types/Notification'; +import { CashflowCost } from '../../../../middle-layer/types/CashflowCost'; +import { CashflowRevenue } from '../../../../middle-layer/types/CashflowRevenue'; /** * Set whether the user is authenticated, update the user object, @@ -47,6 +49,14 @@ export const fetchAllGrants = action("fetchAllGrants", (grants: Grant[]) => ({ grants, })); +export const fetchCashflowRevenues = action("fetchCashflowRevenues", (revenues: CashflowRevenue[]) => ({ + revenues, +})); + +export const fetchCashflowCosts = action("fetchCashflowCosts", (costs: CashflowCost[]) => ({ + costs, +})); + export const updateFilter = action("updateFilter", (status: Status | null) => ({ status, })); diff --git a/frontend/src/external/bcanSatchel/mutators.ts b/frontend/src/external/bcanSatchel/mutators.ts index 95e16788..190cf596 100644 --- a/frontend/src/external/bcanSatchel/mutators.ts +++ b/frontend/src/external/bcanSatchel/mutators.ts @@ -16,6 +16,8 @@ import { updateUserQuery, updateUserSort, removeProfilePic, + fetchCashflowRevenues, + fetchCashflowCosts, } from './actions'; import { getAppStore, persistToSessionStorage } from './store'; import { setActiveUsers, setInactiveUsers } from './actions'; @@ -111,6 +113,16 @@ mutator(fetchAllGrants, (actionMessage) => { store.allGrants = actionMessage.grants; }); +mutator(fetchCashflowCosts, (actionMessage) => { + const store = getAppStore(); + store.costSources = actionMessage.costs; +}); + +mutator(fetchCashflowRevenues, (actionMessage) => { + const store = getAppStore(); + store.revenueSources = actionMessage.revenues; +}); + /** * Modifies satchel store to get updated version of the filter */ diff --git a/frontend/src/external/bcanSatchel/store.ts b/frontend/src/external/bcanSatchel/store.ts index d99637e0..f8524291 100644 --- a/frontend/src/external/bcanSatchel/store.ts +++ b/frontend/src/external/bcanSatchel/store.ts @@ -27,8 +27,8 @@ export interface AppState { userSort: {header: keyof User, sort: "asc" | "desc" | "none"} | null; notifications: Notification[]; userQuery: string; - revenueSource: CashflowRevenue[]; - costSource: CashflowCost[]; + revenueSources: CashflowRevenue[]; + costSources: CashflowCost[]; } // Define initial state @@ -52,8 +52,8 @@ const initialState: AppState = { sort: null, userSort: null, userQuery: '', - revenueSource: [], - costSource: [], + revenueSources: [], + costSources: [], }; /** diff --git a/frontend/src/main-page/cash-flow/processCashflowData.ts b/frontend/src/main-page/cash-flow/processCashflowData.ts new file mode 100644 index 00000000..f841a047 --- /dev/null +++ b/frontend/src/main-page/cash-flow/processCashflowData.ts @@ -0,0 +1,57 @@ +import { useEffect } from "react"; +import { getAppStore } from "../../external/bcanSatchel/store.ts"; +import { fetchCashflowCosts, fetchCashflowRevenues } from "../../external/bcanSatchel/actions.ts"; +import {CashflowRevenue} from "../../../../middle-layer/types/CashflowRevenue.ts"; +import {CashflowCost} from "../../../../middle-layer/types/CashflowCost.ts"; +import { api } from "../../api.ts"; + +// fetch line items +export const fetchCosts = async () => { + try { + // Need to replace with actual endpoint + const response = await api("/cost"); + if (!response.ok) { + throw new Error(`HTTP Error, Status: ${response.status}`); + } + const updatedCosts: CashflowCost[] = await response.json(); + fetchCashflowCosts(updatedCosts); + } catch (error) { + console.error("Error fetching costs:", error); + } +}; + +export const fetchRevenues = async () => { + try { + // Need to replace with actual endpoint + const response = await api("/revenue"); + if (!response.ok) { + throw new Error(`HTTP Error, Status: ${response.status}`); + } + const updatedRevenues: CashflowRevenue[] = await response.json(); + fetchCashflowRevenues(updatedRevenues); + } catch (error) { + console.error("Error fetching revenues:", error); + } +}; + + +// could contain callbacks for sorting and filtering line items +// stores state for list of costs/revenues +export const ProcessCashflowData = () => { + const { + costSources, + revenueSources + } = getAppStore(); + + // fetch costs on mount if empty + useEffect(() => { + if (costSources.length === 0) fetchCosts(); + }, [costSources.length]); + + // fetch revenues on mount if empty + useEffect(() => { + if (revenueSources.length === 0) fetchRevenues(); + }, [revenueSources.length]); + + return { costs: costSources, revenues: revenueSources }; +}; diff --git a/frontend/src/main-page/cash-flow/processCashflowEditSave.ts b/frontend/src/main-page/cash-flow/processCashflowEditSave.ts new file mode 100644 index 00000000..6a2358cd --- /dev/null +++ b/frontend/src/main-page/cash-flow/processCashflowEditSave.ts @@ -0,0 +1,211 @@ +import {CashflowRevenue} from "../../../../middle-layer/types/CashflowRevenue.ts"; +import {CashflowCost} from "../../../../middle-layer/types/CashflowCost.ts"; +import { api } from "../../api.ts"; +import { fetchCosts, fetchRevenues } from "./processCashflowData.ts"; + +// save a new revenue +export const createNewRevenue = async (newRevenue: CashflowRevenue) => { + try { + // Need to replace with acual endpoint + const response = await api("/revenue/new-revenue", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(newRevenue), + }); + + if (response.ok) { + await fetchRevenues(); + return { success: true }; + } else { + const errorData = await response.json(); + throw new Error(errorData.message || "Failed to create revenue"); + } + } catch (error) { + console.error("Error creating revenue:", error); + console.log(newRevenue); + console.log(newRevenue.installments); + return { + success: false, + error: + error instanceof Error + ? error.message + : "Server error. Please try again.", + }; + } +}; + +// save a new cost +export const createNewCost = async (newCost: CashflowCost) => { + try { + // Need to replace with acual endpoint + const response = await api("/cost/new-cost", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(newCost), + }); + + if (response.ok) { + await fetchCosts(); + return { success: true }; + } else { + const errorData = await response.json(); + throw new Error(errorData.message || "Failed to create cost"); + } + } catch (error) { + console.error("Error creating cost:", error); + console.log(newCost); + return { + success: false, + error: + error instanceof Error + ? error.message + : "Server error. Please try again.", + }; + } +}; + +export const saveRevenueEdits = async (updatedRevenue: CashflowRevenue) => { + try { + // Need to replace with acual endpoint + const response = await api("/revenue/save", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(updatedRevenue), + }); + + if (response.ok) { + await fetchRevenues(); + return { success: true }; + } else { + const errorData = await response.json(); + throw new Error(errorData.message || "Failed to update revenue"); + } + } catch (error) { + console.error("Error updating revenue:", error); + console.log(updatedRevenue); + return { + success: false, + error: + error instanceof Error + ? error.message + : "Server error. Please try again.", + }; + } +}; + +export const saveCostEdits = async (updatedCost: CashflowCost) => { + try { + // Need to replace with acual endpoint + const response = await api("/cost/save", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(updatedCost), + }); + + if (response.ok) { + await fetchCosts(); + return { success: true }; + } else { + const errorData = await response.json(); + throw new Error(errorData.message || "Failed to update cost"); + } + } catch (error) { + console.error("Error updating cost:", error); + console.log(updatedCost); + return { + success: false, + error: + error instanceof Error + ? error.message + : "Server error. Please try again.", + }; + } +}; + +export const deleteRevenue = async (revenueId: any) => { + // Need to replace with acual endpoint + try { + const response = await api(`/revenue/${revenueId}`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + }); + + console.log("Response status:", response.status); + console.log("Response ok:", response.ok); + + if (response.ok) { + console.log("✅ Revenue deleted successfully"); + // Refetch revenues to update UI + await fetchRevenues(); + } else { + // Get error details + const errorText = await response.text(); + console.error("❌ Error response:", errorText); + + let errorData; + try { + errorData = JSON.parse(errorText); + console.error("Parsed error:", errorData); + } catch { + console.error("Could not parse error response"); + } + } + } catch (err) { + console.error("=== EXCEPTION CAUGHT ==="); + console.error( + "Error type:", + err instanceof Error ? "Error" : typeof err + ); + console.error( + "Error message:", + err instanceof Error ? err.message : err + ); + console.error("Full error:", err); + } + }; + +export const deleteCost = async (costId: any) => { + // Need to replace with acual endpoint + try { + const response = await api(`/revenue/${costId}`, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + }); + + console.log("Response status:", response.status); + console.log("Response ok:", response.ok); + + if (response.ok) { + console.log("✅ Cost deleted successfully"); + // Refetch costs to update UI + await fetchCosts(); + } else { + // Get error details + const errorText = await response.text(); + console.error("❌ Error response:", errorText); + + let errorData; + try { + errorData = JSON.parse(errorText); + console.error("Parsed error:", errorData); + } catch { + console.error("Could not parse error response"); + } + } + } catch (err) { + console.error("=== EXCEPTION CAUGHT ==="); + console.error( + "Error type:", + err instanceof Error ? "Error" : typeof err + ); + console.error( + "Error message:", + err instanceof Error ? err.message : err + ); + console.error("Full error:", err); + } + }; \ No newline at end of file From 103ba909c500f79fe36f767a5791cb6193e77c0d Mon Sep 17 00:00:00 2001 From: Jane Kamata Date: Sun, 22 Mar 2026 16:51:29 -0400 Subject: [PATCH 2/4] Adding comments --- frontend/src/main-page/cash-flow/processCashflowData.ts | 3 +++ ...ocessCashflowEditSave.ts => processCashflowDataEditSave.ts} | 3 +++ 2 files changed, 6 insertions(+) rename frontend/src/main-page/cash-flow/{processCashflowEditSave.ts => processCashflowDataEditSave.ts} (97%) diff --git a/frontend/src/main-page/cash-flow/processCashflowData.ts b/frontend/src/main-page/cash-flow/processCashflowData.ts index f841a047..b9abd30b 100644 --- a/frontend/src/main-page/cash-flow/processCashflowData.ts +++ b/frontend/src/main-page/cash-flow/processCashflowData.ts @@ -5,6 +5,9 @@ import {CashflowRevenue} from "../../../../middle-layer/types/CashflowRevenue.ts import {CashflowCost} from "../../../../middle-layer/types/CashflowCost.ts"; import { api } from "../../api.ts"; +// This has not been tested yet but the basic structure when implemented should be the same +// Mirrored format for processGrantData.ts + // fetch line items export const fetchCosts = async () => { try { diff --git a/frontend/src/main-page/cash-flow/processCashflowEditSave.ts b/frontend/src/main-page/cash-flow/processCashflowDataEditSave.ts similarity index 97% rename from frontend/src/main-page/cash-flow/processCashflowEditSave.ts rename to frontend/src/main-page/cash-flow/processCashflowDataEditSave.ts index 6a2358cd..f529210a 100644 --- a/frontend/src/main-page/cash-flow/processCashflowEditSave.ts +++ b/frontend/src/main-page/cash-flow/processCashflowDataEditSave.ts @@ -3,6 +3,9 @@ import {CashflowCost} from "../../../../middle-layer/types/CashflowCost.ts"; import { api } from "../../api.ts"; import { fetchCosts, fetchRevenues } from "./processCashflowData.ts"; +// This has not been tested yet but the basic structure when implemented should be the same +// Mirrored format for processGrantDataEditSave.ts + // save a new revenue export const createNewRevenue = async (newRevenue: CashflowRevenue) => { try { From a323bc25da8a45dd3f3d4a42c5494525b09e3395 Mon Sep 17 00:00:00 2001 From: Jane Kamata Date: Sun, 22 Mar 2026 17:03:13 -0400 Subject: [PATCH 3/4] Adding basic structures to connect to store --- .../src/main-page/cash-flow/CashFlowPage.tsx | 24 +++++++++++-------- .../cash-flow/components/CashFlowCard.tsx | 2 +- .../cash-flow/components/CashProjection.tsx | 11 +++++++-- .../components/CashProjectionChart.tsx | 9 ++++++- .../cash-flow/components/CashSourceList.tsx | 5 +++- 5 files changed, 36 insertions(+), 15 deletions(-) diff --git a/frontend/src/main-page/cash-flow/CashFlowPage.tsx b/frontend/src/main-page/cash-flow/CashFlowPage.tsx index 1fc1da5e..e275a764 100644 --- a/frontend/src/main-page/cash-flow/CashFlowPage.tsx +++ b/frontend/src/main-page/cash-flow/CashFlowPage.tsx @@ -1,5 +1,5 @@ import { observer } from "mobx-react-lite"; -import CashFlowCard from "./components/CashFlowCard"; +import CashflowKPICard from "./components/CashFlowCard"; import { faDollarSign, faArrowTrendUp, @@ -9,33 +9,37 @@ import "../dashboard/styles/Dashboard.css"; import CashPosition from "./components/CashPosition"; import CashAnnualSettings from "./components/CashAnnualSettings"; import CashProjection from "./components/CashProjection"; -import CashAdd from "./components/CashCreateLineItem"; import CashSourceList from "./components/CashSourceList"; +import { ProcessCashflowData } from "./processCashflowData"; +import CashCreateLineItem from "./components/CashCreateLineItem"; const CashFlowPage = observer(() => { + + const { costs, revenues } = ProcessCashflowData(); + return (
{/* Row 1 */} - - - - { {/* Row 3 */} - - + + {/* Row 4 */} - - + +
); diff --git a/frontend/src/main-page/cash-flow/components/CashFlowCard.tsx b/frontend/src/main-page/cash-flow/components/CashFlowCard.tsx index a24c88e6..d08b8f91 100644 --- a/frontend/src/main-page/cash-flow/components/CashFlowCard.tsx +++ b/frontend/src/main-page/cash-flow/components/CashFlowCard.tsx @@ -9,7 +9,7 @@ type CardProps = { size?: "small" | "medium"; }; -export default function CashFlowCard({ +export default function CashflowKPICard({ text, value, className, diff --git a/frontend/src/main-page/cash-flow/components/CashProjection.tsx b/frontend/src/main-page/cash-flow/components/CashProjection.tsx index 253f047a..9fd2aa41 100644 --- a/frontend/src/main-page/cash-flow/components/CashProjection.tsx +++ b/frontend/src/main-page/cash-flow/components/CashProjection.tsx @@ -1,6 +1,13 @@ +import { CashflowCost } from "../../../../../middle-layer/types/CashflowCost"; +import { CashflowRevenue } from "../../../../../middle-layer/types/CashflowRevenue"; import CashProjectionChart from "./CashProjectionChart"; -export default function CashProjection() { +type ProjectionProps = { + costs: CashflowCost[]; + revenues: CashflowRevenue[]; +}; + +export default function CashProjection({costs, revenues}:ProjectionProps) { // replace with actual data const cards = [ @@ -15,7 +22,7 @@ export default function CashProjection() {
{"36-Month Cash Flow Projection"}
- +
{cards.map((c) => (
{ +type ChartProps = { + costs: CashflowCost[]; + revenues: CashflowRevenue[]; +}; + +const CashProjectionChart = observer(({}: ChartProps) => { // replace with actual data, filter for 36 months const data = [ diff --git a/frontend/src/main-page/cash-flow/components/CashSourceList.tsx b/frontend/src/main-page/cash-flow/components/CashSourceList.tsx index 6977061b..bdd2195f 100644 --- a/frontend/src/main-page/cash-flow/components/CashSourceList.tsx +++ b/frontend/src/main-page/cash-flow/components/CashSourceList.tsx @@ -1,10 +1,13 @@ +import { CashflowCost } from "../../../../../middle-layer/types/CashflowCost"; +import { CashflowRevenue } from "../../../../../middle-layer/types/CashflowRevenue"; import CashEditLineItem from "./CashEditLineItem"; type SourceProps = { type: "Revenue" | "Cost"; + lineItems: CashflowRevenue[] | CashflowCost[]; }; -export default function CashSourceList({ type }: SourceProps) { +export default function CashSourceList({ type, lineItems }: SourceProps) { return (
From ab6e333117a09b6083267228ceb98390b8aee7a9 Mon Sep 17 00:00:00 2001 From: Jane Kamata Date: Sun, 22 Mar 2026 17:05:00 -0400 Subject: [PATCH 4/4] Small fixes --- frontend/src/main-page/cash-flow/CashFlowPage.tsx | 2 +- frontend/src/main-page/cash-flow/components/CashSourceList.tsx | 2 +- .../components/{CashFlowCard.tsx => CashflowKPICard.tsx} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename frontend/src/main-page/cash-flow/components/{CashFlowCard.tsx => CashflowKPICard.tsx} (100%) diff --git a/frontend/src/main-page/cash-flow/CashFlowPage.tsx b/frontend/src/main-page/cash-flow/CashFlowPage.tsx index e275a764..4cd441d6 100644 --- a/frontend/src/main-page/cash-flow/CashFlowPage.tsx +++ b/frontend/src/main-page/cash-flow/CashFlowPage.tsx @@ -1,5 +1,5 @@ import { observer } from "mobx-react-lite"; -import CashflowKPICard from "./components/CashFlowCard"; +import CashflowKPICard from "./components/CashflowKPICard"; import { faDollarSign, faArrowTrendUp, diff --git a/frontend/src/main-page/cash-flow/components/CashSourceList.tsx b/frontend/src/main-page/cash-flow/components/CashSourceList.tsx index bdd2195f..5246775a 100644 --- a/frontend/src/main-page/cash-flow/components/CashSourceList.tsx +++ b/frontend/src/main-page/cash-flow/components/CashSourceList.tsx @@ -7,7 +7,7 @@ type SourceProps = { lineItems: CashflowRevenue[] | CashflowCost[]; }; -export default function CashSourceList({ type, lineItems }: SourceProps) { +export default function CashSourceList({ type }: SourceProps) { return (
diff --git a/frontend/src/main-page/cash-flow/components/CashFlowCard.tsx b/frontend/src/main-page/cash-flow/components/CashflowKPICard.tsx similarity index 100% rename from frontend/src/main-page/cash-flow/components/CashFlowCard.tsx rename to frontend/src/main-page/cash-flow/components/CashflowKPICard.tsx