feat:new analytics dashboard
This commit is contained in:
commit
c82b3dfc21
|
|
@ -44,27 +44,82 @@ function processMonthlyData(count: number[]): {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getRangeAcrossMonths(
|
||||||
|
data: any[],
|
||||||
|
startMonth: number,
|
||||||
|
startDay: number,
|
||||||
|
endMonth: number,
|
||||||
|
endDay: number
|
||||||
|
) {
|
||||||
|
const view: number[] = [];
|
||||||
|
const comment: number[] = [];
|
||||||
|
const share: number[] = [];
|
||||||
|
const labels: string[] = [];
|
||||||
|
|
||||||
|
const sortedData = data.sort((a, b) => a.month - b.month);
|
||||||
|
for (const monthData of sortedData) {
|
||||||
|
const { month, view: v, comment: c, share: s } = monthData;
|
||||||
|
|
||||||
|
if (month < startMonth || month > endMonth) continue;
|
||||||
|
|
||||||
|
let startIndex = 0;
|
||||||
|
let endIndex = v.length - 1;
|
||||||
|
|
||||||
|
if (month === startMonth) startIndex = startDay - 1;
|
||||||
|
if (month === endMonth) endIndex = endDay - 1;
|
||||||
|
|
||||||
|
for (let i = startIndex; i <= endIndex; i++) {
|
||||||
|
view.push(v[i]);
|
||||||
|
comment.push(c[i]);
|
||||||
|
share.push(s[i]);
|
||||||
|
|
||||||
|
const label = `${(i + 1).toString().padStart(2, "0")} - ${month
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0")}`;
|
||||||
|
labels.push(label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { view, comment, share, labels };
|
||||||
|
}
|
||||||
|
|
||||||
const ApexChartColumn = (props: {
|
const ApexChartColumn = (props: {
|
||||||
type: string;
|
type: string;
|
||||||
date: string;
|
date: string;
|
||||||
view: string[];
|
view: string[];
|
||||||
|
range: { start: any; end: any };
|
||||||
}) => {
|
}) => {
|
||||||
const { date, type, view } = props;
|
const { date, type, view, range } = props;
|
||||||
const [categories, setCategories] = useState<string[]>([]);
|
const [categories, setCategories] = useState<string[]>([]);
|
||||||
const [series, setSeries] = useState<{ name: string; data: number[] }[]>([]);
|
const [series, setSeries] = useState<{ name: string; data: number[] }[]>([]);
|
||||||
const [seriesComment, setSeriesComment] = useState<number[]>([]);
|
const [seriesComment, setSeriesComment] = useState<number[]>([]);
|
||||||
const [seriesView, setSeriesView] = useState<number[]>([]);
|
const [seriesView, setSeriesView] = useState<number[]>([]);
|
||||||
const [seriesShare, setSeriesShare] = useState<number[]>([]);
|
const [seriesShare, setSeriesShare] = useState<number[]>([]);
|
||||||
|
const [years, setYear] = useState("");
|
||||||
|
const [datas, setDatas] = useState<any>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
initFetch();
|
initFetch();
|
||||||
}, [date, type, view]);
|
}, [date, type, view, range]);
|
||||||
|
|
||||||
const initFetch = async () => {
|
const initFetch = async () => {
|
||||||
const splitDate = date.split(" ");
|
const splitDate = date.split(" ");
|
||||||
|
const splitDateDaily = String(range.start.year);
|
||||||
|
let data = [];
|
||||||
|
|
||||||
const res = await getStatisticMonthly(splitDate[1], getUnixTimestamp());
|
if (
|
||||||
const data = res?.data?.data;
|
(type === "monthly" && splitDate[1] === years) ||
|
||||||
|
(type === "daily" && splitDateDaily === years)
|
||||||
|
) {
|
||||||
|
data = datas;
|
||||||
|
} else {
|
||||||
|
const res = await getStatisticMonthly(
|
||||||
|
type === "monthly" ? splitDate[1] : splitDateDaily,
|
||||||
|
getUnixTimestamp()
|
||||||
|
);
|
||||||
|
data = res?.data?.data;
|
||||||
|
setDatas(data);
|
||||||
|
}
|
||||||
const getDatas = data?.find(
|
const getDatas = data?.find(
|
||||||
(a: any) =>
|
(a: any) =>
|
||||||
a.month == Number(splitDate[0]) && a.year === Number(splitDate[1])
|
a.month == Number(splitDate[0]) && a.year === Number(splitDate[1])
|
||||||
|
|
@ -95,16 +150,32 @@ const ApexChartColumn = (props: {
|
||||||
setSeriesView(getDatas.view);
|
setSeriesView(getDatas.view);
|
||||||
setSeriesShare(getDatas.share);
|
setSeriesShare(getDatas.share);
|
||||||
}
|
}
|
||||||
if (type === "weekly") {
|
if (type === "daily") {
|
||||||
const category = [];
|
const mappedData = getRangeAcrossMonths(
|
||||||
for (let i = 1; i <= temp1.weeks.length; i++) {
|
data,
|
||||||
category.push(`Week ${i}`);
|
range.start.month,
|
||||||
}
|
range.start.day,
|
||||||
setCategories(category);
|
range.end.month,
|
||||||
|
range.end.day
|
||||||
|
);
|
||||||
|
|
||||||
|
setSeriesComment(mappedData.comment);
|
||||||
|
setSeriesView(mappedData.view);
|
||||||
|
setSeriesShare(mappedData.share);
|
||||||
|
setCategories(mappedData.labels);
|
||||||
}
|
}
|
||||||
|
// if (type === "weekly") {
|
||||||
|
// const category = [];
|
||||||
|
// for (let i = 1; i <= temp1.weeks.length; i++) {
|
||||||
|
// category.push(`Week ${i}`);
|
||||||
|
// }
|
||||||
|
// setCategories(category);
|
||||||
|
// }
|
||||||
} else {
|
} else {
|
||||||
setSeriesComment([]);
|
setSeriesComment([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setYear(type === "monthly" ? splitDate[1] : splitDateDaily);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -142,7 +213,7 @@ const ApexChartColumn = (props: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
},
|
},
|
||||||
xaxis: {
|
xaxis: {
|
||||||
categories: type == "weekly" ? categories : [],
|
categories: type == "daily" ? categories : [],
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
series={series}
|
series={series}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,369 @@
|
||||||
|
import React, { useState, useCallback, useEffect, useRef } from "react";
|
||||||
|
import ReactApexChart from "react-apexcharts";
|
||||||
|
import ApexCharts from "apexcharts";
|
||||||
|
import { init } from "next/dist/compiled/webpack/webpack";
|
||||||
|
|
||||||
|
const wilayah = ["sumut", "bali", "jateng", "jabar", "metro", "papua", "riau"];
|
||||||
|
|
||||||
|
type TypeItem = {
|
||||||
|
year: number;
|
||||||
|
month: number;
|
||||||
|
user: {
|
||||||
|
list: { name: string; count: number }[];
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
|
||||||
|
interface MappedData {
|
||||||
|
result: { x: string; y: number }[] | [];
|
||||||
|
details: { name: string; count: number }[][];
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateDummyData = () => {
|
||||||
|
const getDaysInMonth = (year: number, month: number) => {
|
||||||
|
return new Date(year, month, 0).getDate();
|
||||||
|
};
|
||||||
|
|
||||||
|
const dummy = [];
|
||||||
|
|
||||||
|
for (let month = 1; month <= 7; month++) {
|
||||||
|
const userData = [];
|
||||||
|
const daysInMonth = getDaysInMonth(2025, month);
|
||||||
|
|
||||||
|
for (let day = 1; day <= daysInMonth; day++) {
|
||||||
|
// Buat 2-4 wilayah acak per hari
|
||||||
|
const regionCount = Math.floor(Math.random() * 3) + 2;
|
||||||
|
const usedRegions = wilayah
|
||||||
|
.sort(() => 0.5 - Math.random())
|
||||||
|
.slice(0, regionCount);
|
||||||
|
|
||||||
|
const list = usedRegions.map((name) => ({
|
||||||
|
name,
|
||||||
|
count: Math.floor(Math.random() * 10) + 1,
|
||||||
|
}));
|
||||||
|
|
||||||
|
userData.push({ list });
|
||||||
|
}
|
||||||
|
|
||||||
|
dummy.push({
|
||||||
|
year: 2025,
|
||||||
|
month,
|
||||||
|
user: userData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return dummy;
|
||||||
|
};
|
||||||
|
|
||||||
|
const dummy = generateDummyData();
|
||||||
|
|
||||||
|
const colors = ["#008FFB"];
|
||||||
|
|
||||||
|
export const makeDataByMonth = (
|
||||||
|
data: TypeItem[],
|
||||||
|
month: number
|
||||||
|
): MappedData => {
|
||||||
|
const result: { x: string; y: number }[] = [];
|
||||||
|
const details: { name: string; count: number }[][] = [];
|
||||||
|
|
||||||
|
const filtered = data.find((entry) => entry.month === month);
|
||||||
|
|
||||||
|
if (!filtered) return { result: [], details: [] };
|
||||||
|
|
||||||
|
filtered.user.forEach((u, idx) => {
|
||||||
|
const total = u.list.reduce((sum, item) => sum + item.count, 0);
|
||||||
|
details.push(u.list);
|
||||||
|
console.log("u.list", u.list);
|
||||||
|
result.push({
|
||||||
|
x: (idx + 1).toString(),
|
||||||
|
y: total,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return { result, details };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const makeDataByRange = (
|
||||||
|
data: TypeItem[],
|
||||||
|
startMonth: number,
|
||||||
|
startDay: number,
|
||||||
|
endMonth: number,
|
||||||
|
endDay: number
|
||||||
|
) => {
|
||||||
|
const user: number[] = [];
|
||||||
|
const labels: string[] = [];
|
||||||
|
const details = [];
|
||||||
|
const result: { x: string; y: number }[] = [];
|
||||||
|
const sortedData = data.sort((a, b) => a.month - b.month);
|
||||||
|
for (const monthData of sortedData) {
|
||||||
|
const { month, user: u } = monthData;
|
||||||
|
|
||||||
|
if (month < startMonth || month > endMonth) continue;
|
||||||
|
|
||||||
|
let startIndex = 0;
|
||||||
|
let endIndex = u.length - 1;
|
||||||
|
|
||||||
|
if (month === startMonth) startIndex = startDay - 1;
|
||||||
|
if (month === endMonth) endIndex = endDay - 1;
|
||||||
|
|
||||||
|
for (let i = startIndex; i <= endIndex; i++) {
|
||||||
|
const userEntry = u[i];
|
||||||
|
|
||||||
|
if (!userEntry) continue;
|
||||||
|
|
||||||
|
const total = userEntry.list.reduce((sum, item) => sum + item.count, 0);
|
||||||
|
user.push(total);
|
||||||
|
details.push(userEntry.list);
|
||||||
|
const label = `${(i + 1).toString().padStart(2, "0")} - ${month
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0")}`;
|
||||||
|
labels.push(label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < user.length; i++) {
|
||||||
|
result.push({ x: labels[i], y: user[i] });
|
||||||
|
}
|
||||||
|
|
||||||
|
return { result, details };
|
||||||
|
};
|
||||||
|
|
||||||
|
const ApexChartDynamic = (props: {
|
||||||
|
type: string;
|
||||||
|
date: string;
|
||||||
|
range: { start: any; end: any };
|
||||||
|
}) => {
|
||||||
|
const { date, type, range } = props;
|
||||||
|
const [state, setState] = useState<{
|
||||||
|
series: ApexAxisChartSeries;
|
||||||
|
options: ApexCharts.ApexOptions;
|
||||||
|
seriesQuarter: ApexAxisChartSeries;
|
||||||
|
}>({
|
||||||
|
series: [{ data: [] }],
|
||||||
|
options: {
|
||||||
|
chart: {
|
||||||
|
id: "barYear",
|
||||||
|
height: 600,
|
||||||
|
width: "100%",
|
||||||
|
type: "bar",
|
||||||
|
events: {
|
||||||
|
dataPointSelection: function (e, chart, opts) {
|
||||||
|
const quarterChartEl = document.querySelector("#chart-quarter");
|
||||||
|
const yearChartEl = document.querySelector("#chart-year");
|
||||||
|
|
||||||
|
if (!quarterChartEl || !yearChartEl) return;
|
||||||
|
|
||||||
|
if (opts.selectedDataPoints[0].length === 1) {
|
||||||
|
if (quarterChartEl.classList.contains("active")) {
|
||||||
|
updateQuarterChart(chart, "barQuarter");
|
||||||
|
} else {
|
||||||
|
yearChartEl.classList.add("chart-quarter-activated");
|
||||||
|
quarterChartEl.classList.add("active");
|
||||||
|
updateQuarterChart(chart, "barQuarter");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
updateQuarterChart(chart, "barQuarter");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.selectedDataPoints[0].length === 0) {
|
||||||
|
yearChartEl.classList.remove("chart-quarter-activated");
|
||||||
|
quarterChartEl.classList.remove("active");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updated: function (chart) {
|
||||||
|
updateQuarterChart(chart, "barQuarter");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plotOptions: {
|
||||||
|
bar: {
|
||||||
|
distributed: true,
|
||||||
|
horizontal: true,
|
||||||
|
barHeight: "100%",
|
||||||
|
dataLabels: {
|
||||||
|
position: "bottom",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
dataLabels: {
|
||||||
|
enabled: true,
|
||||||
|
textAnchor: "start",
|
||||||
|
style: {
|
||||||
|
colors: ["#fff"],
|
||||||
|
},
|
||||||
|
formatter: function (_val, opt) {
|
||||||
|
return opt.w.globals.labels[opt.dataPointIndex];
|
||||||
|
},
|
||||||
|
offsetX: 0,
|
||||||
|
dropShadow: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
colors: colors,
|
||||||
|
states: {
|
||||||
|
normal: {
|
||||||
|
filter: {
|
||||||
|
type: "desaturate",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
active: {
|
||||||
|
allowMultipleDataPointsSelection: false,
|
||||||
|
filter: {
|
||||||
|
type: "darken",
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
x: { show: false },
|
||||||
|
y: {
|
||||||
|
title: {
|
||||||
|
formatter: ((_seriesName: string, opts: any) =>
|
||||||
|
opts.w.globals.labels[opts.dataPointIndex]) as (
|
||||||
|
seriesName: string
|
||||||
|
) => string,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
yaxis: {
|
||||||
|
labels: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
seriesQuarter: [{ data: [] }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const [years, setYear] = useState("");
|
||||||
|
const [datas, setDatas] = useState<any>([]);
|
||||||
|
const [details, setDetails] = useState<{ name: string; count: number }[][]>(
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const detailsRef = useRef(details);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
detailsRef.current = details;
|
||||||
|
}, [details]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initFetch();
|
||||||
|
}, [date, range.start, range.end, type]);
|
||||||
|
|
||||||
|
const initFetch = async () => {
|
||||||
|
const splitDate = date.split(" ");
|
||||||
|
const splitDateDaily = String(range.start.year);
|
||||||
|
const currentYear = type === "monthly" ? splitDate[1] : splitDateDaily;
|
||||||
|
|
||||||
|
let data = [];
|
||||||
|
|
||||||
|
if (currentYear === years) {
|
||||||
|
console.log("if", datas);
|
||||||
|
data = datas;
|
||||||
|
} else {
|
||||||
|
// const res = await getStatisticMonthly(
|
||||||
|
// type === "monthly" ? splitDate[1] : splitDateDaily
|
||||||
|
// );
|
||||||
|
// data = res?.data?.data;
|
||||||
|
data = dummy;
|
||||||
|
console.log("dataaa", data);
|
||||||
|
setDatas(data);
|
||||||
|
setYear(currentYear);
|
||||||
|
}
|
||||||
|
// console.log("datas", data);
|
||||||
|
if (data) {
|
||||||
|
if (type == "monthly") {
|
||||||
|
const mappedData: MappedData = makeDataByMonth(
|
||||||
|
data,
|
||||||
|
Number(splitDate[0])
|
||||||
|
);
|
||||||
|
console.log("mapped month", mappedData);
|
||||||
|
setDetails(mappedData.details);
|
||||||
|
setState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
series: [{ data: mappedData.result }],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type == "daily") {
|
||||||
|
const mappedData = makeDataByRange(
|
||||||
|
data,
|
||||||
|
range.start.month,
|
||||||
|
range.start.day,
|
||||||
|
range.end.month,
|
||||||
|
range.end.day
|
||||||
|
);
|
||||||
|
console.log("mmapped,", mappedData.details);
|
||||||
|
setDetails(mappedData.details);
|
||||||
|
setState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
series: [{ data: mappedData.result }],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
series: [{ data: [] }],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateQuarterChart = useCallback(
|
||||||
|
(chart: any, id: string) => {
|
||||||
|
const selectedIndex = chart?.w?.config?.series[0]?.data?.findIndex(
|
||||||
|
(d: any, i: number) => {
|
||||||
|
return chart.w.globals.selectedDataPoints[0]?.includes(i);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selectedIndex !== -1) {
|
||||||
|
const counts = detailsRef.current[selectedIndex];
|
||||||
|
console.log("countres", counts, detailsRef);
|
||||||
|
|
||||||
|
const quarterData = [
|
||||||
|
{
|
||||||
|
name: `${selectedIndex + 1}`,
|
||||||
|
data: counts,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
ApexCharts.exec(id, "updateSeries", quarterData);
|
||||||
|
|
||||||
|
setState((prev: any) => ({
|
||||||
|
...prev,
|
||||||
|
seriesQuarter: quarterData,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[detailsRef]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="lg:h-[600px]">
|
||||||
|
<div id="wrap" className="flex flex-col lg:flex-row gap-2">
|
||||||
|
<div id="chart-year" className="lg:w-[80%] h-[600px]">
|
||||||
|
<ReactApexChart
|
||||||
|
options={state.options}
|
||||||
|
series={state.series}
|
||||||
|
type="bar"
|
||||||
|
height={600}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="chart-quarter" className="w-full lg:w-[20%]">
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
{state.seriesQuarter[0].data.map((list: any, index) => (
|
||||||
|
<div key={index} className="flex flex-row gap-2">
|
||||||
|
<p className="font-semibold capitalize">{list?.name} : </p>
|
||||||
|
<p>{list?.count}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ApexChartDynamic;
|
||||||
|
|
@ -0,0 +1,233 @@
|
||||||
|
"use client";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import ReactApexChart from "react-apexcharts";
|
||||||
|
import dummy from "../../../../const/dummy.json";
|
||||||
|
import { getStatisticUsersMonthly } from "@/services/article";
|
||||||
|
|
||||||
|
function getRangeAcrossMonths(
|
||||||
|
data: any[],
|
||||||
|
startMonth: number,
|
||||||
|
startDay: number,
|
||||||
|
endMonth: number,
|
||||||
|
endDay: number
|
||||||
|
) {
|
||||||
|
const labels: string[] = [];
|
||||||
|
const users: { name: string; data: number[] }[] = [];
|
||||||
|
const sortedData = data.sort((a, b) => a.month - b.month);
|
||||||
|
console.log("sorted,data", sortedData);
|
||||||
|
for (const monthData of sortedData) {
|
||||||
|
const { month, user_levels: u } = monthData;
|
||||||
|
if (month < startMonth || month > endMonth) continue;
|
||||||
|
console.log("uuu", month, startMonth, endMonth, u.length);
|
||||||
|
|
||||||
|
let startIndex = 0;
|
||||||
|
let endIndex = u[0].data.length - 1;
|
||||||
|
|
||||||
|
if (month === startMonth) startIndex = startDay - 1;
|
||||||
|
if (month === endMonth) endIndex = endDay - 1;
|
||||||
|
|
||||||
|
console.log("start,eend", startIndex, endIndex, month);
|
||||||
|
for (let j = 0; j < u.length; j++) {
|
||||||
|
const now = u[j].data;
|
||||||
|
// console.log("u.j", now);
|
||||||
|
const temp = [];
|
||||||
|
for (let i = startIndex; i <= endIndex; i++) {
|
||||||
|
temp.push(now[i]);
|
||||||
|
|
||||||
|
if (j == 0) {
|
||||||
|
const label = `${(i + 1).toString().padStart(2, "0")} - ${month
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0")}`;
|
||||||
|
|
||||||
|
labels.push(label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const existing = users.find((item) => item.name === u[j].name);
|
||||||
|
if (existing) {
|
||||||
|
existing.data.push(...temp); // gabungkan data
|
||||||
|
} else {
|
||||||
|
users.push({ name: u[j].name, data: temp }); // tambahkan baru
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("users", users);
|
||||||
|
|
||||||
|
return { users, labels };
|
||||||
|
}
|
||||||
|
|
||||||
|
const ApexMultiLineChart = (props: {
|
||||||
|
type: string;
|
||||||
|
date: string;
|
||||||
|
range: { start: any; end: any };
|
||||||
|
}) => {
|
||||||
|
const { date, type, range } = props;
|
||||||
|
const [datas, setDatas] = useState<any>([]);
|
||||||
|
const [years, setYear] = useState("");
|
||||||
|
|
||||||
|
const [series, setSeries] = useState<any>([]);
|
||||||
|
const [categories, setCategories] = useState<string[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initFetch();
|
||||||
|
}, [date, type, range.start, range.end]);
|
||||||
|
|
||||||
|
const initFetch = async () => {
|
||||||
|
const splitDate = date.split(" ");
|
||||||
|
const splitDateDaily = String(range.start.year);
|
||||||
|
let data: any = [];
|
||||||
|
|
||||||
|
if (
|
||||||
|
(type === "monthly" && splitDate[1] === years) ||
|
||||||
|
(type === "daily" && splitDateDaily === years)
|
||||||
|
) {
|
||||||
|
data = datas;
|
||||||
|
} else {
|
||||||
|
const res = await getStatisticUsersMonthly(
|
||||||
|
type === "monthly" ? splitDate[1] : splitDateDaily
|
||||||
|
);
|
||||||
|
data = res?.data?.data;
|
||||||
|
// data = dummy.data;
|
||||||
|
setDatas(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "daily") {
|
||||||
|
const mappedData = getRangeAcrossMonths(
|
||||||
|
data,
|
||||||
|
range.start.month,
|
||||||
|
range.start.day,
|
||||||
|
range.end.month,
|
||||||
|
range.end.day
|
||||||
|
);
|
||||||
|
setSeries(mappedData.users);
|
||||||
|
setCategories(mappedData.labels);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "monthly") {
|
||||||
|
const getDatas = data?.find(
|
||||||
|
(a: any) =>
|
||||||
|
a.month == Number(splitDate[0]) && a.year === Number(splitDate[1])
|
||||||
|
);
|
||||||
|
if (getDatas) {
|
||||||
|
setSeries(getDatas.user_levels);
|
||||||
|
} else {
|
||||||
|
setSeries([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setYear(type === "monthly" ? splitDate[1] : splitDateDaily);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div id="chart">
|
||||||
|
<ReactApexChart
|
||||||
|
series={series}
|
||||||
|
options={{
|
||||||
|
chart: {
|
||||||
|
height: 600,
|
||||||
|
type: "line",
|
||||||
|
dropShadow: {
|
||||||
|
enabled: true,
|
||||||
|
color: "#000",
|
||||||
|
top: 18,
|
||||||
|
left: 7,
|
||||||
|
blur: 10,
|
||||||
|
opacity: 0.5,
|
||||||
|
},
|
||||||
|
zoom: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
toolbar: {
|
||||||
|
show: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
colors: [
|
||||||
|
"#1f77b4",
|
||||||
|
"#2ca02c",
|
||||||
|
"#9467bd",
|
||||||
|
"#8c564b",
|
||||||
|
"#e377c2",
|
||||||
|
"#7f7f7f",
|
||||||
|
"#bcbd22",
|
||||||
|
"#17becf",
|
||||||
|
"#aec7e8",
|
||||||
|
"#98df8a",
|
||||||
|
"#c5b0d5",
|
||||||
|
"#c49c94",
|
||||||
|
"#9edae5",
|
||||||
|
"#393b79",
|
||||||
|
"#637939",
|
||||||
|
"#8c6d31",
|
||||||
|
"#843c39",
|
||||||
|
"#7b4173",
|
||||||
|
"#3182bd",
|
||||||
|
"#6baed6",
|
||||||
|
"#9ecae1",
|
||||||
|
"#31a354",
|
||||||
|
"#74c476",
|
||||||
|
"#a1d99b",
|
||||||
|
"#756bb1",
|
||||||
|
"#9e9ac8",
|
||||||
|
"#bcbddc",
|
||||||
|
"#636363",
|
||||||
|
"#969696",
|
||||||
|
"#bdbdbd",
|
||||||
|
"#17becf",
|
||||||
|
"#8da0cb",
|
||||||
|
"#66c2a5",
|
||||||
|
"#a6d854",
|
||||||
|
"#ffd92f",
|
||||||
|
"#b3b3b3",
|
||||||
|
"#80b1d3",
|
||||||
|
"#fdb462",
|
||||||
|
],
|
||||||
|
dataLabels: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
stroke: {
|
||||||
|
curve: "smooth",
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
text: "Users",
|
||||||
|
align: "left",
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
borderColor: "#e7e7e7",
|
||||||
|
row: {
|
||||||
|
colors: ["#f3f3f3", "transparent"],
|
||||||
|
opacity: 0.5,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
markers: {
|
||||||
|
size: 0.5,
|
||||||
|
},
|
||||||
|
xaxis: {
|
||||||
|
categories: type == "daily" ? categories : [],
|
||||||
|
|
||||||
|
title: {
|
||||||
|
text: "Days",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
title: {
|
||||||
|
text: "Articles",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
position: "right",
|
||||||
|
horizontalAlign: "right",
|
||||||
|
floating: false,
|
||||||
|
offsetY: -25,
|
||||||
|
offsetX: -5,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
type="line"
|
||||||
|
height={600}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div id="html-dist"></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ApexMultiLineChart;
|
||||||
|
|
@ -0,0 +1,198 @@
|
||||||
|
"use client";
|
||||||
|
import React, { Component, useEffect, useState } from "react";
|
||||||
|
import ReactApexChart from "react-apexcharts";
|
||||||
|
import dummyData from "../../../../const/dummy.json";
|
||||||
|
import {
|
||||||
|
getStatisticMonthly,
|
||||||
|
getStatisticVisitorsMonthly,
|
||||||
|
} from "@/services/article";
|
||||||
|
|
||||||
|
type WeekData = {
|
||||||
|
week: number;
|
||||||
|
days: number[];
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RemainingDays = {
|
||||||
|
days: number[];
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
function processMonthlyData(count: number[]): {
|
||||||
|
weeks: WeekData[];
|
||||||
|
remaining_days: RemainingDays;
|
||||||
|
} {
|
||||||
|
const weeks: WeekData[] = [];
|
||||||
|
let weekIndex = 1;
|
||||||
|
|
||||||
|
for (let i = 0; i < count.length; i += 7) {
|
||||||
|
const weekData = count.slice(i, i + 7);
|
||||||
|
weeks.push({
|
||||||
|
week: weekIndex,
|
||||||
|
days: weekData,
|
||||||
|
total: weekData.reduce((sum, day) => sum + day, 0),
|
||||||
|
});
|
||||||
|
weekIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const remainingDays: RemainingDays = {
|
||||||
|
days: count.length % 7 === 0 ? [] : count.slice(-count.length % 7),
|
||||||
|
total: count.slice(-count.length % 7).reduce((sum, day) => sum + day, 0),
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
weeks,
|
||||||
|
remaining_days: remainingDays,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRangeAcrossMonths(
|
||||||
|
data: any[],
|
||||||
|
startMonth: number,
|
||||||
|
startDay: number,
|
||||||
|
endMonth: number,
|
||||||
|
endDay: number
|
||||||
|
) {
|
||||||
|
const visitors: number[] = [];
|
||||||
|
|
||||||
|
const labels: string[] = [];
|
||||||
|
|
||||||
|
const sortedData = data.sort((a, b) => a.month - b.month);
|
||||||
|
for (const monthData of sortedData) {
|
||||||
|
const { month, data: v } = monthData;
|
||||||
|
|
||||||
|
if (month < startMonth || month > endMonth) continue;
|
||||||
|
|
||||||
|
let startIndex = 0;
|
||||||
|
let endIndex = v.length - 1;
|
||||||
|
|
||||||
|
if (month === startMonth) startIndex = startDay - 1;
|
||||||
|
if (month === endMonth) endIndex = endDay - 1;
|
||||||
|
|
||||||
|
for (let i = startIndex; i <= endIndex; i++) {
|
||||||
|
visitors.push(v[i]);
|
||||||
|
|
||||||
|
const label = `${(i + 1).toString().padStart(2, "0")} - ${month
|
||||||
|
.toString()
|
||||||
|
.padStart(2, "0")} `;
|
||||||
|
labels.push(label);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { visitors, labels };
|
||||||
|
}
|
||||||
|
|
||||||
|
const ApexChartColumnVisitors = (props: {
|
||||||
|
type: string;
|
||||||
|
date: string;
|
||||||
|
range: { start: any; end: any };
|
||||||
|
total: (data: number) => void;
|
||||||
|
}) => {
|
||||||
|
const { date, type, range, total } = props;
|
||||||
|
const [categories, setCategories] = useState<string[]>([]);
|
||||||
|
const [series, setSeries] = useState<{ name: string; data: number[] }[]>([]);
|
||||||
|
const [seriesVisitors, setSeriesVisitors] = useState<number[]>([]);
|
||||||
|
|
||||||
|
const [years, setYear] = useState("");
|
||||||
|
const [datas, setDatas] = useState<any>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
initFetch();
|
||||||
|
}, [date, type, range]);
|
||||||
|
|
||||||
|
const initFetch = async () => {
|
||||||
|
const splitDate = date.split(" ");
|
||||||
|
const splitDateDaily = String(range.start.year);
|
||||||
|
let data = [];
|
||||||
|
|
||||||
|
if (
|
||||||
|
(type === "monthly" && splitDate[1] === years) ||
|
||||||
|
(type === "daily" && splitDateDaily === years)
|
||||||
|
) {
|
||||||
|
data = datas;
|
||||||
|
} else {
|
||||||
|
const res = await getStatisticVisitorsMonthly(
|
||||||
|
type === "monthly" ? splitDate[1] : splitDateDaily
|
||||||
|
);
|
||||||
|
data = res?.data?.data;
|
||||||
|
console.log("daaaa", data);
|
||||||
|
setDatas(data);
|
||||||
|
}
|
||||||
|
const getDatas = data?.find(
|
||||||
|
(a: any) =>
|
||||||
|
a.month == Number(splitDate[0]) && a.year === Number(splitDate[1])
|
||||||
|
);
|
||||||
|
if (getDatas) {
|
||||||
|
if (type == "monthly") {
|
||||||
|
setSeriesVisitors(getDatas.data);
|
||||||
|
total(getDatas.data.reduce((a: number, b: number) => a + b, 0));
|
||||||
|
}
|
||||||
|
if (type === "daily") {
|
||||||
|
const mappedData = getRangeAcrossMonths(
|
||||||
|
data,
|
||||||
|
range.start.month,
|
||||||
|
range.start.day,
|
||||||
|
range.end.month,
|
||||||
|
range.end.day
|
||||||
|
);
|
||||||
|
|
||||||
|
setSeriesVisitors(mappedData.visitors);
|
||||||
|
total(mappedData.visitors.reduce((a: number, b: number) => a + b, 0));
|
||||||
|
|
||||||
|
setCategories(mappedData.labels);
|
||||||
|
}
|
||||||
|
// if (type === "weekly") {
|
||||||
|
// const category = [];
|
||||||
|
// for (let i = 1; i <= temp1.weeks.length; i++) {
|
||||||
|
// category.push(`Week ${i}`);
|
||||||
|
// }
|
||||||
|
// setCategories(category);
|
||||||
|
// }
|
||||||
|
} else {
|
||||||
|
setSeriesVisitors([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
setYear(type === "monthly" ? splitDate[1] : splitDateDaily);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const temp = [
|
||||||
|
{
|
||||||
|
name: "Visitors",
|
||||||
|
data: seriesVisitors || [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
setSeries(temp);
|
||||||
|
}, [seriesVisitors]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="h-full">
|
||||||
|
<div id="chart" className="h-full">
|
||||||
|
<ReactApexChart
|
||||||
|
options={{
|
||||||
|
chart: {
|
||||||
|
height: "100%",
|
||||||
|
type: "area",
|
||||||
|
},
|
||||||
|
stroke: {
|
||||||
|
curve: "smooth",
|
||||||
|
},
|
||||||
|
dataLabels: {
|
||||||
|
enabled: false,
|
||||||
|
},
|
||||||
|
xaxis: {
|
||||||
|
categories: type == "daily" ? categories : [],
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
series={series}
|
||||||
|
type="area"
|
||||||
|
height="100%"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div id="html-dist"></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ApexChartColumnVisitors;
|
||||||
|
|
@ -61,7 +61,17 @@ import {
|
||||||
parseAbsoluteToLocal,
|
parseAbsoluteToLocal,
|
||||||
} from "@internationalized/date";
|
} from "@internationalized/date";
|
||||||
import { Input } from "@heroui/input";
|
import { Input } from "@heroui/input";
|
||||||
import { EyeIconMdi, SearchIcons } from "@/components/icons";
|
import {
|
||||||
|
ChevronLeftIcon,
|
||||||
|
ChevronRightIcon,
|
||||||
|
EyeIconMdi,
|
||||||
|
SearchIcons,
|
||||||
|
} from "@/components/icons";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import ApexChartColumnVisitors from "./chart/visitor-chart";
|
||||||
|
import IndonesiaMap from "@/components/ui/maps-charts";
|
||||||
|
import ApexChartDynamic from "./chart/dynamic-bar-char";
|
||||||
|
import ApexMultiLineChart from "./chart/multiline-chart";
|
||||||
|
|
||||||
type ArticleData = Article & {
|
type ArticleData = Article & {
|
||||||
no: number;
|
no: number;
|
||||||
|
|
@ -75,6 +85,16 @@ interface TopPages {
|
||||||
viewCount: number;
|
viewCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const colors = [
|
||||||
|
"#38bdf8",
|
||||||
|
"#155e75",
|
||||||
|
"#0e7490",
|
||||||
|
"#0284c7",
|
||||||
|
"#0ea5e9",
|
||||||
|
"#38bdf8",
|
||||||
|
"#7dd3fc",
|
||||||
|
];
|
||||||
|
|
||||||
interface PostCount {
|
interface PostCount {
|
||||||
userLevelId: number;
|
userLevelId: number;
|
||||||
no: number;
|
no: number;
|
||||||
|
|
@ -82,6 +102,67 @@ interface PostCount {
|
||||||
totalArticle: number;
|
totalArticle: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const months = [
|
||||||
|
"Jan",
|
||||||
|
"Feb",
|
||||||
|
"Mar",
|
||||||
|
"Apr",
|
||||||
|
"May",
|
||||||
|
"Jun",
|
||||||
|
"Jul",
|
||||||
|
"Aug",
|
||||||
|
"Sep",
|
||||||
|
"Oct",
|
||||||
|
"Nov",
|
||||||
|
"Dec",
|
||||||
|
];
|
||||||
|
|
||||||
|
const rawData = [
|
||||||
|
{ name: "Aceh", value: 32 },
|
||||||
|
{ name: "Sumatera Utara", value: 78 },
|
||||||
|
{ name: "Sumatera Barat", value: 45 },
|
||||||
|
{ name: "Riau", value: 63 },
|
||||||
|
{ name: "Jambi", value: 21 },
|
||||||
|
{ name: "Sumatera Selatan", value: 56 },
|
||||||
|
{ name: "Bengkulu", value: 11 },
|
||||||
|
{ name: "Lampung", value: 49 },
|
||||||
|
{ name: "Kepulauan Bangka Belitung", value: 27 },
|
||||||
|
{ name: "Kepulauan Riau", value: 19 },
|
||||||
|
{ name: "DKI Jakarta", value: 97 },
|
||||||
|
{ name: "Jawa Barat", value: 84 },
|
||||||
|
{ name: "Jawa Tengah", value: 88 },
|
||||||
|
{ name: "Daerah Istimewa Yogyakarta", value: 36 },
|
||||||
|
{ name: "Jawa Timur", value: 91 },
|
||||||
|
{ name: "Banten", value: 52 },
|
||||||
|
{ name: "Bali", value: 66 },
|
||||||
|
{ name: "Nusa Tenggara Barat", value: 40 },
|
||||||
|
{ name: "Nusa Tenggara Timur", value: 59 },
|
||||||
|
{ name: "Kalimantan Barat", value: 61 },
|
||||||
|
{ name: "Kalimantan Tengah", value: 29 },
|
||||||
|
{ name: "Kalimantan Selatan", value: 71 },
|
||||||
|
{ name: "Kalimantan Timur", value: 76 },
|
||||||
|
{ name: "Kalimantan Utara", value: 18 },
|
||||||
|
{ name: "Sulawesi Utara", value: 55 },
|
||||||
|
{ name: "Sulawesi Tengah", value: 31 },
|
||||||
|
{ name: "Sulawesi Selatan", value: 80 },
|
||||||
|
{ name: "Sulawesi Tenggara", value: 34 },
|
||||||
|
{ name: "Gorontalo", value: 23 },
|
||||||
|
{ name: "Sulawesi Barat", value: 12 },
|
||||||
|
{ name: "Maluku", value: 25 },
|
||||||
|
{ name: "Maluku Utara", value: 30 },
|
||||||
|
{ name: "Papua", value: 37 },
|
||||||
|
{ name: "Papua Barat", value: 33 },
|
||||||
|
{ name: "Papua Tengah", value: 16 },
|
||||||
|
{ name: "Papua Pegunungan", value: 8 },
|
||||||
|
{ name: "Papua Selatan", value: 14 },
|
||||||
|
{ name: "Papua Barat Daya", value: 6 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const getTotalData: number[] = [];
|
||||||
|
rawData.map((list) => {
|
||||||
|
getTotalData.push(list.value);
|
||||||
|
});
|
||||||
|
|
||||||
export default function DashboardContainer() {
|
export default function DashboardContainer() {
|
||||||
const { isOpen, onOpen, onOpenChange } = useDisclosure();
|
const { isOpen, onOpen, onOpenChange } = useDisclosure();
|
||||||
|
|
||||||
|
|
@ -122,7 +203,6 @@ export default function DashboardContainer() {
|
||||||
endDate: parseDate(convertDateFormatNoTimeV2(new Date())),
|
endDate: parseDate(convertDateFormatNoTimeV2(new Date())),
|
||||||
});
|
});
|
||||||
|
|
||||||
const [typeDate, setTypeDate] = useState("monthly");
|
|
||||||
const [summary, setSummary] = useState<any>();
|
const [summary, setSummary] = useState<any>();
|
||||||
|
|
||||||
const [topPages, setTopPages] = useState<TopPages[]>([]);
|
const [topPages, setTopPages] = useState<TopPages[]>([]);
|
||||||
|
|
@ -135,10 +215,62 @@ export default function DashboardContainer() {
|
||||||
name: string;
|
name: string;
|
||||||
}>();
|
}>();
|
||||||
const roleId = Cookies.get("urie");
|
const roleId = Cookies.get("urie");
|
||||||
|
const today = new Date();
|
||||||
|
const [chartVisitorTotal, setChartVisitorTotal] = useState(0);
|
||||||
const [recapArticlePage, setRecapArticlePage] = useState(1);
|
const [recapArticlePage, setRecapArticlePage] = useState(1);
|
||||||
const [recapArticleTotalPage, setRecapArticleTotalPage] = useState(1);
|
const [recapArticleTotalPage, setRecapArticleTotalPage] = useState(1);
|
||||||
|
|
||||||
|
const [typeDate, setTypeDate] = useState("daily");
|
||||||
|
const [year, setYear] = useState(today.getFullYear());
|
||||||
|
const [selectedMonth, setSelectedMonth] = useState<Date | null>(today);
|
||||||
|
const [viewsDailyDate, setViewsDailyDate] = useState({
|
||||||
|
start: parseDate(
|
||||||
|
convertDateFormatNoTimeV2(
|
||||||
|
new Date(new Date().setDate(new Date().getDate() - 7))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
end: parseDate(convertDateFormatNoTimeV2(today)),
|
||||||
|
});
|
||||||
|
|
||||||
|
const [typeDateVisitor, setTypeDateVisitor] = useState("daily");
|
||||||
|
const [visitorYear, setVisitorYear] = useState(today.getFullYear());
|
||||||
|
const [visitorSelectedMonth, setVisitorSelectedMonth] = useState<Date | null>(
|
||||||
|
today
|
||||||
|
);
|
||||||
|
const [visitorDailyDate, setVisitorDailyDate] = useState({
|
||||||
|
start: parseDate(
|
||||||
|
convertDateFormatNoTimeV2(
|
||||||
|
new Date(new Date().setDate(new Date().getDate() - 7))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
end: parseDate(convertDateFormatNoTimeV2(today)),
|
||||||
|
});
|
||||||
|
|
||||||
|
const [typeDateUsers, setTypeDateUsers] = useState("daily");
|
||||||
|
const [usersYear, setUsersYear] = useState(today.getFullYear());
|
||||||
|
const [usersSelectedMonth, setUsersSelectedMonth] = useState<Date | null>(
|
||||||
|
today
|
||||||
|
);
|
||||||
|
const [usersDailyDate, setUsersDailyDate] = useState({
|
||||||
|
start: parseDate(
|
||||||
|
convertDateFormatNoTimeV2(
|
||||||
|
new Date(new Date().setDate(new Date().getDate() - 7))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
end: parseDate(convertDateFormatNoTimeV2(today)),
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleMonthClick = (monthIndex: number) => {
|
||||||
|
setSelectedMonth(new Date(year, monthIndex, 1));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMonthClickVisitor = (monthIndex: number) => {
|
||||||
|
setVisitorSelectedMonth(new Date(visitorYear, monthIndex, 1));
|
||||||
|
};
|
||||||
|
const handleMonthClickUsers = (monthIndex: number) => {
|
||||||
|
setUsersSelectedMonth(new Date(usersYear, monthIndex, 1));
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchSummary();
|
fetchSummary();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
@ -251,32 +383,6 @@ export default function DashboardContainer() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getMonthYear = (date: any) => {
|
|
||||||
return date.month + " " + date.year;
|
|
||||||
};
|
|
||||||
const getMonthYearName = (date: any) => {
|
|
||||||
const newDate = new Date(date);
|
|
||||||
|
|
||||||
const months = [
|
|
||||||
"Januari",
|
|
||||||
"Februari",
|
|
||||||
"Maret",
|
|
||||||
"April",
|
|
||||||
"Mei",
|
|
||||||
"Juni",
|
|
||||||
"Juli",
|
|
||||||
"Agustus",
|
|
||||||
"September",
|
|
||||||
"Oktober",
|
|
||||||
"November",
|
|
||||||
"Desember",
|
|
||||||
];
|
|
||||||
const year = newDate.getFullYear();
|
|
||||||
|
|
||||||
const month = months[newDate.getMonth()];
|
|
||||||
return month + " " + year;
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const temp = Array.from(selectedAccordion);
|
const temp = Array.from(selectedAccordion);
|
||||||
console.log("selecette", temp);
|
console.log("selecette", temp);
|
||||||
|
|
@ -593,141 +699,6 @@ export default function DashboardContainer() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col w-full lg:w-[45%] gap-6 shadow-md bg-white dark:bg-[#18181b] rounded-lg p-8 text-sm">
|
|
||||||
<div className="flex justify-between font-semibold">
|
|
||||||
<p>Recent Article</p>
|
|
||||||
<Link href="/admin/article/create">
|
|
||||||
<Button color="primary" variant="bordered">
|
|
||||||
Buat Article
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
{article?.map((list: any) => (
|
|
||||||
<div
|
|
||||||
key={list?.id}
|
|
||||||
className="flex flex-row gap-2 items-center border-b-2 py-2"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
alt="thumbnail"
|
|
||||||
src={list?.thumbnailUrl || `/no-image.jpg`}
|
|
||||||
className="h-[70px] w-[70px] object-cover rounded-lg"
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<p>{textEllipsis(list?.title, 78)}</p>
|
|
||||||
<p className="text-xs">
|
|
||||||
{convertDateFormat(list?.createdAt)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<div className="my-2 w-full flex justify-center">
|
|
||||||
<Pagination
|
|
||||||
isCompact
|
|
||||||
showControls
|
|
||||||
showShadow
|
|
||||||
color="primary"
|
|
||||||
classNames={{
|
|
||||||
base: "bg-transparent",
|
|
||||||
wrapper: "bg-transparent",
|
|
||||||
item: "w-fit px-3",
|
|
||||||
cursor: "w-fit px-3",
|
|
||||||
}}
|
|
||||||
page={page}
|
|
||||||
total={totalPage}
|
|
||||||
onChange={(page) => setPage(page)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-full flex flex-col lg:flex-row gap-6 justify-center ">
|
|
||||||
<div className="border-1 shadow-sm w-screen rounded-lg lg:w-[55%] p-6 flex flex-col">
|
|
||||||
<div className="flex justify-between mb-3">
|
|
||||||
<div className="font-semibold flex flex-col">
|
|
||||||
Analytics
|
|
||||||
<div className="font-normal text-xs text-gray-600 flex flex-row gap-2">
|
|
||||||
<CheckboxGroup
|
|
||||||
label=""
|
|
||||||
value={analyticsView}
|
|
||||||
orientation="vertical"
|
|
||||||
onValueChange={setAnalyticView}
|
|
||||||
className="lg:hidden"
|
|
||||||
>
|
|
||||||
<Checkbox size="sm" value="comment">
|
|
||||||
Comment
|
|
||||||
</Checkbox>
|
|
||||||
<Checkbox size="sm" value="view" color="success">
|
|
||||||
View
|
|
||||||
</Checkbox>
|
|
||||||
<Checkbox size="sm" value="share" color="warning">
|
|
||||||
Share
|
|
||||||
</Checkbox>
|
|
||||||
</CheckboxGroup>
|
|
||||||
<CheckboxGroup
|
|
||||||
label=""
|
|
||||||
value={analyticsView}
|
|
||||||
orientation="horizontal"
|
|
||||||
onValueChange={setAnalyticView}
|
|
||||||
className="hidden lg:block"
|
|
||||||
>
|
|
||||||
<Checkbox size="sm" value="comment">
|
|
||||||
Comment
|
|
||||||
</Checkbox>
|
|
||||||
<Checkbox size="sm" value="view" color="success">
|
|
||||||
View
|
|
||||||
</Checkbox>
|
|
||||||
<Checkbox size="sm" value="share" color="warning">
|
|
||||||
Share
|
|
||||||
</Checkbox>
|
|
||||||
</CheckboxGroup>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col lg:flex-row gap-2">
|
|
||||||
<Button
|
|
||||||
color="primary"
|
|
||||||
variant={typeDate === "monthly" ? "solid" : "bordered"}
|
|
||||||
onPress={() => setTypeDate("monthly")}
|
|
||||||
className="w-[140px] text-xs lg:text-sm h-[30px] lg:h-[40px] rounded-sm lg:rounded-lg"
|
|
||||||
>
|
|
||||||
Bulanan
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
color="primary"
|
|
||||||
onPress={() => setTypeDate("weekly")}
|
|
||||||
variant={typeDate === "weekly" ? "solid" : "bordered"}
|
|
||||||
className="w-[140px] text-xs lg:text-sm h-[30px] lg:h-[40px] rounded-sm lg:rounded-lg"
|
|
||||||
>
|
|
||||||
Mingguan
|
|
||||||
</Button>
|
|
||||||
<div className="w-[140px]">
|
|
||||||
<Popover
|
|
||||||
placement="bottom"
|
|
||||||
classNames={{ content: ["!bg-transparent", "p-0"] }}
|
|
||||||
>
|
|
||||||
<PopoverTrigger>
|
|
||||||
<Button className="w-full">
|
|
||||||
{getMonthYearName(startDateValue)}
|
|
||||||
</Button>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="bg-transparent">
|
|
||||||
<Calendar
|
|
||||||
value={startDateValue}
|
|
||||||
onChange={setStartDateValue}
|
|
||||||
/>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-row w-full h-full">
|
|
||||||
<div className="w-full h-[30vh] lg:h-full text-black">
|
|
||||||
<ApexChartColumn
|
|
||||||
type={typeDate}
|
|
||||||
date={`${startDateValue.month} ${startDateValue.year}`}
|
|
||||||
view={analyticsView}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col w-full lg:w-[45%] gap-6 shadow-md bg-white dark:bg-[#18181b] rounded-lg p-8 text-xs lg:text-sm">
|
<div className="flex flex-col w-full lg:w-[45%] gap-6 shadow-md bg-white dark:bg-[#18181b] rounded-lg p-8 text-xs lg:text-sm">
|
||||||
<div className="flex justify-between font-semibold">
|
<div className="flex justify-between font-semibold">
|
||||||
<p>Top Pages</p>
|
<p>Top Pages</p>
|
||||||
|
|
@ -817,6 +788,360 @@ export default function DashboardContainer() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="w-full flex flex-col gap-6 justify-center">
|
||||||
|
<div className="border-1 shadow-sm w-full rounded-lg p-6 flex flex-col">
|
||||||
|
<div className="flex justify-between mb-3">
|
||||||
|
<div className="font-semibold flex flex-col">
|
||||||
|
Engagement Analytics
|
||||||
|
<div className="font-normal text-xs text-gray-600 flex flex-row gap-2">
|
||||||
|
<CheckboxGroup
|
||||||
|
label=""
|
||||||
|
value={analyticsView}
|
||||||
|
orientation="vertical"
|
||||||
|
onValueChange={setAnalyticView}
|
||||||
|
className="lg:hidden"
|
||||||
|
>
|
||||||
|
<Checkbox size="sm" value="comment">
|
||||||
|
Comment
|
||||||
|
</Checkbox>
|
||||||
|
<Checkbox size="sm" value="view" color="success">
|
||||||
|
View
|
||||||
|
</Checkbox>
|
||||||
|
<Checkbox size="sm" value="share" color="warning">
|
||||||
|
Share
|
||||||
|
</Checkbox>
|
||||||
|
</CheckboxGroup>
|
||||||
|
<CheckboxGroup
|
||||||
|
label=""
|
||||||
|
value={analyticsView}
|
||||||
|
orientation="horizontal"
|
||||||
|
onValueChange={setAnalyticView}
|
||||||
|
className="hidden lg:block"
|
||||||
|
>
|
||||||
|
<Checkbox size="sm" value="comment">
|
||||||
|
Comment
|
||||||
|
</Checkbox>
|
||||||
|
<Checkbox size="sm" value="view" color="success">
|
||||||
|
View
|
||||||
|
</Checkbox>
|
||||||
|
<Checkbox size="sm" value="share" color="warning">
|
||||||
|
Share
|
||||||
|
</Checkbox>
|
||||||
|
</CheckboxGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col lg:flex-row gap-2">
|
||||||
|
<Select
|
||||||
|
className="w-full md:w-[140px] text-xs lg:text-sm h-[30px] lg:h-[40px]"
|
||||||
|
label=""
|
||||||
|
labelPlacement="outside"
|
||||||
|
selectedKeys={[typeDate]}
|
||||||
|
onChange={(e) =>
|
||||||
|
e.target.value !== "" && setTypeDate(e.target.value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectItem key="monthly">Bulanan</SelectItem>
|
||||||
|
<SelectItem key="daily">Harian</SelectItem>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
{typeDate === "monthly" ? (
|
||||||
|
<Popover
|
||||||
|
placement="bottom"
|
||||||
|
showArrow={true}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Button className="w-[140px] text-xs lg:text-sm h-[30px] lg:h-[40px] rounded-sm lg:rounded-lg">
|
||||||
|
{" "}
|
||||||
|
{selectedMonth
|
||||||
|
? format(selectedMonth, "MMMM yyyy")
|
||||||
|
: "Pilih Bulan"}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-4 w-[200px]">
|
||||||
|
<div className="flex items-center justify-between mb-2 px-1 w-full">
|
||||||
|
<button
|
||||||
|
className="text-gray-500 hover:text-black"
|
||||||
|
onClick={() => setYear((prev) => prev - 1)}
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon />
|
||||||
|
</button>
|
||||||
|
<span className="font-semibold text-center">
|
||||||
|
{year}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
className="text-gray-500 hover:text-black"
|
||||||
|
onClick={() => setYear((prev) => prev + 1)}
|
||||||
|
>
|
||||||
|
<ChevronRightIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-2 w-full">
|
||||||
|
{months.map((month, idx) => (
|
||||||
|
<button
|
||||||
|
key={idx}
|
||||||
|
onClick={() => handleMonthClick(idx)}
|
||||||
|
className={`py-1 rounded-md text-sm transition-colors ${
|
||||||
|
selectedMonth &&
|
||||||
|
selectedMonth.getMonth() === idx &&
|
||||||
|
selectedMonth.getFullYear() === year
|
||||||
|
? "bg-blue-500 text-white"
|
||||||
|
: "hover:bg-gray-200 text-gray-700"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{month}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
) : (
|
||||||
|
<div className="w-[220px]">
|
||||||
|
<DateRangePicker
|
||||||
|
className="h-[40px]"
|
||||||
|
value={viewsDailyDate}
|
||||||
|
onChange={(e) => e !== null && setViewsDailyDate(e)}
|
||||||
|
label=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row w-full h-full">
|
||||||
|
<div className="w-full h-[30vh] lg:h-full text-black">
|
||||||
|
<ApexChartColumn
|
||||||
|
type={typeDate}
|
||||||
|
date={`${
|
||||||
|
convertDateFormatNoTimeV2(String(selectedMonth)).split(
|
||||||
|
"-"
|
||||||
|
)[1]
|
||||||
|
} ${
|
||||||
|
convertDateFormatNoTimeV2(String(selectedMonth)).split(
|
||||||
|
"-"
|
||||||
|
)[0]
|
||||||
|
}`}
|
||||||
|
view={analyticsView}
|
||||||
|
range={viewsDailyDate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="border-1 shadow-sm w-full rounded-lg p-6 flex flex-col h-[700px]">
|
||||||
|
<div className="flex justify-between mb-3">
|
||||||
|
<div className="font-semibold flex flex-col">
|
||||||
|
Article Contribution Analytics
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col lg:flex-row gap-2">
|
||||||
|
<Select
|
||||||
|
className="w-full md:w-[140px] text-xs lg:text-sm h-[30px] lg:h-[40px]"
|
||||||
|
label=""
|
||||||
|
labelPlacement="outside"
|
||||||
|
selectedKeys={[typeDateUsers]}
|
||||||
|
onChange={(e) =>
|
||||||
|
e.target.value !== "" && setTypeDateUsers(e.target.value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectItem key="monthly">Bulanan</SelectItem>
|
||||||
|
<SelectItem key="daily">Harian</SelectItem>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
{typeDateUsers === "monthly" ? (
|
||||||
|
<Popover
|
||||||
|
placement="bottom"
|
||||||
|
showArrow={true}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Button className="w-[140px] text-xs lg:text-sm h-[30px] lg:h-[40px] rounded-sm lg:rounded-lg">
|
||||||
|
{" "}
|
||||||
|
{usersSelectedMonth
|
||||||
|
? format(usersSelectedMonth, "MMMM yyyy")
|
||||||
|
: "Pilih Bulan"}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-4 w-[200px]">
|
||||||
|
<div className="flex items-center justify-between mb-2 px-1 w-full">
|
||||||
|
<button
|
||||||
|
className="text-gray-500 hover:text-black"
|
||||||
|
onClick={() => setUsersYear((prev) => prev - 1)}
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon />
|
||||||
|
</button>
|
||||||
|
<span className="font-semibold text-center">
|
||||||
|
{year}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
className="text-gray-500 hover:text-black"
|
||||||
|
onClick={() => setUsersYear((prev) => prev + 1)}
|
||||||
|
>
|
||||||
|
<ChevronRightIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-2 w-full">
|
||||||
|
{months.map((month, idx) => (
|
||||||
|
<button
|
||||||
|
key={idx}
|
||||||
|
onClick={() => handleMonthClickUsers(idx)}
|
||||||
|
className={`py-1 rounded-md text-sm transition-colors ${
|
||||||
|
usersSelectedMonth &&
|
||||||
|
usersSelectedMonth.getMonth() === idx &&
|
||||||
|
usersSelectedMonth.getFullYear() === year
|
||||||
|
? "bg-blue-500 text-white"
|
||||||
|
: "hover:bg-gray-200 text-gray-700"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{month}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
) : (
|
||||||
|
<div className="w-[220px]">
|
||||||
|
<DateRangePicker
|
||||||
|
className="h-[40px]"
|
||||||
|
value={usersDailyDate}
|
||||||
|
onChange={(e) => e !== null && setUsersDailyDate(e)}
|
||||||
|
label=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row w-full h-full">
|
||||||
|
<div className="w-full h-[30vh] lg:h-full text-black">
|
||||||
|
<ApexMultiLineChart
|
||||||
|
type={typeDateUsers}
|
||||||
|
date={`${
|
||||||
|
convertDateFormatNoTimeV2(String(usersSelectedMonth)).split(
|
||||||
|
"-"
|
||||||
|
)[1]
|
||||||
|
} ${
|
||||||
|
convertDateFormatNoTimeV2(String(usersSelectedMonth)).split(
|
||||||
|
"-"
|
||||||
|
)[0]
|
||||||
|
}`}
|
||||||
|
range={usersDailyDate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{roleId && Number(roleId) < 3 && (
|
||||||
|
<div>
|
||||||
|
<IndonesiaMap />
|
||||||
|
|
||||||
|
<div className="border-1 shadow-sm w-full rounded-lg p-6 flex flex-col mt-4 h-[600px]">
|
||||||
|
<div className="flex justify-between mb-3">
|
||||||
|
<div className="font-semibold flex flex-col">
|
||||||
|
Visitor Analytics
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col lg:flex-row gap-2">
|
||||||
|
<Select
|
||||||
|
className="w-full md:w-[140px] text-xs lg:text-sm h-[30px] lg:h-[40px]"
|
||||||
|
label=""
|
||||||
|
labelPlacement="outside"
|
||||||
|
selectedKeys={[typeDateVisitor]}
|
||||||
|
onChange={(e) =>
|
||||||
|
e.target.value !== "" &&
|
||||||
|
setTypeDateVisitor(e.target.value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectItem key="monthly">Bulanan</SelectItem>
|
||||||
|
<SelectItem key="daily">Harian</SelectItem>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
{typeDateVisitor === "monthly" ? (
|
||||||
|
<Popover
|
||||||
|
placement="bottom"
|
||||||
|
showArrow={true}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Button className="w-[140px] text-xs lg:text-sm h-[30px] lg:h-[40px] rounded-sm lg:rounded-lg">
|
||||||
|
{" "}
|
||||||
|
{visitorSelectedMonth
|
||||||
|
? format(visitorSelectedMonth, "MMMM yyyy")
|
||||||
|
: "Pilih Bulan"}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-4 w-[200px]">
|
||||||
|
<div className="flex items-center justify-between mb-2 px-1 w-full">
|
||||||
|
<button
|
||||||
|
className="text-gray-500 hover:text-black"
|
||||||
|
onClick={() => setVisitorYear((prev) => prev - 1)}
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon />
|
||||||
|
</button>
|
||||||
|
<span className="font-semibold text-center">
|
||||||
|
{year}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
className="text-gray-500 hover:text-black"
|
||||||
|
onClick={() => setVisitorYear((prev) => prev + 1)}
|
||||||
|
>
|
||||||
|
<ChevronRightIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-2 w-full">
|
||||||
|
{months.map((month, idx) => (
|
||||||
|
<button
|
||||||
|
key={idx}
|
||||||
|
onClick={() => handleMonthClickVisitor(idx)}
|
||||||
|
className={`py-1 rounded-md text-sm transition-colors ${
|
||||||
|
visitorSelectedMonth &&
|
||||||
|
visitorSelectedMonth.getMonth() === idx &&
|
||||||
|
visitorSelectedMonth.getFullYear() === year
|
||||||
|
? "bg-blue-500 text-white"
|
||||||
|
: "hover:bg-gray-200 text-gray-700"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{month}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
) : (
|
||||||
|
<div className="w-[220px]">
|
||||||
|
<DateRangePicker
|
||||||
|
className="h-[40px]"
|
||||||
|
value={visitorDailyDate}
|
||||||
|
onChange={(e) => e !== null && setVisitorDailyDate(e)}
|
||||||
|
label=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row w-full h-full">
|
||||||
|
<div className="w-full h-[30vh] lg:h-full text-black">
|
||||||
|
<ApexChartColumnVisitors
|
||||||
|
type={typeDateVisitor}
|
||||||
|
date={`${
|
||||||
|
convertDateFormatNoTimeV2(
|
||||||
|
String(visitorSelectedMonth)
|
||||||
|
).split("-")[1]
|
||||||
|
} ${
|
||||||
|
convertDateFormatNoTimeV2(
|
||||||
|
String(visitorSelectedMonth)
|
||||||
|
).split("-")[0]
|
||||||
|
}`}
|
||||||
|
range={visitorDailyDate}
|
||||||
|
total={(data) => setChartVisitorTotal(data)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-row gap-2 mt-5 border-1 p-5 rounded-lg">
|
||||||
|
<div className="w-1/5 text-center">Chart Total</div>
|
||||||
|
<div className="w-4/5 text-center">{chartVisitorTotal}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange} size="3xl">
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange} size="3xl">
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import {
|
||||||
Autocomplete,
|
Autocomplete,
|
||||||
AutocompleteItem,
|
AutocompleteItem,
|
||||||
Calendar,
|
Calendar,
|
||||||
|
Checkbox,
|
||||||
Chip,
|
Chip,
|
||||||
ChipProps,
|
ChipProps,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
|
|
@ -50,7 +51,7 @@ import {
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@heroui/react";
|
} from "@heroui/react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Key, useCallback, useEffect, useState } from "react";
|
import { Key, useCallback, useEffect, useRef, useState } from "react";
|
||||||
import Datepicker from "react-tailwindcss-datepicker";
|
import Datepicker from "react-tailwindcss-datepicker";
|
||||||
import Swal from "sweetalert2";
|
import Swal from "sweetalert2";
|
||||||
import withReactContent from "sweetalert2-react-content";
|
import withReactContent from "sweetalert2-react-content";
|
||||||
|
|
@ -112,6 +113,8 @@ export default function ArticleTable() {
|
||||||
endDate: null,
|
endDate: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [selectedArticles, setSelectedArticles] = useState<any>(new Set([]));
|
||||||
|
|
||||||
const [articleDate, setArticleDate] = useState<{
|
const [articleDate, setArticleDate] = useState<{
|
||||||
startDate: any;
|
startDate: any;
|
||||||
endDate: any;
|
endDate: any;
|
||||||
|
|
@ -290,27 +293,66 @@ export default function ArticleTable() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selectedArticlesRef = useRef(selectedArticles);
|
||||||
|
const articlesRef = useRef(article);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
selectedArticlesRef.current = selectedArticles;
|
||||||
|
articlesRef.current = article;
|
||||||
|
}, [selectedArticles, article]);
|
||||||
|
|
||||||
|
const doBulkDelete = useCallback(() => {
|
||||||
|
console.log("issame", Array.from(selectedArticlesRef.current));
|
||||||
|
const now = Array.from(selectedArticlesRef.current);
|
||||||
|
if (now.length < 1) {
|
||||||
|
error("Pilih Article");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const isAll = now.join("") === "all";
|
||||||
|
|
||||||
|
MySwal.fire({
|
||||||
|
title: "Hapus Artikel yang Dipilih?",
|
||||||
|
icon: "warning",
|
||||||
|
showCancelButton: true,
|
||||||
|
cancelButtonColor: "#3085d6",
|
||||||
|
confirmButtonColor: "#d33",
|
||||||
|
confirmButtonText: "Hapus",
|
||||||
|
}).then((result) => {
|
||||||
|
if (result.isConfirmed) {
|
||||||
|
if (isAll) {
|
||||||
|
const temp = [];
|
||||||
|
for (const element of articlesRef.current) {
|
||||||
|
temp.push(String(element.id));
|
||||||
|
}
|
||||||
|
deleteBulkProcess(temp);
|
||||||
|
} else {
|
||||||
|
deleteBulkProcess(now as string[]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const deleteBulkProcess = async (data: string[]) => {
|
||||||
|
loading();
|
||||||
|
for (const element of data) {
|
||||||
|
const resDelete = await deleteArticle(element);
|
||||||
|
}
|
||||||
|
close();
|
||||||
|
success("Berhasil Hapus");
|
||||||
|
setPage(1);
|
||||||
|
initState();
|
||||||
|
};
|
||||||
|
|
||||||
const renderCell = useCallback(
|
const renderCell = useCallback(
|
||||||
(article: any, columnKey: Key) => {
|
(article: any, columnKey: Key) => {
|
||||||
const cellValue = article[columnKey as keyof any];
|
const cellValue = article[columnKey as keyof any];
|
||||||
|
|
||||||
switch (columnKey) {
|
switch (columnKey) {
|
||||||
case "isPublish":
|
case "isPublish":
|
||||||
return (
|
return <p>{article.isPublish ? "Publish" : "Draft"}</p>;
|
||||||
// <Chip
|
|
||||||
// className="capitalize "
|
|
||||||
// color={statusColorMap[article.status]}
|
|
||||||
// size="lg"
|
|
||||||
// variant="flat"
|
|
||||||
// >
|
|
||||||
// <div className="flex flex-row items-center gap-2 justify-center">
|
|
||||||
// {article.status}
|
|
||||||
// </div>
|
|
||||||
// </Chip>
|
|
||||||
<p>{article.isPublish ? "Publish" : "Draft"}</p>
|
|
||||||
);
|
|
||||||
case "isBanner":
|
case "isBanner":
|
||||||
return <p>{article.isBanner ? "Ya" : "Tidak"}</p>;
|
return <p>{article.isBanner ? "Ya" : "Tidak"}</p>;
|
||||||
|
|
||||||
case "createdAt":
|
case "createdAt":
|
||||||
return <p>{convertDateFormat(article.createdAt)}</p>;
|
return <p>{convertDateFormat(article.createdAt)}</p>;
|
||||||
case "category":
|
case "category":
|
||||||
|
|
@ -400,6 +442,23 @@ export default function ArticleTable() {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
|
<DropdownItem
|
||||||
|
key="deleteBulk"
|
||||||
|
onPress={doBulkDelete}
|
||||||
|
className={roleId && Number(roleId) < 3 ? "" : "hidden"}
|
||||||
|
>
|
||||||
|
{roleId && Number(roleId) < 3 && (
|
||||||
|
<>
|
||||||
|
{" "}
|
||||||
|
<DeleteIcon
|
||||||
|
color="red"
|
||||||
|
size={18}
|
||||||
|
className="inline ml-1 mr-2 mb-1"
|
||||||
|
/>
|
||||||
|
Bulk Delete
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</DropdownItem>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -409,7 +468,7 @@ export default function ArticleTable() {
|
||||||
return cellValue;
|
return cellValue;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[article, page]
|
[article, page, selectedArticles]
|
||||||
);
|
);
|
||||||
|
|
||||||
let typingTimer: NodeJS.Timeout;
|
let typingTimer: NodeJS.Timeout;
|
||||||
|
|
@ -499,30 +558,6 @@ export default function ArticleTable() {
|
||||||
placeholder="Kategori..."
|
placeholder="Kategori..."
|
||||||
name="sub-module"
|
name="sub-module"
|
||||||
options={categories}
|
options={categories}
|
||||||
// styles={{
|
|
||||||
// control: (base) => ({
|
|
||||||
// ...base,
|
|
||||||
// width: "100%",
|
|
||||||
// overflowX: "auto",
|
|
||||||
// }),
|
|
||||||
// valueContainer: (base) => ({
|
|
||||||
// ...base,
|
|
||||||
// display: "flex",
|
|
||||||
// flexWrap: "nowrap",
|
|
||||||
// overflowX: "auto",
|
|
||||||
// whiteSpace: "nowrap",
|
|
||||||
// gap: "4px",
|
|
||||||
// }),
|
|
||||||
// multiValue: (base) => ({
|
|
||||||
// ...base,
|
|
||||||
// whiteSpace: "nowrap",
|
|
||||||
// flexShrink: 0,
|
|
||||||
// }),
|
|
||||||
// multiValueLabel: (base) => ({
|
|
||||||
// ...base,
|
|
||||||
// whiteSpace: "nowrap",
|
|
||||||
// }),
|
|
||||||
// }}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{roleId && Number(roleId) < 3 && (
|
{roleId && Number(roleId) < 3 && (
|
||||||
|
|
@ -642,6 +677,9 @@ export default function ArticleTable() {
|
||||||
wrapper:
|
wrapper:
|
||||||
"min-h-[50px] bg-transpararent text-black dark:text-white ",
|
"min-h-[50px] bg-transpararent text-black dark:text-white ",
|
||||||
}}
|
}}
|
||||||
|
selectionMode={roleId && Number(roleId) < 3 ? "multiple" : "none"}
|
||||||
|
selectedKeys={selectedArticles}
|
||||||
|
onSelectionChange={setSelectedArticles}
|
||||||
>
|
>
|
||||||
<TableHeader
|
<TableHeader
|
||||||
columns={
|
columns={
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,6 @@ export const Breadcrumb = () => {
|
||||||
return capitalizedWords.join(" ");
|
return capitalizedWords.join(" ");
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("pathname : ", pathnameTransformed);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setCurrentPage(pathnameSplit[pathnameSplit.length - 1]);
|
setCurrentPage(pathnameSplit[pathnameSplit.length - 1]);
|
||||||
}, [pathnameSplit]);
|
}, [pathnameSplit]);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,312 @@
|
||||||
|
import React, { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import * as echarts from "echarts";
|
||||||
|
import indonesiaGeo from "../../public/assets/geo/indonesia.json";
|
||||||
|
import { getStatisticForMaps } from "@/services/article";
|
||||||
|
import { convertDateFormatNoTimeV2 } from "@/utils/global";
|
||||||
|
import {
|
||||||
|
parseDate,
|
||||||
|
getLocalTimeZone,
|
||||||
|
parseZonedDateTime,
|
||||||
|
parseAbsoluteToLocal,
|
||||||
|
} from "@internationalized/date";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DateRangePicker,
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
Select,
|
||||||
|
SelectItem,
|
||||||
|
} from "@heroui/react";
|
||||||
|
import { format } from "date-fns";
|
||||||
|
import { ChevronLeftIcon, ChevronRightIcon } from "../icons";
|
||||||
|
const months = [
|
||||||
|
"Jan",
|
||||||
|
"Feb",
|
||||||
|
"Mar",
|
||||||
|
"Apr",
|
||||||
|
"May",
|
||||||
|
"Jun",
|
||||||
|
"Jul",
|
||||||
|
"Aug",
|
||||||
|
"Sep",
|
||||||
|
"Oct",
|
||||||
|
"Nov",
|
||||||
|
"Dec",
|
||||||
|
];
|
||||||
|
|
||||||
|
const IndonesiaMap = () => {
|
||||||
|
const chartRef = useRef<HTMLDivElement>(null);
|
||||||
|
const myChart = useRef<echarts.EChartsType | null>(null);
|
||||||
|
const [selectedProvince, setSelectedProvince] = useState("");
|
||||||
|
|
||||||
|
const selectedProvinceRef = useRef<string | null>(null);
|
||||||
|
const chartOptionRef = useRef<any>(null);
|
||||||
|
const today = new Date();
|
||||||
|
|
||||||
|
const [typeDate, setTypeDate] = useState("daily");
|
||||||
|
const [year, setYear] = useState(today.getFullYear());
|
||||||
|
const [selectedMonth, setSelectedMonth] = useState<Date | null>(today);
|
||||||
|
const [viewsDailyDate, setViewsDailyDate] = useState({
|
||||||
|
start: parseDate(
|
||||||
|
convertDateFormatNoTimeV2(
|
||||||
|
new Date(new Date().setDate(new Date().getDate() - 7))
|
||||||
|
)
|
||||||
|
),
|
||||||
|
end: parseDate(convertDateFormatNoTimeV2(today)),
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleMonthClick = (monthIndex: number) => {
|
||||||
|
setSelectedMonth(new Date(year, monthIndex, 1));
|
||||||
|
};
|
||||||
|
const [minMax, setMinMax] = useState({ min: 0, max: 0 });
|
||||||
|
const [data, setData] = useState<{ name: string; value: number }[]>([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetchData();
|
||||||
|
}, [viewsDailyDate]);
|
||||||
|
|
||||||
|
const fetchData = async () => {
|
||||||
|
const getDate = (data: any) => {
|
||||||
|
return `${data.year}-${data.month < 10 ? `0${data.month}` : data.month}-${
|
||||||
|
data.day < 10 ? `0${data.day}` : data.day
|
||||||
|
}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await getStatisticForMaps(
|
||||||
|
getDate(viewsDailyDate.start),
|
||||||
|
getDate(viewsDailyDate.end)
|
||||||
|
);
|
||||||
|
if (res?.data?.data) {
|
||||||
|
const temp = [];
|
||||||
|
let min = Infinity;
|
||||||
|
let max = -Infinity;
|
||||||
|
for (const element of res?.data?.data) {
|
||||||
|
const value = element.totalVisitor;
|
||||||
|
|
||||||
|
const now = {
|
||||||
|
name: element.regionName.toUpperCase(),
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (value < min) min = value;
|
||||||
|
if (value > max) max = value;
|
||||||
|
temp.push(now);
|
||||||
|
}
|
||||||
|
setData(temp);
|
||||||
|
setMinMax({ min: min, max: max });
|
||||||
|
} else {
|
||||||
|
setData([]);
|
||||||
|
setMinMax({ min: 0, max: 0 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const option = useMemo(
|
||||||
|
() => ({
|
||||||
|
backgroundColor: "#d3d3d3",
|
||||||
|
title: {
|
||||||
|
text: "Data Distribution",
|
||||||
|
left: "center",
|
||||||
|
textStyle: { color: "#000" },
|
||||||
|
},
|
||||||
|
visualMap: {
|
||||||
|
min: minMax.min,
|
||||||
|
max: minMax.max,
|
||||||
|
show: true,
|
||||||
|
seriesIndex: 0,
|
||||||
|
inRange: {
|
||||||
|
color: ["#ff0000", "#ffffff"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
type: "map",
|
||||||
|
map: "indonesia",
|
||||||
|
roam: true,
|
||||||
|
zoom: 1.2,
|
||||||
|
center: [118, -2],
|
||||||
|
label: {
|
||||||
|
show: false,
|
||||||
|
color: "#000",
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
borderColor: "#999",
|
||||||
|
},
|
||||||
|
emphasis: {
|
||||||
|
label: {
|
||||||
|
show: true,
|
||||||
|
color: "#000",
|
||||||
|
},
|
||||||
|
itemStyle: {
|
||||||
|
areaColor: "#ffcccc",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data: data,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
[data, minMax]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!chartRef.current) return;
|
||||||
|
(window as any).echarts = echarts;
|
||||||
|
|
||||||
|
const fixedGeo = {
|
||||||
|
...indonesiaGeo,
|
||||||
|
features: indonesiaGeo.features.map((f) => ({
|
||||||
|
...f,
|
||||||
|
properties: {
|
||||||
|
...f.properties,
|
||||||
|
name: f.properties.PROVINSI,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
echarts.registerMap("indonesia", fixedGeo as any);
|
||||||
|
|
||||||
|
const chart = echarts.init(chartRef.current);
|
||||||
|
myChart.current = chart;
|
||||||
|
|
||||||
|
chart.on("click", (params: any) => {
|
||||||
|
highlightProvince(params.name);
|
||||||
|
});
|
||||||
|
chartOptionRef.current = option;
|
||||||
|
chart.setOption(option);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
chart.dispose();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!myChart.current) return;
|
||||||
|
|
||||||
|
chartOptionRef.current = option;
|
||||||
|
myChart.current.setOption(option);
|
||||||
|
}, [option]);
|
||||||
|
|
||||||
|
const highlightProvince = (name: string) => {
|
||||||
|
if (!myChart.current || !chartOptionRef.current) return;
|
||||||
|
|
||||||
|
myChart.current.clear();
|
||||||
|
myChart.current.setOption(chartOptionRef.current);
|
||||||
|
|
||||||
|
myChart.current.dispatchAction({
|
||||||
|
type: "highlight",
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
|
||||||
|
myChart.current.dispatchAction({
|
||||||
|
type: "showTip",
|
||||||
|
name,
|
||||||
|
});
|
||||||
|
|
||||||
|
selectedProvinceRef.current = name;
|
||||||
|
setSelectedProvince(name);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col lg:flex-row gap-6 w-full pr-2 md:pr-0">
|
||||||
|
<div className="w-screen md:w-[70%] h-[360px] lg:h-[600px]">
|
||||||
|
<div ref={chartRef} style={{ height: "100%", width: "100%" }} />
|
||||||
|
</div>
|
||||||
|
<div className="w-screen md:w-[30%] h-[360px] lg:h-[600px] overflow-auto flex flex-col">
|
||||||
|
<div className="flex flex-col lg:flex-row gap-2">
|
||||||
|
{/* <Select
|
||||||
|
className="w-full md:w-[140px] text-xs lg:text-sm h-[30px] lg:h-[40px]"
|
||||||
|
label=""
|
||||||
|
labelPlacement="outside"
|
||||||
|
selectedKeys={[typeDate]}
|
||||||
|
onChange={(e) =>
|
||||||
|
e.target.value !== "" && setTypeDate(e.target.value)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectItem key="monthly">Bulanan</SelectItem>
|
||||||
|
<SelectItem key="daily">Harian</SelectItem>
|
||||||
|
</Select> */}
|
||||||
|
|
||||||
|
{typeDate === "monthly" ? (
|
||||||
|
<Popover placement="bottom" showArrow={true} className="w-full">
|
||||||
|
<PopoverTrigger>
|
||||||
|
<Button className="w-[140px] text-xs lg:text-sm h-[30px] lg:h-[40px] rounded-sm lg:rounded-lg">
|
||||||
|
{" "}
|
||||||
|
{selectedMonth
|
||||||
|
? format(selectedMonth, "MMMM yyyy")
|
||||||
|
: "Pilih Bulan"}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-4 w-[200px]">
|
||||||
|
<div className="flex items-center justify-between mb-2 px-1 w-full">
|
||||||
|
<button
|
||||||
|
className="text-gray-500 hover:text-black"
|
||||||
|
onClick={() => setYear((prev) => prev - 1)}
|
||||||
|
>
|
||||||
|
<ChevronLeftIcon />
|
||||||
|
</button>
|
||||||
|
<span className="font-semibold text-center">{year}</span>
|
||||||
|
<button
|
||||||
|
className="text-gray-500 hover:text-black"
|
||||||
|
onClick={() => setYear((prev) => prev + 1)}
|
||||||
|
>
|
||||||
|
<ChevronRightIcon />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-3 gap-2 w-full">
|
||||||
|
{months.map((month, idx) => (
|
||||||
|
<button
|
||||||
|
key={idx}
|
||||||
|
onClick={() => handleMonthClick(idx)}
|
||||||
|
className={`py-1 rounded-md text-sm transition-colors ${
|
||||||
|
selectedMonth &&
|
||||||
|
selectedMonth.getMonth() === idx &&
|
||||||
|
selectedMonth.getFullYear() === year
|
||||||
|
? "bg-blue-500 text-white"
|
||||||
|
: "hover:bg-gray-200 text-gray-700"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{month}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
) : (
|
||||||
|
<div className="w-full mb-3">
|
||||||
|
<DateRangePicker
|
||||||
|
className="h-[40px] self-end"
|
||||||
|
value={viewsDailyDate}
|
||||||
|
onChange={(e) => e !== null && setViewsDailyDate(e)}
|
||||||
|
label=""
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{data?.length > 0 &&
|
||||||
|
data?.map((list: any) => (
|
||||||
|
<a
|
||||||
|
key={list.name}
|
||||||
|
onClick={() => highlightProvince(list.name)}
|
||||||
|
className={`w-full flex flex-row cursor-pointer gap-2 ${
|
||||||
|
selectedProvince === list.name ? "bg-slate-300" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<p className="w-4/5">{list.name}</p>{" "}
|
||||||
|
<p
|
||||||
|
className={`w-1/5 ${
|
||||||
|
selectedProvince === list.name
|
||||||
|
? "bg-slate-300"
|
||||||
|
: "bg-[#a9d7e4]"
|
||||||
|
} text-black border-white text-center`}
|
||||||
|
>
|
||||||
|
{list.value}
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IndonesiaMap;
|
||||||
164
const/dummy.json
164
const/dummy.json
|
|
@ -1,46 +1,148 @@
|
||||||
{
|
{
|
||||||
"data": [
|
"data": [
|
||||||
{
|
{
|
||||||
"id": 1,
|
|
||||||
"year": 2024,
|
|
||||||
"month": 11,
|
|
||||||
"suggestions": [
|
|
||||||
14, 32, 10, 21, 15, 18, 24, 30, 12, 25, 19, 28, 14, 17, 22, 31, 27, 13,
|
|
||||||
20, 24, 29, 18, 21, 26, 23, 14, 19, 17, 28, 22
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 2,
|
|
||||||
"year": 2024,
|
|
||||||
"month": 12,
|
|
||||||
"suggestions": [
|
|
||||||
15, 23, 19, 14, 18, 20, 22, 17, 21, 19, 23, 16, 25, 20, 18, 19, 22, 24,
|
|
||||||
15, 18, 21, 26, 28, 23, 17, 20, 19, 22, 22, 42, 32
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"id": 3,
|
|
||||||
"year": 2025,
|
"year": 2025,
|
||||||
"month": 1,
|
"month": 4,
|
||||||
"suggestions": [
|
"users": [
|
||||||
14, 32, 10, 21, 15, 18, 24, 30, 12, 25, 19, 28, 14, 17, 22, 31, 27, 13,
|
{
|
||||||
20, 24, 29, 18, 21, 26, 23, 14, 19, 17, 28, 22, 21
|
"name": "Polda Aceh",
|
||||||
|
"data": [
|
||||||
|
12, 7, 5, 19, 3, 8, 15, 4, 9, 6, 14, 11, 13, 10, 2, 18, 7, 5, 8, 17,
|
||||||
|
4, 6, 14, 15, 3, 10, 12, 9, 6, 13
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Polda Sumatera Utara",
|
||||||
|
"data": [
|
||||||
|
10, 4, 6, 7, 9, 8, 14, 13, 5, 12, 11, 3, 17, 16, 7, 9, 10, 8, 5, 14,
|
||||||
|
11, 4, 6, 9, 15, 13, 3, 7, 10, 12
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Polda Sumatera Barat",
|
||||||
|
"data": [
|
||||||
|
7, 6, 3, 8, 12, 4, 6, 10, 11, 14, 13, 5, 9, 10, 6, 7, 8, 13, 15, 6,
|
||||||
|
7, 5, 8, 14, 11, 13, 4, 6, 12, 10
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Polda Metro Jaya",
|
||||||
|
"data": [
|
||||||
|
12, 7, 5, 19, 3, 8, 15, 4, 9, 6, 14, 11, 13, 10, 2, 18, 7, 5, 8, 17,
|
||||||
|
4, 6, 9, 6, 13, 5, 14, 15, 3, 10
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Polda Jabar",
|
||||||
|
"data": [
|
||||||
|
10, 4, 6, 17, 16, 7, 9, 10, 8, 5, 14, 11, 4, 6, 9, 15, 13, 3, 7, 10,
|
||||||
|
12, 9, 8, 14, 13, 21, 5, 12, 11
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Polda Jateng",
|
||||||
|
"data": [
|
||||||
|
7, 6, 3, 13, 5, 9, 10, 6, 7, 8, 13, 15, 6, 7, 5, 8, 14, 11, 13, 4,
|
||||||
|
4, 6, 10, 11, 14, 2, 6, 12, 10, 8
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 4,
|
|
||||||
"year": 2025,
|
"year": 2025,
|
||||||
"month": 2,
|
"month": 5,
|
||||||
"suggestions": [
|
"users": [
|
||||||
15, 23, 19, 14, 18, 20, 22, 17, 21, 19, 23, 16, 25, 20, 18, 19, 22, 24,
|
{
|
||||||
15, 18, 21, 26, 28, 23, 17, 20, 19, 22
|
"name": "Polda Aceh",
|
||||||
|
"data": [
|
||||||
|
12, 7, 5, 19, 3, 8, 15, 4, 9, 6, 14, 11, 13, 10, 2, 18, 7, 5, 8, 17,
|
||||||
|
4, 6, 14, 15, 3, 10, 12, 9, 6, 13, 5
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Polda Sumatera Utara",
|
||||||
|
"data": [
|
||||||
|
10, 4, 6, 7, 5, 12, 11, 3, 17, 16, 7, 9, 10, 8, 5, 14, 11, 4, 6, 9,
|
||||||
|
15, 13, 3, 7, 10, 12, 9, 8, 14, 13, 4
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Polda Sumatera Barat",
|
||||||
|
"data": [
|
||||||
|
7, 6, 3, 13, 5, 9, 10, 6, 7, 8, 13, 15, 6, 7, 5, 8, 14, 11, 13, 4,
|
||||||
|
6, 12, 10, 8, 12, 4, 6, 10, 11, 14, 2
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Polda Metro Jaya",
|
||||||
|
"data": [
|
||||||
|
12, 7, 5, 19, 3, 8, 15, 4, 9, 6, 14, 11, 13, 10, 2, 18, 7, 5, 8, 17,
|
||||||
|
4, 6, 9, 6, 13, 5, 14, 15, 3, 10, 12
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Polda Jabar",
|
||||||
|
"data": [
|
||||||
|
10, 4, 6, 17, 16, 7, 9, 10, 8, 5, 14, 11, 4, 6, 9, 15, 13, 3, 7, 10,
|
||||||
|
12, 9, 8, 14, 13, 2, 5, 12, 11, 3, 2
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Polda Jateng",
|
||||||
|
"data": [
|
||||||
|
7, 6, 3, 13, 5, 9, 10, 6, 7, 8, 13, 15, 6, 7, 5, 8, 14, 11, 13, 4,
|
||||||
|
4, 6, 10, 11, 14, 2, 6, 12, 10, 8, 12
|
||||||
|
]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": 5,
|
|
||||||
"year": 2025,
|
"year": 2025,
|
||||||
"month": 3,
|
"month": 6,
|
||||||
"suggestions": [14, 32, 10, 21, 15, 18]
|
"users": [
|
||||||
|
{
|
||||||
|
"name": "Polda Aceh",
|
||||||
|
"data": [
|
||||||
|
12, 7, 5, 19, 3, 8, 15, 4, 9, 6, 14, 11, 13, 10, 2, 18, 7, 5, 8, 17,
|
||||||
|
4, 6, 14, 15, 3, 10, 12, 9, 6, 13, 5
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Polda Sumatera Utara",
|
||||||
|
"data": [
|
||||||
|
10, 4, 6, 7, 5, 12, 11, 3, 17, 16, 7, 9, 10, 8, 5, 14, 11, 4, 6, 9,
|
||||||
|
15, 13, 3, 7, 10, 12, 9, 8, 14, 13, 4
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Polda Sumatera Barat",
|
||||||
|
"data": [
|
||||||
|
7, 6, 3, 13, 5, 9, 10, 6, 7, 8, 13, 15, 6, 7, 5, 8, 14, 11, 13, 4,
|
||||||
|
6, 12, 10, 8, 12, 4, 6, 10, 11, 14, 2
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Polda Metro Jaya",
|
||||||
|
"data": [
|
||||||
|
12, 7, 5, 19, 3, 8, 15, 4, 9, 6, 14, 11, 13, 10, 2, 18, 7, 5, 8, 17,
|
||||||
|
4, 6, 9, 6, 13, 5, 14, 15, 3, 10, 12
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Polda Jabar",
|
||||||
|
"data": [
|
||||||
|
10, 4, 6, 17, 16, 7, 9, 10, 8, 5, 14, 11, 4, 6, 9, 15, 13, 3, 7, 10,
|
||||||
|
12, 9, 8, 14, 13, 20, 5, 12, 11, 3
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Polda Jateng",
|
||||||
|
"data": [
|
||||||
|
7, 6, 3, 13, 5, 9, 10, 6, 7, 8, 13, 15, 6, 7, 5, 8, 14, 11, 13, 4,
|
||||||
|
4, 6, 10, 11, 14, 2, 6, 12, 10, 8, 12
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,11 +26,13 @@
|
||||||
"@react-aria/ssr": "^3.8.0",
|
"@react-aria/ssr": "^3.8.0",
|
||||||
"@react-aria/visually-hidden": "^3.8.6",
|
"@react-aria/visually-hidden": "^3.8.6",
|
||||||
"@tawk.to/tawk-messenger-react": "^2.0.2",
|
"@tawk.to/tawk-messenger-react": "^2.0.2",
|
||||||
|
"@types/echarts": "^5.0.0",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/node": "20.5.7",
|
"@types/node": "^20.5.7",
|
||||||
"@types/react": "19.1.2",
|
"@types/react": "19.1.2",
|
||||||
"@types/react-datepicker": "^6.0.1",
|
"@types/react-datepicker": "^6.0.1",
|
||||||
"@types/react-dom": "19.1.2",
|
"@types/react-dom": "19.1.2",
|
||||||
|
"@types/topojson-client": "^3.1.5",
|
||||||
"apexcharts": "^3.48.0",
|
"apexcharts": "^3.48.0",
|
||||||
"autoprefixer": "10.4.16",
|
"autoprefixer": "10.4.16",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.6.8",
|
||||||
|
|
@ -39,6 +41,8 @@
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"docx": "^9.3.0",
|
"docx": "^9.3.0",
|
||||||
"dompurify": "^3.2.0",
|
"dompurify": "^3.2.0",
|
||||||
|
"echarts": "^5.6.0",
|
||||||
|
"echarts-gl": "^2.0.9",
|
||||||
"eslint": "8.48.0",
|
"eslint": "8.48.0",
|
||||||
"eslint-config-next": "15.3.0",
|
"eslint-config-next": "15.3.0",
|
||||||
"framer-motion": "^10.18.0",
|
"framer-motion": "^10.18.0",
|
||||||
|
|
@ -68,6 +72,7 @@
|
||||||
"swiper": "^11.0.6",
|
"swiper": "^11.0.6",
|
||||||
"tailwind-variants": "^0.1.18",
|
"tailwind-variants": "^0.1.18",
|
||||||
"tailwindcss": "3.3.5",
|
"tailwindcss": "3.3.5",
|
||||||
|
"topojson-client": "^3.1.0",
|
||||||
"typescript": "5.0.4",
|
"typescript": "5.0.4",
|
||||||
"xlsx": "^0.18.5",
|
"xlsx": "^0.18.5",
|
||||||
"zod": "^3.23.8",
|
"zod": "^3.23.8",
|
||||||
|
|
@ -9397,6 +9402,15 @@
|
||||||
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.5.tgz",
|
||||||
"integrity": "sha512-j2K5UJqGTxeesj6oQuGpMgifpT5k9HprgQd8D1Y0lOFqKHl3PJu5GMeS4Y5EgjS55AE6OQxf8mPED9uaGbf4Cg=="
|
"integrity": "sha512-j2K5UJqGTxeesj6oQuGpMgifpT5k9HprgQd8D1Y0lOFqKHl3PJu5GMeS4Y5EgjS55AE6OQxf8mPED9uaGbf4Cg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/echarts": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/echarts/-/echarts-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-5uc/16BlYpzH8kU/u8aeRRgY2FV6yRY7RjPnYfUFPowl0F3kvNgfaz09PmeVdLkqdAtMft3XkCfqiJPJjG2DNQ==",
|
||||||
|
"deprecated": "This is a stub types definition. echarts provides its own type definitions, so you do not need this installed.",
|
||||||
|
"dependencies": {
|
||||||
|
"echarts": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/eslint": {
|
"node_modules/@types/eslint": {
|
||||||
"version": "9.6.1",
|
"version": "9.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz",
|
||||||
|
|
@ -9423,6 +9437,11 @@
|
||||||
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
|
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/geojson": {
|
||||||
|
"version": "7946.0.16",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
|
||||||
|
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="
|
||||||
|
},
|
||||||
"node_modules/@types/glob": {
|
"node_modules/@types/glob": {
|
||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz",
|
||||||
|
|
@ -9522,6 +9541,23 @@
|
||||||
"@types/react": "*"
|
"@types/react": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/topojson-client": {
|
||||||
|
"version": "3.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/topojson-client/-/topojson-client-3.1.5.tgz",
|
||||||
|
"integrity": "sha512-C79rySTyPxnQNNguTZNI1Ct4D7IXgvyAs3p9HPecnl6mNrJ5+UhvGNYcZfpROYV2lMHI48kJPxwR+F9C6c7nmw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/geojson": "*",
|
||||||
|
"@types/topojson-specification": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/topojson-specification": {
|
||||||
|
"version": "1.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/topojson-specification/-/topojson-specification-1.0.5.tgz",
|
||||||
|
"integrity": "sha512-C7KvcQh+C2nr6Y2Ub4YfgvWvWCgP2nOQMtfhlnwsRL4pYmmwzBS7HclGiS87eQfDOU/DLQpX6GEscviaz4yLIQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/geojson": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/trusted-types": {
|
"node_modules/@types/trusted-types": {
|
||||||
"version": "2.0.7",
|
"version": "2.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
|
|
@ -10903,6 +10939,11 @@
|
||||||
"resolved": "vendor/ckeditor5",
|
"resolved": "vendor/ckeditor5",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/claygl": {
|
||||||
|
"version": "1.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/claygl/-/claygl-1.3.0.tgz",
|
||||||
|
"integrity": "sha512-+gGtJjT6SSHD2l2yC3MCubW/sCV40tZuSs5opdtn79vFSGUgp/lH139RNEQ6Jy078/L0aV8odCw8RSrUcMfLaQ=="
|
||||||
|
},
|
||||||
"node_modules/clean-stack": {
|
"node_modules/clean-stack": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz",
|
||||||
|
|
@ -11695,6 +11736,32 @@
|
||||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
|
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="
|
||||||
},
|
},
|
||||||
|
"node_modules/echarts": {
|
||||||
|
"version": "5.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.6.0.tgz",
|
||||||
|
"integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "2.3.0",
|
||||||
|
"zrender": "5.6.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/echarts-gl": {
|
||||||
|
"version": "2.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/echarts-gl/-/echarts-gl-2.0.9.tgz",
|
||||||
|
"integrity": "sha512-oKeMdkkkpJGWOzjgZUsF41DOh6cMsyrGGXimbjK2l6Xeq/dBQu4ShG2w2Dzrs/1bD27b2pLTGSaUzouY191gzA==",
|
||||||
|
"dependencies": {
|
||||||
|
"claygl": "^1.2.1",
|
||||||
|
"zrender": "^5.1.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"echarts": "^5.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/echarts/node_modules/tslib": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
|
||||||
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.140",
|
"version": "1.5.140",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.140.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.140.tgz",
|
||||||
|
|
@ -17563,6 +17630,24 @@
|
||||||
"node": ">=8.0"
|
"node": ">=8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/topojson-client": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/topojson-client/-/topojson-client-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==",
|
||||||
|
"dependencies": {
|
||||||
|
"commander": "2"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"topo2geo": "bin/topo2geo",
|
||||||
|
"topomerge": "bin/topomerge",
|
||||||
|
"topoquantize": "bin/topoquantize"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/topojson-client/node_modules/commander": {
|
||||||
|
"version": "2.20.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||||
|
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="
|
||||||
|
},
|
||||||
"node_modules/ts-api-utils": {
|
"node_modules/ts-api-utils": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
|
||||||
|
|
@ -18599,6 +18684,19 @@
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/zrender": {
|
||||||
|
"version": "5.6.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.1.tgz",
|
||||||
|
"integrity": "sha512-OFXkDJKcrlx5su2XbzJvj/34Q3m6PvyCZkVPHGYpcCJ52ek4U/ymZyfuV1nKE23AyBJ51E/6Yr0mhZ7xGTO4ag==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "2.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zrender/node_modules/tslib": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
|
||||||
|
},
|
||||||
"node_modules/zustand": {
|
"node_modules/zustand": {
|
||||||
"version": "5.0.3",
|
"version": "5.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz",
|
||||||
|
|
|
||||||
|
|
@ -27,11 +27,13 @@
|
||||||
"@react-aria/ssr": "^3.8.0",
|
"@react-aria/ssr": "^3.8.0",
|
||||||
"@react-aria/visually-hidden": "^3.8.6",
|
"@react-aria/visually-hidden": "^3.8.6",
|
||||||
"@tawk.to/tawk-messenger-react": "^2.0.2",
|
"@tawk.to/tawk-messenger-react": "^2.0.2",
|
||||||
|
"@types/echarts": "^5.0.0",
|
||||||
"@types/js-cookie": "^3.0.6",
|
"@types/js-cookie": "^3.0.6",
|
||||||
"@types/node": "20.5.7",
|
"@types/node": "^20.5.7",
|
||||||
"@types/react": "19.1.2",
|
"@types/react": "19.1.2",
|
||||||
"@types/react-datepicker": "^6.0.1",
|
"@types/react-datepicker": "^6.0.1",
|
||||||
"@types/react-dom": "19.1.2",
|
"@types/react-dom": "19.1.2",
|
||||||
|
"@types/topojson-client": "^3.1.5",
|
||||||
"apexcharts": "^3.48.0",
|
"apexcharts": "^3.48.0",
|
||||||
"autoprefixer": "10.4.16",
|
"autoprefixer": "10.4.16",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.6.8",
|
||||||
|
|
@ -40,6 +42,8 @@
|
||||||
"dayjs": "^1.11.13",
|
"dayjs": "^1.11.13",
|
||||||
"docx": "^9.3.0",
|
"docx": "^9.3.0",
|
||||||
"dompurify": "^3.2.0",
|
"dompurify": "^3.2.0",
|
||||||
|
"echarts": "^5.6.0",
|
||||||
|
"echarts-gl": "^2.0.9",
|
||||||
"eslint": "8.48.0",
|
"eslint": "8.48.0",
|
||||||
"eslint-config-next": "15.3.0",
|
"eslint-config-next": "15.3.0",
|
||||||
"framer-motion": "^10.18.0",
|
"framer-motion": "^10.18.0",
|
||||||
|
|
@ -69,6 +73,7 @@
|
||||||
"swiper": "^11.0.6",
|
"swiper": "^11.0.6",
|
||||||
"tailwind-variants": "^0.1.18",
|
"tailwind-variants": "^0.1.18",
|
||||||
"tailwindcss": "3.3.5",
|
"tailwindcss": "3.3.5",
|
||||||
|
"topojson-client": "^3.1.0",
|
||||||
"typescript": "5.0.4",
|
"typescript": "5.0.4",
|
||||||
"xlsx": "^0.18.5",
|
"xlsx": "^0.18.5",
|
||||||
"zod": "^3.23.8",
|
"zod": "^3.23.8",
|
||||||
|
|
|
||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -215,6 +215,17 @@ export async function getUserLevelDataStat(
|
||||||
}&timeStamp=${timeStamp}`
|
}&timeStamp=${timeStamp}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getStatisticForMaps(startDate: string, endDate: string) {
|
||||||
|
const headers = {
|
||||||
|
"content-type": "application/json",
|
||||||
|
// Authorization: `Bearer ${token}`,
|
||||||
|
};
|
||||||
|
return await httpGet(
|
||||||
|
`/activity-logs/visitors-by-region-stats?startDate=${startDate}&endDate=${endDate}`,
|
||||||
|
headers
|
||||||
|
);
|
||||||
|
}
|
||||||
export async function getStatisticMonthly(year: string, timeStamp: number) {
|
export async function getStatisticMonthly(year: string, timeStamp: number) {
|
||||||
const headers = {
|
const headers = {
|
||||||
"content-type": "application/json",
|
"content-type": "application/json",
|
||||||
|
|
@ -224,6 +235,26 @@ export async function getStatisticMonthly(year: string, timeStamp: number) {
|
||||||
`/articles/statistic/monthly?year=${year}&timeStamp=${timeStamp}`
|
`/articles/statistic/monthly?year=${year}&timeStamp=${timeStamp}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
export async function getStatisticVisitorsMonthly(year: string) {
|
||||||
|
const headers = {
|
||||||
|
"content-type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
};
|
||||||
|
return await httpGet(
|
||||||
|
`/activity-logs/visitors-monthly-stats?year=${year}`,
|
||||||
|
headers
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export async function getStatisticUsersMonthly(year: string) {
|
||||||
|
const headers = {
|
||||||
|
"content-type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
};
|
||||||
|
return await httpGet(
|
||||||
|
`/articles/statistic/monthly-per-user-level?year=${year}`,
|
||||||
|
headers
|
||||||
|
);
|
||||||
|
}
|
||||||
export async function getStatisticMonthlyFeedback(
|
export async function getStatisticMonthlyFeedback(
|
||||||
year: string,
|
year: string,
|
||||||
timeStamp: number
|
timeStamp: number
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue