feat: Implementasi pagination pada endpoint mobile donation

4
      5 - Menambahkan pagination pada endpoint GET /api/mobile/donation
      6 - Menambahkan pagination pada endpoint GET /api/mobile/donation/[id]/news
      7 - Menambahkan pagination pada endpoint GET /api/mobile/donation/[id]/donatur
      8 - Memperbaiki validasi payload pada endpoint POST /api/mobile/auth/device-tokens
      9 - Menangani struktur payload yang bersarang dan langsung pada device token endpoint
     10 - Menambahkan informasi pagination ke dalam respons API

### NO Issue
This commit is contained in:
2026-02-10 17:31:39 +08:00
parent 934d6a3ef1
commit e89886e1db
8 changed files with 158 additions and 19 deletions

View File

@@ -1,5 +1,5 @@
File utama: src/app/api/mobile/donation/[id]/[status]/route.ts
File utama: src/app/api/mobile/donation/[id]/donatur/route.ts
Terapkan pagination pada file "File utama" pada method GET
Analisa juga file "File utama", jika belum memiliki page dari seachParams maka terapkan. Juga pastikan take dan skip sudah sesuai dengan pagination. Buat default nya menjadi 10 untuk take data
@@ -34,3 +34,12 @@ Buatkan auto input untuk method POST dengan data yang dibutuhkan sesuai dengan s
- imageId: number ( cm60j9q3m000xc9dc584v8rh8 )
Untuk sisa nya anda bisa bebas mengisi data tersebut.
<!-- COMMIT & PUSH -->
Branch: mobile-api/10-feb-26
Jalankan perintah ini: git checkout -b "Branch"
Setelah itu jalankan perintah ini: git add .
Setelah itu jalankan perintah ini: git commit -m "
<Berikan semua catatan perubahan pada branch ini, tampilan pada saya dan pastikan dalam bahasa indonesia. Saya akan cek baru saya akan berikan perintah push>
"
Setelah itu jalankan perintah ini: git push origin "Branch"

View File

@@ -2,7 +2,7 @@
## Project Overview
HIPMI (Himpunan Pengusaha Muda Indonesia) is a comprehensive Next.js-based web application built for the Indonesian Young Entrepreneurs Association. The project is a sophisticated platform that provides multiple business functionalities including investment management, donations, events, job listings, forums, voting systems, and collaborative projects.
HIPMI (Himpunan Pengusaha Muya Indonesia) is a comprehensive Next.js-based web application built for the Indonesian Young Entrepreneurs Association. The project is a sophisticated platform that provides multiple business functionalities including investment management, donations, events, job listings, forums, voting systems, and collaborative projects.
### Key Technologies
- **Framework**: Next.js 13+ (with App Router)

View File

@@ -4,13 +4,42 @@ import { prisma } from "@/lib";
export { POST, GET };
async function POST(request: NextRequest) {
const { data } = await request.json();
try {
// Parse the request body - can accept either nested under 'data' or directly
const requestBody = await request.json();
const { userId, platform, deviceId, model, appVersion, fcmToken } = data;
// Check if the data is nested under 'data' property (as described in the issue)
// or if it's directly in the request body (more common pattern)
const payload = requestBody.data ? requestBody.data : requestBody;
const { userId, platform, deviceId, model, appVersion, fcmToken } = payload;
// Validate required fields
if (!fcmToken) {
return NextResponse.json({ error: "Missing Token" }, { status: 400 });
return NextResponse.json(
{ error: "Missing FCM token", field: "fcmToken" },
{ status: 400 }
);
}
if (!userId) {
return NextResponse.json(
{ error: "Missing user ID", field: "userId" },
{ status: 400 }
);
}
// Verify that the user exists before creating/updating the device token
const userExists = await prisma.user.findUnique({
where: { id: userId },
select: { id: true }
});
if (!userExists) {
return NextResponse.json(
{ error: "User not found", field: "userId" },
{ status: 404 }
);
}
const existing = await prisma.tokenUserDevice.findFirst({
@@ -23,7 +52,6 @@ async function POST(request: NextRequest) {
},
});
console.log("✅ EX", existing);
let deviceToken;
@@ -31,7 +59,7 @@ async function POST(request: NextRequest) {
if (existing) {
deviceToken = await prisma.tokenUserDevice.update({
where: {
id: existing?.id,
id: existing.id,
},
data: {
platform,
@@ -43,7 +71,7 @@ async function POST(request: NextRequest) {
},
});
} else {
// Buat baru jika belum ada
// Create new device token record
deviceToken = await prisma.tokenUserDevice.create({
data: {
token: fcmToken,
@@ -58,9 +86,16 @@ async function POST(request: NextRequest) {
}
return NextResponse.json({ success: true, data: deviceToken });
} catch (error) {
} catch (error: any) {
console.error("Error registering device token:", error);
// Return more informative error response
return NextResponse.json(
{ error: (error as Error).message },
{
error: "Internal server error",
message: error.message || "An unexpected error occurred",
field: "server"
},
{ status: 500 }
);
}

View File

@@ -1,6 +1,7 @@
import _ from "lodash";
import { NextResponse } from "next/server";
import prisma from "@/lib/prisma";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { GET, PUT };
@@ -12,7 +13,7 @@ async function GET(
const fixStatus = _.startCase(status);
const { searchParams } = new URL(request.url);
const page = Number(searchParams.get("page")) || 1;
const takeData = 5
const takeData = PAGINATION_DEFAULT_TAKE
const skipData = page * takeData - takeData;
let fixData;

View File

@@ -1,5 +1,6 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { GET };
@@ -7,7 +8,7 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
const { id } = params;
const { searchParams } = new URL(request.url);
const page = Number(searchParams.get("page"));
const takeData = 5;
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page * takeData - takeData;
try {

View File

@@ -1,3 +1,4 @@
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
import prisma from "@/lib/prisma";
import { NextResponse } from "next/server";
@@ -9,11 +10,12 @@ export async function GET(
let fixData;
const { id } = params;
const { searchParams } = new URL(request.url);
const page = Number(searchParams.get("page"));
const takeData = 10;
const page = Number(searchParams.get("page")) || 1; // Default page 1 jika tidak ada atau invalid
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page * takeData - takeData;
fixData = await prisma.donasi_Invoice.findMany({
// Query data dengan pagination
const data = await prisma.donasi_Invoice.findMany({
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
orderBy: {
@@ -59,10 +61,31 @@ export async function GET(
},
});
// Hitung total data untuk pagination
const totalCount = await prisma.donasi_Invoice.count({
where: {
donasiId: id,
DonasiMaster_StatusInvoice: {
name: "Berhasil",
},
},
});
// Hitung total halaman
const totalPages = Math.ceil(totalCount / takeData);
fixData = data;
return NextResponse.json({
success: true,
message: "Data berhasil diambil",
data: fixData,
pagination: {
currentPage: page,
totalPages: totalPages,
totalData: totalCount,
dataPerPage: takeData,
},
});
} catch (error) {
return NextResponse.json({

View File

@@ -8,6 +8,7 @@ import {
} from "../../../../../../../types/type-mobile-notification";
import { routeUserMobile } from "@/lib/mobile/route-page-mobile";
import { funFindDonaturList } from "@/lib/mobile/donation/find-donatur-list";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { POST, GET, PUT, DELETE };
@@ -94,11 +95,16 @@ async function GET(
const { id } = params;
const { searchParams } = new URL(request.url);
const category = searchParams.get("category");
const page = Number(searchParams.get("page")) || 1; // Default page 1 jika tidak ada
const takeData = PAGINATION_DEFAULT_TAKE; // Default 10 data per halaman
const skipData = page * takeData - takeData;
let fixData;
let totalCount = 0; // Untuk menghitung total data
try {
if (category === "get-all") {
fixData = await prisma.donasi_Kabar.findMany({
const data = await prisma.donasi_Kabar.findMany({
orderBy: {
updatedAt: "desc",
},
@@ -106,6 +112,8 @@ async function GET(
donasiId: id,
active: true,
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
select: {
id: true,
title: true,
@@ -113,6 +121,17 @@ async function GET(
createdAt: true,
},
});
// Hitung total data untuk pagination
totalCount = await prisma.donasi_Kabar.count({
where: {
donasiId: id,
active: true,
},
});
fixData = data;
} else if (category === "get-one") {
const data = await prisma.donasi_Kabar.findUnique({
where: {
@@ -135,11 +154,24 @@ async function GET(
};
}
// Hitung total halaman jika kategori adalah get-all
let pagination = undefined;
if (category === "get-all") {
const totalPages = Math.ceil(totalCount / takeData);
pagination = {
currentPage: page,
totalPages: totalPages,
totalData: totalCount,
dataPerPage: takeData,
};
}
return NextResponse.json({
status: 200,
success: true,
message: "Berhasil mengambil kabar",
data: fixData,
pagination: pagination,
});
} catch (error) {
console.error("[ERROR GET NEWS]", error);

View File

@@ -4,6 +4,7 @@ import _ from "lodash";
import { NextResponse } from "next/server";
import { NotificationMobileBodyType } from "../../../../../types/type-mobile-notification";
import { routeAdminMobile } from "@/lib/mobile/route-page-mobile";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { POST, GET };
@@ -125,7 +126,12 @@ async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const category = searchParams.get("category");
const authorId = searchParams.get("authorId");
const page = Number(searchParams.get("page")) || 1; // Default page 1 jika tidak ada
const takeData = PAGINATION_DEFAULT_TAKE; // Default 10 data per halaman
const skipData = page * takeData - takeData;
let fixData;
let totalCount = 0; // Untuk menghitung total data
try {
if (category === "beranda") {
@@ -137,6 +143,8 @@ async function GET(request: Request) {
donasiMaster_StatusDonasiId: "1",
active: true,
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
select: {
id: true,
imageId: true,
@@ -152,6 +160,14 @@ async function GET(request: Request) {
},
});
// Hitung total data untuk pagination
totalCount = await prisma.donasi.count({
where: {
donasiMaster_StatusDonasiId: "1",
active: true,
},
});
fixData = data.map((v: any) => ({
..._.omit(v, ["DonasiMaster_Durasi"]),
durasiDonasi: v.DonasiMaster_Durasi.name,
@@ -164,6 +180,8 @@ async function GET(request: Request) {
where: {
authorId: authorId,
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
select: {
id: true,
nominal: true,
@@ -190,6 +208,13 @@ async function GET(request: Request) {
},
});
// Hitung total data untuk pagination
totalCount = await prisma.donasi_Invoice.count({
where: {
authorId: authorId,
},
});
fixData = data.map((v: any) => ({
..._.omit(v, ["DonasiMaster_StatusInvoice", "Donasi"]),
statusInvoice: v.DonasiMaster_StatusInvoice.name,
@@ -202,8 +227,21 @@ async function GET(request: Request) {
}));
}
// Hitung total halaman
const totalPages = Math.ceil(totalCount / takeData);
return NextResponse.json(
{ success: true, message: "Data berhasil diambil", data: fixData },
{
success: true,
message: "Data berhasil diambil",
data: fixData,
pagination: {
currentPage: page,
totalPages: totalPages,
totalData: totalCount,
dataPerPage: takeData,
}
},
{ status: 200 }
);
} catch (error) {