diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..4e7467d Binary files /dev/null and b/.DS_Store differ diff --git a/Helpers/PdfHelper.php b/Helpers/PdfHelper.php new file mode 100644 index 0000000..ace93d9 --- /dev/null +++ b/Helpers/PdfHelper.php @@ -0,0 +1,83 @@ + '<', + '>' => '>', + '&' => '&', + '"' => '"', + "'" => ''', + '≤' => '≤', + '≥' => '≥', + '≠' => '!=', + '≈' => '~', + '×' => 'x', + '÷' => '/', + '–' => '-', + '—' => '-', + '' => '"', + '' => '"', + '' => "'", + '' => "'", + ]; + + // First pass: replace with HTML entities + $safeText = str_replace(array_keys($replacements), array_values($replacements), $text); + + // Ensure UTF-8 encoding + if (!mb_check_encoding($safeText, 'UTF-8')) { + $safeText = mb_convert_encoding($safeText, 'UTF-8', 'auto'); + } + + // Remove any remaining non-ASCII characters that could cause issues + $safeText = preg_replace('/[^\x20-\x7E\xA0-\xFF]/', '', $safeText); + + return $safeText; + } + + /** + * Format mathematical symbols to text representation + * + * @param string $text + * @return string + */ + public static function formatMathSymbols($text) + { + if (empty($text)) { + return ''; + } + + $mathReplacements = [ + '<' => 'kurang dari', + '>' => 'lebih dari', + '<=' => 'kurang dari sama dengan', + '>=' => 'lebih dari sama dengan', + '!=' => 'tidak sama dengan', + '==' => 'sama dengan', + '≤' => 'kurang dari sama dengan', + '≥' => 'lebih dari sama dengan', + '≠' => 'tidak sama dengan', + '≈' => 'kira-kira', + '≡' => 'identik dengan', + '≅' => 'hampir sama dengan', + ]; + + return str_replace(array_keys($mathReplacements), array_values($mathReplacements), $text); + } +} diff --git a/Http/Controllers/ImageController.php b/Http/Controllers/ImageController.php new file mode 100644 index 0000000..5542013 --- /dev/null +++ b/Http/Controllers/ImageController.php @@ -0,0 +1,27 @@ +imageService = $imageService; + } + + public function show(Request $request, $path) + { + $width = $request->query('w'); + $quality = $request->query('q', 80); + + $resizedPath = $this->imageService->resize($path, $width, null, $quality); + + return response()->file(storage_path('app/public/' . $resizedPath)); + } +} diff --git a/app/.DS_Store b/app/.DS_Store new file mode 100644 index 0000000..a7bb668 Binary files /dev/null and b/app/.DS_Store differ diff --git a/app/Exports/ReferensiLinkExport.php b/app/Exports/ReferensiLinkExport.php new file mode 100644 index 0000000..2af7269 --- /dev/null +++ b/app/Exports/ReferensiLinkExport.php @@ -0,0 +1,128 @@ +filters = $filters; + } + + /** + * Query data yang akan diexport + */ + public function query() + { + $query = ReferensiLink::with(['createdBy', 'updatedBy']) + ->select('referensi_link.*'); + + // Apply filters + if (isset($this->filters['kategori']) && !empty($this->filters['kategori'])) { + $query->where('kategori', $this->filters['kategori']); + } + + if (isset($this->filters['status']) && $this->filters['status'] !== '') { + $query->where('is_active', $this->filters['status']); + } + + if (isset($this->filters['search']) && !empty($this->filters['search'])) { + $query->search($this->filters['search']); + } + + return $query->ordered(); + } + + /** + * Header kolom + */ + public function headings(): array + { + return [ + 'No', + 'Nama', + 'Link', + 'Kategori', + 'Deskripsi', + 'Status Aktif', + 'Urutan', + 'Dibuat Oleh', + 'Diupdate Oleh', + 'Tanggal Dibuat', + 'Tanggal Diupdate', + ]; + } + + /** + * Mapping data untuk setiap baris + */ + public function map($referensiLink): array + { + static $rowNumber = 0; + $rowNumber++; + + return [ + $rowNumber, + $referensiLink->name, + $referensiLink->link, + $referensiLink->kategori ?? '-', + $referensiLink->deskripsi ?? '-', + $referensiLink->is_active ? 'Aktif' : 'Tidak Aktif', + $referensiLink->urutan, + $referensiLink->createdBy ? $referensiLink->createdBy->name : '-', + $referensiLink->updatedBy ? $referensiLink->updatedBy->name : '-', + $referensiLink->created_at->format('d-m-Y H:i:s'), + $referensiLink->updated_at->format('d-m-Y H:i:s'), + ]; + } + + /** + * Styling untuk worksheet + */ + public function styles(Worksheet $sheet) + { + return [ + // Header styling + 1 => [ + 'font' => ['bold' => true, 'size' => 12], + 'fill' => ['fillType' => \PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID, 'startColor' => ['rgb' => 'E2EFDA']], + 'alignment' => ['horizontal' => \PhpOffice\PhpSpreadsheet\Style\Alignment::HORIZONTAL_CENTER], + ], + // Alternating row colors + 'A2:K1000' => [ + 'fill' => ['fillType' => \PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID, 'startColor' => ['rgb' => 'F8F9FA']], + ], + ]; + } + + /** + * Format kolom + */ + public function columnFormats(): array + { + return [ + 'F' => NumberFormat::FORMAT_DATE_DDMMYYYY, // Tanggal dibuat + 'G' => NumberFormat::FORMAT_DATE_DDMMYYYY, // Tanggal diupdate + ]; + } + + /** + * Custom title untuk sheet + */ + public function title(): string + { + return 'Referensi Link'; + } +} \ No newline at end of file diff --git a/app/Http/Controllers/# Run on VM.sh b/app/Http/Controllers/# Run on VM.sh new file mode 100644 index 0000000..93ed1a1 --- /dev/null +++ b/app/Http/Controllers/# Run on VM.sh @@ -0,0 +1,32 @@ +# Run on VM +rclone hashsum MD5 "gdrive:01_Data Mentah/itdel/bawang_putih/BPM1" --drive-shared-with-me --output-file itdel_bawang_putih_gdrive.txt +rclone hashsum MD5 "gcs:transit_bucket_ysds/itdel/BPM1" --output-file itdel_bawang_putih_gcs.txt +rclone check --checksum "gcs:transit_bucket_ysds/itdel/BPM1" "gdrive:01_Data Mentah/itdel/bawang_putih/BPM1" --drive-shared-with-me --combined="itdel_bawang_putih_cheksum_bucket_drive.txt" --log-file="itdel_bawang_putih_count_checksum_bucket_drive.txt" +# Run on Local Machine +rclone hashsum MD5 "/data/storageserver/0.RAW/WGS/revio call/Raw/itdel/bawang_putih/BPM1" --output-file itdel_bawang_putih_local.txt +rclone check --checksum "/data/storageserver/0.RAW/WGS/revio call/Raw/itdel/bawang_putih/BPM1" "gdrive:01_Data Mentah/itdel/bawang_putih/BPM1" --drive-shared-with-me --combined="itdel_bawang_putih_checksum_local_gdrive.txt" --log-file="itdel_bawang_putih_count_checksum_local_gdrive.txt" + +# Run on VM +rclone hashsum MD5 "gdrive:01_Data Mentah/itdel/kemenyan" --drive-shared-with-me --output-file itdel_kemenyan_gdrive.txt +rclone hashsum MD5 "gcs:transit_bucket_ysds/itdel/kemenyan" --output-file itdel_kemenyan_gcs.txt +rclone check --checksum "gcs:transit_bucket_ysds/itdel/kemenyan" "gdrive:01_Data Mentah/itdel/kemenyan" --drive-shared-with-me --combined="itdel_kemenyan_checksum_bucket_drive.txt" --log-file="itdel_kemenyan_count_checksum_bucket_drive.txt" +# Run on Local Machine +rclone hashsum MD5 "/data/storageserver/0.RAW/WGS/revio call/Raw/itdel/kemenyan" --output-file itdel_kemenyan_local.txt +rclone check --checksum "/data/storageserver/0.RAW/WGS/revio call/Raw/itdel/kemenyan" "gdrive:01_Data Mentah/itdel/kemenyan" --drive-shared-with-me --combined="itdel_kemenyan_checksum_local_gdrive.txt" --log-file="itdel_kemenyan_count_checksum_local_gdrive.txt" + +# Run on VM +rclone hashsum MD5 "gdrive:01_Data Mentah/ugm/prima/bawang_putih" --drive-shared-with-me --output-file ugm_prima_bawang_putih_gdrive.txt +rclone hashsum MD5 "gcs:transit_bucket_ysds/bawang_putih_prima_UGM" --output-file ugm_prima_bawang_putih_gcs.txt +rclone check --checksum "gcs:transit_bucket_ysds/bawang_putih_prima_UGM" "gdrive:01_Data Mentah/ugm/prima/bawang_putih" --drive-shared-with-me --combined="ugm_prima_bawang_putih_checksum_bucket_drive.txt" --log-file="ugm_prima_bawang_putih_count_checksum_bucket_drive.txt" +# Run on Local Machine +rclone hashsum MD5 "/data/storageserver/0.RAW/WGS/revio call/Raw/bawang_putih" --output-file ugm_prima_bawang_putih_local.txt +rclone check --checksum "/data/storageserver/0.RAW/WGS/revio call/Raw/bawang_putih" "gdrive:01_Data Mentah/ugm/prima/bawang_putih" --drive-shared-with-me --combined="ugm_prima_bawang_putih_checksum_local_gdrive.txt" --log-file="ugm_prima_bawang_putih_count_checksum_local_gdrive.txt" + + +# Run on VM +rclone hashsum MD5 "gdrive:01_Data Mentah/polije/netty/bawang_merah" --drive-shared-with-me --output-file polije_netty_bawang_merah_gdrive.txt +rclone hashsum MD5 "gcs:transit_bucket_ysds/bawang_merah_netty_polije" --output-file polije_netty_bawang_merah_gcs.txt +rclone check --checksum "gcs:transit_bucket_ysds/bawang_merah_netty_polije" "gdrive:01_Data Mentah/polije/netty/bawang_merah" --drive-shared-with-me --combined="polije_netty_bawang_merah_checksum_bucket_drive.txt" --log-file="polije_netty_bawang_merah_count_checksum_bucket_drive.txt" +# Run on Local Machine +rclone hashsum MD5 "/data/storageserver/0.RAW/WGS/revio call/Raw/bawang_merah" --output-file polije_netty_bawang_merah_local.txt +rclone check --checksum "/data/storageserver/0.RAW/WGS/revio call/Raw/bawang_merah" "gdrive:01_Data Mentah/polije/netty/bawang_merah" --drive-shared-with-me --combined="polije_netty_bawang_merah_checksum_local_gdrive.txt" --log-file="polije_netty_bawang_merah_count_checksum_local_gdrive.txt" diff --git a/app/Http/Controllers/BankDataController.php b/app/Http/Controllers/BankDataController.php index d42ce24..3d74a27 100644 --- a/app/Http/Controllers/BankDataController.php +++ b/app/Http/Controllers/BankDataController.php @@ -3,8 +3,8 @@ namespace Modules\Lpj\Http\Controllers; use Illuminate\Http\Request; - use Illuminate\Routing\Controller; - use Log; +use Illuminate\Routing\Controller; +use Illuminate\Support\Facades\Log; use Modules\Location\Models\Province; use Modules\Lpj\Http\Requests\BankDataRequest; use Modules\Lpj\Models\BankData; @@ -88,7 +88,8 @@ } } else { // Invalid coordinates - Log::warning("Invalid coordinates: Lat: $_lat, Lng: $_lng");// Do something to handle this situation, such as logging an error or skipping the record + Log::warning("Invalid coordinates: Lat: $_lat, Lng: $_lng"); + // Do something to handle this situation, such as logging an error or skipping the record } } @@ -138,7 +139,8 @@ } } else { // Invalid coordinates - Log::warning("Invalid coordinates: Lat: $lat, Lng: $lng");// Do something to handle this situation, such as logging an error or skipping the record + Log::warning("Invalid coordinates: Lat: $lat, Lng: $lng"); + // Do something to handle this situation, such as logging an error or skipping the record } } } @@ -197,6 +199,24 @@ // Retrieve data from the database $query = BankData::query(); + // Check if show_all parameter is set + $showAll = $request->has('show_all') && $request->get('show_all') === 'true'; + + // If show_all is true, we'll get all data without pagination + if ($showAll) { + // Get all records without pagination + $data = $query->get(); + + return response()->json([ + 'data' => $data, + 'recordsTotal' => $data->count(), + 'recordsFiltered' => $data->count(), + 'page' => 1, + 'pageSize' => $data->count(), + 'total' => 1 + ]); + } + // Apply search filter if provided if ($request->has('search') && !empty($request->get('search'))) { $search = $request->get('search'); @@ -252,8 +272,10 @@ // Get the total count of records $totalRecords = $query->count(); - // Apply pagination if provided - if ($request->has('page') && $request->has('size')) { + // Apply pagination only if explicitly requested or not first load + $shouldPaginate = $request->has('page') && $request->has('size') && !$request->has('show_all'); + + if ($shouldPaginate) { $page = $request->get('page'); $size = $request->get('size'); $offset = ($page - 1) * $size; // Calculate the offset @@ -287,11 +309,11 @@ ]; }); - // Calculate the page count - $pageCount = ceil($totalRecords / $request->get('size')); + // Calculate the page count (1 if showing all data) + $pageCount = $shouldPaginate ? ceil($totalRecords / $request->get('size')) : 1; // Calculate the current page number - $currentPage = $request->get('page', 1); + $currentPage = $shouldPaginate ? $request->get('page', 1) : 1; // Ensure current page doesn't exceed page count $currentPage = min($currentPage, $pageCount); diff --git a/app/Http/Controllers/ReferensiLinkController.php b/app/Http/Controllers/ReferensiLinkController.php new file mode 100644 index 0000000..5a2e988 --- /dev/null +++ b/app/Http/Controllers/ReferensiLinkController.php @@ -0,0 +1,441 @@ +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' => '' . Str::limit($row->link, 50) . ' ', + 'kategori' => $row->kategori, + 'status_badge' => $row->status_badge, + 'urutan' => $row->urutan, + 'actions' => ( + (auth()->user()->can('referensi-link.update') ? ' Edit' : '') . + (auth()->user()->can('referensi-link.delete') ? ' Hapus' : '') + ), + ]; + }); + + 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()); + } + } +} diff --git a/app/Http/Controllers/SurveyorController.php b/app/Http/Controllers/SurveyorController.php index 5d17de3..4976b8a 100644 --- a/app/Http/Controllers/SurveyorController.php +++ b/app/Http/Controllers/SurveyorController.php @@ -883,11 +883,10 @@ class SurveyorController extends Controller $penilaian = Penilaian::findOrFail($id); $permohonan = Permohonan::where('nomor_registrasi', $penilaian->nomor_registrasi)->first(); - ; if (Carbon::parse($validate['waktu_penilaian']) <= Carbon::parse($penilaian->tanggal_kunjungan)) { return response()->json([ 'success' => false, - 'message' => 'Waktu penilaian harus lebih besar dari tanggal assign.' + 'message' => 'Waktu penilaian harus lebih besar dari tanggal assign.'.$penilaian->tanggal_kunjungan.' '.$validate['waktu_penilaian'] ], 422); } @@ -2924,29 +2923,29 @@ class SurveyorController extends Controller // Jika ada data tanpa dokument_id, cek apakah ada data dengan dokument_id yang sama if ($dataWithoutDokument->isNotEmpty() && $dataWithDokument->isNotEmpty()) { - + // Group data dengan dokument_id by created_by $groupedDataWithDokument = $dataWithDokument->groupBy('created_by'); - + // Group data tanpa dokument_id by created_by $groupedDataWithoutDokument = $dataWithoutDokument->groupBy('created_by'); // Proses cleanup untuk setiap user foreach ($groupedDataWithDokument as $userId => $userDataWithDokument) { - + // Cek apakah user ini juga memiliki data tanpa dokument_id if (isset($groupedDataWithoutDokument[$userId])) { - + // Ambil salah satu data dengan dokument_id sebagai referensi untuk logging $referenceData = $userDataWithDokument->first(); - + Log::info('SurveyorController: Menemukan data lengkap untuk user, akan menghapus data tidak lengkap', [ 'user_id' => $userId, 'permohonan_id' => $permohonanId, 'reference_dokument_id' => $referenceData->dokument_id, 'data_count_to_delete' => $groupedDataWithoutDokument[$userId]->count() ]); - + // Ambil semua data tanpa dokument_id untuk user ini $userDataWithoutDokument = $groupedDataWithoutDokument[$userId]; @@ -2956,7 +2955,7 @@ class SurveyorController extends Controller try { // Soft delete data $dataToDelete->delete(); - + Log::info('SurveyorController: Data inspeksi berhasil di-soft delete', [ 'id' => $dataToDelete->id, 'permohonan_id' => $dataToDelete->permohonan_id, diff --git a/app/Http/Requests/ReferensiLinkRequest.php b/app/Http/Requests/ReferensiLinkRequest.php new file mode 100644 index 0000000..5d7ecce --- /dev/null +++ b/app/Http/Requests/ReferensiLinkRequest.php @@ -0,0 +1,114 @@ + + */ + public function rules(): array + { + $rules = [ + 'name' => 'required|string|max:255', + 'link' => 'required|string|max:500', + 'kategori' => 'nullable|string|max:100', + 'deskripsi' => 'nullable|string|max:2000', + 'is_active' => 'boolean', + 'urutan' => 'nullable|integer|min:0', + ]; + + // Validasi tambahan untuk link + $rules['link'] .= '|url'; + + return $rules; + } + + /** + * Determine if the user is authorized to make this request. + * + * @return bool + */ + public function authorize(): bool + { + return true; + } + + /** + * Get custom messages for validator errors. + * + * @return array + */ + public function messages(): array + { + return [ + 'name.required' => 'Nama referensi link wajib diisi', + 'name.string' => 'Nama referensi link harus berupa teks', + 'name.max' => 'Nama referensi link maksimal 255 karakter', + 'link.required' => 'Link wajib diisi', + 'link.string' => 'Link harus berupa teks', + 'link.max' => 'Link maksimal 500 karakter', + 'link.url' => 'Link harus berupa URL yang valid', + 'kategori.string' => 'Kategori harus berupa teks', + 'kategori.max' => 'Kategori maksimal 100 karakter', + 'deskripsi.string' => 'Deskripsi harus berupa teks', + 'deskripsi.max' => 'Deskripsi maksimal 2000 karakter', + 'is_active.boolean' => 'Status aktif harus berupa ya/tidak', + 'urutan.integer' => 'Urutan harus berupa angka', + 'urutan.min' => 'Urutan minimal 0', + ]; + } + + /** + * Get custom attributes for validator errors. + * + * @return array + */ + public function attributes(): array + { + return [ + 'name' => 'Nama Referensi Link', + 'link' => 'Link', + 'kategori' => 'Kategori', + 'deskripsi' => 'Deskripsi', + 'is_active' => 'Status Aktif', + 'urutan' => 'Urutan', + ]; + } + + /** + * Prepare the data for validation. + * + * @return void + */ + protected function prepareForValidation(): void + { + // Format link jika belum memiliki protocol + if ($this->has('link')) { + $link = $this->input('link'); + if ($link && !preg_match('/^(https?:\/\/)/i', $link)) { + $this->merge([ + 'link' => 'https://' . $link + ]); + } + } + + // Set default is_active jika tidak diset + if (!$this->has('is_active')) { + $this->merge([ + 'is_active' => true + ]); + } + + // Set default urutan jika tidak diset atau 0 + if (!$this->has('urutan') || empty($this->input('urutan'))) { + $this->merge([ + 'urutan' => 0 + ]); + } + } +} \ No newline at end of file diff --git a/app/Imports/ReferensiLinkImport.php b/app/Imports/ReferensiLinkImport.php new file mode 100644 index 0000000..e82a94a --- /dev/null +++ b/app/Imports/ReferensiLinkImport.php @@ -0,0 +1,166 @@ +importedCount++; + + return new ReferensiLink([ + 'name' => $row['nama'] ?? $row['name'], + 'link' => $this->formatLink($row['link'] ?? $row['url'] ?? ''), + 'kategori' => $row['kategori'] ?? $row['category'] ?? 'lainnya', + 'deskripsi' => $row['deskripsi'] ?? $row['description'] ?? null, + 'is_active' => $this->parseStatus($row['status_aktif'] ?? $row['is_active'] ?? 'aktif'), + 'urutan' => $this->parseUrutan($row['urutan'] ?? $row['order'] ?? 0), + 'created_by' => Auth::id(), + 'updated_by' => Auth::id(), + ]); + + } catch (\Exception $e) { + $this->failedCount++; + $this->errors[] = "Baris {$row['row_number']}: " . $e->getMessage(); + Log::error('Import ReferensiLink Error: ' . $e->getMessage(), ['row' => $row]); + return null; + } + } + + /** + * Validation rules + */ + public function rules(): array + { + return [ + 'nama' => 'required|string|max:255', + 'name' => 'required|string|max:255', + 'link' => 'required|string|max:500', + 'url' => 'required|string|max:500', + 'kategori' => 'nullable|string|max:100', + 'category' => 'nullable|string|max:100', + 'deskripsi' => 'nullable|string|max:2000', + 'description' => 'nullable|string|max:2000', + 'status_aktif' => 'nullable|string|in:aktif,tidak aktif,1,0,true,false', + 'is_active' => 'nullable|string|in:aktif,tidak aktif,1,0,true,false', + 'urutan' => 'nullable|integer|min:0', + 'order' => 'nullable|integer|min:0', + ]; + } + + /** + * Custom validation messages + */ + public function customValidationMessages() + { + return [ + 'nama.required' => 'Nama wajib diisi', + 'name.required' => 'Nama wajib diisi', + 'link.required' => 'Link wajib diisi', + 'url.required' => 'Link wajib diisi', + 'link.url' => 'Link harus berupa URL yang valid', + 'url.url' => 'Link harus berupa URL yang valid', + 'kategori.max' => 'Kategori maksimal 100 karakter', + 'category.max' => 'Kategori maksimal 100 karakter', + 'deskripsi.max' => 'Deskripsi maksimal 2000 karakter', + 'description.max' => 'Deskripsi maksimal 2000 karakter', + ]; + } + + /** + * Batch size for inserts + */ + public function batchSize(): int + { + return 100; + } + + /** + * Chunk size for reading + */ + public function chunkSize(): int + { + return 100; + } + + /** + * Format link to ensure it has protocol + */ + private function formatLink($link) + { + if (empty($link)) { + return null; + } + + // Remove any whitespace + $link = trim($link); + + // Add protocol if not present + if (!preg_match('/^(https?:\/\/)/i', $link)) { + $link = 'https://' . $link; + } + + return $link; + } + + /** + * Parse status value + */ + private function parseStatus($status) + { + if (empty($status)) { + return true; + } + + $status = strtolower(trim($status)); + + return in_array($status, ['aktif', '1', 'true', 'yes', 'ya']); + } + + /** + * Parse order value + */ + private function parseUrutan($urutan) + { + if (empty($urutan)) { + return 0; + } + + return (int) $urutan; + } + + /** + * Get import statistics + */ + public function getImportStats(): array + { + return [ + 'imported' => $this->importedCount, + 'failed' => $this->failedCount, + 'errors' => $this->errors, + 'success' => $this->importedCount - $this->failedCount, + ]; + } +} \ No newline at end of file diff --git a/app/Models/ReferensiLink.php b/app/Models/ReferensiLink.php new file mode 100644 index 0000000..374fc85 --- /dev/null +++ b/app/Models/ReferensiLink.php @@ -0,0 +1,152 @@ + 'boolean', + 'urutan' => 'integer', + 'created_at' => 'datetime', + 'updated_at' => 'datetime', + ]; + + /** + * Boot the model. + */ + protected static function boot() + { + parent::boot(); + + static::creating(function ($model) { + if (Auth::check()) { + $model->created_by = Auth::id(); + $model->updated_by = Auth::id(); + } + }); + + static::updating(function ($model) { + if (Auth::check()) { + $model->updated_by = Auth::id(); + } + }); + } + + /** + * Scope untuk filter data aktif + */ + public function scopeActive($query) + { + return $query->where('is_active', true); + } + + /** + * Scope untuk filter berdasarkan kategori + */ + public function scopeByKategori($query, $kategori) + { + return $query->where('kategori', $kategori); + } + + /** + * Scope untuk urutkan berdasarkan urutan + */ + public function scopeOrdered($query) + { + return $query->orderBy('urutan', 'asc')->orderBy('name', 'asc'); + } + + /** + * Scope untuk pencarian + */ + public function scopeSearch($query, $search) + { + return $query->where(function ($q) use ($search) { + $q->where('name', 'like', "%{$search}%") + ->orWhere('link', 'like', "%{$search}%") + ->orWhere('kategori', 'like', "%{$search}%") + ->orWhere('deskripsi', 'like', "%{$search}%"); + }); + } + + /** + * Relasi ke user yang membuat + */ + public function createdBy() + { + return $this->belongsTo(User::class, 'created_by'); + } + + /** + * Relasi ke user yang update + */ + public function updatedBy() + { + return $this->belongsTo(User::class, 'updated_by'); + } + + /** + * Accessor untuk status badge + */ + public function getStatusBadgeAttribute() + { + return $this->is_active + ? 'Aktif' + : 'Tidak Aktif'; + } + + /** + * Accessor untuk link yang diformat + */ + public function getFormattedLinkAttribute() + { + return $this->link ? url($this->link) : null; + } + + /** + * Mutator untuk memastikan link valid + */ + public function setLinkAttribute($value) + { + // Validasi dan format link + if ($value && !preg_match('/^(https?:\/\/)/i', $value)) { + $value = 'https://' . $value; + } + $this->attributes['link'] = $value; + } +} diff --git a/commit-message-staged.txt b/commit-message-staged.txt new file mode 100644 index 0000000..30c9b5a --- /dev/null +++ b/commit-message-staged.txt @@ -0,0 +1,75 @@ +🐛 fix(lpj): Perbaikan format Rupiah, role access, dan validasi data + +## Ringkasan +Melakukan perbaikan pada helper format Rupiah, akses role user, validasi data MIG, serta penyesuaian tampilan laporan dan dokumentasi. + +## Perubahan Detail + +### 🔧 Helper Function +**app/Helpers/Lpj.php**: +- Menambahkan parameter opsional `withSymbol` pada fungsi `formatRupiah()` untuk kontrol simbol Rp +- Menambahkan handling untuk menghapus titik (.) dari input number sebelum proses +- Memperbaiki return value untuk null/empty string sesuai parameter `withSymbol` +- Mengganti `str_pad()` dengan `sprintf()` untuk generate random number (lebih efisien) + +### 🛠️ Service Layer +**app/Services/PreviewLaporanService.php**: +- Memperbaiki validasi data MIG dengan menambahkan pengecekan `is_mig` flag +- Menambahkan null safety pada property `mig_mst_lpj_tot_nilai_pasar` +- Memperbaiki kondisi logic untuk memo dan validasi nilai pasar + +### 🎨 View Components +**resources/views/component/print-out-dokument.blade.php**: +- Memperbaiki syntax Blade dari `@isset` menjadi `isset()` yang lebih proper + +**resources/views/debitur/components/debitur.blade.php**: +- Memperbaiki role checking dari `hasRole()` menjadi `hasAnyRole()` untuk multiple roles + +**resources/views/debitur/index.blade.php**: +- Menambahkan role 'admin' pada kondisi edit dan delete actions +- Memperbaiki permission checking untuk administrator dan admin + +**resources/views/laporan/index.blade.php**: +- Menyederhanakan logic tombol laporan dan resume +- Menghapus logic role-based yang kompleks untuk tombol laporan +- Memperbaiki route URL untuk print-out laporan +- Menghapus function `generateLaporanButton()` yang tidak digunakan + +**resources/views/penilai/components/lpj-sederhana-standar.blade.php**: +- Menambahkan role 'penilai' pada permission tombol simpan + +**resources/views/penilai/components/print-out-sederhana.blade.php**: +- Memperbaiki tampilan data dokumen dengan menambahkan kolom nomor dokumen +- Mengganti `number_format()` dengan `formatRupiah()` untuk konsistensi format +- Menambahkan fallback untuk data tanah dan bangunan ketika `npw_tambahan` tidak tersedia +- Memperbaiki perhitungan total nilai pasar wajar dengan proper parsing +- Memperbaiki format tampilan nilai likuidasi +- Memperbaiki struktur HTML tabel untuk dokumentasi + +**resources/views/penilai/components/signature-approval.blade.php**: +- Memperbaiki route dan parameter untuk approval signature + +**resources/views/permohonan/index.blade.php**: +- Menambahkan role 'admin' pada permission actions + +## Alasan Perubahan +1. **Format Rupiah**: Menambahkan fleksibilitas untuk menampilkan nominal dengan atau tanpa simbol Rp sesuai kebutuhan tampilan +2. **Validasi Data**: Memperkuat validasi data MIG untuk mencegah error pada data yang tidak lengkap +3. **Role Access**: Memperbaiki permission checking untuk mencakup role admin yang sebelumnya terlewat +4. **Tampilan Laporan**: Menyederhanakan UI dan memperbaiki format tampilan nilai untuk konsistensi +5. **Fallback Data**: Menambahkan handling untuk kasus data tidak lengkap pada laporan penilaian + +## Dampak +- ✅ Format Rupiah lebih fleksibel dengan opsi simbol +- ✅ Validasi data MIG lebih kuat dan aman +- ✅ Role admin sekarang memiliki akses yang sesuai +- ✅ Tampilan laporan lebih konsisten dan rapi +- ✅ Penanganan error untuk data tidak lengkap lebih baik + +## Testing +Pastikan untuk: +1. Test format Rupiah dengan berbagai skenario (dengan/ tanpa simbol) +2. Test akses role admin pada semua fitur yang diperbarui +3. Test validasi data MIG dengan data lengkap dan tidak lengkap +4. Test tampilan laporan dengan data npw_tambahan kosong +5. Verifikasi perhitungan total nilai pasar wajar tetap akurat \ No newline at end of file diff --git a/database/.DS_Store b/database/.DS_Store new file mode 100644 index 0000000..cdbc7bb Binary files /dev/null and b/database/.DS_Store differ diff --git a/database/migrations/2025_11_26_153010_create_referensi_link_table.php b/database/migrations/2025_11_26_153010_create_referensi_link_table.php new file mode 100644 index 0000000..599034c --- /dev/null +++ b/database/migrations/2025_11_26_153010_create_referensi_link_table.php @@ -0,0 +1,45 @@ +id(); + $table->string('name', 255)->nullable(false)->comment('Nama referensi link'); + $table->text('link')->nullable(false)->comment('URL link referensi'); + $table->string('kategori', 100)->nullable()->comment('Kategori referensi (misal: regulasi, panduan, dll)'); + $table->text('deskripsi')->nullable()->comment('Deskripsi lengkap referensi'); + $table->boolean('is_active')->default(true)->comment('Status aktif referensi'); + $table->integer('urutan')->default(0)->comment('Urutan tampil referensi'); + $table->unsignedBigInteger('created_by')->nullable()->comment('ID user yang membuat'); + $table->unsignedBigInteger('updated_by')->nullable()->comment('ID user yang update terakhir'); + $table->timestamps(); + + // Indexes + $table->index('kategori'); + $table->index('is_active'); + $table->index('urutan'); + $table->index('created_by'); + + // Foreign keys + $table->foreign('created_by')->references('id')->on('users')->onDelete('set null'); + $table->foreign('updated_by')->references('id')->on('users')->onDelete('set null'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('referensi_link'); + } +}; diff --git a/module.json b/module.json index b437699..2e0b569 100644 --- a/module.json +++ b/module.json @@ -290,15 +290,7 @@ "classes": "", "attributes": [], "permission": "", - "roles": [ - "administrator", - "admin", - "DD Appraisal", - "EO Appraisal", - "senior-officer", - "penilai", - "surveyor" - ] + "roles": [] }, { "title": "Data Permohonan", @@ -1231,21 +1223,43 @@ "system": [ { "title": "Daftar Pustaka", - "path": "daftar-pustaka", + "path": "", "icon": "ki-filled ki-filter-tablet text-lg text-primary", "classes": "", "attributes": [], "permission": "", "roles": [ "administrator", - "pemohon-ao", - "pemohon-eo", - "admin", - "DD Appraisal", - "EO Appraisal", - "senior-officer" + "admin" + ], + "sub": [ + { + "title": "Daftar Pustaka", + "path": "daftar-pustaka", + "icon": "ki-filled ki-external-link text-lg text-primary", + "classes": "", + "attributes": [], + "permission": "", + "roles": [ + "administrator", + "admin" + ] + }, + { + "title": "Referensi Link", + "path": "basicdata.referensi-link", + "icon": "ki-filled ki-external-link text-lg text-primary", + "classes": "", + "attributes": [], + "permission": "", + "roles": [ + "administrator", + "admin" + ] + } ] } ] } } + diff --git a/resources/.DS_Store b/resources/.DS_Store new file mode 100644 index 0000000..9b32c64 Binary files /dev/null and b/resources/.DS_Store differ diff --git a/resources/views/bank-data/index.blade.php b/resources/views/bank-data/index.blade.php index 8ad4b42..e6527eb 100644 --- a/resources/views/bank-data/index.blade.php +++ b/resources/views/bank-data/index.blade.php @@ -5,17 +5,47 @@ @endsection @section('content') -
+ + +
+
+
+
-
-
+
+

Filter

-
+
-
+
@@ -77,14 +107,20 @@
- +
- +
-
+
+
@@ -95,21 +131,21 @@
-
-
+
+

Maps

-
+
-
-
-
+
+
+

Daftar Bank Data

@@ -128,7 +164,7 @@
- +
@@ -174,13 +210,13 @@
-