Merge branch 'main' of https://github.com/Wind-Explorer/ecoconnect
This commit is contained in:
@@ -21,7 +21,7 @@ import HBContestPage from "./pages/HBContestPage";
|
||||
import HBFormPage from "./pages/HBFormPage";
|
||||
import DefaultLayout from "./layouts/default";
|
||||
import AdministratorLayout from "./layouts/administrator";
|
||||
import UsersManagement from "./pages/UsersManagement";
|
||||
import UsersManagement from "./pages/UsersManagementPage";
|
||||
import ResetPasswordPage from "./pages/ResetPasswordPage";
|
||||
import ForgotPasswordPage from "./pages/ForgotPasswordPage";
|
||||
import RestrictedLayout from "./layouts/restricted";
|
||||
|
||||
106
client/src/components/UserCountGraph.tsx
Normal file
106
client/src/components/UserCountGraph.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import instance from "../security/http";
|
||||
import config from "../config";
|
||||
import { Line } from "react-chartjs-2";
|
||||
import {
|
||||
Chart as ChartJS,
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ChartOptions,
|
||||
} from "chart.js";
|
||||
|
||||
ChartJS.register(
|
||||
CategoryScale,
|
||||
LinearScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Title,
|
||||
Tooltip,
|
||||
Legend
|
||||
);
|
||||
|
||||
export default function UserCountGraph() {
|
||||
const [userInformationCollection, setUserInformationCollection] = useState<
|
||||
any[]
|
||||
>([]);
|
||||
const [chartData, setChartData] = useState<any>(null);
|
||||
|
||||
const retrieveUserInformationCollection = () => {
|
||||
instance.get(`${config.serverAddress}/users/all`).then((values) => {
|
||||
setUserInformationCollection(values.data);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
retrieveUserInformationCollection();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (userInformationCollection.length > 0) {
|
||||
const lastMonth = new Date();
|
||||
lastMonth.setMonth(lastMonth.getMonth() - 1);
|
||||
const daysCount = new Array(31).fill(0); // 记录过去一个月每天的用户注册数量
|
||||
|
||||
userInformationCollection.forEach((user) => {
|
||||
const createdAt = new Date(user.createdAt);
|
||||
if (createdAt > lastMonth) {
|
||||
const dayIndex = Math.floor(
|
||||
(createdAt.getTime() - lastMonth.getTime()) / (1000 * 60 * 60 * 24)
|
||||
);
|
||||
daysCount[dayIndex]++;
|
||||
}
|
||||
});
|
||||
|
||||
const labels = Array.from({ length: 31 }, (_, i) => {
|
||||
const currentDate = new Date();
|
||||
currentDate.setDate(currentDate.getDate() - (30 - i));
|
||||
return currentDate.toISOString().split("T")[0]; // 返回日期字符串 YYYY-MM-DD
|
||||
});
|
||||
|
||||
setChartData({
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{
|
||||
label: "User Onboard",
|
||||
data: daysCount,
|
||||
fill: false,
|
||||
backgroundColor: "rgba(75,192,192,0.4)",
|
||||
borderColor: "rgba(75,192,192,1)",
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}, [userInformationCollection]);
|
||||
|
||||
// 定义图表选项
|
||||
const options: ChartOptions<"line"> = {
|
||||
responsive: true,
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
max: Math.max(
|
||||
10,
|
||||
Math.ceil(Math.max(...(chartData?.datasets[0]?.data || [10])))
|
||||
), // 动态设置最大值,最低为10
|
||||
ticks: {
|
||||
stepSize: 1, // 设置步长为1
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{chartData ? (
|
||||
<Line data={chartData} options={options} />
|
||||
) : (
|
||||
<p>Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
106
client/src/components/UserTownCouncilDistributionChart.tsx
Normal file
106
client/src/components/UserTownCouncilDistributionChart.tsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import instance from "../security/http";
|
||||
import config from "../config";
|
||||
import { Pie } from "react-chartjs-2";
|
||||
import { Chart as ChartJS, ArcElement, Tooltip, Legend } from "chart.js";
|
||||
|
||||
// Register Chart.js components
|
||||
ChartJS.register(ArcElement, Tooltip, Legend);
|
||||
|
||||
export default function UserTownCouncilDistributionChart() {
|
||||
const [userInformationCollection, setUserInformationCollection] = useState<
|
||||
any[]
|
||||
>([]);
|
||||
const [townCouncilInformation, setTownCouncilInformation] = useState<
|
||||
string[]
|
||||
>([]);
|
||||
const [chartData, setChartData] = useState<any>(null);
|
||||
|
||||
const retrieveInformation = () => {
|
||||
instance.get(`${config.serverAddress}/users/all`).then((values) => {
|
||||
setUserInformationCollection(values.data);
|
||||
});
|
||||
instance
|
||||
.get(`${config.serverAddress}/users/town-councils-metadata`)
|
||||
.then((values) => {
|
||||
setTownCouncilInformation(JSON.parse(values.data).townCouncils);
|
||||
});
|
||||
};
|
||||
|
||||
const generateChartData = () => {
|
||||
const townCouncilCounts: { [key: string]: number } = {};
|
||||
userInformationCollection.forEach((user: any) => {
|
||||
const townCouncil = user.townCouncil || "Unknown";
|
||||
if (townCouncilCounts[townCouncil]) {
|
||||
townCouncilCounts[townCouncil]++;
|
||||
} else {
|
||||
townCouncilCounts[townCouncil] = 1;
|
||||
}
|
||||
});
|
||||
|
||||
const labels = Object.keys(townCouncilCounts);
|
||||
const data = Object.values(townCouncilCounts);
|
||||
|
||||
const chartData = {
|
||||
labels,
|
||||
datasets: [
|
||||
{
|
||||
data,
|
||||
backgroundColor: labels.map(
|
||||
(label, index) => `hsl(${index * (360 / labels.length)}, 70%, 50%)`
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
setChartData(chartData);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
retrieveInformation();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
userInformationCollection.length > 0 &&
|
||||
townCouncilInformation.length > 0
|
||||
) {
|
||||
generateChartData();
|
||||
}
|
||||
}, [userInformationCollection, townCouncilInformation]);
|
||||
|
||||
const options = {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
position: "left" as const,
|
||||
labels: {
|
||||
font: {
|
||||
size: 20,
|
||||
},
|
||||
color: "black",
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function (tooltipItem: any) {
|
||||
const dataset = tooltipItem.dataset;
|
||||
const dataIndex = tooltipItem.dataIndex;
|
||||
const value = dataset.data[dataIndex];
|
||||
return `${chartData.labels[dataIndex]}: ${value}`;
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{chartData ? (
|
||||
<Pie data={chartData} options={options} />
|
||||
) : (
|
||||
<p>Loading...</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,8 @@ import UserProfilePicture from "../components/UserProfilePicture";
|
||||
import { Button, Card } from "@nextui-org/react";
|
||||
import { PencilSquareIcon } from "../icons";
|
||||
import EcoconnectFullLogo from "../components/EcoconnectFullLogo";
|
||||
import UserCountGraph from "../components/UserCountGraph";
|
||||
import UserTownCouncilDistributionChart from "../components/UserTownCouncilDistributionChart";
|
||||
|
||||
export default function AdministratorSpringboard() {
|
||||
const navigate = useNavigate();
|
||||
@@ -75,28 +77,18 @@ export default function AdministratorSpringboard() {
|
||||
<Card className="w-full bg-primary-500 p-8 text-white rounded-r-none">
|
||||
<div className="flex flex-col gap-4">
|
||||
<p className="font-bold text-4xl">Statistics Overview</p>
|
||||
<div className="h-[500px] w-full bg-primary-600 rounded-2xl flex flex-row p-6">
|
||||
<div className="w-60 flex flex-col justify-between">
|
||||
<div className="flex flex-col">
|
||||
<p className="text-2xl">User Count</p>
|
||||
<p className="opacity-70">(past 30 days)</p>
|
||||
</div>
|
||||
<p className="text-lg">
|
||||
Total: <span className="font-bold">2139</span> users
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-full h-full bg-white rounded-xl">
|
||||
{/* TODO: Graph */}
|
||||
<p className="text-black">GRAPH HERE</p>
|
||||
<div className="w-full h-full bg-primary-600 rounded-2xl flex flex-col gap-4 p-6">
|
||||
<p className="text-3xl">Users Onboard</p>
|
||||
<div className="w-full p-4 bg-white rounded-xl">
|
||||
<UserCountGraph />
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-[500px] w-full bg-primary-600 rounded-2xl flex flex-row p-6">
|
||||
<div className="w-60 flex flex-col justify-between">
|
||||
<p className="text-2xl">Population Distribution</p>
|
||||
</div>
|
||||
<div className="w-full h-full bg-white rounded-xl">
|
||||
{/* TODO: Graph */}
|
||||
<p className="text-black">GRAPH HERE</p>
|
||||
<div className="w-full h-full bg-primary-600 rounded-2xl flex flex-col gap-4 p-6">
|
||||
<p className="text-3xl">Population Distribution</p>
|
||||
<div className="w-full bg-white rounded-xl">
|
||||
<div className="w-[600px] mx-auto">
|
||||
<UserTownCouncilDistributionChart />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -34,6 +34,10 @@ export default function UsersManagement() {
|
||||
key: "phoneNumber",
|
||||
label: "TELEPHONE",
|
||||
},
|
||||
{
|
||||
key: "townCouncil",
|
||||
label: "TOWN COUNCIL",
|
||||
},
|
||||
{
|
||||
key: "accountType",
|
||||
label: "ACCOUNT TYPE",
|
||||
Reference in New Issue
Block a user