feat:notif

This commit is contained in:
Rama Priyanto 2026-01-26 07:10:02 +07:00
parent f0cbdab261
commit 018f23f833
5 changed files with 217 additions and 18 deletions

View File

@ -2908,3 +2908,45 @@ export const AddAgentIcon = ({
</g>
</svg>
);
export const NotificationIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
{...props}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M5 19q-.425 0-.712-.288T4 18t.288-.712T5 17h1v-7q0-2.075 1.25-3.687T10.5 4.2v-.7q0-.625.438-1.062T12 2t1.063.438T13.5 3.5v.7q2 .5 3.25 2.113T18 10v7h1q.425 0 .713.288T20 18t-.288.713T19 19zm7 3q-.825 0-1.412-.587T10 20h4q0 .825-.587 1.413T12 22"
/>
</svg>
);
export const NotificationUnreadIcon = ({
size,
height = 24,
width = 24,
fill = "currentColor",
...props
}: IconSvgProps) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size || width}
height={size || height}
{...props}
viewBox="0 0 24 24"
>
<path
fill="currentColor"
d="M12 22q-.825 0-1.412-.587T10 20h4q0 .825-.587 1.413T12 22m-7-3q-.425 0-.712-.288T4 18t.288-.712T5 17h1v-7q0-2.075 1.25-3.687T10.5 4.2v-.7q0-.625.438-1.062T12 2t1.063.438T13.5 3.5v.325q-.25.5-.375 1.05T13 6q0 2.075 1.463 3.538T18 11v6h1q.425 0 .713.288T20 18t-.288.713T19 19zM18 9q-1.25 0-2.125-.875T15 6t.875-2.125T18 3t2.125.875T21 6t-.875 2.125T18 9"
/>
</svg>
);

View File

@ -14,6 +14,25 @@ import {
} from "./ui/dropdown-menu";
import Cookies from "js-cookie";
import { useRouter } from "next/navigation";
import {
getNotificationsData,
mareReadNotification,
} from "@/service/notification";
import {
getCookiesDecrypt,
getTimeStamp,
parseDateNoTZ,
} from "@/utils/globals";
import { NotificationIcon, NotificationUnreadIcon } from "./icons";
interface NotifList {
id: number;
isRead: boolean;
sendBy: number;
sendByName: string;
message: string;
createdAt: string;
}
export default function Navbar(props: {
title: string;
@ -21,17 +40,22 @@ export default function Navbar(props: {
subSubTitle?: string;
}) {
const [isCollapsed, setIsCollapsed] = useState(
typeof window !== "undefined" && localStorage.getItem("sidebar") === "open"
typeof window !== "undefined" && localStorage.getItem("sidebar") === "open",
);
const { title, subTitle, subSubTitle } = props;
const titleCondition = subSubTitle
? "subSubTitle"
: subTitle
? "subTitle"
: "title";
? "subTitle"
: "title";
const fullname = Cookies.get("ufne");
const username = Cookies.get("username");
const userId = String(getCookiesDecrypt("uie"));
const [notifList, setNotifList] = useState<NotifList[]>([]);
const [isUnread, setIsUnread] = useState(true);
const router = useRouter();
useEffect(() => {
@ -41,12 +65,10 @@ export default function Navbar(props: {
}, [fullname]);
const handleLogout = () => {
// Hapus semua cookies
Object.keys(Cookies.get()).forEach((cookieName) => {
Cookies.remove(cookieName);
});
// Redirect ke halaman login
router.push("/auth");
};
@ -65,15 +87,33 @@ export default function Navbar(props: {
setIsCollapsed(newState);
localStorage.setItem("sidebar", newState ? "open" : "close");
// ✅ Kirim event manual agar sidebar langsung tahu (tanpa tunggu reload)
window.dispatchEvent(
new StorageEvent("storage", {
key: "sidebar",
newValue: newState ? "open" : "close",
})
}),
);
};
useEffect(() => {
getNotifications();
}, []);
const getNotifications = async () => {
const res = await getNotificationsData(userId, 1, 5);
const data: NotifList[] = res?.data?.data ?? [];
const temp = data.map((a) => {
return a.isRead;
});
setIsUnread(temp.includes(false));
setNotifList(data);
};
const markRead = async (id: number) => {
const res = await mareReadNotification(id);
getNotifications();
};
return (
<div className="w-full flex justify-between items-center h-8 lg:text-2xl mt-2 lg:mt-0">
<div className="flex flex-row">
@ -137,6 +177,29 @@ export default function Navbar(props: {
<Search className="absolute left-2 top-2.5 h-4 w-4 text-gray-500" />
<Input placeholder="Search" className="pl-8 text-sm h-8 w-48" />
</div>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<a className="cursor-pointer">
{isUnread ? <NotificationUnreadIcon /> : <NotificationIcon />}
</a>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56" align="start">
<div className="flex flex-col gap-1 text-sm">
{notifList.map((notif) => (
<a
key={notif.id}
className={`cursor-pointer flex flex-col ${notif.isRead ? "" : "bg-gray-100"}`}
onClick={() => markRead(notif.id)}
>
<p>From: {notif.sendByName}</p>
<p className="text-xs">{notif.message}</p>
<p>{getTimeStamp(parseDateNoTZ(notif.createdAt))}</p>
</a>
))}
</div>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger>
<div className="flex flex-row items-center gap-2">

View File

@ -17,6 +17,8 @@ import { getCookiesDecrypt } from "@/utils/globals";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { close, error, loading } from "@/config/swal";
import { sendNotifications } from "@/service/notification";
import Cookies from "js-cookie";
interface KnowledgeBase {
id: number;
@ -42,6 +44,7 @@ export default function DataKnowledge() {
const [totalData, setTotalData] = useState(0);
const ulne = getCookiesDecrypt("ulne");
const uie = getCookiesDecrypt("uie");
const ufne = Cookies.get("ufne");
useEffect(() => {
initFetch();
@ -82,6 +85,18 @@ export default function DataKnowledge() {
if (statusNumber == 1) {
await sendFileKnowledgeBase(id);
}
if (statusNumber == 2) {
const findKB = knowledgeBase.find((a) => a.id == id);
const req = {
sentTo: Number(findKB?.createdById),
sendBy: Number(uie),
sendByName: ufne as string,
message: `Data Knowlegde ${findKB?.title} Rejected`,
};
const resNotif = await sendNotifications(req);
}
initFetch();
MySwal.fire({
title: "Sukses",
@ -128,7 +143,6 @@ export default function DataKnowledge() {
tempFileName.push(getDocumentName(fileVideoUrl));
}
console.log("tempfilename", tempFileName);
const res = await deleteKnowledgeBaseData(id);
if (res?.error) {
error(res?.message);
@ -141,7 +155,7 @@ export default function DataKnowledge() {
`https://narasiahli.com/ai/api/v1/agents/${data?.agentId}/s3-documents/${element}`,
{
method: "DELETE",
}
},
);
}
}
@ -176,7 +190,7 @@ export default function DataKnowledge() {
{
method: "POST",
body: formData,
}
},
);
// const json = await res.json();
@ -281,16 +295,16 @@ export default function DataKnowledge() {
item.status == 0
? "bg-orange-300"
: item.status == 1
? "bg-green-600"
: "bg-red-600"
? "bg-green-600"
: "bg-red-600"
}`}
>
{" "}
{item.status == 0
? "Waiting"
: item.status == 1
? "Approved"
: "Rejected"}
? "Approved"
: "Rejected"}
</p>
</td>

36
service/notification.tsx Normal file
View File

@ -0,0 +1,36 @@
import {
httpGetInterceptor,
httpPostInterceptor,
httpPutInterceptor,
httpPatchInterceptor,
httpDeleteInterceptor,
} from "./http-config/http-interceptor-services";
import { httpGet } from "./http-config/http-base-services";
export async function getNotificationsData(
user: string,
page: number,
limit: number,
status?: boolean,
) {
const response = await httpGet(
`/notifications/${user}?page=${page}&limit=${limit}${status ? `&status${status}` : ""}`,
);
return response.data;
}
export async function mareReadNotification(id: number) {
const response = await httpPutInterceptor(`/notifications/${id}/read`, {});
return response;
}
export async function sendNotifications(data: {
message: string;
sendBy: number;
sendByName: string;
sentTo: number;
}) {
const response = await httpPostInterceptor(`/notifications`, data);
return response;
}

View File

@ -77,7 +77,7 @@ export function delay(ms: number) {
export function textEllipsis(
str: string,
maxLength: number,
{ side = "end", ellipsis = "..." } = {}
{ side = "end", ellipsis = "..." } = {},
) {
if (str !== undefined && str?.length > maxLength) {
switch (side) {
@ -141,12 +141,12 @@ export function formatMonthString(dateString: string) {
export function setCookiesEncrypt(
param: string,
data: any,
options?: Cookies.CookieAttributes
options?: Cookies.CookieAttributes,
) {
// Enkripsi data
const cookiesEncrypt = CryptoJS.AES.encrypt(
JSON.stringify(data),
`${param}_EncryptKey@humas`
`${param}_EncryptKey@humas`,
).toString(); // Tambahkan .toString() di sini
// Simpan data terenkripsi di cookie
@ -159,7 +159,7 @@ export function getCookiesDecrypt(param: any) {
if (cookiesEncrypt != undefined) {
const output = CryptoJS.AES.decrypt(
cookiesEncrypt.toString(),
`${param}_EncryptKey@humas`
`${param}_EncryptKey@humas`,
).toString(CryptoJS.enc.Utf8);
if (output.startsWith('"')) {
return output.slice(1, -1);
@ -185,3 +185,47 @@ export function createSlug(text?: string) {
return "";
}
}
export const getTimeStamp = (createdAt: Date): string => {
const now = new Date();
const differenceInSeconds = Math.floor(
(now.getTime() - createdAt.getTime()) / 1000,
);
console.log("adatw", differenceInSeconds, now, createdAt);
const intervals: { [key: string]: number } = {
year: 31536000,
month: 2592000,
week: 604800,
day: 86400,
hour: 3600,
minute: 60,
second: 1,
};
for (const interval in intervals) {
const intervalSeconds = intervals[interval];
const count = Math.floor(differenceInSeconds / intervalSeconds);
if (count >= 1) {
return `${count} ${interval}${count > 1 ? "s" : ""} ago`;
}
}
return "just now";
};
export const parseDateNoTZ = (iso: string): Date => {
const [datePart, timePartRaw] = iso.split("T");
const [year, month, day] = datePart.split("-").map(Number);
const timePart = timePartRaw ?? "00:00:00";
const [hh, mm, ssRaw] = timePart.split(":");
const seconds = Number(ssRaw?.split(".")[0] ?? 0);
const ms = Number((ssRaw?.split(".")[1] ?? "0").slice(0, 3));
return new Date(year, month - 1, day, Number(hh), Number(mm), seconds, ms);
};