Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
295 changes: 259 additions & 36 deletions frontend/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"preview": "vite preview"
},
"dependencies": {
"@bitnoi.se/react-scheduler": "^0.3.1",
"@chakra-ui/react": "^3.2.5",
"@emotion/react": "^11.14.0",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,11 @@ const BarYearGrantStatus = observer(
</label>
</div>
</div>
<ResponsiveContainer width="100%" height={300} min-width={400}>
<ResponsiveContainer width="100%" height={450} min-width={400}>
<BarChart
data={checked ? data_money : data_count}
layout="vertical"
margin={{ top: 10, right: 60, left: 20, bottom: 30 }}
margin={{ top: 10, right: 60, left: 40, bottom: 30 }}
>
<YAxis
axisLine={false}
Expand Down
215 changes: 155 additions & 60 deletions frontend/src/main-page/dashboard/Charts/GanttYearGrantTimeline.tsx
Original file line number Diff line number Diff line change
@@ -1,74 +1,169 @@
import { Scheduler, SchedulerData } from "@bitnoi.se/react-scheduler";
import dayjs from "dayjs";
import { observer } from "mobx-react-lite";
import { SetStateAction, useCallback, useState } from "react";
import { Grant } from "../../../../../middle-layer/types/Grant";
import { getColorStatus } from "../../../../../middle-layer/types/Status";
import "../styles/Dashboard.css";

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",
// },
// ],
// },
// };
({
recentYear,
grants,
uniqueYears,
}: {
recentYear: number;
grants: Grant[];
uniqueYears: number[];
}) => {
// Filter grants for the max selected year
// and if the current year is selected in the filter include that as well
const filterYear =
recentYear < new Date().getFullYear()
? recentYear
: Math.min(recentYear, new Date().getFullYear());

// If application deadline or any report deadline is in the range
const recentData = grants.filter((grant) => {
const appYear = new Date(grant.application_deadline).getFullYear();
const appInRange = appYear >= filterYear && appYear <= recentYear;

const reportInRange = grant.report_deadlines?.some((rd) => {
const year = new Date(rd).getFullYear();
return year >= filterYear && year <= recentYear;
});

return appInRange || reportInRange;
});

// Formatting the data for SchedulerData
const data: SchedulerData = recentData.map((grant) => {
const application_deadline = new Date(grant.application_deadline);
const startDate = new Date(
application_deadline.getFullYear(),
application_deadline.getMonth(),
application_deadline.getDate() - 14
);
const endDate = new Date(
application_deadline.getFullYear(),
application_deadline.getMonth(),
application_deadline.getDate()
);

// Create application task
const tasks = [
{
id: `${grant.grantId}-application`,
startDate,
endDate,
occupancy: 0,
title: grant.organization,
description: `App Deadline: ${application_deadline.toLocaleDateString()}`,
bgColor: getColorStatus(grant.status),
},
];

// Add a task for each report deadline (if any)
if (grant.report_deadlines && grant.report_deadlines.length > 0) {
grant.report_deadlines.forEach((rd, index) => {
const report_deadline = new Date(rd);
const report_startDate = new Date(
report_deadline.getFullYear(),
report_deadline.getMonth(),
report_deadline.getDate() - 14
);
const report_endDate = new Date(
report_deadline.getFullYear(),
report_deadline.getMonth(),
report_deadline.getDate()
);

tasks.push({
id: `${grant.grantId}-report-${index}`,
startDate: report_startDate,
endDate: report_endDate,
occupancy: 0,
title: grant.organization,
description: `Report Deadline: ${report_deadline.toLocaleDateString()}`,
bgColor: getColorStatus(grant.status),
});
});
}

return {
id: String(grant.grantId),
label: {
icon: "",
title: grant.organization,
subtitle: `${grant.status} • $${grant.amount.toLocaleString()}`,
},
data: tasks,
};
});

const [range, setRange] = useState({
startDate: new Date(),
endDate: new Date(),
});

const handleRangeChange = useCallback(
(range: SetStateAction<{ startDate: Date; endDate: Date }>) => {
setRange(range);
},
[]
);

// Filtering events that are included in current date range
// Example can be also found on video https://youtu.be/9oy4rTVEfBQ?t=118&si=52BGKSIYz6bTZ7fx
// and in the react-scheduler repo App.tsx file https://github.com/Bitnoise/react-scheduler/blob/master/src/App.tsx
const filteredMockedSchedulerData = data.map((grant) => ({
...grant,
data: grant.data.filter((project) => {
const startInRange =
(dayjs(project.startDate).isAfter(range.startDate, "day") &&
dayjs(project.startDate).isBefore(range.endDate, "day")) ||
dayjs(project.startDate).isSame(range.startDate, "day") ||
dayjs(project.startDate).isSame(range.endDate, "day");

const endInRange =
(dayjs(project.endDate).isAfter(range.startDate, "day") &&
dayjs(project.endDate).isBefore(range.endDate, "day")) ||
dayjs(project.endDate).isSame(range.startDate, "day") ||
dayjs(project.endDate).isSame(range.endDate, "day");

const fullySpansRange =
dayjs(project.startDate).isBefore(range.startDate, "day") &&
dayjs(project.endDate).isAfter(range.endDate, "day");

return startInRange || endInRange || fullySpansRange;
}),
}));

return (
<div className="chart-container h-full">
<div className="chart-container h-full w-full">
{/* Title */}
<div className="text-lg w-full text-left font-semibold">
Year Grant Timeline
</div>
{/* Year */}
<div className="text-sm w-full text-left">{recentYear}</div>

<div className="py-4">{grants.length}</div>
<div className="text-sm w-full text-left">
{filterYear !== recentYear && uniqueYears.includes(filterYear)
? filterYear + "-"
: ""}
{recentYear}
</div>
<div className="w-full h-full max-w-screen relative">
<Scheduler
data={filteredMockedSchedulerData}
isLoading={false}
onRangeChange={handleRangeChange}
config={{
zoom: 0,
filterButtonState: -1,
showTooltip: false,
}}
/>
</div>
</div>
);
}
Expand Down
38 changes: 22 additions & 16 deletions frontend/src/main-page/dashboard/Charts/KPICards.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { Grant } from "../../../../../middle-layer/types/Grant";
import { aggregateMoneyGrantsByYear, YearAmount } from "../grantCalculations";
import {
aggregateCountGrantsByYear,
aggregateMoneyGrantsByYear,
YearAmount,
} from "../grantCalculations";
import KPICard from "./KPICard";
import "../styles/Dashboard.css";
import { observer } from "mobx-react-lite";
Expand All @@ -15,6 +19,7 @@ const KPICards = observer(
recentYear: number;
priorYear: number;
}) => {

// Helper to sum values for given statuses
const sumByStatus = (data: Record<string, number>, statuses: string[]) =>
Object.entries(data)
Expand All @@ -29,31 +34,32 @@ const KPICards = observer(
unreceived: sumByStatus(grant.data, getListApplied(false)),
})
);
// Aggregate count by year
const dataCount = aggregateCountGrantsByYear(grants, "status").map(
(grant: YearAmount) => ({
year: grant.year,
receivedCount: sumByStatus(grant.data, getListApplied(true)),
unreceivedCount: sumByStatus(grant.data, getListApplied(false)),
})
);

// Get metrics for a specific year
// Get metrics for a specific year and merge money and count data
const getYearMetrics = (year: number) => {
const entry = dataMoney.find((d) => d.year === year);
if (!entry)
return {
moneyReceived: 0,
moneyUnreceived: 0,
countReceived: 0,
countUnreceived: 0,
};
const money = dataMoney.find((d) => d.year === year);
const count = dataCount.find((d) => d.year === year);

const { received, unreceived } = entry;
return {
moneyReceived: received,
moneyUnreceived: unreceived,
countReceived: received > 0 ? 1 : 0,
countUnreceived: unreceived > 0 ? 1 : 0,
moneyReceived: money?.received ?? 0,
moneyUnreceived: money?.unreceived ?? 0,
countReceived: count?.receivedCount ?? 0,
countUnreceived: count?.unreceivedCount ?? 0,
};
};

const recent = getYearMetrics(recentYear);
const prior = getYearMetrics(priorYear);

// Helper: percent change formula
// Percent change formula
const percentChange = (current: number, previous: number) => {
return previous === 0 ? 0 : ((current - previous) / previous) * 100;
};
Expand Down
12 changes: 6 additions & 6 deletions frontend/src/main-page/dashboard/Charts/LineChartSuccessRate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ const LineChartSuccessRate = observer(({ grants }: { grants: Grant[] }) => {
const captured =
received + unreceived > 0 ? received / (received + unreceived) : 0;

// Convert year date for time series (e.g. "2024" → "2024-01-02")
// Convert year to date for time series
return {
date: new Date(`${grant.year}-01-02`),
date: new Date(`${grant.year}-01-03`),
money_captured: Number(captured.toFixed(2)),
};
}
Expand All @@ -63,9 +63,9 @@ const LineChartSuccessRate = observer(({ grants }: { grants: Grant[] }) => {
const captured =
received + unreceived > 0 ? received / (received + unreceived) : 0;

// Convert year date for time series (e.g. "2024" → "2024-01-02")
// Convert year to date for time series
return {
date: new Date(`${grant.year}-01-02`),
date: new Date(`${grant.year}-01-04`),
grants_captured: Number(captured.toFixed(2)),
};
}
Expand All @@ -87,14 +87,14 @@ const LineChartSuccessRate = observer(({ grants }: { grants: Grant[] }) => {
data.sort((a, b) => a.date.getTime() - b.date.getTime());

return (
<div className="chart-container">
<div className="chart-container h-full">
{/* Title */}
<div className="text-lg w-full text-left font-semibold align">
Success Rate by Year
</div>
<ResponsiveContainer
width="100%"
height={200}
height="100%"
maxHeight={300}
min-width={400}
>
Expand Down
Loading