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
4 changes: 2 additions & 2 deletions app/Http/Controllers/Api/TrackController.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ public function __invoke(TrackRequest $request)
$request->merge(['desa_id' => $desa->id])->only(['desa_id', 'url_referrer', 'request_uri', 'client_ip', 'external_ip', 'opensid_version', 'tgl'])
);
}
$notifikasi = Notifikasi::semuaNotifDesa($desa->id);
NotifikasiDesa::nonAktifkan(collect($notifikasi), $desa->id);
$notifikasi = Notifikasi::semuaNotifDesa($desa->id)->get();
NotifikasiDesa::nonAktifkan($notifikasi, $desa->id);

DB::commit();

Expand Down
21 changes: 20 additions & 1 deletion app/Models/Notifikasi.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,16 @@ class Notifikasi extends Model
/** {@inheritdoc} */
protected $table = 'notifikasi';

/**
* Relasi ke tabel notifikasi_desa (pivot).
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function notifikasiDesa()
{
return $this->hasMany(NotifikasiDesa::class, 'id_notifikasi');
}

/**
* Scope semua notif dari desa.
*
Expand All @@ -22,6 +32,15 @@ class Notifikasi extends Model
*/
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[CRITICAL] ⚡ Performance: whereHas + orWhereDoesntHave Subquery Tanpa Index

Masalah: Query menggunakan whereHas dan orWhereDoesntHave yang menghasilkan 2 subquery EXISTS/NOT EXISTS. Pada tabel notifikasi_desa dengan ribuan rows, ini akan sangat lambat tanpa composite index pada (id_desa, id_notifikasi, status).

Kode:

$q->whereHas('notifikasiDesa', function ($q) use ($desaId) {
    $q->where('id_desa', $desaId)
        ->where('status', '!=', 0);
})
->orWhereDoesntHave('notifikasiDesa', function ($q) use ($desaId) {
    $q->where('id_desa', $desaId);
});

Dampak: Pada production dengan 10,000+ notifikasi dan 50,000+ notifikasi_desa records:

  • Query time: 500ms - 2 detik per request
  • Database CPU spike saat traffic tinggi
  • Potential timeout pada shared hosting

Fix:

// 1. Tambahkan composite index di migration:
Schema::table('notifikasi_desa', function (Blueprint $table) {
    $table->index(['id_desa', 'id_notifikasi', 'status'], 'idx_desa_notif_status');
});

// 2. Alternatif: Gunakan JOIN untuk performa lebih baik
public function scopeSemuaNotifDesa($query, $desaId)
{
    return $query->where('notifikasi.aktif', 1)
        ->leftJoin('notifikasi_desa as nd', function($join) use ($desaId) {
            $join->on('nd.id_notifikasi', '=', 'notifikasi.id')
                 ->where('nd.id_desa', '=', $desaId);
        })
        ->where(function($q) {
            $q->whereNull('nd.id')  // tidak ada entry untuk desa ini
              ->orWhere('nd.status', '!=', 0);  // atau ada entry dengan status aktif
        })
        ->select('notifikasi.*')
        ->distinct();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[HIGH] ⚡ Performance: Missing Eager Loading untuk Relasi

Masalah: Scope ini tidak melakukan eager loading untuk relasi notifikasiDesa. Jika di controller atau view ada akses ke $notifikasi->notifikasiDesa, akan terjadi N+1 query problem.

Kode:

return $query->where('aktif', 1)
    ->where(function ($q) use ($desaId) {
        // ... whereHas logic
    });
// Missing: ->with('notifikasiDesa')

Dampak: Jika controller memanggil Notifikasi::semuaNotifDesa($desaId)->get() dan menghasilkan 50 notifikasi, lalu di view ada loop yang akses relasi:

  • 1 query utama + 50 query relasi = 51 total queries
  • Response time bertambah 200-500ms
  • Database connection pool exhaustion pada concurrent requests

Fix:

public function scopeSemuaNotifDesa($query, $desaId)
{
    return $query->where('aktif', 1)
        ->where(function ($q) use ($desaId) {
            $q->whereHas('notifikasiDesa', function ($q) use ($desaId) {
                $q->where('id_desa', $desaId)
                    ->where('status', '!=', 0);
            })
            ->orWhereDoesntHave('notifikasiDesa', function ($q) use ($desaId) {
                $q->where('id_desa', $desaId);
            });
        })
        ->with(['notifikasiDesa' => function($q) use ($desaId) {
            $q->where('id_desa', $desaId);
        }]);
}

// Atau di controller:
Notifikasi::semuaNotifDesa($desaId)->with('notifikasiDesa')->get();

public function scopeSemuaNotifDesa($query, $desaId)
{
return DB::select("select n.* from notifikasi as n where n.aktif = 1 and ((select nd.id from notifikasi_desa as nd where nd.id_notifikasi = n.id and nd.id_desa = '{$desaId}' and nd.status <> 0) is not null or (select nd.id from notifikasi_desa as nd where nd.id_notifikasi = n.id and nd.id_desa = '{$desaId}') is null)");
return $query->where('aktif', 1)
->where(function ($q) use ($desaId) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[HIGH] 📝 Code Quality: Missing Type Hints

Kategori: PHP Quality
Masalah: Method scopeSemuaNotifDesa tidak memiliki type hints untuk parameter dan return type. Ini mengurangi type safety dan IDE autocomplete support, terutama penting karena ini adalah refactoring dari raw SQL ke Eloquent.

Kode:

public function scopeSemuaNotifDesa($query, $desaId)

Fix:

public function scopeSemuaNotifDesa(\Illuminate\Database\Eloquent\Builder $query, int|string $desaId): \Illuminate\Database\Eloquent\Builder

Penjelasan:

  • Parameter $query harus di-type hint sebagai Builder
  • Parameter $desaId harus di-type hint (int atau string tergantung tipe kolom id_desa)
  • Return type harus Builder untuk mendukung method chaining

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[HIGH] 📝 Code Quality: Missing Tests

Kategori: Testing
Masalah: Perubahan dari raw SQL ke Eloquent query builder adalah logic change yang critical dan digunakan di production controller (NotifikasiController::index()), namun tidak ada test coverage untuk memverifikasi behavior tetap identik.

Kode:

public function scopeSemuaNotifDesa($query, $desaId)
{
    return $query->where('aktif', 1)
        ->where(function ($q) use ($desaId) {
            $q->whereHas('notifikasiDesa', function ($q) use ($desaId) {
                $q->where('id_desa', $desaId)
                    ->where('status', '!=', 0);
            })
            ->orWhereDoesntHave('notifikasiDesa', function ($q) use ($desaId) {
                $q->where('id_desa', $desaId);
            });
        });
}

Fix:
Buat test file tests/Unit/Models/NotifikasiTest.php:

<?php

namespace Tests\Unit\Models;

use App\Models\Notifikasi;
use App\Models\NotifikasiDesa;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class NotifikasiTest extends TestCase
{
    use RefreshDatabase;

    /** @test */
    public function scope_semua_notif_desa_returns_active_notifications_with_desa_relation()
    {
        $desaId = 1;
        
        // Notifikasi aktif dengan relasi desa (status != 0)
        $notif1 = Notifikasi::factory()->create(['aktif' => 1]);
        NotifikasiDesa::factory()->create([
            'id_notifikasi' => $notif1->id,
            'id_desa' => $desaId,
            'status' => 1
        ]);
        
        // Notifikasi aktif tanpa relasi desa (global)
        $notif2 = Notifikasi::factory()->create(['aktif' => 1]);
        
        // Notifikasi aktif dengan relasi desa tapi status = 0 (tidak muncul)
        $notif3 = Notifikasi::factory()->create(['aktif' => 1]);
        NotifikasiDesa::factory()->create([
            'id_notifikasi' => $notif3->id,
            'id_desa' => $desaId,
            'status' => 0
        ]);
        
        // Notifikasi tidak aktif (tidak muncul)
        $notif4 = Notifikasi::factory()->create(['aktif' => 0]);
        
        $result = Notifikasi::semuaNotifDesa($desaId)->get();
        
        $this->assertCount(2, $result);
        $this->assertTrue($result->contains($notif1));
        $this->assertTrue($result->contains($notif2));
        $this->assertFalse($result->contains($notif3));
        $this->assertFalse($result->contains($notif4));
    }
}

Penjelasan: Test ini memverifikasi bahwa logic baru menghasilkan hasil yang sama dengan query SQL lama untuk berbagai skenario.

$q->whereHas('notifikasiDesa', function ($q) use ($desaId) {
$q->where('id_desa', $desaId)
->where('status', '!=', 0);
})
->orWhereDoesntHave('notifikasiDesa', function ($q) use ($desaId) {
$q->where('id_desa', $desaId);
});
});
}
}
90 changes: 46 additions & 44 deletions resources/views/laporan/desa.blade.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
<th>Tema</th>
<th>Modul TTE</th>
<th>Surat ter-TTE</th>
@auth
{{-- @auth
<th>Penduduk</th>
<th>Artikel</th>
<th>Surat Keluar</th>
Expand All @@ -71,7 +71,7 @@
<th>Persil</th>
<th>Dokumen</th>
<th>Keluarga</th>
@endauth
@endauth --}}
<th>Akses Terakhir</th>
</tr>
</thead>
Expand Down Expand Up @@ -223,51 +223,53 @@
data: 'jml_surat_tte',
searchable: false,
},
@auth {
data: 'jml_penduduk',
searchable: false,
},
{
data: 'jml_artikel',
searchable: false,
},
{
data: 'jml_surat_keluar',
searchable: false,
},
{
data: 'jml_bantuan',
searchable: false,
},
{
data: 'jml_mandiri',
searchable: false,
},
{
data: 'jml_pengguna',
searchable: false,
},
{
data: 'jml_unsur_peta',
searchable: false,
},
{
data: 'jml_persil',
searchable: false,
},
{
data: 'jml_dokumen',
searchable: false,
},
{
data: 'jml_keluarga',
searchable: false,
},
@endauth {
// @auth
// {
// data: 'jml_penduduk',
// searchable: false,
// },
// {
// data: 'jml_artikel',
// searchable: false,
// },
// {
// data: 'jml_surat_keluar',
// searchable: false,
// },
// {
// data: 'jml_bantuan',
// searchable: false,
// },
// {
// data: 'jml_mandiri',
// searchable: false,
// },
// {
// data: 'jml_pengguna',
// searchable: false,
// },
// {
// data: 'jml_unsur_peta',
// searchable: false,
// },
// {
// data: 'jml_persil',
// searchable: false,
// },
// {
// data: 'jml_dokumen',
// searchable: false,
// },
// {
// data: 'jml_keluarga',
// searchable: false,
// },
// @endauth
{
data: 'tgl_akses',
searchable: false,
}, ],
@auth
@auth
order: [
[22 - {{ count($hiddenColumns) }}, 'desc']
],
Expand Down
Loading