From 03b45e58c67069a1533fbbad6236ec293e6f416e Mon Sep 17 00:00:00 2001 From: amel Date: Fri, 20 Sep 2024 14:28:08 +0800 Subject: [PATCH] upd: notifikasi Deskripsi: - jumlah notifikasi - nama desa di page home - masang notifikasi pada project dan pengumuman No Issues --- src/app/api/announcement/route.ts | 41 +++++-- src/app/api/home/notification/route.ts | 94 ++++++++++++++ src/app/api/home/route.ts | 19 +++ src/app/api/project/route.ts | 73 ++++++++++- src/module/home/lib/api_notification.ts | 13 ++ src/module/home/lib/api_search.ts | 2 - src/module/home/lib/type_notification.ts | 9 ++ src/module/home/ui/header_home.tsx | 70 +++++++++++ src/module/home/ui/icon_navbar.tsx | 30 ----- src/module/home/ui/list_notification.tsx | 150 ++++++++++++++++++----- src/module/home/ui/view_home.tsx | 17 +-- 11 files changed, 431 insertions(+), 87 deletions(-) create mode 100644 src/app/api/home/notification/route.ts create mode 100644 src/module/home/lib/api_notification.ts create mode 100644 src/module/home/lib/type_notification.ts create mode 100644 src/module/home/ui/header_home.tsx delete mode 100644 src/module/home/ui/icon_navbar.tsx diff --git a/src/app/api/announcement/route.ts b/src/app/api/announcement/route.ts index 3bf8a44..9c12699 100644 --- a/src/app/api/announcement/route.ts +++ b/src/app/api/announcement/route.ts @@ -117,6 +117,7 @@ export async function POST(request: Request) { const { title, desc, groups } = (await request.json()); const villaId = user.idVillage const userId = user.id + const userRoleLogin = user.idUserRole const data = await prisma.announcement.create({ data: { @@ -177,27 +178,43 @@ export async function POST(request: Request) { desc: 'Anda memiliki pengumuman baru. Silahkan periksa detailnya.' })) + if (userRoleLogin != "supadmin") { + const perbekel = await prisma.user.findFirst({ + where: { + isActive: true, + idUserRole: "supadmin", + idVillage: user.idVillage + } + }) - + dataNotif.push({ + idUserTo: perbekel?.id, + idUserFrom: userId, + category: 'announcement', + idContent: data.id, + title: 'Pengumuman Baru', + desc: 'Anda memiliki pengumuman baru. Silahkan periksa detailnya.' + }) + } const insertNotif = await prisma.notifications.createMany({ data: dataNotif }) - for (let index = 0; index < dataNotif.length; index++) { + // for (let index = 0; index < dataNotif.length; index++) { - const user = dataNotif[index].idUserTo - const title = dataNotif[index].title - const desc = dataNotif[index].desc + // const user = dataNotif[index].idUserTo + // const title = dataNotif[index].title + // const desc = dataNotif[index].desc - mtqq_client.publish("app_SDM", JSON.stringify({ - "user": "clzm6swhg000tfgbhm3bau9ti", - "title": title, - "category": "announcement", - "description": desc - })) - } + // mtqq_client.publish("app_SDM", JSON.stringify({ + // "user": "clzm6swhg000tfgbhm3bau9ti", + // "title": title, + // "category": "announcement", + // "description": desc + // })) + // } // create log user diff --git a/src/app/api/home/notification/route.ts b/src/app/api/home/notification/route.ts new file mode 100644 index 0000000..10e47bf --- /dev/null +++ b/src/app/api/home/notification/route.ts @@ -0,0 +1,94 @@ +import { prisma } from "@/module/_global"; +import { funGetUserByCookies } from "@/module/auth"; +import { createLogUser } from "@/module/user"; +import _ from "lodash"; +import moment from "moment"; +import { NextResponse } from "next/server"; + +// GET ALL NOTIFIKASI +export async function GET(request: Request) { + try { + const user = await funGetUserByCookies(); + if (user.id == undefined) { + return NextResponse.json({ success: false, message: "Anda harus login untuk mengakses ini" }, { status: 401 }); + } + + const { searchParams } = new URL(request.url); + const page = searchParams.get('page'); + const dataSkip = Number(page) * 10 - 10; + + const announcements = await prisma.notifications.findMany({ + skip: dataSkip, + take: 10, + where: { + isActive: true, + idUserTo: user.id + }, + orderBy: [ + { + isRead: 'asc' + }, + { + createdAt: 'desc' + } + ] + + }); + + const allData = announcements.map((v: any) => ({ + ..._.omit(v, ["createdAt"]), + createdAt: moment(v.createdAt).format("ll") + })) + + return NextResponse.json({ success: true, message: "Berhasil mendapatkan notifikasi", data: allData, }, { status: 200 }); + } catch (error) { + console.error(error); + return NextResponse.json({ success: false, message: "Gagal mendapatkan notifikasi, coba lagi nanti", reason: (error as Error).message, }, { status: 500 }); + } +} + + + +// UPDATE READ NOTIFIKASI +export async function PUT(request: Request, context: { params: { id: string } }) { + try { + const user = await funGetUserByCookies() + if (user.id == undefined) { + return NextResponse.json({ success: false, message: "Anda harus login untuk mengakses ini" }, { status: 401 }); + } + const { id } = await request.json(); + const data = await prisma.notifications.count({ + where: { + id: id, + }, + }); + + if (data == 0) { + return NextResponse.json( + { + success: false, + message: "Gagal mendapatkan data, data tidak ditemukan", + }, + { status: 404 } + ); + } + + const result = await prisma.notifications.update({ + where: { + id: id, + }, + data: { + isActive: false, + }, + }); + + // create log user + const log = await createLogUser({ act: 'UPDATE', desc: 'User membaca notifikasi', table: 'notifications', data: id }) + + return NextResponse.json( { success: true, message: "Berhasil mendapatkan notifikasi", }, { status: 200 } ); + + } catch (error) { + console.error(error); + return NextResponse.json({ success: false, message: "Gagal mendapatkan notifikasi, coba lagi nanti", reason: (error as Error).message, }, { status: 500 }); + } +} \ No newline at end of file diff --git a/src/app/api/home/route.ts b/src/app/api/home/route.ts index 918c174..2bdc630 100644 --- a/src/app/api/home/route.ts +++ b/src/app/api/home/route.ts @@ -358,6 +358,25 @@ export async function GET(request: Request) { date: moment(v.dateStart).format("ll"), user: v.User.name })) + } else if (kategori == "header") { + const total = await prisma.notifications.count({ + where: { + isActive: true, + isRead: false, + idUserTo: user.id + } + }) + + const desa = await prisma.village.findUnique({ + where: { + id: idVillage + } + }) + + allData = { + totalNotif: total, + village: desa?.name + } } return NextResponse.json({ success: true, message: "Berhasil mendapatkan data", data: allData }, { status: 200 }); diff --git a/src/app/api/project/route.ts b/src/app/api/project/route.ts index 0e81b2a..8cc83f9 100644 --- a/src/app/api/project/route.ts +++ b/src/app/api/project/route.ts @@ -137,7 +137,7 @@ export async function POST(request: Request) { const { idGroup, title, task, member } = JSON.parse(dataBody as string) const userId = user.id - + const userRoleLogin = user.idUserRole const data = await prisma.project.create({ data: { @@ -198,6 +198,77 @@ export async function POST(request: Request) { } } + const memberNotif = await prisma.projectMember.findMany({ + where: { + idProject: data.id + }, + select: { + idUser: true + } + }) + + const dataNotif = memberNotif.map((v: any) => ({ + ..._.omit(v, ["idUser"]), + idUserTo: v.idUser, + idUserFrom: userId, + category: 'project', + idContent: data.id, + title: 'Kegiatan Baru', + desc: 'Terdapat kegiatan baru. Silahkan periksa detailnya.' + })) + + if (userRoleLogin != "supadmin") { + const perbekel = await prisma.user.findFirst({ + where: { + isActive: true, + idUserRole: "supadmin", + idVillage: user.idVillage + } + }) + + dataNotif.push({ + idUserTo: perbekel?.id, + idUserFrom: userId, + category: 'project', + idContent: data.id, + title: 'Kegiatan Baru', + desc: 'Terdapat kegiatan baru. Silahkan periksa detailnya.' + }) + } else { + const atasanGroup = await prisma.user.findMany({ + where: { + isActive: true, + idGroup: idGroup, + AND: { + OR: [ + { idUserRole: 'cosupadmin' }, + { idUserRole: 'admin' }, + ] + } + }, + select:{ + id: true + } + }) + + const omitData = atasanGroup.map((v: any) => ({ + ..._.omit(v, ["id"]), + idUserTo: v.id, + idUserFrom: userId, + category: 'project', + idContent: data.id, + title: 'Kegiatan Baru', + desc: 'Terdapat kegiatan baru. Silahkan periksa detailnya.' + })) + + dataNotif.push(...omitData) + + } + + const insertNotif = await prisma.notifications.createMany({ + data: dataNotif + }) + // create log user const log = await createLogUser({ act: 'CREATE', desc: 'User membuat data kegiatan', table: 'project', data: data.id }) diff --git a/src/module/home/lib/api_notification.ts b/src/module/home/lib/api_notification.ts new file mode 100644 index 0000000..60e8000 --- /dev/null +++ b/src/module/home/lib/api_notification.ts @@ -0,0 +1,13 @@ +export const funGetAllNotification = async (path?: string) => { + const response = await fetch(`/api/home/notification${(path) ? path : ''}`, { next: { tags: ['notification'] } }); + return await response.json().catch(() => null); +} + + +export const funReadNotification = async (data: { id: string }) => { + const response = await fetch(`/api/home/notification`, { + method: "PUT", + body: JSON.stringify(data), + }); + return await response.json().catch(() => null); +} \ No newline at end of file diff --git a/src/module/home/lib/api_search.ts b/src/module/home/lib/api_search.ts index 406c010..5fbad0e 100644 --- a/src/module/home/lib/api_search.ts +++ b/src/module/home/lib/api_search.ts @@ -1,5 +1,3 @@ - - export const funGetSearchAll = async (path?: string) => { const response = await fetch(`/api/home/search${(path) ? path : ''}`, { next: { tags: ['search'] } }); return await response.json().catch(() => null); diff --git a/src/module/home/lib/type_notification.ts b/src/module/home/lib/type_notification.ts new file mode 100644 index 0000000..73fefa7 --- /dev/null +++ b/src/module/home/lib/type_notification.ts @@ -0,0 +1,9 @@ +export interface IListNotification { + id: string + title: string + desc: string + category: string + idContent: string + isRead: boolean + createdAt: string +} \ No newline at end of file diff --git a/src/module/home/ui/header_home.tsx b/src/module/home/ui/header_home.tsx new file mode 100644 index 0000000..d0fadaa --- /dev/null +++ b/src/module/home/ui/header_home.tsx @@ -0,0 +1,70 @@ +'use client' +import { LayoutNavbarHome, TEMA } from "@/module/_global"; +import { useHookstate } from "@hookstate/core"; +import { ActionIcon, Box, Group, Indicator, Text } from "@mantine/core"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import toast from "react-hot-toast"; +import { HiMagnifyingGlass, HiOutlineBell, HiOutlineUser } from "react-icons/hi2"; +import { funGetHome } from "../lib/api_home"; +import { useShallowEffect } from "@mantine/hooks"; + +export default function HeaderHome() { + const router = useRouter() + const tema = useHookstate(TEMA) + const [isDesa, setDesa] = useState("") + const [isNotif, setNotif] = useState(0) + + + const fetchData = async () => { + try { + const response = await funGetHome('?cat=header') + if (response.success) { + setDesa(response.data.village) + setNotif(response.data.totalNotif) + } else { + toast.error(response.message); + } + } catch (error) { + toast.error("Gagal mendapatkan data, coba lagi nanti"); + console.error(error); + } + }; + + + useShallowEffect(() => { + fetchData(); + }, []); + + + return ( + + + {isDesa} + + + router.push('/home?cat=search')} variant="light" bg={tema.get().bgIcon} size="lg" radius="lg" aria-label="Settings"> + + + { + isNotif > 0 ? + + router.push('/home?cat=notification')} variant="light" bg={tema.get().bgIcon} size="lg" radius="lg" aria-label="Settings"> + + + + : + router.push('/home?cat=notification')} variant="light" bg={tema.get().bgIcon} size="lg" radius="lg" aria-label="Settings"> + + + } + + router.push('/profile')} variant="light" bg={tema.get().bgIcon} size="lg" radius="lg" aria-label="Settings"> + + + + + + + ) +} \ No newline at end of file diff --git a/src/module/home/ui/icon_navbar.tsx b/src/module/home/ui/icon_navbar.tsx deleted file mode 100644 index c7399eb..0000000 --- a/src/module/home/ui/icon_navbar.tsx +++ /dev/null @@ -1,30 +0,0 @@ -"use client" -import { TEMA, WARNA } from '@/module/_global'; -import { useHookstate } from '@hookstate/core'; -import { ActionIcon, Box, Group, Indicator, Text } from '@mantine/core'; -import { useRouter } from 'next/navigation'; -import React from 'react'; -import { HiMagnifyingGlass, HiOutlineBell, HiOutlineUser } from 'react-icons/hi2'; - -export default function IconNavbar() { - const router = useRouter() - const tema = useHookstate(TEMA) - return ( - - - router.push('/home?cat=search')} variant="light" bg={tema.get().bgIcon} size="lg" radius="lg" aria-label="Settings"> - - - - router.push('/home?cat=notification')} variant="light" bg={tema.get().bgIcon} size="lg" radius="lg" aria-label="Settings"> - - - - router.push('/profile')} variant="light" bg={tema.get().bgIcon} size="lg" radius="lg" aria-label="Settings"> - - - - - ); -} - diff --git a/src/module/home/ui/list_notification.tsx b/src/module/home/ui/list_notification.tsx index da3b346..be7bfe5 100644 --- a/src/module/home/ui/list_notification.tsx +++ b/src/module/home/ui/list_notification.tsx @@ -1,11 +1,14 @@ "use client" -import { TEMA, WARNA } from '@/module/_global'; +import { currentScroll, TEMA, WARNA } from '@/module/_global'; import { useHookstate } from '@hookstate/core'; -import { ActionIcon, Box, Center, Grid, Group, Spoiler, Text } from '@mantine/core'; -import { useMediaQuery } from '@mantine/hooks'; +import { ActionIcon, Box, Center, Flex, Grid, Group, Spoiler, Text } from '@mantine/core'; +import { useMediaQuery, useShallowEffect } from '@mantine/hooks'; import { useRouter } from 'next/navigation'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { FaBell } from 'react-icons/fa6'; +import { IListNotification } from '../lib/type_notification'; +import { funGetAllNotification, funReadNotification } from '../lib/api_notification'; +import toast from 'react-hot-toast'; const dataNotification = [ { @@ -67,38 +70,125 @@ const dataNotification = [ export default function ListNotification() { const router = useRouter() - const isMobile = useMediaQuery('(max-width: 369px)'); + const isMobile = useMediaQuery('(max-width: 369px)') + const [isData, setData] = useState([]) const tema = useHookstate(TEMA) + const { value: containerRef } = useHookstate(currentScroll) + const [isPage, setPage] = useState(1) + const [loading, setLoading] = useState(true) + + async function fetchData(loading: boolean) { + try { + if (loading) + setLoading(true) + const res = await funGetAllNotification('?page=' + isPage) + if (res.success) { + if (isPage == 1) { + setData(res.data) + } else { + setData([...isData, ...res.data]) + } + + } else { + toast.error(res.message) + } + } catch (error) { + console.error(error) + toast.error("Gagal memuat data, coba lagi nanti") + } finally { + setLoading(false) + } + } + + useShallowEffect(() => { + fetchData(true) + }, []) + + useShallowEffect(() => { + fetchData(false) + }, [isPage]) + + useEffect(() => { + const handleScroll = async () => { + if (containerRef && containerRef.current) { + const scrollTop = containerRef.current.scrollTop; + const containerHeight = containerRef.current.clientHeight; + const scrollHeight = containerRef.current.scrollHeight; + + if (scrollTop + containerHeight >= scrollHeight) { + setPage(isPage + 1) + } + + } + }; + + const container = containerRef?.current; + container?.addEventListener("scroll", handleScroll); + return () => { + container?.removeEventListener("scroll", handleScroll); + }; + }, [containerRef, isPage]); + + + + async function onReadNotif(category: string, idContent: string, idData: string) { + try { + const response = await funReadNotification({ id: idData }) + if (response.success) { + router.push(`/${category}/${idContent}`) + } else { + toast.error(response.message) + } + } catch (error) { + console.error(error) + toast.error("Gagal memuat data, coba lagi nanti") + } + } + + + return ( - {dataNotification.map((v, i) => { - return ( - - - - - - - + Tidak ada notifikasi + + : + isData.map((v, i) => { + return ( + + { + onReadNotif(v.category, v.idContent, v.id) + }} > - {v.title} + + + + + + {v.title} + + + + {v.desc} + - - - {v.description} - - - - ) - })} + + ) + }) + } + { } ); } diff --git a/src/module/home/ui/view_home.tsx b/src/module/home/ui/view_home.tsx index a9f2f65..4ee8bd5 100644 --- a/src/module/home/ui/view_home.tsx +++ b/src/module/home/ui/view_home.tsx @@ -1,30 +1,23 @@ "use client" -import { LayoutNavbarHome, NotificationCustome, ReloadButtonTop, TEMA, WARNA } from '@/module/_global'; -import { Box, Group, Notification, Stack, Text } from '@mantine/core'; -import React, { useState } from 'react'; +import { ReloadButtonTop } from '@/module/_global'; +import { Box, Stack } from '@mantine/core'; +import React from 'react'; import Carosole from './carosole'; import Features from './features'; -import IconNavbar from './icon_navbar'; import ListProjects from './list_project'; import ListDivisi from './list_divisi'; import ListDiscussion from './list_discussion'; import ListEventHome from './list_event'; import ChartProgressHome from './chart_progress_tugas'; import ChartDocumentHome from './chart_document'; -import { useHookstate } from '@hookstate/core'; +import HeaderHome from './header_home'; export default function ViewHome() { - const tema = useHookstate(TEMA) return ( <> - - - Perbekel Darmasaba - - - + {