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