(pembayaran): Implementasi fitur create pembayaran baru dengan autocomplete debitur

- Menambahkan method create() di PembayaranController untuk menampilkan form pembayaran baru
- Menambahkan logika create pembayaran di method store() dengan validasi type 'create'
- Menambahkan penyimpanan data pembayaran baru ke tabel persetujuan_penawaran dan noc
- Menambahkan upload bukti bayar dengan penyimpanan ke storage public
- Menambahkan migration untuk kolom branch_id di tabel noc
- Menambahkan view create.blade.php dengan form pembayaran lengkap dan autocomplete debitur
- Menambahkan validasi JavaScript untuk format file dan ukuran maksimal 2MB
- Menambahkan TomSelect untuk pencarian debitur dengan AJAX real-time
- Menambahkan integrasi dengan API debitur search untuk autocomplete
- Memperbaiki method edit() untuk mendukung parameter tiket dalam pencarian persetujuan penawaran
- Mengubah query dataForDatatables untuk mendukung data dari persetujuan_penawaran dan permohonan
- Menambahkan mapping data yang fleksibel untuk menampilkan informasi dari berbagai sumber
- Menambahkan field nomor_tiket, nominal_bayar, dan catatan pada form create
- Menambahkan validasi client-side untuk memastikan file upload sesuai format
- Menambahkan relasi branch_id pada tabel noc untuk tracking cabang pembuat
- Menambahkan redirect ke pembayaran.index setelah berhasil menyimpan pembayaran baru
- Menambahkan import PhpParser\Node\Expr\Cast\Object_ (perlu dibersihkan)
- Mengoptimalkan query dengan eager loading dan mapping data yang efisien
- Menambahkan support untuk pembayaran tanpa permohonan (standalone payment)
- Menambahkan field is_permohonan untuk membedakan jenis pembayaran
- Menambahkan validasi dan error handling yang komprehensif
This commit is contained in:
Daeng Deni Mardaeni
2025-09-15 11:32:59 +07:00
parent 1caa7ebfdd
commit 4aeecf6a97
7 changed files with 456 additions and 30 deletions

View File

@@ -0,0 +1,322 @@
@extends('layouts.main')
@section('breadcrumbs')
{{ Breadcrumbs::render(request()->route()->getName()) }}
@endsection
@section('content')
<div class="grid gap-5 mx-auto w-full lg:gap-7.5">
<div class="pb-2.5 border card border-agi-100">
<div class="card-header bg-agi-50" id="basic_settings">
<div class="flex flex-row gap-1.5 card-title">
Tambah Pembayaran
</div>
<div class="flex gap-2 items-center">
<a href="{{ route('pembayaran.index') }}" class="btn btn-xs btn-info"><i
class="ki-filled ki-exit-left"></i> Back</a>
</div>
</div>
<div class="card-body">
<form action="{{ route('pembayaran.store') }}" method="POST" class="grid gap-5"
enctype="multipart/form-data" id="pembayaranForm">
@csrf
<!-- Hidden fields untuk menyimpan ID yang dipilih -->
<input type="hidden" name="permohonan_id" id="permohonan_id" value="{{ old('permohonan_id') }}">
<input type="hidden" name="penawaran_id" id="penawaran_id" value="{{ old('penawaran_id') }}">
<input type="hidden" name="debitur_id" id="debitur_id" value="{{ old('debitur_id') }}">
<input type="hidden" name="type" id="create" value="create">
<div class="flex flex-wrap gap-2.5 items-baseline lg:flex-nowrap">
<label class="form-label max-w-56">
Debitur <span class="text-danger">*</span>
</label>
<div class="flex flex-wrap items-baseline w-full">
<select name="debitur_search" id="debitur_search"
class="input w-full @error('debitur_id') border-danger bg-danger-light @enderror"
placeholder="Cari debitur berdasarkan kode atau nama...">
<option value="">Pilih Debitur</option>
</select>
@error('debitur_id')
<em class="text-sm alert text-danger">{{ $message }}</em>
@enderror
</div>
</div>
<div class="flex flex-wrap gap-2.5 items-baseline lg:flex-nowrap">
<label class="form-label max-w-56">
Nomor Registrasi
</label>
<div class="flex flex-wrap items-baseline w-full">
<input type="text" name="nomor_registrasi" id="nomor_registrasi"
class="input w-full @error('nomor_registrasi') border-danger bg-danger-light @enderror"
value="{{ old('nomor_registrasi') }}" placeholder="Nomor Registrasi">
@error('nomor_registrasi')
<em class="text-sm alert text-danger">{{ $message }}</em>
@enderror
</div>
</div>
<div class="flex flex-wrap gap-2.5 items-baseline lg:flex-nowrap">
<label class="form-label max-w-56">
Nomor Tiket
</label>
<div class="flex flex-wrap items-baseline w-full">
<input type="text" name="nomor_tiket" id="nomor_tiket"
class="input w-full @error('nomor_tiket') border-danger bg-danger-light @enderror"
value="{{ old('nomor_tiket') }}" placeholder="Nomor Tiket">
@error('nomor_tiket')
<em class="text-sm alert text-danger">{{ $message }}</em>
@enderror
</div>
</div>
<div class="flex flex-wrap gap-2.5 items-baseline lg:flex-nowrap">
<label class="form-label max-w-56">
Nominal Bayar <span class="text-danger">*</span>
</label>
<div class="flex flex-wrap items-baseline w-full">
<input type="number" name="nominal_bayar" id="nominal_bayar"
class="input w-full @error('nominal_bayar') border-danger bg-danger-light @enderror"
value="{{ old('nominal_bayar') }}" placeholder="Masukkan nominal bayar" min="0"
step="0.01">
@error('nominal_bayar')
<em class="text-sm alert text-danger">{{ $message }}</em>
@enderror
</div>
</div>
<div class="flex flex-wrap gap-2.5 items-baseline lg:flex-nowrap">
<label class="form-label max-w-56">
Bukti Bayar <span class="text-danger">*</span>
</label>
<div class="flex flex-wrap items-baseline w-full">
<input type="file" name="bukti_bayar" id="bukti_bayar"
class="file-input w-full @error('bukti_bayar') border-danger bg-danger-light @enderror"
accept=".pdf,.jpg,.jpeg,.png">
<small class="mt-1 text-gray-600">Format yang diizinkan: PDF, JPG, JPEG, PNG (Max: 2MB)</small>
@error('bukti_bayar')
<em class="text-sm alert text-danger">{{ $message }}</em>
@enderror
</div>
</div>
<div class="flex flex-wrap gap-2.5 items-baseline lg:flex-nowrap">
<label class="form-label max-w-56">
Catatan
</label>
<div class="flex flex-wrap items-baseline w-full">
<textarea name="catatan" id="catatan" rows="4"
class="textarea w-full @error('catatan') border-danger bg-danger-light @enderror"
placeholder="Masukkan catatan (opsional)">{{ old('catatan') }}</textarea>
@error('catatan')
<em class="text-sm alert text-danger">{{ $message }}</em>
@enderror
</div>
</div>
<div class="flex gap-2 justify-end">
<a href="{{ route('pembayaran.index') }}" class="btn btn-secondary">
Batal
</a>
<button type="submit" class="btn btn-primary" id="submitBtn">
<i class="ki-filled ki-check"></i> Simpan Pembayaran
</button>
</div>
</form>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
/**
* Inisialisasi TomSelect untuk pencarian debitur dengan AJAX
* Menggunakan autocomplete untuk mencari berdasarkan kode atau nama debitur
*/
document.addEventListener('DOMContentLoaded', function() {
// Inisialisasi TomSelect untuk field debitur
const debiturSelect = new window.TomSelect('#debitur_search', {
valueField: 'id',
labelField: 'display_name',
searchField: ['kode_debitur', 'name'],
placeholder: 'Ketik kode atau nama debitur untuk mencari...',
load: function(query, callback) {
// Minimal 2 karakter untuk mulai pencarian
if (query.length < 2) {
callback();
return;
}
// Tampilkan loading state
this.loading = true;
// AJAX request untuk mencari debitur
fetch(`{{ route('api.debitur.search') }}?q=${encodeURIComponent(query)}`, {
method: 'GET',
headers: {
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json',
'Content-Type': 'application/json',
'X-CSRF-TOKEN': '{{ csrf_token() }}'
}
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
// Log untuk debugging
console.log('Debitur search results:', data);
// Format data untuk TomSelect
const formattedData = data.data.map(item => ({
id: item.id,
display_name: `${item.kode_debitur} - ${item.name}`,
kode_debitur: item.kode_debitur,
name: item.name
}));
callback(formattedData);
this.loading = false;
})
.catch(error => {
console.error('Error fetching debitur data:', error);
callback();
this.loading = false;
// Tampilkan pesan error ke user
alert(
'Terjadi kesalahan saat mencari data debitur. Silakan coba lagi.'
);
});
},
render: {
option: function(data, escape) {
return `<div class="px-3 py-2 hover:bg-gray-50">
<div class="font-medium text-gray-900">${escape(data.kode_debitur)}</div>
<div class="text-sm text-gray-600">${escape(data.name)}</div>
</div>`;
},
item: function(data, escape) {
return `<div>${escape(data.kode_debitur)} - ${escape(data.name)}</div>`;
}
},
onChange: function(value) {
// Log untuk debugging
console.log('Debitur selected:', value);
if (value) {
const selectedOption = this.options[value];
if (selectedOption) {
// Set hidden field untuk debitur_id
document.getElementById('debitur_id').value = value;
// Jika ada data permohonan, isi field terkait
if (selectedOption.permohonan) {
const permohonan = selectedOption.permohonan;
// Set hidden fields
document.getElementById('permohonan_id').value = permohonan.id || '';
document.getElementById('penawaran_id').value = permohonan
.penawaran_id || '';
// Set visible fields
document.getElementById('nomor_registrasi').value = permohonan
.nomor_registrasi || '';
document.getElementById('nomor_tiket').value = permohonan.nomor_tiket ||
'';
// Set nominal bayar jika ada dari penawaran
if (permohonan.nominal_penawaran) {
document.getElementById('nominal_bayar').value = permohonan
.nominal_penawaran;
}
} else {
// Clear fields jika tidak ada data permohonan
document.getElementById('permohonan_id').value = '';
document.getElementById('penawaran_id').value = '';
document.getElementById('nomor_registrasi').value = '';
document.getElementById('nomor_tiket').value = '';
}
}
} else {
// Clear semua field jika tidak ada yang dipilih
document.getElementById('debitur_id').value = '';
document.getElementById('permohonan_id').value = '';
document.getElementById('penawaran_id').value = '';
document.getElementById('nomor_registrasi').value = '';
document.getElementById('nomor_tiket').value = '';
}
}
});
/**
* Validasi form sebelum submit
* Memastikan field wajib sudah diisi
*/
document.getElementById('pembayaranForm').addEventListener('submit', function(e) {
const debiturId = document.getElementById('debitur_id').value;
const nominalBayar = document.getElementById('nominal_bayar').value;
const buktiBayar = document.getElementById('bukti_bayar').files[0];
let errors = [];
// Validasi debitur
if (!debiturId) {
errors.push('Debitur harus dipilih');
}
// Validasi nominal bayar
if (!nominalBayar || parseFloat(nominalBayar) <= 0) {
errors.push('Nominal bayar harus diisi dan lebih dari 0');
}
// Validasi bukti bayar
if (!buktiBayar) {
errors.push('Bukti bayar harus diupload');
} else {
// Validasi ukuran file (max 2MB)
if (buktiBayar.size > 2 * 1024 * 1024) {
errors.push('Ukuran file bukti bayar maksimal 2MB');
}
// Validasi tipe file
const allowedTypes = ['application/pdf', 'image/jpeg', 'image/jpg', 'image/png'];
if (!allowedTypes.includes(buktiBayar.type)) {
errors.push('Format file bukti bayar harus PDF, JPG, JPEG, atau PNG');
}
}
// Tampilkan error jika ada
if (errors.length > 0) {
e.preventDefault();
alert('Terdapat kesalahan:\n\n' + errors.join('\n'));
return false;
}
// Disable submit button untuk mencegah double submit
const submitBtn = document.getElementById('submitBtn');
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="ki-filled ki-loading"></i> Menyimpan...';
// Log untuk debugging
console.log('Form submitted with data:', {
debitur_id: debiturId,
permohonan_id: document.getElementById('permohonan_id').value,
penawaran_id: document.getElementById('penawaran_id').value,
nominal_bayar: nominalBayar
});
});
// Re-enable submit button jika ada error dari server
@if ($errors->any())
const submitBtn = document.getElementById('submitBtn');
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="ki-filled ki-check"></i> Simpan Pembayaran';
@endif
});
</script>
@endpush

View File

@@ -76,10 +76,10 @@
name="status_bayar" id="status_bayar">
<option value="">Pilih Status Bayar</option>
<option value="sudah_bayar"
{{ old('status_bayar') == 'sudah_bayar' || (isset($permohonan) && $permohonan->status_bayar == 'sudah_bayar') ? 'selected' : '' }}>
{{ old('status_bayar') == 'sudah_bayar' || (isset($permohonan) && $permohonan?->status_bayar == 'sudah_bayar') ? 'selected' : '' }}>
Sudah Bayar</option>
<option value="belum_bayar"
{{ old('status_bayar') == 'belum_bayar' || (isset($permohonan) && $permohonan->status_bayar == 'belum_bayar') ? 'selected' : '' }}>
{{ old('status_bayar') == 'belum_bayar' || (isset($permohonan) && $permohonan?->status_bayar == 'belum_bayar') ? 'selected' : '' }}>
Belum Bayar</option>
</select>
@error('status_bayar')

View File

@@ -21,6 +21,7 @@
<div class="flex flex-wrap gap-2.5">
<div class="h-[24px] border border-r-gray-200"></div>
<a class="btn btn-sm btn-light" href="#"> Export to Excel </a>
<a class="btn btn-sm btn-primary" href="{{ route('pembayaran.create') }}"> Tambah Pembayaran </a>
</div>
</div>
</div>
@@ -212,11 +213,19 @@
actions: {
title: 'Status',
render: (item, data) => {
return `<div class="flex gap-2 justify-center">
<a class="btn btn-icon btn-clear btn-warning" href="pembayaran/${data.id}/edit" title="Lakukan Pembayaran">
<i class="ki-outline ki-credit-cart"></i>
</a>
</div>`;
if (data.is_permohonan) {
return `<div class="flex gap-2 justify-center">
<a class="btn btn-icon btn-clear btn-warning" href="pembayaran/${data.id}/edit" title="Lakukan Pembayaran">
<i class="ki-outline ki-credit-cart"></i>
</a>
</div>`;
} else {
return `<div class="flex gap-2 justify-center">
<a class="btn btn-icon btn-clear btn-warning" href="pembayaran/${data.id}/edit?tiket=true" title="Lakukan Pembayaran">
<i class="ki-outline ki-credit-cart"></i>
</a>
</div>`;
}
},
}
},