Fix notifikasi count di home

This commit is contained in:
2024-12-19 12:08:27 +08:00
parent 09c4a878c0
commit c9291a749d
10 changed files with 361 additions and 203 deletions

2
.gitignore vendored
View File

@@ -35,7 +35,7 @@ yarn-error.log*
.vercel
# logs
logs
/logs
# typescript
*.tsbuildinfo

View File

@@ -0,0 +1,138 @@
/* app/admin/logs/logs.module.css */
.container {
padding: 24px;
max-width: 1200px;
margin: 0 auto;
}
.title {
font-size: 24px;
font-weight: bold;
margin-bottom: 16px;
}
.filterContainer {
margin-bottom: 16px;
}
.select {
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
min-width: 200px;
}
.logsContainer {
display: flex;
flex-direction: column;
gap: 8px;
}
.logItem {
padding: 16px;
border-radius: 4px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.errorLog {
background-color: #fef2f2;
}
.warnLog {
background-color: #fefce8;
}
.infoLog {
background-color: #ffffff;
}
.logHeader {
display: flex;
align-items: center;
gap: 8px;
}
.timestamp {
font-size: 14px;
color: #666;
}
.level {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.errorLevel {
background-color: #fee2e2;
color: #991b1b;
}
.warnLevel {
background-color: #fef3c7;
color: #92400e;
}
.infoLevel {
background-color: #dbeafe;
color: #1e40af;
}
.message {
margin-top: 8px;
}
.metadata {
margin-top: 8px;
padding: 8px;
background-color: #f9fafb;
border-radius: 4px;
font-size: 14px;
overflow-x: auto;
white-space: pre-wrap;
}
.loading {
text-align: center;
padding: 24px;
color: #666;
}
.error {
color: #dc2626;
text-align: center;
padding: 24px;
}
/* Hover effects */
.logItem:hover {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.select:hover {
border-color: #999;
}
/* Focus states */
.select:focus {
outline: none;
border-color: #2563eb;
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.2);
}
/* Responsive adjustments */
@media (max-width: 768px) {
.container {
padding: 16px;
}
.logHeader {
flex-direction: column;
align-items: flex-start;
}
.metadata {
font-size: 12px;
}
}

View File

@@ -0,0 +1,106 @@
// app/admin/logs/page.tsx
"use client";
import { useEffect, useState } from "react";
import { format } from "date-fns";
import styles from "./logs.module.css";
interface LogEntry {
timestamp: string;
level: string;
message: string;
metadata?: any;
}
export default function LogsPage() {
const [logs, setLogs] = useState<LogEntry[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [filter, setFilter] = useState<"all" | "error" | "info" | "warn">(
"all"
);
useEffect(() => {
fetchLogs();
}, []);
async function fetchLogs() {
try {
const response = await fetch("/api/logs/view");
if (!response.ok) throw new Error("Failed to fetch logs");
const data = await response.json();
setLogs(data.logs);
} catch (err) {
setError(err instanceof Error ? err.message : "Error fetching logs");
} finally {
setLoading(false);
}
}
const filteredLogs = logs.filter((log) =>
filter === "all" ? true : log.level === filter
);
if (loading) return <div className={styles.loading}>Loading logs...</div>;
if (error) return <div className={styles.error}>Error: {error}</div>;
return (
<div className={styles.container}>
<h1 className={styles.title}>System Logs</h1>
<div className={styles.filterContainer}>
<select
value={filter}
onChange={(e) => setFilter(e.target.value as any)}
className={styles.select}
>
<option value="all">All Logs</option>
<option value="error">Errors</option>
<option value="warn">Warnings</option>
<option value="info">Info</option>
</select>
</div>
<div className={styles.logsContainer}>
{filteredLogs.map((log, index) => (
<div
key={index}
className={`${styles.logItem} ${
log.level === "error"
? styles.errorLog
: log.level === "warn"
? styles.warnLog
: styles.infoLog
}`}
>
<div className={styles.logHeader}>
<span className={styles.timestamp}>
{format(new Date(log.timestamp), "yyyy-MM-dd HH:mm:ss")}
</span>
<span
className={`${styles.level} ${
log.level === "error"
? styles.errorLevel
: log.level === "warn"
? styles.warnLevel
: styles.infoLevel
}`}
>
{log.level.toUpperCase()}
</span>
</div>
<div className={styles.message}>{log.message}</div>
{log.metadata && (
<pre className={styles.metadata}>
{JSON.stringify(log.metadata, null, 2)}
</pre>
)}
</div>
))}
</div>
</div>
);
}

View File

@@ -0,0 +1,71 @@
// app/api/logs/view/route.ts
import { NextRequest, NextResponse } from "next/server";
import fs from "fs/promises";
import path from "path";
interface LogEntry {
timestamp: string;
level: string;
message: string;
metadata?: any;
}
async function readLogFiles(directory: string): Promise<LogEntry[]> {
try {
const logPath = path.join(process.cwd(), directory);
const files = await fs.readdir(logPath);
const logFiles = files.filter((file) => file.endsWith(".log"));
const allLogs: LogEntry[] = [];
for (const file of logFiles) {
const filePath = path.join(logPath, file);
const content = await fs.readFile(filePath, "utf-8");
// Parse setiap baris log
const logs = content
.split("\n")
.filter(Boolean)
.map((line) => {
try {
return JSON.parse(line);
} catch (e) {
return null;
}
})
.filter(Boolean);
allLogs.push(...logs);
}
// Sort berdasarkan timestamp, terbaru di atas
return allLogs.sort(
(a, b) =>
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
);
} catch (error) {
console.error("Error reading log files:", error);
return [];
}
}
export async function GET(request: NextRequest) {
try {
// Baca logs dari frontend dan backend
const frontendLogs = await readLogFiles("logs/frontend");
const backendLogs = await readLogFiles("logs/backend");
// Gabungkan dan sort semua logs
const allLogs = [...frontendLogs, ...backendLogs].sort(
(a, b) =>
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
);
return NextResponse.json({ logs: allLogs });
} catch (error) {
return NextResponse.json(
{ error: "Failed to fetch logs" },
{ status: 500 }
);
}
}

View File

@@ -1,21 +1,9 @@
import { HomeViewNew } from "@/app_modules/home";
import notifikasi_countUserNotifikasi from "@/app_modules/notifikasi/fun/count/fun_count_by_id";
export default async function PageHome() {
// const userLoginId = await funGetUserIdByToken();
// const dataUser = await user_getOneByUserId(userLoginId as string);
// const dataJob = await job_getTwoForHomeView();
const countNotifikasi = await notifikasi_countUserNotifikasi();
return (
<>
{/* <HomeView
dataUser={dataUser as any}
dataJob={dataJob as any}
countNotifikasi={countNotifikasi}
/> */}
<HomeViewNew countNotifikasi={countNotifikasi}/>
<HomeViewNew />
</>
);
}

View File

@@ -7,9 +7,13 @@ export default async function Layout({
}: {
children: React.ReactNode;
}) {
const userId = await newFunGetUserId();
return (
<>
<RealtimeProvider
userId={userId}
WIBU_REALTIME_TOKEN={
ServerEnv.value?.NEXT_PUBLIC_WIBU_REALTIME_TOKEN as string
}

View File

@@ -25,6 +25,6 @@ process.on("SIGINT", async () => {
process.exit(0);
});
console.log("==> Test prisma");
// console.log("==> Test prisma");
export default prisma;

View File

@@ -17,8 +17,6 @@ import {
gs_votingTiggerBeranda,
IRealtimeData,
} from "./global_state";
import { newFunGetUserId } from "./new_fun_user_id";
import { useState } from "react";
// const WIBU_REALTIME_TOKEN: string | undefined =
// process.env.NEXT_PUBLIC_WIBU_REALTIME_TOKEN;
@@ -28,15 +26,16 @@ export type TypeNotification = {
type: "message" | "notification" | "trigger";
pushNotificationTo: "ADMIN" | "USER";
dataMessage?: IRealtimeData;
userLoginId?: string;
userId?: string;
};
export default function RealtimeProvider({
userId,
WIBU_REALTIME_TOKEN,
}: {
userId: string;
WIBU_REALTIME_TOKEN: string;
}) {
const [userLoginId, setUserLoginId] = useState("");
const [dataRealtime, setDataRealtime] = useAtom(gs_realtimeData);
const [newAdminNtf, setNewAdminNtf] = useAtom(gs_admin_ntf);
const [newUserNtf, setNewUserNtf] = useAtom(gs_user_ntf);
@@ -72,15 +71,7 @@ export default function RealtimeProvider({
gs_donasiTriggerBeranda
);
async function loadUserId() {
const userId = await newFunGetUserId();
setUserLoginId(userId as string);
}
useShallowEffect(() => {
loadUserId();
try {
WibuRealtime.init({
project: "hipmi",
@@ -97,7 +88,7 @@ export default function RealtimeProvider({
if (
data.type == "notification" &&
data.pushNotificationTo == "USER" &&
data.dataMessage?.userId == userLoginId
data.dataMessage?.userId == userId
) {
setNewUserNtf((e) => e + 1);
setDataRealtime(data.dataMessage as any);
@@ -144,7 +135,7 @@ export default function RealtimeProvider({
data.type == "notification" &&
data.pushNotificationTo == "USER" &&
data.dataMessage?.status == "Peserta Event" &&
userLoginId !== data.dataMessage?.userId
userId !== data.dataMessage?.userId
) {
setNewUserNtf((e) => e + 1);
}
@@ -172,7 +163,7 @@ export default function RealtimeProvider({
data.type == "notification" &&
data.pushNotificationTo == "USER" &&
data.dataMessage?.status == "Voting Masuk" &&
userLoginId !== data.dataMessage?.userId
userId !== data.dataMessage?.userId
) {
setNewUserNtf((e) => e + 1);
}
@@ -200,9 +191,9 @@ export default function RealtimeProvider({
// data.type == "notification" &&
// data.pushNotificationTo == "ADMIN" &&
// data.dataMessage?.status == "Menunggu" &&
// userLoginId !== data.dataMessage?.userId
// userId !== data.dataMessage?.userId
// ) {
// console.log("yes");
// }
// ---------------------- DONASI ------------------------- //

View File

@@ -1,5 +1,6 @@
"use client";
import { IRealtimeData } from "@/app/lib/global_state";
import { RouterAdminJob } from "@/app/lib/router_admin/router_admin_job";
import ComponentGlobal_InputCountDown from "@/app_modules/_global/component/input_countdown";
import { ComponentGlobal_NotifikasiBerhasil } from "@/app_modules/_global/notif_global/notifikasi_berhasil";
@@ -8,7 +9,6 @@ import { ComponentAdminGlobal_TitlePage } from "@/app_modules/admin/_admin_globa
import ComponentAdminGlobal_HeaderTamplate from "@/app_modules/admin/_admin_global/header_tamplate";
import adminNotifikasi_funCreateToUser from "@/app_modules/admin/notifikasi/fun/create/fun_create_notif_user";
import { MODEL_JOB } from "@/app_modules/job/model/interface";
import mqtt_client from "@/util/mqtt_client";
import {
Button,
Center,
@@ -22,11 +22,12 @@ import {
Table,
Text,
TextInput,
Textarea
Textarea,
} from "@mantine/core";
import { IconBan, IconPhotoCheck, IconSearch } from "@tabler/icons-react";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { WibuRealtime } from "wibu-pkg";
import { AdminJob_funEditCatatanById } from "../../fun/edit/fun_edit_catatan_by_id";
import adminJob_getListReject from "../../fun/get/get_list_reject";
@@ -311,7 +312,7 @@ async function onReject({
const loadData = await adminJob_getListReject({ page: 1 });
onSetData(loadData);
const dataNotif = {
const dataNotifikasi: IRealtimeData = {
appId: reject.data?.id as any,
status: reject.data?.MasterStatus?.name as any,
userId: reject.data?.authorId as any,
@@ -321,14 +322,15 @@ async function onReject({
};
const notif = await adminNotifikasi_funCreateToUser({
data: dataNotif as any,
data: dataNotifikasi as any,
});
if (notif.status === 201) {
mqtt_client.publish(
"USER",
JSON.stringify({ userId: reject?.data?.authorId, count: 1 })
);
WibuRealtime.setData({
type: "notification",
pushNotificationTo: "USER",
dataMessage: dataNotifikasi,
});
}
ComponentGlobal_NotifikasiBerhasil(reject.message);

View File

@@ -1,5 +1,7 @@
"use client";
import { API_RouteNotifikasi } from "@/app/lib/api_user_router/route_api_notifikasi";
import { gs_count_ntf, gs_user_ntf } from "@/app/lib/global_state";
import { RouterProfile } from "@/app/lib/router_hipmi/router_katalog";
import { RouterNotifikasi } from "@/app/lib/router_hipmi/router_notifikasi";
import { RouterUserSearch } from "@/app/lib/router_hipmi/router_user_search";
import { ActionIcon, Indicator, Text } from "@mantine/core";
@@ -9,46 +11,35 @@ import { useAtom } from "jotai";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { MainColor } from "../_global/color";
import { ComponentGlobal_NotifikasiPeringatan } from "../_global/notif_global";
import UIGlobal_LayoutHeaderTamplate from "../_global/ui/ui_header_tamplate";
import UIGlobal_LayoutTamplate from "../_global/ui/ui_layout_tamplate";
import notifikasi_countUserNotifikasi from "../notifikasi/fun/count/fun_count_by_id";
import { gs_notifikasi_kategori_app } from "../notifikasi/lib";
import BodyHome from "./component/body_home";
import FooterHome from "./component/footer_home";
import { apiGetDataHome } from "./fun/get/api_home";
import { RouterProfile } from "@/app/lib/router_hipmi/router_katalog";
import { gs_notifikasi_kategori_app } from "../notifikasi/lib";
export default function HomeViewNew({
countNotifikasi,
}: {
countNotifikasi: number;
}) {
const [countNtf, setCountNtf] = useState(countNotifikasi);
export default function HomeViewNew() {
const [countNtf, setCountNtf] = useAtom(gs_count_ntf);
const [newUserNtf, setNewUserNtf] = useAtom(gs_user_ntf);
const [countLoadNtf, setCountLoadNtf] = useAtom(gs_count_ntf);
const [dataUser, setDataUser] = useState<any>({});
const [categoryPage, setCategoryPage] = useAtom(gs_notifikasi_kategori_app);
const router = useRouter();
useShallowEffect(() => {
onLoadNotifikasi({
onLoad(val) {
setCountNtf(val);
},
});
setCountNtf(countLoadNtf as any);
}, [countLoadNtf, setCountNtf]);
onLoadNotifikasi();
}, []);
useShallowEffect(() => {
setCountNtf(countNtf + newUserNtf);
setNewUserNtf(0);
}, [newUserNtf, setCountNtf]);
if (countNtf != null) {
setCountNtf(countNtf + newUserNtf);
setNewUserNtf(0);
}
}, [newUserNtf, countNtf]);
async function onLoadNotifikasi({ onLoad }: { onLoad: (val: any) => void }) {
const loadNotif = await notifikasi_countUserNotifikasi();
onLoad(loadNotif);
async function onLoadNotifikasi() {
const loadNotif = await fetch(API_RouteNotifikasi.get_count_by_id());
const data = await loadNotif.json().then((res) => res.data);
setCountNtf(data);
}
useShallowEffect(() => {
@@ -75,6 +66,7 @@ export default function HomeViewNew({
customButtonLeft={
<ActionIcon
radius={"xl"}
disabled={countNtf == null}
variant={"transparent"}
onClick={() => {
if (
@@ -93,6 +85,7 @@ export default function HomeViewNew({
customButtonRight={
<ActionIcon
variant="transparent"
disabled={countNtf == null}
onClick={() => {
if (
dataUser.profile === undefined ||
@@ -100,24 +93,23 @@ export default function HomeViewNew({
) {
router.push(RouterProfile.create, { scroll: false });
} else {
setCategoryPage("Semua")
setCategoryPage("Semua");
router.push(
RouterNotifikasi.categoryApp({ name: "semua" }),
{
scroll: false,
}
);
}
}}
>
{countNotifikasi > 0 ? (
{countNtf != null && countNtf > 0 ? (
<Indicator
processing
color={MainColor.yellow}
label={
<Text fz={10} c={MainColor.darkblue}>
{countNotifikasi > 99 ? "99+" : countNotifikasi}
{countNtf > 99 ? "99+" : countNtf}
</Text>
}
>
@@ -137,137 +129,3 @@ export default function HomeViewNew({
</>
);
}
// "use client";
// import { API_RouteNotifikasi } from "@/app/lib/api_user_router/route_api_notifikasi";
// import { gs_count_ntf, gs_user_ntf } from "@/app/lib/global_state";
// import { RouterProfile } from "@/app/lib/router_hipmi/router_katalog";
// import { RouterNotifikasi } from "@/app/lib/router_hipmi/router_notifikasi";
// import { RouterUserSearch } from "@/app/lib/router_hipmi/router_user_search";
// import { ActionIcon, Indicator, Text } from "@mantine/core";
// import { useShallowEffect } from "@mantine/hooks";
// import { IconBell, IconUserSearch } from "@tabler/icons-react";
// import { useAtom } from "jotai";
// import { useRouter } from "next/navigation";
// import { useState } from "react";
// import { MainColor } from "../_global/color";
// import UIGlobal_LayoutHeaderTamplate from "../_global/ui/ui_header_tamplate";
// import UIGlobal_LayoutTamplate from "../_global/ui/ui_layout_tamplate";
// import { gs_notifikasi_kategori_app } from "../notifikasi/lib";
// import BodyHome from "./component/body_home";
// import FooterHome from "./component/footer_home";
// import { apiGetDataHome } from "./fun/get/api_home";
// export default function HomeViewNew() {
// const [countNtf, setCountNtf] = useAtom(gs_count_ntf);
// const [newUserNtf, setNewUserNtf] = useAtom(gs_user_ntf);
// const [dataUser, setDataUser] = useState<any>({});
// const [categoryPage, setCategoryPage] = useAtom(gs_notifikasi_kategori_app);
// const router = useRouter();
// useShallowEffect(() => {
// onLoadNotifikasi();
// }, []);
// useShallowEffect(() => {
// if (countNtf != null) {
// setCountNtf(countNtf + newUserNtf);
// setNewUserNtf(0);
// }
// console.log("notif baru", newUserNtf);
// console.log("notif baru", countNtf);
// }, [newUserNtf, countNtf]);
// async function onLoadNotifikasi() {
// const loadNotif = await fetch(API_RouteNotifikasi.get_count_by_id());
// const data = await loadNotif.json().then((res) => res.data);
// setCountNtf(data);
// }
// useShallowEffect(() => {
// cekUserLogin();
// }, []);
// async function cekUserLogin() {
// try {
// const response = await apiGetDataHome("?cat=cek_profile");
// if (response.success) {
// setDataUser(response.data);
// }
// } catch (error) {
// console.error(error);
// }
// }
// return (
// <>
// <UIGlobal_LayoutTamplate
// header={
// <UIGlobal_LayoutHeaderTamplate
// title="HIPMI"
// customButtonLeft={
// <ActionIcon
// radius={"xl"}
// disabled={countNtf == null}
// variant={"transparent"}
// onClick={() => {
// if (
// dataUser.profile === undefined ||
// dataUser?.profile === null
// ) {
// router.push(RouterProfile.create, { scroll: false });
// } else {
// router.push(RouterUserSearch.main, { scroll: false });
// }
// }}
// >
// <IconUserSearch color="white" />
// </ActionIcon>
// }
// customButtonRight={
// <ActionIcon
// variant="transparent"
// disabled={countNtf == null}
// onClick={() => {
// if (
// dataUser.profile === undefined ||
// dataUser?.profile === null
// ) {
// router.push(RouterProfile.create, { scroll: false });
// } else {
// setCategoryPage("Semua");
// router.push(
// RouterNotifikasi.categoryApp({ name: "semua" }),
// {
// scroll: false,
// }
// );
// }
// }}
// >
// {countNtf != null && countNtf > 0 ? (
// <Indicator
// processing
// color={MainColor.yellow}
// label={
// <Text fz={10} c={MainColor.darkblue}>
// {countNtf > 99 ? "99+" : countNtf}
// </Text>
// }
// >
// <IconBell color="white" />
// </Indicator>
// ) : (
// <IconBell color="white" />
// )}
// </ActionIcon>
// }
// />
// }
// footer={<FooterHome />}
// >
// <BodyHome />
// </UIGlobal_LayoutTamplate>
// </>
// );
// }