feat(lpj): Tambah fitur Referensi Link dan perbaikan Bank Data

Menambahkan fitur manajemen referensi link lengkap dengan CRUD, import/export Excel, serta melakukan perbaikan pada modul Bank Data untuk menampilkan semua data.

## Perubahan Detail

### 🔗 Fitur Referensi Link (Baru)
**Model & Database:**
- Membuat model `ReferensiLink` dengan relasi ke user (created_by, updated_by)
- Membuat migration `create_referensi_link_table` dengan struktur lengkap
- Menambahkan scopes untuk filtering dan searching

**Controller & Request:**
- Membuat `ReferensiLinkController` dengan fitur lengkap (CRUD, datatable, export/import)
- Membuat `ReferensiLinkRequest` dengan validasi comprehensive
- Menambahkan fitur toggle status aktif/inaktif

**Export/Import:**
- Membuat `ReferensiLinkExport` untuk export ke Excel dengan styling
- Membuat `ReferensiLinkImport` untuk import dari Excel dengan validasi
- Menambahkan template download untuk import

**View & Navigation:**
- Menambahkan menu "Referensi Link" di navigasi sistem
- Membuat struktur role access untuk administrator dan admin

### 📊 Perbaikan Bank Data
**Controller:**
- Menambahkan fitur "show_all" untuk menampilkan semua data tanpa pagination
- Memperbaiki Log facade import dari `Log` menjadi `Illuminate\Support\Facades\Log`
- Menambahkan loading overlay untuk UX yang lebih baik

**View:**
- Menambahkan checkbox "Tampilkan Semua Data" di filter
- Memperbaiki styling dan layout tabel
- Menambahkan loading spinner saat filter diterapkan

### 🛠️ Helper & Utilitas
**PdfHelper (Baru):**
- Membuat helper untuk format teks PDF dengan handling karakter spesial
- Menambahkan fungsi untuk konversi simbol matematika ke teks
- Memastikan encoding UTF-8 yang proper

**ImageController (Baru):**
- Membuat controller untuk resize gambar dengan parameter width dan quality
- Menggunakan ImageResizeService untuk processing gambar

### 🔧 Perbaikan Lainnya
**View Components:**
- Memperbaiki syntax HTML dan Blade template
- Menambahkan role checking yang lebih proper
- Memperbaiki format tampilan nilai menggunakan formatRupiah()

### 📁 File Baru
- `Helpers/PdfHelper.php` - Helper untuk format PDF
- `Http/Controllers/ImageController.php` - Controller untuk image resize
- `Http/Controllers/ReferensiLinkController.php` - Controller referensi link
- `Http/Requests/ReferensiLinkRequest.php` - Validasi referensi link
- `Exports/ReferensiLinkExport.php` - Export Excel
- `Imports/ReferensiLinkImport.php` - Import Excel
- `Models/ReferensiLink.php` - Model referensi link
- Database migration untuk tabel referensi_link

### 🔄 File Diperbarui
- `module.json` - Menambahkan menu navigasi
- `BankDataController.php` - Fitur show_all dan perbaikan Log
- `resources/views/bank-data/index.blade.php` - UI improvements
- Beberapa view components untuk perbaikan syntax dan role checking

## Alasan Perubahan
1. **Fitur Referensi Link**: Menyediakan manajemen link referensi yang terstruktur untuk kebutuhan dokumentasi dan regulasi
2. **Import/Export**: Memudahkan pengelolaan data referensi dalam jumlah besar via Excel
3. **Show All Data**: Memenuhi kebutuhan menampilkan semua data bank data di peta tanpa pagination
4. **PDF Helper**: Menangani masalah karakter spesial dalam generate PDF
5. **Image Controller**: Menyediakan endpoint untuk resize gambar secara dinamis
This commit is contained in:
Daeng Deni Mardaeni
2026-01-30 14:44:14 +07:00
parent f402c0831a
commit 2c56dd1d68
21 changed files with 2029 additions and 84 deletions

View File

@@ -0,0 +1,441 @@
<?php
namespace Modules\Lpj\Http\Controllers;
use App\Http\Controllers\Controller;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Log;
use Maatwebsite\Excel\Facades\Excel;
use Modules\Lpj\Models\ReferensiLink;
use Modules\Lpj\Http\Requests\ReferensiLinkRequest;
use Modules\Lpj\Exports\ReferensiLinkExport;
use Modules\Lpj\Imports\ReferensiLinkImport;
class ReferensiLinkController extends Controller
{
public $user;
public function __construct()
{
$this->middleware('auth');
$this->middleware(function ($request, $next) {
$this->user = auth()->user();
return $next($request);
});
}
/**
* Display a listing of the resource.
*/
public function index()
{
//$this->authorize('referensi-link.view', $this->user);
$data = [
'title' => 'Referensi Link',
'subtitle' => 'Daftar Referensi Link',
'breadcrumb' => [
['url' => route('dashboard'), 'text' => 'Dashboard'],
['text' => 'Referensi Link']
],
'kategoriOptions' => $this->getKategoriOptions(),
];
return view('lpj::referensi_link.index', $data);
}
/**
* Show the form for creating a new resource.
*/
public function create()
{
//$this->authorize('referensi-link.create', $this->user);
$data = [
'title' => 'Tambah Referensi Link',
'subtitle' => 'Form Tambah Referensi Link Baru',
'breadcrumb' => [
['url' => route('dashboard'), 'text' => 'Dashboard'],
['url' => route('basicdata.referensi-link.index'), 'text' => 'Referensi Link'],
['text' => 'Tambah']
],
'kategoriOptions' => $this->getKategoriOptions(),
];
return view('lpj::referensi_link.create', $data);
}
/**
* Store a newly created resource in storage.
*/
public function store(ReferensiLinkRequest $request)
{
//$this->authorize('referensi-link.create', $this->user);
try {
$validated = $request->validated();
// Set urutan otomatis jika belum diisi
if (empty($validated['urutan'])) {
$validated['urutan'] = ReferensiLink::max('urutan') + 1;
}
$referensiLink = ReferensiLink::create($validated);
return redirect()
->route('basicdata.referensi-link.index')
->with('success', 'Referensi Link berhasil ditambahkan');
} catch (Exception $e) {
return redirect()
->back()
->withInput()
->with('error', 'Gagal menambahkan Referensi Link: ' . $e->getMessage());
}
}
/**
* Show the form for editing the specified resource.
*/
public function edit($id)
{
//$this->authorize('referensi-link.update', $this->user);
$referensiLink = ReferensiLink::findOrFail($id);
$data = [
'title' => 'Edit Referensi Link',
'subtitle' => 'Form Edit Referensi Link',
'breadcrumb' => [
['url' => route('dashboard'), 'text' => 'Dashboard'],
['url' => route('basicdata.referensi-link.index'), 'text' => 'Referensi Link'],
['text' => 'Edit']
],
'referensiLink' => $referensiLink,
'kategoriOptions' => $this->getKategoriOptions(),
];
return view('lpj::referensi_link.create', $data);
}
/**
* Update the specified resource in storage.
*/
public function update(ReferensiLinkRequest $request, $id)
{
//$this->authorize('referensi-link.update', $this->user);
try {
$referensiLink = ReferensiLink::findOrFail($id);
$validated = $request->validated();
$referensiLink->update($validated);
return redirect()
->route('basicdata.referensi-link.index')
->with('success', 'Referensi Link berhasil diperbarui');
} catch (Exception $e) {
return redirect()
->back()
->withInput()
->with('error', 'Gagal memperbarui Referensi Link: ' . $e->getMessage());
}
}
/**
* Remove the specified resource from storage.
*/
public function destroy($id)
{
//$this->authorize('referensi-link.delete', $this->user);
try {
$referensiLink = ReferensiLink::findOrFail($id);
$referensiLink->delete();
return response()->json([
'success' => true,
'message' => 'Referensi Link berhasil dihapus'
]);
} catch (Exception $e) {
return response()->json([
'success' => false,
'message' => 'Gagal menghapus Referensi Link: ' . $e->getMessage()
], 500);
}
}
/**
* Datatable API for KTDataTable
*/
public function dataTable(Request $request)
{
//$this->authorize('referensi-link.view', $this->user);
$query = ReferensiLink::with(['createdBy', 'updatedBy'])
->select('referensi_link.*');
// Search
$search = $request->input('search');
if (!empty($search)) {
$query->where(function ($q) use ($search) {
$q->where('name', 'LIKE', "%{$search}%")
->orWhere('link', 'LIKE', "%{$search}%")
->orWhere('kategori', 'LIKE', "%{$search}%");
});
}
// Optional filters (support multiple request shapes)
$filters = $request->input('filters', []);
$kategori = $request->input('kategori', $filters['kategori'] ?? null);
if (!empty($kategori)) {
if (is_array($kategori)) {
$query->whereIn('kategori', $kategori);
} else {
$values = preg_split('/[,|]/', (string) $kategori, -1, PREG_SPLIT_NO_EMPTY);
if (count($values) > 1) {
$query->whereIn('kategori', $values);
} else {
$query->where('kategori', $kategori);
}
}
}
$statusRaw = $request->input('status', $filters['status'] ?? $request->input('is_active'));
$statusParsed = $this->parseActiveFilter($statusRaw);
if ($statusParsed !== null) {
$query->where('is_active', $statusParsed);
}
// Sorting
$allowedSortFields = ['id', 'name', 'link', 'kategori', 'urutan', 'is_active', 'created_at', 'updated_at'];
$sortField = in_array($request->input('sortField', 'urutan'), $allowedSortFields, true)
? $request->input('sortField', 'urutan')
: 'urutan';
$sortOrder = strtolower($request->input('sortOrder', 'asc'));
if (in_array($sortOrder, ['asc', 'desc'], true)) {
$query->orderBy($sortField, $sortOrder);
}
// Pagination
$page = max((int) $request->input('page', 1), 1);
$size = max((int) $request->input('size', 10), 1);
$totalRecords = (clone $query)->count();
$offset = ($page - 1) * $size;
$items = $query->skip($offset)->take($size)->get();
// Map data rows
$data = $items->map(function ($row) {
return [
'id' => $row->id,
'name' => $row->name,
'link' => '<a href="' . $row->link . '" target="_blank" class="text-primary">' . Str::limit($row->link, 50) . ' <i class="fas fa-external-link-alt"></i></a>',
'kategori' => $row->kategori,
'status_badge' => $row->status_badge,
'urutan' => $row->urutan,
'actions' => (
(auth()->user()->can('referensi-link.update') ? '<a class="dropdown-item" href="' . route('basicdata.referensi-link.edit', $row->id) . '"><i class="fas fa-edit"></i> Edit</a>' : '') .
(auth()->user()->can('referensi-link.delete') ? '<a class="dropdown-item text-danger" href="javascript:void(0)" onclick="deleteData(' . $row->id . ')"><i class="fas fa-trash"></i> Hapus</a>' : '')
),
];
});
return response()->json([
'draw' => (int) $request->input('draw'),
'recordsTotal' => $totalRecords,
'recordsFiltered' => $totalRecords,
'pageCount' => (int) ceil($totalRecords / $size),
'page' => $page,
'totalCount' => $totalRecords,
'data' => $data,
]);
}
/**
* Export data to Excel
*/
public function export(Request $request)
{
//$this->authorize('referensi-link.export', $this->user);
try {
$filename = 'referensi_link_' . date('YmdHis') . '.xlsx';
return Excel::download(new ReferensiLinkExport($request->all()), $filename);
} catch (Exception $e) {
return redirect()
->back()
->with('error', 'Gagal export data: ' . $e->getMessage());
}
}
/**
* Show import form
*/
public function import()
{
//$this->authorize('referensi-link.import', $this->user);
$data = [
'title' => 'Import Referensi Link',
'subtitle' => 'Import data Referensi Link dari Excel',
'breadcrumb' => [
['url' => route('dashboard'), 'text' => 'Dashboard'],
['url' => route('basicdata.referensi-link.index'), 'text' => 'Referensi Link'],
['text' => 'Import']
],
];
return view('lpj::referensi_link.import', $data);
}
/**
* Process import
*/
public function importProcess(Request $request)
{
//$this->authorize('referensi-link.import', $this->user);
$request->validate([
'file' => 'required|mimes:xlsx,xls|max:10240', // max 10MB
]);
try {
$import = new ReferensiLinkImport();
Excel::import($import, $request->file('file'));
$stats = $import->getImportStats();
$message = "Import berhasil! {$stats['success']} data berhasil diimport";
if ($stats['failed'] > 0) {
$message .= ", {$stats['failed']} data gagal diimport";
}
return redirect()
->route('basicdata.referensi-link.index')
->with('success', $message);
} catch (Exception $e) {
Log::error('ReferensiLink import error: ' . $e->getMessage());
return redirect()
->back()
->with('error', 'Gagal import data: ' . $e->getMessage());
}
}
/**
* Toggle status (active/inactive)
*/
public function toggleStatus($id)
{
//$this->authorize('referensi-link.update', $this->user);
try {
$referensiLink = ReferensiLink::findOrFail($id);
$referensiLink->is_active = !$referensiLink->is_active;
$referensiLink->save();
return response()->json([
'success' => true,
'message' => 'Status berhasil diubah',
'status' => $referensiLink->is_active
]);
} catch (Exception $e) {
return response()->json([
'success' => false,
'message' => 'Gagal mengubah status: ' . $e->getMessage()
], 500);
}
}
/**
* Get kategori options for dropdown
*/
private function getKategoriOptions()
{
return [
'regulasi' => 'Regulasi',
'panduan' => 'Panduan',
'prosedur' => 'Prosedur',
'formulir' => 'Formulir',
'laporan' => 'Laporan',
'lainnya' => 'Lainnya'
];
}
private function parseActiveFilter($value): ?bool
{
if ($value === null || $value === '') {
return null;
}
if (is_bool($value)) {
return $value;
}
$val = strtolower(trim((string) $value));
if (in_array($val, ['1', 'true', 'aktif', 'active', 'yes', 'y'], true)) {
return true;
}
if (in_array($val, ['0', 'false', 'tidak', 'inactive', 'nonaktif', 'no', 'n'], true)) {
return false;
}
return null;
}
/**
* Download import template
*/
public function downloadTemplate()
{
//$this->authorize('referensi-link.import', $this->user);
try {
$headers = [
'Nama',
'Link',
'Kategori',
'Deskripsi',
'Status Aktif',
'Urutan'
];
$filename = 'template_referensi_link_' . date('YmdHis') . '.xlsx';
return Excel::download(new class($headers) implements \Maatwebsite\Excel\Concerns\FromArray, \Maatwebsite\Excel\Concerns\WithHeadings {
private $headers;
public function __construct($headers)
{
$this->headers = $headers;
}
public function array(): array
{
return [
['Contoh Referensi', 'https://example.com', 'panduan', 'Deskripsi contoh referensi link', 'aktif', 1],
['Contoh Regulasi', 'https://regulasi.example.com', 'regulasi', 'Deskripsi regulasi', 'aktif', 2],
];
}
public function headings(): array
{
return $this->headers;
}
}, $filename);
} catch (Exception $e) {
return redirect()
->back()
->with('error', 'Gagal download template: ' . $e->getMessage());
}
}
}