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 8586d70..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[] | 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..44618cb --- /dev/null +++ b/frontend/src/main-page/dashboard/Charts/BarYearGrantStatus.tsx @@ -0,0 +1,141 @@ +import { + BarChart, + Bar, + XAxis, + YAxis, + Tooltip, + ResponsiveContainer, + LabelList, +} from "recharts"; +import { observer } from "mobx-react-lite"; +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( + (grant) => + new Date(grant.application_deadline).getFullYear() == recentYear + ); + + // Formatting data for chart + 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, + value, + })) + ) + .sort((a, b) => b.value - a.value); + + return ( +
+
+
+ {/* Title */} +
+ Year Grant Status +
+ {/* Year */} +
{recentYear}
+
+ {/* Toggle */} +
+ +
+
+ + + + + checked ? `$${value / 1000}k` : `${value}` + } + /> + + + typeof label === "number" + ? checked + ? `$${label / 1000}k` + : `${label}` + : label + } + /> + + + checked ? `$${value.toLocaleString()}` : `${value}` + } + /> + + +
+ ); + } +); + +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..f45ebb6 --- /dev/null +++ b/frontend/src/main-page/dashboard/Charts/DonutMoneyApplied.tsx @@ -0,0 +1,147 @@ +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..258d119 --- /dev/null +++ b/frontend/src/main-page/dashboard/Charts/KPICards.tsx @@ -0,0 +1,126 @@ +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) => { + 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..86f5092 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,21 +11,65 @@ 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 useEffect(() => { - updateYearFilter(null); + updateYearFilter([]); updateFilter(null); updateEndDateFilter(null); 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]; + + const { grants } = ProcessGrantData(); + return ( -
- - - +
+
+ + +
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
); }); diff --git a/frontend/src/main-page/dashboard/DateFilter.tsx b/frontend/src/main-page/dashboard/DateFilter.tsx index bee90e9..e1a941f 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..48cc75f 100644 --- a/frontend/src/main-page/dashboard/styles/Dashboard.css +++ b/frontend/src/main-page/dashboard/styles/Dashboard.css @@ -1,28 +1,99 @@ -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; +} + +/* Hide the default checkbox */ +input[type="checkbox"] { + appearance: none; + -webkit-appearance: none; /* For Safari */ + margin: 0; /* Remove default margin */ + /* Additional styling for the custom checkbox container */ + width: 1.2em; + height: 1.2em; + border: 2px solid #ccc; + border-radius: 4px; + display: inline-block; + vertical-align: middle; + position: relative; +} + +/* Style the custom checkmark using a pseudo-element */ +input[type="checkbox"]::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%) rotate(45deg); + width: 0.4em; + height: 0.8em; + border: solid white; + border-width: 0 2px 2px 0; + display: none; /* Hidden by default */ +} + +input[type="checkbox"]:checked::before { + display: block; /* Show the checkmark */ + border-color:black; } /* 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/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); 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