From fdf754174263173d4a5ec3d05c94ce9ed1a2f4a6 Mon Sep 17 00:00:00 2001 From: Jane Kamata Date: Mon, 3 Nov 2025 14:45:21 -0500 Subject: [PATCH 1/8] Dashboard implementation mostly --- frontend/src/external/bcanSatchel/store.ts | 4 +- .../dashboard/Charts/BarYearGrantStatus.tsx | 95 +++++++++ .../dashboard/Charts/DonutMoneyApplied.tsx | 148 ++++++++++++++ .../Charts/GanttYearGrantTimeline.tsx | 77 +++++++ .../main-page/dashboard/Charts/KPICard.tsx | 53 +++++ .../main-page/dashboard/Charts/KPICards.tsx | 127 ++++++++++++ .../dashboard/Charts/LineChartSuccessRate.tsx | 190 ++++++++++++++++++ .../dashboard/Charts/SampleChart.tsx | 65 ------ .../Charts/StackedBarMoneyReceived.tsx | 149 ++++++++++++++ .../main-page/dashboard/CsvExportButton.tsx | 9 +- .../src/main-page/dashboard/Dashboard.tsx | 56 +++++- .../src/main-page/dashboard/DateFilter.tsx | 82 +++++--- .../main-page/dashboard/grantCalculations.ts | 6 +- .../main-page/dashboard/styles/Dashboard.css | 79 ++++++-- middle-layer/types/Status.ts | 9 + 15 files changed, 1025 insertions(+), 124 deletions(-) create mode 100644 frontend/src/main-page/dashboard/Charts/BarYearGrantStatus.tsx create mode 100644 frontend/src/main-page/dashboard/Charts/DonutMoneyApplied.tsx create mode 100644 frontend/src/main-page/dashboard/Charts/GanttYearGrantTimeline.tsx create mode 100644 frontend/src/main-page/dashboard/Charts/KPICard.tsx create mode 100644 frontend/src/main-page/dashboard/Charts/KPICards.tsx create mode 100644 frontend/src/main-page/dashboard/Charts/LineChartSuccessRate.tsx delete mode 100644 frontend/src/main-page/dashboard/Charts/SampleChart.tsx create mode 100644 frontend/src/main-page/dashboard/Charts/StackedBarMoneyReceived.tsx diff --git a/frontend/src/external/bcanSatchel/store.ts b/frontend/src/external/bcanSatchel/store.ts index 8586d70..58ed568 100644 --- a/frontend/src/external/bcanSatchel/store.ts +++ b/frontend/src/external/bcanSatchel/store.ts @@ -13,7 +13,7 @@ export interface AppState { startDateFilter: Date | null; endDateFilter: Date | null; searchQuery: string; - yearFilter:number[] | null; + yearFilter:number[]; } // Define initial state @@ -26,7 +26,7 @@ const initialState: AppState = { startDateFilter: null, endDateFilter: null, searchQuery: '', - yearFilter: null + yearFilter: [] }; const store = createStore('appStore', initialState); diff --git a/frontend/src/main-page/dashboard/Charts/BarYearGrantStatus.tsx b/frontend/src/main-page/dashboard/Charts/BarYearGrantStatus.tsx new file mode 100644 index 0000000..29ec8bc --- /dev/null +++ b/frontend/src/main-page/dashboard/Charts/BarYearGrantStatus.tsx @@ -0,0 +1,95 @@ +import { + BarChart, + Bar, + XAxis, + YAxis, + Tooltip, + ResponsiveContainer, + LabelList, +} from "recharts"; +import { observer } from "mobx-react-lite"; +import { aggregateMoneyGrantsByYear, YearAmount } from "../grantCalculations"; +import "../styles/Dashboard.css"; +import { Grant } from "../../../../../middle-layer/types/Grant"; + +const BarYearGrantStatus = observer( + ({ recentYear, grants }: { recentYear: number; grants: Grant[] }) => { + + // Filtering data for most receny year + const recentData = grants.filter( + (grant) => + new Date(grant.application_deadline).getFullYear() == recentYear + ); + + // Formatting data for chart + const data = aggregateMoneyGrantsByYear(recentData, "status") + .flatMap((grant: YearAmount) => + Object.entries(grant.data).map(([key, value]) => ({ + name: key, + value, + })) + ) + .sort((a, b) => b.value - a.value); + + return ( +
+ {/* Title */} +
+ Year Grant Status +
+ {/* Year */} +
{recentYear}
+ + + + `$${value / 1000}k`} + /> + + + typeof label === "number" ? `$${label / 1000}k` : label + } + /> + + `$${value.toLocaleString()}`} + /> + + +
+ ); + } +); + +export default BarYearGrantStatus; diff --git a/frontend/src/main-page/dashboard/Charts/DonutMoneyApplied.tsx b/frontend/src/main-page/dashboard/Charts/DonutMoneyApplied.tsx new file mode 100644 index 0000000..2efa950 --- /dev/null +++ b/frontend/src/main-page/dashboard/Charts/DonutMoneyApplied.tsx @@ -0,0 +1,148 @@ +import { PieChart, Pie, Tooltip, ResponsiveContainer } from "recharts"; +import { observer } from "mobx-react-lite"; +import { aggregateMoneyGrantsByYear, YearAmount } from "../grantCalculations"; +import "../styles/Dashboard.css"; +import { Grant } from "../../../../../middle-layer/types/Grant"; +import { getListApplied } from "../../../../../middle-layer/types/Status"; + +const DonutMoneyApplied = observer(({ grants }: { grants: Grant[] }) => { + + // Helper to sum values for given statuses + const sumByStatus = (data: Record, statuses: string[]) => + Object.entries(data) + .filter(([status]) => statuses.includes(status)) + .reduce((sum, [, value]) => sum + value, 0); + + // Aggregate money by year + const dataMoney = aggregateMoneyGrantsByYear(grants, "status").map( + (grant: YearAmount) => ({ + year: grant.year.toString(), + received: sumByStatus(grant.data, getListApplied(true)), + unreceived: sumByStatus(grant.data, getListApplied(false)), + }) + ); + + // Summing values across years + const [sumReceived, sumUnreceived] = dataMoney.reduce( + ([sumR, sumU], { received, unreceived }) => [ + sumR + received, + sumU + unreceived, + ], + [0, 0] + ); + const total = sumReceived + sumUnreceived; + const data = [ + { name: "Received", value: sumReceived, fill: "#F8CC16" }, + { name: "Unreceived", value: sumUnreceived, fill: "#F58D5C" }, + ]; + + // Creating the label for the slices + const LabelItem = ({ + name, + value, + percent, + color, + }: { + name: string; + value: number; + percent: number; + color: string; + }) => { + return ( +
+
{name}
+
+
+ {`${(percent * 100).toFixed(0)}% ($${(value / 1_000_000).toFixed( + 2 + )}M)`} +
+
+ ); + }; + + return ( +
+
+ {/* Title */} +
+ Money Applied For {/* Total Amount */} +
+ {`$${((sumReceived + sumUnreceived) / 1000000).toLocaleString( + "en-us", + { + maximumFractionDigits: 2, + } + )}M`} +
+ {/* Floating Right Label */} + {sumUnreceived > 0 && ( +
+ +
+ )} + {/* Floating Left Label */} + {sumReceived > 0 && ( +
+ +
+ )} +
+
+ + + + [ + `$${value.toLocaleString()}`, + name, + ]} + contentStyle={{ + borderRadius: "12px", + backgroundColor: "#fff", + border: "1px solid #ccc", + boxShadow: "0 2px 8px rgba(0,0,0,0.1)", + }} + /> + + +
+ ); +}); + +export default DonutMoneyApplied; diff --git a/frontend/src/main-page/dashboard/Charts/GanttYearGrantTimeline.tsx b/frontend/src/main-page/dashboard/Charts/GanttYearGrantTimeline.tsx new file mode 100644 index 0000000..2712681 --- /dev/null +++ b/frontend/src/main-page/dashboard/Charts/GanttYearGrantTimeline.tsx @@ -0,0 +1,77 @@ +import { observer } from "mobx-react-lite"; +import { Grant } from "../../../../../middle-layer/types/Grant"; + +export const GanttYearGrantTimeline = observer( + ({ recentYear, grants }: { recentYear: number; grants: Grant[] }) => { + // Filter grants for the selected year + // const recentData = grants.filter( + // (grant) => + // new Date(grant.application_deadline).getFullYear() === recentYear + // ); + + // const data: (string | Date | number | null)[][] = [ + // [ + // "Task ID", + // "Task Name", + // "Resource ID", + // "Start Date", + // "End Date", + // "Duration", + // "Percent Complete", + // "Dependencies", + // ], + // ...recentData.map((grant) => { + // const deadline = new Date(grant.application_deadline); + // const startDate = new Date(deadline.getFullYear(), deadline.getMonth(), deadline.getDate() - 14); + // const endDate = new Date(deadline.getFullYear(), deadline.getMonth(), deadline.getDate()); + + // return [ + // String(grant.grantId), // Task ID must be string + // `${grant.organization} (${grant.status}) $${grant.amount}`, // Task Name + // null, // Resource ID + // startDate, // Start Date + // endDate, // End Date + // 0, // Duration (null) + // 100, // Percent Complete + // null, // Dependencies + // ]; + // }), + // ]; + + // const options = { + // height: recentData.length * 50 + 50, + // gantt: { + // trackHeight: 30, + // barHeight: 20, + // criticalPathEnabled: false, + // labelStyle: { + // fontName: "Arial", + // fontSize: 12, + // color: "#000", + // }, + // palette: [ + // { + // color: "#f58d5c", // All bars same color + // dark: "#f58d5c", + // light: "#f58d5c", + // }, + // ], + // }, + // }; + + return ( +
+ {/* Title */} +
+ Year Grant Timeline +
+ {/* Year */} +
{recentYear}
+ +
{grants.length}
+
+ ); + } +); + +export default GanttYearGrantTimeline; diff --git a/frontend/src/main-page/dashboard/Charts/KPICard.tsx b/frontend/src/main-page/dashboard/Charts/KPICard.tsx new file mode 100644 index 0000000..7f18d14 --- /dev/null +++ b/frontend/src/main-page/dashboard/Charts/KPICard.tsx @@ -0,0 +1,53 @@ +import { observer } from "mobx-react-lite"; +import { FaArrowTrendUp, FaArrowTrendDown } from "react-icons/fa6"; + +const KPICard = observer( + ({ + title, + recentYear, + priorYear, + formattedValue, + percentChange, + }: { + title: string; + recentYear: number; + priorYear: number; + formattedValue: string; + percentChange: number; + }) => { + return ( +
+ {/* Title */} +
{title}
+ + {/* Value and Percent Change */} +
+
+ {formattedValue} +
+ + {priorYear && ( +
+ {percentChange >= 0 + ? `+${percentChange.toFixed(0)}%` + : `-${Math.abs(percentChange).toFixed(0)}%`} + {percentChange >= 0 ? ( + + ) : ( + + )} +
+ )} +
+ + {/* Year comparison at bottom */} +
+ {recentYear} + {priorYear ? ` vs. ${priorYear}` : ""} +
+
+ ); + } +); + +export default KPICard; diff --git a/frontend/src/main-page/dashboard/Charts/KPICards.tsx b/frontend/src/main-page/dashboard/Charts/KPICards.tsx new file mode 100644 index 0000000..191f909 --- /dev/null +++ b/frontend/src/main-page/dashboard/Charts/KPICards.tsx @@ -0,0 +1,127 @@ +import { Grant } from "../../../../../middle-layer/types/Grant"; +import { aggregateMoneyGrantsByYear, YearAmount } from "../grantCalculations"; +import KPICard from "./KPICard"; +import "../styles/Dashboard.css"; +import { observer } from "mobx-react-lite"; +import { getListApplied } from "../../../../../middle-layer/types/Status"; + +const KPICards = observer( + ({ + grants, + recentYear, + priorYear, + }: { + grants: Grant[]; + recentYear: number; + priorYear: number; + }) => { + // Helper to sum values for given statuses + const sumByStatus = (data: Record, statuses: string[]) => + Object.entries(data) + .filter(([status]) => statuses.includes(status)) + .reduce((sum, [, value]) => sum + value, 0); + + // Aggregate money by year + const dataMoney = aggregateMoneyGrantsByYear(grants, "status").map( + (grant: YearAmount) => ({ + year: grant.year, + received: sumByStatus(grant.data, getListApplied(true)), + unreceived: sumByStatus(grant.data, getListApplied(false)), + }) + ); + + // Get metrics for a specific year + const getYearMetrics = (year: number) => { + const entry = dataMoney.find((d) => d.year === year); + if (!entry) + return { + moneyReceived: 0, + moneyUnreceived: 0, + countReceived: 0, + countUnreceived: 0, + }; + + const { received, unreceived } = entry; + return { + moneyReceived: received, + moneyUnreceived: unreceived, + countReceived: received > 0 ? 1 : 0, + countUnreceived: unreceived > 0 ? 1 : 0, + }; + }; + + const recent = getYearMetrics(recentYear); + const prior = getYearMetrics(priorYear); + + // Helper: percent change formula + const percentChange = (current: number, previous: number) => { + console.log("current:", current, "previous:", previous); + return previous === 0 ? 0 : ((current - previous) / previous) * 100; + }; + + // KPIs + const grantsAppliedRecent = recent.countReceived + recent.countUnreceived; + const grantsAppliedPrior = prior.countReceived + prior.countUnreceived; + + const grantsReceivedRecent = recent.countReceived; + const grantsReceivedPrior = prior.countReceived; + + const moneyCapturedRecent = + recent.moneyReceived + recent.moneyUnreceived > 0 + ? (recent.moneyReceived / + (recent.moneyReceived + recent.moneyUnreceived)) * + 100 + : 0; + const moneyCapturedPrior = + prior.moneyReceived + prior.moneyUnreceived > 0 + ? (prior.moneyReceived / + (prior.moneyReceived + prior.moneyUnreceived)) * + 100 + : 0; + + const avgAmountRecent = + recent.countReceived > 0 + ? recent.moneyReceived / recent.countReceived + : 0; + const avgAmountPrior = + prior.countReceived > 0 ? prior.moneyReceived / prior.countReceived : 0; + + return ( +
+ + + + +
+ ); + } +); + +export default KPICards; diff --git a/frontend/src/main-page/dashboard/Charts/LineChartSuccessRate.tsx b/frontend/src/main-page/dashboard/Charts/LineChartSuccessRate.tsx new file mode 100644 index 0000000..d081eea --- /dev/null +++ b/frontend/src/main-page/dashboard/Charts/LineChartSuccessRate.tsx @@ -0,0 +1,190 @@ +import React from "react"; +import { + LineChart, + Line, + CartesianGrid, + XAxis, + YAxis, + Tooltip, + Legend, + ResponsiveContainer, +} from "recharts"; +import { observer } from "mobx-react-lite"; +import { + aggregateMoneyGrantsByYear, + aggregateCountGrantsByYear, + YearAmount, +} from "../grantCalculations"; +import "../styles/Dashboard.css"; +import { Grant } from "../../../../../middle-layer/types/Grant"; +import { getListApplied } from "../../../../../middle-layer/types/Status"; + +const LineChartSuccessRate = observer(({ grants }: { grants: Grant[] }) => { + // Wrap Legend with a React component type to satisfy JSX typing + const LegendComp = Legend as unknown as React.ComponentType; + + // Get status lists for received and unreceived + const moneyReceived = getListApplied(true); + const moneyUnreceived = getListApplied(false); + + // Formatting money data + const data_money = aggregateMoneyGrantsByYear(grants, "status").map( + (grant: YearAmount) => { + const received = Object.entries(grant.data) + .filter(([status]) => moneyReceived.includes(status)) + .reduce((sum, [, value]) => sum + value, 0); + + const unreceived = Object.entries(grant.data) + .filter(([status]) => moneyUnreceived.includes(status)) + .reduce((sum, [, value]) => sum + value, 0); + + const captured = + received + unreceived > 0 ? received / (received + unreceived) : 0; + + // Convert year → date for time series (e.g. "2024" → "2024-01-02") + return { + date: new Date(`${grant.year}-01-02`), + money_captured: Number(captured.toFixed(2)), + }; + } + ); + + // Formatting count data + const data_count = aggregateCountGrantsByYear(grants, "status").map( + (grant: YearAmount) => { + const received = Object.entries(grant.data) + .filter(([status]) => moneyReceived.includes(status)) + .reduce((sum, [, value]) => sum + value, 0); + + const unreceived = Object.entries(grant.data) + .filter(([status]) => moneyUnreceived.includes(status)) + .reduce((sum, [, value]) => sum + value, 0); + + const captured = + received + unreceived > 0 ? received / (received + unreceived) : 0; + + // Convert year → date for time series (e.g. "2024" → "2024-01-02") + return { + date: new Date(`${grant.year}-01-02`), + grants_captured: Number(captured.toFixed(2)), + }; + } + ); + + // Merging the data into format for chart + const data = data_money.map((moneyItem) => { + const countItem = data_count.find( + (c) => c.date.getFullYear() === moneyItem.date.getFullYear() + ); + return { + date: moneyItem.date, + money_captured: moneyItem.money_captured, + grants_captured: countItem?.grants_captured ?? 0, + }; + }); + + // Sort by date to ensure correct line order + data.sort((a, b) => a.date.getTime() - b.date.getTime()); + + return ( +
+ {/* Title */} +
+ Success Rate by Year +
+ + + + > + | Iterable + | React.ReactPortal + | null + | undefined + ) => ( + + {value} + + )} + /> + + + + + date.getFullYear().toString()} + axisLine={false} + tickLine={false} + /> + + `${(value * 100).toFixed(0)}%`} + /> + + date.getFullYear().toString()} + formatter={(value: number) => `${(value * 100).toFixed(0)}%`} + /> + + +
+ ); +}); + +export default LineChartSuccessRate; diff --git a/frontend/src/main-page/dashboard/Charts/SampleChart.tsx b/frontend/src/main-page/dashboard/Charts/SampleChart.tsx deleted file mode 100644 index 6ad2e26..0000000 --- a/frontend/src/main-page/dashboard/Charts/SampleChart.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import React from "react"; -import { - BarChart, - Bar, - CartesianGrid, - XAxis, - YAxis, - Tooltip, - Legend, -} from "recharts"; -import { observer } from "mobx-react-lite"; -import { ProcessGrantData } from "../../../main-page/grants/filter-bar/processGrantData"; -import { aggregateMoneyGrantsByYear, YearAmount } from "../grantCalculations"; - -const SampleChart: React.FC = observer(() => { - const { grants } = ProcessGrantData(); - // Wrap Legend with a React component type to satisfy JSX typing - const LegendComp = Legend as unknown as React.ComponentType; - const data = aggregateMoneyGrantsByYear(grants, "status").map( - (grant: YearAmount) => ({ - name: grant.year.toString(), - active: grant.Active, - inactive: grant.Inactive, - }) - ); - - return ( -
- - - - - - `$${value / 1000}k`} - /> - `$${value.toLocaleString()}`} /> - - -
- ); -}); - -export default SampleChart; diff --git a/frontend/src/main-page/dashboard/Charts/StackedBarMoneyReceived.tsx b/frontend/src/main-page/dashboard/Charts/StackedBarMoneyReceived.tsx new file mode 100644 index 0000000..13448fc --- /dev/null +++ b/frontend/src/main-page/dashboard/Charts/StackedBarMoneyReceived.tsx @@ -0,0 +1,149 @@ +import React from "react"; +import { + BarChart, + Bar, + CartesianGrid, + XAxis, + YAxis, + Tooltip, + Legend, + ResponsiveContainer, + LabelList, +} from "recharts"; +import { observer } from "mobx-react-lite"; +import { aggregateMoneyGrantsByYear, YearAmount } from "../grantCalculations"; +import "../styles/Dashboard.css"; +import { Grant } from "../../../../../middle-layer/types/Grant"; +import { getListApplied } from "../../../../../middle-layer/types/Status"; + +const StackedBarMoneyReceived = observer(({ grants }: { grants: Grant[] }) => { + // Wrap Legend with a React component type to satisfy JSX typing + const LegendComp = Legend as unknown as React.ComponentType; + + // Formatting data for chart + const data = aggregateMoneyGrantsByYear(grants, "status").map( + (grant: YearAmount) => { + const received = Object.entries(grant.data) + .filter(([status]) => getListApplied(true).includes(status)) + .reduce((sum, [, value]) => sum + value, 0); + + const unreceived = Object.entries(grant.data) + .filter(([status]) => getListApplied(false).includes(status)) + .reduce((sum, [, value]) => sum + value, 0); + + return { + name: grant.year.toString(), + received, + unreceived, + }; + } + ); + + return ( +
+ {/* Title */} +
+ Money Received by Year +
+ + + + > + | Iterable + | React.ReactPortal + | null + | undefined + ) => ( + + {value} + + )} + /> + + + + typeof label === "number" && label > 0 + ? `$${label / 1000}k` + : "" + } + /> + + + + typeof label === "number" && label > 0 + ? `$${label / 1000}k` + : "" + } + /> + + + `$${value / 1000}k`} + /> + `$${value.toLocaleString()}`} + /> + + +
+ ); +}); + +export default StackedBarMoneyReceived; diff --git a/frontend/src/main-page/dashboard/CsvExportButton.tsx b/frontend/src/main-page/dashboard/CsvExportButton.tsx index 650883d..9f36a0f 100644 --- a/frontend/src/main-page/dashboard/CsvExportButton.tsx +++ b/frontend/src/main-page/dashboard/CsvExportButton.tsx @@ -1,11 +1,11 @@ import { useState } from "react"; import { downloadCsv, CsvColumn } from "../../utils/csvUtils"; import { Grant } from "../../../../middle-layer/types/Grant"; -import { ProcessGrantData } from "../../main-page/grants/filter-bar/processGrantData"; +import { ProcessGrantData } from "../../main-page/grants/filter-bar/processGrantData"; import { observer } from "mobx-react-lite"; import "../grants/styles/GrantButton.css"; import { getAppStore } from "../../external/bcanSatchel/store"; - +import { BiExport } from "react-icons/bi"; // Define the columns for the CSV export, including any necessary formatting. const columns: CsvColumn[] = [ { key: "grantId", title: "Grant ID" }, @@ -105,13 +105,14 @@ const CsvExportButton: React.FC = observer(() => { return ( ); }); diff --git a/frontend/src/main-page/dashboard/Dashboard.tsx b/frontend/src/main-page/dashboard/Dashboard.tsx index 910ea10..968affe 100644 --- a/frontend/src/main-page/dashboard/Dashboard.tsx +++ b/frontend/src/main-page/dashboard/Dashboard.tsx @@ -3,7 +3,7 @@ import CsvExportButton from "./CsvExportButton"; import DateFilter from "./DateFilter"; import "./styles/Dashboard.css"; import { observer } from "mobx-react-lite"; -import SampleChart from "./Charts/SampleChart"; +import StackedBarMoneyReceived from "./Charts/StackedBarMoneyReceived"; import { useEffect } from "react"; import { updateYearFilter, @@ -11,6 +11,13 @@ import { updateEndDateFilter, updateStartDateFilter, } from "../../external/bcanSatchel/actions"; +import { getAppStore } from "../../external/bcanSatchel/store"; +import BarYearGrantStatus from "./Charts/BarYearGrantStatus"; +import LineChartSuccessRate from "./Charts/LineChartSuccessRate"; +import GanttYearGrantTimeline from "./Charts/GanttYearGrantTimeline"; +import DonutMoneyApplied from "./Charts/DonutMoneyApplied"; +import { ProcessGrantData } from "../grants/filter-bar/processGrantData"; +import KPICards from "./Charts/KPICards"; const Dashboard = observer(() => { // reset filters on initial render @@ -21,11 +28,50 @@ const Dashboard = observer(() => { updateStartDateFilter(null); }, []); + const { yearFilter, allGrants } = getAppStore(); + + const uniqueYears = Array.from( + new Set( + yearFilter?.length > 0 + ? yearFilter + : allGrants.map((g) => new Date(g.application_deadline).getFullYear()) + ) + ).sort((a, b) => b - a); + + const recentYear = uniqueYears[0]; + const priorYear = uniqueYears[1]; + + console.log("Recent Year:", recentYear, "Prior Year:", priorYear); + + const { grants } = ProcessGrantData(); + return ( -
- - - +
+
+ + +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
); }); diff --git a/frontend/src/main-page/dashboard/DateFilter.tsx b/frontend/src/main-page/dashboard/DateFilter.tsx index bee90e9..ee8112a 100644 --- a/frontend/src/main-page/dashboard/DateFilter.tsx +++ b/frontend/src/main-page/dashboard/DateFilter.tsx @@ -2,9 +2,11 @@ import { useState, useEffect } from "react"; import { updateYearFilter } from "../../external/bcanSatchel/actions"; import { getAppStore } from "../../external/bcanSatchel/store"; import { observer } from "mobx-react-lite"; +import { FaChevronDown } from "react-icons/fa"; const DateFilter: React.FC = observer(() => { const { allGrants, yearFilter } = getAppStore(); + const [showDropdown, setShowDropdown] = useState(false); // Generate unique years dynamically from grants const uniqueYears = Array.from( @@ -18,9 +20,9 @@ const DateFilter: React.FC = observer(() => { // Keep local selection in sync if store changes useEffect(() => { - (yearFilter && yearFilter.length) === 0 - ? setSelectedYears(uniqueYears) - : setSelectedYears(yearFilter ?? uniqueYears); + if (uniqueYears.length > 0 && selectedYears.length === 0) { + setSelectedYears(yearFilter?.length ? yearFilter : uniqueYears); + } }, [yearFilter, uniqueYears]); // Update local store and state on checkbox change @@ -35,31 +37,63 @@ const DateFilter: React.FC = observer(() => { updatedYears = selectedYears.filter((y) => y !== year); } - setSelectedYears(updatedYears); + setSelectedYears(updatedYears.sort((a, b) => a - b)); updateYearFilter(updatedYears); }; + // Update local store and state on checkbox change + const handleReset = () => { + setSelectedYears(uniqueYears); + updateYearFilter(uniqueYears); + setShowDropdown(false); + }; + return ( -
- {uniqueYears.map((year) => ( -
- - -
- ))} +
+ +
+
    + {uniqueYears.map((year) => ( +
  • +
    + + +
    +
  • + ))} +
+
+ +
); }); diff --git a/frontend/src/main-page/dashboard/grantCalculations.ts b/frontend/src/main-page/dashboard/grantCalculations.ts index 64cee7d..eadb206 100644 --- a/frontend/src/main-page/dashboard/grantCalculations.ts +++ b/frontend/src/main-page/dashboard/grantCalculations.ts @@ -2,7 +2,7 @@ import { Grant } from "../../../../middle-layer/types/Grant"; export type YearAmount = { year: number; - [key: string]: number; + data: {[key: string]: number}; }; /** @@ -29,7 +29,7 @@ export function aggregateMoneyGrantsByYear( return Object.entries(grouped) .map(([year, groups]) => ({ year: Number(year), - ...groups, + data:{...groups}, })) .sort((a, b) => a.year - b.year); } @@ -62,7 +62,7 @@ export function aggregateCountGrantsByYear( for (const [key, ids] of Object.entries(groups)) { counts[key] = ids.size; } - return { year: Number(year), ...counts }; + return { year: Number(year), data:{...counts }}; }) .sort((a, b) => a.year - b.year); } diff --git a/frontend/src/main-page/dashboard/styles/Dashboard.css b/frontend/src/main-page/dashboard/styles/Dashboard.css index 4a95e4e..045b4b2 100644 --- a/frontend/src/main-page/dashboard/styles/Dashboard.css +++ b/frontend/src/main-page/dashboard/styles/Dashboard.css @@ -1,28 +1,65 @@ -html, body { - margin: 0; - padding: 0; - height: 100%; - overflow-x: hidden; +html, +body { + margin: 0; + padding: 0; + height: 100%; + overflow-x: hidden; + font-size: medium; } /* Main container styling */ .dashboard-page { - display: flex; - flex-direction: column; - justify-content: flex-start; - background-color: #F2EBE4; - width: 100%; - height: 100%; - height: 100%; - overflow-y: auto; - padding-bottom: 1em; + display: flex; + flex-direction: column; + justify-content: flex-start; + background-color: #f2ebe4; + width: 100%; + height: 100%; + overflow-y: auto; + overflow-x: hidden; + padding-bottom: 1em; } .chart-container { - display: flex; - justify-content: center; - align-items: center; - padding: 12px; - border-radius: 1.2rem; - border: 0.1rem solid #E0E0E0; -} \ No newline at end of file + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: center; + padding: 24px; + background-color: #ffffff; + border-radius: 1.2rem; + border: 0.1rem solid black; +} + +.kpi-card { + background-color: #FFd5C2; +} + +.tooltip { + border-radius: 12px; + background-color: "#fffff"; + border: "1px solid #ccc"; + box-shadow: "0 2px 8px rgba(0,0,0,0.1)"; +} + +.axis { + font-size: 0.875rem; /* Tailwind's text-sm */ +} +.data { + white-space: nowrap; + border-radius: 16px; + background-color: #fff; + border: 1px solid #ccc; +} + +.my-custom-gantt-theme { + --wx-gantt-task-color: #4f81bd !important; /* persist bar color */ + --wx-gantt-task-border: none !important; + --wx-gantt-bar-border-radius: 8px !important; +} + +.my-custom-gantt-theme .wx-task { + background-color: var(--wx-gantt-task-color) !important; + border-radius: 8px !important; + overflow: hidden !important; +} diff --git a/middle-layer/types/Status.ts b/middle-layer/types/Status.ts index 44dbc44..56de810 100644 --- a/middle-layer/types/Status.ts +++ b/middle-layer/types/Status.ts @@ -39,4 +39,13 @@ export function getColorStatus(status: string) { case "Pending": return "#FFA500" // orange default: return 'Unknown'; } +} + +// Get list of status types for received and unreceived grants +export function getListApplied(received: boolean){ + if(received){ + return ["Active", "Inactive"] + } else{ + return ["Pending", "Rejected"] + } } \ No newline at end of file From 3ad8931bbf82065ae02226cb4e0b476668c381bc Mon Sep 17 00:00:00 2001 From: Jane Kamata Date: Mon, 3 Nov 2025 14:59:19 -0500 Subject: [PATCH 2/8] Fixing build error --- frontend/src/external/bcanSatchel/actions.ts | 2 +- frontend/src/external/bcanSatchel/store.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/external/bcanSatchel/actions.ts b/frontend/src/external/bcanSatchel/actions.ts index 306814f..253de1e 100644 --- a/frontend/src/external/bcanSatchel/actions.ts +++ b/frontend/src/external/bcanSatchel/actions.ts @@ -51,7 +51,7 @@ export const updateEndDateFilter = action ( ) export const updateYearFilter = action ( 'updateYearFilter', - (yearFilter: number[] | null) => ({yearFilter}) + (yearFilter: number[] | []) => ({yearFilter}) ) /** diff --git a/frontend/src/external/bcanSatchel/store.ts b/frontend/src/external/bcanSatchel/store.ts index 58ed568..205ed5c 100644 --- a/frontend/src/external/bcanSatchel/store.ts +++ b/frontend/src/external/bcanSatchel/store.ts @@ -13,7 +13,7 @@ export interface AppState { startDateFilter: Date | null; endDateFilter: Date | null; searchQuery: string; - yearFilter:number[]; + yearFilter:number[] | []; } // Define initial state From dfd6327bd8868279ee113a79a464bd431bf5454b Mon Sep 17 00:00:00 2001 From: Jane Kamata Date: Mon, 3 Nov 2025 15:18:08 -0500 Subject: [PATCH 3/8] Fixing build errors --- frontend/src/main-page/dashboard/Dashboard.tsx | 2 +- frontend/src/main-page/grants/GrantPage.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/main-page/dashboard/Dashboard.tsx b/frontend/src/main-page/dashboard/Dashboard.tsx index 968affe..7bc77dd 100644 --- a/frontend/src/main-page/dashboard/Dashboard.tsx +++ b/frontend/src/main-page/dashboard/Dashboard.tsx @@ -22,7 +22,7 @@ import KPICards from "./Charts/KPICards"; const Dashboard = observer(() => { // reset filters on initial render useEffect(() => { - updateYearFilter(null); + updateYearFilter([]); updateFilter(null); updateEndDateFilter(null); updateStartDateFilter(null); diff --git a/frontend/src/main-page/grants/GrantPage.tsx b/frontend/src/main-page/grants/GrantPage.tsx index c443193..4dfa18b 100644 --- a/frontend/src/main-page/grants/GrantPage.tsx +++ b/frontend/src/main-page/grants/GrantPage.tsx @@ -15,7 +15,7 @@ function GrantPage() { // reset filters on initial render useEffect(() => { - updateYearFilter(null); + updateYearFilter([]); updateFilter(null); updateEndDateFilter(null); updateStartDateFilter(null); From 2be5eb472bec28757a968a1c7b2e150480bedc13 Mon Sep 17 00:00:00 2001 From: Jane Kamata Date: Mon, 3 Nov 2025 16:00:36 -0500 Subject: [PATCH 4/8] Fixing donut labels --- .../dashboard/Charts/DonutMoneyApplied.tsx | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/frontend/src/main-page/dashboard/Charts/DonutMoneyApplied.tsx b/frontend/src/main-page/dashboard/Charts/DonutMoneyApplied.tsx index 2efa950..f45ebb6 100644 --- a/frontend/src/main-page/dashboard/Charts/DonutMoneyApplied.tsx +++ b/frontend/src/main-page/dashboard/Charts/DonutMoneyApplied.tsx @@ -6,8 +6,7 @@ import { Grant } from "../../../../../middle-layer/types/Grant"; import { getListApplied } from "../../../../../middle-layer/types/Status"; const DonutMoneyApplied = observer(({ grants }: { grants: Grant[] }) => { - - // Helper to sum values for given statuses + // Helper to sum values for given statuses const sumByStatus = (data: Record, statuses: string[]) => Object.entries(data) .filter(([status]) => statuses.includes(status)) @@ -49,18 +48,18 @@ const DonutMoneyApplied = observer(({ grants }: { grants: Grant[] }) => { color: string; }) => { return ( -
+
{name}
-
+
{`${(percent * 100).toFixed(0)}% ($${(value / 1_000_000).toFixed( 2 )}M)`} @@ -88,7 +87,7 @@ const DonutMoneyApplied = observer(({ grants }: { grants: Grant[] }) => {
{/* Floating Right Label */} {sumUnreceived > 0 && ( -
+
{ )} {/* Floating Left Label */} {sumReceived > 0 && ( -
+
Date: Mon, 3 Nov 2025 21:07:27 -0500 Subject: [PATCH 5/8] Adding toggle --- .../dashboard/Charts/BarYearGrantStatus.tsx | 69 +++++++++++++++---- 1 file changed, 57 insertions(+), 12 deletions(-) diff --git a/frontend/src/main-page/dashboard/Charts/BarYearGrantStatus.tsx b/frontend/src/main-page/dashboard/Charts/BarYearGrantStatus.tsx index 29ec8bc..1a96934 100644 --- a/frontend/src/main-page/dashboard/Charts/BarYearGrantStatus.tsx +++ b/frontend/src/main-page/dashboard/Charts/BarYearGrantStatus.tsx @@ -8,12 +8,18 @@ import { LabelList, } from "recharts"; import { observer } from "mobx-react-lite"; -import { aggregateMoneyGrantsByYear, YearAmount } from "../grantCalculations"; +import { + aggregateCountGrantsByYear, + aggregateMoneyGrantsByYear, + YearAmount, +} from "../grantCalculations"; import "../styles/Dashboard.css"; import { Grant } from "../../../../../middle-layer/types/Grant"; +import { useState } from "react"; const BarYearGrantStatus = observer( ({ recentYear, grants }: { recentYear: number; grants: Grant[] }) => { + const [checked, setChecked] = useState(true); // Filtering data for most receny year const recentData = grants.filter( @@ -22,7 +28,16 @@ const BarYearGrantStatus = observer( ); // Formatting data for chart - const data = aggregateMoneyGrantsByYear(recentData, "status") + const data_money = aggregateMoneyGrantsByYear(recentData, "status") + .flatMap((grant: YearAmount) => + Object.entries(grant.data).map(([key, value]) => ({ + name: key, + value, + })) + ) + .sort((a, b) => b.value - a.value); + + const data_count = aggregateCountGrantsByYear(recentData, "status") .flatMap((grant: YearAmount) => Object.entries(grant.data).map(([key, value]) => ({ name: key, @@ -33,15 +48,37 @@ const BarYearGrantStatus = observer( return (
- {/* Title */} -
- Year Grant Status +
+
+ {/* Title */} +
+ Year Grant Status +
+ {/* Year */} +
{recentYear}
+
+ {/* Toggle */} +
+ +
- {/* Year */} -
{recentYear}
@@ -57,7 +94,9 @@ const BarYearGrantStatus = observer( width="auto" hide key={grants.length} - tickFormatter={(value: number) => `$${value / 1000}k`} + tickFormatter={(value: number) => + checked ? `$${value / 1000}k` : `${value}` + } /> - typeof label === "number" ? `$${label / 1000}k` : label + typeof label === "number" + ? checked + ? `$${label / 1000}k` + : `${label}` + : label } /> @@ -83,7 +126,9 @@ const BarYearGrantStatus = observer( border: "1px solid #ccc", boxShadow: "0 2px 8px rgba(0,0,0,0.1)", }} - formatter={(value: number) => `$${value.toLocaleString()}`} + formatter={(value: number) => + checked ? `$${value.toLocaleString()}` : `${value}` + } /> From 3d3d38f368a805ff681cf282db5ad897745032e9 Mon Sep 17 00:00:00 2001 From: Jane Kamata Date: Mon, 3 Nov 2025 21:10:40 -0500 Subject: [PATCH 6/8] Remove console logs --- frontend/src/main-page/dashboard/Charts/KPICards.tsx | 1 - frontend/src/main-page/dashboard/Dashboard.tsx | 2 -- 2 files changed, 3 deletions(-) diff --git a/frontend/src/main-page/dashboard/Charts/KPICards.tsx b/frontend/src/main-page/dashboard/Charts/KPICards.tsx index 191f909..258d119 100644 --- a/frontend/src/main-page/dashboard/Charts/KPICards.tsx +++ b/frontend/src/main-page/dashboard/Charts/KPICards.tsx @@ -55,7 +55,6 @@ const KPICards = observer( // Helper: percent change formula const percentChange = (current: number, previous: number) => { - console.log("current:", current, "previous:", previous); return previous === 0 ? 0 : ((current - previous) / previous) * 100; }; diff --git a/frontend/src/main-page/dashboard/Dashboard.tsx b/frontend/src/main-page/dashboard/Dashboard.tsx index 7bc77dd..86f5092 100644 --- a/frontend/src/main-page/dashboard/Dashboard.tsx +++ b/frontend/src/main-page/dashboard/Dashboard.tsx @@ -41,8 +41,6 @@ const Dashboard = observer(() => { const recentYear = uniqueYears[0]; const priorYear = uniqueYears[1]; - console.log("Recent Year:", recentYear, "Prior Year:", priorYear); - const { grants } = ProcessGrantData(); return ( From 36f8d98725064c4dfc5092d6eff81701944dd6c7 Mon Sep 17 00:00:00 2001 From: Jane Kamata Date: Mon, 3 Nov 2025 21:17:17 -0500 Subject: [PATCH 7/8] Updating checkbox styling --- .../src/main-page/dashboard/DateFilter.tsx | 2 +- .../main-page/dashboard/styles/Dashboard.css | 34 +++++++++++++++++++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/frontend/src/main-page/dashboard/DateFilter.tsx b/frontend/src/main-page/dashboard/DateFilter.tsx index ee8112a..e1a941f 100644 --- a/frontend/src/main-page/dashboard/DateFilter.tsx +++ b/frontend/src/main-page/dashboard/DateFilter.tsx @@ -74,7 +74,7 @@ const DateFilter: React.FC = observer(() => {
Date: Mon, 3 Nov 2025 21:22:08 -0500 Subject: [PATCH 8/8] Updating styling for toggle --- frontend/src/main-page/dashboard/Charts/BarYearGrantStatus.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/main-page/dashboard/Charts/BarYearGrantStatus.tsx b/frontend/src/main-page/dashboard/Charts/BarYearGrantStatus.tsx index 1a96934..44618cb 100644 --- a/frontend/src/main-page/dashboard/Charts/BarYearGrantStatus.tsx +++ b/frontend/src/main-page/dashboard/Charts/BarYearGrantStatus.tsx @@ -68,6 +68,7 @@ const BarYearGrantStatus = observer( checked={checked} onChange={() => setChecked(!checked)} className="sr-only peer" + style={{display:"none"}} />