- ✅ - ✅ - ✅
5.3 KiB
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/refundstanpa konteks trip atau cara cancel trip-nya.
Baseline
- ✅
tripService.closeTrip(tripId, organizerId)di server/services/trip.service.ts sudah handle cancel + auto-refund semua booking PAID atomically. Hanya menerimaorganizerId— perlu varian admin. - ✅
tripRepo.findAll()dantripRepo.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 includeparticipants,images,reviews,itineraryItems. - Tampilkan semua participant (PENDING/CONFIRMED/CANCELLED) — admin perlu konteks lengkap.
- Drill-down ke booking detail (lihat 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 |
| 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 |
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.closeTripagar terimaactor: { 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). TambahadminNotedi refund record kalau actor ADMIN supaya audit trail jelas. - Tambah kolom
Trip.cancelledByAdminId(nullable) +Trip.cancelledReasondi 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 — 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 |
| 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 — opsional, transparansi |
Tindakan manual:
- Setelah deploy, brief admin tentang kapan boleh pakai (kriteria: organizer unreachable >7 hari, dispute peserta tidak terselesaikan, safety issue).
- 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 |
| 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) |
Tindakan manual: tidak ada (skip phase ini sampai ada keluhan konkret).