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
2 changes: 1 addition & 1 deletion next-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/dev/types/routes.d.ts";

// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
52 changes: 47 additions & 5 deletions src/app/admin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
Popover,
Checkbox,
TextInput,
Stepper,
} from "@mantine/core";
import { MonthPickerInput } from "@mantine/dates";
import { useState, useEffect, useCallback, useMemo } from "react";
Expand Down Expand Up @@ -99,7 +100,7 @@ const monthMap: Record<string, string> = {
December: "12",
};

const years: Array<string> = ["All", "2023", "2024", "2025"];
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];

const statuses = (Object.values(status) as string[]).map((s) => ({
value: s,
Expand Down Expand Up @@ -209,6 +210,49 @@ export default function Page() {
};
}, [fetchPartners, fetchPercentages]);

const [uploadedMonths, setUploadedMonths] = useState<number[]>([]);
const [lastUploaded, setLastUploaded] = useState<string | null>(null);
const [years, setYears] = useState<string[]>(["All"]);
const currentYear = new Date().getFullYear();

const fetchTimelineData = useCallback(async () => {
try {
const res = await fetch(`/api/timeline-slider?year=${currentYear}`);
const data = await res.json();
if (data.years) {
setYears(["All", ...data.years.map(String)]);
}
if (data.months) {
const validMonths = data.months.filter(
(d: { Month: string | null; Year: string | null }) =>
typeof d.Month === "string" && typeof d.Year === "string"
);
const currentYearMonths = validMonths.filter(
(d: { Month: string; Year: string }) => d.Year === String(currentYear)
);
const indices = currentYearMonths
.map((d: { Month: string; Year: string }) =>
MONTHS.findIndex((m) => d.Month.startsWith(m))
)
.filter((index: number) => index >= 0);
setUploadedMonths(indices);

if (validMonths.length > 0) {
const last = validMonths[validMonths.length - 1];
setLastUploaded(`${last.Month} ${last.Year}`);
} else {
setLastUploaded(null);
}
}
} catch (err) {
console.error("Failed to fetch timeline data:", err);
}
}, [currentYear]);

useEffect(() => {
fetchTimelineData();
}, [fetchTimelineData]);

const formatMonthKeyFromDate = (date: Date) =>
`${date.getUTCFullYear()}-${String(date.getUTCMonth() + 1).padStart(2, "0")}`;

Expand Down Expand Up @@ -401,17 +445,15 @@ export default function Page() {
<Title order={2}>Hello, Rachel 👋</Title>
<Group gap="xl" wrap="wrap">
<Text size="sm" c="dimmed">
Last data uploaded: Monday, 30 Aug, 2025
</Text>
<Text size="sm" c="dimmed">
Last updated: Friday, 2 Sep, 2025
Last data uploaded: {lastUploaded ?? "N/A"}
</Text>
</Group>
</Stack>
<UploadNewData
opened={openedUploadDataForm}
onClose={closeUploadDataForm}
onUploaded={fetchDistributions}
uploadedMonths={uploadedMonths}
/>
<AddPartnerForm
opened={openedPartnerForm}
Expand Down
57 changes: 53 additions & 4 deletions src/app/api/timeline-slider/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,26 @@ import { prisma } from "@/lib/prisma";

export const revalidate = 2592000;

const MONTH_ORDER: Record<string, number> = {
January: 0,
February: 1,
March: 2,
April: 3,
May: 4,
June: 5,
July: 6,
August: 7,
September: 8,
October: 9,
November: 10,
December: 11,
};

type TimelineMonthRow = {
year: string;
month: string;
};

export async function GET() {
try {
const yearly_data = await prisma.yearlyData.findMany({
Expand All @@ -16,12 +36,41 @@ export async function GET() {

const distributions = await prisma.distribution.findMany({
distinct: ["year", "month"],
select: {
year: true,
month: true,
},
});

const months = distributions.map((distribution) => ({
Month: distribution.month,
Year: distribution.year,
}));
const validDistributions = distributions.reduce<TimelineMonthRow[]>(
(acc, distribution) => {
if (
distribution.month !== null &&
distribution.year !== null &&
distribution.month in MONTH_ORDER
) {
acc.push({
month: distribution.month,
year: distribution.year,
});
}

return acc;
},
[],
);

const months = validDistributions
.map((distribution) => ({
Month: distribution.month,
Year: distribution.year,
}))
.sort((a, b) => {
const yearDiff = Number(a.Year) - Number(b.Year);
if (yearDiff !== 0) return yearDiff;

return MONTH_ORDER[a.Month] - MONTH_ORDER[b.Month];
});

return NextResponse.json({ years, months });
} catch (error) {
Expand Down
24 changes: 24 additions & 0 deletions src/components/admin/AddPartnerForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,30 @@ export default function AddPartnerForm({
setIsSubmitting(true);
setSubmitWarning("");

// Check for duplicate partner name before submitting.
try {
const trimmedName = values.organization.trim();
const checkRes = await fetch(
`/api/partners?search=${encodeURIComponent(trimmedName)}`,
);
if (checkRes.ok) {
const checkData = await checkRes.json();
const duplicate = checkData.data?.find(
(p: { name: string }) =>
p.name.trim().toLowerCase() === trimmedName.toLowerCase(),
);
if (duplicate) {
const duplicateWarning = `The partner ${trimmedName} already exists.`;
form.setFieldError("organization", duplicateWarning);
setSubmitWarning(duplicateWarning);
setIsSubmitting(false);
return;
}
}
} catch {
// If the check fails, allow the submission to proceed.
}

const cityPercentages = values.cities.map((city) => {
const raw = Number(percentages[city] ?? 0);
const normalized = Number.isFinite(raw)
Expand Down
19 changes: 12 additions & 7 deletions src/components/admin/DeleteDistributionDataButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ const MONTH_NAMES = [
];

interface TimelineSliderMonth {
Month: string;
Year: string;
Month: string | null;
Year: string | null;
}

// export default function MonthSelectionModal({opened, onClose, onSubmit} : MonthSelectionModalProps) {
Expand All @@ -69,11 +69,16 @@ export default function DeleteDistributionDataButton({
const response = await fetch("/api/timeline-slider");
if (response.ok) {
const data = await response.json();
// Map to lowercase month for easier matching
const months = data.months.map((m: TimelineSliderMonth) => ({
month: m.Month.toLowerCase(),
year: Number(m.Year),
}));
const months = (Array.isArray(data.months) ? data.months : [])
.filter(
(m: TimelineSliderMonth) =>
typeof m.Month === "string" && typeof m.Year === "string",
)
.map((m: TimelineSliderMonth) => ({
month: m.Month!.toLowerCase(),
year: Number(m.Year),
}))
.filter((m: { month: string; year: number }) => !Number.isNaN(m.year));
setAvailableMonths(months);
}
} catch (error) {
Expand Down
112 changes: 104 additions & 8 deletions src/components/admin/UploadDistributionDataForm.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import { Modal, Button, Group, Text, Stack, Alert } from "@mantine/core";
import {
Modal,
Button,
Group,
Text,
Stack,
SimpleGrid,
Paper,
Box,
Alert,
} from "@mantine/core";
import FileUpload, { FileInfo } from "../sprint2/FileUpload";
import { MonthPickerInput } from "@mantine/dates";
import { useState } from "react";
Expand All @@ -7,12 +17,16 @@ interface UploadNewDataProps {
opened: boolean;
onClose: () => void;
onUploaded?: () => void;
uploadedMonths: number[];
}

const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];

export default function UploadNewData({
opened,
onClose,
onUploaded,
uploadedMonths,
}: UploadNewDataProps) {
const [datasetMonth, setDatasetMonth] = useState<string | null>(null);
const [fileInfo, setFileInfo] = useState<FileInfo | null>(null);
Expand All @@ -24,6 +38,8 @@ export default function UploadNewData({
onClose();
};

const currentYear = new Date().getFullYear();
const uploadedMonthSet = new Set(uploadedMonths);

const handleUpload = async () => {
setWarnings([]);
Expand All @@ -38,14 +54,8 @@ export default function UploadNewData({
return;
}






setIsUploading(true);
try {

const response = await fetch("/api/distributions/upload", {
method: "POST",
headers: { "Content-Type": "application/json" },
Expand Down Expand Up @@ -93,7 +103,7 @@ export default function UploadNewData({
</Text>
}
>
<Stack gap="sm">
<Stack gap="md">
{warnings.length > 0 ? (
<Alert color="red" title="Please fix the following:">
<Stack gap={4}>
Expand All @@ -106,6 +116,92 @@ export default function UploadNewData({
</Alert>
) : null}

<Paper
withBorder
radius="md"
p="md"
style={{
borderColor: "#d9e1ea",
backgroundColor: "#fafbfc",
}}
>
<Group justify="space-between" align="flex-start" mb="sm" gap="sm">
<div style={{ flex: 1, minWidth: 0 }}>
<Text fw={700} size="sm">
Uploaded months in {currentYear}
</Text>
<Text size="sm" c="dimmed">
Use this as a quick reference before uploading a new dataset.
</Text>
</div>
<Group gap="xs" wrap="wrap">
<Group gap={6}>
<Box
style={{
width: 8,
height: 8,
borderRadius: "50%",
backgroundColor: "#2f8a22",
}}
/>
<Text size="xs" c="dimmed" fw={500}>
Uploaded
</Text>
</Group>
<Group gap={6}>
<Box
style={{
width: 8,
height: 8,
borderRadius: "50%",
backgroundColor: "#c1c7d0",
}}
/>
<Text size="xs" c="dimmed" fw={500}>
Missing
</Text>
</Group>
</Group>
</Group>

<SimpleGrid cols={{ base: 2, sm: 3, md: 4, lg: 6 }} spacing="sm">
{MONTHS.map((month, index) => {
const isUploaded = uploadedMonthSet.has(index);

return (
<Paper
key={month}
radius="md"
p="xs"
withBorder
style={{
minHeight: 72,
display: "flex",
flexDirection: "column",
alignItems: "flex-start",
justifyContent: "center",
gap: 4,
backgroundColor: isUploaded ? "#edf7eb" : "#ffffff",
borderColor: isUploaded ? "#8bc17f" : "#d9e1ea",
}}
>
<Text fw={700} size="md" c="#495057">
{month}
</Text>
<Text
size="xs"
fw={600}
c={isUploaded ? "#2f8a22" : "#868e96"}
style={{ lineHeight: 1.2 }}
>
{isUploaded ? "Uploaded" : "Missing"}
</Text>
</Paper>
);
})}
</SimpleGrid>
</Paper>

<Group justify="center" grow>
<MonthPickerInput
label="Dataset Information"
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.tsbuildinfo

Large diffs are not rendered by default.

Loading