Notifikasi mobile job

Add:
 src/lib/mobile/
 types/type-mobile-notification.ts

Fix:
src/app/api/auth/mobile-register/route.ts
src/app/api/mobile/job/route.ts

### No Issue
This commit is contained in:
2026-01-06 17:52:28 +08:00
parent 44d6788f6e
commit 87515ae19f
5 changed files with 212 additions and 47 deletions

View File

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

View File

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

View File

@@ -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,
})
)
);
}

View File

@@ -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`,
};

View File

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