feat: tampilkan jam_survei di admin list dan detail booking, tambah fillable jam_survei di model
This commit is contained in:
parent
0c29575843
commit
ca476cb8e6
|
|
@ -58,80 +58,169 @@ public function show(Request $request, $id)
|
|||
}
|
||||
|
||||
/**
|
||||
* Create booking baru
|
||||
* Create pengajuan baru (survei atau sewa) dari mobile
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$jenis = $request->input('jenis_pengajuan', 'sewa');
|
||||
|
||||
if ($jenis === 'survei') {
|
||||
return $this->storeSurvei($request);
|
||||
}
|
||||
|
||||
return $this->storeSewa($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simpan pengajuan survei
|
||||
*/
|
||||
private function storeSurvei(Request $request)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'kontrakan_id' => 'required|exists:kontrakans,id',
|
||||
'tanggal_survei' => 'required|date|after:today',
|
||||
'jam_survei' => 'required|string',
|
||||
'catatan' => 'nullable|string|max:1000',
|
||||
], [
|
||||
'kontrakan_id.required' => 'Kontrakan wajib dipilih',
|
||||
'tanggal_survei.required' => 'Tanggal survei wajib diisi',
|
||||
'tanggal_survei.after' => 'Tanggal survei harus setelah hari ini',
|
||||
'jam_survei.required' => 'Jam survei wajib diisi',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Validasi gagal',
|
||||
'error_code' => 'VALIDATION_ERROR',
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
$kontrakan = Kontrakan::find($request->kontrakan_id);
|
||||
|
||||
if (!$kontrakan) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Kontrakan tidak ditemukan',
|
||||
'error_code' => 'NOT_FOUND',
|
||||
], 404);
|
||||
}
|
||||
|
||||
// Untuk survei, kontrakan cukup tersedia (tidak occupied)
|
||||
if ($kontrakan->status === 'occupied') {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Kontrakan sedang ditempati',
|
||||
'error_code' => 'NOT_AVAILABLE',
|
||||
], 400);
|
||||
}
|
||||
|
||||
$tanggalSurvei = Carbon::parse($request->tanggal_survei);
|
||||
|
||||
$booking = Booking::create([
|
||||
'user_id' => $request->user()->id,
|
||||
'kontrakan_id' => $request->kontrakan_id,
|
||||
'jenis_pengajuan' => 'survei',
|
||||
'tanggal_survei' => $tanggalSurvei,
|
||||
'jam_survei' => $request->jam_survei,
|
||||
'start_date' => $tanggalSurvei,
|
||||
'end_date' => $tanggalSurvei,
|
||||
'amount' => 0,
|
||||
'status' => 'pending',
|
||||
'payment_status' => 'unpaid',
|
||||
'booking_source' => 'user',
|
||||
'notes' => $request->catatan,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Pengajuan survei berhasil dikirim. Tunggu konfirmasi dari pemilik.',
|
||||
'data' => $booking->load('kontrakan'),
|
||||
], 201);
|
||||
}
|
||||
|
||||
/**
|
||||
* Simpan pengajuan sewa
|
||||
*/
|
||||
private function storeSewa(Request $request)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'kontrakan_id' => 'required|exists:kontrakans,id',
|
||||
'tanggal_mulai' => 'required|date|after:today',
|
||||
'durasi_bulan' => 'required|integer|min:1|max:12',
|
||||
'catatan' => 'nullable|string',
|
||||
'payment_proof' => 'required|image|mimes:jpeg,jpg,png|max:5120',
|
||||
'catatan' => 'nullable|string|max:1000',
|
||||
'ktp_photo' => 'nullable|image|mimes:jpeg,jpg,png|max:5120',
|
||||
], [
|
||||
'payment_proof.required' => 'Bukti pembayaran wajib diunggah',
|
||||
'payment_proof.image' => 'File harus berupa gambar',
|
||||
'payment_proof.mimes' => 'Format file harus jpeg, jpg, atau png',
|
||||
'payment_proof.max' => 'Ukuran file maksimal 5MB',
|
||||
'kontrakan_id.required' => 'Kontrakan wajib dipilih',
|
||||
'tanggal_mulai.required' => 'Tanggal mulai sewa wajib diisi',
|
||||
'tanggal_mulai.after' => 'Tanggal mulai harus setelah hari ini',
|
||||
'durasi_bulan.required' => 'Durasi sewa wajib diisi',
|
||||
'ktp_photo.image' => 'Foto KTP harus berupa gambar',
|
||||
'ktp_photo.mimes' => 'Format KTP harus jpeg, jpg, atau png',
|
||||
'ktp_photo.max' => 'Ukuran foto KTP maksimal 5MB',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Validasi gagal',
|
||||
'success' => false,
|
||||
'message' => 'Validasi gagal',
|
||||
'error_code' => 'VALIDATION_ERROR',
|
||||
'errors' => $validator->errors()
|
||||
'errors' => $validator->errors(),
|
||||
], 422);
|
||||
}
|
||||
|
||||
// Check kontrakan availability
|
||||
$kontrakan = Kontrakan::find($request->kontrakan_id);
|
||||
|
||||
// Handle both status values: 'tersedia' and 'available'
|
||||
|
||||
if (!$kontrakan) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Kontrakan tidak ditemukan',
|
||||
'error_code' => 'NOT_FOUND',
|
||||
], 404);
|
||||
}
|
||||
|
||||
// Cek ketersediaan kontrakan
|
||||
if (!in_array($kontrakan->status, ['tersedia', 'available'])) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Kontrakan tidak tersedia',
|
||||
'success' => false,
|
||||
'message' => 'Kontrakan tidak tersedia untuk disewa',
|
||||
'error_code' => 'NOT_AVAILABLE',
|
||||
], 400);
|
||||
}
|
||||
|
||||
// Calculate tanggal selesai
|
||||
$startDate = Carbon::parse($request->tanggal_mulai);
|
||||
$endDate = $startDate->copy()->addMonths((int)$request->durasi_bulan);
|
||||
|
||||
// Calculate total biaya from annual price pro-rated to monthly duration
|
||||
$amount = $kontrakan->harga * ((int)$request->durasi_bulan / 12);
|
||||
$endDate = $startDate->copy()->addMonths((int)$request->durasi_bulan);
|
||||
$amount = $kontrakan->harga * ((int)$request->durasi_bulan / 12);
|
||||
|
||||
$bookingData = [
|
||||
'user_id' => $request->user()->id,
|
||||
'kontrakan_id' => $request->kontrakan_id,
|
||||
'start_date' => $startDate,
|
||||
'end_date' => $endDate,
|
||||
'amount' => $amount,
|
||||
'status' => 'pending',
|
||||
'notes' => $request->catatan,
|
||||
'user_id' => $request->user()->id,
|
||||
'kontrakan_id' => $request->kontrakan_id,
|
||||
'jenis_pengajuan' => 'sewa',
|
||||
'start_date' => $startDate,
|
||||
'end_date' => $endDate,
|
||||
'amount' => $amount,
|
||||
'status' => 'pending',
|
||||
'payment_status' => 'unpaid',
|
||||
'booking_source' => 'user',
|
||||
'notes' => $request->catatan,
|
||||
];
|
||||
|
||||
// Handle payment proof upload
|
||||
if ($request->hasFile('payment_proof')) {
|
||||
// ✅ Store sensitive payment proof in PRIVATE storage
|
||||
$path = $request->file('payment_proof')->store(self::PAYMENT_PROOF_DIR, self::PAYMENT_PROOF_PRIVATE_DISK);
|
||||
$bookingData['payment_proof'] = $path;
|
||||
$bookingData['payment_status'] = 'paid';
|
||||
$bookingData['payment_method'] = 'transfer';
|
||||
$bookingData['paid_at'] = now();
|
||||
// Upload foto KTP jika disertakan
|
||||
if ($request->hasFile('ktp_photo')) {
|
||||
$ktpPath = $request->file('ktp_photo')->store('ktp_photos', self::PAYMENT_PROOF_PRIVATE_DISK);
|
||||
$bookingData['ktp_photo'] = $ktpPath;
|
||||
}
|
||||
|
||||
$booking = Booking::create($bookingData);
|
||||
|
||||
// Update status kontrakan ke booked saat ada booking masuk
|
||||
// Update status kontrakan ke booked
|
||||
$kontrakan->update(['status' => 'booked']);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Booking berhasil dibuat',
|
||||
'data' => $booking->load('kontrakan')
|
||||
'message' => 'Pengajuan sewa berhasil dikirim. Tunggu persetujuan pemilik.',
|
||||
'data' => $booking->load('kontrakan'),
|
||||
], 201);
|
||||
}
|
||||
|
||||
|
|
@ -279,10 +368,12 @@ public function uploadPaymentProof(Request $request, $id)
|
|||
], 400);
|
||||
}
|
||||
|
||||
if ($booking->payment_status === 'paid') {
|
||||
if (in_array($booking->payment_status, ['paid', 'verification'])) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Pembayaran booking ini sudah dikonfirmasi',
|
||||
'success' => false,
|
||||
'message' => $booking->payment_status === 'paid'
|
||||
? 'Pembayaran booking ini sudah dikonfirmasi'
|
||||
: 'Bukti pembayaran sudah diunggah dan sedang menunggu verifikasi admin',
|
||||
'error_code' => 'ALREADY_PAID',
|
||||
], 400);
|
||||
}
|
||||
|
|
@ -298,17 +389,17 @@ public function uploadPaymentProof(Request $request, $id)
|
|||
// ✅ Store sensitive payment proof in PRIVATE storage
|
||||
$path = $request->file('payment_proof')->store(self::PAYMENT_PROOF_DIR, self::PAYMENT_PROOF_PRIVATE_DISK);
|
||||
|
||||
// Update booking
|
||||
// Update booking – status jadi 'verification' agar admin bisa memverifikasi
|
||||
$booking->update([
|
||||
'payment_proof' => $path,
|
||||
'payment_status' => 'paid',
|
||||
'payment_status' => 'verification',
|
||||
'payment_method' => 'transfer',
|
||||
'paid_at' => now(),
|
||||
'payment_rejection_reason' => null,
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'Bukti pembayaran berhasil diunggah. Pembayaran Anda telah dikonfirmasi.',
|
||||
'message' => 'Bukti pembayaran berhasil diunggah. Menunggu verifikasi admin.',
|
||||
'data' => $booking->fresh()->load('kontrakan'),
|
||||
], 200);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -594,6 +594,45 @@ public function verifyPayment(Request $request, Booking $booking)
|
|||
return back()->with('success', 'Pembayaran berhasil diverifikasi dan ditandai lunas.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tolak verifikasi pembayaran (status verification -> unpaid)
|
||||
*/
|
||||
public function rejectPayment(Request $request, Booking $booking)
|
||||
{
|
||||
// ========== AUTHORIZATION ===========
|
||||
$admin = auth()->guard('admin')->user();
|
||||
if ($admin) {
|
||||
if ($booking->kontrakan->admin_id !== $admin->id) {
|
||||
abort(403, 'Anda tidak memiliki akses ke booking ini.');
|
||||
}
|
||||
}
|
||||
|
||||
// Only allow rejection when status is verification
|
||||
if ($booking->payment_status !== 'verification') {
|
||||
return back()->with('error', 'Status pembayaran bukan verifikasi.');
|
||||
}
|
||||
|
||||
$request->validate([
|
||||
'rejection_reason' => 'required|string|max:1000',
|
||||
], [
|
||||
'rejection_reason.required' => 'Alasan penolakan wajib diisi.',
|
||||
]);
|
||||
|
||||
// Hapus file bukti pembayaran yang salah/tidak valid
|
||||
if ($booking->payment_proof) {
|
||||
Storage::disk(self::PAYMENT_PROOF_PUBLIC_DISK)->delete($booking->payment_proof);
|
||||
Storage::disk(self::PAYMENT_PROOF_PRIVATE_DISK)->delete($booking->payment_proof);
|
||||
}
|
||||
|
||||
$booking->update([
|
||||
'payment_status' => 'unpaid',
|
||||
'payment_proof' => null,
|
||||
'payment_rejection_reason' => $request->rejection_reason,
|
||||
]);
|
||||
|
||||
return back()->with('success', 'Pembayaran ditolak. Penyewa telah diinformasikan untuk mengunggah ulang.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Hapus booking
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -78,6 +78,9 @@ public static function syncKontrakanStatus($kontrakanId)
|
|||
'checked_out_at',
|
||||
'cancelled_at',
|
||||
'cancellation_reason',
|
||||
'payment_rejection_reason',
|
||||
'jam_survei',
|
||||
'tanggal_survei',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
|
|
|
|||
|
|
@ -9,6 +9,46 @@ class Kontrakan extends Model
|
|||
{
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* Boot method to handle model events.
|
||||
*/
|
||||
protected static function boot()
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
static::deleting(function ($kontrakan) {
|
||||
// Hapus foto utama jika ada
|
||||
if ($kontrakan->foto) {
|
||||
$paths = [
|
||||
public_path('uploads/kontrakan/' . $kontrakan->foto),
|
||||
public_path('uploads/Kontrakan/' . $kontrakan->foto)
|
||||
];
|
||||
foreach ($paths as $path) {
|
||||
if (file_exists($path)) {
|
||||
@unlink($path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hapus semua foto di galeri beserta file fisiknya
|
||||
if ($kontrakan->galeri) {
|
||||
foreach ($kontrakan->galeri as $g) {
|
||||
$filePath = public_path('uploads/galeri/kontrakan/' . $g->foto);
|
||||
if (file_exists($filePath)) {
|
||||
@unlink($filePath);
|
||||
}
|
||||
$g->delete();
|
||||
}
|
||||
}
|
||||
|
||||
// Hapus reviews terkait
|
||||
$kontrakan->reviews()->delete();
|
||||
|
||||
// Hapus favorites terkait
|
||||
$kontrakan->favorites()->delete();
|
||||
});
|
||||
}
|
||||
|
||||
protected $fillable = [
|
||||
'admin_id',
|
||||
'nama',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('bookings', function (Blueprint $table) {
|
||||
// Jenis pengajuan: survei atau sewa
|
||||
if (!Schema::hasColumn('bookings', 'jenis_pengajuan')) {
|
||||
$table->string('jenis_pengajuan')->default('sewa')->after('notes');
|
||||
}
|
||||
// Tanggal dan jam survei (hanya untuk jenis_pengajuan = survei)
|
||||
if (!Schema::hasColumn('bookings', 'tanggal_survei')) {
|
||||
$table->date('tanggal_survei')->nullable()->after('jenis_pengajuan');
|
||||
}
|
||||
if (!Schema::hasColumn('bookings', 'jam_survei')) {
|
||||
$table->string('jam_survei')->nullable()->after('tanggal_survei');
|
||||
}
|
||||
// Foto KTP (untuk pengajuan sewa)
|
||||
if (!Schema::hasColumn('bookings', 'ktp_photo')) {
|
||||
$table->string('ktp_photo')->nullable()->after('jam_survei');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('bookings', function (Blueprint $table) {
|
||||
$table->dropColumn(['jenis_pengajuan', 'tanggal_survei', 'jam_survei', 'ktp_photo']);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('bookings', function (Blueprint $table) {
|
||||
if (!Schema::hasColumn('bookings', 'payment_rejection_reason')) {
|
||||
$table->text('payment_rejection_reason')->nullable()->after('payment_proof');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('bookings', function (Blueprint $table) {
|
||||
$table->dropColumn('payment_rejection_reason');
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
@ -172,10 +172,23 @@
|
|||
<small class="text-muted">{{ $booking->tenant_phone }}</small>
|
||||
</td>
|
||||
<td>
|
||||
<div>{{ $booking->start_date->format('d M Y') }}</div>
|
||||
<small class="text-muted">s/d {{ $booking->end_date->format('d M Y') }}</small>
|
||||
<br>
|
||||
<span class="badge bg-light text-dark">{{ $booking->duration_days }} hari</span>
|
||||
@if($booking->jenis_pengajuan === 'survei')
|
||||
<div class="fw-semibold text-success">
|
||||
<i class="bi bi-calendar-event me-1"></i>{{ $booking->start_date->format('d M Y') }}
|
||||
</div>
|
||||
@if($booking->jam_survei)
|
||||
<small class="text-muted">
|
||||
<i class="bi bi-clock me-1"></i>Pukul {{ $booking->jam_survei }} WIB
|
||||
</small>
|
||||
@endif
|
||||
<br>
|
||||
<span class="badge bg-success bg-opacity-10 text-success border border-success border-opacity-25">Survei</span>
|
||||
@else
|
||||
<div>{{ $booking->start_date->format('d M Y') }}</div>
|
||||
<small class="text-muted">s/d {{ $booking->end_date->format('d M Y') }}</small>
|
||||
<br>
|
||||
<span class="badge bg-light text-dark">{{ $booking->duration_days }} hari</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
<span class="fw-semibold">Rp {{ number_format($booking->amount, 0, ',', '.') }}</span>
|
||||
|
|
@ -200,6 +213,9 @@
|
|||
<button type="submit" class="btn btn-sm btn-success" onclick="return confirm('Verifikasi pembayaran?')">
|
||||
Verifikasi
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-danger ms-1" data-bs-toggle="modal" data-bs-target="#rejectPaymentModal" data-action="{{ route('admin.bookings.reject-payment', $booking->id) }}">
|
||||
Tolak
|
||||
</button>
|
||||
</form>
|
||||
@endif
|
||||
@if($booking->payment_proof)
|
||||
|
|
@ -278,11 +294,49 @@
|
|||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Reject Payment Modal --}}
|
||||
<div class="modal fade" id="rejectPaymentModal" tabindex="-1" aria-labelledby="rejectPaymentModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form id="rejectPaymentForm" method="POST">
|
||||
@csrf
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title fw-bold" id="rejectPaymentModalLabel">Tolak Verifikasi Pembayaran</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="rejection_reason" class="form-label">Alasan Penolakan</label>
|
||||
<textarea class="form-control" id="rejection_reason" name="rejection_reason" rows="3" required placeholder="Masukkan alasan penolakan (misal: Bukti pembayaran tidak valid/kurang jelas)"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Batal</button>
|
||||
<button type="submit" class="btn btn-danger">Tolak Pembayaran</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
||||
@section('scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
// Handle Reject Payment Modal action URL
|
||||
const rejectPaymentModal = document.getElementById('rejectPaymentModal');
|
||||
if (rejectPaymentModal) {
|
||||
rejectPaymentModal.addEventListener('show.bs.modal', function (event) {
|
||||
const button = event.relatedTarget;
|
||||
const actionUrl = button.getAttribute('data-action');
|
||||
const form = document.getElementById('rejectPaymentForm');
|
||||
if (form) {
|
||||
form.setAttribute('action', actionUrl);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const selectAll = document.getElementById('selectAllBookings');
|
||||
const checkboxes = Array.from(document.querySelectorAll('.booking-checkbox'));
|
||||
const selectableCheckboxes = checkboxes.filter(item => !item.disabled);
|
||||
|
|
|
|||
|
|
@ -192,11 +192,24 @@ class="rounded me-3" style="width: 120px; height: 90px; object-fit: cover;">
|
|||
</div>
|
||||
<div class="card-body">
|
||||
<div class="mb-3">
|
||||
@if($booking->jenis_pengajuan === 'survei')
|
||||
<label class="text-muted small">Jadwal Survei</label>
|
||||
<div class="fw-semibold">
|
||||
<i class="bi bi-calendar-event me-1 text-success"></i>{{ $booking->start_date->format('d M Y') }}
|
||||
</div>
|
||||
@if($booking->jam_survei)
|
||||
<div class="fw-semibold mt-1">
|
||||
<i class="bi bi-clock me-1 text-success"></i>Pukul {{ $booking->jam_survei }} WIB
|
||||
</div>
|
||||
@endif
|
||||
<span class="badge bg-success mt-1">Survei</span>
|
||||
@else
|
||||
<label class="text-muted small">Periode Sewa</label>
|
||||
<div class="fw-semibold">
|
||||
{{ $booking->start_date->format('d M Y') }} - {{ $booking->end_date->format('d M Y') }}
|
||||
</div>
|
||||
<span class="badge bg-light text-dark">{{ $booking->duration_days }} hari ({{ $booking->duration_months }} bulan)</span>
|
||||
@endif
|
||||
</div>
|
||||
<hr>
|
||||
<div class="d-flex justify-content-between mb-2">
|
||||
|
|
@ -304,6 +317,9 @@ class="btn btn-sm btn-outline-success mt-1">
|
|||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<button type="button" class="btn btn-danger w-100 mt-2" data-bs-toggle="modal" data-bs-target="#rejectPaymentModal">
|
||||
<i class="bi bi-x-circle me-1"></i>Tolak Pembayaran
|
||||
</button>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
|
|
@ -361,4 +377,29 @@ class="btn btn-sm btn-outline-success mt-1">
|
|||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
{{-- Reject Payment Modal --}}
|
||||
<div class="modal fade" id="rejectPaymentModal" tabindex="-1" aria-labelledby="rejectPaymentModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form action="{{ route('admin.bookings.reject-payment', $booking->id) }}" method="POST">
|
||||
@csrf
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title fw-bold" id="rejectPaymentModalLabel">Tolak Verifikasi Pembayaran</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="rejection_reason" class="form-label">Alasan Penolakan</label>
|
||||
<textarea class="form-control" id="rejection_reason" name="rejection_reason" rows="3" required placeholder="Masukkan alasan penolakan (misal: Bukti pembayaran tidak valid/kurang jelas)"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Batal</button>
|
||||
<button type="submit" class="btn btn-danger">Tolak Pembayaran</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
|
|
|||
|
|
@ -147,6 +147,7 @@
|
|||
Route::post('/{booking}/mark-paid', [BookingController::class, 'markPaid'])->name('mark-paid');
|
||||
Route::post('/{booking}/toggle-payment', [BookingController::class, 'togglePaymentStatus'])->name('toggle-payment');
|
||||
Route::post('/{booking}/verify-payment', [BookingController::class, 'verifyPayment'])->name('verify-payment');
|
||||
Route::post('/{booking}/reject-payment', [BookingController::class, 'rejectPayment'])->name('reject-payment');
|
||||
|
||||
// API & History
|
||||
Route::post('/check-availability', [BookingController::class, 'checkAvailability'])->name('check-availability');
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
class Booking {
|
||||
class Booking {
|
||||
final int id;
|
||||
final int userId;
|
||||
final int kontrakanId;
|
||||
|
|
@ -18,6 +18,7 @@
|
|||
// Pembayaran hanya digunakan pada pengajuan sewa.
|
||||
final String paymentStatus;
|
||||
final String? paymentProof;
|
||||
final String? paymentRejectionReason;
|
||||
|
||||
Booking({
|
||||
required this.id,
|
||||
|
|
@ -35,6 +36,7 @@
|
|||
this.surveyFollowUpExpiresAt,
|
||||
this.paymentStatus = 'unpaid',
|
||||
this.paymentProof,
|
||||
this.paymentRejectionReason,
|
||||
});
|
||||
|
||||
static DateTime? _parseDate(dynamic value) {
|
||||
|
|
@ -75,6 +77,7 @@
|
|||
),
|
||||
paymentStatus: json['payment_status']?.toString() ?? 'unpaid',
|
||||
paymentProof: json['payment_proof']?.toString(),
|
||||
paymentRejectionReason: json['payment_rejection_reason']?.toString(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1073,6 +1073,54 @@ class _BookingHistoryScreenState extends State<BookingHistoryScreen>
|
|||
),
|
||||
),
|
||||
],
|
||||
if (booking.paymentRejectionReason != null &&
|
||||
booking.paymentRejectionReason!.isNotEmpty) ...[
|
||||
Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.only(top: 14),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFFFF1F0),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: const Color(0xFFFFA39E)),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline_rounded,
|
||||
color: Color(0xFFF5222D),
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'Pembayaran Ditolak',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFFF5222D),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
booking.paymentRejectionReason!,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Color(0xFF8C1D1D),
|
||||
height: 1.35,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
if (booking.canUploadPaymentProof) ...[
|
||||
const SizedBox(height: 14),
|
||||
SizedBox(
|
||||
|
|
|
|||
Loading…
Reference in New Issue