# Setrip — Admin Trip Operations Roadmap Admin perlu visibilitas penuh atas trip dan bisa intervensi (cancel + auto-refund) saat organizer unreachable atau ada masalah safety. > **Skenario nyata:** peserta lapor trip berjalan tidak sesuai itinerary. Organizer tidak responsif. Hari berikutnya peserta minta refund. Saat ini admin harus refund satu-satu manual via `/admin/refunds` tanpa konteks trip atau cara cancel trip-nya. --- ## Baseline - ✅ `tripService.closeTrip(tripId, organizerId)` di [server/services/trip.service.ts](server/services/trip.service.ts) sudah handle cancel + auto-refund semua booking PAID atomically. Hanya menerima `organizerId` — perlu varian admin. - ✅ `tripRepo.findAll()` dan `tripRepo.findById()` ada — siap dipakai untuk admin list/detail. - ❌ Tidak ada page `/admin/trips`. - ❌ Tidak ada UI search/filter trip untuk admin. - ❌ Tidak ada UI view detail trip dari sisi admin (kondisi participant, booking, payment). --- ## Phase 1 — Trip List + Detail View (admin read-only) ⏳ Foundation. Tanpa cara cari & lihat trip, admin tidak tahu apa yang mau di-intervene. **Keputusan asumsi:** - Reuse `tripRepo.findAll()` tapi tambah filter param: `status`, `organizerId`, `q` (search title/destination). - Detail page reuse `tripService.getTripById()` yang sudah include `participants`, `images`, `reviews`, `itineraryItems`. - Tampilkan **semua participant** (PENDING/CONFIRMED/CANCELLED) — admin perlu konteks lengkap. - Drill-down ke booking detail (lihat [ADMIN_PAYMENT_OPS_ROADMAP.md](ADMIN_PAYMENT_OPS_ROADMAP.md)) untuk lihat payment timeline. | # | Item | Status | File | |---|---|---|---| | 1.1 | `tripRepo.searchForAdmin({ q?, status?, organizerId?, dateFrom?, dateTo? })` | ⏳ | [server/repositories/trip.repo.ts](server/repositories/trip.repo.ts) | | 1.2 | Page `/admin/trips` — list + tab status (OPEN/FULL/CLOSED/COMPLETED) + search bar | ⏳ | `app/admin/trips/page.tsx` | | 1.3 | Filter: tanggal berangkat range, organizer (dropdown), kategori | ⏳ | `app/admin/trips/page.tsx` | | 1.4 | Page `/admin/trips/[id]` — full detail (trip core + itinerary items + participants + bookings ringkasan) | ⏳ | `app/admin/trips/[id]/page.tsx` | | 1.5 | Badge metrics di detail: peserta PAID/AWAITING/PENDING, total revenue (sum amount PAID), refund total | ⏳ | `app/admin/trips/[id]/page.tsx` | | 1.6 | Tambah link "Trips" di admin navbar | ⏳ | [app/admin/layout.tsx](app/admin/layout.tsx) | **Tindakan manual:** tidak ada. --- ## Phase 2 — Admin Force-Cancel Trip dengan Auto-Refund ⏳ Tombol "Cancel trip" di admin detail page yang setara dengan organizer cancel, tapi dilakukan oleh admin untuk emergency intervention. **Keputusan asumsi:** - **Tidak buat method baru di service**. Refactor `tripService.closeTrip` agar terima `actor: { type: "ORGANIZER", id } | { type: "ADMIN", id, reason }`. Atomic dalam satu serializable transaction (sama seperti existing). - Refund yang dibuat pakai `RefundReason.ORGANIZER_CANCELLED` (tetap, karena dari perspektif peserta sama saja). Tambah `adminNote` di refund record kalau actor ADMIN supaya audit trail jelas. - Tambah kolom `Trip.cancelledByAdminId` (nullable) + `Trip.cancelledReason` di schema — bukan kolom umum, hanya saat admin yang cancel. - Modal konfirmasi wajib tampilkan: jumlah booking PAID yang akan auto-refund, total nominal. Kalau organizer yang biasa cancel sudah ada confirm modal di [cancel-trip-button.tsx](features/trip/components/cancel-trip-button.tsx) — reuse pola. - Idempotent: kalau trip sudah CLOSED, tolak dengan pesan jelas. | # | Item | Status | File | |---|---|---|---| | 2.1 | Migration: tambah `cancelledByAdminId` (FK User) + `cancelledReason` di `Trip` | ⏳ | `prisma/migrations/` | | 2.2 | Refactor `tripService.closeTrip` terima `actor` discriminated union | ⏳ | [server/services/trip.service.ts](server/services/trip.service.ts) | | 2.3 | Server action `adminCancelTripAction(tripId, reason)` — guard `isAdmin`, panggil closeTrip dengan actor ADMIN | ⏳ | `features/trip/actions.ts` | | 2.4 | UI: tombol "Cancel trip (admin)" di `/admin/trips/[id]` dengan modal konfirmasi + textarea reason wajib | ⏳ | `app/admin/trips/[id]/page.tsx` (atau component terpisah) | | 2.5 | Tampilkan badge "Dibatalkan admin" + reason di trip detail saat `cancelledByAdminId` not null | ⏳ | [app/(public)/trips/[id]/page.tsx](app/(public)/trips/[id]/page.tsx) — opsional, transparansi | **Tindakan manual:** 1. Setelah deploy, brief admin tentang kapan boleh pakai (kriteria: organizer unreachable >7 hari, dispute peserta tidak terselesaikan, safety issue). 2. Tulis SOP internal: kategori reason yang valid + template komunikasi ke peserta. --- ## Phase 3 — Trip Edit Override (opsional, low priority) ⏳ Admin bisa edit field non-critical (description, meetingPoint, itinerary) atas request organizer saat organizer tidak bisa login. Skip untuk MVP. | # | Item | Status | File | |---|---|---|---| | 3.1 | `tripService.adminUpdateTrip(tripId, partial, adminId, reason)` — whitelist field | ⏳ | [server/services/trip.service.ts](server/services/trip.service.ts) | | 3.2 | UI form edit di `/admin/trips/[id]/edit` | ⏳ | `app/admin/trips/[id]/edit/page.tsx` | | 3.3 | Audit log entry untuk setiap edit (siapa, field apa, before/after) | ⏳ | TBD (lihat [ADMIN_AUDIT_ROADMAP.md](ADMIN_AUDIT_ROADMAP.md)) | **Tindakan manual:** tidak ada (skip phase ini sampai ada keluhan konkret).