Skip to content

Commit c8803a2

Browse files
authored
Merge pull request #205 from Code-4-Community/184-dev---implement-dashboard-page-fully
Dashboard implementation
2 parents e563e6d + 9db8e5e commit c8803a2

File tree

17 files changed

+1104
-127
lines changed

17 files changed

+1104
-127
lines changed

frontend/src/external/bcanSatchel/actions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export const updateEndDateFilter = action (
5151
)
5252
export const updateYearFilter = action (
5353
'updateYearFilter',
54-
(yearFilter: number[] | null) => ({yearFilter})
54+
(yearFilter: number[] | []) => ({yearFilter})
5555
)
5656

5757
/**

frontend/src/external/bcanSatchel/store.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export interface AppState {
1313
startDateFilter: Date | null;
1414
endDateFilter: Date | null;
1515
searchQuery: string;
16-
yearFilter:number[] | null;
16+
yearFilter:number[] | [];
1717
}
1818

1919
// Define initial state
@@ -26,7 +26,7 @@ const initialState: AppState = {
2626
startDateFilter: null,
2727
endDateFilter: null,
2828
searchQuery: '',
29-
yearFilter: null
29+
yearFilter: []
3030
};
3131

3232
const store = createStore<AppState>('appStore', initialState);
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import {
2+
BarChart,
3+
Bar,
4+
XAxis,
5+
YAxis,
6+
Tooltip,
7+
ResponsiveContainer,
8+
LabelList,
9+
} from "recharts";
10+
import { observer } from "mobx-react-lite";
11+
import {
12+
aggregateCountGrantsByYear,
13+
aggregateMoneyGrantsByYear,
14+
YearAmount,
15+
} from "../grantCalculations";
16+
import "../styles/Dashboard.css";
17+
import { Grant } from "../../../../../middle-layer/types/Grant";
18+
import { useState } from "react";
19+
20+
const BarYearGrantStatus = observer(
21+
({ recentYear, grants }: { recentYear: number; grants: Grant[] }) => {
22+
const [checked, setChecked] = useState(true);
23+
24+
// Filtering data for most receny year
25+
const recentData = grants.filter(
26+
(grant) =>
27+
new Date(grant.application_deadline).getFullYear() == recentYear
28+
);
29+
30+
// Formatting data for chart
31+
const data_money = aggregateMoneyGrantsByYear(recentData, "status")
32+
.flatMap((grant: YearAmount) =>
33+
Object.entries(grant.data).map(([key, value]) => ({
34+
name: key,
35+
value,
36+
}))
37+
)
38+
.sort((a, b) => b.value - a.value);
39+
40+
const data_count = aggregateCountGrantsByYear(recentData, "status")
41+
.flatMap((grant: YearAmount) =>
42+
Object.entries(grant.data).map(([key, value]) => ({
43+
name: key,
44+
value,
45+
}))
46+
)
47+
.sort((a, b) => b.value - a.value);
48+
49+
return (
50+
<div className="chart-container">
51+
<div className="flex flex-row w-full justify-between">
52+
<div>
53+
{/* Title */}
54+
<div className="text-lg w-full text-left font-semibold align">
55+
Year Grant Status
56+
</div>
57+
{/* Year */}
58+
<div className="text-sm w-full text-left align">{recentYear}</div>
59+
</div>
60+
{/* Toggle */}
61+
<div className="mt-2">
62+
<label className="inline-flex items-center mb-5 cursor-pointer">
63+
<span className="me-3 text-sm font-medium text-gray-900 dark:text-gray-300">
64+
Count
65+
</span>
66+
<input
67+
type="checkbox"
68+
checked={checked}
69+
onChange={() => setChecked(!checked)}
70+
className="sr-only peer"
71+
style={{display:"none"}}
72+
/>
73+
<div className=" bg-light-orange relative w-9 h-5 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-dark-orange rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border after:rounded-full after:h-4 after:w-4 after:transition-allpeer-checked:bg-blue-600 dark:peer-checked:bg-blue-600"></div>
74+
<span className="ms-3 text-sm font-medium text-gray-900 dark:text-gray-300">
75+
Money
76+
</span>
77+
</label>
78+
</div>
79+
</div>
80+
<ResponsiveContainer width="100%" height={300} min-width={400}>
81+
<BarChart
82+
data={checked ? data_money : data_count}
83+
layout="vertical"
84+
margin={{ top: 10, right: 60, left: 20, bottom: 30 }}
85+
>
86+
<YAxis
87+
axisLine={false}
88+
type="category"
89+
dx={-10}
90+
dataKey="name"
91+
tickLine={false}
92+
/>
93+
<XAxis
94+
type="number"
95+
width="auto"
96+
hide
97+
key={grants.length}
98+
tickFormatter={(value: number) =>
99+
checked ? `$${value / 1000}k` : `${value}`
100+
}
101+
/>
102+
<Bar
103+
type="monotone"
104+
stackId="a"
105+
dataKey="value"
106+
fill="#F58D5C"
107+
strokeWidth={2}
108+
name="Grants"
109+
radius={[15, 15, 15, 15]}
110+
>
111+
<LabelList
112+
dataKey="value"
113+
position="right"
114+
formatter={(label: any) =>
115+
typeof label === "number"
116+
? checked
117+
? `$${label / 1000}k`
118+
: `${label}`
119+
: label
120+
}
121+
/>
122+
</Bar>
123+
<Tooltip
124+
contentStyle={{
125+
borderRadius: "12px",
126+
backgroundColor: "#fff",
127+
border: "1px solid #ccc",
128+
boxShadow: "0 2px 8px rgba(0,0,0,0.1)",
129+
}}
130+
formatter={(value: number) =>
131+
checked ? `$${value.toLocaleString()}` : `${value}`
132+
}
133+
/>
134+
</BarChart>
135+
</ResponsiveContainer>
136+
</div>
137+
);
138+
}
139+
);
140+
141+
export default BarYearGrantStatus;
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { PieChart, Pie, Tooltip, ResponsiveContainer } from "recharts";
2+
import { observer } from "mobx-react-lite";
3+
import { aggregateMoneyGrantsByYear, YearAmount } from "../grantCalculations";
4+
import "../styles/Dashboard.css";
5+
import { Grant } from "../../../../../middle-layer/types/Grant";
6+
import { getListApplied } from "../../../../../middle-layer/types/Status";
7+
8+
const DonutMoneyApplied = observer(({ grants }: { grants: Grant[] }) => {
9+
// Helper to sum values for given statuses
10+
const sumByStatus = (data: Record<string, number>, statuses: string[]) =>
11+
Object.entries(data)
12+
.filter(([status]) => statuses.includes(status))
13+
.reduce((sum, [, value]) => sum + value, 0);
14+
15+
// Aggregate money by year
16+
const dataMoney = aggregateMoneyGrantsByYear(grants, "status").map(
17+
(grant: YearAmount) => ({
18+
year: grant.year.toString(),
19+
received: sumByStatus(grant.data, getListApplied(true)),
20+
unreceived: sumByStatus(grant.data, getListApplied(false)),
21+
})
22+
);
23+
24+
// Summing values across years
25+
const [sumReceived, sumUnreceived] = dataMoney.reduce(
26+
([sumR, sumU], { received, unreceived }) => [
27+
sumR + received,
28+
sumU + unreceived,
29+
],
30+
[0, 0]
31+
);
32+
const total = sumReceived + sumUnreceived;
33+
const data = [
34+
{ name: "Received", value: sumReceived, fill: "#F8CC16" },
35+
{ name: "Unreceived", value: sumUnreceived, fill: "#F58D5C" },
36+
];
37+
38+
// Creating the label for the slices
39+
const LabelItem = ({
40+
name,
41+
value,
42+
percent,
43+
color,
44+
}: {
45+
name: string;
46+
value: number;
47+
percent: number;
48+
color: string;
49+
}) => {
50+
return (
51+
<div className="w-[100px] ">
52+
<div style={{ fontWeight: 500 }}>{name}</div>
53+
<div
54+
style={{
55+
height: 3,
56+
width: "80%",
57+
backgroundColor: color,
58+
marginTop: 0,
59+
borderRadius: 2,
60+
}}
61+
/>
62+
<div style={{ fontSize: 12, color: "#555", marginTop: 0 }}>
63+
{`${(percent * 100).toFixed(0)}% ($${(value / 1_000_000).toFixed(
64+
2
65+
)}M)`}
66+
</div>
67+
</div>
68+
);
69+
};
70+
71+
return (
72+
<div
73+
className="chart-container"
74+
style={{ display: "flex", flexDirection: "column", alignItems: "center" }}
75+
>
76+
<div className="relative w-full h-full flex flex-col">
77+
{/* Title */}
78+
<div className="text-lg font-semibold relative text-left">
79+
Money Applied For {/* Total Amount */}
80+
<div className="text-2xl font-semibold mt-1 absolute">
81+
{`$${((sumReceived + sumUnreceived) / 1000000).toLocaleString(
82+
"en-us",
83+
{
84+
maximumFractionDigits: 2,
85+
}
86+
)}M`}
87+
</div>
88+
{/* Floating Right Label */}
89+
{sumUnreceived > 0 && (
90+
<div className="absolute top-2 right-2 p-4 mx-10 my-4 z-50 rounded-3xl bg-white bg-opacity-50">
91+
<LabelItem
92+
name="Unreceived"
93+
value={sumUnreceived}
94+
percent={sumUnreceived / total}
95+
color="#F58D5C"
96+
/>
97+
</div>
98+
)}
99+
{/* Floating Left Label */}
100+
{sumReceived > 0 && (
101+
<div className="absolute -bottom-[240px] left-2 p-4 mx-10 my-4 z-50 rounded-3xl bg-white bg-opacity-50">
102+
<LabelItem
103+
name="Received"
104+
value={sumReceived}
105+
percent={sumReceived / total}
106+
color="#F8CC16"
107+
/>
108+
</div>
109+
)}
110+
</div>
111+
</div>
112+
<ResponsiveContainer width="100%" height={250}>
113+
<PieChart
114+
style={{ maxWidth: "1000px", maxHeight: "300px", aspectRatio: 1 }}
115+
>
116+
<Pie
117+
data={data}
118+
startAngle={90}
119+
endAngle={450}
120+
dataKey="value"
121+
nameKey="name"
122+
innerRadius="60%"
123+
outerRadius="80%"
124+
cornerRadius={50}
125+
stroke="#fff"
126+
strokeWidth={2}
127+
label={false}
128+
/>
129+
<Tooltip
130+
formatter={(value: number, name: string) => [
131+
`$${value.toLocaleString()}`,
132+
name,
133+
]}
134+
contentStyle={{
135+
borderRadius: "12px",
136+
backgroundColor: "#fff",
137+
border: "1px solid #ccc",
138+
boxShadow: "0 2px 8px rgba(0,0,0,0.1)",
139+
}}
140+
/>
141+
</PieChart>
142+
</ResponsiveContainer>
143+
</div>
144+
);
145+
});
146+
147+
export default DonutMoneyApplied;
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { observer } from "mobx-react-lite";
2+
import { Grant } from "../../../../../middle-layer/types/Grant";
3+
4+
export const GanttYearGrantTimeline = observer(
5+
({ recentYear, grants }: { recentYear: number; grants: Grant[] }) => {
6+
// Filter grants for the selected year
7+
// const recentData = grants.filter(
8+
// (grant) =>
9+
// new Date(grant.application_deadline).getFullYear() === recentYear
10+
// );
11+
12+
// const data: (string | Date | number | null)[][] = [
13+
// [
14+
// "Task ID",
15+
// "Task Name",
16+
// "Resource ID",
17+
// "Start Date",
18+
// "End Date",
19+
// "Duration",
20+
// "Percent Complete",
21+
// "Dependencies",
22+
// ],
23+
// ...recentData.map((grant) => {
24+
// const deadline = new Date(grant.application_deadline);
25+
// const startDate = new Date(deadline.getFullYear(), deadline.getMonth(), deadline.getDate() - 14);
26+
// const endDate = new Date(deadline.getFullYear(), deadline.getMonth(), deadline.getDate());
27+
28+
// return [
29+
// String(grant.grantId), // Task ID must be string
30+
// `${grant.organization} (${grant.status}) $${grant.amount}`, // Task Name
31+
// null, // Resource ID
32+
// startDate, // Start Date
33+
// endDate, // End Date
34+
// 0, // Duration (null)
35+
// 100, // Percent Complete
36+
// null, // Dependencies
37+
// ];
38+
// }),
39+
// ];
40+
41+
// const options = {
42+
// height: recentData.length * 50 + 50,
43+
// gantt: {
44+
// trackHeight: 30,
45+
// barHeight: 20,
46+
// criticalPathEnabled: false,
47+
// labelStyle: {
48+
// fontName: "Arial",
49+
// fontSize: 12,
50+
// color: "#000",
51+
// },
52+
// palette: [
53+
// {
54+
// color: "#f58d5c", // All bars same color
55+
// dark: "#f58d5c",
56+
// light: "#f58d5c",
57+
// },
58+
// ],
59+
// },
60+
// };
61+
62+
return (
63+
<div className="chart-container h-full">
64+
{/* Title */}
65+
<div className="text-lg w-full text-left font-semibold">
66+
Year Grant Timeline
67+
</div>
68+
{/* Year */}
69+
<div className="text-sm w-full text-left">{recentYear}</div>
70+
71+
<div className="py-4">{grants.length}</div>
72+
</div>
73+
);
74+
}
75+
);
76+
77+
export default GanttYearGrantTimeline;

0 commit comments

Comments
 (0)