feat(lpj): implementasi fitur Memo Penyelesaian dengan controller, view, dan routing lengkap

Perubahan yang dilakukan:

**Controller MemoController:**
- Menambahkan MemoController untuk mengelola memo penyelesaian permohonan.
- Method index() untuk menampilkan daftar permohonan yang bisa dipilih.
- Method create() untuk form pembuatan memo dengan pemilihan data bulk.
- Method store() untuk menyimpan memo dan mengupdate status permohonan terkait.
- Method show() untuk menampilkan detail memo yang telah dibuat.
- Method dataForDatatables() untuk API datatables dengan filter, search, dan pagination.
- Implementasi DB transaction untuk menjaga integritas data.
- Logging dan error handling komprehensif di setiap method.

**View Template:**
- index.blade.php: Tabel data permohonan dengan fitur checkbox selection (bulk).
- create.blade.php: Form pembuatan memo dari data yang dipilih.
- show.blade.php: Halaman detail memo penyelesaian.
- Menggunakan Bootstrap untuk styling dan interaksi dinamis dengan JavaScript.
- Validasi client-side untuk memastikan data sesuai sebelum dikirim.

**Routing dan Navigasi:**
- Menambahkan route resource untuk operasi CRUD Memo.
- Menambahkan route khusus untuk datatables API dan bulk create.
- Integrasi menu "Memo Penyelesaian" di navigasi utama aplikasi.
- Role-based access control untuk keamanan akses fitur.

**Integrasi Data:**
- Menggunakan model Permohonan sebagai sumber data utama dengan eager loading.
- Relasi dengan tabel user, debitur, branch, dan tujuan penilaian.
- Menambahkan status management untuk mempermudah tracking progress permohonan.

**Keamanan dan Validasi:**
- Validasi input baik di sisi controller maupun client-side.
- CSRF protection dan XSS prevention untuk menjaga keamanan aplikasi.
- Permission checking sesuai level user.

**Performance dan UX:**
- Pagination dan query optimization untuk performa lebih baik.
- Caching strategi untuk data yang sering diakses.
- Interface yang intuitif, dengan loading state dan feedback message.
- Responsive design untuk desktop dan mobile.
- Shortcut keyboard untuk efisiensi power user.

**Teknis dan Testing:**
- Struktur kode mengikuti Laravel best practice dan design pattern.
- Siap untuk unit test dan integration test.
- Logging lengkap untuk monitoring dan debugging.
- Error scenario handling dan fallback yang robust.

Tujuan perubahan:
- Menyediakan fitur pengelolaan memo penyelesaian permohonan secara bulk dengan user experience yang optimal dan performa efisien.
This commit is contained in:
Daeng Deni Mardaeni
2025-07-15 10:05:22 +07:00
parent 36ac1370d7
commit 5c3a93c49b
7 changed files with 1114 additions and 1 deletions

View File

@@ -0,0 +1,243 @@
@extends('layouts.main')
@section('content')
<div class="grid gap-5 mx-auto w-full lg:gap-7.5">
<!-- Form Memo Penyelesaian -->
<div class="card">
<div class="card-header">
<h3 class="card-title">
Buat Memo Penyelesaian
</h3>
<div class="flex items-center gap-2">
<a href="{{ route('memo.index') }}" class="btn btn-sm btn-light">
<i class="ki-filled ki-black-left"></i>
Kembali
</a>
</div>
</div>
<div class="card-body">
@if(session('error'))
<div class="alert alert-danger">
{{ session('error') }}
</div>
@endif
<form action="{{ route('memo.store') }}" method="POST" id="memo-form">
@csrf
<!-- Form Input Memo -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5 mb-7">
<div class="flex flex-col gap-1">
<label class="form-label text-gray-900">Judul Memo <span class="text-red-500">*</span></label>
<input type="text" name="memo_title" class="input" placeholder="Masukkan judul memo penyelesaian"
value="{{ old('memo_title') }}" required>
@error('memo_title')
<span class="text-red-500 text-sm">{{ $message }}</span>
@enderror
</div>
<div class="flex flex-col gap-1">
<label class="form-label text-gray-900">Tanggal Memo <span class="text-red-500">*</span></label>
<input type="date" name="memo_date" class="input"
value="{{ old('memo_date', date('Y-m-d')) }}" required>
@error('memo_date')
<span class="text-red-500 text-sm">{{ $message }}</span>
@enderror
</div>
</div>
<div class="flex flex-col gap-1 mb-7">
<label class="form-label text-gray-900">Isi Memo <span class="text-red-500">*</span></label>
<textarea name="memo_content" class="textarea" rows="6"
placeholder="Masukkan isi memo penyelesaian" required>{{ old('memo_content') }}</textarea>
@error('memo_content')
<span class="text-red-500 text-sm">{{ $message }}</span>
@enderror
</div>
<!-- Daftar Permohonan yang Dipilih -->
@if(count($permohonanList) > 0)
<div class="mb-7">
<h4 class="text-lg font-semibold mb-4">Daftar Permohonan yang Akan Diproses</h4>
<div class="overflow-x-auto">
<table class="table table-border">
<thead>
<tr class="bg-gray-50">
<th class="text-center w-12">
<input type="checkbox" id="select-all" class="checkbox checkbox-sm" checked>
</th>
<th>Nomor Registrasi</th>
<th>Debitur</th>
<th>Cabang</th>
<th>AO</th>
<th>Tujuan Penilaian</th>
<th>Status</th>
</tr>
</thead>
<tbody>
@foreach($permohonanList as $permohonan)
<tr>
<td class="text-center">
<input type="checkbox" name="permohonan_ids[]"
value="{{ $permohonan->id }}"
class="checkbox checkbox-sm permohonan-checkbox" checked>
</td>
<td class="font-medium">{{ $permohonan->nomor_registrasi }}</td>
<td>{{ $permohonan->debiture->name ?? '-' }}</td>
<td>{{ $permohonan->branch->name ?? '-' }}</td>
<td>{{ $permohonan->user->name ?? '-' }}</td>
<td>
@if($permohonan->tujuanPenilaian)
<span class="badge badge-sm badge-primary">
{{ $permohonan->tujuanPenilaian->name }}
</span>
@else
-
@endif
</td>
<td>
<span class="badge badge-sm badge-warning uppercase">
{{ $permohonan->status }}
</span>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<div class="mt-4 p-4 bg-blue-50 rounded-lg">
<div class="flex items-center gap-2">
<i class="ki-filled ki-information text-blue-600"></i>
<span class="text-blue-800 font-medium">Informasi:</span>
</div>
<p class="text-blue-700 mt-1">
Total <span id="selected-count">{{ count($permohonanList) }}</span> permohonan akan diproses untuk memo penyelesaian.
Anda dapat menghapus centang pada permohonan yang tidak ingin diproses.
</p>
</div>
</div>
<!-- Tombol Submit -->
<div class="flex justify-end gap-2">
<a href="{{ route('memo.index') }}" class="btn btn-light">
Batal
</a>
<button type="submit" class="btn btn-primary" id="submit-btn">
<i class="ki-filled ki-check"></i>
Buat Memo Penyelesaian
</button>
</div>
@else
<div class="text-center py-10">
<div class="mb-4">
<i class="ki-filled ki-information-2 text-6xl text-gray-400"></i>
</div>
<h4 class="text-lg font-semibold text-gray-600 mb-2">Tidak Ada Data</h4>
<p class="text-gray-500 mb-4">Tidak ada permohonan yang dipilih untuk diproses.</p>
<a href="{{ route('memo.index') }}" class="btn btn-primary">
Kembali ke Daftar
</a>
</div>
@endif
</form>
</div>
</div>
</div>
@endsection
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', function() {
const selectAllCheckbox = document.getElementById('select-all');
const permohonanCheckboxes = document.querySelectorAll('.permohonan-checkbox');
const selectedCountSpan = document.getElementById('selected-count');
const submitBtn = document.getElementById('submit-btn');
const memoForm = document.getElementById('memo-form');
/**
* Fungsi untuk mengupdate jumlah item yang dipilih
*/
function updateSelectedCount() {
const checkedBoxes = document.querySelectorAll('.permohonan-checkbox:checked');
const count = checkedBoxes.length;
if (selectedCountSpan) {
selectedCountSpan.textContent = count;
}
// Disable submit button jika tidak ada yang dipilih
if (submitBtn) {
submitBtn.disabled = count === 0;
if (count === 0) {
submitBtn.innerHTML = '<i class="ki-filled ki-check"></i> Pilih minimal 1 permohonan';
submitBtn.classList.add('btn-disabled');
} else {
submitBtn.innerHTML = `<i class="ki-filled ki-check"></i> Buat Memo Penyelesaian (${count} item)`;
submitBtn.classList.remove('btn-disabled');
}
}
// Update select all checkbox state
if (selectAllCheckbox) {
selectAllCheckbox.checked = count === permohonanCheckboxes.length;
selectAllCheckbox.indeterminate = count > 0 && count < permohonanCheckboxes.length;
}
}
/**
* Event listener untuk select all checkbox
*/
if (selectAllCheckbox) {
selectAllCheckbox.addEventListener('change', function() {
const isChecked = this.checked;
permohonanCheckboxes.forEach(checkbox => {
checkbox.checked = isChecked;
});
updateSelectedCount();
});
}
/**
* Event listener untuk individual checkboxes
*/
permohonanCheckboxes.forEach(checkbox => {
checkbox.addEventListener('change', function() {
updateSelectedCount();
});
});
/**
* Event listener untuk form submission
*/
if (memoForm) {
memoForm.addEventListener('submit', function(e) {
const checkedBoxes = document.querySelectorAll('.permohonan-checkbox:checked');
if (checkedBoxes.length === 0) {
e.preventDefault();
alert('Pilih minimal 1 permohonan untuk diproses!');
return false;
}
// Konfirmasi sebelum submit
const confirmed = confirm(`Apakah Anda yakin ingin membuat memo penyelesaian untuk ${checkedBoxes.length} permohonan?`);
if (!confirmed) {
e.preventDefault();
return false;
}
// Disable submit button untuk mencegah double submit
if (submitBtn) {
submitBtn.disabled = true;
submitBtn.innerHTML = '<i class="ki-filled ki-loading"></i> Memproses...';
}
});
}
// Initialize count
updateSelectedCount();
});
</script>
@endpush