feat: implement role-based sidebar navigation and admin dashboard layout with statistics components
All checks were successful
Build and Release / release (push) Successful in 30s

This commit is contained in:
2026-05-09 11:09:51 +07:00
parent 45b7b379ce
commit 91b0eef9c8
11 changed files with 505 additions and 152 deletions

4
api.ts
View File

@@ -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}`,
}
}

View File

@@ -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() {
<div className="grid grid-cols-12 gap-4 md:gap-6">
<div className="col-span-12 space-y-6 xl:col-span-7">
<EcommerceMetrics />
<MonthlySalesChart />
<DailyNewChart />
</div>
<div className="col-span-12 xl:col-span-5">
<MonthlyTarget />
<DetailedStatsTable />
</div>
<div className="col-span-12">
<StatisticsChart />
</div>
<div className="col-span-12 xl:col-span-5">
<DemographicCard />
</div>
<div className="col-span-12 xl:col-span-7">
<RecentOrders />
</div>
</div>
);
}

View File

@@ -17,7 +17,7 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
<body className={`${inter.className} dark:bg-gray-900`}>
<body className={`${inter.className} dark:bg-gray-900`} suppressHydrationWarning>
<StoreProvider>
<ThemeProvider>
<SidebarProvider>{children} <Toaster closeButton richColors position="top-right" /> </SidebarProvider>

View File

@@ -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<string>("");
const [endDate, setEndDate] = useState<string>("");
const [chartData, setChartData] = useState<{ month: string; value: number }[]>([]);
const [selectedMetricKey, setSelectedMetricKey] = useState<string>("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<HTMLInputElement>) => {
const val = e.target.value;
setStartDate(val);
fetchChartData(val, endDate, selectedMetricKey);
};
const handleEndDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const val = e.target.value;
setEndDate(val);
fetchChartData(startDate, val, selectedMetricKey);
};
const handleMetricChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
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 (
<div className="overflow-hidden rounded-2xl border border-gray-200 bg-white px-5 pt-5 dark:border-gray-800 dark:bg-white/[0.03] sm:px-6 sm:pt-6">
<div className="flex flex-col gap-4 mb-4 sm:flex-row sm:items-center sm:justify-between">
<h3 className="text-lg font-semibold text-gray-800 dark:text-white/90">
Lượng tạo mới hàng tháng
</h3>
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
<select
value={selectedMetricKey}
onChange={handleMetricChange}
className="px-2 py-1 text-xs font-medium text-gray-700 bg-white border border-gray-200 rounded-lg focus:outline-none focus:border-brand-300 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
>
{METRICS.map((metric) => (
<option key={metric.key} value={metric.key}>
{metric.name}
</option>
))}
</select>
<input
type="date"
value={startDate}
onChange={handleStartDateChange}
className="px-2 py-1 text-xs rounded-lg border border-gray-200 bg-white text-gray-700 outline-none dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300 cursor-pointer"
/>
<input
type="date"
value={endDate}
onChange={handleEndDateChange}
className="px-2 py-1 text-xs rounded-lg border border-gray-200 bg-white text-gray-700 outline-none dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300 cursor-pointer"
/>
</div>
</div>
<div className="max-w-full overflow-x-auto custom-scrollbar">
<div className="-ml-5 min-w-[650px] xl:min-w-full pl-2">
{loading ? (
<div className="flex items-center justify-center h-[180px]">Đang tải...</div>
) : (
<Chart options={options} series={series} type="bar" height={180} />
)}
</div>
</div>
</div>
);
}

View File

@@ -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<StatisticResponse | null>(null);
const [yesterdayData, setYesterdayData] = useState<StatisticResponse | null>(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 (
<div className="rounded-2xl border border-gray-200 bg-white p-5 dark:border-gray-800 dark:bg-white/[0.03] md:p-6">
<h3 className="mb-4 text-lg font-semibold text-gray-800 dark:text-white/90">
Bảng Số Liệu Chi Tiết (Hôm nay vs Hôm qua)
</h3>
<div className="overflow-x-auto">
<table className="w-full text-left border-collapse">
<thead>
<tr className="border-b border-gray-200 dark:border-gray-800">
<th className="py-3 text-sm font-medium text-gray-500 dark:text-gray-400">Nội dung</th>
<th className="py-3 text-sm font-medium text-gray-500 dark:text-gray-400">Hôm nay</th>
<th className="py-3 text-sm font-medium text-gray-500 dark:text-gray-400">Hôm qua</th>
<th className="py-3 text-sm font-medium text-gray-500 dark:text-gray-400">Tăng trưởng</th>
</tr>
</thead>
<tbody>
{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 (
<tr key={metric.key} className="border-b border-gray-100 dark:border-gray-800/50 last:border-0">
<td className="py-3 text-sm font-medium text-gray-800 dark:text-white/90">{metric.name}</td>
<td className="py-3 text-sm text-gray-600 dark:text-gray-300">{loading ? "..." : todayVal.toLocaleString()}</td>
<td className="py-3 text-sm text-gray-600 dark:text-gray-300">{loading ? "..." : yesterdayVal.toLocaleString()}</td>
<td className={`py-3 text-sm font-medium ${growth >= 0 ? "text-success-500" : "text-error-500"}`}>
{loading ? "..." : `${growth >= 0 ? "+" : ""}${growth.toFixed(2)}%`}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
);
}

View File

@@ -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<StatisticResponse | null>(null);
const [yesterdayData, setYesterdayData] = useState<StatisticResponse | null>(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 (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:gap-6">
{/* <!-- Metric Item Start --> */}
@@ -15,15 +56,15 @@ export const EcommerceMetrics = () => {
<div className="flex items-end justify-between mt-5">
<div>
<span className="text-sm text-gray-500 dark:text-gray-400">
Customers
Tổng Người Dùng
</span>
<h4 className="mt-2 font-bold text-gray-800 text-title-sm dark:text-white/90">
3,782
{loading ? "..." : todayData?.total_users?.toLocaleString() || "N/A"}
</h4>
</div>
<Badge color="success">
<ArrowUpIcon />
11.01%
<Badge color={userGrowth >= 0 ? "success" : "error"}>
{userGrowth >= 0 ? <ArrowUpIcon /> : <ArrowDownIcon className="text-error-500" />}
{Math.abs(userGrowth).toFixed(2)}%
</Badge>
</div>
</div>
@@ -37,16 +78,16 @@ export const EcommerceMetrics = () => {
<div className="flex items-end justify-between mt-5">
<div>
<span className="text-sm text-gray-500 dark:text-gray-400">
Orders
Tổng Dự Án
</span>
<h4 className="mt-2 font-bold text-gray-800 text-title-sm dark:text-white/90">
5,359
{loading ? "..." : todayData?.total_projects?.toLocaleString() || "N/A"}
</h4>
</div>
<Badge color="error">
<ArrowDownIcon className="text-error-500" />
9.05%
<Badge color={projectGrowth >= 0 ? "success" : "error"}>
{projectGrowth >= 0 ? <ArrowUpIcon /> : <ArrowDownIcon className="text-error-500" />}
{Math.abs(projectGrowth).toFixed(2)}%
</Badge>
</div>
</div>
@@ -54,3 +95,5 @@ export const EcommerceMetrics = () => {
</div>
);
};
export default EcommerceMetrics;

View File

@@ -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<HTMLInputElement>(null);
const [startDate, setStartDate] = useState<string>("");
const [endDate, setEndDate] = useState<string>("");
const [chartData, setChartData] = useState<StatisticResponse[]>([]);
const [selectedMetricKey, setSelectedMetricKey] = useState<string>("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:
'<svg class="stroke-current" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12.5 15L7.5 10L12.5 5" stroke="" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
nextArrow:
'<svg class="stroke-current" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M7.5 15L12.5 10L7.5 5" stroke="" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
});
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<HTMLInputElement>) => {
const val = e.target.value;
setStartDate(val);
fetchChartData(val, endDate);
};
const handleEndDateChange = (e: React.ChangeEvent<HTMLInputElement>) => {
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 (
<div className="rounded-2xl border border-gray-200 bg-white px-5 pb-5 pt-5 dark:border-gray-800 dark:bg-white/[0.03] sm:px-6 sm:pt-6">
<div className="flex flex-col gap-5 mb-6 sm:flex-row sm:justify-between">
<div className="w-full">
<h3 className="text-lg font-semibold text-gray-800 dark:text-white/90">
Statistics
Thống hệ thống
</h3>
<p className="mt-1 text-gray-500 text-theme-sm dark:text-gray-400">
Target you've set for each month
Dữ liệu thống theo thời gian
</p>
</div>
<div className="flex items-center gap-3 sm:justify-end">
<ChartTab />
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-end">
{/* Dropdown chọn Metric */}
<select
value={selectedMetricKey}
onChange={(e) => setSelectedMetricKey(e.target.value)}
className="px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-200 rounded-lg focus:outline-none focus:border-brand-300 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300"
>
{METRICS.map((metric) => (
<option key={metric.key} value={metric.key}>
{metric.name}
</option>
))}
</select>
{/* Date Picker Start */}
<div className="relative inline-flex items-center">
<CalenderIcon className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 lg:left-3 lg:top-1/2 lg:translate-x-0 lg:-translate-y-1/2 text-gray-500 dark:text-gray-400 pointer-events-none z-10" />
<input
ref={datePickerRef}
className="h-10 w-10 lg:w-40 lg:h-auto lg:pl-10 lg:pr-3 lg:py-2 rounded-lg border border-gray-200 bg-white text-sm font-medium text-transparent lg:text-gray-700 outline-none dark:border-gray-700 dark:bg-gray-800 dark:lg:text-gray-300 cursor-pointer"
placeholder="Select date range"
type="date"
value={startDate}
onChange={handleStartDateChange}
className="px-3 py-2 rounded-lg border border-gray-200 bg-white text-sm font-medium text-gray-700 outline-none dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300 cursor-pointer"
/>
</div>
{/* Date Picker End */}
<div className="relative inline-flex items-center">
<input
type="date"
value={endDate}
onChange={handleEndDateChange}
className="px-3 py-2 rounded-lg border border-gray-200 bg-white text-sm font-medium text-gray-700 outline-none dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300 cursor-pointer"
/>
</div>
</div>
@@ -172,7 +170,11 @@ export default function StatisticsChart() {
<div className="max-w-full overflow-x-auto custom-scrollbar">
<div className="min-w-[1000px] xl:min-w-full">
<Chart options={options} series={series} type="area" height={310} />
{loading ? (
<div className="flex items-center justify-center h-[310px]">Đang tải...</div>
) : (
<Chart options={options} series={series} type="area" height={310} />
)}
</div>
</div>
</div>

View File

@@ -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;
}

View File

@@ -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' }}
/>
<Image
width={154}
@@ -97,6 +96,7 @@ const AppHeader: React.FC = () => {
className="hidden dark:block"
src="./images/logo/logo.svg"
alt="Logo"
style={{ height: 'auto' }}
/>
</Link>
@@ -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]"
/>
<button className="absolute right-2.5 top-1/2 inline-flex -translate-y-1/2 items-center gap-0.5 rounded-lg border border-gray-200 bg-gray-50 px-[7px] py-[4.5px] text-xs -tracking-[0.2px] text-gray-500 dark:border-gray-800 dark:bg-white/[0.03] dark:text-gray-400">
<button className="absolute right-2.5 top-1/2 inline-flex -translate-y-1/2 items-center gap-0.5 rounded-lg border border-gray-200 bg-gray-50 px-[7px] py-[4.5px] text-xs -tracking-[0.2px] text-gray-500 dark:border-gray-800 dark:bg-white/3 dark:text-gray-400">
<span> </span>
<span> K </span>
</button>

View File

@@ -275,6 +275,7 @@ const AppSidebar: React.FC = () => {
alt="Logo"
width={isExpanded || isHovered || isMobileOpen ? 80 : 32}
height={isExpanded || isHovered || isMobileOpen ? 50 : 32}
style={{ height: 'auto' }}
/>
</Link>
</div>

View File

@@ -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<CommonResponse<StatisticResponse[]>> => {
const response = await api.get(API.Statistics.GET_ALL, { params });
return response?.data;
};
export const getStatisticByDate = async (date: string): Promise<CommonResponse<StatisticResponse>> => {
const response = await api.get(API.Statistics.GET_BY_DATE(date));
return response?.data;
};