feat:user analytic

This commit is contained in:
Rama Priyanto 2025-06-25 17:39:34 +07:00
parent cc45c2dabb
commit 686d29439c
5 changed files with 594 additions and 123 deletions

View File

@ -69,98 +69,98 @@ export default function Login() {
error("Username & Password Wajib Diisi !");
} else {
// login dengan otp
loading();
const response: any = await emailValidation(data);
if (response?.error) {
console.log("error", response);
if (response?.message?.messages[0]?.includes("failed to send mail")) {
error("Gagal Mengirim OTP");
return false;
}
// loading();
// const response: any = await emailValidation(data);
// if (response?.error) {
// console.log("error", response);
// if (response?.message?.messages[0]?.includes("failed to send mail")) {
// error("Gagal Mengirim OTP");
// return false;
// }
if (response?.message?.messages[0]?.includes("username")) {
error("Username / Password Tidak Sesuai");
return false;
}
error("Unknown Error");
return false;
}
close();
if (response?.data?.messages[0] === "Continue to setup email") {
setFirstLogin(true);
} else {
setNeedOtp(true);
}
// if (response?.message?.messages[0]?.includes("username")) {
// error("Username / Password Tidak Sesuai");
// return false;
// }
// error("Unknown Error");
// return false;
// }
// close();
// if (response?.data?.messages[0] === "Continue to setup email") {
// setFirstLogin(true);
// } else {
// setNeedOtp(true);
// }
// login tanpa otp
// loading();
// const response = await postSignIn(data);
// if (response?.error) {
// error("Username / Password Tidak Sesuai");
// } else {
// const profile = await getProfile(response?.data?.data?.access_token);
// const dateTime: any = new Date();
loading();
const response = await postSignIn(data);
if (response?.error) {
error("Username / Password Tidak Sesuai");
} else {
const profile = await getProfile(response?.data?.data?.access_token);
const dateTime: any = new Date();
// const newTime: any = dateTime.getTime() + 10 * 60 * 1000;
const newTime: any = dateTime.getTime() + 10 * 60 * 1000;
// Cookies.set("access_token", response?.data?.data?.access_token, {
// expires: 1,
// });
// Cookies.set("refresh_token", response?.data?.data?.refresh_token, {
// expires: 1,
// });
// Cookies.set("time_refresh", newTime, {
// expires: 1,
// });
// Cookies.set("is_first_login", "true", {
// secure: true,
// sameSite: "strict",
// });
// const resActivity = await saveActivity(
// {
// activityTypeId: 1,
// url: "https://kontenhumas.com/auth",
// userId: profile?.data?.data?.id,
// },
// accessData?.id_token
// );
// Cookies.set("profile_picture", profile?.data?.data?.profilePictureUrl, {
// expires: 1,
// });
// Cookies.set("uie", profile?.data?.data?.id, {
// expires: 1,
// });
// Cookies.set("ufne", profile?.data?.data?.fullname, {
// expires: 1,
// });
// Cookies.set("ulie", profile?.data?.data?.userLevelGroup, {
// expires: 1,
// });
// Cookies.set("username", profile?.data?.data?.username, {
// expires: 1,
// });
// Cookies.set("urie", profile?.data?.data?.userRoleId, {
// expires: 1,
// });
// Cookies.set("masterPoldaId", profile?.data?.data?.masterPoldaId, {
// expires: 1,
// });
// Cookies.set("ulne", profile?.data?.data?.userLevelId, {
// expires: 1,
// });
// // Cookies.set("urce", profile?.data?.data?.roleCode, {
// // expires: 1,
// // });
// Cookies.set("email", profile?.data?.data?.email, {
// expires: 1,
// });
// router.push("/admin/dashboard");
// Cookies.set("status", "login", {
// expires: 1,
// });
Cookies.set("access_token", response?.data?.data?.access_token, {
expires: 1,
});
Cookies.set("refresh_token", response?.data?.data?.refresh_token, {
expires: 1,
});
Cookies.set("time_refresh", newTime, {
expires: 1,
});
Cookies.set("is_first_login", "true", {
secure: true,
sameSite: "strict",
});
const resActivity = await saveActivity(
{
activityTypeId: 1,
url: "https://kontenhumas.com/auth",
userId: profile?.data?.data?.id,
},
accessData?.id_token
);
Cookies.set("profile_picture", profile?.data?.data?.profilePictureUrl, {
expires: 1,
});
Cookies.set("uie", profile?.data?.data?.id, {
expires: 1,
});
Cookies.set("ufne", profile?.data?.data?.fullname, {
expires: 1,
});
Cookies.set("ulie", profile?.data?.data?.userLevelGroup, {
expires: 1,
});
Cookies.set("username", profile?.data?.data?.username, {
expires: 1,
});
Cookies.set("urie", profile?.data?.data?.userRoleId, {
expires: 1,
});
Cookies.set("masterPoldaId", profile?.data?.data?.masterPoldaId, {
expires: 1,
});
Cookies.set("ulne", profile?.data?.data?.userLevelId, {
expires: 1,
});
// Cookies.set("urce", profile?.data?.data?.roleCode, {
// expires: 1,
// });
Cookies.set("email", profile?.data?.data?.email, {
expires: 1,
});
router.push("/admin/dashboard");
Cookies.set("status", "login", {
expires: 1,
});
// close();
// }
close();
}
}
};

View File

@ -56,7 +56,6 @@ function getRangeAcrossMonths(
const labels: string[] = [];
const sortedData = data.sort((a, b) => a.month - b.month);
console.log("sorted data", sortedData);
for (const monthData of sortedData) {
const { month, view: v, comment: c, share: s } = monthData;
@ -75,7 +74,7 @@ function getRangeAcrossMonths(
const label = `${(i + 1).toString().padStart(2, "0")} - ${month
.toString()
.padStart(2, "0")} `;
.padStart(2, "0")}`;
labels.push(label);
}
}
@ -106,16 +105,11 @@ const ApexChartColumn = (props: {
const splitDate = date.split(" ");
const splitDateDaily = String(range.start.year);
let data = [];
console.log(
"aaawwww",
type === "monthly" && splitDate[1] === years,
type === "daily" && splitDateDaily === years
);
if (
(type === "monthly" && splitDate[1] === years) ||
(type === "daily" && splitDateDaily === years)
) {
console.log("if", datas);
data = datas;
} else {
const res = await getStatisticMonthly(
@ -124,7 +118,6 @@ const ApexChartColumn = (props: {
data = res?.data?.data;
setDatas(data);
}
console.log("datas", data);
const getDatas = data?.find(
(a: any) =>
a.month == Number(splitDate[0]) && a.year === Number(splitDate[1])

View File

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

View File

@ -56,7 +56,6 @@ function getRangeAcrossMonths(
const labels: string[] = [];
const sortedData = data.sort((a, b) => a.month - b.month);
console.log("sorted data", sortedData);
for (const monthData of sortedData) {
const { month, view: v, comment: c, share: s } = monthData;
@ -106,16 +105,11 @@ const ApexChartColumnVisitors = (props: {
const splitDate = date.split(" ");
const splitDateDaily = String(range.start.year);
let data = [];
console.log(
"aaawwww",
type === "monthly" && splitDate[1] === years,
type === "daily" && splitDateDaily === years
);
if (
(type === "monthly" && splitDate[1] === years) ||
(type === "daily" && splitDateDaily === years)
) {
console.log("if", datas);
data = datas;
} else {
const res = await getStatisticMonthly(
@ -124,7 +118,6 @@ const ApexChartColumnVisitors = (props: {
data = res?.data?.data;
setDatas(data);
}
console.log("datas", data);
const getDatas = data?.find(
(a: any) =>
a.month == Number(splitDate[0]) && a.year === Number(splitDate[1])

View File

@ -69,6 +69,7 @@ import {
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";
type ArticleData = Article & {
no: number;
@ -198,7 +199,11 @@ export default function DashboardContainer() {
today
);
const [visitorDailyDate, setVisitorDailyDate] = useState({
start: parseDate(convertDateFormatNoTimeV2(today)),
start: parseDate(
convertDateFormatNoTimeV2(
new Date(new Date().setDate(new Date().getDate() - 30))
)
),
end: parseDate(convertDateFormatNoTimeV2(today)),
});
@ -722,7 +727,7 @@ export default function DashboardContainer() {
)}
</div>
</div>
<div className="w-full flex flex-col lg:flex-row gap-6 justify-center min-h-[480px]">
<div className="w-full flex flex-col lg:flex-row gap-6 justify-center lg:h-[700px]">
<div className="border-1 shadow-sm w-screen rounded-lg lg:w-[50%] p-6 flex flex-col">
<div className="flex justify-between mb-3">
<div className="font-semibold flex flex-col">
@ -861,11 +866,9 @@ export default function DashboardContainer() {
</div>
</div>
</div>
<div className="border-1 shadow-sm w-screen rounded-lg md:w-[50%] p-6 flex flex-col">
<div className="border-1 shadow-sm w-screen rounded-lg md:w-[50%] p-6 flex flex-col h-[700px]">
<div className="flex justify-between mb-3">
<div className="font-semibold flex flex-col">
Visitor Analytics
</div>
<div className="font-semibold flex flex-col">Users 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]"
@ -937,7 +940,7 @@ export default function DashboardContainer() {
<DateRangePicker
className="h-[40px]"
value={visitorDailyDate}
onChange={(e) => e !== null && setViewsDailyDate(e)}
onChange={(e) => e !== null && setVisitorDailyDate(e)}
label=""
/>
</div>
@ -946,7 +949,16 @@ export default function DashboardContainer() {
</div>
<div className="flex flex-row w-full h-full">
<div className="w-full h-[30vh] lg:h-full text-black">
<ApexChartColumnVisitors
<ApexChartDynamic
key={`${
convertDateFormatNoTimeV2(
String(visitorSelectedMonth)
).split("-")[1]
} ${
convertDateFormatNoTimeV2(
String(visitorSelectedMonth)
).split("-")[0]
}-${typeDateVisitor}-${visitorDailyDate}`}
type={typeDateVisitor}
date={`${
convertDateFormatNoTimeV2(
@ -957,7 +969,6 @@ export default function DashboardContainer() {
String(visitorSelectedMonth)
).split("-")[0]
}`}
view={["visitor"]}
range={visitorDailyDate}
/>
</div>
@ -965,19 +976,124 @@ export default function DashboardContainer() {
</div>
</div>
{roleId && Number(roleId) < 3 && (
<div className="flex flex-row gap-6 w-full">
<div className="w-screen md:w-[80%] h-[360px] lg:h-[600px]">
<IndonesiaMap />
<div>
<div className="flex flex-col lg:flex-row gap-6 w-full pr-2 md:pr-0">
<div className="w-screen md:w-[80%] h-[360px] lg:h-[600px]">
<IndonesiaMap />
</div>
<div className="w-screen md:w-[300px] h-[360px] lg:h-[600px] overflow-auto gap-2 flex flex-col">
{dummyData.map((list) => (
<div key={list.id} className="w-full flex flex-row gap-2">
<p className="w-1/2">{list.name}</p>{" "}
<p className="w-1/2 bg-[#0e7490] text-white border-white ">
{list.value}
</p>
</div>
))}
</div>
</div>
<div className="w-screen md:w-[300px] h-[360px] lg:h-[600px] overflow-auto gap-2 flex flex-col">
{dummyData.map((list) => (
<div key={list.id} className="w-full flex flex-row gap-2">
<p className="w-1/2">{list.name}</p>{" "}
<p className="w-1/2 bg-[#0e7490] text-white border-white ">
{list.value}
</p>
<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]
}`}
view={["visitor"]}
range={visitorDailyDate}
/>
</div>
</div>
</div>
</div>
)}