diff --git a/frontend/src/external/bcanSatchel/actions.ts b/frontend/src/external/bcanSatchel/actions.ts
index d7be555..3c4f28a 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 95e1678..190cf59 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 d99637e..f852429 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/CashFlowPage.tsx b/frontend/src/main-page/cash-flow/CashFlowPage.tsx
index 1fc1da5..4cd441d 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/CashflowKPICard";
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/CashProjection.tsx b/frontend/src/main-page/cash-flow/components/CashProjection.tsx
index 253f047..9fd2aa4 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 6977061..5246775 100644
--- a/frontend/src/main-page/cash-flow/components/CashSourceList.tsx
+++ b/frontend/src/main-page/cash-flow/components/CashSourceList.tsx
@@ -1,7 +1,10 @@
+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) {
diff --git a/frontend/src/main-page/cash-flow/components/CashFlowCard.tsx b/frontend/src/main-page/cash-flow/components/CashflowKPICard.tsx
similarity index 95%
rename from frontend/src/main-page/cash-flow/components/CashFlowCard.tsx
rename to frontend/src/main-page/cash-flow/components/CashflowKPICard.tsx
index a24c88e..d08b8f9 100644
--- a/frontend/src/main-page/cash-flow/components/CashFlowCard.tsx
+++ b/frontend/src/main-page/cash-flow/components/CashflowKPICard.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/processCashflowData.ts b/frontend/src/main-page/cash-flow/processCashflowData.ts
new file mode 100644
index 0000000..b9abd30
--- /dev/null
+++ b/frontend/src/main-page/cash-flow/processCashflowData.ts
@@ -0,0 +1,60 @@
+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";
+
+// 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 {
+ // 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/processCashflowDataEditSave.ts b/frontend/src/main-page/cash-flow/processCashflowDataEditSave.ts
new file mode 100644
index 0000000..f529210
--- /dev/null
+++ b/frontend/src/main-page/cash-flow/processCashflowDataEditSave.ts
@@ -0,0 +1,214 @@
+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";
+
+// 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 {
+ // 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