Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions app/Exports/ExportDataSarana.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

namespace App\Exports;

use Illuminate\Support\Collection;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithStyles;
use Maatwebsite\Excel\Concerns\WithEvents;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use Maatwebsite\Excel\Events\AfterSheet;

class ExportDataSarana implements FromCollection, WithHeadings, WithStyles, WithEvents
{
protected $author;
protected $data;

public function __construct(Collection $data, $author = 'Admin')
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] ⚡ Performance: Memory Overflow pada Export Besar

Masalah: Method collection() load semua data sekaligus ke memory tanpa chunking. Untuk dataset besar (>10k rows), ini akan menyebabkan memory exhaustion.

Kode: return DataSarana::with('desa')->get();

Dampak: Export 50k sarana dengan relasi desa bisa consume 500MB+ memory. Server dengan memory_limit 256M akan crash dengan error "Allowed memory size exhausted".

Fix:

// Ganti FromCollection dengan FromQuery + chunking
use Maatwebsite\Excel\Concerns\FromQuery;

class ExportDataSarana implements FromQuery, WithHeadings, WithStyles, WithEvents
{
    public function query()
    {
        return DataSarana::query()->with('desa:id,nama');
    }
    
    // Maatwebsite Excel akan otomatis chunk query ini
    // Atau gunakan cursor() untuk memory efficiency
}

{
$this->data = $data;
$this->author = $author;
}

public function collection()
{
return $this->data->map(function ($item) {
return [
$item->id,
$item->desa->nama ?? '-',
$item->nama,
$item->jumlah,
$item->kategori,
$item->keterangan,
];
});
}

public function headings(): array
{
return [
['Laporan Data Sarana'],
[''],
['ID', 'Desa', 'Nama Sarana', 'Jumlah', 'Kategori', 'Keterangan'],
];
}

public function styles(Worksheet $sheet)
{
return [
1 => ['font' => ['bold' => true, 'size' => 14]],
3 => ['font' => ['bold' => true, 'color' => ['rgb' => 'FFFFFF']]],
];
}

public function registerEvents(): array
{
return [
AfterSheet::class => function (AfterSheet $event) {
$sheet = $event->sheet->getDelegate();

$sheet->mergeCells('A1:F1');
$sheet->getStyle('A1')->getAlignment()->setHorizontal('center');

$sheet->setCellValue('A2', 'Nama: ' . $this->author);
$sheet->setCellValue('F2', 'Tanggal: ' . date('d-m-Y'));

$sheet->getStyle('A2')->getAlignment()->setHorizontal('left');
$sheet->getStyle('F2')->getAlignment()->setHorizontal('right');

$sheet->getStyle('A3:F3')->getFill()
->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID)
->getStartColor()->setARGB('4CAF50');

$lastRow = $sheet->getHighestRow();
$sheet->getStyle("A3:F{$lastRow}")
->getBorders()
->getAllBorders()
->setBorderStyle(\PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN);
},
];
}
}
169 changes: 169 additions & 0 deletions app/Http/Controllers/Data/DataSaranaController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
<?php

namespace App\Http\Controllers\Data;

use App\Http\Controllers\Controller;
use App\Models\DataDesa;
use App\Models\DataSarana;
use Illuminate\Http\Request;
use Maatwebsite\Excel\Facades\Excel;
use App\Exports\ExportDataSarana;
use App\Imports\ImportDataSarana;
use Illuminate\Support\Facades\DB;
use Yajra\DataTables\Facades\DataTables;

class DataSaranaController extends Controller
{
public function index()
{
$page_title = 'Data Sarana';
$page_description = 'Daftar Sarana Desa';

$desaSelect = DataDesa::all();

return view('data.data_sarana.index', compact('page_title', 'page_description', 'desaSelect'));
}

public function getData(Request $request)
{
$query = DataSarana::with('desa');

if ($request->desa_id) {
$query->where('desa_id', $request->desa_id);
}
if ($request->kategori) {
$query->where('kategori', $request->kategori);
}

return datatables()->of($query)
->addColumn('desa', function ($row) {
return $row->desa ? $row->desa->nama : '-';
})
->addColumn('aksi', function ($row) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] ⚡ Performance: N+1 Query di DataTables

Masalah: Method getData() tidak melakukan eager loading relasi desa, menyebabkan N+1 queries saat render DataTables dengan banyak rows.

Kode: $query = DataSarana::query();

Dampak: Pada list dengan 100 sarana, akan terjadi 1 query utama + 100 query untuk load relasi desa = 101 queries total. Di production dengan traffic tinggi, ini bisa menyebabkan database bottleneck.

Fix:

// Line 42 - Tambahkan eager loading
$query = DataSarana::with('desa:id,nama')->query();

// Atau jika perlu filter desa
if ($request->has('desa_id') && $request->desa_id != '') {
    $query->where('desa_id', $request->desa_id);
}

$editUrl = route('data.data-sarana.edit', $row->id);
$deleteUrl = route('data.data-sarana.destroy', $row->id);

return view('data.data_sarana.partials.action', compact('editUrl', 'deleteUrl'))->render();
})
->rawColumns(['aksi'])
->make(true);
}


public function create()
{
$page_title = "Tambah Sarana";
$page_description = "Form tambah data sarana";

$desas = DataDesa::all();
return view('data.data_sarana.create', compact('page_title', 'page_description', 'desas'));
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 📝 Code Quality: Missing FormRequest Class

Kategori: Architecture
Masalah: Validasi dilakukan langsung di controller dengan $request->validate(). Best practice Laravel: buat FormRequest class untuk validasi terpusat dan reusable
Kode: Method store() dan update() melakukan validasi inline

Fix:

// 1. Buat app/Http/Requests/DataSaranaRequest.php
namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class DataSaranaRequest extends FormRequest
{
    public function authorize()
    {
        return true;
    }

    public function rules()
    {
        return [
            'desa_id' => 'required|integer|exists:das_data_desa,id',
            'nama' => 'required|string|max:255',
            'jumlah' => 'required|integer|min:0',
            'kategori' => 'required|string|max:255',
            'keterangan' => 'nullable|string|max:255',
        ];
    }
}

// 2. Update controller methods:
public function store(DataSaranaRequest $request)
{
    DataSarana::create($request->validated());
    return redirect()->route('data.data-sarana.index')
        ->with('success', 'Data Sarana berhasil ditambahkan');
}

public function update(DataSaranaRequest $request, $id)
{
    $sarana = DataSarana::findOrFail($id);
    $sarana->update($request->validated());
    return redirect()->route('data.data-sarana.index')
        ->with('success', 'Data Sarana berhasil diupdate');
}


public function store(Request $request)
{
try {
$request->validate([
'desa_id' => 'required|integer:desa_id',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] 📝 Code Quality: Invalid Validation Rule Syntax

Kategori: PHP Quality
Masalah: Syntax validasi salah - integer:desa_id bukan format valid Laravel. Seharusnya integer|exists:table,column
Kode: 'desa_id' => 'required|integer:desa_id',

Fix:

'desa_id' => 'required|integer|exists:das_data_desa,id',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] 🐛 Bug: Invalid Validation Rule Syntax

Kode: 'desa_id' => 'required|integer:desa_id'

Skenario: Ketika user submit form create, Laravel validation akan throw exception karena rule 'integer:desa_id' adalah syntax yang salah. Rule 'integer' tidak menerima parameter dengan colon. Ini akan menyebabkan 500 error di production.

Dampak: Form create tidak bisa digunakan sama sekali. Setiap submit akan crash dengan error "Method Illuminate\Validation\Validator::validateInteger does not exist" atau validation gagal silent.

Fix:

'desa_id' => 'required|integer|exists:das_data_desa,id',

'nama' => 'required|string|max:255',
'jumlah' => 'required|integer|min:0',
'kategori' => 'required|string|max:100',
'keterangan' => 'required|string:max:255',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] 📝 Code Quality: Invalid Validation Rule Syntax

Kategori: PHP Quality
Masalah: Syntax validasi salah - string:max:255 bukan format valid. Seharusnya string|max:255
Kode: 'keterangan' => 'required|string:max:255',

Fix:

'keterangan' => 'nullable|string|max:255',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] 🐛 Bug: Invalid Validation Rule Syntax

Kode: 'keterangan' => 'required|string:max:255'

Skenario: Syntax validation salah, seharusnya 'string|max:255' bukan 'string:max:255'. Colon digunakan untuk parameter rule tertentu (seperti max:255), bukan untuk chain rule. Ini akan menyebabkan validation error atau exception.

Dampak: Form create akan crash atau validation tidak berjalan dengan benar. User tidak bisa menyimpan data sarana baru.

Fix:

'keterangan' => 'nullable|string|max:255',
// Ubah juga dari 'required' ke 'nullable' karena keterangan seharusnya optional

]);
DataSarana::create($request->all());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] 📝 Code Quality: Mass Assignment Vulnerability

Kategori: Architecture
Masalah: Menggunakan $request->all() membuka celah mass assignment - user bisa inject field yang tidak diinginkan (misal: id, created_at, dll)
Kode: DataSarana::create($request->all());

Fix:

DataSarana::create($request->only(['desa_id', 'nama', 'jumlah', 'kategori', 'keterangan']));
// atau lebih baik:
DataSarana::create($request->validated());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 🐛 Bug: Mass Assignment Vulnerability

Kode: DataSarana::create($request->all());

Skenario: Menggunakan $request->all() memungkinkan attacker mengirim field tambahan yang tidak diinginkan dalam POST request. Jika model tidak memiliki $fillable/$guarded yang ketat, field seperti 'id', 'created_at', atau field lain bisa di-inject.

Dampak: Security vulnerability. Attacker bisa manipulasi data dengan menambahkan field tidak authorized dalam request payload. Misalnya inject 'id' untuk overwrite record lain, atau field internal lainnya.

Fix:

DataSarana::create($request->only(['desa_id', 'nama', 'jumlah', 'kategori', 'keterangan']));
// Atau lebih baik:
DataSarana::create($request->validated());

} catch (\Exception $e) {
report($e);
return back()->withInput()->with('error', 'Data Sarana gagal disimpan!');
}
return redirect()->route('data.data-sarana.index')->with('success', 'Data Sarana berhasil disimpan!');
}

public function edit($id)
{
$page_title = 'Edit Data Sarana';
$page_description = 'Ubah informasi sarana desa';

$sarana = DataSarana::findOrFail($id);
$desas = DataDesa::all();

return view('data.data_sarana.edit', compact('page_title', 'page_description', 'sarana', 'desas'));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 🐛 Bug: Missing Null Check Before Method Call

Kode: $sarana = DataSarana::find($id); return view('data.data_sarana.edit', compact('sarana', 'desas'));

Skenario: Jika DataSarana::find($id) tidak menemukan record (return null), view akan menerima $sarana = null. Ketika blade template mencoba akses $sarana->nama, $sarana->kategori, dll, akan terjadi error "Attempt to read property on null".

Dampak: User yang akses URL /edit/999 (ID tidak exist) akan mendapat 500 error instead of 404. Ini juga bisa terjadi karena race condition (record dihapus setelah user buka halaman list tapi sebelum klik edit).

Fix:

$sarana = DataSarana::findOrFail($id);
// findOrFail() otomatis throw 404 jika record tidak ditemukan

}

public function update(Request $request, $id)
{
try {
$request->validate([
'desa_id' => 'required|integer:desa_id',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] 📝 Code Quality: Invalid Validation Rule Syntax

Kategori: PHP Quality
Masalah: Syntax validasi salah - integer:desa_id bukan format valid Laravel. Seharusnya integer|exists:table,column
Kode: 'desa_id' => 'required|integer:desa_id',

Fix:

'desa_id' => 'required|integer|exists:das_data_desa,id',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] 🐛 Bug: Invalid Validation Rule Syntax

Kode: 'desa_id' => 'required|integer:desa_id'

Skenario: Bug yang sama dengan line 66, tapi di method update(). Validation rule syntax salah akan menyebabkan exception saat user submit form edit.

Dampak: Form edit tidak bisa digunakan. Setiap update akan crash dengan validation error atau exception.

Fix:

'desa_id' => 'required|integer|exists:das_data_desa,id',

'nama' => 'required|string|max:255',
'jumlah' => 'required|integer|min:0',
'kategori' => 'required|string|max:100',
'keterangan' => 'required|string:max:255',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] 📝 Code Quality: Invalid Validation Rule Syntax

Kategori: PHP Quality
Masalah: Syntax validasi salah - string:max:255 bukan format valid. Seharusnya string|max:255
Kode: 'keterangan' => 'required|string:max:255',

Fix:

'keterangan' => 'nullable|string|max:255',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[CRITICAL] 🐛 Bug: Invalid Validation Rule Syntax

Kode: 'keterangan' => 'required|string:max:255'

Skenario: Bug yang sama dengan line 70, tapi di method update(). Syntax validation salah akan crash form edit.

Dampak: User tidak bisa update data sarana yang sudah ada. Form edit tidak fungsional.

Fix:

'keterangan' => 'nullable|string|max:255',

]);
$sarana = DataSarana::findOrFail($id);
$sarana->update($request->all());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 📝 Code Quality: Mass Assignment Vulnerability

Kategori: Architecture
Masalah: Menggunakan $request->all() membuka celah mass assignment - user bisa inject field yang tidak diinginkan
Kode: $sarana->update($request->all());

Fix:

$sarana->update($request->only(['desa_id', 'nama', 'jumlah', 'kategori', 'keterangan']));
// atau lebih baik:
$sarana->update($request->validated());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 🐛 Bug: Mass Assignment Vulnerability

Kode: $sarana->update($request->all());

Skenario: Sama seperti line 72, menggunakan $request->all() di update memungkinkan injection field tidak diinginkan. Attacker bisa menambahkan field dalam PUT request untuk memodifikasi data yang seharusnya protected.

Dampak: Security vulnerability. Attacker bisa manipulasi field yang tidak seharusnya bisa diubah melalui form edit.

Fix:

$sarana->update($request->only(['desa_id', 'nama', 'jumlah', 'kategori', 'keterangan']));
// Atau:
$sarana->update($request->validated());

} catch (\Exception $e) {
report($e);
return back()->withInput()->with('error', 'Data Sarana gagal diperbarui');
}
return redirect()->route('data.data-sarana.index')->with('success', 'Data Sarana berhasil diperbarui');
}

public function destroy($id)
{
try {
$sarana = DataSarana::findOrFail($id);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 🐛 Bug: Missing Null Check Before Method Call

Kode: $sarana = DataSarana::find($id); $sarana->delete();

Skenario: Jika DataSarana::find($id) return null (record tidak ditemukan), memanggil ->delete() pada null akan throw fatal error "Call to a member function delete() on null". Ini bisa terjadi karena race condition (2 user delete record yang sama secara bersamaan) atau direct API call dengan ID invalid.

Dampak: Application crash dengan 500 error. User mendapat error page instead of graceful error message. Logs penuh dengan fatal error.

Fix:

$sarana = DataSarana::findOrFail($id);
$sarana->delete();
// Atau dengan manual check:
if (!$sarana) {
    return back()->with('error', 'Data tidak ditemukan');
}

$sarana->delete();
} catch (\Exception $e) {
report($e);
return back()->withInput()->with('error', 'Data Sarana gagal dihapus');
}
return redirect()->route('data.data-sarana.index')->with('success', 'Data Sarana berhasil dihapus');
}

public function export()
{
try {
$search = request('search');
$kategori = request('kategori');
$startDate = request('start_date');
$endDate = request('end_date');

$data = \App\Models\DataSarana::with('desa')
->when($search, fn($q) => $q->where('nama', 'like', "%$search%"))
->when($kategori, fn($q) => $q->where('kategori', $kategori))
->when($startDate, fn($q) => $q->whereDate('created_at', '>=', $startDate))
->when($endDate, fn($q) => $q->whereDate('created_at', '<=', $endDate))
->latest('id')
->get();

return Excel::download(new ExportDataSarana($data, 'Admin Desa'), 'data_sarana.xlsx');
} catch (\Exception $e) {
report($e);
return back()->withInput()->with('error', 'Data Sarana gagal dihapus');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 📝 Code Quality: Wrong Error Message

Kategori: PHP Quality
Masalah: Method export() menampilkan pesan error "gagal dihapus" yang tidak sesuai konteks - seharusnya "gagal diexport"
Kode: return back()->withInput()->with('error', 'Data Sarana gagal dihapus');

Fix:

return back()->with('error', 'Data Sarana gagal diexport');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 🐛 Bug: Wrong Error Message

Kode: return back()->withInput()->with('error', 'Data Sarana gagal dihapus');

Skenario: Ini ada di dalam method export(), bukan destroy(). Ketika export gagal, user melihat pesan error "Data Sarana gagal dihapus" yang sangat misleading. User akan bingung kenapa ada pesan delete padahal mereka sedang export.

Dampak: User confusion. Troubleshooting jadi sulit karena error message tidak match dengan action yang dilakukan. User mungkin report bug yang salah.

Fix:

return back()->with('error', 'Data Sarana gagal diexport');
// Juga hapus withInput() karena tidak perlu di export

}
}

public function import()
{
$page_title = "Import Sarana";
$page_description = "Upload data sarana";

$desas = DataDesa::all();

return view('data.data_sarana.import', compact('page_title', 'page_description', 'desas'));
}

public function importExcel(Request $request)
{
try {
$request->validate([
'file' => 'required|mimes:xlsx,xls,csv'
]);
Excel::import(new ImportDataSarana, $request->file('file'));
} catch (\Exception $e) {
report($e);
return back()->withInput()->with('error', 'Data Sarana gagal diimport');
}
return redirect()->route('data.data-sarana.index')->with('success', 'Data Sarana berhasil diimport');
}

}
8 changes: 7 additions & 1 deletion app/Http/Controllers/Data/DataUmumController.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
use App\Models\DataUmum;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Facades\DB;

class DataUmumController extends Controller
{
Expand All @@ -51,7 +52,12 @@ public function index()
$page_title = 'Data Umum';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] ⚡ Performance: Missing Cache untuk Data Statis

Masalah: Query agregasi sarana dipanggil setiap kali form edit dibuka, padahal data ini relatif statis dan bisa di-cache.

Kode: $pengaturan = PpidPengaturan::first(); dan 13 query SUM untuk setiap kategori sarana

Dampak: Setiap page load form edit = 14 queries (1 untuk pengaturan + 13 untuk rekap sarana). Dengan 1000 page views/hari = 14,000 queries yang sebenarnya bisa di-cache.

Fix:

// Cache rekap sarana per desa selama 1 jam
$rekapSarana = Cache::remember("sarana_rekap_{$desa_id}", 3600, function() use ($desa_id) {
    return [
        'puskesmas' => DataSarana::where('desa_id', $desa_id)
            ->where('kategori', 'puskesmas')->sum('jumlah'),
        'puskesmas_pembantu' => DataSarana::where('desa_id', $desa_id)
            ->where('kategori', 'puskesmas_pembantu')->sum('jumlah'),
        // ... dst untuk 13 kategori
    ];
});

// Invalidate cache saat ada perubahan di DataSaranaController:
// Cache::forget("sarana_rekap_{$request->desa_id}");

$page_description = 'Ubah Data Umum';

return view('data.data_umum.edit', compact('page_title', 'page_description', 'data_umum', 'luas_wilayah'));
$rekapKategori = DB::table('das_data_sarana')
->select('kategori', DB::raw('SUM(jumlah) as total'))
->groupBy('kategori')
->pluck('total', 'kategori');

return view('data.data_umum.edit', compact('page_title', 'page_description', 'data_umum', 'luas_wilayah', 'rekapKategori'));
}

/**
Expand Down
31 changes: 31 additions & 0 deletions app/Imports/ImportDataSarana.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace App\Imports;

use App\Models\DataDesa;
use App\Models\DataSarana;
use Maatwebsite\Excel\Concerns\ToModel;
use Maatwebsite\Excel\Concerns\WithHeadingRow;

class ImportDataSarana implements ToModel, WithHeadingRow
{
/**
* @param array $row
*
* @return \Illuminate\Database\Eloquent\Model|null
*/
public function model(array $row)
{
if (!DataDesa::where('id', $row['desa_id'])->exists()) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] ⚡ Performance: Query Validasi di Loop Import

Masalah: Setiap row import melakukan query DataDesa::where('id', $row['desa_id'])->exists() secara individual, menyebabkan N queries untuk N rows.

Kode: if (!DataDesa::where('id', $row['desa_id'])->exists()) { return null; }

Dampak: Import 1000 rows = 1000 queries validasi. Proses import yang seharusnya 5 detik bisa jadi 30+ detik karena database round-trip overhead.

Fix:

// Preload semua valid desa_id sekali di constructor
private $validDesaIds;

public function __construct()
{
    $this->validDesaIds = DataDesa::pluck('id')->flip();
}

public function model(array $row)
{
    // Validasi menggunakan in-memory array
    if (!isset($this->validDesaIds[$row['desa_id']])) {
        \Log::warning("Import skipped: desa_id {$row['desa_id']} not found");
        return null;
    }
    
    return new DataSarana([
        'desa_id' => $row['desa_id'],
        'nama' => $row['nama'],
        'jumlah' => $row['jumlah'],
        'kategori' => $row['kategori'],
        'keterangan' => $row['keterangan'] ?? null,
    ]);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 🐛 Bug: Silent Failure Without Logging

Kode: if (!DataDesa::where('id', $row['desa_id'])->exists()) { return null; }

Skenario: Ketika import Excel dengan 100 rows, jika 20 rows memiliki desa_id yang tidak valid, mereka akan di-skip secara silent (return null). User hanya melihat "Import berhasil" tapi tidak tahu bahwa 20 rows gagal. Tidak ada cara untuk track row mana yang gagal atau kenapa.

Dampak: Data loss tanpa notifikasi. User mengira semua data ter-import tapi sebenarnya ada yang hilang. Debugging sangat sulit karena tidak ada log. Production data bisa incomplete tanpa disadari.

Fix:

if (!DataDesa::where('id', $row['desa_id'])->exists()) {
    \Log::warning('Import skipped: desa_id not found', [
        'desa_id' => $row['desa_id'],
        'row' => $row
    ]);
    return null;
}

return null;
}

return new DataSarana([
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[HIGH] 🐛 Bug: Missing Array Key Validation

Kode: return new DataSarana([ 'desa_id' => $row['desa_id'], 'nama' => $row['nama'], ... ]);

Skenario: Jika user upload Excel file dengan kolom yang hilang (misalnya tidak ada kolom 'kategori'), akses $row['kategori'] akan throw "Undefined array key 'kategori'" error di PHP 8+. Import akan crash di tengah jalan.

Dampak: Import gagal total dengan 500 error. User tidak mendapat feedback yang jelas tentang apa yang salah dengan file Excel mereka. Harus trial-error untuk menemukan kolom mana yang hilang.

Fix:

// Tambahkan validasi di awal method model()
if (!isset($row['desa_id'], $row['nama'], $row['jumlah'], $row['kategori'])) {
    \Log::warning('Import skipped: missing required columns', ['row' => $row]);
    return null;
}

return new DataSarana([
    'desa_id' => $row['desa_id'],
    'nama' => $row['nama'],
    'jumlah' => $row['jumlah'],
    'kategori' => $row['kategori'],
    'keterangan' => $row['keterangan'] ?? null,
]);

'desa_id' => $row['desa_id'],
'nama' => $row['nama'],
'jumlah' => $row['jumlah'],
'kategori' => $row['kategori'],
'keterangan' => $row['keterangan'],
]);
}
}
5 changes: 5 additions & 0 deletions app/Models/DataDesa.php
Original file line number Diff line number Diff line change
Expand Up @@ -181,4 +181,9 @@ public function pembangunan()
{
return $this->hasMany(Pembangunan::class, 'desa_id', 'desa_id');
}

public function saranas()
{
return $this->hasMany(DataSarana::class, 'desa_id', 'id');
}
}
20 changes: 20 additions & 0 deletions app/Models/DataSarana.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;


class DataSarana extends Model
{
use HasFactory;

protected $table = 'das_data_sarana';
protected $fillable = ['desa_id','kategori','nama','jumlah','keterangan'];

public function desa()
{
return $this->belongsTo(DataDesa::class, 'desa_id', 'id');
}
}
27 changes: 27 additions & 0 deletions database/factories/DataSaranaFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace Database\Factories;

use App\Models\DataSarana;
use App\Models\DataDesa;
use Illuminate\Database\Eloquent\Factories\Factory;

class DataSaranaFactory extends Factory
{
protected $model = DataSarana::class;

public function definition()
{
return [
'desa_id' => DataDesa::factory(), // otomatis buat desa baru
'nama' => $this->faker->word,
'jumlah' => $this->faker->numberBetween(1, 100),
'kategori' => $this->faker->randomElement([
'puskesmas','puskesmas_pembantu','posyandu','pondok_bersalin',
'paud','sd','smp','sma',
'masjid_besar','mushola','gereja','pasar','balai_pertemuan'
]),
'keterangan' => $this->faker->sentence,
];
}
}
Loading