Compare commits

...

64 Commits

Author SHA1 Message Date
f8319b9ab5 Build with Github 2026-03-04 14:12:12 +08:00
2c1d74973b Fix bug
modified:   next.config.js
        modified:   src/app/api/mobile/forum/[id]/preview-report-comment/route.ts

### No Issue
2026-03-03 16:42:39 +08:00
04c2e0d580 chore(release): 1.6.6 2026-03-03 16:41:45 +08:00
bipproduction
6dba07baac fix: prisma connection exhaustion & firebase lazy init
- prisma/schema.prisma: tambah binaryTargets debian & linux-musl untuk Docker
- src/lib/prisma.ts: pakai global singleton di dev & prod, hapus eager $connect()
- src/lib/firebase-admin.ts: lazy initialization agar tidak crash saat build time
- .env.example: lengkap dengan semua env variable + connection_limit & pool_timeout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-03 16:26:48 +08:00
b76c7a4b1c Merge pull request 'bug-prisma 3' (#65) from bug-prisma/3-mar-26 into staging
Reviewed-on: #65
2026-03-03 15:36:43 +08:00
240f6eb7c2 Fix console 2026-03-03 15:34:36 +08:00
f64ae42825 Fix Prisma
1 fix: error koneksi Prisma dengan retry mechanism
      2
      3 Perubahan:
      4 - src/lib/prisma.ts: Tambah retry (3x) dengan exponential backoff saat connect
      5 - src/lib/prisma-retry.ts: NEW - Utility wrapper untuk retry operations
      6 - src/app/api/user-validate/route.ts: Improve error logging dengan detail
      7 - src/middleware.tsx: Clean up commented code
      8
      9 Fitur:
     10 - Auto retry saat database connection gagal
     11 - Explicit () di production
     12 - Better error logging untuk debugging
     13 - Reusable retry wrapper (withRetry, withTimeout)
     14
     15 Testing:
     16 - Build berhasil 
     17 - Type checking passed 
     18
     19 Fixes: Error in PostgreSQL connection: Error { kind: Closed, cause: None }

### No Issue
2026-03-03 15:30:34 +08:00
df5d1aad48 chore(release): 1.6.5 2026-03-03 15:25:39 +08:00
82e69309a1 Merge pull request 'bug-prisma 2' (#64) from bug-prisma/3-mar-26 into staging
Reviewed-on: #64
2026-03-03 14:19:10 +08:00
a6588818b5 fix: error koneksi Prisma - DATABASE_URL tidak loaded di
production
- Tambah validasi DATABASE_URL di prisma.ts
- Tambah copy .env file di postbuild script

### No Issue
2026-03-03 14:15:15 +08:00
58e1afaa45 chore(release): 1.6.4 2026-03-03 14:14:00 +08:00
f65f9b7834 Merge pull request 'bug-prisma' (#63) from bug-prisma/3-mar-26 into staging
Reviewed-on: #63
2026-03-03 12:11:04 +08:00
250b7c5261 Fix Prisma 2026-03-03 12:03:30 +08:00
935e519662 chore(release): 1.6.3 2026-03-03 11:55:25 +08:00
36b9248ed7 Merge pull request 'fix-bug middle' (#62) from fix-bug/25-feb-26 into staging
Reviewed-on: #62
2026-02-25 15:35:20 +08:00
c6dbd152d5 Fix middleware
### NO Issue
2026-02-25 15:34:22 +08:00
714cf5cd5a chore(release): 1.6.2 2026-02-25 15:22:40 +08:00
a2c5f053da Merge pull request 'fix-bug/25-feb-26' (#61) from fix-bug/25-feb-26 into staging
Reviewed-on: #61
2026-02-25 14:34:14 +08:00
a68343599d Fix type env
Fix:
- modified:   types/env.d.ts

### No Issue
2026-02-25 14:25:13 +08:00
419b87fc92 chore(release): 1.6.1 2026-02-25 12:00:20 +08:00
bedc0cc88b Merge pull request 'Delete termsOfServiceAccepted on register' (#60) from mobile-api/24-feb-26 into staging
Reviewed-on: #60
2026-02-25 10:46:17 +08:00
0271c87ba9 Delete termsOfServiceAccepted on register
### No issue
2026-02-24 18:04:05 +08:00
a9fbd544e5 Merge pull request 'mobile-api/24-feb-26' (#59) from mobile-api/24-feb-26 into staging
Reviewed-on: #59
2026-02-24 07:42:44 +08:00
5551f30721 Fix API and clear code
modified:   src/app/api/auth/register/route.ts
 modified:   src/app_modules/auth/login/view.tsx

### No Issue
2026-02-24 07:38:44 +08:00
00d36454d1 chore(release): 1.6.0 2026-02-24 07:33:28 +08:00
a762fbe9b1 Fix Api Mobile
API – Admin Forum
- src/app/api/mobile/admin/forum/route.ts
- src/app/api/mobile/admin/forum/[id]/comment/route.ts
- src/app/api/mobile/admin/forum/[id]/report-posting/route.ts

Docs
- PROMPT-AI.md

### No Issue
2026-02-20 16:47:28 +08:00
a98ab18423 Fix API Mobile
API – Admin Forum & Investment
- src/app/api/mobile/admin/forum/route.ts
- src/app/api/mobile/admin/investment/route.ts
- src/app/api/mobile/admin/investment/[id]/investor/route.ts

Docs
- PROMPT-AI.md

### No Issue
2026-02-19 16:44:17 +08:00
9afd741d4f Merge pull request 'Fix Admin API Mobile' (#58) from mobile-api/18-feb-26 into staging
Reviewed-on: #58
2026-02-18 17:33:11 +08:00
1c227a2850 Fix Admin API Mobile
API – Admin Donation
- src/app/api/mobile/admin/donation/[id]/disbursement/route.ts
- src/app/api/mobile/admin/donation/[id]/donatur/route.ts
- src/app/api/mobile/admin/donation/route.ts

API – Master Data (Admin)
- src/app/api/mobile/admin/master/donation/route.ts
- src/app/api/mobile/admin/master/type-of-event/route.ts

API – Admin Voting
- src/app/api/mobile/admin/voting/route.ts

Docs
- PROMPT-AI.md
- QWEN.md

Deleted
- CHANGELOG_BRANCH.md

### No Issue
2026-02-18 17:22:54 +08:00
817919f8f7 Merge pull request '### Fitur: Penambahan Pagination pada Endpoint Admin Mobile' (#57) from mobile-api/14-feb-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/57
2026-02-14 16:25:39 +08:00
5bdb998d2e ### Fitur: Penambahan Pagination pada Endpoint Admin Mobile
#### Deskripsi Umum
Telah dilakukan penambahan fitur pagination pada beberapa endpoint admin mobile untuk meningkatkan kinerja dan pengalaman pengguna saat mengakses data dalam jumlah besar.

#### File yang Diubah

1. **src/app/api/mobile/admin/job/route.ts**
   - Ditambahkan parameter  dari
   - Diterapkan logika pagination dengan  (default 10) dan
   - Query  telah dimodifikasi untuk mendukung pagination

2. **src/app/api/mobile/admin/event/route.ts**
   - Diperbaiki definisi variabel  untuk memastikan tipe data yang konsisten
   - Ditambahkan default value 1 untuk parameter
   - Perhitungan  disesuaikan agar lebih efisien

3. **src/app/api/mobile/admin/event/[id]/participants/route.ts**
   - Ditambahkan parameter  dari
   - Diterapkan logika pagination dengan  (default 10) dan
   - Query  telah dimodifikasi untuk mendukung pagination

#### Tujuan Perubahan
- Meningkatkan kinerja aplikasi saat mengambil data dalam jumlah besar
- Memungkinkan pengguna untuk mengakses data secara bertahap melalui halaman-halaman
- Mengurangi beban server saat mengambil data dalam jumlah besar
- Memberikan pengalaman pengguna yang lebih baik saat mengakses data admin

#### Cara Penggunaan
Untuk menggunakan fitur pagination, cukup tambahkan parameter  pada query string saat melakukan permintaan ke endpoint yang telah dimodifikasi. Contoh:

Default jumlah data per halaman adalah 10 item.

### No Issue
2026-02-14 15:36:09 +08:00
90031e23ef Merge pull request 'Fix Api Mobile' (#56) from mobile-api/13-feb-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/56
2026-02-13 17:41:31 +08:00
b585aa3024 Fix Api Mobile
API – Admin Master Data
- src/app/api/mobile/admin/master/bank/route.ts
- src/app/api/mobile/admin/master/business-field/route.ts
- src/app/api/mobile/admin/master/business-field/[id]/route.ts

Docs
- PROMPT-AI.md

### No Issue
2026-02-13 17:40:25 +08:00
596ebd2ff4 Merge pull request 'mobile-api/12-feb-26' (#55) from mobile-api/12-feb-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/55
2026-02-12 17:49:00 +08:00
a8f9d2ac0d Fix API Mobile Admin
API – Admin User (Mobile)
- src/app/api/mobile/admin/user/route.ts

Docs
- PROMPT-AI.md

### No Issue
2026-02-12 17:42:06 +08:00
d43f3762a3 Fixed Bug Server
## Summary
This branch contains several bug fixes and performance improvements, primarily focusing on:
- Database connection management
- MQTT client stability
- Logging optimization
- API enhancements

## Detailed Changes

### Fixed Issues
1. **Database Connection Management**
   - Removed  from user-validate API route to prevent connection pool exhaustion
   - Added proper connection handling in global Prisma setup
   - Reduced logging verbosity in production environments

2. **MQTT Client Improvements**
   - Enhanced MQTT client initialization with proper error handling
   - Added reconnection logic with configurable intervals
   - Implemented cleanup functions to prevent memory leaks
   - Added separate initialization logic for server and client-side code

3. **Logging Optimization**
   - Removed excessive logging in middleware that was causing high CPU usage
   - Configured appropriate log levels for development and production

4. **Component Stability**
   - Added safety checks in text editor component to prevent MQTT operations on the server side
   - Improved MQTT publishing logic with client availability checks

### New Files
-  - Utility functions for safe database operations

### Modified Files
1.
   - Removed problematic  call

2.
   - Configured different logging levels for dev/prod
   - Removed process listeners that were causing disconnections
   - Exported prisma instance separately

3.
   - Removed excessive logging statements

4.
   - Enhanced initialization with error handling
   - Added reconnection and timeout configurations

5.
   - Added proper cleanup functions
   - Improved connection handling

6.
   - Added MQTT client availability checks
   - Prevented server-side MQTT operations

### Performance Improvements
- Reduced database connection overhead
- Optimized MQTT connection handling
- Eliminated unnecessary logging in production
- Better memory management with proper cleanup functions

### No Issue
2026-02-12 16:29:03 +08:00
aa700523ca Merge pull request 'feat: Implementasi pagination pada endpoint mobile donation' (#54) from mobile-api/10-feb-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/54
2026-02-10 17:36:02 +08:00
e89886e1db 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
2026-02-10 17:31:39 +08:00
236ab4d4a4 Merge pull request 'Mobile API' (#53) from mobile-api/9-feb-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/53
2026-02-09 17:38:52 +08:00
934d6a3ef1 feat: update mobile donation API and related dependencies
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-02-09 17:34:49 +08:00
a7694bd7d5 feat: Tambahkan pagination pada API mobile investasi
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

Deskripsi:

- Tambahkan pagination pada endpoint investor/route.ts

- Tambahkan pagination pada endpoint news/route.ts untuk kategori all-news

- Gunakan konstanta PAGINATION_DEFAULT_TAKE untuk jumlah data per halaman

- Tambahkan metadata pagination (currentPage, totalData, totalPage, dataPerPage)

Fixes #issue-number
2026-02-09 15:08:33 +08:00
eaa7692359 Merge pull request 'Fix API mobile Investment' (#52) from mobile-api/6-feb-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/52
2026-02-06 17:39:36 +08:00
3b0ea3d847 chore(release): 1.5.40 2026-02-06 17:38:19 +08:00
097758a431 Fix API mobile Investment
API – Investment (Mobile)
- src/app/api/mobile/investment/route.ts
- src/app/api/mobile/investment/[id]/[status]/route.ts
- src/app/api/mobile/investment/[id]/invoice/route.ts
- src/app/api/mobile/investment/[id]/document/route.ts

Docs
- PROMPT-AI.md

### No issue
2026-02-06 17:37:47 +08:00
d51ce346e6 Merge pull request 'Fix API mobile' (#51) from mobile-api/5-feb-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/51
2026-02-05 17:36:40 +08:00
6f5849aa29 Fix API mobile
API – Voting (User)
- src/app/api/mobile/voting/route.ts
- src/app/api/mobile/voting/[id]/[status]/route.ts
- src/app/api/mobile/voting/[id]/contribution/route.ts

Docs
- PROMPT-AI.md

### No Issue
2026-02-05 17:35:49 +08:00
91f4bb6c9e Merge pull request 'mobile-api/4-jan-26' (#50) from mobile-api/4-jan-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/50
2026-02-05 10:11:03 +08:00
6aceb212e4 Fix APi voting mobile laod data
API – Event (Mobile)
- src/app/api/mobile/event/route.ts
- src/app/api/mobile/event/[id]/[status]/route.ts
- src/app/api/mobile/event/[id]/participants/route.ts

API – Voting (Mobile)
- src/app/api/mobile/voting/[id]/[status]/route.ts

Docs
- PROMPT-AI.md

### No Issue
2026-02-04 17:49:32 +08:00
42803f9b92 Fix api load data event dan notifikasi
API – Event (Mobile)
- src/app/api/mobile/event/route.ts
- src/app/api/mobile/event/[id]/[status]/route.ts

API – Notification (Mobile)
- src/app/api/mobile/notification/[id]/route.ts

Docs / Experiment
- PROMPT-AI.md
- zCoba.js

### No issue
2026-02-03 17:50:07 +08:00
1fe0001994 Merge pull request 'Fix API Job untuk loaddata:' (#49) from mobile-api/2-feb-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/49
2026-02-02 17:11:32 +08:00
2a857f54e7 Fix API Job untuk loaddata:
API – Job (Mobile)
- src/app/api/mobile/job/route.ts
- src/app/api/mobile/job/[id]/[status]/route.ts

Docs
- PROMPT-AI.md

Constants
- src/lib/constans-value/

### No Issue
2026-02-02 17:03:51 +08:00
b82a283731 Merge pull request 'mobile-api for load data' (#48) from mobile-api/30-jan-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/48
2026-01-30 17:20:09 +08:00
bb79a68f44 API – Mobile Notification
- src/app/api/mobile/notification/[id]/route.ts

API – Portofolio (Mobile)
- src/app/api/mobile/portofolio/route.ts

Untracked Files
- PROMPT-AI.md
- QWEN.md

### No Issue
2026-01-30 17:16:33 +08:00
f103ae93ad chore(release): 1.5.39 2026-01-30 17:16:05 +08:00
6d7d0fd07e Merge pull request 'mobile-notification done' (#46) from mobile-notification/27-jan-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/46
2026-01-27 17:00:26 +08:00
bc80bb3441 Merge pull request 'Notification Donasi & EULA on login' (#45) from mobile-notification/23-jan-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/45
2026-01-23 17:06:56 +08:00
8ab94b9c86 Merge pull request 'mobile-notification invesment' (#44) from mobile-notification/21-jan-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/44
2026-01-21 15:41:18 +08:00
6e37b18e42 Merge pull request 'mobile-notification report comment' (#43) from mobile-notification/19-jan-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/43
2026-01-19 17:54:21 +08:00
a6db03d0b4 Merge pull request 'mobile-notification event dan voting' (#42) from mobile-notification/15-jan-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/42
2026-01-15 17:41:58 +08:00
c550a4e922 Merge pull request 'mobile-notification try to push to apple and android preview' (#41) from mobile-notification/12-jan-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/41
2026-01-15 17:41:13 +08:00
2431a3fa3e Merge pull request 'Mobile notification & EULA route' (#40) from mobile-notification/9-jan-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/40
2026-01-09 17:47:40 +08:00
1ed0da8c7d Merge pull request 'Fix API mobile notifikasi untuk job' (#39) from mobile-notification/7-jan-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/39
2026-01-08 15:26:19 +08:00
e15a5d796d Merge pull request 'mobile notification' (#38) from mobile-notification/6-jan-26 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/38
2026-01-06 17:53:14 +08:00
836ebfaef0 Merge pull request 'mobile notification API' (#37) from mobile-notification/5-jan-25 into staging
Reviewed-on: http://wibugit.wibudev.com/wibu/hipmi/pulls/37
2026-01-05 14:06:58 +08:00
69 changed files with 1917 additions and 522 deletions

53
.env.example Normal file
View File

@@ -0,0 +1,53 @@
# ==============================
# Database
# ==============================
# Tambahkan connection_limit dan pool_timeout untuk mencegah connection exhaustion
DATABASE_URL="postgresql://user:password@localhost:5432/dbname?connection_limit=10&pool_timeout=20"
# ==============================
# Auth / Session
# ==============================
WIBU_PWD="your_wibu_password"
NEXT_PUBLIC_BASE_TOKEN_KEY="your_token_key"
NEXT_PUBLIC_BASE_SESSION_KEY="your_session_key"
# ==============================
# Payment Gateway (Midtrans)
# ==============================
Client_KEY="your_midtrans_client_key"
Server_KEY="your_midtrans_server_key"
# ==============================
# Maps
# ==============================
MAPBOX_TOKEN="your_mapbox_token"
# ==============================
# Realtime (WebSocket)
# ==============================
WS_APIKEY="your_ws_api_key"
NEXT_PUBLIC_WIBU_REALTIME_TOKEN="your_realtime_token"
# ==============================
# Email (Resend)
# ==============================
RESEND_APIKEY="your_resend_api_key"
# ==============================
# WhatsApp
# ==============================
WA_SERVER_TOKEN="your_wa_server_token"
# ==============================
# Firebase Admin (Push Notification)
# ==============================
FIREBASE_ADMIN_PROJECT_ID="your_firebase_project_id"
FIREBASE_ADMIN_CLIENT_EMAIL="your_firebase_client_email@project.iam.gserviceaccount.com"
# Private key: salin dari service account JSON, ganti newline dengan \n
FIREBASE_ADMIN_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\nYOUR_PRIVATE_KEY_HERE\n-----END PRIVATE KEY-----\n"
# ==============================
# App
# ==============================
NEXT_PUBLIC_API_URL="http://localhost:3000"
LOG_LEVEL="info"

59
.github/workflows/publish.yml vendored Normal file
View File

@@ -0,0 +1,59 @@
name: Publish Docker to GHCR
on:
workflow_dispatch:
inputs:
tag:
description: "Image tag (e.g. v1.0.0)"
required: true
default: "latest"
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
publish:
name: Build & Push to GHCR
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Free disk space
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /usr/local/lib/android
sudo rm -rf /opt/ghc
sudo rm -rf /opt/hostedtoolcache/CodeQL
sudo docker image prune --all --force
df -h
- name: Checkout repository
uses: actions/checkout@v4
- name: Extract tag name
id: meta
run: echo "tag=${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
push: true
platforms: linux/amd64
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.tag }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -2,6 +2,41 @@
All notable changes to this project will be documented in this file. See [commit-and-tag-version](https://github.com/absolute-version/commit-and-tag-version) for commit guidelines.
## [1.6.6](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.6.5...v1.6.6) (2026-03-03)
### Bug Fixes
* prisma connection exhaustion & firebase lazy init ([6dba07b](https://wibugit.wibudev.com/wibu/hipmi/commit/6dba07baac6a3ef7d264c13e378a905002401e1b))
## [1.6.5](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.6.4...v1.6.5) (2026-03-03)
### Bug Fixes
* error koneksi Prisma - DATABASE_URL tidak loaded di ([a658881](https://wibugit.wibudev.com/wibu/hipmi/commit/a6588818b5d8018b3a634e0ae0846e309569d370))
## [1.6.4](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.6.3...v1.6.4) (2026-03-03)
## [1.6.3](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.6.2...v1.6.3) (2026-03-03)
## [1.6.2](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.6.1...v1.6.2) (2026-02-25)
## [1.6.1](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.6.0...v1.6.1) (2026-02-25)
## [1.6.0](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.5.40...v1.6.0) (2026-02-23)
### Features
* Implementasi pagination pada endpoint mobile donation ([e89886e](https://wibugit.wibudev.com/wibu/hipmi/commit/e89886e1dbc8cb4d95e6cc7c2787fb22a1dcaf56))
* Tambahkan pagination pada API mobile investasi ([a7694bd](https://wibugit.wibudev.com/wibu/hipmi/commit/a7694bd7d5d72b6499443faf99301faca730d3ed))
* update mobile donation API and related dependencies ([934d6a3](https://wibugit.wibudev.com/wibu/hipmi/commit/934d6a3ef1015367bee85779796df4f11c5e779c))
## [1.5.40](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.5.39...v1.5.40) (2026-02-06)
## [1.5.39](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.5.38...v1.5.39) (2026-01-30)
## [1.5.38](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.5.37...v1.5.38) (2026-01-27)
## [1.5.37](https://wibugit.wibudev.com/wibu/hipmi/compare/v1.5.36...v1.5.37) (2026-01-23)

39
CHANGELOG_COMMIT.md Normal file
View File

@@ -0,0 +1,39 @@
## Catatan Perubahan untuk Commit
### Fitur: Penambahan Pagination pada Endpoint Admin Mobile
#### Deskripsi Umum
Telah dilakukan penambahan fitur pagination pada beberapa endpoint admin mobile untuk meningkatkan kinerja dan pengalaman pengguna saat mengakses data dalam jumlah besar.
#### File yang Diubah
1. **src/app/api/mobile/admin/job/route.ts**
- Ditambahkan parameter `page` dari `searchParams`
- Diterapkan logika pagination dengan `takeData` (default 10) dan `skipData`
- Query `prisma.job.findMany` telah dimodifikasi untuk mendukung pagination
2. **src/app/api/mobile/admin/event/route.ts**
- Diperbaiki definisi variabel `page` untuk memastikan tipe data yang konsisten
- Ditambahkan default value 1 untuk parameter `page`
- Perhitungan `skipData` disesuaikan agar lebih efisien
3. **src/app/api/mobile/admin/event/[id]/participants/route.ts**
- Ditambahkan parameter `page` dari `searchParams`
- Diterapkan logika pagination dengan `takeData` (default 10) dan `skipData`
- Query `prisma.event_Peserta.findMany` telah dimodifikasi untuk mendukung pagination
#### Tujuan Perubahan
- Meningkatkan kinerja aplikasi saat mengambil data dalam jumlah besar
- Memungkinkan pengguna untuk mengakses data secara bertahap melalui halaman-halaman
- Mengurangi beban server saat mengambil data dalam jumlah besar
- Memberikan pengalaman pengguna yang lebih baik saat mengakses data admin
#### Cara Penggunaan
Untuk menggunakan fitur pagination, cukup tambahkan parameter `page` pada query string saat melakukan permintaan ke endpoint yang telah dimodifikasi. Contoh:
```
GET /api/mobile/admin/job?page=2
GET /api/mobile/admin/event?page=3
GET /api/mobile/admin/event/{id}/participants?page=1
```
Default jumlah data per halaman adalah 10 item.

72
Dockerfile Normal file
View File

@@ -0,0 +1,72 @@
# ==============================
# Stage 1: Builder
# ==============================
FROM node:20-bookworm-slim AS builder
WORKDIR /app
# Install system deps
RUN apt-get update && apt-get install -y --no-install-recommends \
libc6 \
git \
openssl \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Copy dependency files first (for better caching)
COPY package.json package-lock.json* bun.lockb* ./
ENV SHARP_IGNORE_GLOBAL_LIBVIPS=1
ENV NEXT_TELEMETRY_DISABLED=1
ENV NODE_OPTIONS="--max-old-space-size=4096"
# 🔥 Skip postinstall scripts (fix onnxruntime error)
RUN npm install --legacy-peer-deps --ignore-scripts
# Copy full source
COPY . .
# Use .env.example as build env
# (Pastikan file ini ada di project)
RUN cp .env.example .env || true
# Generate Prisma Client
ENV PRISMA_CLI_BINARY_TARGETS=debian-openssl-3.0.x
RUN npx prisma generate
# Build Next.js
RUN npm run build
# ==============================
# Stage 2: Runner (Production)
# ==============================
FROM node:20-bookworm-slim AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN apt-get update && apt-get install -y --no-install-recommends \
openssl \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
RUN groupadd --system --gid 1001 nodejs \
&& useradd --system --uid 1001 --gid nodejs nextjs
# Copy standalone output
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
COPY --from=builder /app/public ./public
RUN chown -R nextjs:nodejs /app
USER nextjs
EXPOSE 3000
ENV PORT=3000
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]

45
PROMPT-AI.md Normal file
View File

@@ -0,0 +1,45 @@
File utama: src/app/api/mobile/admin/forum/[id]/comment/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
Contoh:
const page = Number(searchParams.get("page"));
const takeData = 10;
const skipData = page * takeData - takeData;
dan penerapannya pada query
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
Gunakan bahasa indonesia pada cli agar saya mudah membacanya.
<!-- Additinal prompt -->
File refrensi: src/app/api/mobile/event/[id]/[status]/route.ts
Anda bisa menggunakan refrensi dari "File refrensi" jika butuh pemahaman dengan tipe fitur yang sama
<!-- Auto input promt -->
End-point: src/app/api/mobile/donation/route.ts
Buatkan auto input untuk method POST dengan data yang dibutuhkan sesuai dengan struktur data yang ada di file tersebut, tidak perlu "temporary" cukup data "permanent" dengan ketentuan sebagai berikut:
- authorId: string ( cmha6wb9w0001cfndwl9fcse6 )
- title: string
- target: number
- donasiMaster_DurasiId: number ( 3 )
- donasiMaster_KategoriId: number ( 3 )
- namaBank: string
- rekening: string
- 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"

201
QWEN.md Normal file
View File

@@ -0,0 +1,201 @@
# HIPMI Project - QWEN.md
## Project Overview
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)
- **Language**: TypeScript
- **Database**: PostgreSQL with Prisma ORM
- **Styling**: Tailwind CSS with Mantine UI components
- **Authentication**: JWT-based with custom middleware
- **Runtime**: Bun (instead of Node.js)
- **Deployment**: Standalone output configuration
### Team Structure
- **bagas**: Frontend, DevOps
- **lukman**: Frontend, UI
- **lia**: Backend, Frontend, QC
- **malik**: Leader
## Architecture & Features
### Core Modules
The application is organized into several major functional areas:
1. **Authentication System** (`/auth`) - Login, registration, validation
2. **Investment Platform** (`/investasi`) - Investment creation, trading, portfolio management
3. **Donation System** (`/donasi`) - Fundraising campaigns, donation processing
4. **Event Management** (`/event`) - Event creation, participation, management
5. **Voting System** (`/vote`) - Voting creation and participation
6. **Job Board** (`/job`) - Job posting and application system
7. **Forum** (`/forum`) - Discussion platform with reporting features
8. **Collaboration Platform** (`/colab`) - Project collaboration tools
9. **User Profiles** (`/profile`) - User profile management
10. **Business Maps** (`/maps`) - Business location mapping
### Admin Panel
The application includes a comprehensive admin panel (`/admin`) with modules for managing:
- Investment approvals and transfers
- Donation campaign reviews
- Event management
- Forum moderation
- Job posting reviews
- User access management
- Voting oversight
### Technical Architecture
#### Routing & Authentication
- Custom middleware handles authentication and authorization
- Public routes are defined in the middleware configuration
- JWT tokens are stored in cookies with secure options
- Role-based access control (MasterUserRole model)
#### Database Schema
The Prisma schema defines a comprehensive data model with:
- User management with roles and profiles
- Investment and financial systems
- Donation and crowdfunding features
- Event management
- Forum and discussion systems
- Collaboration platforms
- Notification systems
- Business mapping features
#### Environment Configuration
The application uses multiple environment files for different deployment stages:
- Development: `run.env.dev`, `run.env.local.dev`
- Build: `run.env.build.dev`, `run.env.build.local`
- Runtime: `run.env.start.dev`, `run.env.start.local`
## Building and Running
### Prerequisites
- Bun runtime installed
- PostgreSQL database
- Required environment variables configured
### Setup Commands
```bash
# Install dependencies
bun install
# Setup database (Prisma)
bun prisma generate
bun prisma db push
bun prisma db seed
# Development server
bun run dev
# Build for production
bun run build
# Start production server
bun run start
```
### Development Scripts
- `dev`: Starts development server with HTTPS
- `build`: Builds the application for production
- `start`: Starts the production server
- `lint`: Runs Next.js linting
- `ver`: Creates version tags
## Development Conventions
### Git Workflow
The team follows a structured Git workflow:
1. Check status with `git status`
2. Add specific files with `git add <file>` (avoid `git add -A`)
3. Commit with structured messages following conventional commits:
- `feat`: New features
- `fix`: Bug fixes
- `docs`: Documentation updates
- `chore`: Routine tasks
- `refactor`: Code restructuring
- `test`: Testing additions
- `style`: Styling changes
- `perf`: Performance improvements
### Code Standards
- TypeScript with strict mode enabled
- Component header comments with file description, creator, date
- Function comments with parameter and return value descriptions
- Custom type interface comments
- Error handling comments
- Complex logic comments
### Commit Message Format
```
type: Short description
Body:
- Detailed description of changes
- Motivation for changes
- Breaking changes if any
References: #issue-number
```
## Project Structure
### Main Directories
- `src/app`: Next.js App Router pages and layouts
- `src/app_modules`: Reusable application modules
- `src/lib`: Shared libraries and utilities
- `src/util`: Utility functions
- `prisma`: Database schema and migrations
- `public`: Static assets
- `certificates`, `logs`: Application data directories
### Key Files
- `package.json`: Dependencies and scripts
- `next.config.js`: Next.js configuration
- `middleware.tsx`: Authentication and routing middleware
- `tsconfig.json`: TypeScript configuration
- `tailwind.config.js`: Styling configuration
- `gen_page.tsx`: Generated page routing utility
- `prisma/schema.prisma`: Database schema definition
## Special Features
### Real-time Capabilities
- WebSocket integration for real-time updates
- MQTT support for messaging
- Live notifications system
### Payment Integration
- Midtrans payment gateway for investments
- Multiple payment methods
- Invoice generation and tracking
### File Handling
- PDF generation and viewing
- Image processing and storage
- Document management for investments
### Mobile Support
- Responsive design
- Mobile-specific features
- Device token management
## Security Measures
### Authentication
- JWT-based authentication
- Session management
- Role-based access control
- Secure token storage in cookies
### Input Validation
- Prisma schema validations
- Server-side validation
- Sanitization of user inputs
### Data Protection
- Encrypted tokens
- Secure API routes
- Proper CORS configuration

View File

@@ -39,6 +39,7 @@
"@types/react-dom": "18.2.7",
"@types/uuid": "^9.0.4",
"autoprefixer": "10.4.14",
"axios": "^1.13.5",
"bufferutil": "^4.0.8",
"bun": "^1.1.38",
"caniuse-lite": "^1.0.30001757",
@@ -1324,7 +1325,7 @@
"axe-core": ["axe-core@4.10.2", "", {}, "sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w=="],
"axios": ["axios@0.26.1", "", { "dependencies": { "follow-redirects": "^1.14.8" } }, "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA=="],
"axios": ["axios@1.13.5", "", { "dependencies": { "follow-redirects": "^1.15.11", "form-data": "^4.0.5", "proxy-from-env": "^1.1.0" } }, "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q=="],
"axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="],
@@ -1882,7 +1883,7 @@
"fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="],
"follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="],
"follow-redirects": ["follow-redirects@1.15.11", "", {}, "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ=="],
"fontfaceobserver": ["fontfaceobserver@2.3.0", "", {}, "sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg=="],
@@ -1892,7 +1893,7 @@
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
"form-data": ["form-data@4.0.2", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.12" } }, "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w=="],
"form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="],
"form-data-encoder": ["form-data-encoder@1.7.2", "", {}, "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A=="],
@@ -3618,6 +3619,8 @@
"@types/jsonwebtoken/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="],
"@types/node-fetch/form-data": ["form-data@4.0.2", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.12" } }, "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w=="],
"@types/request/@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="],
"@types/request/form-data": ["form-data@2.5.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.35", "safe-buffer": "^5.2.1" } }, "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A=="],
@@ -3630,6 +3633,8 @@
"ast-types/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"autocannon/form-data": ["form-data@4.0.2", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.12" } }, "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w=="],
"autoprefixer/caniuse-lite": ["caniuse-lite@1.0.30001701", "", {}, "sha512-faRs/AW3jA9nTwmJBSO1PQ6L/EOgsB5HMQQq4iCu5zhPgVVgO/pZRHlmatwijZKetFw8/Pr4q6dEN8sJuq8qTw=="],
"babel-plugin-polyfill-corejs2/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
@@ -3810,6 +3815,8 @@
"metro-file-map/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"midtrans-client/axios": ["axios@0.26.1", "", { "dependencies": { "follow-redirects": "^1.14.8" } }, "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA=="],
"minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
@@ -4190,6 +4197,12 @@
"metro/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"midtrans-client/axios/follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="],
"next-dev/axios/follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="],
"next-dev/axios/form-data": ["form-data@4.0.2", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.12" } }, "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w=="],
"next-dev/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],
"next-scroll-loader/react-dom/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="],

View File

@@ -1,27 +1,21 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: false,
output: "standalone",
eslint: { ignoreDuringBuilds: true },
typescript: { ignoreBuildErrors: true },
experimental: {
serverActions: true,
serverComponentsExternalPackages: ["@prisma/client", ".prisma/client"],
},
output: "standalone",
staticPageGenerationTimeout: 180, // tingkatkan menjadi 3 menit
eslint: {
ignoreDuringBuilds: true,
webpack: (config, { isServer }) => {
if (isServer) {
config.externals = config.externals || [];
config.externals.push("@prisma/client");
config.externals.push(".prisma/client");
}
return config;
},
// async headers() {
// return [
// {
// source: "/(.*)",
// headers: [
// {
// key: "Cache-Control",
// value: "no-store, max-age=0",
// },
// ],
// },
// ];
// },
};
module.exports = nextConfig;
module.exports = nextConfig;

View File

@@ -1,15 +1,16 @@
{
"name": "hipmi",
"version": "1.5.38",
"version": "1.6.6",
"private": true,
"prisma": {
"seed": "bun prisma/seed.ts"
},
"scripts": {
"dev": "next dev --experimental-https",
"build": "next build",
"build:dev": "next build",
"build": "prisma generate && next build",
"build:dev": "prisma generate && next build",
"start": "next start",
"postbuild": "node scripts/postbuild.js",
"lint": "next lint",
"ver": "bunx commit-and-tag-version -- --prerelease"
},
@@ -50,6 +51,7 @@
"@types/react-dom": "18.2.7",
"@types/uuid": "^9.0.4",
"autoprefixer": "10.4.14",
"axios": "^1.13.5",
"bufferutil": "^4.0.8",
"bun": "^1.1.38",
"caniuse-lite": "^1.0.30001757",

View File

@@ -4,7 +4,7 @@
generator client {
provider = "prisma-client-js"
engineType = "binary"
binaryTargets = ["native"]
binaryTargets = ["native", "debian-openssl-3.0.x", "linux-musl-openssl-3.0.x"]
}
datasource db {

60
scripts/postbuild.js Normal file
View File

@@ -0,0 +1,60 @@
const fs = require('fs');
const path = require('path');
const standaloneDir = path.join(__dirname, '../.next/standalone');
const prismaDir = path.join(__dirname, '../node_modules/.prisma');
console.log('🚀 Running postbuild script...');
// Copy Prisma binaries ke standalone output
if (fs.existsSync(prismaDir)) {
const dest = path.join(standaloneDir, 'node_modules/.prisma');
fs.mkdirSync(path.dirname(dest), { recursive: true });
fs.cpSync(prismaDir, dest, { recursive: true });
console.log('✓ Prisma binaries copied to standalone output');
} else {
console.warn('⚠ Prisma binaries directory not found, skipping...');
}
// Copy schema.prisma jika diperlukan
const schemaSrc = path.join(__dirname, '../prisma/schema.prisma');
const schemaDest = path.join(standaloneDir, 'prisma/schema.prisma');
if (fs.existsSync(schemaSrc)) {
fs.mkdirSync(path.dirname(schemaDest), { recursive: true });
fs.copyFileSync(schemaSrc, schemaDest);
console.log('✓ schema.prisma copied to standalone output');
} else {
console.warn('⚠ schema.prisma not found, skipping...');
}
// Copy prisma client dari node_modules/@prisma/client
const prismaClientSrc = path.join(__dirname, '../node_modules/@prisma/client');
const prismaClientDest = path.join(standaloneDir, 'node_modules/@prisma/client');
if (fs.existsSync(prismaClientSrc)) {
fs.mkdirSync(path.dirname(prismaClientDest), { recursive: true });
fs.cpSync(prismaClientSrc, prismaClientDest, { recursive: true });
console.log('✓ @prisma/client copied to standalone output');
} else {
console.warn('⚠ @prisma/client not found, skipping...');
}
// Copy .env file jika ada (untuk production)
const envSrc = path.join(__dirname, '../.env');
const envDest = path.join(standaloneDir, '.env');
if (fs.existsSync(envSrc)) {
fs.copyFileSync(envSrc, envDest);
// console.log('✓ .env file copied to standalone output');
} else {
// console.warn('⚠ .env file not found, skipping...');
console.warn(' Pastikan DATABASE_URL di-set di system environment server!');
}
// Copy .env-local file jika ada (opsional)
const envLocalSrc = path.join(__dirname, '../.env-local');
const envLocalDest = path.join(standaloneDir, '.env-local');
if (fs.existsSync(envLocalSrc)) {
fs.copyFileSync(envLocalSrc, envLocalDest);
// console.log('✓ .env-local file copied to standalone output');
}
console.log('✅ Build script completed!');

View File

@@ -14,8 +14,6 @@ export async function POST(req: Request) {
try {
const { data } = await req.json();
console.log("data >>", data);
const cekUsername = await prisma.user.findUnique({
where: {
username: data.username,
@@ -29,12 +27,12 @@ export async function POST(req: Request) {
});
// ✅ Validasi wajib setuju Terms
if (data.termsOfServiceAccepted !== true) {
return NextResponse.json({
success: false,
message: "You must agree to the Terms of Service",
});
}
// if (data.termsOfServiceAccepted !== true) {
// return NextResponse.json({
// success: false,
// message: "You must agree to the Terms of Service",
// });
// }
const createUser = await prisma.user.create({
data: {

View File

@@ -10,6 +10,7 @@ import {
NotificationMobileTitleType,
} from "../../../../../../../../types/type-mobile-notification";
import { routeUserMobile } from "@/lib/mobile/route-page-mobile";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { POST, GET };
@@ -154,7 +155,7 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
const { searchParams } = new URL(request.url);
const category = searchParams.get("category");
const page = searchParams.get("page");
const takeData = 10;
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = Number(page) * takeData - takeData;
console.log("[CATEGORY]", category);
@@ -174,6 +175,7 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
id: true,
createdAt: true,
nominalCair: true,
title: true,
},
});
} else if (category === "get-one") {

View File

@@ -1,6 +1,7 @@
import _ from "lodash";
import { NextResponse } from "next/server";
import { prisma } from "@/lib";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { GET };
@@ -9,7 +10,7 @@ async function GET(req: Request, { params }: { params: { id: string } }) {
const { searchParams } = new URL(req.url);
const page = searchParams.get("page");
const status = searchParams.get("status");
const takeData = 10;
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = Number(page) * takeData - takeData;
const fixStatus = _.startCase(status || "");

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 };
@@ -9,11 +10,10 @@ async function GET(request: Request) {
const category = searchParams.get("category");
const page = searchParams.get("page");
const search = searchParams.get("search");
const takeData = 10;
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = Number(page) * takeData - takeData;
console.log("[CATEGORY]", category);
let fixData;
try {
if (category === "dashboard") {
const publish = await prisma.donasi.count({
@@ -48,7 +48,7 @@ async function GET(request: Request) {
where: {
active: true,
},
}
},
);
const categoryDonation = countCategoryDonation.length;
@@ -68,7 +68,6 @@ async function GET(request: Request) {
},
});
console.log("[STATUS]", checkStatus);
if (!checkStatus) {
return NextResponse.json(
@@ -77,7 +76,7 @@ async function GET(request: Request) {
message: "Failed to get data donation",
reason: "Status not found",
},
{ status: 500 }
{ status: 500 },
);
}
@@ -100,6 +99,12 @@ async function GET(request: Request) {
select: {
id: true,
title: true,
target: true,
DonasiMaster_Durasi: {
select: {
name: true,
},
},
Author: {
select: {
id: true,
@@ -109,7 +114,6 @@ async function GET(request: Request) {
},
});
console.log("[LIST]", fixData);
}
return NextResponse.json(
@@ -118,7 +122,7 @@ async function GET(request: Request) {
message: `Success get data donation ${category}`,
data: fixData,
},
{ status: 200 }
{ status: 200 },
);
} catch (error) {
console.error("Error get data donation:", error);
@@ -128,7 +132,7 @@ async function GET(request: Request) {
message: "Failed to get data donation",
reason: (error as Error).message,
},
{ status: 500 }
{ status: 500 },
);
}
}

View File

@@ -1,9 +1,15 @@
import { NextResponse } from "next/server";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
import prisma from "@/lib/prisma";
import { NextResponse } from "next/server";
export { GET };
async function GET(request: Request, { params }: { params: { id: string } }) {
const { searchParams } = new URL(request.url);
const page = Number(searchParams.get("page")) || 1;
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page * takeData - takeData;
try {
const { id } = params;
@@ -12,6 +18,7 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
eventId: id,
},
select: {
id: true,
eventId: true,
userId: true,
isPresent: true,
@@ -35,6 +42,8 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
},
},
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
});
return NextResponse.json(

View File

@@ -1,7 +1,8 @@
import _ from "lodash";
import { prisma } from "@/lib";
import { NextResponse } from "next/server";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
import _ from "lodash";
import moment from "moment";
import { NextResponse } from "next/server";
export { GET };
@@ -11,13 +12,12 @@ async function GET(request: Request) {
const fixStatus = _.startCase(category || "");
const search = searchParams.get("search");
const page = searchParams.get("page");
const takeData = 10;
const skipData = Number(page) * takeData - takeData;
const page = Number(searchParams.get("page")) || 1;
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page * takeData - takeData;
let fixData;
console.log("[CATEGORY]", category);
// console.log("[FIX STATUS]", fixStatus);
try {
if (category === "dashboard") {
@@ -71,7 +71,6 @@ async function GET(request: Request) {
typeOfEvent,
};
} else if (category === "history") {
console.log("[HISTORY HERE]");
const data = await prisma.event.findMany({
take: page ? takeData : undefined,
@@ -151,21 +150,22 @@ async function GET(request: Request) {
},
},
select: {
id: true,
title: true,
tanggal: true,
Author: {
select: {
id: true,
username: true,
Profile: {
select: {
name: true,
},
id: true,
title: true,
tanggal: true,
tanggalSelesai: true,
Author: {
select: {
id: true,
username: true,
Profile: {
select: {
name: true,
},
},
},
},
},
});
fixData = data;
@@ -177,7 +177,7 @@ async function GET(request: Request) {
message: `Success get data event ${category}`,
data: fixData,
},
{ status: 200 }
{ status: 200 },
);
} catch (error) {
console.log(`[ERROR GET DATA EVENT: ${category}]`, error);
@@ -187,7 +187,7 @@ async function GET(request: Request) {
message: `Error get data event ${category}`,
reason: (error as Error).message,
},
{ status: 500 }
{ status: 500 },
);
}
}

View File

@@ -7,6 +7,7 @@ import {
NotificationMobileTitleType,
} from "../../../../../../../../types/type-mobile-notification";
import { routeUserMobile } from "@/lib/mobile/route-page-mobile";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { GET, PUT };
@@ -14,9 +15,9 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
const { id } = params;
const { searchParams } = new URL(request.url);
const search = searchParams.get("search");
const page = searchParams.get("page");
const takeData = 10;
const skipData = Number(page) * takeData - takeData;
const page = Number(searchParams.get("page"));
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page * takeData - takeData;
const category = searchParams.get("category");
let fixData;
try {

View File

@@ -1,15 +1,16 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export async function GET(
request: Request,
{ params }: { params: { id: string } }
{ params }: { params: { id: string } },
) {
const { id } = params;
const { searchParams } = new URL(request.url);
const search = searchParams.get("search");
const page = searchParams.get("page");
const takeData = 10;
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = Number(page) * takeData - takeData;
let fixData;
@@ -60,7 +61,7 @@ export async function GET(
message: "Success get list report posting",
data: fixData,
},
{ status: 200 }
{ status: 200 },
);
} catch (error) {
console.error("[ERROR GET LIST REPORT POSTING]", error);
@@ -70,7 +71,7 @@ export async function GET(
message: "Error get list report posting",
reason: (error as Error).message,
},
{ status: 500 }
{ status: 500 },
);
}
}

View File

@@ -1,6 +1,7 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib";
import _ from "lodash";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { GET };
@@ -9,7 +10,7 @@ async function GET(request: Request, { params }: { params: { name: string } }) {
const category = searchParams.get("category");
const search = searchParams.get("search");
const page = searchParams.get("page");
const takeData = 10;
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = Number(page) * takeData - takeData;
let fixData;
@@ -79,7 +80,11 @@ async function GET(request: Request, { params }: { params: { name: string } }) {
_count: {
select: {
Forum_ReportPosting: true,
Forum_Komentar: true,
Forum_Komentar: {
where: {
isActive: true,
},
},
},
},
},
@@ -139,6 +144,14 @@ async function GET(request: Request, { params }: { params: { name: string } }) {
},
});
// Hitung count report untuk setiap Forum_Posting id
const countByPostingId = data.reduce((acc: any, item: any) => {
const key = item.Forum_Posting?.id;
if (!key) return acc;
acc[key] = (acc[key] || 0) + 1;
return acc;
}, {});
const filterLatest = (data: any) =>
Object.values(
data.reduce((acc: any, item: any) => {
@@ -151,10 +164,16 @@ async function GET(request: Request, { params }: { params: { name: string } }) {
acc[key] = item;
}
return acc;
}, {})
}, {}),
);
fixData = filterLatest(data);
const filteredData = filterLatest(data);
// Tambahkan count ke setiap item
fixData = filteredData.map((item: any) => ({
...item,
count: countByPostingId[item.Forum_Posting?.id] || 0,
}));
} else if (category === "report_comment") {
const data = await prisma.forum_ReportKomentar.findMany({
take: page ? takeData : undefined,
@@ -193,6 +212,14 @@ async function GET(request: Request, { params }: { params: { name: string } }) {
},
});
// Hitung count report untuk setiap Forum_Komentar id
const countByKomentarId = data.reduce((acc: any, item: any) => {
const key = item.Forum_Komentar?.id;
if (!key) return acc;
acc[key] = (acc[key] || 0) + 1;
return acc;
}, {});
const filterLatest = (data: any) =>
Object.values(
data.reduce((acc: any, item: any) => {
@@ -205,10 +232,16 @@ async function GET(request: Request, { params }: { params: { name: string } }) {
acc[key] = item;
}
return acc;
}, {})
}, {}),
);
fixData = filterLatest(data);
const filteredData = filterLatest(data);
// Tambahkan count ke setiap item
fixData = filteredData.map((item: any) => ({
...item,
count: countByKomentarId[item.Forum_Komentar?.id] || 0,
}));
} else {
return NextResponse.json(
{
@@ -216,7 +249,7 @@ async function GET(request: Request, { params }: { params: { name: string } }) {
message: "Invalid category",
reason: "Invalid category",
},
{ status: 400 }
{ status: 400 },
);
}
@@ -226,7 +259,7 @@ async function GET(request: Request, { params }: { params: { name: string } }) {
message: `Success get data forum ${category}`,
data: fixData,
},
{ status: 200 }
{ status: 200 },
);
} catch (error) {
return NextResponse.json(
@@ -235,7 +268,7 @@ async function GET(request: Request, { params }: { params: { name: string } }) {
message: `Error get data forum ${category}`,
reason: (error as Error).message,
},
{ status: 500 }
{ status: 500 },
);
}
}

View File

@@ -1,19 +1,20 @@
import _ from "lodash";
import { NextResponse } from "next/server";
import prisma from "@/lib/prisma";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export async function GET(
request: Request,
{ params }: { params: { id: string } }
{ params }: { params: { id: string } },
) {
try {
let fixData;
const { id } = params;
const { searchParams } = new URL(request.url);
const page = searchParams.get("page");
const page = Number(searchParams.get("page"));
const status = searchParams.get("status");
const takeData = 10;
const skipData = Number(page) * takeData - takeData;
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page * takeData - takeData;
const fixStatus = _.startCase(status ? status : "");
@@ -43,6 +44,7 @@ export async function GET(
id: true,
Author: true,
StatusInvoice: true,
nominal: true,
},
});
@@ -54,7 +56,7 @@ export async function GET(
message: "Success get status transaksi",
data: fixData,
},
{ status: 200 }
{ status: 200 },
);
} catch (error) {
console.error("Eror get status transaksi", error);
@@ -64,7 +66,7 @@ export async function GET(
message: "Error get status transaksi",
reason: (error as Error).message,
},
{ status: 500 }
{ status: 500 },
);
}
}

View File

@@ -1,6 +1,7 @@
import _ from "lodash";
import { NextResponse } from "next/server";
import { prisma } from "@/lib";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { GET };
@@ -9,12 +10,9 @@ async function GET(request: Request) {
const category = searchParams.get("category");
const search = searchParams.get("search");
const page = searchParams.get("page");
const takeData = 10;
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = Number(page) * takeData - takeData;
console.log("[CATEGORY]", category);
console.log("[PAGE]", page);
let fixData;
try {
if (category === "dashboard") {
@@ -49,7 +47,6 @@ async function GET(request: Request) {
};
} else {
const fixCategoryToStatus = _.startCase(category || "");
console.log("[STATUS]", fixCategoryToStatus);
const data = await prisma.investasi.findMany({
take: page ? takeData : undefined,
@@ -70,6 +67,12 @@ async function GET(request: Request) {
select: {
id: true,
title: true,
targetDana: true,
MasterPencarianInvestor: {
select: {
name: true,
},
},
author: {
select: {
id: true,

View File

@@ -1,6 +1,7 @@
import _ from "lodash";
import { NextResponse } from "next/server";
import { prisma } from "@/lib";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { GET };
@@ -8,6 +9,9 @@ async function GET(request: Request, { params }: { params: { name: string } }) {
const { searchParams } = new URL(request.url);
const category = searchParams.get("category");
const search = searchParams.get("search");
const page = Number(searchParams.get("page")) || 1;
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page * takeData - takeData;
let fixData;
try {
@@ -66,6 +70,8 @@ async function GET(request: Request, { params }: { params: { name: string } }) {
title: true,
Author: true,
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
});
}

View File

@@ -1,14 +1,22 @@
import { prisma } from "@/lib";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
import { NextResponse } from "next/server";
export { GET, POST };
async function GET() {
async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const page = Number(searchParams.get("page"));
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page * takeData - takeData;
const data = await prisma.masterBank.findMany({
orderBy: {
updatedAt: "desc",
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
});
return NextResponse.json(

View File

@@ -1,5 +1,6 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
import { NextResponse } from "next/server";
export { GET, PUT };
@@ -11,6 +12,10 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
const category = searchParams.get("category");
const subBidangId = searchParams.get("subBidangId");
const page = Number(searchParams.get("page")) || 1;
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page * takeData - takeData;
if (category === "all") {
const bidang = await prisma.masterBidangBisnis.findUnique({
where: {
@@ -45,6 +50,16 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
},
});
fixData = subBidang;
} else if (category === "only-sub-bidang") {
const subBidang = await prisma.masterSubBidangBisnis.findMany({
where: {
masterBidangBisnisId: id,
},
take: takeData,
skip: skipData,
});
fixData = subBidang;
}
@@ -71,9 +86,6 @@ async function PUT(request: Request, { params }: { params: { id: string } }) {
const { searchParams } = new URL(request.url);
const category = searchParams.get("category");
console.log("category", category);
console.log("data", data);
try {
if (category === "bidang") {
const updateData = await prisma.masterBidangBisnis.update({

View File

@@ -2,15 +2,24 @@ import { NextResponse } from "next/server";
import { prisma } from "@/lib";
import _ from "lodash";
import { Prisma } from "@prisma/client";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { GET, POST };
async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const page = Number(searchParams.get("page"));
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page * takeData - takeData;
const data = await prisma.masterBidangBisnis.findMany({
orderBy: {
createdAt: "asc",
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
});
return NextResponse.json({

View File

@@ -1,10 +1,14 @@
import { NextResponse } from "next/server";
import { NextRequest, NextResponse } from "next/server";
import prisma from "@/lib/prisma";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { GET, POST };
async function GET(request: Request) {
async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url);
const page = Number(searchParams.get("page"));
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page * takeData - takeData;
// const category = searchParams.get("category");
let fixData;
@@ -13,6 +17,8 @@ async function GET(request: Request) {
orderBy: {
createdAt: "asc",
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
});
// if (category === "category") {

View File

@@ -1,14 +1,22 @@
import { NextResponse } from "next/server";
import { NextRequest, NextResponse } from "next/server";
import prisma from "@/lib/prisma";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { GET, POST };
async function GET(request: Request) {
async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams;
const page = Number(searchParams.get("page"));
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page * takeData - takeData;
const data = await prisma.eventMaster_TipeAcara.findMany({
orderBy: {
updatedAt: "desc",
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
});
return NextResponse.json({

View File

@@ -1,5 +1,6 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
import { NextResponse } from "next/server";
export { GET };
@@ -7,10 +8,16 @@ async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const search = searchParams.get("search");
const category = searchParams.get("category");
const page = Number(searchParams.get("page"));
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page * takeData - takeData;
console.log("SEARCH", search);
console.log("PAGE", page);
let fixData;
try {
if(category === "only-user"){
if (category === "only-user") {
fixData = await prisma.user.findMany({
orderBy: {
updatedAt: "desc",
@@ -22,8 +29,10 @@ async function GET(request: Request) {
mode: "insensitive",
},
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
});
} else if(category === "only-admin"){
} else if (category === "only-admin") {
fixData = await prisma.user.findMany({
orderBy: {
updatedAt: "desc",
@@ -35,8 +44,10 @@ async function GET(request: Request) {
mode: "insensitive",
},
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
});
} else if (category === "all-role"){
} else if (category === "all-role") {
fixData = await prisma.user.findMany({
orderBy: {
updatedAt: "desc",
@@ -48,13 +59,15 @@ async function GET(request: Request) {
},
{
masterUserRoleId: "2",
}
},
],
username: {
contains: search || "",
mode: "insensitive",
},
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
});
}
@@ -65,13 +78,11 @@ async function GET(request: Request) {
data: fixData,
});
} catch (error) {
return NextResponse.json(
{
status: 500,
success: false,
message: "Error get data user access",
reason: (error as Error).message,
},
);
return NextResponse.json({
status: 500,
success: false,
message: "Error get data user access",
reason: (error as Error).message,
});
}
}

View File

@@ -2,6 +2,7 @@ import _ from "lodash";
import moment from "moment";
import { NextResponse } from "next/server";
import { prisma } from "@/lib";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { GET };
@@ -12,7 +13,7 @@ async function GET(request: Request) {
const search = searchParams.get("search");
const page = searchParams.get("page");
const takeData = 10;
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = Number(page) * takeData - takeData;
let fixData;

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();
// 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;
const { userId, platform, deviceId, model, appVersion, fcmToken } = data;
// 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 };
@@ -10,8 +11,14 @@ async function GET(
) {
const { id, status } = params;
const fixStatus = _.startCase(status);
const { searchParams } = new URL(request.url);
const page = Number(searchParams.get("page")) || 1;
const takeData = PAGINATION_DEFAULT_TAKE
const skipData = page * takeData - takeData;
let fixData;
let meta = null;
try {
const checkStatus = await prisma.donasiMaster_StatusDonasi.findFirst({
where: {
@@ -50,18 +57,38 @@ async function GET(
orderBy: {
updatedAt: "desc",
},
take: takeData,
skip: skipData,
});
const totalData = await prisma.donasi.count({
where: {
authorId: id,
donasiMaster_StatusDonasiId: checkStatus.id,
active: true,
},
});
const totalPages = Math.ceil(totalData / takeData);
fixData = res.map((v: any) => ({
..._.omit(v, ["DonasiMaster_Durasi"]),
nameDonasiDurasi: v.DonasiMaster_Durasi.name,
}));
meta = {
currentPage: page,
totalData: totalData,
totalPage: totalPages,
dataPerPage: takeData,
};
return NextResponse.json({
status: 200,
success: true,
message: "Berhasil mendapatkan data",
data: fixData,
...(meta && { meta }),
});
} catch (error) {
console.log("[ERROR]", error);

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) {

View File

@@ -1,6 +1,7 @@
import { NextResponse } from "next/server";
import prisma from "@/lib/prisma";
import _ from "lodash";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { GET, PUT };
@@ -12,6 +13,11 @@ async function GET(
const { id, status } = params;
const fixStatusName = _.startCase(status);
const { searchParams } = new URL(request.url);
const page = Number(searchParams.get("page")) || 1;
const takeData = PAGINATION_DEFAULT_TAKE
const skipData = page * takeData - takeData;
const data = await prisma.event.findMany({
orderBy: {
updatedAt: "desc",
@@ -37,13 +43,35 @@ async function GET(
},
authorId: true,
},
take: takeData,
skip: skipData,
});
// Get total count for pagination info
const totalCount = await prisma.event.count({
where: {
active: true,
authorId: id,
isArsip: false,
EventMaster_Status: {
name: fixStatusName,
},
},
});
const totalPages = Math.ceil(totalCount / takeData);
return NextResponse.json(
{
success: true,
message: "Success get event",
data: data,
pagination: {
currentPage: page,
totalPages: totalPages,
totalData: totalCount,
dataPerPage: takeData,
},
},
{ status: 200 }
);

View File

@@ -6,6 +6,7 @@ import {
NotificationMobileTitleType,
} from "../../../../../../../types/type-mobile-notification";
import { routeUserMobile } from "@/lib/mobile/route-page-mobile";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { GET, POST };
@@ -47,7 +48,7 @@ async function POST(request: Request, { params }: { params: { id: string } }) {
message: "Success join event",
data: createJoin,
},
{ status: 200 }
{ status: 200 },
);
} catch (error) {
return NextResponse.json(
@@ -56,7 +57,7 @@ async function POST(request: Request, { params }: { params: { id: string } }) {
message: "Error join event",
reason: (error as Error).message,
},
{ status: 500 }
{ status: 500 },
);
}
}
@@ -64,12 +65,17 @@ async function POST(request: Request, { params }: { params: { id: string } }) {
async function GET(request: Request, { params }: { params: { id: string } }) {
try {
const { id } = params;
const { searchParams } = new URL(request.url);
const page = Number(searchParams.get("page")) || 1;
const takeData = PAGINATION_DEFAULT_TAKE
const skipData = page * takeData - takeData;
const data = await prisma.event_Peserta.findMany({
where: {
eventId: id,
},
select: {
id: true,
eventId: true,
userId: true,
isPresent: true,
@@ -87,6 +93,8 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
},
},
},
take: takeData,
skip: skipData,
});
return NextResponse.json(
@@ -94,8 +102,14 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
success: true,
message: "Success get participants",
data: data,
meta: {
page,
take: takeData,
total: await prisma.event_Peserta.count({ where: { eventId: id } }),
totalPages: Math.ceil(await prisma.event_Peserta.count({ where: { eventId: id } }) / takeData),
},
},
{ status: 200 }
{ status: 200 },
);
} catch (error) {
return NextResponse.json(
@@ -104,7 +118,7 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
message: "Error get participants",
reason: (error as Error).message,
},
{ status: 500 }
{ status: 500 },
);
}
}

View File

@@ -5,6 +5,7 @@ import _ from "lodash";
import moment from "moment";
import { NextResponse } from "next/server";
import { NotificationMobileBodyType } from "../../../../../types/type-mobile-notification";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { GET, POST };
@@ -76,11 +77,15 @@ async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const category = searchParams.get("category");
const userId = searchParams.get("userId");
const page = Number(searchParams.get("page")) || 1;
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page * takeData - takeData;
console.log("[CAT]", category);
console.log("[USER]", userId);
let fixData;
let totalCount = 0;
if (category === "beranda") {
const allData = await prisma.event.findMany({
@@ -108,84 +113,96 @@ async function GET(request: Request) {
}
}
// const takeData = 10;
// const skipData = page * takeData - takeData;
const data = await prisma.event.findMany({
// take: takeData,
// skip: skipData,
orderBy: [
{
tanggal: "asc",
const [data, count] = await Promise.all([
prisma.event.findMany({
take: takeData,
skip: skipData,
orderBy: [
{
tanggal: "asc",
},
],
where: {
active: true,
eventMaster_StatusId: "1",
isArsip: false,
},
],
where: {
active: true,
eventMaster_StatusId: "1",
isArsip: false,
},
select: {
id: true,
title: true,
deskripsi: true,
tanggal: true,
tanggalSelesai: true,
EventMaster_Status: {
select: {
name: true,
select: {
id: true,
title: true,
deskripsi: true,
tanggal: true,
tanggalSelesai: true,
EventMaster_Status: {
select: {
name: true,
},
},
authorId: true,
Author: {
include: {
Profile: true,
},
},
},
authorId: true,
Author: {
include: {
Profile: true,
},
}),
prisma.event.count({
where: {
active: true,
eventMaster_StatusId: "1",
isArsip: false,
},
},
});
})
]);
fixData = data;
totalCount = count;
} else if (category === "contribution") {
const data = await prisma.event_Peserta.findMany({
where: {
userId: userId,
},
select: {
eventId: true,
userId: true,
Event: {
select: {
id: true,
title: true,
tanggal: true,
Author: {
select: {
id: true,
username: true,
Profile: {
select: {
id: true,
name: true,
imageId: true,
const [data, count] = await Promise.all([
prisma.event_Peserta.findMany({
take: takeData,
skip: skipData,
where: {
userId: userId,
},
select: {
id: true,
eventId: true,
userId: true,
Event: {
select: {
id: true,
title: true,
tanggal: true,
Author: {
select: {
id: true,
username: true,
Profile: {
select: {
id: true,
name: true,
imageId: true,
},
},
},
},
},
Event_Peserta: {
take: 4,
orderBy: {
createdAt: "desc",
},
select: {
id: true,
userId: true,
User: {
select: {
Profile: {
select: {
id: true,
name: true,
imageId: true,
Event_Peserta: {
take: 4,
orderBy: {
createdAt: "desc",
},
select: {
id: true,
userId: true,
User: {
select: {
Profile: {
select: {
id: true,
name: true,
imageId: true,
},
},
},
},
@@ -194,86 +211,109 @@ async function GET(request: Request) {
},
},
},
// User: {
// select: {
// id: true,
// username: true,
// Profile: {
// select: {
// id: true,
// name: true,
// imageId: true,
// },
// },
// },
// },
},
});
}),
prisma.event_Peserta.count({
where: {
userId: userId,
},
})
]);
fixData = data;
totalCount = count;
} else if (category === "all-history") {
const data = await prisma.event.findMany({
orderBy: {
tanggal: "desc",
},
where: {
eventMaster_StatusId: "1",
isArsip: true,
},
select: {
id: true,
title: true,
tanggal: true,
deskripsi: true,
active: true,
authorId: true,
Author: {
select: {
id: true,
username: true,
Profile: true,
const [data, count] = await Promise.all([
prisma.event.findMany({
take: takeData,
skip: skipData,
orderBy: {
tanggal: "desc",
},
where: {
eventMaster_StatusId: "1",
isArsip: true,
},
select: {
id: true,
title: true,
tanggal: true,
deskripsi: true,
active: true,
authorId: true,
Author: {
select: {
id: true,
username: true,
Profile: true,
},
},
},
},
});
}),
prisma.event.count({
where: {
eventMaster_StatusId: "1",
isArsip: true,
},
})
]);
fixData = data;
totalCount = count;
} else if (category === "my-history") {
const data = await prisma.event.findMany({
orderBy: {
tanggal: "desc",
},
where: {
authorId: userId,
eventMaster_StatusId: "1",
isArsip: true,
},
select: {
id: true,
title: true,
tanggal: true,
deskripsi: true,
active: true,
authorId: true,
Author: {
select: {
id: true,
username: true,
Profile: true,
const [data, count] = await Promise.all([
prisma.event.findMany({
take: takeData,
skip: skipData,
orderBy: {
tanggal: "desc",
},
where: {
authorId: userId,
eventMaster_StatusId: "1",
isArsip: true,
},
select: {
id: true,
title: true,
tanggal: true,
deskripsi: true,
active: true,
authorId: true,
Author: {
select: {
id: true,
username: true,
Profile: true,
},
},
},
},
});
}),
prisma.event.count({
where: {
authorId: userId,
eventMaster_StatusId: "1",
isArsip: true,
},
})
]);
fixData = data;
totalCount = count;
}
const totalPages = Math.ceil(totalCount / takeData);
return NextResponse.json(
{
success: true,
message: "Success get event",
data: fixData,
pagination: {
currentPage: page,
totalPages: totalPages,
totalData: totalCount,
dataPerPage: takeData,
},
},
{ status: 200 }
);

View File

@@ -1,4 +1,5 @@
import { NextResponse } from "next/server";
import prisma from "@/lib/prisma"
export async function GET(
request: Request,

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 };
@@ -9,6 +10,10 @@ async function GET(
{ params }: { params: { id: string; status: string } }
) {
const { id, status } = params;
const { searchParams } = new URL(request.url);
const page = Number(searchParams.get("page"));
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page ? page * takeData - takeData : 0;
const fixStatusName = _.startCase(status);
try {
@@ -22,6 +27,8 @@ async function GET(
name: fixStatusName,
},
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
select: {
id: true,
title: true,

View File

@@ -6,6 +6,7 @@ import {
NotificationMobileTitleType,
} from "../../../../../../../types/type-mobile-notification";
import { routeAdminMobile, routeUserMobile } from "@/lib/mobile/route-page-mobile";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { POST, GET, DELETE };
@@ -98,6 +99,9 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
const { id } = params;
const { searchParams } = new URL(request.url);
const category = searchParams.get("category");
const page = Number(searchParams.get("page"));
const takeData = PAGINATION_DEFAULT_TAKE
const skipData = page * takeData - takeData
try {
let fixData;
@@ -117,6 +121,8 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
investasiId: id,
active: true,
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
});
}

View File

@@ -1,10 +1,16 @@
import { NextResponse } from "next/server";
import prisma from "@/lib/prisma";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { GET };
async function GET(request: Request, { params }: { params: { id: string } }) {
const { id } = params;
const { searchParams } = new URL(request.url);
const page = Number(searchParams.get("page")) || 1;
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page * takeData - takeData;
try {
const data = await prisma.investasi_Invoice.findMany({
@@ -29,13 +35,30 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
},
},
},
take: takeData,
skip: skipData,
});
const totalData = await prisma.investasi_Invoice.count({
where: {
investasiId: id,
statusInvoiceId: "1",
},
});
const totalPages = Math.ceil(totalData / takeData);
return NextResponse.json({
status: 200,
success: true,
message: "Berhasil Mendapatkan Data",
data: data,
meta: {
currentPage: page,
totalData: totalData,
totalPage: totalPages,
dataPerPage: takeData,
},
});
} catch (error) {
return NextResponse.json({

View File

@@ -7,6 +7,7 @@ import {
NotificationMobileTitleType,
NotificationMobileBodyType,
} from "../../../../../../../types/type-mobile-notification";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { POST, GET, PUT };
@@ -53,6 +54,9 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
const { searchParams } = new URL(request.url);
const category = searchParams.get("category");
const authorId = searchParams.get("authorId");
const page = Number(searchParams.get("page"));
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page ? page * takeData - takeData : 0;
console.log("[ID INVOICE]", id);
@@ -103,6 +107,8 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
statusInvoiceId: "1",
isActive: true,
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
select: {
id: true,
nominal: true,
@@ -129,6 +135,8 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
where: {
authorId: authorId,
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
select: {
id: true,
statusInvoiceId: true,

View File

@@ -2,6 +2,7 @@ import _ from "lodash";
import { prisma } from "@/lib";
import { NextResponse } from "next/server";
import { sendNotificationInvestmentAddNews } from "@/lib/mobile/notification/notification-add-news-investment";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { POST, GET, DELETE };
@@ -88,8 +89,13 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
console.log("id", id);
const { searchParams } = new URL(request.url);
const category = searchParams.get("category");
const page = Number(searchParams.get("page")) || 1;
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page * takeData - takeData;
let fixData;
let meta = null;
try {
if (category === "one-news") {
const data = await prisma.beritaInvestasi.findFirst({
@@ -113,7 +119,7 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
fixData = newData;
} else if (category === "all-news") {
fixData = await prisma.beritaInvestasi.findMany({
const newsData = await prisma.beritaInvestasi.findMany({
orderBy: {
updatedAt: "desc",
},
@@ -121,7 +127,27 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
investasiId: id,
active: true,
},
take: takeData,
skip: skipData,
});
const totalData = await prisma.beritaInvestasi.count({
where: {
investasiId: id,
active: true,
},
});
const totalPages = Math.ceil(totalData / takeData);
fixData = newsData;
meta = {
currentPage: page,
totalData: totalData,
totalPage: totalPages,
dataPerPage: takeData,
};
}
return NextResponse.json({
@@ -129,6 +155,7 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
success: true,
message: "Berita berhasil diambil",
data: fixData,
...(meta && { meta }),
});
} catch (error) {
console.log("[ERROR]", error);

View File

@@ -5,6 +5,7 @@ import moment from "moment";
import { sendNotificationMobileToManyUser } from "@/lib/mobile/notification/send-notification";
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 };
@@ -73,6 +74,9 @@ 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"));
const takeData = PAGINATION_DEFAULT_TAKE
const skipData = page ? page * takeData - takeData : 0;
console.log("[CATEGORY]", category);
console.log("[AUTHOR ID]", authorId);
@@ -132,6 +136,8 @@ async function GET(request: Request) {
where: {
masterStatusInvestasiId: "1",
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
select: {
id: true,
imageId: true,
@@ -156,6 +162,8 @@ async function GET(request: Request) {
authorId: authorId,
statusInvoiceId: "1",
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
select: {
id: true,
investasiId: true,

View File

@@ -1,3 +1,4 @@
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
import prisma from "@/lib/prisma";
import _ from "lodash";
import { NextResponse } from "next/server";
@@ -12,6 +13,11 @@ async function GET(
const { id, status } = params;
const fixStatusName = _.startCase(status);
const { searchParams } = new URL(request.url);
const page = Number(searchParams.get("page"));
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page ? page * takeData - takeData : 0;
const data = await prisma.job.findMany({
orderBy: {
updatedAt: "desc",
@@ -28,13 +34,20 @@ async function GET(
id: true,
title: true,
},
take: takeData,
skip: skipData,
});
return NextResponse.json(
{
success: true,
message: "Success get job",
data: data,
pagination: {
currentPage: page,
dataPerPage: takeData,
},
},
{ status: 200 }
);

View File

@@ -3,6 +3,7 @@ import { routeAdminMobile } from "@/lib/mobile/route-page-mobile";
import prisma from "@/lib/prisma";
import { NextResponse } from "next/server";
import { NotificationMobileBodyType } from "../../../../../types/type-mobile-notification";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { POST, GET };
@@ -45,7 +46,7 @@ async function POST(request: Request) {
message: "Berhasil disimpan",
data: create,
},
{ status: 201 }
{ status: 201 },
);
} catch (error) {
return NextResponse.json(
@@ -54,7 +55,7 @@ async function POST(request: Request) {
message: "Error create job",
reason: (error as Error).message,
},
{ status: 500 }
{ status: 500 },
);
}
}
@@ -64,94 +65,129 @@ async function GET(request: Request) {
const search = searchParams.get("search");
const category = searchParams.get("category");
const authorId = searchParams.get("authorId");
const page = Number(searchParams.get("page")) || 1;
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page * takeData - takeData;
let fixData;
try {
if (category === "archive") {
const data = await prisma.job.findMany({
where: {
authorId: authorId,
isActive: true,
isArsip: true,
MasterStatus: {
name: "Publish",
},
// title: {
// contains: search || "",
// mode: "insensitive",
// },
},
orderBy: {
createdAt: "desc",
},
select: {
id: true,
title: true,
deskripsi: true,
authorId: true,
MasterStatus: {
select: {
name: true,
const [data, count] = await Promise.all([
prisma.job.findMany({
where: {
authorId: authorId,
isActive: true,
isArsip: true,
MasterStatus: {
name: "Publish",
},
// title: {
// contains: search || "",
// mode: "insensitive",
// },
},
Author: {
select: {
id: true,
username: true,
Profile: {
select: {
id: true,
name: true,
imageId: true,
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,
},
},
},
},
},
},
});
take: takeData,
skip: skipData,
}),
prisma.job.count({
where: {
authorId: authorId,
isActive: true,
isArsip: true,
MasterStatus: {
name: "Publish",
},
},
}),
]);
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,
const [data, count] = await Promise.all([
prisma.job.findMany({
where: {
isActive: true,
isArsip: false,
MasterStatus: {
name: "Publish",
},
title: {
contains: search || "",
mode: "insensitive",
},
},
Author: {
select: {
id: true,
username: true,
Profile: {
select: {
id: true,
name: true,
imageId: true,
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,
},
},
},
},
},
},
});
take: takeData,
skip: skipData,
}),
prisma.job.count({
where: {
isActive: true,
isArsip: false,
MasterStatus: {
name: "Publish",
},
title: {
contains: search || "",
mode: "insensitive",
},
},
}),
]);
fixData = data;
}
@@ -161,8 +197,12 @@ async function GET(request: Request) {
success: true,
message: "Success get data job-vacancy",
data: fixData,
pagination: {
currentPage: page,
dataPerPage: takeData,
},
},
{ status: 200 }
{ status: 200 },
);
} catch (error) {
return NextResponse.json(
@@ -171,7 +211,7 @@ async function GET(request: Request) {
message: "Error get data job-vacancy",
reason: (error as Error).message,
},
{ status: 500 }
{ status: 500 },
);
}
}

View File

@@ -6,17 +6,25 @@ import { adminMessaging } from "@/lib/firebase-admin";
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
{ params }: { params: { id: string } },
) {
const { id } = params;
console.log("ID", id);
const { searchParams } = new URL(request.url);
const category = searchParams.get("category");
const fixCategory = _.upperCase(category || "");
const page = Number(searchParams.get("page"));
console.log("page", page);
const takeData = 10;
const skipData = page * takeData - takeData;
let fixData;
const fixCategory = _.upperCase(category || "");
try {
const data = await prisma.notifikasi.findMany({
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
orderBy: {
createdAt: "desc",
},
@@ -26,23 +34,51 @@ export async function GET(
},
});
// Jika pagination digunakan, ambil juga total count untuk informasi
let totalCount;
let totalPages;
if (page) {
totalCount = await prisma.notifikasi.count({
where: {
recipientId: id,
kategoriApp: fixCategory,
},
});
totalPages = Math.ceil(totalCount / takeData);
}
fixData = data;
return NextResponse.json({
const response = {
success: true,
data: fixData,
});
};
// Tambahkan metadata pagination jika parameter page disertakan
if (page) {
Object.assign(response, {
meta: {
page,
take: takeData,
skip: skipData,
total: totalCount,
totalPages,
},
});
}
return NextResponse.json(response);
} catch (error) {
return NextResponse.json(
{ error: (error as Error).message },
{ status: 500 }
{ status: 500 },
);
}
}
export async function PUT(
request: NextRequest,
{ params }: { params: { id: string } }
{ params }: { params: { id: string } },
) {
const { id } = params;
const { searchParams } = new URL(request.url);
@@ -79,7 +115,7 @@ export async function PUT(
console.error("Error marking notifications as read:", error);
return NextResponse.json(
{ error: (error as Error).message },
{ status: 500 }
{ status: 500 },
);
}
}

View File

@@ -7,8 +7,13 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
try {
const { searchParams } = new URL(request.url);
const id = searchParams.get("id");
const page = parseInt(searchParams.get("page") || "1");
const take = 10; // Default 10 data
const skip = page * take - take;
const data = await prisma.portofolio.findMany({
skip,
take,
orderBy: {
createdAt: "desc",
},
@@ -18,22 +23,30 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
},
});
if (!data)
return NextResponse.json(
{
success: false,
message: "Data tidak ditemukan",
},
{ status: 404 }
);
// Hitung total data untuk informasi pagination
const total = await prisma.portofolio.count({
where: {
profileId: id,
active: true,
},
});
const totalPages = Math.ceil(total / take);
return NextResponse.json(
{
success: true,
message: "Berhasil mendapatkan data",
data: data,
meta: {
page,
take,
skip,
total,
totalPages,
},
},
{ status: 200 }
{ status: 200 },
);
} catch (error) {
return NextResponse.json(
@@ -42,7 +55,7 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
message: "API Error Get Data Potofolio",
reason: (error as Error).message,
},
{ status: 500 }
{ status: 500 },
);
}
}
@@ -66,7 +79,10 @@ async function POST(request: Request) {
},
});
if (data.subBidang.length > 0 || data.subBidang.map((item: any) => item.id !== "")) {
if (
data.subBidang.length > 0 ||
data.subBidang.map((item: any) => item.id !== "")
) {
for (let i of data.subBidang) {
const createSubBidang =
await prisma.portofolio_BidangDanSubBidangBisnis.create({
@@ -84,7 +100,7 @@ async function POST(request: Request) {
success: false,
message: "Gagal membuat sub bidang bisnis",
},
{ status: 400 }
{ status: 400 },
);
}
}
@@ -95,7 +111,7 @@ async function POST(request: Request) {
success: false,
message: "Gagal membuat portofolio",
},
{ status: 400 }
{ status: 400 },
);
const createMedsos = await prisma.portofolio_MediaSosial.create({
@@ -115,7 +131,7 @@ async function POST(request: Request) {
success: false,
message: "Gagal menambahkan medsos",
},
{ status: 400 }
{ status: 400 },
);
return NextResponse.json(
@@ -124,7 +140,7 @@ async function POST(request: Request) {
message: "Berhasil mendapatkan data",
data: createPortofolio,
},
{ status: 200 }
{ status: 200 },
);
} catch (error) {
return NextResponse.json(
@@ -133,7 +149,7 @@ async function POST(request: Request) {
message: "API Error Post Data",
reason: (error as Error).message,
},
{ status: 500 }
{ status: 500 },
);
}
}

View File

@@ -27,18 +27,18 @@ export async function GET(request: Request) {
NOT: {
Profile: null,
},
OR: [
{
MasterUserRole: {
name: "User",
},
},
{
MasterUserRole: {
name: "Admin",
},
},
],
// OR: [
// {
// MasterUserRole: {
// name: "User",
// },
// },
// {
// MasterUserRole: {
// name: "Admin",
// },
// },
// ],
},
include: {
Profile: {

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 };
@@ -10,14 +11,33 @@ async function GET(
) {
try {
const { id, status } = params;
console.log("[ID]", id);
const fixStatusName = _.startCase(status);
console.log("[STATUS]", fixStatusName);
let fixData;
const { searchParams } = new URL(request.url);
const page = Number(searchParams.get("page")) || 1;
const takeData = PAGINATION_DEFAULT_TAKE
const skipData = page * takeData - takeData;
let data;
let totalCount;
if (fixStatusName === "Publish") {
fixData = await prisma.voting.findMany({
data = await prisma.voting.findMany({
where: {
authorId: id,
isActive: true,
akhirVote: {
gte: new Date(),
},
Voting_Status: {
name: fixStatusName,
},
},
take: takeData,
skip: skipData,
});
totalCount = await prisma.voting.count({
where: {
authorId: id,
isActive: true,
@@ -30,7 +50,18 @@ async function GET(
},
});
} else {
fixData = await prisma.voting.findMany({
data = await prisma.voting.findMany({
where: {
authorId: id,
Voting_Status: {
name: fixStatusName,
},
},
take: takeData,
skip: skipData,
});
totalCount = await prisma.voting.count({
where: {
authorId: id,
Voting_Status: {
@@ -40,10 +71,18 @@ async function GET(
});
}
const totalPages = Math.ceil(totalCount / takeData);
return NextResponse.json({
success: true,
message: "Success get voting",
data: fixData,
data: data,
pagination: {
currentPage: page,
totalPages: totalPages,
totalData: totalCount,
dataPerPage: takeData,
},
});
} catch (error) {
console.log("[ERROR]", error);
@@ -61,9 +100,7 @@ async function PUT(
) {
try {
const { id, status } = params;
console.log("[ID]", id);
const fixStatusName = _.startCase(status);
console.log("[STATUS]", fixStatusName);
const checkData = await prisma.voting.findFirst({
where: {
@@ -79,8 +116,6 @@ async function PUT(
},
});
console.log("[CHECKDATA]", checkData);
if (!checkData)
return NextResponse.json({
success: false,
@@ -115,8 +150,6 @@ async function PUT(
},
});
console.log("[UPDATE]", updateData);
return NextResponse.json({
success: true,
message: "Success update voting",

View File

@@ -1,5 +1,6 @@
import { NextResponse } from "next/server";
import prisma from "@/lib/prisma";
import { PAGINATION_DEFAULT_TAKE } from "@/lib/constans-value/constansValue";
export { GET };
@@ -8,10 +9,9 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
const { searchParams } = new URL(request.url);
const authorId = searchParams.get("authorId");
const category = searchParams.get("category");
console.log("[ID]", id);
console.log("[AUTHOR ID]", authorId);
console.log("[CATEGORY]", category);
const page = Number(searchParams.get("page"));
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page ? page * takeData - takeData : 0;
let fixData;
@@ -53,7 +53,10 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
where: {
votingId: id,
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
select: {
id: true,
Voting_DaftarNamaVote: {
select: {
value: true,
@@ -75,8 +78,6 @@ async function GET(request: Request, { params }: { params: { id: string } }) {
},
});
console.log("[LIST KONTRIBUTOR]", listKontributor);
fixData = listKontributor;
}

View File

@@ -4,6 +4,7 @@ import _ from "lodash";
import { sendNotificationMobileToManyUser } from "@/lib/mobile/notification/send-notification";
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 };
@@ -87,8 +88,9 @@ async function GET(request: Request) {
const category = searchParams.get("category");
const authorId = searchParams.get("authorId");
const userLoginId = searchParams.get("userLoginId");
console.log("userLoginId >>", userLoginId);
const page = Number(searchParams.get("page"));
const takeData = PAGINATION_DEFAULT_TAKE;
const skipData = page * takeData - takeData;
let fixData;
@@ -123,6 +125,8 @@ async function GET(request: Request) {
},
},
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
include: {
Voting_DaftarNamaVote: {
orderBy: {
@@ -154,6 +158,8 @@ async function GET(request: Request) {
where: {
authorId: authorId,
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
include: {
Voting: {
select: {
@@ -211,6 +217,8 @@ async function GET(request: Request) {
mode: "insensitive",
},
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
include: {
Voting_DaftarNamaVote: {
orderBy: {
@@ -249,6 +257,8 @@ async function GET(request: Request) {
mode: "insensitive",
},
},
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
include: {
Voting_DaftarNamaVote: {
orderBy: {

View File

@@ -76,15 +76,27 @@ export async function GET(req: Request) {
data: user,
});
} catch (error) {
console.error("Error in user validation:", error);
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
const errorStack = error instanceof Error ? error.stack : 'No stack';
// Log detailed error for debugging
console.error("❌ [USER-VALIDATE] Error:", errorMsg);
console.error("❌ [USER-VALIDATE] Stack:", errorStack);
console.error("❌ [USER-VALIDATE] Time:", new Date().toISOString());
// Check if it's a database connection error
if (errorMsg.includes("Prisma") || errorMsg.includes("database") || errorMsg.includes("connection")) {
console.error("❌ [USER-VALIDATE] Database connection error detected!");
console.error("❌ [USER-VALIDATE] DATABASE_URL exists:", !!process.env.DATABASE_URL);
}
return NextResponse.json(
{
success: false,
message: "Terjadi kesalahan pada server",
error: process.env.NODE_ENV === 'development' ? errorMsg : 'Internal server error',
},
{ status: 500 }
);
} finally {
await prisma.$disconnect();
}
}

View File

@@ -28,6 +28,7 @@ import { useRouter } from "next/navigation";
import { IconMoodSmileFilled } from "@tabler/icons-react";
import { listStiker } from "../../lib/stiker";
import { UIGlobal_Modal } from "../../ui";
import mqtt_client from "@/util/mqtt_client";
const ReactQuill = dynamic(
async () => {
@@ -248,10 +249,12 @@ function ButtonAction({ value, lengthData }: ButtonActionProps) {
ComponentGlobal_NotifikasiBerhasil(create.message);
router.back();
mqtt_client.publish(
"Forum_create_new",
JSON.stringify({ isNewPost: true, count: 1 })
);
if (typeof window !== 'undefined' && mqtt_client) {
mqtt_client.publish(
"Forum_create_new",
JSON.stringify({ isNewPost: true, count: 1 })
);
}
} else {
ComponentGlobal_NotifikasiGagal(create.message);
}

View File

@@ -28,13 +28,10 @@ export default function Login({ version }: { version: string }) {
const [countryCode, setCountryCode] = useState<string>("62"); // default ke Indonesia
async function onLogin() {
console.log("phone >>", phone);
const nomor = phone;
if (nomor.length <= 4) return setError(true);
const fixPhone = `${countryCode}${nomor}`;
console.log("fixPhone >>", fixPhone);
try {
setLoading(true);
@@ -46,7 +43,6 @@ export default function Login({ version }: { version: string }) {
router.push("/validasi", { scroll: false });
} else {
setLoading(false);
console.log("respone >>", respone);
ComponentGlobal_NotifikasiPeringatan(respone?.message);
}
} catch (error) {
@@ -108,9 +104,6 @@ export default function Login({ version }: { version: string }) {
// Simpan hasil akhir
setCountryCode(dialCode);
setPhone(localNumber);
// console.log("Country Code:", dialCode);
// console.log("Clean Local Number:", localNumber);
}}
/>

View File

@@ -25,15 +25,21 @@ export default function WaitingRoom_View({
const [isLoadingHome, setIsLoadingHome] = useState(false);
async function onClickLogout() {
setLoading(true);
const res = await fetch(`/api/auth/logout?id=${userLoginId}`, {
method: "GET",
});
try {
setLoading(true);
const res = await fetch(`/api/auth/logout?id=${userLoginId}`, {
method: "GET",
});
const result = await res.json();
if (res.status === 200) {
ComponentGlobal_NotifikasiBerhasil(result.message);
router.push("/", { scroll: false });
const result = await res.json();
if (res.status === 200) {
ComponentGlobal_NotifikasiBerhasil(result.message);
router.push("/", { scroll: false });
}
} catch (error) {
console.error("Error button to home", error);
} finally {
setLoading(false);
}
}
@@ -83,7 +89,8 @@ export default function WaitingRoom_View({
</Text>
<Text fw={"bold"} c={"white"} align="center">
Harap tunggu, Anda akan menerima pemberitahuan melalui
Whatsapp setelah disetujui.
Whatsapp setelah disetujui, untuk sementara anda bisa
menunggu pada halaman ini atau keluar.
</Text>
</Stack>
{isAccess && (
@@ -110,6 +117,10 @@ export default function WaitingRoom_View({
Home
</Button>
)}
<Button color="red" loading={loading} onClick={onClickLogout}>
Keluar
</Button>
</Stack>
)}
</ComponentGlobal_CardStyles>

View File

@@ -0,0 +1 @@
export const PAGINATION_DEFAULT_TAKE = 10;

View File

@@ -1,24 +1,25 @@
// lib/firebase-admin.ts
import { cert, getApp, getApps, initializeApp } from 'firebase-admin/app';
import { getMessaging } from 'firebase-admin/messaging';
import { getMessaging, Messaging } from 'firebase-admin/messaging';
// Ambil dari environment
const serviceAccount = {
projectId: process.env.FIREBASE_ADMIN_PROJECT_ID,
clientEmail: process.env.FIREBASE_ADMIN_CLIENT_EMAIL,
privateKey: process.env.FIREBASE_ADMIN_PRIVATE_KEY?.replace(/\\n/g, '\n'),
};
function getAdminApp() {
if (getApps().length > 0) return getApp();
if (!serviceAccount.projectId || !serviceAccount.clientEmail || !serviceAccount.privateKey) {
throw new Error('Firebase Admin credentials are missing in environment variables');
const privateKey = process.env.FIREBASE_ADMIN_PRIVATE_KEY?.replace(/\\n/g, '\n');
const projectId = process.env.FIREBASE_ADMIN_PROJECT_ID;
const clientEmail = process.env.FIREBASE_ADMIN_CLIENT_EMAIL;
if (!projectId || !clientEmail || !privateKey) {
throw new Error('Firebase Admin credentials are missing in environment variables');
}
return initializeApp({
credential: cert({ projectId, clientEmail, privateKey }),
projectId,
});
}
// Inisialisasi hanya sekali
const app = !getApps().length
? initializeApp({
credential: cert(serviceAccount),
projectId: serviceAccount.projectId,
})
: getApp();
export const adminMessaging = getMessaging(app);
export const adminMessaging: Pick<Messaging, 'send' | 'sendEachForMulticast'> = {
send: (message) => getMessaging(getAdminApp()).send(message),
sendEachForMulticast: (message) => getMessaging(getAdminApp()).sendEachForMulticast(message),
};

188
src/lib/prisma-retry.ts Normal file
View File

@@ -0,0 +1,188 @@
import { prisma } from './prisma';
/**
* Retry configuration for database operations
*/
interface RetryConfig {
maxRetries: number;
initialDelay: number;
maxDelay: number;
factor: number;
}
const DEFAULT_RETRY_CONFIG: RetryConfig = {
maxRetries: 3,
initialDelay: 100,
maxDelay: 5000,
factor: 2,
};
/**
* Check if error is retryable (transient error)
*/
function isRetryableError(error: any): boolean {
const errorMsg = error instanceof Error ? error.message : '';
// Retry on connection-related errors
const retryablePatterns = [
'ECONNRESET',
'ECONNREFUSED',
'ETIMEDOUT',
'ENOTFOUND',
'connection closed',
'connection terminated',
'connection timeout',
'socket hang up',
'too many connections',
'pool is full',
'server login has been failing',
'FATAL:',
'PrismaClientUnknownRequestError',
];
return retryablePatterns.some(pattern =>
errorMsg.toLowerCase().includes(pattern.toLowerCase())
);
}
/**
* Execute database operation with retry mechanism
*
* @param operation - The database operation to execute
* @param config - Retry configuration (optional)
* @param operationName - Name of the operation for logging
*
* @example
* const user = await withRetry(
* () => prisma.user.findUnique({ where: { id: '123' } }),
* undefined,
* 'findUser'
* );
*/
export async function withRetry<T>(
operation: () => Promise<T>,
config?: Partial<RetryConfig>,
operationName?: string
): Promise<T> {
const retryConfig = { ...DEFAULT_RETRY_CONFIG, ...config };
let lastError: any;
for (let attempt = 1; attempt <= retryConfig.maxRetries; attempt++) {
try {
const result = await operation();
// Log success if it was a retry
if (attempt > 1 && operationName) {
console.log(`✅ [DB-RETRY] ${operationName} succeeded after ${attempt} attempts`);
}
return result;
} catch (error) {
lastError = error;
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
// Check if we should retry
if (attempt < retryConfig.maxRetries && isRetryableError(error)) {
// Calculate delay with exponential backoff + jitter
const delay = Math.min(
retryConfig.initialDelay * Math.pow(retryConfig.factor, attempt - 1),
retryConfig.maxDelay
);
const jitter = Math.random() * 0.3 * delay; // Add 30% jitter
if (operationName) {
console.warn(
`⚠️ [DB-RETRY] ${operationName} failed (attempt ${attempt}/${retryConfig.maxRetries}): ${errorMsg}`
);
console.log(`⏳ [DB-RETRY] Retrying in ${Math.round(delay + jitter)}ms...`);
}
await new Promise(resolve => setTimeout(resolve, delay + jitter));
} else {
// Don't retry - either max retries reached or not a retryable error
if (operationName) {
console.error(
`❌ [DB-RETRY] ${operationName} failed after ${attempt} attempts: ${errorMsg}`
);
}
break;
}
}
}
// All retries exhausted, throw the last error
throw lastError;
}
/**
* Execute database operation with timeout
*
* @param operation - The database operation to execute
* @param timeout - Timeout in milliseconds (default: 30000)
* @param operationName - Name of the operation for logging
*/
export async function withTimeout<T>(
operation: () => Promise<T>,
timeout: number = 30000,
operationName?: string
): Promise<T> {
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => {
reject(new Error(`Operation timed out after ${timeout}ms`));
}, timeout);
});
try {
return await Promise.race([operation(), timeoutPromise]);
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
if (errorMsg.includes('timed out')) {
if (operationName) {
console.error(`⏱️ [DB-TIMEOUT] ${operationName} timed out after ${timeout}ms`);
}
}
throw error;
}
}
/**
* Combine retry and timeout for robust database operations
*
* @param operation - The database operation to execute
* @param options - Retry and timeout options
* @param operationName - Name of the operation for logging
*/
export async function withRetryAndTimeout<T>(
operation: () => Promise<T>,
options?: {
retry?: Partial<RetryConfig>;
timeout?: number;
},
operationName?: string
): Promise<T> {
return withRetry(
() => withTimeout(operation, options?.timeout, operationName),
options?.retry,
operationName
);
}
/**
* Health check for database connection
*/
export async function checkDatabaseConnection(): Promise<boolean> {
try {
await withTimeout(
() => prisma.$queryRaw`SELECT 1`,
5000,
'healthCheck'
);
return true;
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Unknown error';
console.error('❌ [DB-HEALTH] Database connection check failed:', errorMsg);
return false;
}
}
export { prisma };

View File

@@ -1,53 +1,21 @@
import { PrismaClient } from "@prisma/client";
// Deklarasikan variabel global untuk menandai apakah listener sudah ditambahkan
declare global {
var prisma: PrismaClient;
var prismaListenersAdded: boolean; // Flag untuk menandai listener
var prisma: PrismaClient | undefined;
}
let prisma: PrismaClient;
if (process.env.NODE_ENV === "production") {
prisma = new PrismaClient();
} else {
if (!global.prisma) {
global.prisma = new PrismaClient();
}
prisma = global.prisma;
if (!process.env.DATABASE_URL) {
throw new Error("DATABASE_URL is required but not found in environment variables");
}
// Tambahkan listener hanya jika belum ditambahkan sebelumnya
if (!global.prismaListenersAdded) {
// Handle uncaught errors
process.on("uncaughtException", async (error) => {
console.error("Uncaught Exception:", error);
await prisma.$disconnect();
process.exit(1);
const prisma =
global.prisma ??
new PrismaClient({
log: process.env.NODE_ENV === "development" ? ["error", "warn", "query"] : ["error", "warn"],
});
// Handle unhandled promise rejections
process.on("unhandledRejection", async (error) => {
console.error("Unhandled Rejection:", error);
await prisma.$disconnect();
process.exit(1);
});
// Handle graceful shutdown
process.on("SIGINT", async () => {
console.log("Received SIGINT signal. Closing database connections...");
await prisma.$disconnect();
process.exit(0);
});
process.on("SIGTERM", async () => {
console.log("Received SIGTERM signal. Closing database connections...");
await prisma.$disconnect();
process.exit(0);
});
// Tandai bahwa listener sudah ditambahkan
global.prismaListenersAdded = true;
}
// Selalu assign ke global agar hanya ada 1 instance (dev: cegah hot-reload, prod: cegah multiple instances)
global.prisma = prisma;
export default prisma;
export { prisma };

24
src/lib/prismaUtils.ts Normal file
View File

@@ -0,0 +1,24 @@
import { prisma } from './prisma';
/**
* Utility function to safely execute Prisma operations
* This prevents improper disconnection of the Prisma client
* which was causing high CPU usage and connection pool issues
*/
export async function executeDbOperation<T>(
operation: () => Promise<T>,
errorMessage: string = "Database operation failed"
): Promise<{ success: boolean; data?: T; error?: string }> {
try {
const data = await operation();
return { success: true, data };
} catch (error) {
console.error(errorMessage, error);
return { success: false, error: (error as Error).message };
}
// Note: We intentionally do NOT call prisma.$disconnect() here
// Prisma manages connection pooling automatically and disconnecting
// on each request causes performance issues
}
export { prisma };

View File

@@ -65,10 +65,7 @@ export const middleware = async (req: NextRequest) => {
const { pathname } = req.nextUrl;
const apiBaseUrl = new URL(req.url).origin || process.env.NEXT_PUBLIC_API_URL;
const dbUrl = process.env.DATABASE_URL;
console.log("DATABASE_URL >>", dbUrl);
console.log("URL Access >>", req.url);
const apiBaseUrl = process.env.NEXT_PUBLIC_API_URL || new URL(req.url).origin;
// Handle CORS preflight
const corsResponse = handleCors(req);

View File

@@ -4,7 +4,66 @@ declare global {
var mqtt_client: mqtt.MqttClient;
}
const mqtt_client =
globalThis.mqtt_client || mqtt.connect("wss://io.wibudev.com");
// Initialize MQTT client with proper error handling and reconnection settings
let mqtt_client: mqtt.MqttClient;
if (typeof window === 'undefined') {
// Server-side code
mqtt_client = globalThis.mqtt_client || (() => {
const client = mqtt.connect("wss://io.wibudev.com", {
reconnectPeriod: 5000, // Reconnect every 5 seconds
connectTimeout: 30 * 1000, // 30 second timeout
// Clean session to avoid message queue buildup
clean: true,
// Reduce unnecessary pings
keepalive: 60
});
// Prevent multiple initializations
globalThis.mqtt_client = client;
// Add error handling
client.on('error', (error) => {
console.error('MQTT Connection Error:', error);
});
client.on('reconnect', () => {
console.log('MQTT Reconnecting...');
});
client.on('close', () => {
console.log('MQTT Connection Closed');
});
return client;
})();
} else {
// Client-side code - initialize only once
if (!(globalThis as any).mqtt_client) {
(globalThis as any).mqtt_client = mqtt.connect("wss://io.wibudev.com", {
reconnectPeriod: 5000, // Reconnect every 5 seconds
connectTimeout: 30 * 1000, // 30 second timeout
// Clean session to avoid message queue buildup
clean: true,
// Reduce unnecessary pings
keepalive: 60
});
// Add error handling
(globalThis as any).mqtt_client.on('error', (error: any) => {
console.error('MQTT Connection Error:', error);
});
(globalThis as any).mqtt_client.on('reconnect', () => {
console.log('MQTT Reconnecting...');
});
(globalThis as any).mqtt_client.on('close', () => {
console.log('MQTT Connection Closed');
});
}
mqtt_client = (globalThis as any).mqtt_client;
}
export default mqtt_client;

View File

@@ -3,20 +3,27 @@
import { useEffect } from "react";
import mqtt_client from "./mqtt_client";
export default function MqttLoader() {
export default function MqttLoader() {
useEffect(() => {
mqtt_client.on("connect", () => {
console.log("connected");
});
// Only set up connection handlers once
const handleConnect = () => {
console.log("MQTT connected");
};
const handleError = (error: any) => {
console.error("MQTT Error:", error);
};
// Subscribe to events
mqtt_client.on("connect", handleConnect);
mqtt_client.on("error", handleError);
// Cleanup function to unsubscribe when component unmounts
return () => {
mqtt_client.off("connect", handleConnect);
mqtt_client.off("error", handleError);
};
}, []);
return null;
// <>
// <Stack>
// <Button onClick={onClick}>Tekan</Button>
// <Button onClick={onClick2}>Tekan 2</Button>
// </Stack>
// </>
// );
}

4
types/env.d.ts vendored
View File

@@ -11,5 +11,9 @@ declare namespace NodeJS {
NEXT_PUBLIC_BASE_SESSION_KEY?: string;
RESEND_APIKEY?: string;
WA_SERVER_TOKEN?: string;
FIREBASE_ADMIN_PRIVATE_KEY?: string;
FIREBASE_ADMIN_CLIENT_EMAIL?: string;
FIREBASE_ADMIN_PROJECT_ID?: string;
NEXT_PUBLIC_API_URL?: string;
}
}

View File

@@ -1,28 +0,0 @@
// const data = [
// {
// authorId: "clx8pl7r90005su4mldioo0v1",
// Donasi: {
// id: "clyr304q0000410ljvzms3mag",
// title: "Donasi Bencana Alam Aceh",
// },
// },
// {
// authorId: "clx8pl7r90005su4mldioo0v1",
// Donasi: {
// id: "clyr304q0000410ljvzms3mag",
// title: "Donasi Bencana Alam Aceh",
// },
// },
// {
// authorId: "clycka5eu0001ina3i1ssgze9",
// Donasi: {
// id: "clyr304q0000410ljvzms3mag",
// title: "Donasi Bencana Alam Aceh",
// },
// },
// ];
console.error("errornya disini klik aja",import.meta.url);
console.log(new Set(data.map((d) => d.authorId)));