131 lines
7.9 KiB
Markdown
131 lines
7.9 KiB
Markdown
# SeTrip
|
||
|
||
Aplikasi open trip pendakian yang mempertemukan **organizer** (pembuat trip) dengan **peserta** yang ingin naik gunung bareng atau mencari teman trip.
|
||
|
||
Stack: [Next.js](https://nextjs.org) (App Router), NextAuth, Prisma (PostgreSQL), Tailwind CSS.
|
||
|
||
## Alur aplikasi
|
||
|
||
### 1. Autentikasi
|
||
|
||
- Pengguna baru mendaftar di `/register` (nama, email, password disimpan di database).
|
||
- Login di `/login` melalui NextAuth; sesi dipakai di server action dan di halaman client (navbar, form buat trip, join).
|
||
|
||
Tanpa login, pengguna tetap bisa melihat daftar trip dan detail trip, tetapi tidak bisa membuat trip atau join.
|
||
|
||
### 2. Organizer: membuat trip
|
||
|
||
1. Setelah login, organizer membuka **Buat Trip** (`/create-trip`) dari navbar, `/trips`, beranda, atau tombol (+).
|
||
2. Form memvalidasi sesi; jika belum login, ditampilkan ajakan login.
|
||
3. Organizer mengisi judul, gunung, lokasi, deskripsi (opsional), **meeting point**, **itinerary**, **termasuk / tidak termasuk** (opsional), rentang tanggal berangkat–pulang, maks peserta, harga (Rupiah), dan URL gambar opsional.
|
||
4. Submit → `createTripAction` → validasi Zod → `tripService.createTrip` menulis `Trip` ke database (status default **OPEN**) beserta gambar jika ada.
|
||
5. Pengguna diarahkan ke detail trip `/trips/[id]`.
|
||
|
||
Organizer **tidak** bisa join trip sendiri; di detail trip ditampilkan bahwa dia adalah organizer trip ini.
|
||
|
||
**Verifikasi organizer (untuk trip berbayar).** Trip dengan harga > 0 hanya bisa dibuat oleh user yang sudah mengirim KTP, foto liveness (memegang kertas tulisan "SETRIP"), dan data rekening di `/verify` lalu disetujui admin di `/admin/verifications`. Trip gratis tidak butuh verifikasi. Organizer yang sudah disetujui tampil dengan badge **✅ Verified Organizer** di halaman detail trip.
|
||
|
||
### 3. Peserta: mencari trip
|
||
|
||
1. **Beranda** (`/`) dan **Open Trip** (`/trips`) menampilkan trip **OPEN** dengan tanggal berangkat yang masih relevan (`tripService.getOpenTrips`).
|
||
2. Filter pencarian (`TripFilter`) mengirim query string (`q`, `from`, `to`); penyaringan dilakukan di server.
|
||
3. Dari **TripCard**, pengguna membuka **detail** `/trips/[id]`: melihat info trip, kuota, panel kepercayaan organizer (jumlah trip, rating, badge jika ada), daftar peserta yang sudah disetujui, dan ulasan.
|
||
|
||
---
|
||
|
||
### 4. Alur lengkap: dari join hingga pembayaran sukses
|
||
|
||
Alur ini menggambarkan satu peserta dari pertama kali mendaftar sampai pembayaran dianggap selesai di aplikasi (pembayaran **manual** / transfer di luar gateway).
|
||
|
||
```text
|
||
[Peserta] [Sistem] [Organizer]
|
||
| | |
|
||
|-- buka detail trip ------>| |
|
||
| | |
|
||
|-- "Join Trip" ------------>| status partisipasi: PENDING |
|
||
| | (mengisi slot kuota trip) |
|
||
|<-- menunggu persetujuan ---| |
|
||
| |<-- lihat "Permintaan join" ------|
|
||
| | |
|
||
| |<-- "Setujui" atau "Tolak" -------|
|
||
| | Setujui -> CONFIRMED |
|
||
| | Tolak -> CANCELLED |
|
||
|<-- terkonfirmasi ikut -----| (jika disetujui) |
|
||
| | |
|
||
|-- transfer uang (WA/rek) --| (di luar app) |
|
||
| | |
|
||
|-- "Saya sudah bayar" ----->| markedPaidAt = sekarang |
|
||
|<-- menunggu konfirmasi ----| |
|
||
| | |
|
||
| |<-- "Konfirmasi pembayaran" ------|
|
||
| | paymentConfirmedAt = sekarang |
|
||
|<-- pembayaran dikonfirmasi-| |
|
||
```
|
||
|
||
**Langkah per langkah**
|
||
|
||
1. **Join trip**
|
||
Di halaman detail, peserta login menekan **Join Trip Sekarang** → `joinTripAction` → `tripService.joinTrip`.
|
||
Dibuat atau diaktifkan kembali baris `TripParticipant` dengan status **`PENDING`**. Kuota trip (peserta aktif: PENDING + CONFIRMED) bertambah; trip bisa menjadi **FULL** jika slot habis.
|
||
|
||
2. **Menunggu organizer**
|
||
Peserta melihat status bahwa permintaan **menunggu persetujuan organizer**. Dia bisa **Batal ikut** selama tanggal berangkat belum lewat (status menjadi `CANCELLED`, slot bisa longgar lagi).
|
||
|
||
3. **Organizer menyetujui atau menolak**
|
||
Di blok **Permintaan join**, organizer menekan **Setujui** → status partisipasi menjadi **`CONFIRMED`**, atau **Tolak** → **`CANCELLED`**.
|
||
Jika ada peserta yang sudah menandai bayar sebelum disetujui, penolakan juga membersihkan data tandai bayar pada baris itu.
|
||
|
||
4. **Peserta terkonfirmasi**
|
||
Setelah **CONFIRMED**, peserta dianggap bagian dari daftar “peserta terkonfirmasi” di halaman trip. Fitur ulasan trip setelah selesai hanya relevan untuk partisipasi terkonfirmasi (sesuai aturan di service).
|
||
|
||
5. **Menandai sudah membayar**
|
||
Peserta mentransfer sesuai instruksi organizer (di luar app). Di app dia menekan **Saya sudah bayar** → kolom **`markedPaidAt`** diisi. Tombol ini tidak muncul jika pembayaran sudah dikonfirmasi atau tanggal trip sudah lewat.
|
||
Aksi ini **atomik** (aman dari double klik).
|
||
|
||
6. **Organizer mengonfirmasi pembayaran**
|
||
Di blok **Konfirmasi pembayaran**, organizer mengecek mutasi/bukti lalu menekan **Konfirmasi pembayaran** untuk peserta bersangkutan → kolom **`paymentConfirmedAt`** diisi.
|
||
Ini **idempoten** (konfirmasi ganda tidak mengubah hasil akhir yang salah).
|
||
|
||
7. **Selesai (sukses payment di app)**
|
||
Peserta melihat bahwa pembayaran **sudah dikonfirmasi organizer**. Di sini alur “commit” peserta + uang dalam konteks SeTrip dianggap lengkap dari sisi data aplikasi.
|
||
|
||
**Catatan produk:** tidak ada payment gateway; bukti transfer dan nominal final tetap komunikasi organizer–peserta (misalnya WA).
|
||
|
||
### 5. Ringkasan peran data
|
||
|
||
| Konsep | Penyimpanan |
|
||
|--------|-------------|
|
||
| Trip | `Trip`: judul, gunung, lokasi, tanggal, kuota, harga, status trip (`OPEN` / `FULL` / …), meeting point, itinerary, termasuk/tidak termasuk, relasi ke organizer |
|
||
| Peserta | `TripParticipant` unik per `(tripId, userId)`: status **`PENDING`** / **`CONFIRMED`** / **`CANCELLED`**, serta **`markedPaidAt`** & **`paymentConfirmedAt`** untuk alur bayar manual |
|
||
| Organizer (kepercayaan) | `OrganizerVerification` (1-1 ke `User`) berisi KTP, foto liveness (memegang kertas "SETRIP"), rekening, dan status (`PENDING` / `APPROVED` / `REJECTED`); badge **Verified Organizer** muncul ketika `status === "APPROVED"` (helper `lib/trust.ts → isVerifiedOrganizer`). Agregat rating & jumlah trip dihitung dari ulasan & trip. |
|
||
| Persetujuan T&C / Privasi | `User.acceptedTermsAndPrivacy` + `User.acceptedAt`, dicentang saat registrasi (link ke `/terms` & `/privacy`). |
|
||
|
||
## Menjalankan secara lokal
|
||
|
||
Pastikan PostgreSQL berjalan dan variabel `DATABASE_URL` di `.env` mengarah ke database yang valid.
|
||
|
||
```bash
|
||
npm install
|
||
npx prisma migrate dev
|
||
npm run seed # opsional: data contoh
|
||
npm run dev
|
||
```
|
||
|
||
Buka [http://localhost:3000](http://localhost:3000).
|
||
|
||
## Perbaikan bug (yang relevan dengan join & listing)
|
||
|
||
1. **Join lagi setelah “Batal ikut”**
|
||
Satu user hanya satu baris per trip (`@@unique([tripId, userId])`). Jika baris sudah `CANCELLED`, join berikutnya **mengaktifkan kembali** partisipasi ke **`PENDING`** (bukan insert baru).
|
||
|
||
2. **Jumlah peserta di kartu / daftar**
|
||
`_count.participants` hanya menghitung status **bukan** `CANCELLED`, agar slot dan label “penuh” konsisten.
|
||
|
||
3. **Segar halaman setelah aksi**
|
||
`revalidatePath` dipanggil setelah join, batal, buat trip, setujui/tolak peserta, dan konfirmasi pembayaran agar daftar dan detail konsisten.
|
||
|
||
## Learn More
|
||
|
||
- [Next.js Documentation](https://nextjs.org/docs)
|
||
- [Prisma Documentation](https://www.prisma.io/docs)
|