diff --git a/src/app/api/auth/mobile-register/route.ts b/src/app/api/auth/mobile-register/route.ts index 33754e47..380a3a81 100644 --- a/src/app/api/auth/mobile-register/route.ts +++ b/src/app/api/auth/mobile-register/route.ts @@ -85,9 +85,8 @@ export async function POST(req: Request) { // =========== START SEND NOTIFICATION =========== // const findAllUserBySendTo = await prisma.user.findMany({ - where: { - masterUserRoleId: "2", - }, + where: { masterUserRoleId: "2" }, + select: { id: true }, }); console.log("Users to notify:", findAllUserBySendTo); diff --git a/src/app/api/mobile/job/route.ts b/src/app/api/mobile/job/route.ts index 527302ed..3744f347 100644 --- a/src/app/api/mobile/job/route.ts +++ b/src/app/api/mobile/job/route.ts @@ -1,3 +1,5 @@ +import { sendNotificationMobileToManyUser } from "@/lib/mobile/notification/send-notification"; +import { routeAdminMobile } from "@/lib/mobile/route-page-mobile"; import prisma from "@/lib/prisma"; import { NextResponse } from "next/server"; @@ -17,6 +19,25 @@ async function POST(request: Request) { }, }); + // kirim notifikasi ke semua admin untuk mengetahui ada job baru yang harus di review + + const adminUsers = await prisma.user.findMany({ + where: { masterUserRoleId: "2" }, + select: { id: true }, + }); + + await sendNotificationMobileToManyUser({ + recipientIds: adminUsers.map((user) => user.id), + senderId: data.authorId, + payload: { + title: "Job: Pengajuan Review", + body: data.title, + type: "announcement", + deepLink: routeAdminMobile.jobByStatus({ status: "review" }), + kategoriApp: "JOB", + }, + }); + return NextResponse.json( { success: true, @@ -54,10 +75,10 @@ async function GET(request: Request) { MasterStatus: { name: "Publish", }, - // title: { - // contains: search || "", - // mode: "insensitive", - // }, + // title: { + // contains: search || "", + // mode: "insensitive", + // }, }, orderBy: { createdAt: "desc", @@ -90,46 +111,46 @@ async function GET(request: Request) { fixData = data; } else if (category === "beranda") { - const data = await prisma.job.findMany({ - where: { - isActive: true, - isArsip: false, - MasterStatus: { - name: "Publish", - }, - title: { - contains: search || "", - mode: "insensitive", - }, - }, - orderBy: { - createdAt: "desc", - }, - select: { - id: true, - title: true, - deskripsi: true, - authorId: true, - MasterStatus: { - select: { - name: true, - }, - }, - Author: { - select: { - id: true, - username: true, - Profile: { - select: { - id: true, - name: true, - imageId: true, - }, - }, - }, - }, - }, - }); + const data = await prisma.job.findMany({ + where: { + isActive: true, + isArsip: false, + MasterStatus: { + name: "Publish", + }, + title: { + contains: search || "", + mode: "insensitive", + }, + }, + orderBy: { + createdAt: "desc", + }, + select: { + id: true, + title: true, + deskripsi: true, + authorId: true, + MasterStatus: { + select: { + name: true, + }, + }, + Author: { + select: { + id: true, + username: true, + Profile: { + select: { + id: true, + name: true, + imageId: true, + }, + }, + }, + }, + }, + }); fixData = data; } diff --git a/src/lib/mobile/notification/send-notification.ts b/src/lib/mobile/notification/send-notification.ts new file mode 100644 index 00000000..faf8898e --- /dev/null +++ b/src/lib/mobile/notification/send-notification.ts @@ -0,0 +1,111 @@ +// lib/notifications/send-notification.ts +import { adminMessaging } from "@/lib/firebase-admin"; +import prisma from "@/lib/prisma"; +import { NotificationMobilePayload } from "../../../../types/type-mobile-notification"; + +/** + * Kirim notifikasi ke satu user (semua device aktifnya) + * @param recipientId - ID penerima + * @param senderId - ID pengirim + * @param payload - Data notifikasi + */ + +export async function sendNotificationMobileToOneUser({ + recipientId, + senderId, + payload, +}: { + recipientId: string; + senderId: string; + payload: NotificationMobilePayload; +}) { + try { + // 1. Simpan notifikasi ke DB + const notification = await prisma.notifikasi.create({ + data: { + title: payload.title, + pesan: payload.body, + deepLink: payload.deepLink, + kategoriApp: payload.kategoriApp, + recipientId: recipientId, + senderId: senderId, + }, + }); + + // 2. Ambil semua token aktif milik penerima + const tokens = await prisma.tokenUserDevice.findMany({ + where: { userId: recipientId, isActive: true }, + select: { token: true, id: true }, + }); + + if (tokens.length === 0) { + console.warn(`No active tokens found for user ${recipientId}`); + return; + } + + // 3. Kirim FCM ke semua token + + await Promise.allSettled( + tokens.map(async ({ token, id }) => { + try { + await adminMessaging.send({ + token, + notification: { + title: payload.title, + body: payload.body, + }, + data: { + sentAt: new Date().toISOString(), // ✅ Simpan metadata di data + id: notification.id, + deepLink: payload.deepLink, + }, + android: { + priority: "high" as const, + notification: { channelId: "default" }, + ttl: 0 as const, + }, + apns: { + payload: { aps: { sound: "default" as const } }, + }, + }); + } catch (fcmError: any) { + // Hapus token jika invalid + console.log("fcmError", fcmError); + if (fcmError.code === "messaging/invalid-registration-token") { + await prisma.tokenUserDevice.delete({ where: { id: id } }); + console.log(`❌ Invalid token removed: ${token}`); + } + console.error(`FCM failed for token ${token}:`, fcmError.message); + } + }) + ); + + console.log(`✅ Notification sent to user ${recipientId}`); + } catch (error) { + console.error("Failed to send notification:", error); + throw error; // biarkan caller handle error + } +} + +/** + * Kirim notifikasi ke banyak user + */ +export async function sendNotificationMobileToManyUser({ + recipientIds, + senderId, + payload, +}: { + recipientIds: string[]; + senderId: string; + payload: NotificationMobilePayload; +}) { + await Promise.allSettled( + recipientIds.map((id) => + sendNotificationMobileToOneUser({ + recipientId: id, + senderId: senderId, + payload: payload, + }) + ) + ); +} diff --git a/src/lib/mobile/route-page-mobile.ts b/src/lib/mobile/route-page-mobile.ts new file mode 100644 index 00000000..d9ba6084 --- /dev/null +++ b/src/lib/mobile/route-page-mobile.ts @@ -0,0 +1,14 @@ +export { routeAdminMobile, routeUserMobile }; + +type StatusApp = "review" | "draft" | "reject" | "publish"; + +const routeAdminMobile = { + userAccess: ({ id }: { id: string }) => `/admin/user-access/${id}`, + // JOB + jobDetail: ({ id, status }: { id: string; status: StatusApp }) => `/admin/job/${id}/${status}`, + jobByStatus: ({ status }: { status: StatusApp }) => `/admin/job/${status}/status`, +}; + +const routeUserMobile = { + home: `/(user)/home`, +}; diff --git a/types/type-mobile-notification.ts b/types/type-mobile-notification.ts new file mode 100644 index 00000000..b3c9884f --- /dev/null +++ b/types/type-mobile-notification.ts @@ -0,0 +1,20 @@ +export type NotificationMobilePayload = { + title: string; + body: string; + userLoginId?: string; + appId?: string; + status?: string; + type: "announcement" | "trigger"; + deepLink: string; + kategoriApp: TypeNotificationCategoryApp +}; + +export type TypeNotificationCategoryApp = + | "EVENT" + | "JOB" + | "VOTING" + | "DONASI" + | "INVESTASI" + | "COLLABORATION" + | "FORUM" + | "OTHER";