Skip to content

Commit 9bb57e1

Browse files
authored
Merge pull request #178 from Code-4-Community/176-dev---exporting-dashboard-data
176 dev exporting dashboard data
2 parents f5021ea + 9b29052 commit 9bb57e1

File tree

10 files changed

+258
-24
lines changed

10 files changed

+258
-24
lines changed

frontend/package-lock.json

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
"mobx-react-lite": "^4.1.0",
2626
"react": "^18.3.1",
2727
"react-app-polyfill": "^3.0.0",
28+
"react-csv": "^2.2.2",
2829
"react-datepicker": "^8.2.1",
2930
"react-dom": "^18.3.1",
3031
"react-icons": "^5.4.0",

frontend/src/main-page/dashboard/Charts/SampleChart.tsx

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
1-
import { BarChart, Bar, CartesianGrid, XAxis, YAxis, Tooltip } from "recharts";
1+
import React from "react";
2+
import {
3+
BarChart,
4+
Bar,
5+
CartesianGrid,
6+
XAxis,
7+
YAxis,
8+
Tooltip,
9+
Legend,
10+
} from "recharts";
211
import { observer } from "mobx-react-lite";
312
import { ProcessGrantData } from "../../../main-page/grants/filter-bar/processGrantData";
413
import { aggregateMoneyGrantsByYear, YearAmount } from "../grantCalculations";
514

615
const SampleChart: React.FC = observer(() => {
7-
const { grants } = ProcessGrantData();
16+
const { grants } = ProcessGrantData();
17+
// Wrap Legend with a React component type to satisfy JSX typing
18+
const LegendComp = Legend as unknown as React.ComponentType<any>;
819
const data = aggregateMoneyGrantsByYear(grants, "status").map(
920
(grant: YearAmount) => ({
1021
name: grant.year.toString(),
@@ -14,12 +25,12 @@ const SampleChart: React.FC = observer(() => {
1425
);
1526

1627
return (
17-
<div>
28+
<div className="chart-container">
1829
<BarChart
1930
width={600}
2031
height={300}
2132
data={data}
22-
margin={{ top: 5, right: 20, bottom: 5, left: 0 }}
33+
margin={{ top: 20, right: 30, left: 20, bottom: 5 }}
2334
>
2435
<CartesianGrid stroke="#aaa" strokeDasharray="5 5" />
2536
<Bar
@@ -28,22 +39,24 @@ const SampleChart: React.FC = observer(() => {
2839
dataKey="active"
2940
fill="#90c4e5"
3041
strokeWidth={2}
31-
name="My data series name"
42+
name="Active Grants"
3243
/>
3344
<Bar
3445
type="monotone"
3546
stackId="a"
3647
dataKey="inactive"
3748
fill="#F58D5C"
3849
strokeWidth={2}
39-
name="My data series name"
50+
name="Inactive Grants"
4051
/>
4152
<XAxis dataKey="name" />
4253
<YAxis
4354
width="auto"
44-
label={{ value: "UV", position: "insideLeft", angle: -90 }}
55+
key={grants.length}
56+
tickFormatter={(value: number) => `$${value / 1000}k`}
4557
/>
46-
<Tooltip />
58+
<Tooltip formatter={(value: number) => `$${value.toLocaleString()}`} />
59+
<LegendComp />
4760
</BarChart>
4861
</div>
4962
);
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { useState } from "react";
2+
import { downloadCsv, CsvColumn } from "../../utils/csvUtils";
3+
import { Grant } from "../../../../middle-layer/types/Grant";
4+
import { useProcessGrantData } from "../../main-page/grants/filter-bar/processGrantData";
5+
import { observer } from "mobx-react-lite";
6+
import "../grants/styles/GrantButton.css";
7+
import { getAppStore } from "../../external/bcanSatchel/store";
8+
9+
// Define the columns for the CSV export, including any necessary formatting.
10+
const columns: CsvColumn<Grant>[] = [
11+
{ key: "grantId", title: "Grant ID" },
12+
{ key: "organization", title: "Organization" },
13+
{
14+
key: "does_bcan_qualify",
15+
title: "BCAN Qualifies",
16+
formatValue: (value: boolean) => (value ? "Yes" : "No"),
17+
},
18+
{ key: "status", title: "Status" },
19+
{
20+
key: "amount",
21+
title: "Amount ($)",
22+
formatValue: (value: number) => value.toLocaleString(),
23+
},
24+
{
25+
key: "grant_start_date",
26+
title: "Grant Start Date",
27+
formatValue: (value: string) => new Date(value).toLocaleDateString(),
28+
},
29+
{
30+
key: "application_deadline",
31+
title: "Application Deadline",
32+
formatValue: (value: string) => new Date(value).toLocaleDateString(),
33+
},
34+
{
35+
key: "report_deadlines",
36+
title: "Report Deadlines",
37+
formatValue: (value?: string[]) =>
38+
value?.length ? value.join(", ") : "None",
39+
},
40+
{
41+
key: "description",
42+
title: "Description",
43+
formatValue: (value?: string) => value ?? "",
44+
},
45+
{ key: "timeline", title: "Timeline (Years)" },
46+
{
47+
key: "estimated_completion_time",
48+
title: "Estimated Completion Time (Hours)",
49+
},
50+
{
51+
key: "grantmaker_poc",
52+
title: "Grantmaker POC",
53+
formatValue: (value?: { POC_name: string; POC_email: string }) =>
54+
value
55+
? `${value.POC_name}${value.POC_email ? ` (${value.POC_email})` : ""}`
56+
: "N/A",
57+
},
58+
{
59+
key: "bcan_poc",
60+
title: "BCAN POC",
61+
formatValue: (value: { POC_name: string; POC_email: string }) =>
62+
`${value.POC_name}${value.POC_email ? ` (${value.POC_email})` : ""}`,
63+
},
64+
{
65+
key: "attachments",
66+
title: "Attachments",
67+
formatValue: (attachments: string[]) =>
68+
attachments?.length ? attachments.join(" | ") : "None",
69+
},
70+
{
71+
key: "isRestricted",
72+
title: "Restricted?",
73+
formatValue: (value: boolean) => (value ? "Yes" : "No"),
74+
},
75+
];
76+
77+
const CsvExportButton: React.FC = observer(() => {
78+
const { yearFilter } = getAppStore();
79+
const [isProcessing, setIsProcessing] = useState(false);
80+
const { grants } = useProcessGrantData();
81+
const onClickDownload = async () => {
82+
setIsProcessing(true);
83+
84+
const data = grants as Grant[];
85+
86+
// Optional: handle empty or invalid data gracefully
87+
if (!data || data.length === 0) {
88+
alert("No data available to export.");
89+
setIsProcessing(false);
90+
return;
91+
}
92+
93+
// Simulate delay for UX
94+
await new Promise((resolve) => setTimeout(resolve, 1000));
95+
96+
downloadCsv(
97+
data,
98+
columns,
99+
`BCAN Data ${(yearFilter ?? []).join("_")} as of ${
100+
new Date().toISOString().split("T")[0]
101+
}`
102+
);
103+
setIsProcessing(false);
104+
};
105+
106+
return (
107+
<button
108+
className="grant-button add-grant-button bg-medium-orange"
109+
type="button"
110+
onClick={onClickDownload}
111+
disabled={isProcessing}
112+
title="Export the grants data including any applied filters."
113+
>
114+
{isProcessing ? "Please wait..." : "Export CSV"}
115+
</button>
116+
);
117+
});
118+
119+
export default CsvExportButton;
Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,33 @@
1+
import CsvExportButton from "./CsvExportButton";
12

23
import DateFilter from "./DateFilter";
34
import "./styles/Dashboard.css";
45
import { observer } from "mobx-react-lite";
56
import SampleChart from "./Charts/SampleChart";
67
import { useEffect } from "react";
7-
import { updateYearFilter, updateFilter, updateEndDateFilter, updateStartDateFilter } from "../../external/bcanSatchel/actions";
8+
import {
9+
updateYearFilter,
10+
updateFilter,
11+
updateEndDateFilter,
12+
updateStartDateFilter,
13+
} from "../../external/bcanSatchel/actions";
814

915
const Dashboard = observer(() => {
10-
1116
// reset filters on initial render
12-
useEffect(() => {
13-
updateYearFilter(null);
14-
updateFilter(null);
15-
updateEndDateFilter(null);
16-
updateStartDateFilter(null);
17-
}, []);
17+
useEffect(() => {
18+
updateYearFilter(null);
19+
updateFilter(null);
20+
updateEndDateFilter(null);
21+
updateStartDateFilter(null);
22+
}, []);
1823

1924
return (
2025
<div className="dashboard-page px-12 py-4">
26+
<CsvExportButton />
2127
<DateFilter />
22-
<SampleChart/>
28+
<SampleChart />
2329
</div>
2430
);
25-
})
31+
});
2632

2733
export default Dashboard;

frontend/src/main-page/dashboard/DateFilter.tsx

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,18 +40,25 @@ const DateFilter: React.FC = observer(() => {
4040
};
4141

4242
return (
43-
<div className="flex flex-col space-y-2 mb-4">
43+
<div className="flex flex-col space-y-2 m-4">
4444
{uniqueYears.map((year) => (
45-
<label key={year} className="flex items-center space-x-2">
45+
<div className="flex items-center me-4">
4646
<input
4747
type="checkbox"
48+
id={year.toString()}
4849
value={year}
4950
checked={selectedYears.includes(year)}
5051
onChange={handleCheckboxChange}
5152
defaultChecked={true}
5253
/>
53-
<span>{year}</span>
54-
</label>
54+
<label
55+
htmlFor={year.toString()}
56+
key={year}
57+
className="ms-2 text-sm font-medium"
58+
>
59+
{year}
60+
</label>
61+
</div>
5562
))}
5663
</div>
5764
);

frontend/src/main-page/dashboard/styles/Dashboard.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,13 @@ html, body {
1616
height: 100%;
1717
overflow-y: auto;
1818
padding-bottom: 1em;
19+
}
20+
21+
.chart-container {
22+
display: flex;
23+
justify-content: center;
24+
align-items: center;
25+
padding: 12px;
26+
border-radius: 1.2rem;
27+
border: 0.1rem solid #E0E0E0;
1928
}

frontend/src/main-page/grants/filter-bar/FilterBar.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import {
88
updateFilter,
99
} from "../../../external/bcanSatchel/actions.ts";
1010
import { observer } from "mobx-react-lite";
11-
// import { ProcessGrantData } from "./processGrantData.ts";
1211
import CalendarDropdown from "./CalendarDropdown.tsx";
1312
import { FaChevronRight } from "react-icons/fa";
1413

@@ -32,7 +31,6 @@ const linkList: FilterBarProps[] = [
3231
*/
3332
const FilterBar: React.FC = observer(() => {
3433
const [selected, setSelected] = useState("All Grants");
35-
// const { grants } = ProcessGrantData();
3634
function categoryClicked(
3735
e: React.MouseEvent,
3836
category: string,

frontend/src/main-page/grants/filter-bar/grantFilters.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,9 @@ export const searchFilter = (searchQuery: string) => (grant: Grant) => {
3535

3636
return organization.includes(query);
3737
};
38+
39+
export const yearFilterer = (years: number[] | null) => (grant: Grant) => {
40+
if (!years || years.length == 0) return true;
41+
const grantYear = new Date(grant.application_deadline).getFullYear();
42+
return years.includes(grantYear);
43+
}

0 commit comments

Comments
 (0)