137 lines
9.6 KiB
Markdown
137 lines
9.6 KiB
Markdown
# Setrip — Trust & Trip Detail Roadmap
|
|
|
|
Status implementasi yang menaikkan kepercayaan calon peserta — trip detail experience yang meyakinkan + sistem reputasi organizer yang transparan.
|
|
|
|
> **Prinsip:** trust = fungsi dari (a) kelengkapan informasi trip dan (b) reputasi organizer yang transparan. Setiap fitur dievaluasi: apakah memberi calon peserta alasan obyektif untuk percaya?
|
|
|
|
---
|
|
|
|
## Audit state sekarang (baseline)
|
|
|
|
**Trip detail (~80% sudah ada):**
|
|
- ✅ Itinerary, include/exclude, meeting point — schema lengkap di `Trip`, ditampilkan via `TripProgramBlock`.
|
|
- ✅ Participant preview (kartu confirmed peserta dengan avatar, kota, interests).
|
|
- ✅ Slot tersisa (X / Y) dengan progress bar berwarna.
|
|
- ⚠️ Urgency message ada tapi tidak mencolok saat slot menipis.
|
|
- ⚠️ Itinerary text bebas — organizer butuh hint format supaya konsisten isi lengkap.
|
|
|
|
**Review/trust (~40% sudah ada):**
|
|
- ✅ Model `TripReview` (rating + comment per trip+user, unique).
|
|
- ✅ Trust panel di trip detail (verified badge, trips created, avg rating, review count).
|
|
- ❌ Profil organizer publik `/u/[id]` belum tampilkan rating/review aggregate.
|
|
- ❌ Belum ada total participants served, completion rate, rating breakdown.
|
|
- ❌ Belum ada list ulasan terkumpul per organizer.
|
|
- ❌ `TripStatus.COMPLETED` enum-nya ada tapi tidak pernah di-set.
|
|
|
|
File baseline: [app/trips/[id]/page.tsx](app/trips/%5Bid%5D/page.tsx), [server/services/trust.service.ts](server/services/trust.service.ts), [server/services/review.service.ts](server/services/review.service.ts), [app/u/[id]/page.tsx](app/u/%5Bid%5D/page.tsx).
|
|
|
|
---
|
|
|
|
## PR-1 — Trip Detail Polish (UI only) ⏳
|
|
|
|
Cosmetic. Tidak ada migration, tidak ada perubahan service/repo.
|
|
|
|
| # | Item | Status | File |
|
|
|---|---|---|---|
|
|
| 1.1 | Urgency badge mencolok saat `spotsLeft <= 3` ("⚡ Tinggal X spot!") di header progress bar | ⏳ | [app/trips/[id]/page.tsx](app/trips/%5Bid%5D/page.tsx) |
|
|
| 1.2 | Participant preview ringkas di blok progress ("👥 Sudah join: Andi, Rina, Budi +4") — first impression tanpa scroll | ⏳ | [app/trips/[id]/page.tsx](app/trips/%5Bid%5D/page.tsx) |
|
|
| 1.3 | Hint deskriptif + placeholder lebih konkret di field itinerary form create-trip | ⏳ | [features/trip/components/create-trip-form.tsx](features/trip/components/create-trip-form.tsx) |
|
|
|
|
**Tindakan manual:** tidak ada.
|
|
|
|
---
|
|
|
|
## PR-2 — Organizer Trust Aggregates (service + UI, tanpa migration) ✅
|
|
|
|
Selesai. `tsc --noEmit` lulus. Tanpa schema baru, tanpa migration.
|
|
|
|
**Keputusan asumsi yang dipakai:**
|
|
- `tripsCompleted` ≠ `Trip.status = COMPLETED` (status itu tidak pernah di-set). Pakai `endDate < now()` (fallback `date < now()`) AND `status != CLOSED`.
|
|
- `tripsCancelled` = `Trip.status = CLOSED` (organizer batalkan trip eksplisit).
|
|
- `completionRate` butuh sample ≥ 3 (`COMPLETION_RATE_MIN_SAMPLE` di [lib/trust.ts](lib/trust.ts)) supaya tidak menyesatkan organizer baru.
|
|
- Rating breakdown di-render sebagai bar chart kecil (visual cue lebih kuat dari angka mentah).
|
|
- `OrganizerStatsPanel` di profil publik tidak di-render untuk user yang murni peserta — query Prisma juga di-skip kalau `organizedTrips.length === 0 && !isVerifiedOrganizer`.
|
|
- Trip detail: stat box "Trip dibuat" diganti jadi **"Trip selesai"** (lebih meaningful) + tambah **"Peserta dilayani"**. Total 3 stat box, masih kompak.
|
|
|
|
| # | Item | Status | File |
|
|
|---|---|---|---|
|
|
| 2.1 | Extend `OrganizerTrust` type: `tripsCompleted`, `tripsCancelled`, `totalParticipantsServed`, `completionRate`, `ratingBreakdown` | ✅ | [server/services/trust.service.ts](server/services/trust.service.ts) |
|
|
| 2.2 | `tripsCompleted` di-derive dari `endDate < now()` (fallback `date < now()`) AND `status != CLOSED` | ✅ | [server/services/trust.service.ts](server/services/trust.service.ts) |
|
|
| 2.3 | `totalParticipantsServed` = count `TripParticipant CONFIRMED` di trip yang sudah lewat & tidak dibatalkan | ✅ | [server/services/trust.service.ts](server/services/trust.service.ts) |
|
|
| 2.4 | `completionRate` = `tripsCompleted / (tripsCompleted + tripsCancelled)`. Null bila sample < 3 | ✅ | [server/services/trust.service.ts](server/services/trust.service.ts), [lib/trust.ts](lib/trust.ts) |
|
|
| 2.5 | `ratingBreakdown` via `prisma.tripReview.groupBy({ by: ['rating'] })` | ✅ | [server/services/trust.service.ts](server/services/trust.service.ts) |
|
|
| 2.6 | Komponen `OrganizerStatsPanel` (badges + 4 stat box + bar chart breakdown) | ✅ | [features/profile/components/organizer-stats-panel.tsx](features/profile/components/organizer-stats-panel.tsx) |
|
|
| 2.7 | Update `OrganizerTrustPanel` di trip detail — Trip selesai (+ subtitle "berjalan"), Peserta dilayani, Rating | ✅ | [features/trip/components/organizer-trust-panel.tsx](features/trip/components/organizer-trust-panel.tsx) |
|
|
| 2.8 | Render `OrganizerStatsPanel` di `/u/[id]` (skip query untuk non-organizer) | ✅ | [app/u/[id]/page.tsx](app/u/%5Bid%5D/page.tsx) |
|
|
|
|
**Tindakan manual:** tidak ada.
|
|
|
|
---
|
|
|
|
## PR-3 — Organizer Reviews Aggregator (service + UI, tanpa migration) ✅
|
|
|
|
Selesai. `tsc --noEmit` lulus. Tanpa schema baru, tanpa migration.
|
|
|
|
**Keputusan asumsi yang dipakai:**
|
|
- Default limit 20 ulasan terbaru — cukup untuk MVP, tidak perlu pagination dulu.
|
|
- Komponen RSC (server component, no `"use client"`) — pure render, tidak ada interaktivitas.
|
|
- Tipe `OrganizerReviewItem` di-extract dari `Awaited<ReturnType<typeof reviewRepo.findByOrganizer>>[number]` supaya schema repo = sumber kebenaran tanpa duplikasi tipe.
|
|
- Fetch trust + reviews via single `Promise.all` di `/u/[id]` (paralel, hemat 1 round-trip).
|
|
- Komponen di-skip kalau `reviews.length === 0` — biar tidak makin "kosong" di profil organizer baru. Stats panel sudah punya pesan "Belum ada ulasan".
|
|
- Rating ditampilkan sebagai bintang penuh (`★★★★☆`) bukan angka — visual cue lebih kuat untuk testimoni.
|
|
- Header section: "X terbaru dari Y ulasan" kalau di-limit, atau cuma "Y ulasan" kalau seluruh list ditampilkan.
|
|
|
|
| # | Item | Status | File |
|
|
|---|---|---|---|
|
|
| 3.1 | `reviewService.getReviewsByOrganizer(organizerId, limit?)` + tipe `OrganizerReviewItem` | ✅ | [server/services/review.service.ts](server/services/review.service.ts) |
|
|
| 3.2 | Repo helper `findByOrganizer` (default limit 20, urut newest, include user + trip) | ✅ | [server/repositories/review.repo.ts](server/repositories/review.repo.ts) |
|
|
| 3.3 | Komponen `OrganizerReviewsList` (avatar + name + bintang + trip link + tanggal + comment) | ✅ | [features/review/components/organizer-reviews-list.tsx](features/review/components/organizer-reviews-list.tsx) |
|
|
| 3.4 | Render di `/u/[id]` di bawah `OrganizerStatsPanel`, fetch via `Promise.all` paralel | ✅ | [app/u/[id]/page.tsx](app/u/%5Bid%5D/page.tsx) |
|
|
|
|
**Tindakan manual:** tidak ada.
|
|
|
|
---
|
|
|
|
## PR-4 — Trip Completion Mechanism (opsional, butuh diskusi) ⏳
|
|
|
|
Saat ini `Trip.status = COMPLETED` tidak pernah di-set oleh kode mana pun. PR ini hanya perlu kalau ingin pakai `status` sebagai sumber kebenaran formal (bukan computed-from-`endDate`).
|
|
|
|
Opsi:
|
|
- **A. Manual** — organizer klik "Tandai trip selesai" pasca-pulang. Pro: kontrol di organizer. Con: gampang lupa, `status` tidak accurate kalau organizer pasif.
|
|
- **B. Cron job** — daily job set `status = COMPLETED` untuk trip dengan `endDate < today() AND status IN ('OPEN','FULL')`. Pro: otomatis akurat. Con: butuh infra cron (belum ada di project).
|
|
- **C. Skip — biarkan computed-from-`endDate`** di service layer. Pro: paling sederhana, sejalan dengan PR-2 yang juga compute on-the-fly. Con: field `status` jadi sebagian "live" (OPEN/FULL/CLOSED murni, COMPLETED computed).
|
|
|
|
**Rekomendasi:** **C** dulu sampai ada kebutuhan riil untuk transisi formal (mis. trigger payout organizer pasca-trip atau notif post-trip continuity di Phase C SOCIAL_ROADMAP).
|
|
|
|
| # | Item | Status |
|
|
|---|---|---|
|
|
| 4.1 | Pilih opsi A/B/C | ⏳ |
|
|
| 4.2 | Implementasi sesuai pilihan (atau dokumentasikan keputusan kalau C) | ⏳ |
|
|
|
|
---
|
|
|
|
## ❌ Anti-list (yang harus DITOLAK kalau muncul)
|
|
|
|
- **Model `OrganizerReview` terpisah** — `TripReview` sudah cukup, 1 trip = 1 organizer. Bikin model baru = duplikasi data + sumber kebenaran ambigu.
|
|
- **Denormalisasi cache** (mis. `User.cachedAvgRating`) sebelum aggregate query terbukti lambat. Premature optimization → drift jadi tech debt cepat.
|
|
- **Auto-hapus review buruk** atau organizer "respon" review (untuk MVP). Bisa nanti — fokus dulu menampilkan data jujur.
|
|
- **"Trust score" gabungan satu angka** — kasih breakdown agar calon peserta evaluasi sendiri. Single number gampang dimanipulasi & menyesatkan.
|
|
- **Review user/peserta** (no-show, kooperatif?) — itu C6 di [SOCIAL_ROADMAP.md](SOCIAL_ROADMAP.md). Scope berbeda, jangan campur.
|
|
- **Rating dengan setengah bintang / kustom 1-10** — tetap 1-5 integer (sudah di schema). Granularitas lebih halus tidak meningkatkan trust, hanya menambah noise.
|
|
|
|
---
|
|
|
|
## Saran phasing
|
|
|
|
PR berurutan. Setiap PR mandiri (siap di-deploy):
|
|
|
|
1. **PR-1** — Trip detail polish. Cepat, low-risk, no migration. **Mulai dari sini.**
|
|
2. **PR-2** — Trust aggregates di service + UI. Read-only, no migration.
|
|
3. **PR-3** — Reviews list per organizer di service + UI. Read-only, no migration.
|
|
4. **PR-4** — Diskusi opsi completion mechanism (atau skip kalau opsi C dipilih).
|
|
|
|
**Pertanyaan terbuka sebelum PR-2:**
|
|
1. Apakah `tripsCompleted` count termasuk trip dengan `status = CLOSED` yang `endDate < now()`? Saran: tidak — CLOSED = dibatalkan, dipisah ke `tripsCancelled`.
|
|
2. Threshold minimum supaya `completionRate` ditampilkan? Saran: min 3 trip selesai supaya angka tidak menyesatkan (1 trip dibatalkan dari 1 trip = 0% looks bad untuk organizer baru).
|
|
3. Tampilkan rating breakdown sebagai bar chart atau hanya angka? Saran: bar chart kecil — visual cue lebih kuat untuk credibility.
|