diff --git a/api.ts b/api.ts
index 9ab4373..004fdd5 100644
--- a/api.ts
+++ b/api.ts
@@ -64,5 +64,9 @@ export const API = {
UPDATE_STATUS: (id: number | string) => `${API_URL_ROOT}/submissions/${id}/status`,
CREATE: `${API_URL_ROOT}/submissions`,
DELETE: (id: string) => `${API_URL_ROOT}/submissions/${id}`,
+ },
+ Statistics: {
+ GET_ALL: `${API_URL_ROOT}/statistics`,
+ GET_BY_DATE: (date: string) => `${API_URL_ROOT}/statistics/${date}`,
}
}
\ No newline at end of file
diff --git a/src/app/(admin)/page.tsx b/src/app/(admin)/page.tsx
index 65843b4..ef7c5c3 100644
--- a/src/app/(admin)/page.tsx
+++ b/src/app/(admin)/page.tsx
@@ -1,15 +1,12 @@
import type { Metadata } from "next";
import { EcommerceMetrics } from "@/components/ecommerce/EcommerceMetrics";
import React from "react";
-import MonthlyTarget from "@/components/ecommerce/MonthlyTarget";
-import MonthlySalesChart from "@/components/ecommerce/MonthlySalesChart";
+import DailyNewChart from "@/components/ecommerce/DailyNewChart";
import StatisticsChart from "@/components/ecommerce/StatisticsChart";
-import RecentOrders from "@/components/ecommerce/RecentOrders";
-import DemographicCard from "@/components/ecommerce/DemographicCard";
+import DetailedStatsTable from "@/components/ecommerce/DetailedStatsTable";
export const metadata: Metadata = {
- title:
- "Admin Dashboard",
+ title: "Admin Dashboard",
description: "This is Dashboard Home for History Web",
};
@@ -18,25 +15,16 @@ export default function Ecommerce() {
-
-
+
-
+
-
-
-
-
-
-
-
-
);
}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index b6b20bb..f3dc742 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -17,7 +17,7 @@ export default function RootLayout({
}>) {
return (
-
+
{children}
diff --git a/src/components/ecommerce/DailyNewChart.tsx b/src/components/ecommerce/DailyNewChart.tsx
new file mode 100644
index 0000000..84d3b15
--- /dev/null
+++ b/src/components/ecommerce/DailyNewChart.tsx
@@ -0,0 +1,181 @@
+"use client";
+import { useEffect, useState } from "react";
+import dynamic from "next/dynamic";
+import { ApexOptions } from "apexcharts";
+import { getStatistics } from "@/service/statisticsService";
+import { StatisticResponse } from "@/interface/statistics";
+
+const Chart = dynamic(() => import("react-apexcharts"), { ssr: false });
+
+const METRICS = [
+ { key: "users", name: "Người dùng" },
+ { key: "projects", name: "Dự án" },
+ { key: "commits", name: "Bản lưu" },
+ { key: "submissions", name: "Duyệt tin" },
+ { key: "medias", name: "Tệp đa phương tiện" },
+ { key: "wikis", name: "Bài viết Wiki" },
+ { key: "entities", name: "Thực thể" },
+ { key: "geometries", name: "Dữ liệu không gian" },
+];
+
+export default function MonthlyNewChart() {
+ const [startDate, setStartDate] = useState("");
+ const [endDate, setEndDate] = useState("");
+ const [chartData, setChartData] = useState<{ month: string; value: number }[]>([]);
+ const [selectedMetricKey, setSelectedMetricKey] = useState("users");
+ const [loading, setLoading] = useState(true);
+
+ const fetchChartData = async (start: string, end: string, metric: string) => {
+ if (!start || !end) return;
+ setLoading(true);
+ try {
+ const response = await getStatistics({ start_date: start, end_date: end });
+ if (response?.data) {
+ // Tổng hợp theo tháng
+ const monthsMap: { [key: string]: number } = {};
+
+ response.data.forEach(item => {
+ const date = new Date(item.date);
+ const monthKey = `${String(date.getMonth() + 1).padStart(2, '0')}/${date.getFullYear()}`;
+ const val = item[`new_${metric}` as keyof StatisticResponse] as number || 0;
+
+ if (!monthsMap[monthKey]) {
+ monthsMap[monthKey] = 0;
+ }
+ monthsMap[monthKey] += val;
+ });
+
+ const aggregated = Object.entries(monthsMap).map(([key, value]) => ({
+ month: key,
+ value: value
+ })).sort((a, b) => {
+ const [mA, yA] = a.month.split('/').map(Number);
+ const [mB, yB] = b.month.split('/').map(Number);
+ return yA !== yB ? yA - yB : mA - mB;
+ });
+
+ setChartData(aggregated);
+ }
+ } catch (error) {
+ console.error("Failed to fetch chart data", error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ const today = new Date();
+ const startOfYear = new Date(today.getFullYear(), 0, 1); // Từ đầu năm
+
+ const todayStr = today.toISOString().split('T')[0];
+ const startOfYearStr = startOfYear.toISOString().split('T')[0];
+
+ setStartDate(startOfYearStr);
+ setEndDate(todayStr);
+
+ fetchChartData(startOfYearStr, todayStr, selectedMetricKey);
+ }, []);
+
+ const handleStartDateChange = (e: React.ChangeEvent) => {
+ const val = e.target.value;
+ setStartDate(val);
+ fetchChartData(val, endDate, selectedMetricKey);
+ };
+
+ const handleEndDateChange = (e: React.ChangeEvent) => {
+ const val = e.target.value;
+ setEndDate(val);
+ fetchChartData(startDate, val, selectedMetricKey);
+ };
+
+ const handleMetricChange = (e: React.ChangeEvent) => {
+ const val = e.target.value;
+ setSelectedMetricKey(val);
+ fetchChartData(startDate, endDate, val);
+ };
+
+ const categories = chartData.map(item => item.month);
+ const series = [
+ {
+ name: "Tạo mới",
+ data: chartData.map(item => item.value),
+ }
+ ];
+
+ const options: ApexOptions = {
+ colors: ["#465fff"],
+ chart: {
+ fontFamily: "Outfit, sans-serif",
+ type: "bar",
+ height: 180,
+ toolbar: { show: false },
+ },
+ plotOptions: {
+ bar: {
+ horizontal: false,
+ columnWidth: "40%",
+ borderRadius: 5,
+ borderRadiusApplication: "end",
+ },
+ },
+ dataLabels: { enabled: false },
+ stroke: { show: true, width: 4, colors: ["transparent"] },
+ xaxis: {
+ categories: categories,
+ axisBorder: { show: false },
+ axisTicks: { show: false },
+ },
+ legend: { show: false },
+ yaxis: { labels: { style: { colors: ["#6B7280"] } } },
+ grid: { yaxis: { lines: { show: true } } },
+ fill: { opacity: 1 },
+ tooltip: { y: { formatter: (val: number) => `${val}` } },
+ };
+
+ return (
+
+
+
+
+
+ {loading ? (
+
Đang tải...
+ ) : (
+
+ )}
+
+
+
+ );
+}
diff --git a/src/components/ecommerce/DetailedStatsTable.tsx b/src/components/ecommerce/DetailedStatsTable.tsx
new file mode 100644
index 0000000..4581718
--- /dev/null
+++ b/src/components/ecommerce/DetailedStatsTable.tsx
@@ -0,0 +1,92 @@
+"use client";
+import React, { useEffect, useState } from "react";
+import { getStatisticByDate } from "@/service/statisticsService";
+import { StatisticResponse } from "@/interface/statistics";
+
+const METRICS_MAP = [
+ { key: "users", name: "Người dùng" },
+ { key: "projects", name: "Dự án" },
+ { key: "commits", name: "Bản lưu" },
+ { key: "submissions", name: "Duyệt tin" },
+ { key: "medias", name: "Tệp đa phương tiện" },
+ { key: "wikis", name: "Bài viết Wiki" },
+ { key: "entities", name: "Thực thể" },
+ { key: "geometries", name: "Dữ liệu không gian" },
+];
+
+export default function DetailedStatsTable() {
+ const [todayData, setTodayData] = useState(null);
+ const [yesterdayData, setYesterdayData] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ const fetchData = async () => {
+ try {
+ const today = new Date();
+ const yesterday = new Date();
+ yesterday.setDate(today.getDate() - 1);
+
+ const todayStr = today.toISOString().split('T')[0];
+ const yesterdayStr = yesterday.toISOString().split('T')[0];
+
+ const [todayRes, yesterdayRes] = await Promise.all([
+ getStatisticByDate(todayStr),
+ getStatisticByDate(yesterdayStr)
+ ]);
+
+ if (todayRes?.data) setTodayData(todayRes.data);
+ if (yesterdayRes?.data) setYesterdayData(yesterdayRes.data);
+ } catch (error) {
+ console.error("Failed to fetch detailed statistics", error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchData();
+ }, []);
+
+ const calculateGrowth = (today: number, yesterday: number) => {
+ if (!yesterday || yesterday === 0) return 0;
+ return ((today - yesterday) / yesterday) * 100;
+ };
+
+ return (
+
+
+ Bảng Số Liệu Chi Tiết (Hôm nay vs Hôm qua)
+
+
+
+
+
+
+ | Nội dung |
+ Hôm nay |
+ Hôm qua |
+ Tăng trưởng |
+
+
+
+ {METRICS_MAP.map((metric) => {
+ const todayVal = todayData ? (todayData[`total_${metric.key}` as keyof StatisticResponse] as number) : 0;
+ const yesterdayVal = yesterdayData ? (yesterdayData[`total_${metric.key}` as keyof StatisticResponse] as number) : 0;
+ const growth = calculateGrowth(todayVal, yesterdayVal);
+
+ return (
+
+ | {metric.name} |
+ {loading ? "..." : todayVal.toLocaleString()} |
+ {loading ? "..." : yesterdayVal.toLocaleString()} |
+ = 0 ? "text-success-500" : "text-error-500"}`}>
+ {loading ? "..." : `${growth >= 0 ? "+" : ""}${growth.toFixed(2)}%`}
+ |
+
+ );
+ })}
+
+
+
+
+ );
+}
diff --git a/src/components/ecommerce/EcommerceMetrics.tsx b/src/components/ecommerce/EcommerceMetrics.tsx
index 58ab6c1..0b0b212 100644
--- a/src/components/ecommerce/EcommerceMetrics.tsx
+++ b/src/components/ecommerce/EcommerceMetrics.tsx
@@ -1,9 +1,50 @@
"use client";
-import React from "react";
+import React, { useEffect, useState } from "react";
import Badge from "../ui/badge/Badge";
import { ArrowDownIcon, ArrowUpIcon, BoxIconLine, GroupIcon } from "@/icons";
+import { getStatisticByDate } from "@/service/statisticsService";
+import { StatisticResponse } from "@/interface/statistics";
export const EcommerceMetrics = () => {
+ const [todayData, setTodayData] = useState(null);
+ const [yesterdayData, setYesterdayData] = useState(null);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ const fetchData = async () => {
+ try {
+ const today = new Date();
+ const yesterday = new Date();
+ yesterday.setDate(today.getDate() - 1);
+
+ const todayStr = today.toISOString().split('T')[0];
+ const yesterdayStr = yesterday.toISOString().split('T')[0];
+
+ const [todayRes, yesterdayRes] = await Promise.all([
+ getStatisticByDate(todayStr),
+ getStatisticByDate(yesterdayStr)
+ ]);
+
+ if (todayRes?.data) setTodayData(todayRes.data);
+ if (yesterdayRes?.data) setYesterdayData(yesterdayRes.data);
+ } catch (error) {
+ console.error("Failed to fetch statistics", error);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ fetchData();
+ }, []);
+
+ const calculateGrowth = (today: number, yesterday: number) => {
+ if (!yesterday || yesterday === 0) return 0;
+ return ((today - yesterday) / yesterday) * 100;
+ };
+
+ const userGrowth = todayData && yesterdayData ? calculateGrowth(todayData.total_users, yesterdayData.total_users) : 0;
+ const projectGrowth = todayData && yesterdayData ? calculateGrowth(todayData.total_projects, yesterdayData.total_projects) : 0;
+
return (
{/* */}
@@ -15,15 +56,15 @@ export const EcommerceMetrics = () => {
- Customers
+ Tổng Người Dùng
- 3,782
+ {loading ? "..." : todayData?.total_users?.toLocaleString() || "N/A"}
-
-
- 11.01%
+ = 0 ? "success" : "error"}>
+ {userGrowth >= 0 ? : }
+ {Math.abs(userGrowth).toFixed(2)}%
@@ -37,16 +78,16 @@ export const EcommerceMetrics = () => {
- Orders
+ Tổng Dự Án
- 5,359
+ {loading ? "..." : todayData?.total_projects?.toLocaleString() || "N/A"}
-
-
- 9.05%
+ = 0 ? "success" : "error"}>
+ {projectGrowth >= 0 ? : }
+ {Math.abs(projectGrowth).toFixed(2)}%
@@ -54,3 +95,5 @@ export const EcommerceMetrics = () => {
);
};
+
+export default EcommerceMetrics;
diff --git a/src/components/ecommerce/StatisticsChart.tsx b/src/components/ecommerce/StatisticsChart.tsx
index a88e71b..ff29ac0 100644
--- a/src/components/ecommerce/StatisticsChart.tsx
+++ b/src/components/ecommerce/StatisticsChart.tsx
@@ -1,170 +1,168 @@
"use client";
-import { useEffect, useRef } from "react";
+import { useEffect, useState } from "react";
import dynamic from "next/dynamic";
import { ApexOptions } from "apexcharts";
-import flatpickr from "flatpickr";
-import ChartTab from "../common/ChartTab";
-import { CalenderIcon } from "../../icons";
+import { getStatistics } from "@/service/statisticsService";
+import { StatisticResponse } from "@/interface/statistics";
const Chart = dynamic(() => import("react-apexcharts"), { ssr: false });
+const METRICS = [
+ { key: "users", name: "Người dùng" },
+ { key: "projects", name: "Dự án" },
+ { key: "commits", name: "Bản lưu" },
+ { key: "submissions", name: "Duyệt tin" },
+ { key: "medias", name: "Tệp đa phương tiện" },
+ { key: "wikis", name: "Bài viết Wiki" },
+ { key: "entities", name: "Thực thể" },
+ { key: "geometries", name: "Dữ liệu không gian" },
+];
+
export default function StatisticsChart() {
- const datePickerRef = useRef(null);
+ const [startDate, setStartDate] = useState("");
+ const [endDate, setEndDate] = useState("");
+ const [chartData, setChartData] = useState([]);
+ const [selectedMetricKey, setSelectedMetricKey] = useState("users");
+ const [loading, setLoading] = useState(true);
+
+ const fetchChartData = async (start: string, end: string) => {
+ if (!start || !end) return;
+ setLoading(true);
+ try {
+ const response = await getStatistics({ start_date: start, end_date: end });
+ if (response?.data) {
+ setChartData(response.data);
+ }
+ } catch (error) {
+ console.error("Failed to fetch chart data", error);
+ } finally {
+ setLoading(false);
+ }
+ };
useEffect(() => {
- if (!datePickerRef.current) return;
-
const today = new Date();
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(today.getDate() - 6);
- const fp = flatpickr(datePickerRef.current, {
- mode: "range",
- static: true,
- monthSelectorType: "static",
- dateFormat: "M d",
- defaultDate: [sevenDaysAgo, today],
- clickOpens: true,
- prevArrow:
- '',
- nextArrow:
- '',
- });
+ const todayStr = today.toISOString().split('T')[0];
+ const sevenDaysAgoStr = sevenDaysAgo.toISOString().split('T')[0];
- return () => {
- if (!Array.isArray(fp)) {
- fp.destroy();
- }
- };
+ setStartDate(sevenDaysAgoStr);
+ setEndDate(todayStr);
+
+ fetchChartData(sevenDaysAgoStr, todayStr);
}, []);
- const options: ApexOptions = {
- legend: {
- show: false, // Hide legend
- position: "top",
- horizontalAlign: "left",
+ const handleStartDateChange = (e: React.ChangeEvent) => {
+ const val = e.target.value;
+ setStartDate(val);
+ fetchChartData(val, endDate);
+ };
+
+ const handleEndDateChange = (e: React.ChangeEvent) => {
+ const val = e.target.value;
+ setEndDate(val);
+ fetchChartData(startDate, val);
+ };
+
+ const categories = chartData.map(item => {
+ const date = new Date(item.date);
+ return `${date.getDate()}/${date.getMonth() + 1}`;
+ });
+
+ const series = [
+ {
+ name: "Tổng cộng",
+ data: chartData.map(item => item[`total_${selectedMetricKey}` as keyof StatisticResponse] as number || 0),
},
- colors: ["#465FFF", "#9CB9FF"], // Define line colors
+ {
+ name: "Mới",
+ data: chartData.map(item => item[`new_${selectedMetricKey}` as keyof StatisticResponse] as number || 0),
+ }
+ ];
+
+ const options: ApexOptions = {
+ legend: { show: true, position: "top" },
+ colors: ["#465FFF", "#9CB9FF"],
chart: {
fontFamily: "Outfit, sans-serif",
height: 310,
- type: "line", // Set the chart type to 'line'
- toolbar: {
- show: false, // Hide chart toolbar
- },
+ type: "area",
+ toolbar: { show: false },
},
- stroke: {
- curve: "straight", // Define the line style (straight, smooth, or step)
- width: [2, 2], // Line width for each dataset
- },
-
+ stroke: { curve: "smooth", width: [2, 2] },
fill: {
type: "gradient",
- gradient: {
- opacityFrom: 0.55,
- opacityTo: 0,
- },
- },
- markers: {
- size: 0, // Size of the marker points
- strokeColors: "#fff", // Marker border color
- strokeWidth: 2,
- hover: {
- size: 6, // Marker size on hover
- },
+ gradient: { opacityFrom: 0.55, opacityTo: 0 },
},
grid: {
- xaxis: {
- lines: {
- show: false, // Hide grid lines on x-axis
- },
- },
- yaxis: {
- lines: {
- show: true, // Show grid lines on y-axis
- },
- },
- },
- dataLabels: {
- enabled: false, // Disable data labels
+ xaxis: { lines: { show: false } },
+ yaxis: { lines: { show: true } },
},
+ dataLabels: { enabled: false },
tooltip: {
- enabled: true, // Enable tooltip
- x: {
- format: "dd MMM yyyy", // Format for x-axis tooltip
- },
+ enabled: true,
+ x: { format: "dd MMM" },
},
xaxis: {
- type: "category", // Category-based x-axis
- categories: [
- "Jan",
- "Feb",
- "Mar",
- "Apr",
- "May",
- "Jun",
- "Jul",
- "Aug",
- "Sep",
- "Oct",
- "Nov",
- "Dec",
- ],
- axisBorder: {
- show: false, // Hide x-axis border
- },
- axisTicks: {
- show: false, // Hide x-axis ticks
- },
- tooltip: {
- enabled: false, // Disable tooltip for x-axis points
- },
+ type: "category",
+ categories: categories,
+ axisBorder: { show: false },
+ axisTicks: { show: false },
},
yaxis: {
labels: {
style: {
- fontSize: "12px", // Adjust font size for y-axis labels
- colors: ["#6B7280"], // Color of the labels
- },
- },
- title: {
- text: "", // Remove y-axis title
- style: {
- fontSize: "0px",
+ fontSize: "12px",
+ colors: ["#6B7280"],
},
},
},
};
- const series = [
- {
- name: "Sales",
- data: [180, 190, 170, 160, 175, 165, 170, 205, 230, 210, 240, 235],
- },
- {
- name: "Revenue",
- data: [40, 30, 50, 40, 55, 40, 70, 100, 110, 120, 150, 140],
- },
- ];
return (
- Statistics
+ Thống kê hệ thống
- Target you've set for each month
+ Dữ liệu thống kê theo thời gian
-
-
+
@@ -172,7 +170,11 @@ export default function StatisticsChart() {
-
+ {loading ? (
+
Đang tải...
+ ) : (
+
+ )}
diff --git a/src/interface/statistics.ts b/src/interface/statistics.ts
new file mode 100644
index 0000000..7f11d8a
--- /dev/null
+++ b/src/interface/statistics.ts
@@ -0,0 +1,28 @@
+export interface StatisticResponse {
+ id: string;
+ date: string;
+ total_users: number;
+ total_projects: number;
+ total_commits: number;
+ total_submissions: number;
+ total_medias: number;
+ total_wikis: number;
+ total_entities: number;
+ total_geometries: number;
+ total_storage_bytes: number;
+ new_users: number;
+ new_projects: number;
+ new_commits: number;
+ new_submissions: number;
+ new_medias: number;
+ new_wikis: number;
+ new_entities: number;
+ new_geometries: number;
+ new_storage_bytes: number;
+ created_at: string;
+}
+
+export interface GetStatisticsParams {
+ start_date?: string;
+ end_date?: string;
+}
diff --git a/src/layout/AppHeader.tsx b/src/layout/AppHeader.tsx
index b0f9e14..f1216ca 100644
--- a/src/layout/AppHeader.tsx
+++ b/src/layout/AppHeader.tsx
@@ -1,6 +1,4 @@
"use client";
-import { ThemeToggleButton } from "@/components/common/ThemeToggleButton";
-import NotificationDropdown from "@/components/header/NotificationDropdown";
import UserDropdown from "@/components/header/UserDropdown";
import { useSidebar } from "@/context/SidebarContext";
import Image from "next/image";
@@ -90,6 +88,7 @@ const AppHeader: React.FC = () => {
className="dark:hidden"
src="./images/logo/logo.svg"
alt="Logo"
+ style={{ height: 'auto' }}
/>
{
className="hidden dark:block"
src="./images/logo/logo.svg"
alt="Logo"
+ style={{ height: 'auto' }}
/>
@@ -144,10 +144,10 @@ const AppHeader: React.FC = () => {
ref={inputRef}
type="text"
placeholder="Search or type command..."
- className="dark:bg-dark-900 h-11 w-full rounded-lg border border-gray-200 bg-transparent py-2.5 pl-12 pr-14 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-800 dark:bg-gray-900 dark:bg-white/[0.03] dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800 xl:w-[430px]"
+ className="dark:bg-dark-900 h-11 w-full rounded-lg border border-gray-200 bg-transparent py-2.5 pl-12 pr-14 text-sm text-gray-800 shadow-theme-xs placeholder:text-gray-400 focus:border-brand-300 focus:outline-hidden focus:ring-3 focus:ring-brand-500/10 dark:border-gray-800 dark:bg-gray-900 dark:text-white/90 dark:placeholder:text-white/30 dark:focus:border-brand-800 xl:w-[430px]"
/>
-
diff --git a/src/service/statisticsService.ts b/src/service/statisticsService.ts
new file mode 100644
index 0000000..48d4f1f
--- /dev/null
+++ b/src/service/statisticsService.ts
@@ -0,0 +1,14 @@
+import api from "@/config/config";
+import { API } from "../../api";
+import { GetStatisticsParams, StatisticResponse } from "@/interface/statistics";
+import { CommonResponse } from "@/interface/common";
+
+export const getStatistics = async (params?: GetStatisticsParams): Promise
> => {
+ const response = await api.get(API.Statistics.GET_ALL, { params });
+ return response?.data;
+};
+
+export const getStatisticByDate = async (date: string): Promise> => {
+ const response = await api.get(API.Statistics.GET_BY_DATE(date));
+ return response?.data;
+};