Skip to content
Open
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
213 changes: 213 additions & 0 deletions src/app/reports/education/education-client.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
"use client";

import { useMemo } from "react";
import DashboardTop from "@/components/DashboardTop";
import Chart from "@/components/chart";
import { VerticalBarChart } from "@/components/charts/vertical-bar-chart";
import { DonutChart } from "@/components/charts/donut-chart";
import useFilters, { type FilterState } from "@/lib/filterStore";
import { filterRecords } from "@/lib/applyFilters";
import type { EducationRecord } from "@/lib/getEducationData";

type FilterSummary = Pick<
FilterState,
| "selectedLocations"
| "selectedSchools"
| "timeframe"
| "fiscalYear"
| "customRange"
>;

function formatFilters(filters: FilterSummary) {
const parts: string[] = [];
if (
filters.timeframe === "custom" &&
filters.customRange?.from &&
filters.customRange?.to
) {
parts.push(
`${filters.customRange.from.toLocaleDateString()} - ${filters.customRange.to.toLocaleDateString()}`
);
} else {
parts.push(filters.timeframe);
}
if (filters.selectedSchools.length) {
parts.push(`${filters.selectedSchools.length} schools`);
}
if (filters.selectedLocations.length) {
parts.push(`${filters.selectedLocations.length} locations`);
}
return parts.join(" • ");
}

function buildGradeDistribution(data: EducationRecord[]) {
const bins = Array.from({ length: 5 }, (_, index) => ({
label: `${index}`,
value: 0,
}));

data.forEach((record) => {
if (typeof record.grade !== "number" || Number.isNaN(record.grade)) {
return;
}

let bucket = Math.floor(record.grade);
if (bucket < 0) bucket = 0;
if (bucket > 4) bucket = 4;

bins[bucket].value += 1;
});

return bins;
}

function buildSubjectDistribution(data: EducationRecord[]) {
const subjectMap = new Map<string, number>();

data.forEach((record) => {
const subject = record.courseSubject?.trim() || "Unknown";
subjectMap.set(subject, (subjectMap.get(subject) ?? 0) + 1);
});

return Array.from(subjectMap, ([label, value]) => ({ label, value })).sort(
(a, b) => b.value - a.value
);
}

function buildAverageGradeBySchool(data: EducationRecord[]) {
const schoolMap = new Map<string, { total: number; count: number }>();

data.forEach((record) => {
if (typeof record.grade !== "number" || Number.isNaN(record.grade)) {
return;
}
const school = record.school?.trim() || "Unknown";
const entry = schoolMap.get(school) ?? { total: 0, count: 0 };
entry.total += record.grade;
entry.count += 1;
schoolMap.set(school, entry);
});

return Array.from(schoolMap, ([label, summary]) => ({
label,
value: summary.count > 0 ? summary.total / summary.count : 0,
}))
.sort((a, b) => b.value - a.value)
.slice(0, 12);
}

export default function EducationClient({ data }: { data: EducationRecord[] }) {
const selectedLocations = useFilters((s) => s.selectedLocations);
const selectedSchools = useFilters((s) => s.selectedSchools);
const timeframe = useFilters((s) => s.timeframe);
const fiscalYear = useFilters((s) => s.fiscalYear);
const customRange = useFilters((s) => s.customRange);

const filteredData = useMemo(
() =>
filterRecords(data, {
selectedLocations,
selectedSchools,
timeframe,
fiscalYear,
customRange,
}),
[
data,
selectedLocations,
selectedSchools,
timeframe,
fiscalYear,
customRange,
]
);

const gradeDistribution = useMemo(
() => buildGradeDistribution(filteredData),
[filteredData]
);

const subjectDistribution = useMemo(
() => buildSubjectDistribution(filteredData),
[filteredData]
);

const averageBySchool = useMemo(
() => buildAverageGradeBySchool(filteredData),
[filteredData]
);

const averageGrade = useMemo(() => {
const grades = filteredData
.map((record) => record.grade)
.filter(
(grade): grade is number =>
typeof grade === "number" && !Number.isNaN(grade)
);
if (!grades.length) return null;
return grades.reduce((sum, value) => sum + value, 0) / grades.length;
}, [filteredData]);

const filterState: FilterSummary = {
selectedLocations,
selectedSchools,
timeframe,
fiscalYear,
customRange,
};

return (
<div className="w-full">
<DashboardTop
pageTitle="Education Dashboard"
title="Average Grade"
body={averageGrade !== null ? averageGrade.toFixed(1) : "—"}
subtext="Across filtered assessments"
bgColor="bg-[#E0F7F4]"
title1="Students Assessed"
title2="Subject Areas"
bgColor1="bg-[#F0E7ED]"
bgColor2="bg-[#FFF8E9]"
body1={`${filteredData.length}`}
body2={`${subjectDistribution.length}`}
subtext1="Evaluated records"
subtext2="Unique subjects"
mt="-mt-[10px]"
/>

<div className="grid grid-cols-1 items-start gap-8 p-10 lg:grid-cols-2">
<Chart
title="Grade Distribution"
appliedFilters={formatFilters(filterState)}
filterState={filterState}
>
<VerticalBarChart
data={gradeDistribution}
xLabel="Grade Range"
yLabel="Students"
/>
</Chart>

<Chart
title="Students by Subject"
appliedFilters={formatFilters(filterState)}
filterState={filterState}
>
<DonutChart data={subjectDistribution} />
</Chart>

<Chart
title="Average Grade by School"
appliedFilters={formatFilters(filterState)}
filterState={filterState}
>
<VerticalBarChart
data={averageBySchool}
xLabel="School"
yLabel="Average Grade"
/>
</Chart>
</div>
</div>
);
}
27 changes: 11 additions & 16 deletions src/app/reports/education/page.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
import DashboardTop from '@/components/DashboardTop';
import { getEducationData, type EducationRecord } from "@/lib/getEducationData";
import EducationClient from "./education-client";
import FilterBar from "@/components/FilterBar";

export default function Education(){
return(

<div className="w-full">

<div className="sticky top-0 min-h-[40px] bg-white top-0 flex justify-between py-3 drop-shadow-sm z-50">
<FilterBar />
{/* {userName || "John Doe"} */}
export default async function Education() {
const data = await getEducationData();

return (
<div>
<div className="sticky top-0 min-h-[40px] bg-white flex justify-between py-3 drop-shadow-sm z-50">
<FilterBar />
</div>
<DashboardTop pageTitle="Education Dashboard" title= "Total Families Enrolled" body="224" subtext="All-time enrollment" bgColor="bg-[#E0F7F4]" title1="Families Housed to Date" title2="Average Wait Time" bgColor1="bg-[#F0E7ED]" bgColor2="bg-[#FFF8E9]" body1="158" body2="48 days" subtext1="70.5% success rate" subtext2="Intake to housed" mt="-mt-[10px]" />
<EducationClient data={data} />
</div>





);
}
}
24 changes: 17 additions & 7 deletions src/app/reports/reportExportButtons.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
"use client";

import { useState } from "react";
import { Download } from "lucide-react";
import { usePdfMetadataStore } from "@/lib/pdfMetadataStore";
import { redirect } from "next/navigation";
import { useRouter } from "next/navigation";// include router
import { useRouter } from "next/navigation";

export default function ReportExportButton() {
const filename = usePdfMetadataStore((s) => s.filename);
const router = useRouter(); // declare router

const [isExporting, setIsExporting] = useState(false);

async function setPdfTitle() {
setIsExporting(true);
try {
console.log("Sending filename:", filename);

Expand All @@ -28,20 +29,29 @@ export default function ReportExportButton() {
throw new Error("Failed to update report name");
}
router.push("/preview");

} catch (error) {
console.log(error);
} finally {
setIsExporting(false);
}
}

const baseBtn =
"flex flex-row items-center space-x-4 border border-[rgba(0,0,0,0.1)] rounded-2xl p-3 min-w-40 h-10 hover:bg-[#E76C82] transition-colors duration-150 hover:text-white";
"flex flex-row items-center space-x-4 border border-[rgba(0,0,0,0.1)] rounded-2xl p-3 min-w-40 h-10 transition-colors duration-150";

return (
<div className="ExportOptions flex flex-col md:flex-row md:space-x-3 space-y-3">
<button onClick={setPdfTitle} className={baseBtn}>
<button
onClick={setPdfTitle}
disabled={isExporting}
className={`${baseBtn} ${
isExporting
? "bg-gray-200 text-gray-500 cursor-not-allowed"
: "hover:bg-[#E76C82] hover:text-white"
}`}
>
<Download className="w-4 h-4" />
<p>Export as PDF</p>
<p>{isExporting ? "Exporting..." : "Export as PDF"}</p>
</button>
</div>
);
Expand Down
22 changes: 20 additions & 2 deletions src/components/chartPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,36 @@ function PreviewChart({ chart }: { chart: GeneratedChartModel }) {
);
}

if (chart.chartKey === "housing-sources-donut") {
if (
chart.chartKey === "housing-sources-donut" ||
chart.chartKey === "students-by-subject-donut"
) {
return (
<DonutChart
data={chart.data}
centerLabel={chart.centerLabel}
width={640}
height={320}
className="w-full h-[380px]"
/>
);
}

if (
chart.chartKey === "grade-distribution-bar" ||
chart.chartKey === "average-grade-by-school-bar"
) {
return (
<VerticalBarChart
data={chart.data}
xLabel={chart.xLabel}
yLabel={chart.yLabel}
width={640}
height={320}
className="w-full h-[320px]"
/>
);
}

return (
<HorizontalBarChart
data={chart.data}
Expand Down
8 changes: 5 additions & 3 deletions src/components/charts/chart-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ export type Margin = {
export const DEFAULT_COLORS = [
"#F4A6B0",
"#20B2AA",
"#3B82F6",
"#8B5CF6",
"#F59E0B",
"#CfA2B9",
"#82A3E1",
"#9ED0D1",
"#CfA2b9",
"#82c5e5",
];
export const DEFAULT_GRID = "#E5E7EB";
export const DEFAULT_TEXT = "#6B6B6B";
Expand Down
Loading