Compare commits

...

4 Commits

Author SHA1 Message Date
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
1c9459dcf3 Fix comment forum
Forum API (Mobile)
- src/app/api/mobile/forum/[id]/comment/route.ts

### No Issue
2026-01-29 17:41:21 +08:00
8b54f5ca65 Fix send whatsapp
Auth API
- src/app/api/auth/login/route.ts
- src/app/api/auth/mobile-login/route.ts
- src/app/api/auth/mobile-register/route.ts
- src/app/api/auth/resend/route.ts

User API (Mobile)
- src/app/api/mobile/user/route.ts
- src/app/api/mobile/admin/user/[id]/route.ts

Utility
- src/lib/code-otp-sender.ts

### No issue
2026-01-29 15:04:40 +08:00
14 changed files with 342 additions and 42 deletions

View File

@@ -2,6 +2,8 @@
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.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)

16
PROMPT-AI.md Normal file
View File

@@ -0,0 +1,16 @@
File utama: src/app/api/mobile/notification/[id]/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.

201
QWEN.md Normal file
View File

@@ -0,0 +1,201 @@
# HIPMI Project - QWEN.md
## Project Overview
HIPMI (Himpunan Pengusaha Muda Indonesia) is a comprehensive Next.js-based web application built for the Indonesian Young Entrepreneurs Association. The project is a sophisticated platform that provides multiple business functionalities including investment management, donations, events, job listings, forums, voting systems, and collaborative projects.
### 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

@@ -1,6 +1,6 @@
{
"name": "hipmi",
"version": "1.5.38",
"version": "1.5.39",
"private": true,
"prisma": {
"seed": "bun prisma/seed.ts"

View File

@@ -2,7 +2,7 @@ import { prisma } from "@/lib";
import { randomOTP } from "@/app_modules/auth/fun/rondom_otp";
import backendLogger from "@/util/backendLogger";
import { NextResponse } from "next/server";
import { sendCodeOtp } from "@/lib/code-otp-sender";
import { funSendToWhatsApp } from "@/lib/code-otp-sender";
export async function POST(req: Request) {
if (req.method !== "POST") {
@@ -30,7 +30,7 @@ export async function POST(req: Request) {
{ status: 400 },
);
const resSendCode = await sendCodeOtp({
const resSendCode = await funSendToWhatsApp({
nomor,
codeOtp: codeOtp.toString(),
});

View File

@@ -1,7 +1,7 @@
import { prisma } from "@/lib";
import { randomOTP } from "@/app_modules/auth/fun/rondom_otp";
import { NextResponse } from "next/server";
import { sendCodeOtp } from "@/lib/code-otp-sender";
import { funSendToWhatsApp } from "@/lib/code-otp-sender";
export async function POST(req: Request) {
try {
@@ -35,7 +35,7 @@ export async function POST(req: Request) {
{ status: 400 },
);
const resSendCode = await sendCodeOtp({
const resSendCode = await funSendToWhatsApp({
nomor,
codeOtp: codeOtp.toString(),
});

View File

@@ -7,7 +7,7 @@ import {
NotificationMobileBodyType,
NotificationMobileTitleType,
} from "../../../../../types/type-mobile-notification";
import { sendCodeOtp } from "@/lib/code-otp-sender";
import { funSendToWhatsApp } from "@/lib/code-otp-sender";
export async function POST(req: Request) {
if (req.method !== "POST") {
@@ -70,7 +70,7 @@ export async function POST(req: Request) {
{ status: 400 }
);
const resSendCode = await sendCodeOtp({
const resSendCode = await funSendToWhatsApp({
nomor: data.nomor,
codeOtp: codeOtp.toString(),
});

View File

@@ -2,7 +2,7 @@ import { prisma } from "@/lib";
import { randomOTP } from "@/app_modules/auth/fun/rondom_otp";
import backendLogger from "@/util/backendLogger";
import { NextResponse } from "next/server";
import { sendCodeOtp } from "@/lib/code-otp-sender";
import { funSendToWhatsApp } from "@/lib/code-otp-sender";
export async function POST(req: Request) {
if (req.method !== "POST") {
@@ -17,7 +17,7 @@ export async function POST(req: Request) {
const body = await req.json();
const { nomor } = body;
const resSendCode = await sendCodeOtp({
const resSendCode = await funSendToWhatsApp({
nomor,
codeOtp: codeOtp.toString(),
});

View File

@@ -1,4 +1,5 @@
import { prisma } from "@/lib";
import { funSendToWhatsApp } from "@/lib/code-otp-sender";
import _ from "lodash";
import { NextResponse } from "next/server";
@@ -50,8 +51,25 @@ async function PUT(request: Request, { params }: { params: { id: string } }) {
data: {
active: data.active,
},
select: {
nomor: true,
},
});
if (data.active) {
const resSendCode = await funSendToWhatsApp({
nomor: updateData.nomor,
newMessage:
"Halo sahabat HIConnect, \nSelamat akun anda telah aktif ! \n\n*Pesan ini di kirim secara otomatis, tidak perlu di balas.",
});
} else {
const resSendCode = await funSendToWhatsApp({
nomor: updateData.nomor,
newMessage:
"Halo sahabat HIConnect, \nMohon maaf akun anda telah dinonaktifkan ! Hubungi admin untuk informasi lebih lanjut. \n\n*Pesan ini di kirim secara otomatis, tidak perlu di balas.",
});
}
console.log("[Update Active Berhasil]", updateData);
} else if (category === "role") {
const fixName = _.startCase(data.role.replace(/_/g, " "));

View File

@@ -90,9 +90,15 @@ async function POST(request: Request, { params }: { params: { id: string } }) {
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 skipData = page * takeData - takeData;
try {
const data = await prisma.forum_Komentar.findMany({
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
orderBy: {
createdAt: "desc",
},

View File

@@ -16,7 +16,13 @@ export async function GET(
const fixCategory = _.upperCase(category || "");
try {
const page = Number(searchParams.get("page"));
const takeData = 10;
const skipData = page ? page * takeData - takeData : 0;
const data = await prisma.notifikasi.findMany({
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
orderBy: {
createdAt: "desc",
},
@@ -26,12 +32,40 @@ 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 },

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

@@ -5,8 +5,16 @@ export async function GET(request: Request) {
try {
const { searchParams } = new URL(request.url);
const search = searchParams.get("search");
const page = Number(searchParams.get("page"));
const takeData = 10;
const skipData = page * takeData - takeData;
console.log("SEARCH", search);
console.log("PAGE", page);
const data = await prisma.user.findMany({
take: page ? takeData : undefined,
skip: page ? skipData : undefined,
orderBy: {
username: "asc",
},
@@ -43,16 +51,12 @@ export async function GET(request: Request) {
},
});
return NextResponse.json(
{
success: true,
message: "Berhasil mendapatkan data",
data: data,
},
{
status: 200,
}
);
return NextResponse.json({
status: 200,
success: true,
message: "Berhasil mendapatkan data",
data: data,
});
} catch (error) {
return NextResponse.json(
{
@@ -62,7 +66,7 @@ export async function GET(request: Request) {
},
{
status: 500,
}
},
);
}

View File

@@ -1,13 +1,16 @@
const sendCodeOtp = async ({
nomor,
codeOtp,
newMessage,
}: {
nomor: string;
codeOtp: string;
codeOtp?: string;
newMessage?: string;
}) => {
const msg = `HIPMI%20-%20Kode%20ini%20bersifat%20RAHASIA%20dan%20JANGAN%20DI%20BAGIKAN%20KEPADA%20SIAPAPUN%2C%20termasuk%20anggota%20ataupun%20pengurus%20HIPMI%20lainnya.%20Kode%20OTP%20anda%3A%20${codeOtp}.`;
const msg = newMessage || `HIPMI - Kode ini bersifat RAHASIA dan JANGAN DI BAGIKAN KEPADA SIAPAPUN, termasuk anggota ataupun pengurus HIPMI lainnya.\n\n>> Kode OTP anda: ${codeOtp}.`;
const enCode = encodeURIComponent(msg);
const res = await fetch(
`https://cld-dkr-prod-wajs-server.wibudev.com/api/wa/code?nom=${nomor}&text=${msg}`,
`https://cld-dkr-prod-wajs-server.wibudev.com/api/wa/code?nom=${nomor}&text=${enCode}`,
{
cache: "no-cache",
headers: {
@@ -25,4 +28,4 @@ const sendCodeOtp = async ({
return res;
};
export { sendCodeOtp };
export { sendCodeOtp as funSendToWhatsApp };