diff --git a/app/Events/ProsedurChanged.php b/app/Events/ProsedurChanged.php new file mode 100644 index 0000000000..3d17be4387 --- /dev/null +++ b/app/Events/ProsedurChanged.php @@ -0,0 +1,55 @@ +prosedur = $prosedur; + } +} diff --git a/app/Exports/ExportAKIAKB.php b/app/Exports/ExportAKIAKB.php index 92c9e66cc7..6c9e3d63d3 100644 --- a/app/Exports/ExportAKIAKB.php +++ b/app/Exports/ExportAKIAKB.php @@ -109,7 +109,7 @@ public function map($akiAkb): array return [ $akiAkb->id, - $akiAkb->desa->nama ?? 'Tidak Diketahui', + nama_desa($akiAkb->desa_id) ?? 'Tidak Diketahui', $akiAkb->desa_id, $akiAkb->aki, $akiAkb->akb, diff --git a/app/Exports/ExportEpidemiPenyakit.php b/app/Exports/ExportEpidemiPenyakit.php index ed09eeb4d5..567decd026 100644 --- a/app/Exports/ExportEpidemiPenyakit.php +++ b/app/Exports/ExportEpidemiPenyakit.php @@ -80,7 +80,7 @@ public function map($epidemiPenyakit): array { return [ $epidemiPenyakit->id, - $epidemiPenyakit->desa->nama ?? '', + nama_desa($epidemiPenyakit->desa_id) ?? '', $epidemiPenyakit->penyakit->nama ?? '', $epidemiPenyakit->jumlah_penderita, months_list()[$epidemiPenyakit->bulan] ?? $epidemiPenyakit->bulan, diff --git a/app/Exports/ExportFasilitasPaud.php b/app/Exports/ExportFasilitasPaud.php index 859e9e4abb..198e0a4310 100644 --- a/app/Exports/ExportFasilitasPaud.php +++ b/app/Exports/ExportFasilitasPaud.php @@ -81,7 +81,7 @@ public function map($fasilitasPaud): array { return [ $fasilitasPaud->id, - $fasilitasPaud->desa->nama ?? '', + nama_desa($fasilitasPaud->desa_id) ?? '', $fasilitasPaud->jumlah_paud, $fasilitasPaud->jumlah_guru_paud, $fasilitasPaud->jumlah_siswa_paud, diff --git a/app/Exports/ExportImunisasi.php b/app/Exports/ExportImunisasi.php index 7417df3fc1..60437ce14e 100644 --- a/app/Exports/ExportImunisasi.php +++ b/app/Exports/ExportImunisasi.php @@ -82,7 +82,7 @@ public function map($imunisasi): array return [ $imunisasi->id, - $imunisasi->desa->nama ?? 'Tidak Diketahui', + nama_desa($imunisasi->desa_id) ?? 'Tidak Diketahui', $imunisasi->desa_id, $imunisasi->cakupan_imunisasi, // Format dengan persen $bulanList[$imunisasi->bulan] ?? 'Tidak Diketahui', diff --git a/app/Exports/ExportPutusSekolah.php b/app/Exports/ExportPutusSekolah.php index df09a7115a..a83d7fcf91 100644 --- a/app/Exports/ExportPutusSekolah.php +++ b/app/Exports/ExportPutusSekolah.php @@ -86,7 +86,7 @@ public function map($putusSekolah): array { return [ $putusSekolah->id, - $putusSekolah->desa->nama ?? '', + nama_desa($putusSekolah->desa_id) ?? '', $putusSekolah->siswa_paud, $putusSekolah->anak_usia_paud, $putusSekolah->siswa_sd, diff --git a/app/Exports/ExportSuplemenTerdata.php b/app/Exports/ExportSuplemenTerdata.php index d32b69a0ee..09e88c43f4 100644 --- a/app/Exports/ExportSuplemenTerdata.php +++ b/app/Exports/ExportSuplemenTerdata.php @@ -41,6 +41,7 @@ class ExportSuplemenTerdata implements FromCollection, WithHeadings, WithMapping, WithStyles { protected $suplemenId; + protected $filters; public function __construct($suplemenId = null, array $filters = []) @@ -77,9 +78,6 @@ public function collection() return $query->get(); } - /** - * @return array - */ public function headings(): array { return [ @@ -99,10 +97,6 @@ public function headings(): array ]; } - /** - * @param mixed $suplemenTerdata - * @return array - */ public function map($suplemenTerdata): array { $penduduk = $suplemenTerdata->penduduk; @@ -126,7 +120,6 @@ public function map($suplemenTerdata): array } /** - * @param Worksheet $sheet * @return array */ public function styles(Worksheet $sheet) diff --git a/app/Exports/ExportSuplemenTerdataGabungan.php b/app/Exports/ExportSuplemenTerdataGabungan.php new file mode 100644 index 0000000000..a450bc6791 --- /dev/null +++ b/app/Exports/ExportSuplemenTerdataGabungan.php @@ -0,0 +1,128 @@ + $pendudukGabunganIds + */ + public function __construct($suplemenId = null, array $filters = [], array $pendudukGabunganIds = []) + { + parent::__construct($suplemenId, $filters); + $this->pendudukGabunganIds = array_map('intval', $pendudukGabunganIds); + } + + public function collection(): Collection + { + $collection = parent::collection(); + + if ($collection->isEmpty() || empty($this->pendudukGabunganIds)) { + return $collection; + } + + $missingIds = $this->pendudukGabunganIds; + + foreach ($collection as $row) { + if ($row->penduduk_id_gabungan) { + $key = (int) $row->penduduk_id_gabungan; + + if (isset($missingIds[$key])) { + unset($missingIds[$key]); + } + } + } + + if (empty($missingIds)) { + return $collection; + } + + $resolved = (new PendudukService())->pendudukGabunganByIds(array_values($missingIds)); + + if ($resolved->isEmpty()) { + return $collection; + } + + $resolvedMap = []; + foreach ($resolved as $item) { + if (! is_array($item)) { + continue; + } + + $penduduk = $this->mapGabunganPenduduk($item); + $resolvedMap[(int) ($item['id'] ?? 0)] = $penduduk; + } + + return $collection->map(function (SuplemenTerdata $row) use ($resolvedMap) { + if ($row->penduduk_id_gabungan && ! $row->penduduk) { + $penduduk = $resolvedMap[(int) $row->penduduk_id_gabungan] ?? null; + + if ($penduduk) { + $row->setRelation('penduduk', $penduduk); + } + } + + return $row; + }); + } + + private function mapGabunganPenduduk(array $item): Penduduk + { + $attributes = $item['attributes'] ?? []; + + $penduduk = new Penduduk(); + $penduduk->forceFill([ + 'id' => $item['id'] ?? null, + 'no_kk' => data_get($attributes, 'keluarga.no_kk') ?? $attributes['no_kk'] ?? null, + 'nik' => $attributes['nik'] ?? null, + 'nama' => $attributes['nama'] ?? null, + 'tempat_lahir' => $attributes['tempatlahir'] ?? $attributes['tempat_lahir'] ?? null, + 'tanggal_lahir' => $attributes['tanggallahir'] ?? $attributes['tanggal_lahir'] ?? null, + 'umur' => $attributes['umur'] ?? null, + 'sex' => $attributes['sex'] ?? null, + 'alamat' => $attributes['alamat_sekarang'] ?? $attributes['alamat'] ?? null, + ]); + + $penduduk->setRelation('desa', new DataDesa([ + 'nama' => data_get($attributes, 'config.nama_desa') ?? $attributes['nama_desa'] ?? null, + 'desa_id' => data_get($attributes, 'config.kode_desa') ?? $attributes['kode_desa'] ?? null, + ])); + + return $penduduk; + } +} diff --git a/app/Exports/ExportTingkatPendidikan.php b/app/Exports/ExportTingkatPendidikan.php index bd264584b4..aa938de29c 100644 --- a/app/Exports/ExportTingkatPendidikan.php +++ b/app/Exports/ExportTingkatPendidikan.php @@ -83,7 +83,7 @@ public function map($tingkatPendidikan): array { return [ $tingkatPendidikan->id, - $tingkatPendidikan->desa->nama ?? '', + nama_desa($tingkatPendidikan->desa_id) ?? '', $tingkatPendidikan->tidak_tamat_sekolah, $tingkatPendidikan->tamat_sd, $tingkatPendidikan->tamat_smp, diff --git a/app/Exports/ExportToiletSanitasi.php b/app/Exports/ExportToiletSanitasi.php index 1122e105b4..081e26bdd5 100644 --- a/app/Exports/ExportToiletSanitasi.php +++ b/app/Exports/ExportToiletSanitasi.php @@ -83,7 +83,7 @@ public function map($toiletSanitasi): array return [ $toiletSanitasi->id, - $toiletSanitasi->desa->nama ?? 'Tidak Diketahui', + nama_desa($toiletSanitasi->desa_id) ?? 'Tidak Diketahui', $toiletSanitasi->desa_id, $toiletSanitasi->toilet, $toiletSanitasi->sanitasi, diff --git a/app/Http/Controllers/Data/AKIAKBController.php b/app/Http/Controllers/Data/AKIAKBController.php index 2553c1f004..b7ceecea61 100644 --- a/app/Http/Controllers/Data/AKIAKBController.php +++ b/app/Http/Controllers/Data/AKIAKBController.php @@ -34,6 +34,7 @@ use App\Exports\ExportAKIAKB; use App\Http\Controllers\Controller; use App\Http\Requests\ImportAKIAKBRequest; +use App\Http\Requests\UpdateAKIAKBRequest; use App\Imports\ImporAKIAKB; use App\Models\AkiAkb; use App\Services\DesaService; @@ -65,8 +66,12 @@ public function index() */ public function getDataAKIAKB() { + $desa = request()->input('desa'); $listDesa = (new DesaService)->listDesa()->pluck('nama', 'desa_id'); - return DataTables::of(AkiAkb::with(['desa'])->get()) + return DataTables::of(AkiAkb::when($desa && $desa !== 'Semua', function ($query) use ($desa) { + return $query->where('desa_id', $desa); + }) + ->with(['desa'])->get()) ->addColumn('aksi', function ($row) { $data['edit_url'] = auth()->user()->can('access.data.aki_akb.edit') ? route('data.aki-akb.edit', $row->id) : null; $data['delete_url'] = auth()->user()->can('access.data.aki_akb.delete') ? route('data.aki-akb.destroy', $row->id) : null; diff --git a/app/Http/Controllers/Data/FasilitasPaudController.php b/app/Http/Controllers/Data/FasilitasPaudController.php index 03e6122b4d..baeaa52bcb 100644 --- a/app/Http/Controllers/Data/FasilitasPaudController.php +++ b/app/Http/Controllers/Data/FasilitasPaudController.php @@ -34,8 +34,10 @@ use App\Exports\ExportFasilitasPaud; use App\Http\Controllers\Controller; use App\Http\Requests\ImportFasilitasPaudRequest; +use App\Http\Requests\UpdateFasilitasPaudRequest; use App\Imports\ImporFasilitasPaud; use App\Models\FasilitasPAUD; +use App\Services\DesaService; use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Facades\Log; @@ -59,12 +61,18 @@ public function index() */ public function getDataFasilitasPAUD() { - return DataTables::of(FasilitasPAUD::with(['desa'])->get()) + $desa = request()->input('desa'); + $listDesa = (new DesaService)->listDesa()->pluck('nama', 'desa_id'); + return DataTables::of(FasilitasPAUD::when($desa && $desa !== 'Semua', function ($query) use ($desa) { + return $query->where('desa_id', $desa); + })->with(['desa'])->get()) ->addColumn('aksi', function ($row) { $data['edit_url'] = auth()->user()->can('access.data.fasilitas_paud.edit') ? route('data.fasilitas-paud.edit', $row->id) : null; $data['delete_url'] = auth()->user()->can('access.data.fasilitas_paud.delete') ? route('data.fasilitas-paud.destroy', $row->id) : null; return view('forms.aksi', $data); + })->addColumn('nama_desa', function ($row) use ($listDesa) { + return $row->desa->nama ?? $listDesa[$row->desa_id] ?? '-'; }) ->rawColumns(['aksi'])->make(); } @@ -116,7 +124,7 @@ public function edit($id) { $fasilitas = FasilitasPAUD::with(['desa'])->findOrFail($id); $page_title = 'Fasilitas PAUD'; - $page_description = 'Ubah Fasilitas PAUD : Desa ' . $fasilitas->desa->nama; + $page_description = 'Ubah Fasilitas PAUD : Desa ' . nama_desa($fasilitas->desa_id); return view('data.fasilitas_paud.edit', compact('page_title', 'page_description', 'fasilitas')); } @@ -127,18 +135,10 @@ public function edit($id) * @param int $id * @return Response */ - public function update(Request $request, $id) + public function update(UpdateFasilitasPaudRequest $request, $id) { - request()->validate([ - 'jumlaah_paud' => 'required', - 'jumlah_guru_paud' => 'required', - 'jumlah_siswa_paud' => 'required', - 'semester' => 'required', - 'tahun' => 'required', - ]); - try { - FasilitasPAUD::findOrFail($id)->update($request->all()); + FasilitasPAUD::findOrFail($id)->update($request->validated()); } catch (\Exception $e) { Log::error('Fasilitas PAUD update failed', [ 'error' => $e->getMessage(), diff --git a/app/Http/Controllers/Data/ImunisasiController.php b/app/Http/Controllers/Data/ImunisasiController.php index ac5a550cca..fef53f2ec0 100644 --- a/app/Http/Controllers/Data/ImunisasiController.php +++ b/app/Http/Controllers/Data/ImunisasiController.php @@ -65,8 +65,12 @@ public function index() */ public function getDataAKIAKB() { + $desa = request()->input('desa'); $listDesa = (new DesaService)->listDesa()->pluck('nama', 'desa_id'); - return DataTables::of(Imunisasi::with(['desa'])->get()) + return DataTables::of(Imunisasi::when($desa && $desa !== 'Semua', function ($query) use ($desa) { + return $query->where('desa_id', $desa); + }) + ->with(['desa'])->get()) ->addColumn('aksi', function ($row) { $data['edit_url'] = auth()->user()->can('access.data.imunisasi.edit') ? route('data.imunisasi.edit', $row->id) : null; $data['delete_url'] = auth()->user()->can('access.data.imunisasi.delete') ? route('data.imunisasi.destroy', $row->id) : null; diff --git a/app/Http/Controllers/Data/LembagaAnggotaController.php b/app/Http/Controllers/Data/LembagaAnggotaController.php index 386de9a6d5..068618de55 100644 --- a/app/Http/Controllers/Data/LembagaAnggotaController.php +++ b/app/Http/Controllers/Data/LembagaAnggotaController.php @@ -2,11 +2,13 @@ namespace App\Http\Controllers\Data; +use App\Models\SettingAplikasi; use Carbon\Carbon; use App\Models\Lembaga; use App\Models\Penduduk; use Illuminate\Http\Request; use App\Models\LembagaAnggota; +use App\Services\PendudukService; use Yajra\DataTables\DataTables; use App\Http\Controllers\Controller; use App\Http\Requests\StoreLembagaAnggotaRequest; @@ -38,17 +40,23 @@ public function getData(Request $request, $slug) if ($request->ajax()) { $lembaga = Lembaga::where('slug', $slug)->firstOrFail(); - // Ambil data anggota lembaga dengan informasi lembaga dan penduduk terkait $anggotaList = LembagaAnggota::with(['lembaga', 'penduduk']) ->where('lembaga_id', $lembaga->id) ->get(); + $pendudukGabunganIds = $anggotaList->pluck('penduduk_id_gabungan')->filter()->unique()->values()->all(); + + $pendudukGabungan = collect(); + if ($this->isDatabaseGabungan() && !empty($pendudukGabunganIds)) { + $pendudukGabungan = (new PendudukService())->pendudukGabunganByIds($pendudukGabunganIds); + } + return DataTables::of($anggotaList) ->addIndexColumn() ->addColumn('aksi', function ($row) use ($slug) { if (!auth()->guest()) { - $data['edit_url'] = auth()->user()->can('access.data.lembaga_anggota.edit') ? route('data.lembaga_anggota.edit', ['slug' => $slug, 'id' => $row->id]) : null; - $data['delete_url'] = auth()->user()->can('access.data.lembaga_anggota.delete') ? route('data.lembaga_anggota.destroy', ['slug' => $slug, 'id' => $row->id]) : null; + $data['edit_url'] = auth()->user()->can('access.data.lembaga.edit') ? route('data.lembaga_anggota.edit', ['slug' => $slug, 'id' => $row->id]) : null; + $data['delete_url'] = auth()->user()->can('access.data.lembaga.delete') ? route('data.lembaga_anggota.destroy', ['slug' => $slug, 'id' => $row->id]) : null; } return view('forms.aksi', $data); @@ -56,31 +64,80 @@ public function getData(Request $request, $slug) ->addColumn('no_anggota', function ($row) { return $row->no_anggota ?: '-'; }) - ->addColumn('nik', function ($row) { + ->addColumn('nik', function ($row) use ($pendudukGabungan) { + if ($this->isDatabaseGabungan()) { + $penduduk = $pendudukGabungan->firstWhere('id', $row->penduduk_id_gabungan); + + return $penduduk ? ($penduduk['attributes']['nik'] ?? '-') : '-'; + } + return $row->penduduk->nik ?? '-'; }) - ->addColumn('nama', function ($row) { + ->addColumn('nama', function ($row) use ($pendudukGabungan) { + if ($this->isDatabaseGabungan()) { + $penduduk = $pendudukGabungan->firstWhere('id', $row->penduduk_id_gabungan); + + return $penduduk ? ($penduduk['attributes']['nama'] ?? '-') : '-'; + } + return $row->penduduk->nama ?? '-'; }) - ->addColumn('tempat_tgl_lahir', function ($row) { + ->addColumn('tempat_tgl_lahir', function ($row) use ($pendudukGabungan) { + if ($this->isDatabaseGabungan()) { + $penduduk = $pendudukGabungan->firstWhere('id', $row->penduduk_id_gabungan); + + $tempat = $penduduk ? ($penduduk['attributes']['tempatlahir'] ?? '-') : '-'; + $tanggalLahir = $penduduk && ($penduduk['attributes']['tanggallahir'] ?? false) + ? Carbon::parse($penduduk['attributes']['tanggallahir'])->translatedFormat('d F Y') + : '-'; + + return "$tempat / $tanggalLahir"; + } + $tempat = $row->penduduk->tempat_lahir ?? '-'; $tanggalLahir = $row->penduduk->tanggal_lahir ? Carbon::parse($row->penduduk->tanggal_lahir)->translatedFormat('d F Y') : '-'; + return "$tempat / $tanggalLahir"; }) - ->addColumn('umur', function ($row) { + ->addColumn('umur', function ($row) use ($pendudukGabungan) { + if ($this->isDatabaseGabungan()) { + $penduduk = $pendudukGabungan->firstWhere('id', $row->penduduk_id_gabungan); + + return $penduduk['attributes']['umur'] ?? '-'; + } + return $row->penduduk && $row->penduduk->tanggal_lahir - ? \Carbon\Carbon::parse($row->penduduk->tanggal_lahir)->age + ? Carbon::parse($row->penduduk->tanggal_lahir)->age : '-'; }) - ->addColumn('sex', function ($row) { + ->addColumn('sex', function ($row) use ($pendudukGabungan) { + if ($this->isDatabaseGabungan()) { + $penduduk = $pendudukGabungan->firstWhere('id', $row->penduduk_id_gabungan); + $sex = ['1' => 'Laki-laki', '2' => 'Perempuan']; + return $penduduk ? ($sex[$penduduk['attributes']['sex']] ?? '-') : '-'; + } + return $row->penduduk && $row->penduduk->pendudukSex ? $row->penduduk->pendudukSex->nama : '-'; }) - ->addColumn('alamat', function ($row) { + ->addColumn('alamat', function ($row) use ($pendudukGabungan) { + if ($this->isDatabaseGabungan()) { + $penduduk = $pendudukGabungan->firstWhere('id', $row->penduduk_id_gabungan); + + if (!$penduduk) { + return '-'; + } + + $alamat = $penduduk['attributes']['alamat_wilayah'] ?? '-'; + + return $alamat; + } + $rt = $row->penduduk->rt ?? '-'; $rw = $row->penduduk->rw ?? '-'; $alamat = $row->penduduk->dusun ?? '-'; + return "RT $rt / RW $rw $alamat"; }) ->addColumn('jabatan', function ($row) { @@ -136,15 +193,18 @@ public function getData(Request $request, $slug) */ public function create($slug) { - // Cari lembaga berdasarkan slug $lembaga = Lembaga::where('slug', $slug)->firstOrFail(); - // Ambil anggota yang sudah ada di lembaga ini + if ($this->isDatabaseGabungan()) { + $page_title = 'Tambah Anggota Lembaga ' . $lembaga->nama; + $page_description = 'Tambah Anggota Lembaga ' . $lembaga->nama; + + return view('data.lembaga_anggota.gabungan.create', compact('page_title', 'page_description', 'lembaga')); + } + $existingAnggotaIds = $lembaga->lembagaAnggota->pluck('penduduk_id')->toArray(); - // Ambil semua penduduk yang belum menjadi anggota $pendudukList = Penduduk::whereNotIn('id', $existingAnggotaIds)->get()->mapWithKeys(function ($penduduk) { - // Membuat string yang menggabungkan NIK, nama, dan alamat $optionText = "NIK: {$penduduk->nik} - {$penduduk->nama} - Dusun {$penduduk->dusun} RT {$penduduk->rt} / RW {$penduduk->rw}"; return [$penduduk->id => $optionText]; @@ -165,24 +225,25 @@ public function create($slug) */ public function store(StoreLembagaAnggotaRequest $request, $slug) { - // Cari lembaga berdasarkan slug $lembaga = Lembaga::where('slug', $slug)->firstOrFail(); - // Cek apakah jabatan yang dipilih adalah Ketua (jabatan_id = 1) if ($request->jabatan_id == 1) { - // Cek apakah sudah ada Ketua di lembaga ini $existingKetua = $lembaga->lembagaAnggota()->where('jabatan', 1)->first(); - // Jika ada, ubah jabatan ketua yang sudah ada menjadi Anggota (jabatan_id = 5) if ($existingKetua) { $existingKetua->update(['jabatan' => 5]); } } - try { + $data = $request->validated(); - $lembaga->lembagaAnggota()->create([ - 'penduduk_id' => $request->penduduk_id, + if (SettingAplikasi::where('key', 'sinkronisasi_database_gabungan')->value('value') === '1') { + $data['penduduk_id'] = null; + $data['penduduk_id_gabungan'] = $data['penduduk_id_gabungan'] ?? null; + } + + try { + $lembaga->lembagaAnggota()->create($data + [ 'no_anggota' => $request->no_anggota, 'jabatan' => $request->jabatan_id, 'no_sk_jabatan' => $request->no_sk_jabatan, @@ -193,7 +254,6 @@ public function store(StoreLembagaAnggotaRequest $request, $slug) 'periode' => $request->periode, 'keterangan' => $request->keterangan, ]); - } catch (\Exception $e) { Log::error('Lembaga Anggota creation failed', [ 'error' => $e->getMessage(), @@ -216,13 +276,24 @@ public function store(StoreLembagaAnggotaRequest $request, $slug) */ public function edit($slug, $id) { - // Cari lembaga berdasarkan slug $lembaga = Lembaga::where('slug', $slug)->firstOrFail(); - - // Cari anggota lembaga berdasarkan ID $anggota = LembagaAnggota::with('penduduk')->where('lembaga_id', $lembaga->id)->findOrFail($id); - // Ambil semua penduduk yang belum menjadi anggota kecuali yang sedang diedit + if ($this->isDatabaseGabungan()) { + $page_title = 'Ubah Anggota Lembaga ' . $lembaga->nama; + $page_description = 'Ubah Anggota Lembaga ' . $lembaga->nama; + + $pendudukGabungan = (new PendudukService())->pendudukGabunganByIds([$anggota->penduduk_id_gabungan]); + $penduduk = new Penduduk(); + if($pendudukGabungan[0]){ + $penduduk->nama = $pendudukGabungan[0]['attributes']['nama']; + $penduduk->nik = $pendudukGabungan[0]['attributes']['nik']; + } + $anggota->setRelation('penduduk', $penduduk); + + return view('data.lembaga_anggota.gabungan.edit', compact('page_title', 'page_description', 'lembaga', 'anggota')); + } + $existingAnggotaIds = $lembaga->lembagaAnggota() ->where('id', '!=', $anggota->id) ->pluck('penduduk_id') @@ -233,7 +304,6 @@ public function edit($slug, $id) return [$penduduk->id => $optionText]; }); - // Tambahkan penduduk saat ini (yang sedang diedit) ke daftar pendudukList $pendudukList->prepend("NIK: {$anggota->penduduk->nik} - {$anggota->penduduk->nama} - Dusun {$anggota->penduduk->dusun} RT {$anggota->penduduk->rt} / RW {$anggota->penduduk->rw}", $anggota->penduduk->id); $page_title = 'Ubah Anggota Lembaga ' . $lembaga->nama; @@ -252,28 +322,28 @@ public function edit($slug, $id) public function update(UpdateLembagaAnggotaRequest $request, $slug, $id) { try { - - // Cari lembaga berdasarkan slug $lembaga = Lembaga::where('slug', $slug)->firstOrFail(); - // Cari anggota berdasarkan id dan pastikan anggota tersebut milik lembaga yang sesuai $anggota = LembagaAnggota::where('id', $id) ->where('lembaga_id', $lembaga->id) ->firstOrFail(); - // Cek apakah jabatan yang dipilih adalah Ketua (jabatan_id = 1) if ($request->jabatan_id == 1) { - // Cek apakah sudah ada Ketua di lembaga ini $existingKetua = $lembaga->lembagaAnggota()->where('jabatan', 1)->first(); - // Jika ada Ketua lain, ubah jabatan Ketua yang sudah ada menjadi Anggota (jabatan_id = 5) if ($existingKetua && $existingKetua->id !== $anggota->id) { $existingKetua->update(['jabatan' => 5]); } } - // Update data anggota - $anggota->update([ + $data = $request->validated(); + + if (SettingAplikasi::where('key', 'sinkronisasi_database_gabungan')->value('value') === '1') { + $data['penduduk_id'] = null; + $data['penduduk_id_gabungan'] = $data['penduduk_id_gabungan'] ?? null; + } + + $anggota->update($data + [ 'no_anggota' => $request->no_anggota, 'jabatan' => $request->jabatan_id, 'no_sk_jabatan' => $request->no_sk_jabatan, @@ -284,7 +354,6 @@ public function update(UpdateLembagaAnggotaRequest $request, $slug, $id) 'periode' => $request->periode, 'keterangan' => $request->keterangan, ]); - } catch (\Exception $e) { Log::error('Lembaga Anggota update failed', [ 'error' => $e->getMessage(), diff --git a/app/Http/Controllers/Data/LembagaController.php b/app/Http/Controllers/Data/LembagaController.php index 8cb9851c60..fd38ed68a8 100644 --- a/app/Http/Controllers/Data/LembagaController.php +++ b/app/Http/Controllers/Data/LembagaController.php @@ -2,17 +2,18 @@ namespace App\Http\Controllers\Data; +use App\Http\Controllers\Controller; +use App\Http\Requests\StoreLembagaRequest; +use App\Http\Requests\UpdateLembagaRequest; +use App\Models\KategoriLembaga; use App\Models\Lembaga; +use App\Models\LembagaAnggota; use App\Models\Penduduk; +use App\Services\PendudukService; use Illuminate\Http\Request; -use App\Models\LembagaAnggota; -use App\Models\KategoriLembaga; -use Yajra\DataTables\DataTables; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; -use App\Http\Controllers\Controller; -use App\Http\Requests\StoreLembagaRequest; -use App\Http\Requests\UpdateLembagaRequest; +use Yajra\DataTables\DataTables; class LembagaController extends Controller { @@ -35,14 +36,22 @@ public function index() public function getData(Request $request) { if ($request->ajax()) { - $lembagaList = Lembaga::with(['lembagaKategori', 'penduduk', 'lembagaAnggota']) + $lembagaList = Lembaga::with(['lembagaKategori', 'penduduk', 'pendudukGabungan', 'lembagaAnggota']) ->withCount('lembagaAnggota as jml_anggota') ->get(); + + $pendudukGabunganIds = $lembagaList->pluck('penduduk_id_gabungan')->filter()->unique()->values()->all(); + + $pendudukGabungan = collect(); + if ($this->isDatabaseGabungan() && !empty($pendudukGabunganIds)) { + $pendudukGabungan = (new PendudukService())->pendudukGabunganByIds($pendudukGabunganIds); + } + return DataTables::of($lembagaList) ->addIndexColumn() ->addColumn('aksi', function ($row) { if (!auth()->guest()) { - $data['detail_url'] = auth()->user()->can('access.data.lembaga_anggota.view') ? route('data.lembaga_anggota.index', $row->slug) : null; + $data['detail_url'] = auth()->user()->can('access.data.lembaga.view') ? route('data.lembaga_anggota.index', $row->slug) : null; $data['edit_url'] = auth()->user()->can('access.data.lembaga.edit') ? route('data.lembaga.edit', $row->id) : null; $data['delete_url'] = auth()->user()->can('access.data.lembaga.delete') ? route('data.lembaga.destroy', $row->id) : null; } @@ -52,7 +61,13 @@ public function getData(Request $request) ->addColumn('kategori', function ($row) { return $row->lembagaKategori ? $row->lembagaKategori->nama : '-'; }) - ->addColumn('ketua', function ($row) { + ->addColumn('ketua', function ($row) use ($pendudukGabungan) { + if ($this->isDatabaseGabungan()) { + $penduduk = $pendudukGabungan->firstWhere('id', $row->penduduk_id_gabungan); + + return $penduduk ? ($penduduk['attributes']['nama'] ?? '-') : '-'; + } + return $row->penduduk ? $row->penduduk->nama : '-'; }) ->rawColumns(['aksi']) @@ -69,12 +84,15 @@ public function create() { $page_title = $this->title; $page_description = 'Tambah ' . $this->title; + + if ($this->isDatabaseGabungan()) { + $view = 'data.lembaga.gabungan.create'; + return view($view, compact('page_title', 'page_description')); + } + $kategoriLembagaList = KategoriLembaga::pluck('nama', 'id'); $pendudukList = Penduduk::all()->mapWithKeys(function ($penduduk) { - // Membuat string yang menggabungkan NIK, nama, dan alamat $optionText = "NIK: {$penduduk->nik} - {$penduduk->nama} - Dusun {$penduduk->dusun} RT {$penduduk->rt} / RW {$penduduk->rw}"; - - // Menambahkan ke array dengan 'id' sebagai key dan $optionText sebagai value return [$penduduk->id => $optionText]; }); @@ -85,24 +103,28 @@ public function create() * Store a newly created resource in storage. * * @param \Illuminate\Http\Request $request + * * @return \Illuminate\Http\Response */ public function store(StoreLembagaRequest $request) { $data = $request->all(); - // Menggunakan scope untuk menghasilkan slug yang unik + if ($this->isDatabaseGabungan()) { + $data['penduduk_id'] = null; + $data['penduduk_id_gabungan'] = $data['penduduk_id_gabungan'] ?? null; + } + $data['slug'] = Lembaga::generateUniqueSlug($request->nama); try { DB::transaction(function () use ($data) { - // Insert ke table 'lembaga' $lembaga = Lembaga::create($data); - // Insert ke tabel 'lembagaanggota' dengan id penduduk yang diambil dari input LembagaAnggota::create([ 'lembaga_id' => $lembaga->id, 'penduduk_id' => $data['penduduk_id'], + 'penduduk_id_gabungan' => $data['penduduk_id_gabungan'] ?? null, 'no_anggota' => 1, 'jabatan' => 1, 'keterangan' => 'Ketua lembaga' @@ -125,6 +147,7 @@ public function store(StoreLembagaRequest $request) * Display the specified resource. * * @param int $id + * * @return \Illuminate\Http\Response */ public function show($id) @@ -136,6 +159,7 @@ public function show($id) * Show the form for editing the specified resource. * * @param int $id + * * @return \Illuminate\Http\Response */ public function edit($id) @@ -143,12 +167,22 @@ public function edit($id) $lembaga = Lembaga::findOrFail($id); $page_title = $this->title; $page_description = 'Ubah ' . $this->title; + + if ($this->isDatabaseGabungan()) { + $view = 'data.lembaga.gabungan.edit'; + $pendudukGabungan = (new PendudukService())->pendudukGabunganByIds([$lembaga->penduduk_id_gabungan]); + $penduduk = new Penduduk(); + if($pendudukGabungan[0]){ + $penduduk->nama = $pendudukGabungan[0]['attributes']['nama']; + $penduduk->nik = $pendudukGabungan[0]['attributes']['nik']; + } + $lembaga->setRelation('penduduk', $penduduk); + return view($view, compact('page_title', 'page_description', 'lembaga')); + } + $kategoriLembagaList = KategoriLembaga::pluck('nama', 'id'); $pendudukList = Penduduk::all()->mapWithKeys(function ($penduduk) { - // Membuat string yang menggabungkan NIK, nama, dan alamat $optionText = "NIK: {$penduduk->nik} - {$penduduk->nama} - Dusun {$penduduk->dusun} RT {$penduduk->rt} / RW {$penduduk->rw}"; - - // Menambahkan ke array dengan 'id' sebagai key dan $optionText sebagai value return [$penduduk->id => $optionText]; }); @@ -160,6 +194,7 @@ public function edit($id) * * @param \Illuminate\Http\Request $request * @param int $id + * * @return \Illuminate\Http\Response */ public function update(UpdateLembagaRequest $request, $id) @@ -168,24 +203,25 @@ public function update(UpdateLembagaRequest $request, $id) try { DB::transaction(function () use ($data, $request, $id) { - // Temukan lembaga yang akan diupdate $lembaga = Lembaga::findOrFail($id); - // Hanya buat slug baru jika nama lembaga berubah if ($request->nama !== $lembaga->nama) { $data['slug'] = Lembaga::generateUniqueSlug($request->nama); } - // Update data lembaga + if ($this->isDatabaseGabungan()) { + $data['penduduk_id'] = null; + $data['penduduk_id_gabungan'] = $data['penduduk_id_gabungan'] ?? null; + } + $lembaga->update($data); - // Cek apakah anggota terkait sudah ada $anggota = LembagaAnggota::where('lembaga_id', $lembaga->id)->first(); if ($anggota) { - // Update penduduk_id pada anggota jika anggota sudah ada $anggota->update([ 'penduduk_id' => $data['penduduk_id'] ?? $lembaga->penduduk_id, + 'penduduk_id_gabungan' => $data['penduduk_id_gabungan'] ?? $lembaga->penduduk_id_gabungan, ]); } }); @@ -202,11 +238,11 @@ public function update(UpdateLembagaRequest $request, $id) return redirect()->route('data.lembaga.index')->with('success', 'Lembaga berhasil diubah!'); } - /** * Remove the specified resource from storage. * * @param int $id + * * @return \Illuminate\Http\Response */ public function destroy($id) diff --git a/app/Http/Controllers/Data/PutusSekolahController.php b/app/Http/Controllers/Data/PutusSekolahController.php index 0273a00616..78d50ba574 100644 --- a/app/Http/Controllers/Data/PutusSekolahController.php +++ b/app/Http/Controllers/Data/PutusSekolahController.php @@ -34,8 +34,10 @@ use App\Exports\ExportPutusSekolah; use App\Http\Controllers\Controller; use App\Http\Requests\ImportPutusSekolahRequest; +use App\Http\Requests\UpdatePutusSekolahRequest; use App\Imports\ImporPutusSekolah; use App\Models\PutusSekolah; +use App\Services\DesaService; use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Facades\Log; @@ -59,12 +61,18 @@ public function index() */ public function getDataPutusSekolah() { - return DataTables::of(PutusSekolah::with(['desa'])->get()) - ->addColumn('aksi', function ($row) { + $desa = request()->input('desa'); + $listDesa = (new DesaService)->listDesa()->pluck('nama', 'desa_id'); + return DataTables::of(PutusSekolah::when($desa && $desa !== 'Semua', function ($query) use ($desa) { + return $query->where('desa_id', $desa); + })->with(['desa'])->get()) + ->addColumn('aksi', function ($row) { $data['edit_url'] = auth()->user()->can('access.data.putus_sekolah.edit') ? route('data.putus-sekolah.edit', $row->id) : null; $data['delete_url'] = auth()->user()->can('access.data.putus_sekolah.delete') ? route('data.putus-sekolah.destroy', $row->id) : null; return view('forms.aksi', $data); + })->addColumn('nama_desa', function ($row) use ($listDesa) { + return $row->desa->nama ?? $listDesa[$row->desa_id] ?? '-'; }) ->rawColumns(['aksi'])->make(); } @@ -116,7 +124,7 @@ public function edit($id) { $siswa = PutusSekolah::with(['desa'])->findOrFail($id); $page_title = 'Siswa Putus Sekolah'; - $page_description = 'Ubah Siswa Putus Sekolah : Desa ' . $siswa->desa->nama; + $page_description = 'Ubah Siswa Putus Sekolah : Desa ' . nama_desa($siswa->desa_id); return view('data.putus_sekolah.edit', compact('page_title', 'page_description', 'siswa')); } @@ -127,23 +135,10 @@ public function edit($id) * @param int $id * @return Response */ - public function update(Request $request, $id) + public function update(UpdatePutusSekolahRequest $request, $id) { - request()->validate([ - 'siswa_paud' => 'required', - 'anak_usia_paud' => 'required', - 'siswa_sd' => 'required', - 'anak_usia_sd' => 'required', - 'siswa_smp' => 'required', - 'anak_usia_smp' => 'required', - 'siswa_sma' => 'required', - 'anak_usia_sma' => 'required', - 'semester' => 'required', - 'tahun' => 'required', - ]); - try { - PutusSekolah::findOrFail($id)->update($request->all()); + PutusSekolah::findOrFail($id)->update($request->validated()); } catch (\Exception $e) { Log::error('Putus Sekolah update failed', [ 'error' => $e->getMessage(), diff --git a/app/Http/Controllers/Data/SuplemenController.php b/app/Http/Controllers/Data/SuplemenController.php index 5bb4f28323..2f8215c0dd 100644 --- a/app/Http/Controllers/Data/SuplemenController.php +++ b/app/Http/Controllers/Data/SuplemenController.php @@ -33,25 +33,31 @@ use App\Exports\ExportSuplemen; use App\Exports\ExportSuplemenTerdata; +use App\Exports\ExportSuplemenTerdataGabungan; use App\Http\Controllers\Controller; +use App\Http\Requests\SuplemenRequest; +use App\Http\Requests\SuplemenTerdataRequest; use App\Models\DataDesa; use App\Models\Penduduk; use App\Models\Suplemen; use App\Models\SuplemenTerdata; +use App\Services\PendudukService; +use Illuminate\Contracts\View\View; +use Illuminate\Http\JsonResponse; +use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; use Illuminate\Support\Str; use Maatwebsite\Excel\Facades\Excel; +use Symfony\Component\HttpFoundation\BinaryFileResponse; use Yajra\DataTables\DataTables; class SuplemenController extends Controller { /** * Display a listing of the resource. - * - * @return \Illuminate\Http\Response */ - public function index() + public function index(): View { $page_title = 'Data Suplemen'; $page_description = 'Daftar Data Suplemen'; @@ -60,11 +66,11 @@ public function index() } /** - * Return datatable Data Suplemen + * Return datatable Data Suplemen. */ - public function getDataSuplemen() + public function getDataSuplemen(Request $request): ?JsonResponse { - if (request()->ajax()) { + if ($request->ajax()) { return DataTables::of(Suplemen::withCount('terdata')->get()) ->addIndexColumn() ->addColumn('aksi', function ($row) { @@ -88,10 +94,8 @@ public function getDataSuplemen() /** * Show the form for creating a new resource. - * - * @return \Illuminate\Http\Response */ - public function create() + public function create(): View { $page_title = 'Data Suplemen'; $page_description = 'Tambah Data Suplemen'; @@ -101,19 +105,14 @@ public function create() /** * Store a newly created resource in storage. - * - * @return \Illuminate\Http\Response */ - public function store(Request $request) + public function store(SuplemenRequest $request): RedirectResponse { - request()->validate([ - 'nama' => 'required', - ]); - - $request['slug'] = Str::slug($request->nama); + $data = $request->validated(); + $data['slug'] = Str::slug($data['nama']); try { - Suplemen::create($request->all()); + Suplemen::create($data); } catch (\Exception $e) { Log::error('Suplemen creation failed', [ 'error' => $e->getMessage(), @@ -129,11 +128,8 @@ public function store(Request $request) /** * Show the form for editing the specified resource. - * - * @param int $id - * @return \Illuminate\Http\Response */ - public function edit($id) + public function edit(int $id): View { $suplemen = Suplemen::findOrFail($id); $page_title = 'Data Suplemen'; @@ -144,20 +140,14 @@ public function edit($id) /** * Update the specified resource in storage. - * - * @param int $id - * @return \Illuminate\Http\Response */ - public function update(Request $request, $id) + public function update(SuplemenRequest $request, int $id): RedirectResponse { - request()->validate([ - 'nama' => 'required', - ]); - - $request['slug'] = Str::slug($request->nama); + $data = $request->validated(); + $data['slug'] = Str::slug($data['nama']); try { - Suplemen::findOrFail($id)->update($request->all()); + Suplemen::findOrFail($id)->update($data); } catch (\Exception $e) { Log::error('Suplemen update failed', [ 'error' => $e->getMessage(), @@ -173,11 +163,8 @@ public function update(Request $request, $id) /** * Remove the specified resource from storage. - * - * @param int $id - * @return \Illuminate\Http\Response */ - public function destroy($id) + public function destroy(int $id): RedirectResponse { if (SuplemenTerdata::where('suplemen_id', $id)->exists()) { return redirect()->route('data.data-suplemen.index')->with('error', 'Tidak dapat menghapus Data Suplemen yang mempunyai anggota!'); @@ -200,46 +187,79 @@ public function destroy($id) /** * Display the specified resource. - * - * @param int $id - * @return \Illuminate\Http\Response */ - public function show($id) + public function show(int $id): View { $suplemen = Suplemen::findOrFail($id); $sasaran = ['1' => 'Penduduk', '2' => 'Keluarga/KK']; $page_title = 'Anggota Suplemen'; $page_description = 'Anggota Suplemen: ' . ucwords(strtolower($suplemen->nama)); - return view('data.data_suplemen.show', compact('page_title', 'page_description', 'suplemen', 'sasaran')); + $view = $this->isDatabaseGabungan() ? 'data.data_suplemen.gabungan.show' : 'data.data_suplemen.show'; + + return view($view, compact('page_title', 'page_description', 'suplemen', 'sasaran')); } /** - * Return datatable Data Suplemen Terdata + * Return datatable Data Suplemen Terdata. + * + * Saat database gabungan aktif, data dari API database gabungan + * (hasil sinkronisasi desa) digabungkan dengan data anggota lokal. */ - public function getDataSuplemenTerdata($id_terdata) + public function getDataSuplemenTerdata(Request $request, int $id_terdata): ?JsonResponse { - if (request()->ajax()) { - return DataTables::of(SuplemenTerdata::with('penduduk', 'penduduk.desa')->where('suplemen_id', $id_terdata)->get()) + if ($request->ajax()) { + $desa = $request->input('desa'); + + $terdata = SuplemenTerdata::with('penduduk', 'penduduk.desa') + ->where('suplemen_id', $id_terdata) + ->when($desa && $desa !== 'Semua', fn ($query) => $query->where('desa_id', $desa)) + ->get(); + + if ($this->isDatabaseGabungan()) { + $pendudukGabungan = $this->pendudukGabunganBatch( + $terdata->pluck('penduduk_id_gabungan')->filter()->values()->all() + ); + + $terdata = $terdata->map(function (SuplemenTerdata $row) use ($pendudukGabungan) { + if ($row->penduduk_id_gabungan && ! $row->penduduk) { + $penduduk = $pendudukGabungan[(int) $row->penduduk_id_gabungan] ?? null; + + if ($penduduk) { + $row->setRelation('penduduk', $penduduk); + } + } + + return $row; + }); + } + + return DataTables::of($terdata) ->addIndexColumn() ->addColumn('aksi', function ($row) { - if (!auth()->guest()) { - $data['edit_url'] = route('data.data-suplemen.editdetail', [$row->id, $row->suplemen_id]); - $data['delete_url'] = route('data.data-suplemen.destroydetail', [$row->id, $row->suplemen_id]); + if (! $row instanceof SuplemenTerdata) { + return null; } + if (auth()->guest()) { + return null; + } + + $data['edit_url'] = auth()->user()->can('access.data.data_suplemen.edit') ? route('data.data-suplemen.editdetail', [$row->id, $row->suplemen_id]) : null; + $data['delete_url'] = auth()->user()->can('access.data.data_suplemen.delete') ? route('data.data-suplemen.destroydetail', [$row->id, $row->suplemen_id]) : null; + return view('forms.aksi', $data); }) ->editColumn('penduduk.sex', function ($row) { $sex = ['1' => 'Laki-laki', '2' => 'Perempuan']; - return $sex[$row->penduduk->sex]; + return $sex[$row->penduduk->sex] ?? '-'; }) ->make(); } } - public function getPenduduk($desa, $suplemen) + public function getPenduduk(string $desa, int $suplemen): JsonResponse { $penduduk = []; @@ -258,83 +278,97 @@ public function getPenduduk($desa, $suplemen) /** * Show the form for creating a new resource. - * - * @return \Illuminate\Http\Response */ - public function createDetail($id_suplemen) + public function createDetail(int $id_suplemen): View { $suplemen = Suplemen::findOrFail($id_suplemen); $sasaran = ['1' => 'Penduduk', '2' => 'Keluarga/KK']; $page_title = 'Anggota Suplemen'; $page_description = 'Tambah Anggota Suplemen'; - $desa = DataDesa::all(); $anggota = null; + $isDatabaseGabungan = $this->isDatabaseGabungan(); - if ($suplemen->sasaran == 1) { - $data = Penduduk::get(); + if ($isDatabaseGabungan) { + $data = collect(); + $desa = collect(); + $view = 'data.data_suplemen.gabungan.create_detail'; } else { - $data = Penduduk::where('kk_level', 1)->get(); + $desa = DataDesa::all(); + $view = 'data.data_suplemen.create_detail'; + + if ($suplemen->sasaran == 1) { + $data = Penduduk::get(); + } else { + $data = Penduduk::where('kk_level', 1)->get(); + } } - return view('data.data_suplemen.create_detail', compact('page_title', 'page_description', 'suplemen', 'sasaran', 'data', 'desa', 'anggota')); + return view($view, compact('page_title', 'page_description', 'suplemen', 'sasaran', 'data', 'desa', 'anggota', 'isDatabaseGabungan')); } /** * Store a newly created resource in storage. - * - * @return \Illuminate\Http\Response */ - public function storeDetail(Request $request) + public function storeDetail(SuplemenTerdataRequest $request): RedirectResponse { - request()->validate( - ['penduduk_id' => 'required'], - ['penduduk_id.required' => 'isian warga atau penduduk wajib diisi'] - ); + $data = $this->normalizePendudukSumber($this->resolveDesaId($request->validated())); try { - SuplemenTerdata::create($request->all()); + SuplemenTerdata::create($data); } catch (\Exception $e) { Log::error('Suplemen Terdata creation failed', [ 'error' => $e->getMessage(), 'user_id' => auth()->id(), - 'suplemen_id' => $request['suplemen_id'], + 'suplemen_id' => $request->input('suplemen_id'), ]); return back()->withInput()->with('error', 'Anggota Suplemen gagal ditambah!'); } - return redirect()->route('data.data-suplemen.show', $request['suplemen_id'])->with('success', 'Anggota Suplemen berhasil ditambah!'); + return redirect()->route('data.data-suplemen.show', $request->input('suplemen_id'))->with('success', 'Anggota Suplemen berhasil ditambah!'); } /** * Show the form for editing the specified resource. - * - * @param int $id - * @return \Illuminate\Http\Response */ - public function editDetail($id, $id_suplemen) + public function editDetail(int $id, int $id_suplemen): View { $suplemen = Suplemen::findOrFail($id_suplemen); $sasaran = ['1' => 'Penduduk', '2' => 'Keluarga/KK']; $page_title = 'Anggota Suplemen'; $page_description = 'Ubah Anggota Suplemen'; - $anggota = SuplemenTerdata::with('penduduk', 'penduduk.desa')->where('id', $id)->first(); - $data = Penduduk::where('id', $anggota->penduduk_id)->get(); + $anggota = SuplemenTerdata::with('penduduk', 'penduduk.desa')->where('id', $id)->firstOrFail(); + $isDatabaseGabungan = $this->isDatabaseGabungan(); + + if ($isDatabaseGabungan) { + $penduduk = null; + + if ($anggota->penduduk_id_gabungan) { + $penduduk = $this->pendudukGabunganBatch([$anggota->penduduk_id_gabungan])[(int) $anggota->penduduk_id_gabungan] ?? null; + } + + $selectedDesaId = $penduduk?->desa?->desa_id ?? $anggota->desa_id; + + return view('data.data_suplemen.gabungan.edit_detail', compact('page_title', 'page_description', 'suplemen', 'sasaran', 'anggota', 'isDatabaseGabungan', 'penduduk', 'selectedDesaId')); + } + $desa = DataDesa::all(); + $data = $anggota->penduduk_id ? Penduduk::where('id', $anggota->penduduk_id)->get() : collect(); + $selectedDesaId = optional($anggota->penduduk)?->desa?->desa_id ?? $anggota->desa_id; + $selectedPendudukId = optional($anggota->penduduk)->id; - return view('data.data_suplemen.edit_detail', compact('page_title', 'page_description', 'suplemen', 'sasaran', 'data', 'desa', 'anggota')); + return view('data.data_suplemen.edit_detail', compact('page_title', 'page_description', 'suplemen', 'sasaran', 'data', 'desa', 'anggota', 'isDatabaseGabungan', 'selectedDesaId', 'selectedPendudukId')); } /** * Update the specified resource in storage. - * - * @param int $id - * @return \Illuminate\Http\Response */ - public function updateDetail(Request $request, $id) + public function updateDetail(SuplemenTerdataRequest $request, int $id): RedirectResponse { + $data = $this->normalizePendudukSumber($this->resolveDesaId($request->validated())); + try { - SuplemenTerdata::findOrFail($id)->update($request->all()); + SuplemenTerdata::findOrFail($id)->update($data); } catch (\Exception $e) { Log::error('Suplemen Terdata update failed', [ 'error' => $e->getMessage(), @@ -345,16 +379,13 @@ public function updateDetail(Request $request, $id) return back()->withInput()->with('error', 'Anggota Suplemen gagal diubah!'); } - return redirect()->route('data.data-suplemen.show', $request['suplemen_id'])->with('success', 'Anggota Suplemen berhasil diubah!'); + return redirect()->route('data.data-suplemen.show', $request->input('suplemen_id'))->with('success', 'Anggota Suplemen berhasil diubah!'); } /** * Remove the specified resource from storage. - * - * @param int $id - * @return \Illuminate\Http\Response */ - public function destroyDetail($id, $id_suplemen) + public function destroyDetail(int $id, int $id_suplemen): RedirectResponse { try { SuplemenTerdata::findOrFail($id)->delete(); @@ -374,11 +405,8 @@ public function destroyDetail($id, $id_suplemen) /** * Export Excel data suplemen. - * - * @param Request $request - * @return \Symfony\Component\HttpFoundation\BinaryFileResponse */ - public function exportExcel(Request $request) + public function exportExcel(Request $request): BinaryFileResponse { $filters = $request->only(['nama', 'sasaran']); $timestamp = date('Y-m-d-H-i-s'); @@ -389,18 +417,139 @@ public function exportExcel(Request $request) /** * Export Excel data suplemen terdata. - * - * @param Request $request - * @param int $id Suplemen ID - * @return \Symfony\Component\HttpFoundation\BinaryFileResponse */ - public function exportTerdataExcel(Request $request, $id) + public function exportTerdataExcel(Request $request, int $id): BinaryFileResponse { $suplemen = Suplemen::findOrFail($id); $filters = $request->only(['desa', 'nama_penduduk']); $timestamp = date('Y-m-d-H-i-s'); $filename = "data-suplemen-terdata-{$suplemen->slug}-{$timestamp}.xlsx"; + if ($this->isDatabaseGabungan()) { + $query = SuplemenTerdata::query()->where('suplemen_id', $id); + + if (!empty($filters['desa'])) { + $query->where(function ($q) use ($filters) { + $q->where('desa_id', $filters['desa']); + }); + } + + if (!empty($filters['nama_penduduk'])) { + $query->whereHas('penduduk', function ($q) use ($filters) { + $q->where('nama', 'like', '%' . $filters['nama_penduduk'] . '%'); + }); + } + + $pendudukGabunganIds = $query->whereNotNull('penduduk_id_gabungan')->pluck('penduduk_id_gabungan')->filter()->values()->all(); + + return Excel::download(new ExportSuplemenTerdataGabungan($id, $filters, $pendudukGabunganIds), $filename); + } + return Excel::download(new ExportSuplemenTerdata($id, $filters), $filename); } + + /** + * Normalisasi sumber penduduk untuk anggota suplemen. + * + * Anggota hanya boleh berisi salah satu sumber: penduduk lokal (penduduk_id) + * atau penduduk dari database gabungan (penduduk_id_gabungan), tidak keduanya. + */ + private function normalizePendudukSumber(array $validated): array + { + if (! empty($validated['penduduk_id_gabungan'])) { + $validated['penduduk_id'] = null; + } else { + $validated['penduduk_id_gabungan'] = null; + } + + return $validated; + } + + /** + * Resolusi desa_id untuk anggota suplemen. + * + * Nilai desa_id dari form diutamakan. Fallback diambil dari respons API + * database gabungan (config.kode_desa) atau kolom desa_id das_penduduk + * untuk penduduk lokal. + */ + private function resolveDesaId(array $data): array + { + if (! empty($data['desa_id'])) { + return $data; + } + + if (! empty($data['penduduk_id_gabungan'])) { + $penduduk = $this->pendudukGabunganBatch([(int) $data['penduduk_id_gabungan']])[(int) $data['penduduk_id_gabungan']] ?? null; + $data['desa_id'] = $penduduk?->desa?->desa_id; + } elseif (! empty($data['penduduk_id'])) { + $data['desa_id'] = Penduduk::where('id', $data['penduduk_id'])->value('desa_id'); + } + + return $data; + } + + /** + * Resolusi beberapa penduduk dari database gabungan sekaligus (batch) + * menggunakan filter id_penduduk berupa array, agar tidak melakukan + * request berulang per anggota. + * + * @param array $ids + * + * @return array + */ + private function pendudukGabunganBatch(array $ids): array + { + if (empty($ids)) { + return []; + } + + try { + $penduduk = []; + + foreach ((new PendudukService())->pendudukGabunganByIds($ids) as $item) { + if (! is_array($item)) { + continue; + } + + $penduduk[(int) ($item['id'] ?? 0)] = $this->mapGabunganPenduduk($item); + } + + return $penduduk; + } catch (\Exception $e) { + Log::error('Penduduk batch fetch from gabungan failed', [ + 'error' => $e->getMessage(), + 'ids' => $ids, + ]); + + return []; + } + } + + /** + * Map respons penduduk API database gabungan (JSON:API) ke model Eloquent + * yang digunakan kolom datatable dan form anggota suplemen. + */ + private function mapGabunganPenduduk(array $item): Penduduk + { + $attributes = $item['attributes'] ?? []; + + $penduduk = new Penduduk(); + $penduduk->forceFill([ + 'id' => $item['id'] ?? null, + 'no_kk' => data_get($attributes, 'keluarga.no_kk') ?? $attributes['no_kk'] ?? null, + 'nik' => $attributes['nik'] ?? null, + 'nama' => $attributes['nama'] ?? null, + 'tempat_lahir' => $attributes['tempatlahir'] ?? $attributes['tempat_lahir'] ?? null, + 'tanggal_lahir' => $attributes['tanggallahir'] ?? $attributes['tanggal_lahir'] ?? null, + 'sex' => $attributes['sex'] ?? null, + 'alamat' => $attributes['alamat_sekarang'] ?? $attributes['alamat'] ?? null, + ]); + + $desa = new DataDesa(); + $desa->nama = data_get($attributes, 'config.nama_desa') ?? $attributes['nama_desa'] ?? null; + $desa->desa_id = data_get($attributes, 'config.kode_desa') ?? $attributes['kode_desa'] ?? null; + $penduduk->setRelation('desa', $desa); + + return $penduduk; + } } diff --git a/app/Http/Controllers/Data/TingkatPendidikanController.php b/app/Http/Controllers/Data/TingkatPendidikanController.php index 2fdab92d08..8f27d095c1 100644 --- a/app/Http/Controllers/Data/TingkatPendidikanController.php +++ b/app/Http/Controllers/Data/TingkatPendidikanController.php @@ -36,6 +36,7 @@ use App\Http\Requests\TingkatPendidikanRequest; use App\Imports\ImporTingkatPendidikan; use App\Models\TingkatPendidikan; +use App\Services\DesaService; use Illuminate\Http\Request; use Illuminate\Http\Response; use Illuminate\Support\Facades\Log; @@ -61,7 +62,7 @@ public function getData() { if (request()->ajax()) { $desa = request()->input('desa'); - + $listDesa = (new DesaService)->listDesa()->pluck('nama', 'desa_id'); return DataTables::of( TingkatPendidikan::when($desa && $desa !== 'Semua', function ($query) use ($desa) { return $query->where('desa_id', $desa); @@ -74,6 +75,9 @@ public function getData() return view('forms.aksi', $data); }) + ->addColumn('nama_desa', function ($row) use ($listDesa) { + return $row->desa->nama ?? $listDesa[$row->desa_id] ?? '-'; + }) ->rawColumns(['aksi']) ->make(); } diff --git a/app/Http/Controllers/Data/ToiletSanitasiController.php b/app/Http/Controllers/Data/ToiletSanitasiController.php index aa60afe6e3..227f6eae17 100644 --- a/app/Http/Controllers/Data/ToiletSanitasiController.php +++ b/app/Http/Controllers/Data/ToiletSanitasiController.php @@ -34,6 +34,7 @@ use App\Exports\ExportToiletSanitasi; use App\Http\Controllers\Controller; use App\Http\Requests\ImportToiletSanitasiRequest; +use App\Http\Requests\UpdateToiletSanitasiRequest; use App\Imports\ImporToiletSanitasi; use App\Models\ToiletSanitasi; use App\Services\DesaService; @@ -65,8 +66,12 @@ public function index() */ public function getDataAKIAKB() { + $desa = request()->input('desa'); $listDesa = (new DesaService)->listDesa()->pluck('nama', 'desa_id'); - return DataTables::of(ToiletSanitasi::with(['desa'])) + return DataTables::of(ToiletSanitasi::when($desa && $desa !== 'Semua', function ($query) use ($desa) { + return $query->where('desa_id', $desa); + }) + ->with(['desa'])) ->addColumn('aksi', function ($row) { $data['edit_url'] = auth()->user()->can('access.data.toilet_sanitasi.edit') ? route('data.toilet-sanitasi.edit', $row->id) : null; $data['delete_url'] = auth()->user()->can('access.data.toilet_sanitasi.delete') ? route('data.toilet-sanitasi.destroy', $row->id) : null; @@ -114,7 +119,7 @@ public function do_import(ImportToiletSanitasiRequest $request) return back()->with('error', 'Import data gagal.'); } - return back()->with('success', 'Import data sukses.'); + return redirect()->route('data.toilet_sanitasi.index')->with('success', 'Data berhasil diimpor!'); } /** @@ -127,7 +132,7 @@ public function edit($id) { $toilet = ToiletSanitasi::with(['desa'])->findOrFail($id); $page_title = 'Toilet & Sanitasi'; - $page_description = 'Ubah Toilet & Sanitasi : ' . $toilet->desa->nama; + $page_description = 'Ubah Toilet & Sanitasi : ' . nama_desa($toilet->desa_id); return view('data.toilet_sanitasi.edit', compact('page_title', 'page_description', 'toilet')); } @@ -138,15 +143,10 @@ public function edit($id) * @param int $id * @return Response */ - public function update(Request $request, $id) + public function update(UpdateToiletSanitasiRequest $request, $id) { - request()->validate([ - 'toilet' => 'required', - 'sanitasi' => 'required', - ]); - try { - ToiletSanitasi::findOrFail($id)->update($request->all()); + ToiletSanitasi::findOrFail($id)->update($request->validated()); } catch (\Exception $e) { Log::error('Toilet Sanitasi update failed', [ 'error' => $e->getMessage(), diff --git a/app/Http/Controllers/FrontEnd/PageController.php b/app/Http/Controllers/FrontEnd/PageController.php index ac9d8eb89a..b665d330a2 100644 --- a/app/Http/Controllers/FrontEnd/PageController.php +++ b/app/Http/Controllers/FrontEnd/PageController.php @@ -34,6 +34,8 @@ use App\Enums\SurveiEnum; use App\Facades\Counter; use App\Http\Controllers\FrontEndController; +use App\Http\Requests\KirimBalasanRequest; +use App\Http\Requests\KirimKomentarRequest; use App\Http\Requests\SurveiRequest; use App\Models\Artikel; use App\Models\Comment; @@ -41,15 +43,18 @@ use App\Models\Kategori; use App\Models\Survei; use App\Services\DesaService; +use Illuminate\Http\JsonResponse; +use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\Session; +use Illuminate\View\View; use Jenssegers\Agent\Agent; class PageController extends FrontEndController { protected $data = []; - public function index() + public function index(): View { Counter::count('beranda'); @@ -65,7 +70,7 @@ public function index() ]); } - public function beritaDesa() + public function beritaDesa(): View { if ($this->isDatabaseGabungan()) { $apiServer = $this->settings['api_server_database_gabungan'] ?? ''; @@ -94,13 +99,7 @@ public function beritaDesa() ]); } - private function getFeeds() - { - $feeds = (new DesaService())->getFeeds(); - return $feeds ?? null; - } - - public function filterFeeds(Request $request) + public function filterFeeds(Request $request): JsonResponse { $this->data = $this->getFeeds(); $feeds = collect($this->data); @@ -141,7 +140,7 @@ public function filterFeeds(Request $request) return response()->json(compact('html')); } - public function PotensiByKategory($slug) + public function PotensiByKategory(string $slug): View { $page_title = 'Potensi'; $page_description = 'Potensi-Potensi'; @@ -149,15 +148,15 @@ public function PotensiByKategory($slug) return view('pages.potensi.index', compact(['page_title', 'page_description', 'slug'])); } - public function PotensiShow($kategori, $slug) + public function PotensiShow(string $kategori, string $id): View { $page_title = 'Potensi'; $page_description = 'Potensi-Potensi Kecamatan'; - return view('pages.potensi.show', compact(['page_title', 'page_description', 'slug'])); + return view('pages.potensi.show', compact(['page_title', 'page_description', 'id'])); } - public function DesaShow($slug) + public function DesaShow(string $slug): View { $desa = (new DesaService)->dataDesa($slug); $page_title = 'Desa '.$desa->nama; @@ -166,12 +165,12 @@ public function DesaShow($slug) return view('pages.desa.desa_show', compact('page_title', 'page_description', 'desa')); } - public function refresh_captcha() + public function refresh_captcha(): JsonResponse { return response()->json(['captcha' => captcha_img('mini')]); } - public function kategoriBerita($slug) + public function kategoriBerita(string $slug): View { $kategori = Kategori::where('slug', $slug)->firstOrFail(); $artikel = Artikel::whereRelation('kategori', 'slug', $slug)->paginate(9); @@ -179,7 +178,7 @@ public function kategoriBerita($slug) return view('pages.berita.kategori', compact('artikel', 'kategori')); } - public function detailBerita($slug, Request $request) + public function detailBerita(string $slug, Request $request): View { // Temukan artikel berdasarkan slug $artikel = Artikel::with(['kategori', 'comments' => function ($query) use ($request) { @@ -214,18 +213,8 @@ public function detailBerita($slug, Request $request) return view('pages.berita.detail', compact('page_title', 'page_description', 'page_image', 'artikel', 'comments')); } - public function kirimKomentar(Request $request) + public function kirimKomentar(KirimKomentarRequest $request): RedirectResponse { - - // Validasi input - $validated = $request->validate([ - 'nama' => 'required|string|max:255', - 'email' => 'required|email|max:255', - 'body' => 'required|string', - 'das_artikel_id' => 'required|exists:das_artikel,id', - 'captcha_main' => 'required|captcha', - ]); - try { // Mendeteksi IP address $ipAddress = $request->ip(); @@ -241,11 +230,11 @@ public function kirimKomentar(Request $request) // Simpan komentar baru $comment = Comment::create([ - 'nama' => $validated['nama'], - 'email' => $validated['email'], - 'body' => $validated['body'], + 'nama' => $request->nama, + 'email' => $request->email, + 'body' => $request->body, 'status' => 'disable', // Set status default ke 'disable' untuk moderasi - 'das_artikel_id' => $validated['das_artikel_id'], + 'das_artikel_id' => $request->das_artikel_id, 'comment_id' => $request->input('comment_id', null), // Jika ini adalah balasan 'ip_address' => $ipAddress, 'device' => $deviceInfo, @@ -262,7 +251,7 @@ public function kirimKomentar(Request $request) } } - public function modalKirimBalasan(Request $request) + public function modalKirimBalasan(Request $request): View { $commentId = $request->input('comment_id'); $artikelId = $request->input('artikel_id'); @@ -270,17 +259,8 @@ public function modalKirimBalasan(Request $request) return view('pages.berita.comment', compact('commentId', 'artikelId')); } - public function kirimBalasan(Request $request) + public function kirimBalasan(KirimBalasanRequest $request): RedirectResponse { - $request->validate([ - 'nama' => 'required|string|max:255', - 'email' => 'required|email|max:255', - 'body' => 'required|string', - 'captcha_main' => 'required|captcha', - 'das_artikel_id' => 'required|exists:das_artikel,id', // Pastikan artikel terkait ada - 'comment_id' => 'required|exists:das_artikel_comment,id', // Pastikan comment_id ada - ]); - try { // Mendeteksi IP address $ipAddress = $request->ip(); @@ -314,7 +294,7 @@ public function kirimBalasan(Request $request) } } - public function eventDetail($slug) + public function eventDetail(string $slug): View { $event = Event::slug($slug)->firstOrFail(); $page_title = $event->event_name; @@ -323,7 +303,7 @@ public function eventDetail($slug) return view('pages.event.event_detail', compact('page_title', 'page_description', 'event')); } - public function kategori($slug) + public function kategori(string $slug): View { return view('pages.berita.kategori', [ 'apiBaseKategori' => url($this->urlApi.'/kategori?'.http_build_query([ @@ -341,7 +321,7 @@ public function kategori($slug) } // fitur survey indeks kepuasan pengguna terhadap penyajian informasi yang tersedia di website - public function survei() + public function survei(): View { $page_title = 'Index Kepuasan Masyarakat'; @@ -360,7 +340,7 @@ public function survei() return view('pages.ikm.index', compact('page_title', 'results')); } - public function surveiSubmit(SurveiRequest $request) + public function surveiSubmit(SurveiRequest $request): RedirectResponse { // Cek ulang session untuk keamanan if (Session::has('survey_submitted')) { @@ -381,4 +361,10 @@ public function surveiSubmit(SurveiRequest $request) return redirect()->back()->with('success', 'Terima kasih atas tanggapan Anda!'); } + + private function getFeeds(): ?array + { + $feeds = (new DesaService())->getFeeds(); + return $feeds ?? null; + } } diff --git a/app/Http/Controllers/FrontEnd/ProfilController.php b/app/Http/Controllers/FrontEnd/ProfilController.php index c966126ec0..1cfd4bfb10 100644 --- a/app/Http/Controllers/FrontEnd/ProfilController.php +++ b/app/Http/Controllers/FrontEnd/ProfilController.php @@ -76,13 +76,14 @@ public function LetakGeografis() Counter::count('profil.letak-geografis'); $wilayah_desa = (new DesaService())->listPathDesa(); + $data_umum = DataUmum::first(); $page_title = 'Letak Geografis'; $page_description = $this->browser_title; $view = $this->isDatabaseGabungan() ? 'pages.profil.gabungan.letakgeografis' : 'pages.profil.letakgeografis'; - return view($view, compact('page_title', 'page_description', 'wilayah_desa')); + return view($view, compact('page_title', 'page_description', 'wilayah_desa', 'data_umum')); } public function StrukturPemerintahan() diff --git a/app/Http/Controllers/FrontEnd/SistemKomplainController.php b/app/Http/Controllers/FrontEnd/SistemKomplainController.php index 052795c816..a653b4c976 100644 --- a/app/Http/Controllers/FrontEnd/SistemKomplainController.php +++ b/app/Http/Controllers/FrontEnd/SistemKomplainController.php @@ -31,15 +31,14 @@ namespace App\Http\Controllers\FrontEnd; +use App\Http\Controllers\FrontEndController; +use App\Http\Requests\FrontEnd\SistemKomplainRequest; +use App\Models\JawabKomplain; use App\Models\Komplain; use App\Models\Penduduk; +use App\Services\PendudukService; use Illuminate\Http\Request; -use App\Models\JawabKomplain; use Illuminate\Http\Response; -use Illuminate\Support\Facades\Validator; -use App\Http\Controllers\FrontEndController; -use App\Rules\ValidasiNikRule; -use App\Services\PendudukService; use Illuminate\Support\Facades\Log; class SistemKomplainController extends FrontEndController @@ -100,34 +99,15 @@ public function tracking(Request $request) } } - public function store(Request $request) + public function store(SistemKomplainRequest $request) { try { - $maxRule = \App\Services\FileUploadService::isLimitEnabled() ? '|max:1024' : ''; - $validator = Validator::make($request->all(), [ - 'nik' => ['required', 'numeric', new ValidasiNikRule($request->input('tanggal_lahir'))], - 'judul' => 'required|string|max:255', - 'kategori' => 'required', - 'laporan' => 'required|string', - 'captcha' => 'required|captcha', - 'tanggal_lahir' => 'required|date', - 'lampiran1' => 'file|mimes:jpeg,png,jpg,gif,svg' . $maxRule . '|valid_file', - 'lampiran2' => 'file|mimes:jpeg,png,jpg,gif,svg' . $maxRule . '|valid_file', - 'lampiran3' => 'file|mimes:jpeg,png,jpg,gif,svg' . $maxRule . '|valid_file', - 'lampiran4' => 'file|mimes:jpeg,png,jpg,gif,svg' . $maxRule . '|valid_file', - ], [ - 'captcha.captcha' => 'Invalid captcha code.', - ]); - - if ($validator->fails()) { - return back()->withInput()->with('error', 'Komplain gagal dikirim!')->withErrors($validator); - } $komplain = new Komplain($request->all()); $penduduk = $this->isDatabaseGabungan() ? (new PendudukService)->cekPendudukNikTanggalLahir($request->input('nik'), $request->input('tanggal_lahir')) : Penduduk::where('nik', $komplain->nik)->first(); - + $komplain->komplain_id = Komplain::generateID(); $komplain->slug = str_slug($komplain->judul) . '-' . $komplain->komplain_id; $komplain->status = 'REVIEW'; @@ -211,6 +191,7 @@ public function store(Request $request) * Display the specified resource. * * @param int slug + * * @return Response */ public function show($slug) @@ -292,5 +273,4 @@ public function getJawabans(Request $request) return view('pages.komplain.jawabans', compact('jawabans', 'komplain'))->render(); } - } diff --git a/app/Http/Controllers/Informasi/PotensiController.php b/app/Http/Controllers/Informasi/PotensiController.php index a2498f43ff..c3110b4e3f 100644 --- a/app/Http/Controllers/Informasi/PotensiController.php +++ b/app/Http/Controllers/Informasi/PotensiController.php @@ -31,11 +31,11 @@ namespace App\Http\Controllers\Informasi; +use App\Http\Controllers\Controller; +use App\Http\Requests\PotensiRequest; use App\Models\Potensi; use App\Models\TipePotensi; use App\Traits\HandlesFileUpload; -use App\Http\Controllers\Controller; -use App\Http\Requests\PotensiRequest; use Illuminate\Support\Facades\Log; use Yajra\DataTables\Facades\DataTables; @@ -103,6 +103,10 @@ public function store(PotensiRequest $request) $this->handleFileUpload($request, $input, 'file_gambar', 'potensi_kecamatan'); + if ($request->hasFile('file_gambar')) { + $input['mime_type'] = $request->file('file_gambar')->getClientMimeType(); + } + Potensi::create($input); } catch (\Exception $e) { Log::error('Potensi creation failed', [ @@ -139,6 +143,10 @@ public function update(PotensiRequest $request, Potensi $potensi) $this->handleFileUpload($request, $input, 'file_gambar', 'potensi_kecamatan'); + if ($request->hasFile('file_gambar')) { + $input['mime_type'] = $request->file('file_gambar')->getClientMimeType(); + } + $potensi->update($input); } catch (\Exception $e) { Log::error('Potensi update failed', [ @@ -175,25 +183,13 @@ public function destroy(Potensi $potensi) public function download(Potensi $potensi) { try { - $path = $potensi->file_gambar; - if (empty($path)) { - return back()->with('error', 'Dokumen potensi tidak ditemukan'); - } + $filePath = $this->resolveSecureFilePath($potensi->file_gambar); - if (file_exists(public_path($path))) { - return response()->download(public_path($path)); - } - - if (file_exists(base_path('public/' . $path))) { - return response()->download(base_path('public/' . $path)); + if (!$filePath) { + return back()->with('error', 'Dokumen potensi tidak ditemukan'); } - $storagePath = storage_path('app/public/' . str_replace('storage/', '', $path)); - if (file_exists($storagePath)) { - return response()->download($storagePath); - } - - return back()->with('error', 'Dokumen potensi tidak ditemukan'); + return response()->download($filePath); } catch (\Exception $e) { Log::error('Potensi download failed', [ 'error' => $e->getMessage(), diff --git a/app/Http/Controllers/Informasi/ProsedurController.php b/app/Http/Controllers/Informasi/ProsedurController.php index 73ec08a3bb..5d4ff1b84c 100644 --- a/app/Http/Controllers/Informasi/ProsedurController.php +++ b/app/Http/Controllers/Informasi/ProsedurController.php @@ -31,12 +31,12 @@ namespace App\Http\Controllers\Informasi; -use App\Models\Prosedur; -use Yajra\DataTables\DataTables; -use App\Traits\HandlesFileUpload; use App\Http\Controllers\Controller; use App\Http\Requests\ProsedurRequest; +use App\Models\Prosedur; +use App\Traits\HandlesFileUpload; use Illuminate\Support\Facades\Log; +use Yajra\DataTables\DataTables; class ProsedurController extends Controller { @@ -98,7 +98,6 @@ public function getDataProsedur() } $data['download_url'] = auth()->user()->can('access.informasi.prosedur.export') ? route('informasi.prosedur.download', $row->id) : null; - $data['preview_url'] = auth()->user()->can('access.informasi.prosedur.view') ? route('informasi.prosedur.preview', $row->id) : null; return view('forms.aksi', $data); }) @@ -196,7 +195,13 @@ public function destroy(Prosedur $prosedur) public function download(Prosedur $prosedur) { try { - return response()->download($prosedur->file_prosedur); + $filePath = $this->resolveSecureFilePath($prosedur->file_prosedur); + + if (!$filePath) { + return back()->with('error', 'Dokumen prosedur tidak ditemukan'); + } + + return response()->download($filePath); } catch (\Exception $e) { Log::error('Prosedur download failed', [ 'error' => $e->getMessage(), @@ -207,28 +212,4 @@ public function download(Prosedur $prosedur) return back()->with('error', 'Dokumen prosedur tidak ditemukan'); } } - - public function preview(Prosedur $prosedur) - { - $path = $prosedur->file_prosedur; - - if (empty($path)) { - abort(404, 'File tidak ditemukan.'); - } - - if (file_exists(public_path($path))) { - return response()->file(public_path($path)); - } - - if (file_exists(base_path('public/' . $path))) { - return response()->file(base_path('public/' . $path)); - } - - $storagePath = storage_path('app/public/' . str_replace('storage/', '', $path)); - if (file_exists($storagePath)) { - return response()->file($storagePath); - } - - abort(404, 'File tidak ditemukan.'); - } } diff --git a/app/Http/Controllers/Informasi/RegulasiController.php b/app/Http/Controllers/Informasi/RegulasiController.php index 580cd12662..d5785a5304 100644 --- a/app/Http/Controllers/Informasi/RegulasiController.php +++ b/app/Http/Controllers/Informasi/RegulasiController.php @@ -31,12 +31,12 @@ namespace App\Http\Controllers\Informasi; -use App\Models\Regulasi; -use App\Traits\HandlesFileUpload; use App\Http\Controllers\Controller; use App\Http\Requests\RegulasiRequest; use App\Http\Requests\RegulasiUpdateRequest; +use App\Models\Regulasi; use App\Models\TipeRegulasi; +use App\Traits\HandlesFileUpload; use Illuminate\Support\Facades\Log; use Yajra\DataTables\Facades\DataTables; @@ -124,7 +124,6 @@ public function update(RegulasiUpdateRequest $request, Regulasi $regulasi) $input['profil_id'] = $this->profil->id; $this->handleFileUpload($request, $input, 'file_regulasi', 'regulasi'); - if ($request->hasFile('file_regulasi')) { $input['mime_type'] = $request->file('file_regulasi')->getMimeType(); } @@ -167,7 +166,13 @@ public function destroy(Regulasi $regulasi) public function download(Regulasi $regulasi) { try { - return response()->download($regulasi->file_regulasi); + $filePath = $this->resolveSecureFilePath($regulasi->file_regulasi); + + if (!$filePath) { + return back()->with('error', 'Dokumen regulasi tidak ditemukan'); + } + + return response()->download($filePath); } catch (\Exception $e) { Log::error('Regulasi download failed', [ 'error' => $e->getMessage(), diff --git a/app/Http/Controllers/LogViewerController.php b/app/Http/Controllers/LogViewerController.php index 0f5e780804..b8ee2ffe90 100644 --- a/app/Http/Controllers/LogViewerController.php +++ b/app/Http/Controllers/LogViewerController.php @@ -81,11 +81,11 @@ public function index() { $folderFiles = []; if ($this->request->input('f')) { - $this->log_viewer->setFolder(Crypt::decrypt($this->request->input('f'))); + $this->log_viewer->setFolder(Crypt::decryptString($this->request->input('f'))); $folderFiles = $this->log_viewer->getFolderFiles(true); } if ($this->request->input('l')) { - $this->log_viewer->setFile(Crypt::decrypt($this->request->input('l'))); + $this->log_viewer->setFile(Crypt::decryptString($this->request->input('l'))); } if ($early_return = $this->earlyReturn()) { @@ -143,7 +143,7 @@ public function index() private function earlyReturn() { if ($this->request->input('f')) { - $this->log_viewer->setFolder(Crypt::decrypt($this->request->input('f'))); + $this->log_viewer->setFolder(Crypt::decryptString($this->request->input('f'))); } if ($this->request->input('dl')) { @@ -178,7 +178,7 @@ private function earlyReturn() */ private function pathFromInput($input_string) { - return $this->log_viewer->pathToLogFile(Crypt::decrypt($this->request->input($input_string))); + return $this->log_viewer->pathToLogFile(Crypt::decryptString($this->request->input($input_string))); } /** diff --git a/app/Http/Controllers/Pesan/PesanController.php b/app/Http/Controllers/Pesan/PesanController.php index ea7046425a..d7a4f8b29b 100644 --- a/app/Http/Controllers/Pesan/PesanController.php +++ b/app/Http/Controllers/Pesan/PesanController.php @@ -32,16 +32,23 @@ namespace App\Http\Controllers\Pesan; use App\Http\Controllers\Controller; +use App\Http\Requests\FilterPesanRequest; +use App\Http\Requests\ReplyPesanRequest; +use App\Http\Requests\SetArsipPesanRequest; +use App\Http\Requests\SetMultipleArsipPesanStatusRequest; +use App\Http\Requests\SetMultipleReadPesanStatusRequest; +use App\Http\Requests\StoreComposePesanRequest; use App\Models\Pesan; use App\Models\PesanDetail; use App\Services\DesaService; -use Illuminate\Http\Request; +use Illuminate\Contracts\View\View; +use Illuminate\Http\RedirectResponse; use Illuminate\Support\Facades\DB; use Stevebauman\Purify\Facades\Purify; class PesanController extends Controller { - public function index(Request $request) + public function index(FilterPesanRequest $request): View { $data = collect([]); $data->put('page_title', 'Pesan'); @@ -72,31 +79,14 @@ public function index(Request $request) }) ->paginate(Pesan::PER_PAGE); - $list_desa = (new DesaService())->listDesa(); + $list_desa = (new DesaService())->listDesa(); $data->put('list_pesan', $pesan); $data->put('list_desa', $list_desa); return view('pesan.masuk.index', $data->all()); } - protected function loadCounter() - { - $counter_unread = Pesan::where([ - 'jenis' => Pesan::PESAN_MASUK, - 'diarsipkan' => Pesan::NON_ARSIP, - 'sudah_dibaca' => Pesan::BELUM_DIBACA])->count(); - $counter_unread_keluar = Pesan::where([ - 'jenis' => Pesan::PESAN_KELUAR, - 'diarsipkan' => Pesan::NON_ARSIP, - 'sudah_dibaca' => Pesan::BELUM_DIBACA])->count(); - - return [ - 'counter_unread' => $counter_unread, - 'counter_unread_keluar' => $counter_unread_keluar, - ]; - } - - public function loadPesanKeluar(Request $request) + public function loadPesanKeluar(FilterPesanRequest $request): View { $data = collect([]); $data->put('desa_id', null); @@ -119,16 +109,14 @@ public function loadPesanKeluar(Request $request) return $q->where('judul', 'LIKE', "%{$request->get('q')}%"); }) - ->paginate(Pesan::PER_PAGE); - - $list_desa = (new DesaService())->listDesa(); + ->paginate(Pesan::PER_PAGE); $data->put('list_pesan', $pesan); - $data->put('list_desa', $list_desa); + return view('pesan.keluar.index', $data->all()); } - public function loadPesanArsip(Request $request) + public function loadPesanArsip(FilterPesanRequest $request): View { $data = collect([]); $data->put('desa_id', null); @@ -159,7 +147,7 @@ public function loadPesanArsip(Request $request) return view('pesan.arsip.index', $data->all()); } - public function readPesan($id_pesan) + public function readPesan(int $id_pesan): View { $pesan = Pesan::findOrFail($id_pesan); if ($pesan->sudah_dibaca == Pesan::BELUM_DIBACA) { @@ -176,7 +164,7 @@ public function readPesan($id_pesan) return view('pesan.read_pesan', $data->all()); } - public function composePesan() + public function composePesan(): View { $data = collect([]); $data->put('page_title', 'Buat Pesan'); @@ -188,18 +176,9 @@ public function composePesan() return view('pesan.compose_pesan', $data->all()); } - /** - * @throws \Illuminate\Validation\ValidationException - */ - public function storeComposePesan(Request $request) + public function storeComposePesan(StoreComposePesanRequest $request): RedirectResponse { try { - $this->validate($request, [ - 'judul' => 'required', - 'das_data_desa_id' => 'required|'.(!$this->isDatabaseGabungan() ? 'exists:das_data_desa,desa_id' : ''), - 'text' => 'required', - ]); - DB::transaction(function () use ($request) { $desa = (new DesaService())->getDesaByKode($request->get('das_data_desa_id')); $id = Pesan::create([ @@ -224,7 +203,7 @@ public function storeComposePesan(Request $request) } } - public function setArsipPesan(Request $request) + public function setArsipPesan(SetArsipPesanRequest $request): RedirectResponse { $pesan = Pesan::findOrFail($request->get('id')); $pesan->diarsipkan = Pesan::MASUK_ARSIP; @@ -235,7 +214,7 @@ public function setArsipPesan(Request $request) return back()->withInput()->with('error', 'Pesan gagal diarsipkan!'); } - public function setMultipleReadPesanStatus(Request $request) + public function setMultipleReadPesanStatus(SetMultipleReadPesanStatusRequest $request): RedirectResponse { $array = json_decode($request->get('array_id')); $pesan = Pesan::whereIn('id', $array)->update([ @@ -249,7 +228,7 @@ public function setMultipleReadPesanStatus(Request $request) return back()->withInput()->with('error', 'Pesan gagal ditandai!'); } - public function setMultipleArsipPesanStatus(Request $request) + public function setMultipleArsipPesanStatus(SetMultipleArsipPesanStatusRequest $request): RedirectResponse { $array = json_decode($request->get('array_id')); $pesan = Pesan::whereIn('id', $array)->update([ @@ -263,7 +242,7 @@ public function setMultipleArsipPesanStatus(Request $request) return back()->withInput()->with('error', 'Pesan gagal diarsipkan!'); } - public function replyPesan(Request $request) + public function replyPesan(ReplyPesanRequest $request): RedirectResponse { $pesan = PesanDetail::create([ 'pesan_id' => $request->get('id'), @@ -278,4 +257,21 @@ public function replyPesan(Request $request) return back()->withInput()->with('error', 'Pesan gagal dikirim!'); } + + protected function loadCounter(): array + { + $counter_unread = Pesan::where([ + 'jenis' => Pesan::PESAN_MASUK, + 'diarsipkan' => Pesan::NON_ARSIP, + 'sudah_dibaca' => Pesan::BELUM_DIBACA])->count(); + $counter_unread_keluar = Pesan::where([ + 'jenis' => Pesan::PESAN_KELUAR, + 'diarsipkan' => Pesan::NON_ARSIP, + 'sudah_dibaca' => Pesan::BELUM_DIBACA])->count(); + + return [ + 'counter_unread' => $counter_unread, + 'counter_unread_keluar' => $counter_unread_keluar, + ]; + } } diff --git a/app/Http/Controllers/Publikasi/GaleriController.php b/app/Http/Controllers/Publikasi/GaleriController.php index 6599741916..ee099435d9 100644 --- a/app/Http/Controllers/Publikasi/GaleriController.php +++ b/app/Http/Controllers/Publikasi/GaleriController.php @@ -105,25 +105,28 @@ public function store(GaleriRequest $request) $imageNames = []; if ($request->hasFile('gambar')) { foreach ($request->file('gambar') as $file) { - $path = Storage::putFile('public/publikasi/galeri', $file); + $path = Storage::disk('public')->putFile('publikasi/galeri', $file); $imageNames[] = basename($path); } $input['gambar'] = $imageNames; + } else { + $input['gambar'] = null; } + $input['link'] = $input['jenis'] == 'file' ? null : ($input['link'] ?? null); $input['album_id'] = Session::get('album_id'); Galeri::create($input); - return redirect()->route('publikasi.galeri.index', Session::get('album_id'))->with('success', 'Album berhasil disimpan!'); + return redirect()->route('publikasi.galeri.index', Session::get('album_id'))->with('success', 'Galeri berhasil disimpan!'); } catch (\Exception $e) { Log::error('Galeri creation failed', [ 'error' => $e->getMessage(), 'user_id' => auth()->id(), ]); - return back()->withInput()->with('error', 'Simpan album gagal!'); + return back()->withInput()->with('error', 'Simpan galeri gagal!'); } } @@ -166,14 +169,18 @@ public function update(GaleriRequest $request, Galeri $galeri) $imageNames = []; if ($request->hasFile('gambar')) { foreach ($request->file('gambar') as $file) { - $path = Storage::putFile('public/publikasi/galeri', $file); + $path = Storage::disk('public')->putFile('publikasi/galeri', $file); $imageNames[] = basename($path); } $input['gambar'] = $imageNames; + } elseif (($input['jenis'] ?? null) == 'file') { + unset($input['gambar']); + } else { + $input['gambar'] = null; } - $input['link'] = $input['jenis'] == 'file' ? null : $input['link']; + $input['link'] = ($input['jenis'] ?? null) == 'file' ? null : ($input['link'] ?? null); $galeri->update($input); } catch (\Exception $e) { @@ -183,7 +190,7 @@ public function update(GaleriRequest $request, Galeri $galeri) 'galeri_id' => $galeri->id, ]); - return back()->withInput()->with('error', 'Galeri gagal dihapus!'); + return back()->withInput()->with('error', 'Galeri gagal diubah!'); } return redirect()->route('publikasi.galeri.index', Session::get('album_id'))->with('success', 'Galeri berhasil diubah!'); diff --git a/app/Http/Middleware/SecurityHeaders.php b/app/Http/Middleware/SecurityHeaders.php index 3fa6c4b3b1..c7441b2932 100644 --- a/app/Http/Middleware/SecurityHeaders.php +++ b/app/Http/Middleware/SecurityHeaders.php @@ -41,7 +41,6 @@ class SecurityHeaders * Handle an incoming request. * * @param \Illuminate\Http\Request $request - * @return mixed */ public function handle($request, Closure $next) { @@ -51,8 +50,7 @@ public function handle($request, Closure $next) if (app()->environment('production')) { $localDomain = env('APP_URL', 'http://localhost'); $urlDatabaseGabungan = config('setting.api_server_database_gabungan'); - $response->headers->set('X-Content-Type-Options', 'nosniff'); - $response->headers->set('Content-Security-Policy', "default-src 'self';script-src 'self' https://pantau.opensid.my.id/ https://cdnjs.cloudflare.com/ajax/libs/tinymce/ https://cdn.jsdelivr.net/npm/ https://cdnjs.cloudflare.com/ajax/libs/numeral.js/2.0.6/numeral.min.js https://website-widgets.pages.dev/dist/sienna.min.js platform.twitter.com unpkg.com 'unsafe-inline' 'unsafe-eval';style-src 'self' https://www.tiny.cloud/ http://www.tinymce.com/css/codepen.min.css https://cdnjs.cloudflare.com/ajax/libs/tinymce/ https://cdn.jsdelivr.net/npm/ fonts.googleapis.com unpkg.com 'unsafe-inline';img-src 'self' * data:;font-src 'self' https://cdnjs.cloudflare.com/ajax/libs/tinymce/ https://cdn.jsdelivr.net/npm/ fonts.gstatic.com data:;connect-src 'self' ".$urlDatabaseGabungan." https://pantau.opensid.my.id/ ;media-src 'self';frame-src 'self' platform.twitter.com github.com *.youtube.com *.vimeo.com *.opensid.my.id;object-src 'none';base-uri 'self';report-uri"); + $response->headers->set('Content-Security-Policy', "default-src 'self';script-src 'self' https://pantau.opensid.my.id/ https://cdnjs.cloudflare.com/ajax/libs/tinymce/ https://cdn.jsdelivr.net/npm/ https://cdnjs.cloudflare.com/ajax/libs/numeral.js/2.0.6/numeral.min.js https://website-widgets.pages.dev/dist/sienna.min.js platform.twitter.com unpkg.com 'unsafe-inline' 'unsafe-eval';style-src 'self' https://www.tiny.cloud/ http://www.tinymce.com/css/codepen.min.css https://cdnjs.cloudflare.com/ajax/libs/tinymce/ https://cdn.jsdelivr.net/npm/ fonts.googleapis.com unpkg.com 'unsafe-inline';img-src 'self' * data: blob:;font-src 'self' https://cdnjs.cloudflare.com/ajax/libs/tinymce/ https://cdn.jsdelivr.net/npm/ fonts.gstatic.com data:;connect-src 'self' ".$urlDatabaseGabungan." https://pantau.opensid.my.id/ ;media-src 'self';frame-src 'self' blob: data: platform.twitter.com github.com *.youtube.com *.vimeo.com *.opensid.my.id;object-src 'self' blob:;frame-ancestors 'self';base-uri 'self';"); } return $response; diff --git a/app/Http/Requests/FilterPesanRequest.php b/app/Http/Requests/FilterPesanRequest.php new file mode 100644 index 0000000000..9c1b0af077 --- /dev/null +++ b/app/Http/Requests/FilterPesanRequest.php @@ -0,0 +1,51 @@ + ['nullable', 'integer'], + 'q' => ['nullable', 'string'], + 'sudahdibaca' => ['nullable', 'in:0,1'], + ]; + } +} diff --git a/app/Http/Requests/FrontEnd/SistemKomplainRequest.php b/app/Http/Requests/FrontEnd/SistemKomplainRequest.php new file mode 100644 index 0000000000..8576ccc69c --- /dev/null +++ b/app/Http/Requests/FrontEnd/SistemKomplainRequest.php @@ -0,0 +1,118 @@ + ['required', 'numeric', $this->nikRule()], + 'judul' => 'required|string|max:255', + 'kategori' => 'required', + 'laporan' => 'required|string', + 'tanggal_lahir' => 'required|date', + 'captcha' => 'required|captcha', + 'lampiran1' => 'file|mimes:jpeg,png,jpg,gif,svg' . $maxRule . '|valid_file', + 'lampiran2' => 'file|mimes:jpeg,png,jpg,gif,svg' . $maxRule . '|valid_file', + 'lampiran3' => 'file|mimes:jpeg,png,jpg,gif,svg' . $maxRule . '|valid_file', + 'lampiran4' => 'file|mimes:jpeg,png,jpg,gif,svg' . $maxRule . '|valid_file', + ]; + } + + /** + * Get custom messages for validator errors. + * + * @return array + */ + public function messages() + { + return [ + 'captcha.captcha' => 'Invalid captcha code.', + ]; + } + + /** + * Determine if penduduk database gabungan is active. + * + * @return bool + */ + protected function isDatabaseGabungan() + { + return SettingAplikasi::where('key', 'sinkronisasi_database_gabungan')->value('value') === '1'; + } + + /** + * NIK validation rule. + * + * When database gabungan is active, validate against the penduduk service, + * otherwise validate against the local penduduk table. + */ + protected function nikRule() + { + return function ($attribute, $value, $fail) { + $tanggalLahir = $this->input('tanggal_lahir'); + + if ($this->isDatabaseGabungan()) { + $valid = (new PendudukService)->cekPendudukNikTanggalLahir($value, $tanggalLahir); + } else { + $valid = Penduduk::where('nik', $value)->where('tanggal_lahir', $tanggalLahir)->exists(); + } + + if (!$valid) { + $fail('NIK tidak ditemukan atau tidak sesuai dengan tanggal lahir.'); + } + }; + } +} diff --git a/app/Http/Requests/GaleriRequest.php b/app/Http/Requests/GaleriRequest.php index bed893553a..430ce60bc9 100644 --- a/app/Http/Requests/GaleriRequest.php +++ b/app/Http/Requests/GaleriRequest.php @@ -2,6 +2,7 @@ namespace App\Http\Requests; +use App\Services\FileUploadService; use Illuminate\Foundation\Http\FormRequest; class GaleriRequest extends FormRequest @@ -23,11 +24,15 @@ public function authorize() */ public function rules() { + $isUrl = $this->input('jenis') === 'url'; + $maxRule = FileUploadService::isLimitEnabled() ? '|max:1024' : ''; + return [ 'judul' => 'required|string|max:191', - 'jenis' => 'required', - 'link' => $this->input('jenis') == 'url' ? 'required|max:255' : 'nullable', - 'gambar.*' => $this->input('jenis') == 'url' ? 'nullable' : 'required|image|mimes:jpg,jpeg,png|max:1024', + 'jenis' => 'required|in:file,url', + 'link' => $isUrl ? 'required|string|max:255' : 'nullable', + 'gambar' => (! $isUrl && $this->isMethod('post')) ? 'required|array' : 'nullable|array', + 'gambar.*' => $isUrl ? 'nullable' : 'required|image|mimes:jpg,jpeg,png' . $maxRule . '|valid_file', 'status' => 'required', ]; } diff --git a/app/Http/Requests/KirimBalasanRequest.php b/app/Http/Requests/KirimBalasanRequest.php new file mode 100644 index 0000000000..3222c562f5 --- /dev/null +++ b/app/Http/Requests/KirimBalasanRequest.php @@ -0,0 +1,25 @@ + 'required|string|max:255', + 'email' => 'required|email|max:255', + 'body' => 'required|string', + 'captcha_main' => 'required|captcha', + 'das_artikel_id' => 'required|exists:das_artikel,id', + 'comment_id' => 'required|exists:das_artikel_comment,id', + ]; + } +} diff --git a/app/Http/Requests/KirimKomentarRequest.php b/app/Http/Requests/KirimKomentarRequest.php new file mode 100644 index 0000000000..15a12539f2 --- /dev/null +++ b/app/Http/Requests/KirimKomentarRequest.php @@ -0,0 +1,24 @@ + 'required|string|max:255', + 'email' => 'required|email|max:255', + 'body' => 'required|string', + 'das_artikel_id' => 'required|exists:das_artikel,id', + 'captcha_main' => 'required|captcha', + ]; + } +} diff --git a/app/Http/Requests/PotensiRequest.php b/app/Http/Requests/PotensiRequest.php index cdef15dea0..cba7f6789c 100644 --- a/app/Http/Requests/PotensiRequest.php +++ b/app/Http/Requests/PotensiRequest.php @@ -57,7 +57,7 @@ public function rules() 'nama_potensi' => 'required|string|max:200', 'deskripsi' => 'required|string', 'lokasi' => 'required|string|max:200', - 'file_gambar' => 'file|mimes:bmp,jpg,jpeg,gif,png' . (\App\Services\FileUploadService::isLimitEnabled() ? '|max:1024' : '') . '|valid_file', + 'file_gambar' => 'file|mimes:bmp,jpg,jpeg,gif,png,pdf' . (\App\Services\FileUploadService::isLimitEnabled() ? '|max:2048' : '') . '|valid_file', ]; } } diff --git a/app/Http/Requests/ReplyPesanRequest.php b/app/Http/Requests/ReplyPesanRequest.php new file mode 100644 index 0000000000..40345edc16 --- /dev/null +++ b/app/Http/Requests/ReplyPesanRequest.php @@ -0,0 +1,50 @@ + ['required', 'integer', 'exists:das_pesan,id'], + 'text' => ['required', 'string'], + ]; + } +} diff --git a/app/Http/Requests/SetArsipPesanRequest.php b/app/Http/Requests/SetArsipPesanRequest.php new file mode 100644 index 0000000000..7eed898f47 --- /dev/null +++ b/app/Http/Requests/SetArsipPesanRequest.php @@ -0,0 +1,49 @@ + ['required', 'integer', 'exists:das_pesan,id'], + ]; + } +} diff --git a/app/Http/Requests/SetMultipleArsipPesanStatusRequest.php b/app/Http/Requests/SetMultipleArsipPesanStatusRequest.php new file mode 100644 index 0000000000..6e716ce60a --- /dev/null +++ b/app/Http/Requests/SetMultipleArsipPesanStatusRequest.php @@ -0,0 +1,49 @@ + ['required', 'string'], + ]; + } +} diff --git a/app/Http/Requests/SetMultipleReadPesanStatusRequest.php b/app/Http/Requests/SetMultipleReadPesanStatusRequest.php new file mode 100644 index 0000000000..b0376710a8 --- /dev/null +++ b/app/Http/Requests/SetMultipleReadPesanStatusRequest.php @@ -0,0 +1,49 @@ + ['required', 'string'], + ]; + } +} diff --git a/app/Http/Requests/StoreComposePesanRequest.php b/app/Http/Requests/StoreComposePesanRequest.php new file mode 100644 index 0000000000..ad1c090460 --- /dev/null +++ b/app/Http/Requests/StoreComposePesanRequest.php @@ -0,0 +1,56 @@ + ['required', 'string'], + 'das_data_desa_id' => ['required', Rule::when(! $isDatabaseGabungan, ['exists:das_data_desa,desa_id'])], + 'text' => ['required', 'string'], + ]; + } +} diff --git a/app/Http/Requests/StoreLembagaAnggotaRequest.php b/app/Http/Requests/StoreLembagaAnggotaRequest.php index 17be9f95f6..f60fd0a0de 100644 --- a/app/Http/Requests/StoreLembagaAnggotaRequest.php +++ b/app/Http/Requests/StoreLembagaAnggotaRequest.php @@ -3,6 +3,7 @@ namespace App\Http\Requests; use App\Models\Lembaga; +use App\Models\SettingAplikasi; use Illuminate\Validation\Rule; use Illuminate\Foundation\Http\FormRequest; @@ -25,9 +26,22 @@ public function authorize() */ public function rules() { - $slug = $this->route('slug'); + $slug = $this->route('slug'); $lembaga = Lembaga::where('slug', $slug)->first(); - + + if (SettingAplikasi::where('key', 'sinkronisasi_database_gabungan')->value('value') === '1') { + return [ + 'penduduk_id' => 'nullable|integer', + 'penduduk_id_gabungan' => 'required|integer', + 'no_anggota' => [ + 'required', + Rule::unique('das_lembaga_anggota', 'no_anggota') + ->where('lembaga_id', $lembaga->id), + ], + 'jabatan_id' => 'required|in:1,2,3,4,5', + ]; + } + return [ 'penduduk_id' => 'required|exists:das_penduduk,id', 'no_anggota' => [ @@ -35,7 +49,7 @@ public function rules() Rule::unique('das_lembaga_anggota', 'no_anggota') ->where('lembaga_id', $lembaga->id), ], - 'jabatan_id' => 'required|in:1,2,3,4,5', // Kode jabatan harus 1-5 + 'jabatan_id' => 'required|in:1,2,3,4,5', ]; } @@ -48,6 +62,7 @@ public function attributes() { return [ 'penduduk_id' => 'Nama Anggota', + 'penduduk_id_gabungan' => 'Nama Anggota', 'no_anggota' => 'Nomor Anggota', 'jabatan_id' => 'Jabatan', ]; diff --git a/app/Http/Requests/StoreLembagaRequest.php b/app/Http/Requests/StoreLembagaRequest.php index 0bd7d662c3..32a7b49884 100644 --- a/app/Http/Requests/StoreLembagaRequest.php +++ b/app/Http/Requests/StoreLembagaRequest.php @@ -2,33 +2,31 @@ namespace App\Http\Requests; +use App\Models\SettingAplikasi; use Illuminate\Foundation\Http\FormRequest; class StoreLembagaRequest extends FormRequest { - /** - * Determine if the user is authorized to make this request. - * - * @return bool - */ public function authorize() { return true; } - /** - * Get the validation rules that apply to the request. - * - * @return array - */ public function rules() { - return [ + $rules = [ 'nama' => 'required|string|max:255', 'kode' => 'required|string|max:255|unique:das_lembaga,kode', 'lembaga_kategori_id' => 'required|exists:das_lembaga_kategori,id', - 'penduduk_id' => 'required|exists:das_penduduk,id', ]; + + if (SettingAplikasi::where('key', 'sinkronisasi_database_gabungan')->value('value') === '1') { + $rules['penduduk_id_gabungan'] = 'required|integer'; + } else { + $rules['penduduk_id'] = 'required|exists:das_penduduk,id'; + } + + return $rules; } /** @@ -43,6 +41,7 @@ public function attributes() 'kode' => 'Kode Lembaga', 'lembaga_kategori_id' => 'Kategori Lembaga', 'penduduk_id' => 'Ketua Lembaga', + 'penduduk_id_gabungan' => 'Ketua Lembaga', ]; } } diff --git a/app/Http/Requests/SuplemenRequest.php b/app/Http/Requests/SuplemenRequest.php new file mode 100644 index 0000000000..49daf802fd --- /dev/null +++ b/app/Http/Requests/SuplemenRequest.php @@ -0,0 +1,57 @@ + ['required', 'string', 'max:255'], + 'sasaran' => ['required', 'in:1,2'], + 'keterangan' => ['nullable', 'string'], + ]; + } +} diff --git a/app/Http/Requests/SuplemenTerdataRequest.php b/app/Http/Requests/SuplemenTerdataRequest.php new file mode 100644 index 0000000000..0d02e7f4fc --- /dev/null +++ b/app/Http/Requests/SuplemenTerdataRequest.php @@ -0,0 +1,93 @@ + ['required', 'exists:das_suplemen,id'], + 'desa_id' => ['required', 'string', 'max:13', 'not_in:Semua'], + 'penduduk_id' => ['nullable', 'integer'], + 'penduduk_id_gabungan' => ['nullable', 'integer'], + 'keterangan' => ['nullable', 'string'], + ]; + } + + /** + * Pastikan hanya salah satu sumber penduduk yang diisi: + * penduduk lokal (penduduk_id) atau penduduk dari database gabungan (penduduk_id_gabungan). + */ + public function withValidator(\Illuminate\Validation\Validator $validator): void + { + $validator->after(function ($validator) { + $hasLocal = $this->filled('penduduk_id'); + $hasGabungan = $this->filled('penduduk_id_gabungan'); + + if (! $hasLocal && ! $hasGabungan) { + $validator->errors()->add('penduduk_id', 'isian warga atau penduduk wajib diisi'); + } + + if ($hasLocal && $hasGabungan) { + $validator->errors()->add('penduduk_id_gabungan', 'hanya boleh memilih salah satu, penduduk lokal atau penduduk database gabungan'); + } + }); + } + + /** + * Get the error messages for the defined validation rules. + */ + public function messages(): array + { + $sebutanDesa = config('setting.sebutan_desa'); + + return [ + 'penduduk_id.required' => 'isian warga atau penduduk wajib diisi', + 'desa_id.required' => "isian {$sebutanDesa} wajib diisi", + 'desa_id.not_in' => "isian {$sebutanDesa} wajib diisi", + ]; + } +} diff --git a/app/Http/Requests/UpdateAKIAKBRequest.php b/app/Http/Requests/UpdateAKIAKBRequest.php new file mode 100644 index 0000000000..a5fb7cb4ec --- /dev/null +++ b/app/Http/Requests/UpdateAKIAKBRequest.php @@ -0,0 +1,29 @@ + 'required|integer', + 'akb' => 'required|integer', + ]; + } + + public function messages() + { + return [ + 'aki.required' => 'Jumlah AKI wajib diisi.', + 'akb.required' => 'Jumlah AKB wajib diisi.', + ]; + } +} diff --git a/app/Http/Requests/UpdateFasilitasPaudRequest.php b/app/Http/Requests/UpdateFasilitasPaudRequest.php new file mode 100644 index 0000000000..d7fd87a0bb --- /dev/null +++ b/app/Http/Requests/UpdateFasilitasPaudRequest.php @@ -0,0 +1,35 @@ + 'required|integer', + 'jumlah_guru_paud' => 'required|integer', + 'jumlah_siswa_paud' => 'required|integer', + 'semester' => 'required|integer', + 'tahun' => 'required|integer', + ]; + } + + public function messages() + { + return [ + 'jumlah_paud.required' => 'Jumlah PAUD wajib diisi.', + 'jumlah_guru_paud.required' => 'Jumlah guru PAUD wajib diisi.', + 'jumlah_siswa_paud.required' => 'Jumlah siswa PAUD wajib diisi.', + 'semester.required' => 'Semester wajib diisi.', + 'tahun.required' => 'Tahun wajib diisi.', + ]; + } +} diff --git a/app/Http/Requests/UpdateLembagaAnggotaRequest.php b/app/Http/Requests/UpdateLembagaAnggotaRequest.php index 6a05da04a5..4bf1a75194 100644 --- a/app/Http/Requests/UpdateLembagaAnggotaRequest.php +++ b/app/Http/Requests/UpdateLembagaAnggotaRequest.php @@ -3,6 +3,7 @@ namespace App\Http\Requests; use App\Models\LembagaAnggota; +use App\Models\SettingAplikasi; use Illuminate\Foundation\Http\FormRequest; class UpdateLembagaAnggotaRequest extends FormRequest @@ -25,22 +26,23 @@ public function authorize() public function rules() { $rules = [ - // 'no_anggota' => 'required|unique:das_lembaga_anggota,no_anggota', // pastikan id penduduk ada dalam tabel - 'jabatan_id' => 'required|in:1,2,3,4,5', // kode jabatan harus 1-5 + 'jabatan_id' => 'required|in:1,2,3,4,5', ]; $id = $this->route('id'); - // cek no_anggota berdasarkan id $anggota = LembagaAnggota::findOrFail($id); - - // jika inputan nomor anggota tidak sama dengan data no_anggota - if($this->input('no_anggota') !== $anggota->no_anggota){ + + if ($this->input('no_anggota') !== $anggota->no_anggota) { $rules['no_anggota'] = 'required|unique:das_lembaga_anggota,no_anggota'; } - return $rules; + if (SettingAplikasi::where('key', 'sinkronisasi_database_gabungan')->value('value') === '1') { + $rules['penduduk_id'] = 'nullable|integer'; + $rules['penduduk_id_gabungan'] = 'required|integer'; + } + return $rules; } /** @@ -51,6 +53,8 @@ public function rules() public function attributes() { return [ + 'penduduk_id' => 'Nama Anggota', + 'penduduk_id_gabungan' => 'Nama Anggota', 'no_anggota' => 'Nomor Anggota', 'jabatan_id' => 'Jabatan', ]; diff --git a/app/Http/Requests/UpdateLembagaRequest.php b/app/Http/Requests/UpdateLembagaRequest.php index 4d75072de9..b93b60cad8 100644 --- a/app/Http/Requests/UpdateLembagaRequest.php +++ b/app/Http/Requests/UpdateLembagaRequest.php @@ -3,43 +3,37 @@ namespace App\Http\Requests; use App\Models\Lembaga; +use App\Models\SettingAplikasi; use Illuminate\Foundation\Http\FormRequest; class UpdateLembagaRequest extends FormRequest { - /** - * Determine if the user is authorized to make this request. - * - * @return bool - */ public function authorize() { return true; } - /** - * Get the validation rules that apply to the request. - * - * @return array - */ public function rules() { $rules = [ 'nama' => 'required|string|max:255', 'lembaga_kategori_id' => 'required|exists:das_lembaga_kategori,id', - 'penduduk_id' => 'required|exists:das_penduduk,id', ]; - // Dapatkan id lembaga dari route $id = $this->route('id'); - - // Cek apakah field kode mengalami perubahan $lembaga = Lembaga::findOrFail($id); - + if ($this->input('kode') !== $lembaga->kode) { $rules['kode'] = 'required|string|max:255|unique:das_lembaga,kode'; } + if (SettingAplikasi::where('key', 'sinkronisasi_database_gabungan')->value('value') === '1') { + $rules['penduduk_id'] = 'nullable|integer'; + $rules['penduduk_id_gabungan'] = 'required|integer'; + } else { + $rules['penduduk_id'] = 'required|exists:das_penduduk,id'; + } + return $rules; } @@ -55,6 +49,7 @@ public function attributes() 'kode' => 'Kode Lembaga', 'lembaga_kategori_id' => 'Kategori Lembaga', 'penduduk_id' => 'Ketua Lembaga', + 'penduduk_id_gabungan' => 'Ketua Lembaga', ]; } } diff --git a/app/Http/Requests/UpdatePutusSekolahRequest.php b/app/Http/Requests/UpdatePutusSekolahRequest.php new file mode 100644 index 0000000000..eed6bca7ca --- /dev/null +++ b/app/Http/Requests/UpdatePutusSekolahRequest.php @@ -0,0 +1,45 @@ + 'required|integer', + 'anak_usia_paud' => 'required|integer', + 'siswa_sd' => 'required|integer', + 'anak_usia_sd' => 'required|integer', + 'siswa_smp' => 'required|integer', + 'anak_usia_smp' => 'required|integer', + 'siswa_sma' => 'required|integer', + 'anak_usia_sma' => 'required|integer', + 'semester' => 'required|integer', + 'tahun' => 'required|integer', + ]; + } + + public function messages() + { + return [ + 'siswa_paud.required' => 'Jumlah siswa PAUD wajib diisi.', + 'anak_usia_paud.required' => 'Jumlah anak usia PAUD wajib diisi.', + 'siswa_sd.required' => 'Jumlah siswa SD wajib diisi.', + 'anak_usia_sd.required' => 'Jumlah anak usia SD wajib diisi.', + 'siswa_smp.required' => 'Jumlah siswa SMP wajib diisi.', + 'anak_usia_smp.required' => 'Jumlah anak usia SMP wajib diisi.', + 'siswa_sma.required' => 'Jumlah siswa SMA wajib diisi.', + 'anak_usia_sma.required' => 'Jumlah anak usia SMA wajib diisi.', + 'semester.required' => 'Semester wajib diisi.', + 'tahun.required' => 'Tahun wajib diisi.', + ]; + } +} diff --git a/app/Http/Requests/UpdateToiletSanitasiRequest.php b/app/Http/Requests/UpdateToiletSanitasiRequest.php new file mode 100644 index 0000000000..545b15a8dc --- /dev/null +++ b/app/Http/Requests/UpdateToiletSanitasiRequest.php @@ -0,0 +1,29 @@ + 'required|integer', + 'sanitasi' => 'required|integer', + ]; + } + + public function messages() + { + return [ + 'toilet.required' => 'Jumlah toilet wajib diisi.', + 'sanitasi.required' => 'Jumlah sanitasi wajib diisi.', + ]; + } +} diff --git a/app/Listeners/ClearProsedurCacheListener.php b/app/Listeners/ClearProsedurCacheListener.php new file mode 100644 index 0000000000..9ec17c2dfe --- /dev/null +++ b/app/Listeners/ClearProsedurCacheListener.php @@ -0,0 +1,57 @@ +removeCachePrefix($prefix); + } catch (\Exception $e) { + Log::error('Exception occurred while clearing prosedur cache', [ + 'message' => $e->getMessage(), + ]); + } + } +} diff --git a/app/Models/Galeri.php b/app/Models/Galeri.php index 6189cf65d9..47e7a4a935 100644 --- a/app/Models/Galeri.php +++ b/app/Models/Galeri.php @@ -63,8 +63,21 @@ public function Album(): BelongsTo return $this->belongsTo(Album::class); } - protected function getGambarPathAttribute(){ - $gambar = $this->gambar[0]; - return $this->attributes['jenis'] == 'file' ? isThumbnail("publikasi/galeri/".$gambar) : asset('/img/no-image.png'); + public function getGambarPathAttribute(): string + { + if (($this->attributes['jenis'] ?? null) === 'file' && ! empty($this->gambar)) { + $gambar = is_array($this->gambar) ? ($this->gambar[0] ?? null) : $this->gambar; + if ($gambar) { + return isThumbnail('publikasi/galeri/' . $gambar); + } + } + + if (($this->attributes['jenis'] ?? null) === 'url' && ! empty($this->link)) { + if (preg_match('/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/ ]{11})/', $this->link, $matches)) { + return 'https://img.youtube.com/vi/' . $matches[1] . '/hqdefault.jpg'; + } + } + + return asset('/img/no-image.png'); } } diff --git a/app/Models/Lembaga.php b/app/Models/Lembaga.php index 12a8cb4583..c74100c757 100644 --- a/app/Models/Lembaga.php +++ b/app/Models/Lembaga.php @@ -31,6 +31,11 @@ public function penduduk() return $this->belongsTo(Penduduk::class, 'penduduk_id', 'id'); } + public function pendudukGabungan() + { + return $this->belongsTo(Penduduk::class, 'penduduk_id_gabungan', 'id'); + } + // Scope untuk membuat slug yang unik public static function generateUniqueSlug($nama) { diff --git a/app/Models/LembagaAnggota.php b/app/Models/LembagaAnggota.php index 8f57d0053e..cfd43179ef 100644 --- a/app/Models/LembagaAnggota.php +++ b/app/Models/LembagaAnggota.php @@ -18,12 +18,16 @@ public function lembaga() return $this->belongsTo(Lembaga::class, 'lembaga_id'); } - // Relasi ke model Penduduk (Many-to-One) public function penduduk() { return $this->belongsTo(Penduduk::class, 'penduduk_id'); } + public function pendudukGabungan() + { + return $this->belongsTo(Penduduk::class, 'penduduk_id_gabungan'); + } + // Menentukan kolom slug yang digunakan untuk pencarian di rute public function getRouteKeyName() { diff --git a/app/Models/Potensi.php b/app/Models/Potensi.php index cf5fd259b0..5ad9bde69f 100644 --- a/app/Models/Potensi.php +++ b/app/Models/Potensi.php @@ -41,13 +41,6 @@ class Potensi extends Model protected $table = 'das_potensi'; - protected static function booted(): void - { - static::created(fn (Potensi $potensi) => PotensiChanged::dispatch($potensi)); - static::updated(fn (Potensi $potensi) => PotensiChanged::dispatch($potensi)); - static::deleted(fn (Potensi $potensi) => PotensiChanged::dispatch($potensi)); - } - protected $fillable = [ 'kategori_id', 'nama_potensi', @@ -56,6 +49,7 @@ protected static function booted(): void 'long', 'lat', 'file_gambar', + 'mime_type', ]; /** @@ -71,4 +65,17 @@ public function tipe() { return $this->hasOne(TipePotensi::class, 'id', 'kategori_id'); } + + public function getIsPdfAttribute(): bool + { + return str_contains(strtolower($this->mime_type ?? ''), 'pdf') + || str_ends_with(strtolower($this->file_gambar ?? ''), '.pdf'); + } + + protected static function booted(): void + { + static::created(fn (Potensi $potensi) => PotensiChanged::dispatch($potensi)); + static::updated(fn (Potensi $potensi) => PotensiChanged::dispatch($potensi)); + static::deleted(fn (Potensi $potensi) => PotensiChanged::dispatch($potensi)); + } } diff --git a/app/Models/Prosedur.php b/app/Models/Prosedur.php index d1a618d313..06d94b80b7 100644 --- a/app/Models/Prosedur.php +++ b/app/Models/Prosedur.php @@ -31,19 +31,30 @@ namespace App\Models; +use App\Events\ProsedurChanged; use App\Traits\HandlesResourceDeletion; -use Illuminate\Database\Eloquent\Model; use Cviebrock\EloquentSluggable\Sluggable; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Model; class Prosedur extends Model { + use HandlesResourceDeletion; use HasFactory; use Sluggable; - use HandlesResourceDeletion; protected $table = 'das_prosedur'; + /** + * Register model lifecycle hooks. + */ + protected static function booted(): void + { + static::created(fn (Prosedur $prosedur) => ProsedurChanged::dispatch($prosedur)); + static::updated(fn (Prosedur $prosedur) => ProsedurChanged::dispatch($prosedur)); + static::deleted(fn (Prosedur $prosedur) => ProsedurChanged::dispatch($prosedur)); + } + protected $fillable = [ 'judul_prosedur', 'file_prosedur', @@ -72,5 +83,9 @@ public function sluggable(): array ]; } + public function getIsPdfAttribute(): bool + { + return str_contains(strtolower($this->mime_type ?? ''), 'pdf') + || str_ends_with(strtolower($this->file_prosedur ?? ''), '.pdf'); + } } - diff --git a/app/Models/Regulasi.php b/app/Models/Regulasi.php index db2a313e3f..542820c1ad 100644 --- a/app/Models/Regulasi.php +++ b/app/Models/Regulasi.php @@ -41,13 +41,6 @@ class Regulasi extends Model protected $table = 'das_regulasi'; - protected static function booted(): void - { - static::created(fn (Regulasi $regulasi) => RegulasiChanged::dispatch($regulasi)); - static::updated(fn (Regulasi $regulasi) => RegulasiChanged::dispatch($regulasi)); - static::deleted(fn (Regulasi $regulasi) => RegulasiChanged::dispatch($regulasi)); - } - protected $fillable = [ 'kecamatan_id', 'tipe_regulasi', @@ -70,4 +63,17 @@ public function tipe() { return $this->hasOne(TipeRegulasi::class, 'id', 'tipe_regulasi'); } + + public function getIsPdfAttribute(): bool + { + return str_contains(strtolower($this->mime_type ?? ''), 'pdf') + || str_ends_with(strtolower($this->file_regulasi ?? ''), '.pdf'); + } + + protected static function booted(): void + { + static::created(fn (Regulasi $regulasi) => RegulasiChanged::dispatch($regulasi)); + static::updated(fn (Regulasi $regulasi) => RegulasiChanged::dispatch($regulasi)); + static::deleted(fn (Regulasi $regulasi) => RegulasiChanged::dispatch($regulasi)); + } } diff --git a/app/Models/SuplemenTerdata.php b/app/Models/SuplemenTerdata.php index 4def4e50e0..b38be739b4 100644 --- a/app/Models/SuplemenTerdata.php +++ b/app/Models/SuplemenTerdata.php @@ -42,7 +42,9 @@ class SuplemenTerdata extends Model protected $fillable = [ 'suplemen_id', + 'desa_id', 'penduduk_id', + 'penduduk_id_gabungan', 'keterangan', ]; diff --git a/app/Observers/GaleriObserver.php b/app/Observers/GaleriObserver.php index 3509f8ddda..2892ff8fc9 100644 --- a/app/Observers/GaleriObserver.php +++ b/app/Observers/GaleriObserver.php @@ -30,15 +30,17 @@ public function updated(Galeri $galeri) if ($galeri->isDirty('gambar')) { // Ambil gambar lama dan baru dari database $oldImages = $galeri->getOriginal('gambar'); - $newImages = $galeri->gambar; + $newImages = $galeri->gambar ?? []; if (is_array($oldImages)) { // Cari gambar yang ada di array lama tapi tidak ada di array baru - $deletedImages = array_diff($oldImages, $newImages); + $deletedImages = array_diff($oldImages, is_array($newImages) ? $newImages : []); // Hapus setiap gambar yang sudah tidak ada di array baru foreach ($deletedImages as $image) { - Storage::disk('public')->delete($image); + if ($image) { + Storage::disk('public')->delete('publikasi/galeri/' . $image); + } } } } @@ -59,10 +61,9 @@ public function deleted(Galeri $galeri) if (is_array($images)) { foreach ($images as $image) { - // Cek jika file ada - if (Storage::disk('public')->exists($image)) { + if ($image && Storage::disk('public')->exists('publikasi/galeri/' . $image)) { // Hapus file gambar dari storage - Storage::disk('public')->delete($image); + Storage::disk('public')->delete('publikasi/galeri/' . $image); } } } diff --git a/app/Repositories/PotensiApiRepository.php b/app/Repositories/PotensiApiRepository.php index f159cabf64..21557cd6a1 100644 --- a/app/Repositories/PotensiApiRepository.php +++ b/app/Repositories/PotensiApiRepository.php @@ -20,6 +20,7 @@ public function __construct(Potensi $model) AllowedFilter::exact('kategori_id'), AllowedFilter::exact('id'), AllowedFilter::exact('tipe.id'), + AllowedFilter::exact('tipe.slug'), ]; $this->allowedSorts = [ 'id', diff --git a/app/Services/BaseApiService.php b/app/Services/BaseApiService.php index c43f092e57..13ac15f86d 100644 --- a/app/Services/BaseApiService.php +++ b/app/Services/BaseApiService.php @@ -5,20 +5,25 @@ use App\Models\SettingAplikasi; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; -use Illuminate\Support\Facades\Session; class BaseApiService { protected $settings; + protected $useDatabaseGabungan; + protected $header; + protected $baseUrl; + protected $kodeKecamatan; + private $fullResponse = false; + public function __construct() { - $this->settings = SettingAplikasi::whereIn('key', ['api_server_database_gabungan', 'api_key_database_gabungan', 'sinkronisasi_database_gabungan'])->pluck('value', 'key'); - + $this->settings = SettingAplikasi::whereIn('key', ['api_server_database_gabungan', 'api_key_database_gabungan', 'sinkronisasi_database_gabungan'])->pluck('value', 'key'); + // Allow mock via config for testing if (config()->has('api_server_database_gabungan')) { $this->settings['api_server_database_gabungan'] = config('api_server_database_gabungan'); @@ -37,30 +42,36 @@ public function __construct() 'Authorization' => 'Bearer ' . ($this->settings['api_key_database_gabungan'] ?? ''), ]; $this->baseUrl = !empty($this->settings['api_server_database_gabungan']) ? $this->settings['api_server_database_gabungan'] : 'http://localhost'; - $this->kodeKecamatan = str_replace('.','',config('profil.kecamatan_id')); + $this->kodeKecamatan = str_replace('.','',config('profil.kecamatan_id')); } /** - * General API Call Method + * General API Call Method. */ - protected function apiRequest(string $endpoint, array $params = []) - { + protected function apiRequest(string $endpoint, array $params = [], string $method = 'GET') + { try { // Buat permintaan API dengan Header dan Parameter - $response = Http::withHeaders($this->header)->get($this->baseUrl . $endpoint, $params); + $response = Http::withHeaders($this->header)->{$this->resolveMethod($method)}($this->baseUrl . $endpoint, $params); session()->forget('error_api'); + + // Jika response gagal (non-2xx), kembalikan array kosong + if (!$response->successful()) { + return []; + } + $jsonResponse = $response->json(); - + if($this->isFullResponse()) { // Jika full response, kembalikan seluruh response return $jsonResponse; } - + // Return JSON hasil, cek apakah ada key 'data', jika tidak ada kembalikan seluruh response - if (isset($jsonResponse['data'])) { + if (array_key_exists('data', $jsonResponse)) { return $jsonResponse['data']; } - + return $jsonResponse; } catch (\Exception $e) { if (app()->environment('testing')) { @@ -70,10 +81,10 @@ protected function apiRequest(string $endpoint, array $params = []) Log::error('Failed get data in '.__FILE__.' function '.__METHOD__.' '. $e->getMessage()); } return []; - } + } /** - * General API Call Method + * General API Call Method. */ protected function apiRequestLengkap(string $endpoint, array $params = []) { @@ -81,11 +92,24 @@ protected function apiRequestLengkap(string $endpoint, array $params = []) return $this->apiRequest($endpoint, $params); } + /** + * General API Call Method with POST. + */ + protected function apiRequestPost(string $endpoint, array $params = []) + { + return $this->apiRequest($endpoint, $params, 'POST'); + } + protected function useDatabaseGabungan() { return ($this->settings['sinkronisasi_database_gabungan'] ?? null) === '1'; } + private function resolveMethod(string $method): string + { + return strtolower($method); + } + private function setFullResponse(bool $fullResponse) { $this->fullResponse = $fullResponse; diff --git a/app/Services/DesaService.php b/app/Services/DesaService.php index bb227eaaf3..51ae88d4c6 100644 --- a/app/Services/DesaService.php +++ b/app/Services/DesaService.php @@ -200,9 +200,9 @@ public function jumlahDesa(array $filters = []) * Dapatkan object desa berdasarkan kode_desa tertentu * * @param string $kodeDesa - * @return collection|DataDesa|stdClass|null + * @return collection|DataDesa|\stdClass|null */ - public function getDesaByKode(string $kodeDesa): Collection|DataDesa|stdClass|null + public function getDesaByKode(string $kodeDesa): Collection|DataDesa|\stdClass|null { $listDesa = $this->listDesa(); return $listDesa->where('desa_id', $kodeDesa)->first(); diff --git a/app/Services/PendudukService.php b/app/Services/PendudukService.php index 8372a9959f..00b5b3936e 100644 --- a/app/Services/PendudukService.php +++ b/app/Services/PendudukService.php @@ -3,14 +3,12 @@ namespace App\Services; use App\Models\Penduduk; -use App\Models\SettingAplikasi; -use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; class PendudukService extends BaseApiService { /** - * Get Unique Desa + * Get Unique Desa. */ public function jumlahPenduduk(array $filters = []) { @@ -33,7 +31,7 @@ public function jumlahPenduduk(array $filters = []) } /** - * Get Unique Desa + * Get Unique Desa. */ public function desa(array $filters = []) { @@ -59,7 +57,7 @@ public function desa(array $filters = []) } /** - * Export Data Penduduk + * Export Data Penduduk. */ public function exportPenduduk($size, $number, $search) { @@ -110,23 +108,43 @@ public function detailPenduduk($id) } /** - * Export Data Penduduk + * Ambil beberapa data penduduk dari database gabungan sekaligus + * menggunakan filter id_penduduk berupa array. + * + * @param array $ids + * + * @return \Illuminate\Support\Collection */ - public function cekPendudukNikTanggalLahir($nik, $tgl_lhr = null) + public function pendudukGabunganByIds(array $ids) { - try { - $baseUrl = $this->settings['api_server_database_gabungan']; + if (empty($ids)) { + return collect(); + } + + $data = $this->apiRequest('/api/v1/opendk/sync-penduduk-opendk', [ + 'filter[kode_kecamatan]' => str_replace('.', '', config('profil.kecamatan_id')), + 'filter[id_penduduk]' => $ids, + 'page[size]' => count($ids), + ]); - $response = Http::post($baseUrl . '/api/v1/opendk/penduduk-nik-tanggalahir', [ - 'kode_kecamatan' => str_replace('.', '', config('profil.kecamatan_id')), + return collect($data)->filter(fn (mixed $item): bool => is_array($item)); + } + + /** + * Export Data Penduduk. + */ + public function cekPendudukNikTanggalLahir(string $nik, $tgl_lhr = null) + { + try { + $data = $this->apiRequestPost('/api/v1/opendk/penduduk-nik-tanggalahir', [ + 'kode_kecamatan' => $this->kodeKecamatan, 'nik' => $nik, 'tanggallahir' => $tgl_lhr, ]); - if ($response->successful() && $response->json('data')) { - $pendudukData = $response->json('data'); + if ($data) { $penduduk = new Penduduk(); - $penduduk->forceFill($pendudukData); + $penduduk->forceFill($data); return $penduduk; } diff --git a/app/Services/ThemeHooksValidator.php b/app/Services/ThemeHooksValidator.php index 73ab3e65d6..2557524ef0 100644 --- a/app/Services/ThemeHooksValidator.php +++ b/app/Services/ThemeHooksValidator.php @@ -10,18 +10,44 @@ class ThemeHooksValidator * Dangerous PHP functions that are blocked in hooks.php. */ private const DANGEROUS_FUNCTIONS = [ + // OS command & process execution 'system', 'exec', 'passthru', 'shell_exec', 'popen', 'proc_open', - 'pcntl_exec', 'assert', 'create_function', 'eval', - // BUG FIX: call_user_func / call_user_func_array bypass (Severity: Critical) + 'pcntl_exec', 'pcntl_fork', 'pcntl_alarm', 'pcntl_signal', 'pcntl_wait', + 'pcntl_waitpid', 'pcntl_wexitstatus', 'dl', 'assert', 'create_function', 'eval', + // Dynamic callback & indirect invocation 'call_user_func', 'call_user_func_array', + 'forward_static_call', 'forward_static_call_array', + 'register_shutdown_function', 'register_tick_function', + 'preg_replace_callback', 'preg_replace_callback_array', 'mb_ereg_replace_callback', + 'array_map', 'array_filter', 'array_reduce', 'array_walk', 'array_walk_recursive', + 'unserialize', + // File system & I/O 'file_put_contents', 'file_get_contents', 'fopen', 'fwrite', 'fputs', 'unlink', 'mkdir', 'rmdir', 'rename', 'copy', 'chmod', 'chown', - 'symlink', 'link', 'tmpfile', 'move_uploaded_file', + 'symlink', 'link', 'tmpfile', 'move_uploaded_file', 'touch', + 'readfile', 'file', 'fpassthru', 'highlight_file', 'show_source', + 'fileperms', 'fileowner', 'filegroup', 'chgrp', 'lchown', 'lchgrp', + 'glob', 'scandir', 'opendir', 'readdir', 'dir', + // Environment, configuration & variable manipulation 'extract', 'parse_str', 'putenv', 'ini_set', 'ini_alter', 'header', 'setcookie', 'define', 'defined', - 'base64_decode', 'urldecode', + // Encoding / Obfuscation helpers + 'base64_decode', 'urldecode', 'hex2bin', + 'gzinflate', 'gzuncompress', 'gzdecode', 'str_rot13', 'convert_uudecode', + ]; + + /** + * Dangerous PHP language construct tokens that are blocked in hooks.php. + */ + private const DANGEROUS_LANGUAGE_TOKENS = [ + T_EVAL, + T_INCLUDE, + T_INCLUDE_ONCE, + T_REQUIRE, + T_REQUIRE_ONCE, + T_HALT_COMPILER, ]; /** @@ -32,7 +58,7 @@ class ThemeHooksValidator ]; /** - * Scan ZIP archive for files with dangerous PHP extensions. + * Scan ZIP archive for files with dangerous PHP extensions and malicious blade templates. * Must be called BEFORE extractTo(). * * @throws \RuntimeException if dangerous files are detected. @@ -44,8 +70,26 @@ public function scanZipForPhp(\ZipArchive $zip): void for ($i = 0; $i < $zip->numFiles; $i++) { $filename = $zip->getNameIndex($i); - // Skip Blade template files — they are safe Laravel templates + // Skip directories + if (str_ends_with($filename, '/')) { + continue; + } + + // Validate Blade template files if (str_ends_with($filename, '.blade.php')) { + // Blade templates must be inside resources/views/ + if (! str_contains($filename, 'resources/views/')) { + $dangerousFiles[] = "{$filename} (Blade template must reside in resources/views/)"; + continue; + } + + // Scan Blade content for PHP/Blade execution blocks containing backtick operator + $bladeContent = $zip->getFromIndex($i); + if ($bladeContent !== false && preg_match('/(<\?php|@php|\{\{|\{!!)[^>}]*`/', $bladeContent)) { + $dangerousFiles[] = "{$filename} (Backtick execution operator detected in PHP/Blade directive)"; + continue; + } + continue; } @@ -144,12 +188,28 @@ public function loadHooks(string $themePath): void } /** - * Validate hooks.php source code using token_get_all(). + * Validate hooks.php source code using multi-layer token and string analysis. * * Returns ['valid' => bool, 'reason' => string, 'details' => array] */ public function validateSource(string $source, string $themeName): array { + // === LAYER 1: Raw character scan === + // Reject backtick execution operators immediately + if (str_contains($source, '`')) { + return [ + 'valid' => false, + 'reason' => 'Backtick execution operator is not allowed', + 'details' => [ + [ + 'function' => 'backtick_execution', + 'line' => 1, + ], + ], + ]; + } + + // === LAYER 2: Token-based AST analysis === $tokens = @token_get_all($source); if (! is_array($tokens)) { return ['valid' => false, 'reason' => 'hooks.php cannot be parsed', 'details' => []]; @@ -161,70 +221,151 @@ public function validateSource(string $source, string $themeName): array $hasBadCharacter = false; for ($i = 0; $i < $count; $i++) { - if (! is_array($tokens[$i])) { + $token = $tokens[$i]; + + // Handle single-character literal tokens (e.g. '`', '$', '(', '}', ']', etc.) + if (! is_array($token)) { + if ($token === '`') { + $dangerousCalls[] = [ + 'function' => 'backtick_execution', + 'line' => 1, + ]; + } elseif ($token === '$') { + // Check for variable variables like $$var + $nextIdx = $this->getNextMeaningfulTokenIndex($tokens, $i, $count); + if ($nextIdx < $count && is_array($tokens[$nextIdx]) && $tokens[$nextIdx][0] === T_VARIABLE) { + $dangerousCalls[] = [ + 'function' => 'variable_variable:' . $tokens[$nextIdx][1], + 'line' => $tokens[$nextIdx][2], + ]; + } + } elseif ($token === '}' || $token === ']') { + // Detect: ${"system"}(), ${'system'}(), ${$a}() → '}(' + // Detect: $_GET["cmd"](), $a['fn']() → '](' + // This catches bypass payloads A, B, C, D from the security review. + $nextIdx = $this->getNextMeaningfulTokenIndex($tokens, $i, $count); + if ($nextIdx < $count && ! is_array($tokens[$nextIdx]) && $tokens[$nextIdx] === '(') { + $dangerousCalls[] = [ + 'function' => $token === '}' ? 'curly_expression_function_call' : 'array_subscript_function_call', + 'line' => 1, + ]; + } + } continue; } + $tokenId = $token[0]; + $tokenText = $token[1]; + $tokenLine = $token[2]; + + // Double check for backtick inside any token text + if (str_contains($tokenText, '`')) { + $dangerousCalls[] = [ + 'function' => 'backtick_execution', + 'line' => $tokenLine, + ]; + } + // Detect syntax errors via T_BAD_CHARACTER - if ($tokens[$i][0] === T_BAD_CHARACTER) { + if ($tokenId === T_BAD_CHARACTER) { $hasBadCharacter = true; } // Reject inline HTML — hooks.php should be pure PHP - if ($tokens[$i][0] === T_INLINE_HTML) { - $content = trim((string) $tokens[$i][1]); + if ($tokenId === T_INLINE_HTML) { + $content = trim((string) $tokenText); if ($content !== '') { $hasInlineHtml = true; } } - // Detect eval() — T_EVAL is a language construct token, not T_STRING - if ($tokens[$i][0] === T_EVAL) { + // Detect dangerous language constructs (eval, include, require, etc.) + if (in_array($tokenId, self::DANGEROUS_LANGUAGE_TOKENS, true)) { $dangerousCalls[] = [ - 'function' => 'eval', - 'line' => $tokens[$i][2], + 'function' => token_name($tokenId), + 'line' => $tokenLine, ]; continue; } - // Look for function calls: T_STRING followed by '(' - if ($tokens[$i][0] === T_STRING) { - $funcName = strtolower((string) $tokens[$i][1]); + // Detect dynamic class instantiation: new $var() + if ($tokenId === T_NEW) { + $nextIdx = $this->getNextMeaningfulTokenIndex($tokens, $i, $count); + if ($nextIdx < $count && is_array($tokens[$nextIdx]) && $tokens[$nextIdx][0] === T_VARIABLE) { + $dangerousCalls[] = [ + 'function' => 'dynamic_instantiation:' . $tokens[$nextIdx][1], + 'line' => $tokenLine, + ]; + } + continue; + } - // Check if the next non-whitespace token is '(' - $nextIdx = $i + 1; - while ($nextIdx < $count && is_array($tokens[$nextIdx]) && $tokens[$nextIdx][0] === T_WHITESPACE) { - $nextIdx++; + // Detect dynamic method/property calls: $obj->$method or $obj->{$method} + $isNullsafe = defined('T_NULLSAFE_OBJECT_OPERATOR') && $tokenId === T_NULLSAFE_OBJECT_OPERATOR; + if ($tokenId === T_OBJECT_OPERATOR || $isNullsafe) { + $nextIdx = $this->getNextMeaningfulTokenIndex($tokens, $i, $count); + if ($nextIdx < $count) { + if (is_array($tokens[$nextIdx]) && $tokens[$nextIdx][0] === T_VARIABLE) { + $dangerousCalls[] = [ + 'function' => 'dynamic_member_access:' . $tokens[$nextIdx][1], + 'line' => $tokenLine, + ]; + } elseif (! is_array($tokens[$nextIdx]) && $tokens[$nextIdx] === '{') { + $dangerousCalls[] = [ + 'function' => 'dynamic_member_expression', + 'line' => $tokenLine, + ]; + } } + continue; + } + + // Detect dynamic static calls: Class::$method() or $class::$method() + if ($tokenId === T_PAAMAYIM_NEKUDOTAYIM) { + $nextIdx = $this->getNextMeaningfulTokenIndex($tokens, $i, $count); + if ($nextIdx < $count) { + if (is_array($tokens[$nextIdx]) && $tokens[$nextIdx][0] === T_VARIABLE) { + $dangerousCalls[] = [ + 'function' => 'dynamic_static_call:' . $tokens[$nextIdx][1], + 'line' => $tokenLine, + ]; + } elseif (! is_array($tokens[$nextIdx]) && $tokens[$nextIdx] === '$') { + $dangerousCalls[] = [ + 'function' => 'dynamic_static_expression', + 'line' => $tokenLine, + ]; + } + } + continue; + } + + // Look for function calls: T_STRING followed by '(' + if ($tokenId === T_STRING) { + $funcName = strtolower((string) $tokenText); + $nextIdx = $this->getNextMeaningfulTokenIndex($tokens, $i, $count); if ($nextIdx < $count && ! is_array($tokens[$nextIdx]) && $tokens[$nextIdx] === '(') { // Skip if this T_STRING is part of a function/method definition - $prevIdx = $i - 1; - while ($prevIdx >= 0 && is_array($tokens[$prevIdx]) && $tokens[$prevIdx][0] === T_WHITESPACE) { - $prevIdx--; - } + $prevIdx = $this->getPrevMeaningfulTokenIndex($tokens, $i); $isDefinition = ($prevIdx >= 0 && is_array($tokens[$prevIdx]) && $tokens[$prevIdx][0] === T_FUNCTION); if (! $isDefinition && in_array($funcName, self::DANGEROUS_FUNCTIONS, true)) { $dangerousCalls[] = [ 'function' => $funcName, - 'line' => $tokens[$i][2], + 'line' => $tokenLine, ]; } } } - // BUG FIX (High): Detect variable function calls, e.g. $f('id') or $obj->method(). - if ($tokens[$i][0] === T_VARIABLE) { - $nextIdx = $i + 1; - while ($nextIdx < $count && is_array($tokens[$nextIdx]) && $tokens[$nextIdx][0] === T_WHITESPACE) { - $nextIdx++; - } + // Detect variable function calls, e.g. $f('id') + if ($tokenId === T_VARIABLE) { + $nextIdx = $this->getNextMeaningfulTokenIndex($tokens, $i, $count); if ($nextIdx < $count && ! is_array($tokens[$nextIdx]) && $tokens[$nextIdx] === '(') { $dangerousCalls[] = [ - 'function' => 'variable_function_call:' . $tokens[$i][1], - 'line' => $tokens[$i][2], + 'function' => 'variable_function_call:' . $tokenText, + 'line' => $tokenLine, ]; } } @@ -256,4 +397,38 @@ public function validateSource(string $source, string $themeName): array return ['valid' => true, 'reason' => '', 'details' => []]; } + + /** + * Get index of next non-whitespace, non-comment token. + */ + private function getNextMeaningfulTokenIndex(array $tokens, int $currentIndex, int $count): int + { + $next = $currentIndex + 1; + while ($next < $count) { + if (is_array($tokens[$next]) && in_array($tokens[$next][0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)) { + $next++; + continue; + } + break; + } + + return $next; + } + + /** + * Get index of previous non-whitespace, non-comment token. + */ + private function getPrevMeaningfulTokenIndex(array $tokens, int $currentIndex): int + { + $prev = $currentIndex - 1; + while ($prev >= 0) { + if (is_array($tokens[$prev]) && in_array($tokens[$prev][0], [T_WHITESPACE, T_COMMENT, T_DOC_COMMENT], true)) { + $prev--; + continue; + } + break; + } + + return $prev; + } } diff --git a/app/Traits/HandlesFileUpload.php b/app/Traits/HandlesFileUpload.php index 5bdea81a7b..ff00826ff6 100644 --- a/app/Traits/HandlesFileUpload.php +++ b/app/Traits/HandlesFileUpload.php @@ -36,7 +36,7 @@ trait HandlesFileUpload { /** - * Daftar MIME types default yang diizinkan + * Daftar MIME types default yang diizinkan. */ protected array $defaultAllowedMimes = [ 'image' => ['jpeg', 'png', 'jpg', 'gif', 'svg', 'webp'], @@ -53,7 +53,9 @@ trait HandlesFileUpload * @param string $directory * @param bool $withDirectory * @param array $allowedMimes Optional list of allowed mime extensions + * * @return void + * * @throws \Illuminate\Validation\ValidationException */ public function handleFileUpload($request, &$input, $field = 'file', $directory = 'uploads', $withDirectory = true, array $allowedMimes = []) @@ -78,12 +80,45 @@ public function handleFileUpload($request, &$input, $field = 'file', $directory } } + /** + * Resolve file path secara aman dan pastikan berada dalam direktori yang diizinkan (mencegah Path Traversal). + */ + public function resolveSecureFilePath(?string $path): ?string + { + if (empty($path)) { + return null; + } + + $candidates = [ + public_path($path), + base_path('public/' . $path), + storage_path('app/public/' . str_replace('storage/', '', $path)), + ]; + + $allowedPublic = realpath(public_path()); + $allowedStorage = realpath(storage_path('app/public')); + + foreach ($candidates as $candidate) { + if (file_exists($candidate)) { + $real = realpath($candidate); + if ($real) { + $isAllowedPublic = $allowedPublic && str_starts_with($real, $allowedPublic); + $isAllowedStorage = $allowedStorage && str_starts_with($real, $allowedStorage); + if ($isAllowedPublic || $isAllowedStorage) { + return $real; + } + } + } + } + + return null; + } + /** * Validasi MIME type file. * - * @param UploadedFile $file * @param array $allowedMimes List of allowed extensions (e.g., ['jpg', 'png', 'pdf']) - * @return void + * * @throws \Illuminate\Validation\ValidationException */ protected function validateMimeType(UploadedFile $file, array $allowedMimes): void @@ -92,7 +127,7 @@ protected function validateMimeType(UploadedFile $file, array $allowedMimes): vo if (!in_array($extension, $allowedMimes)) { throw \Illuminate\Validation\ValidationException::withMessages([ - 'file' => "Tipe file tidak diizinkan. Tipe yang diizinkan: " . implode(', ', $allowedMimes), + 'file' => 'Tipe file tidak diizinkan. Tipe yang diizinkan: ' . implode(', ', $allowedMimes), ]); } @@ -102,16 +137,13 @@ protected function validateMimeType(UploadedFile $file, array $allowedMimes): vo if (!empty($allowedMimeTypes) && !in_array($mimeType, $allowedMimeTypes)) { throw \Illuminate\Validation\ValidationException::withMessages([ - 'file' => "MIME type file tidak valid. Pastikan file yang diupload sesuai dengan ekstensinya.", + 'file' => 'MIME type file tidak valid. Pastikan file yang diupload sesuai dengan ekstensinya.', ]); } } /** * Generate nama file yang aman untuk penyimpanan. - * - * @param UploadedFile $file - * @return string */ protected function generateSafeFileName(UploadedFile $file): string { @@ -120,9 +152,6 @@ protected function generateSafeFileName(UploadedFile $file): string /** * Convert extensions to MIME types. - * - * @param array $extensions - * @return array */ protected function extensionsToMimeTypes(array $extensions): array { @@ -158,7 +187,6 @@ protected function extensionsToMimeTypes(array $extensions): array * Mendapatkan daftar MIME types berdasarkan kategori. * * @param string $category 'image', 'document', atau 'archive' - * @return array */ protected function getAllowedMimesByCategory(string $category): array { diff --git a/catatan_rilis.md b/catatan_rilis.md index 660ee33bae..118ccd4eaf 100644 --- a/catatan_rilis.md +++ b/catatan_rilis.md @@ -1,14 +1,30 @@ -Di rilis versi v2609.0.0 di versi ini terdapat modul komentar pada artikel dan perbaikan lain yang diminta Komunitas. +Di rilis versi v2609.0.1 di versi ini terdapat modul komentar pada artikel dan perbaikan lain yang diminta Komunitas. Terimakasih [isi disini] yang telah berkontribusi langsung mengembangkan aplikasi OpenDK. #### FITUR +1. [#1725](https://github.com/OpenSID/OpenDK/issues/1725) Penambahan fungsi pratinjau file pdf untuk kebutuhan OpenDK tidak di dukung. #### BUG +1. [#1718](https://github.com/OpenSID/OpenDK/issues/1718) Bug pada Modul Potensi – OpenDK v2608.0.2. +2. [#1724](https://github.com/OpenSID/OpenDK/issues/1724) eta Letak Geografis kosong (Bounds are not valid) karena $data_umum tidak dikirim ke view gabungan. +3. [#1721](https://github.com/OpenSID/OpenDK/issues/1721) Halaman Unduhan Dokumen hanya menampilkan data sesuai default pagination API (10 data). +4. [#1731](https://github.com/OpenSID/OpenDK/issues/1731) Perbaikan menambah anggota penduduk untuk data suplemen tidak menampilkan list penduduk & keluarga. +5. [#1732](https://github.com/OpenSID/OpenDK/issues/1732) Berkas download dokumen laporan-penduduk pada data tidak sikron dari OpenSID. +6. [#1727](https://github.com/OpenSID/OpenDK/issues/1727) Perbaikan Tampilan Thumbnail Galeri dan Pratinjau Gambar Dinamis. +7. [#1733](https://github.com/OpenSID/OpenDK/issues/1733) perbaikan filter kesehatan. +8. [#1729](https://github.com/OpenSID/OpenDK/issues/1729) perbaikan tombol sarana ui. +9. [#1730](https://github.com/OpenSID/OpenDK/issues/1730) data yang tampil pada datatable overlaping tidak di handle dengan scroll. +10. [#1735](https://github.com/OpenSID/OpenDK/issues/1735) Periksa bagian tambah ketua lembaga ketika menggunakan API satu data. +11. [#1736](http://github.com/OpenSID/OpenDK/issues/1736) Fix Penyesuaian Validasi NIK pada Pengiriman Keluhan untuk Database Gabungan (API Satu Data). +12. [#1737](https://github.com/OpenSID/OpenDK/issues/1737) fungsi kirim pesan error 500. #### TEKNIS 1. [#1713](https://github.com/OpenSID/OpenDK/issues/1713) File di storage/app/public 404 karena direktori upload dibuat permission 0700 (Flysystem directory visibility). +2. [#58](https://github.com/OpenSID/wiki-keamanan/issues/58) Insecure deserialization via Crypt::decrypt() (unserialize=true) on LogViewer user params. +3. [#57](https://github.com/OpenSID/wiki-keamanan/issues/57) CRITICAL: RCE via Theme Hook include_once — backtick execution operator bypasses ThemeHooksValidator (theme.php). +4. [#1759](https://github.com/OpenSID/OpenDK/issues/1759) Pastikan perubahan Known security vulnerabilities detected masuk rilis. \ No newline at end of file diff --git a/composer.lock b/composer.lock index 2fe00f3a3e..bc53632cb8 100644 --- a/composer.lock +++ b/composer.lock @@ -8,16 +8,16 @@ "packages": [ { "name": "bensampo/laravel-enum", - "version": "v6.14.0", + "version": "v6.14.1", "source": { "type": "git", "url": "https://github.com/BenSampo/laravel-enum.git", - "reference": "067df676131295e2084d9d1a0f1dee811a1d4c99" + "reference": "9a7ee80d245a52bea2f611da81056e2e8ac6cc59" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/BenSampo/laravel-enum/zipball/067df676131295e2084d9d1a0f1dee811a1d4c99", - "reference": "067df676131295e2084d9d1a0f1dee811a1d4c99", + "url": "https://api.github.com/repos/BenSampo/laravel-enum/zipball/9a7ee80d245a52bea2f611da81056e2e8ac6cc59", + "reference": "9a7ee80d245a52bea2f611da81056e2e8ac6cc59", "shasum": "" }, "require": { @@ -88,7 +88,7 @@ ], "support": { "issues": "https://github.com/BenSampo/laravel-enum/issues", - "source": "https://github.com/BenSampo/laravel-enum/tree/v6.14.0" + "source": "https://github.com/BenSampo/laravel-enum/tree/v6.14.1" }, "funding": [ { @@ -96,7 +96,7 @@ "type": "github" } ], - "time": "2026-03-29T17:00:09+00:00" + "time": "2026-09-03T07:17:56+00:00" }, { "name": "brick/math", @@ -159,16 +159,16 @@ }, { "name": "carbonphp/carbon-doctrine-types", - "version": "3.2.0", + "version": "3.2.1", "source": { "type": "git", "url": "https://github.com/CarbonPHP/carbon-doctrine-types.git", - "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d" + "reference": "5fa5eacafd9ef47c8c6ab9143fc901ba0194d3dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/18ba5ddfec8976260ead6e866180bd5d2f71aa1d", - "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d", + "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/5fa5eacafd9ef47c8c6ab9143fc901ba0194d3dc", + "reference": "5fa5eacafd9ef47c8c6ab9143fc901ba0194d3dc", "shasum": "" }, "require": { @@ -183,6 +183,11 @@ "phpunit/phpunit": "^10.3" }, "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, "autoload": { "psr-4": { "Carbon\\Doctrine\\": "src/Carbon/Doctrine/" @@ -208,7 +213,7 @@ ], "support": { "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues", - "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.0" + "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.1" }, "funding": [ { @@ -224,7 +229,7 @@ "type": "tidelift" } ], - "time": "2024-02-09T16:56:22+00:00" + "time": "2026-09-06T14:11:38+00:00" }, { "name": "cocur/slugify", @@ -1236,24 +1241,24 @@ }, { "name": "graham-campbell/result-type", - "version": "v1.1.4", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/GrahamCampbell/Result-Type.git", - "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b" + "reference": "adccca3324eece92ca35463648c12b9e6293c05b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b", - "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b", + "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/adccca3324eece92ca35463648c12b9e6293c05b", + "reference": "adccca3324eece92ca35463648c12b9e6293c05b", "shasum": "" }, "require": { "php": "^7.2.5 || ^8.0", - "phpoption/phpoption": "^1.9.5" + "phpoption/phpoption": "^1.10" }, "require-dev": { - "phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7" + "phpunit/phpunit": "^8.5.52 || ^9.6.34 || ^10.5.63 || ^11.5.55 || ^12.5.14" }, "type": "library", "autoload": { @@ -1282,7 +1287,7 @@ ], "support": { "issues": "https://github.com/GrahamCampbell/Result-Type/issues", - "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4" + "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.2.0" }, "funding": [ { @@ -1294,26 +1299,26 @@ "type": "tidelift" } ], - "time": "2025-12-27T19:43:20+00:00" + "time": "2026-08-24T09:06:52+00:00" }, { "name": "guzzlehttp/guzzle", - "version": "7.15.3", + "version": "7.15.5", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "ae311b8f045ea93ce7b1c9cdb7cec06c53f944bc" + "reference": "ee80339fd9177ba44c49cdb653ff02a4d1106b9a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/ae311b8f045ea93ce7b1c9cdb7cec06c53f944bc", - "reference": "ae311b8f045ea93ce7b1c9cdb7cec06c53f944bc", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/ee80339fd9177ba44c49cdb653ff02a4d1106b9a", + "reference": "ee80339fd9177ba44c49cdb653ff02a4d1106b9a", "shasum": "" }, "require": { "ext-json": "*", - "guzzlehttp/promises": "^2.5.2", - "guzzlehttp/psr7": "^2.13", + "guzzlehttp/promises": "^2.5.3", + "guzzlehttp/psr7": "^2.13.1", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.5 || ^3.0", @@ -1406,7 +1411,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.15.3" + "source": "https://github.com/guzzle/guzzle/tree/7.15.5" }, "funding": [ { @@ -1422,20 +1427,20 @@ "type": "tidelift" } ], - "time": "2026-08-05T19:48:21+00:00" + "time": "2026-08-24T09:21:06+00:00" }, { "name": "guzzlehttp/promises", - "version": "2.5.2", + "version": "2.5.3", "source": { "type": "git", "url": "https://github.com/guzzle/promises.git", - "reference": "2823687acff28b2dbe67b2508a6b300e2c3fa4ce" + "reference": "cde49999552d185d64715fe9c1f77a2aadd2f9f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/promises/zipball/2823687acff28b2dbe67b2508a6b300e2c3fa4ce", - "reference": "2823687acff28b2dbe67b2508a6b300e2c3fa4ce", + "url": "https://api.github.com/repos/guzzle/promises/zipball/cde49999552d185d64715fe9c1f77a2aadd2f9f1", + "reference": "cde49999552d185d64715fe9c1f77a2aadd2f9f1", "shasum": "" }, "require": { @@ -1490,7 +1495,7 @@ ], "support": { "issues": "https://github.com/guzzle/promises/issues", - "source": "https://github.com/guzzle/promises/tree/2.5.2" + "source": "https://github.com/guzzle/promises/tree/2.5.3" }, "funding": [ { @@ -1506,20 +1511,20 @@ "type": "tidelift" } ], - "time": "2026-08-05T19:30:54+00:00" + "time": "2026-08-24T09:11:28+00:00" }, { "name": "guzzlehttp/psr7", - "version": "2.13.0", + "version": "2.13.1", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "dad89620b7a6edb60c15858442eb2e408b45d8f4" + "reference": "95e7828100de18b4e269fb1703be530082d5166d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/dad89620b7a6edb60c15858442eb2e408b45d8f4", - "reference": "dad89620b7a6edb60c15858442eb2e408b45d8f4", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/95e7828100de18b4e269fb1703be530082d5166d", + "reference": "95e7828100de18b4e269fb1703be530082d5166d", "shasum": "" }, "require": { @@ -1609,7 +1614,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.13.0" + "source": "https://github.com/guzzle/psr7/tree/2.13.1" }, "funding": [ { @@ -1625,30 +1630,30 @@ "type": "tidelift" } ], - "time": "2026-07-16T22:23:49+00:00" + "time": "2026-08-24T09:13:11+00:00" }, { "name": "guzzlehttp/uri-template", - "version": "v1.0.10", + "version": "v2.0.1", "source": { "type": "git", "url": "https://github.com/guzzle/uri-template.git", - "reference": "f6c24c21f42b990e9a58912b332d0874df6ba839" + "reference": "7a466ad606491eb6528c717482f7cca77f1851f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/uri-template/zipball/f6c24c21f42b990e9a58912b332d0874df6ba839", - "reference": "f6c24c21f42b990e9a58912b332d0874df6ba839", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/7a466ad606491eb6528c717482f7cca77f1851f3", + "reference": "7a466ad606491eb6528c717482f7cca77f1851f3", "shasum": "" }, "require": { - "php": "^7.2.5 || ^8.0", + "php": "^7.4 || ^8.0", "symfony/polyfill-php80": "^1.25" }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.52 || ^9.6.34", - "uri-template/tests": "1.0.0" + "phpunit/phpunit": "^9.6.34", + "uri-template/tests": "1.0.2" }, "type": "library", "extra": { @@ -1695,7 +1700,7 @@ ], "support": { "issues": "https://github.com/guzzle/uri-template/issues", - "source": "https://github.com/guzzle/uri-template/tree/v1.0.10" + "source": "https://github.com/guzzle/uri-template/tree/v2.0.1" }, "funding": [ { @@ -1711,7 +1716,7 @@ "type": "tidelift" } ], - "time": "2026-07-17T13:53:03+00:00" + "time": "2026-08-24T17:13:02+00:00" }, { "name": "hexadog/laravel-theme-installer", @@ -2445,16 +2450,16 @@ }, { "name": "laravel/framework", - "version": "v13.25.0", + "version": "v13.31.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "ed36fe882bd4eed4e6ff75343cbad8dbda03fdba" + "reference": "7c75fbf93f91fa077d3df1c820cc14f4e59a9774" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/ed36fe882bd4eed4e6ff75343cbad8dbda03fdba", - "reference": "ed36fe882bd4eed4e6ff75343cbad8dbda03fdba", + "url": "https://api.github.com/repos/laravel/framework/zipball/7c75fbf93f91fa077d3df1c820cc14f4e59a9774", + "reference": "7c75fbf93f91fa077d3df1c820cc14f4e59a9774", "shasum": "" }, "require": { @@ -2471,9 +2476,10 @@ "ext-session": "*", "ext-tokenizer": "*", "fruitcake/php-cors": "^1.3", - "guzzlehttp/guzzle": "^7.8.2", - "guzzlehttp/promises": "^2.0.3", - "guzzlehttp/uri-template": "^1.0", + "guzzlehttp/guzzle": "^7.8.2 || ^8.0", + "guzzlehttp/promises": "^2.0.3 || ^3.0", + "guzzlehttp/psr7": "^2.9 || ^3.0", + "guzzlehttp/uri-template": "^1.0 || ^2.0", "laravel/prompts": "^0.3.11", "laravel/serializable-closure": "^2.0.10", "league/commonmark": "^2.8.1", @@ -2485,6 +2491,7 @@ "nunomaduro/termwind": "^2.0", "php": "^8.3", "psr/container": "^1.1.1 || ^2.0.1", + "psr/http-message": "^1.0 || ^2.0", "psr/log": "^1.0 || ^2.0 || ^3.0", "psr/simple-cache": "^1.0 || ^2.0 || ^3.0", "ramsey/uuid": "^4.7", @@ -2559,7 +2566,6 @@ "aws/aws-sdk-php": "^3.322.9", "ext-gmp": "*", "fakerphp/faker": "^1.24", - "guzzlehttp/psr7": "^2.9", "intervention/image": "^4.0", "laravel/pint": "^1.18", "league/flysystem-aws-s3-v3": "^3.25.1", @@ -2575,7 +2581,7 @@ "phpstan/phpstan": "^2.0", "phpunit/phpunit": "^11.5.50 || ^12.5.8 || ^13.0.3", "predis/predis": "^2.3 || ^3.0", - "rector/rector": "^2.3", + "rector/rector": "2.6.3", "resend/resend-php": "^1.0", "symfony/cache": "^7.4.0 || ^8.0.0", "symfony/http-client": "^7.4.0 || ^8.0.0", @@ -2609,7 +2615,6 @@ "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).", "phpunit/phpunit": "Required to use assertions and run tests (^11.5.50 || ^12.5.8 || ^13.0.3).", "predis/predis": "Required to use the predis connector (^2.3 || ^3.0).", - "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).", "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0 || ^7.0).", "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0 || ^1.0).", "spatie/fork": "Required to use the 'fork' concurrency driver (^1.2).", @@ -2668,7 +2673,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2026-08-11T13:56:22+00:00" + "time": "2026-09-08T14:22:55+00:00" }, { "name": "laravel/helpers", @@ -2729,16 +2734,16 @@ }, { "name": "laravel/prompts", - "version": "v0.3.22", + "version": "v0.3.24", "source": { "type": "git", "url": "https://github.com/laravel/prompts.git", - "reference": "02b89b39e8972a998db4d5d4ad4719239dd4aee4" + "reference": "5d3cdef29e93ca3b62b1871359db3078cd99908b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/prompts/zipball/02b89b39e8972a998db4d5d4ad4719239dd4aee4", - "reference": "02b89b39e8972a998db4d5d4ad4719239dd4aee4", + "url": "https://api.github.com/repos/laravel/prompts/zipball/5d3cdef29e93ca3b62b1871359db3078cd99908b", + "reference": "5d3cdef29e93ca3b62b1871359db3078cd99908b", "shasum": "" }, "require": { @@ -2782,22 +2787,22 @@ "description": "Add beautiful and user-friendly forms to your command-line applications.", "support": { "issues": "https://github.com/laravel/prompts/issues", - "source": "https://github.com/laravel/prompts/tree/v0.3.22" + "source": "https://github.com/laravel/prompts/tree/v0.3.24" }, - "time": "2026-08-04T14:50:50+00:00" + "time": "2026-08-20T12:55:36+00:00" }, { "name": "laravel/serializable-closure", - "version": "v2.0.15", + "version": "v2.0.16", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "dccd8bcb851bb03fcc005df650b708b57cc52661" + "reference": "7cfc24e4fa2cca045fb8dd2a797a2b2b13b655ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/dccd8bcb851bb03fcc005df650b708b57cc52661", - "reference": "dccd8bcb851bb03fcc005df650b708b57cc52661", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/7cfc24e4fa2cca045fb8dd2a797a2b2b13b655ed", + "reference": "7cfc24e4fa2cca045fb8dd2a797a2b2b13b655ed", "shasum": "" }, "require": { @@ -2845,7 +2850,7 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2026-07-21T16:49:22+00:00" + "time": "2026-08-18T20:28:54+00:00" }, { "name": "laravel/ui", @@ -2985,16 +2990,16 @@ }, { "name": "league/commonmark", - "version": "2.10.0", + "version": "2.10.1", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "d2d1aa8b35e072966c89bc0c66cf926e56767dc4" + "reference": "9d489ab67a02960fd8ffe624d93f751daf95439e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/d2d1aa8b35e072966c89bc0c66cf926e56767dc4", - "reference": "d2d1aa8b35e072966c89bc0c66cf926e56767dc4", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/9d489ab67a02960fd8ffe624d93f751daf95439e", + "reference": "9d489ab67a02960fd8ffe624d93f751daf95439e", "shasum": "" }, "require": { @@ -3088,7 +3093,7 @@ "type": "tidelift" } ], - "time": "2026-08-11T16:06:25+00:00" + "time": "2026-09-07T13:44:26+00:00" }, { "name": "league/config", @@ -3174,16 +3179,16 @@ }, { "name": "league/flysystem", - "version": "3.35.2", + "version": "3.36.0", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem.git", - "reference": "b277b5dc3d56650b68904117124e79c851e12376" + "reference": "f7fb152932f30072d573510cbd4dd657d6475b25" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/b277b5dc3d56650b68904117124e79c851e12376", - "reference": "b277b5dc3d56650b68904117124e79c851e12376", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/f7fb152932f30072d573510cbd4dd657d6475b25", + "reference": "f7fb152932f30072d573510cbd4dd657d6475b25", "shasum": "" }, "require": { @@ -3251,22 +3256,22 @@ ], "support": { "issues": "https://github.com/thephpleague/flysystem/issues", - "source": "https://github.com/thephpleague/flysystem/tree/3.35.2" + "source": "https://github.com/thephpleague/flysystem/tree/3.36.0" }, - "time": "2026-07-06T14:42:07+00:00" + "time": "2026-09-02T08:00:27+00:00" }, { "name": "league/flysystem-local", - "version": "3.31.0", + "version": "3.35.3", "source": { "type": "git", "url": "https://github.com/thephpleague/flysystem-local.git", - "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079" + "reference": "a099b24dce160f3b2239043d13d47c4a1a214ea4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/2f669db18a4c20c755c2bb7d3a7b0b2340488079", - "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079", + "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/a099b24dce160f3b2239043d13d47c4a1a214ea4", + "reference": "a099b24dce160f3b2239043d13d47c4a1a214ea4", "shasum": "" }, "require": { @@ -3300,9 +3305,9 @@ "local" ], "support": { - "source": "https://github.com/thephpleague/flysystem-local/tree/3.31.0" + "source": "https://github.com/thephpleague/flysystem-local/tree/3.35.3" }, - "time": "2026-01-23T15:30:45+00:00" + "time": "2026-08-12T13:29:21+00:00" }, { "name": "league/fractal", @@ -3614,16 +3619,16 @@ }, { "name": "livewire/livewire", - "version": "v3.8.4", + "version": "v3.8.8", "source": { "type": "git", "url": "https://github.com/livewire/livewire.git", - "reference": "bc06b755058e2253cc4153d95c8720c585f6254c" + "reference": "ec19e5fb1e0b60df22ad15b901c2f7740267ba11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/livewire/livewire/zipball/bc06b755058e2253cc4153d95c8720c585f6254c", - "reference": "bc06b755058e2253cc4153d95c8720c585f6254c", + "url": "https://api.github.com/repos/livewire/livewire/zipball/ec19e5fb1e0b60df22ad15b901c2f7740267ba11", + "reference": "ec19e5fb1e0b60df22ad15b901c2f7740267ba11", "shasum": "" }, "require": { @@ -3678,7 +3683,7 @@ "description": "A front-end framework for Laravel.", "support": { "issues": "https://github.com/livewire/livewire/issues", - "source": "https://github.com/livewire/livewire/tree/v3.8.4" + "source": "https://github.com/livewire/livewire/tree/v3.8.8" }, "funding": [ { @@ -3686,7 +3691,7 @@ "type": "github" } ], - "time": "2026-08-10T15:24:05+00:00" + "time": "2026-09-07T16:02:29+00:00" }, { "name": "maatwebsite/excel", @@ -3956,16 +3961,16 @@ }, { "name": "mews/captcha", - "version": "3.5.0", + "version": "3.5.1", "source": { "type": "git", "url": "https://github.com/mewebstudio/captcha.git", - "reference": "b7add434bfd87c7e900c82d821a56ec566381915" + "reference": "7154a930e88bdb7b02630b14105c5b7b3ecc7fb4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mewebstudio/captcha/zipball/b7add434bfd87c7e900c82d821a56ec566381915", - "reference": "b7add434bfd87c7e900c82d821a56ec566381915", + "url": "https://api.github.com/repos/mewebstudio/captcha/zipball/7154a930e88bdb7b02630b14105c5b7b3ecc7fb4", + "reference": "7154a930e88bdb7b02630b14105c5b7b3ecc7fb4", "shasum": "" }, "require": { @@ -4023,9 +4028,9 @@ ], "support": { "issues": "https://github.com/mewebstudio/captcha/issues", - "source": "https://github.com/mewebstudio/captcha/tree/3.5.0" + "source": "https://github.com/mewebstudio/captcha/tree/3.5.1" }, - "time": "2026-06-08T01:49:41+00:00" + "time": "2026-08-28T22:05:37+00:00" }, { "name": "mobiledetect/mobiledetectlib", @@ -4091,16 +4096,16 @@ }, { "name": "monolog/monolog", - "version": "3.10.0", + "version": "3.12.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0" + "reference": "72c534fc0ab181ef52d92a68382318631e301608" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0", - "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/72c534fc0ab181ef52d92a68382318631e301608", + "reference": "72c534fc0ab181ef52d92a68382318631e301608", "shasum": "" }, "require": { @@ -4126,6 +4131,7 @@ "phpstan/phpstan-strict-rules": "^2", "phpunit/phpunit": "^10.5.17 || ^11.0.7", "predis/predis": "^1.1 || ^2", + "psr/clock": "^1.0", "rollbar/rollbar": "^4.0", "ruflin/elastica": "^7 || ^8", "symfony/mailer": "^5.4 || ^6", @@ -4144,6 +4150,7 @@ "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)", "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "psr/clock": "Required to pass a clock to the Logger and control the timestamp of log records", "rollbar/rollbar": "Allow sending log messages to Rollbar", "ruflin/elastica": "Allow sending log messages to an Elastic Search server" }, @@ -4178,7 +4185,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/3.10.0" + "source": "https://github.com/Seldaek/monolog/tree/3.12.0" }, "funding": [ { @@ -4190,7 +4197,7 @@ "type": "tidelift" } ], - "time": "2026-01-02T08:56:05+00:00" + "time": "2026-09-09T08:34:20+00:00" }, { "name": "mpociot/reflection-docblock", @@ -4986,16 +4993,16 @@ }, { "name": "phpoption/phpoption", - "version": "1.9.5", + "version": "1.10.0", "source": { "type": "git", "url": "https://github.com/schmittjoh/php-option.git", - "reference": "75365b91986c2405cf5e1e012c5595cd487a98be" + "reference": "67b192b6a42ec03944b972d6e633ddec78ad2c6d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be", - "reference": "75365b91986c2405cf5e1e012c5595cd487a98be", + "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/67b192b6a42ec03944b972d6e633ddec78ad2c6d", + "reference": "67b192b6a42ec03944b972d6e633ddec78ad2c6d", "shasum": "" }, "require": { @@ -5003,7 +5010,7 @@ }, "require-dev": { "bamarni/composer-bin-plugin": "^1.8.2", - "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34" + "phpunit/phpunit": "^8.5.54 || ^9.6.36 || ^10.5.64 || ^11.5.56 || ^12.5.33" }, "type": "library", "extra": { @@ -5045,7 +5052,7 @@ ], "support": { "issues": "https://github.com/schmittjoh/php-option/issues", - "source": "https://github.com/schmittjoh/php-option/tree/1.9.5" + "source": "https://github.com/schmittjoh/php-option/tree/1.10.0" }, "funding": [ { @@ -5057,7 +5064,7 @@ "type": "tidelift" } ], - "time": "2025-12-27T19:41:33+00:00" + "time": "2026-08-24T00:54:40+00:00" }, { "name": "proengsoft/laravel-jsvalidation", @@ -5819,16 +5826,16 @@ }, { "name": "sentry/sentry", - "version": "4.30.0", + "version": "4.31.0", "source": { "type": "git", "url": "https://github.com/getsentry/sentry-php.git", - "reference": "95a26e7c946651cacd69d1ded95a8190f952a3ae" + "reference": "39d9f86bd56c6cc6ae7b64c2b849f7f046d38394" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/95a26e7c946651cacd69d1ded95a8190f952a3ae", - "reference": "95a26e7c946651cacd69d1ded95a8190f952a3ae", + "url": "https://api.github.com/repos/getsentry/sentry-php/zipball/39d9f86bd56c6cc6ae7b64c2b849f7f046d38394", + "reference": "39d9f86bd56c6cc6ae7b64c2b849f7f046d38394", "shasum": "" }, "require": { @@ -5899,7 +5906,7 @@ ], "support": { "issues": "https://github.com/getsentry/sentry-php/issues", - "source": "https://github.com/getsentry/sentry-php/tree/4.30.0" + "source": "https://github.com/getsentry/sentry-php/tree/4.31.0" }, "funding": [ { @@ -5911,7 +5918,7 @@ "type": "custom" } ], - "time": "2026-08-05T14:54:26+00:00" + "time": "2026-08-27T12:59:01+00:00" }, { "name": "sentry/sentry-laravel", @@ -6272,22 +6279,22 @@ }, { "name": "spatie/laravel-activitylog", - "version": "5.1.0", + "version": "5.1.1", "source": { "type": "git", "url": "https://github.com/spatie/laravel-activitylog.git", - "reference": "97c9f7e82033b18dc9db25de276f9b71e01449ea" + "reference": "8ce56d3b9d72f4e4551f4449d236a5ed06e71667" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-activitylog/zipball/97c9f7e82033b18dc9db25de276f9b71e01449ea", - "reference": "97c9f7e82033b18dc9db25de276f9b71e01449ea", + "url": "https://api.github.com/repos/spatie/laravel-activitylog/zipball/8ce56d3b9d72f4e4551f4449d236a5ed06e71667", + "reference": "8ce56d3b9d72f4e4551f4449d236a5ed06e71667", "shasum": "" }, "require": { - "illuminate/config": "^13.0", - "illuminate/database": "^13.0", - "illuminate/support": "^13.0", + "illuminate/config": "^12.0 || ^13.0", + "illuminate/database": "^12.0 || ^13.0", + "illuminate/support": "^12.0 || ^13.0", "php": "^8.4", "spatie/laravel-package-tools": "^1.6.3" }, @@ -6295,8 +6302,8 @@ "ext-json": "*", "larastan/larastan": "^3.0", "laravel/pint": "^1.29", - "orchestra/testbench": "^11.0", - "pestphp/pest": "^5.0" + "orchestra/testbench": "^10.0 || ^11.0", + "pestphp/pest": "^4.0 || ^5.0" }, "type": "library", "extra": { @@ -6349,7 +6356,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-activitylog/issues", - "source": "https://github.com/spatie/laravel-activitylog/tree/5.1.0" + "source": "https://github.com/spatie/laravel-activitylog/tree/5.1.1" }, "funding": [ { @@ -6361,20 +6368,20 @@ "type": "github" } ], - "time": "2026-08-12T12:07:38+00:00" + "time": "2026-09-08T12:16:25+00:00" }, { "name": "spatie/laravel-backup", - "version": "10.3.1", + "version": "10.3.2", "source": { "type": "git", "url": "https://github.com/spatie/laravel-backup.git", - "reference": "1160cf6a6faa262586f47e2bd20c59512bb1a77e" + "reference": "705147b2521c19b6e2a0cd598acbf8a0c5f3184f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-backup/zipball/1160cf6a6faa262586f47e2bd20c59512bb1a77e", - "reference": "1160cf6a6faa262586f47e2bd20c59512bb1a77e", + "url": "https://api.github.com/repos/spatie/laravel-backup/zipball/705147b2521c19b6e2a0cd598acbf8a0c5f3184f", + "reference": "705147b2521c19b6e2a0cd598acbf8a0c5f3184f", "shasum": "" }, "require": { @@ -6449,7 +6456,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-backup/issues", - "source": "https://github.com/spatie/laravel-backup/tree/10.3.1" + "source": "https://github.com/spatie/laravel-backup/tree/10.3.2" }, "funding": [ { @@ -6461,7 +6468,7 @@ "type": "other" } ], - "time": "2026-07-28T13:19:06+00:00" + "time": "2026-08-20T12:06:47+00:00" }, { "name": "spatie/laravel-fractal", @@ -6690,16 +6697,16 @@ }, { "name": "spatie/laravel-package-tools", - "version": "1.93.1", + "version": "1.93.2", "source": { "type": "git", "url": "https://github.com/spatie/laravel-package-tools.git", - "reference": "d5552849801f2642aea710557463234b59ef65eb" + "reference": "e927d5b5b05e9fecae098e559e51918aa608ad02" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/d5552849801f2642aea710557463234b59ef65eb", - "reference": "d5552849801f2642aea710557463234b59ef65eb", + "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/e927d5b5b05e9fecae098e559e51918aa608ad02", + "reference": "e927d5b5b05e9fecae098e559e51918aa608ad02", "shasum": "" }, "require": { @@ -6739,7 +6746,7 @@ ], "support": { "issues": "https://github.com/spatie/laravel-package-tools/issues", - "source": "https://github.com/spatie/laravel-package-tools/tree/1.93.1" + "source": "https://github.com/spatie/laravel-package-tools/tree/1.93.2" }, "funding": [ { @@ -6747,7 +6754,7 @@ "type": "github" } ], - "time": "2026-05-19T14:06:37+00:00" + "time": "2026-08-26T09:13:25+00:00" }, { "name": "spatie/laravel-permission", @@ -6835,16 +6842,16 @@ }, { "name": "spatie/laravel-query-builder", - "version": "7.3.3", + "version": "7.3.5", "source": { "type": "git", "url": "https://github.com/spatie/laravel-query-builder.git", - "reference": "eb1e0835018fff80900160d4bfd730c560c54ec4" + "reference": "595523f861caf751ebd29fc377ddc525cd2e3fb1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/spatie/laravel-query-builder/zipball/eb1e0835018fff80900160d4bfd730c560c54ec4", - "reference": "eb1e0835018fff80900160d4bfd730c560c54ec4", + "url": "https://api.github.com/repos/spatie/laravel-query-builder/zipball/595523f861caf751ebd29fc377ddc525cd2e3fb1", + "reference": "595523f861caf751ebd29fc377ddc525cd2e3fb1", "shasum": "" }, "require": { @@ -6905,7 +6912,7 @@ "type": "custom" } ], - "time": "2026-08-07T11:50:57+00:00" + "time": "2026-09-03T13:42:08+00:00" }, { "name": "spatie/laravel-signal-aware-command", @@ -8402,16 +8409,16 @@ }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.38.1", + "version": "v1.42.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "dc21118016c039a66235cf93d96b435ffb282412" + "reference": "51b5ff5ba85452b31ec6f55490b08148612339d9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/dc21118016c039a66235cf93d96b435ffb282412", - "reference": "dc21118016c039a66235cf93d96b435ffb282412", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/51b5ff5ba85452b31ec6f55490b08148612339d9", + "reference": "51b5ff5ba85452b31ec6f55490b08148612339d9", "shasum": "" }, "require": { @@ -8465,7 +8472,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.38.1" + "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.42.0" }, "funding": [ { @@ -8485,20 +8492,20 @@ "type": "tidelift" } ], - "time": "2026-05-25T15:22:23+00:00" + "time": "2026-08-24T10:51:20+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.38.0", + "version": "v1.42.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b" + "reference": "aa20edea75bd9c48cfecc8360922e5a6e5c44502" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b", - "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/aa20edea75bd9c48cfecc8360922e5a6e5c44502", + "reference": "aa20edea75bd9c48cfecc8360922e5a6e5c44502", "shasum": "" }, "require": { @@ -8550,7 +8557,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.42.0" }, "funding": [ { @@ -8570,7 +8577,7 @@ "type": "tidelift" } ], - "time": "2026-05-25T13:48:31+00:00" + "time": "2026-08-07T06:33:24+00:00" }, { "name": "symfony/polyfill-mbstring", @@ -9363,16 +9370,16 @@ }, { "name": "symfony/service-contracts", - "version": "v3.7.1", + "version": "v3.7.3", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0" + "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/c0a284bab1ed8aa0417e3d69250ab437739563a0", - "reference": "c0a284bab1ed8aa0417e3d69250ab437739563a0", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/15e6a07ec2a2c75ceb1b21dd98105ee8456d2257", + "reference": "15e6a07ec2a2c75ceb1b21dd98105ee8456d2257", "shasum": "" }, "require": { @@ -9426,7 +9433,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.7.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.7.3" }, "funding": [ { @@ -9446,7 +9453,7 @@ "type": "tidelift" } ], - "time": "2026-06-16T09:55:08+00:00" + "time": "2026-07-27T15:39:01+00:00" }, { "name": "symfony/string", @@ -10249,23 +10256,23 @@ }, { "name": "vlucas/phpdotenv", - "version": "v5.6.4", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/vlucas/phpdotenv.git", - "reference": "416df702837983f8d5ff48c9c3fee4f5f57b980b" + "reference": "301c07936b16d88628b126b01d082ba153cf4c40" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/416df702837983f8d5ff48c9c3fee4f5f57b980b", - "reference": "416df702837983f8d5ff48c9c3fee4f5f57b980b", + "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/301c07936b16d88628b126b01d082ba153cf4c40", + "reference": "301c07936b16d88628b126b01d082ba153cf4c40", "shasum": "" }, "require": { "ext-pcre": "*", - "graham-campbell/result-type": "^1.1.4", + "graham-campbell/result-type": "^1.2", "php": "^7.2.5 || ^8.0", - "phpoption/phpoption": "^1.9.5", + "phpoption/phpoption": "^1.10", "symfony/polyfill-ctype": "^1.26", "symfony/polyfill-mbstring": "^1.26", "symfony/polyfill-php80": "^1.26" @@ -10309,7 +10316,7 @@ "homepage": "https://github.com/vlucas" } ], - "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", + "description": "Loads environment variables from `.env` to `$_ENV` and `$_SERVER` automagically, and optionally to `getenv()`.", "keywords": [ "dotenv", "env", @@ -10317,7 +10324,7 @@ ], "support": { "issues": "https://github.com/vlucas/phpdotenv/issues", - "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.4" + "source": "https://github.com/vlucas/phpdotenv/tree/v5.7.0" }, "funding": [ { @@ -10329,7 +10336,7 @@ "type": "tidelift" } ], - "time": "2026-07-06T19:11:50+00:00" + "time": "2026-08-24T18:07:49+00:00" }, { "name": "voku/anti-xss", @@ -10589,16 +10596,16 @@ }, { "name": "yajra/laravel-datatables-oracle", - "version": "v13.2.0", + "version": "v13.3.0", "source": { "type": "git", "url": "https://github.com/yajra/laravel-datatables.git", - "reference": "1a3d674cdc0ef6a226c76f5ff0c3b6d8f3af15cc" + "reference": "45fc543117f9cbbbdb47d47bada1e36cef3eed20" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/yajra/laravel-datatables/zipball/1a3d674cdc0ef6a226c76f5ff0c3b6d8f3af15cc", - "reference": "1a3d674cdc0ef6a226c76f5ff0c3b6d8f3af15cc", + "url": "https://api.github.com/repos/yajra/laravel-datatables/zipball/45fc543117f9cbbbdb47d47bada1e36cef3eed20", + "reference": "45fc543117f9cbbbdb47d47bada1e36cef3eed20", "shasum": "" }, "require": { @@ -10666,7 +10673,7 @@ ], "support": { "issues": "https://github.com/yajra/laravel-datatables/issues", - "source": "https://github.com/yajra/laravel-datatables/tree/v13.2.0" + "source": "https://github.com/yajra/laravel-datatables/tree/v13.3.0" }, "funding": [ { @@ -10674,7 +10681,7 @@ "type": "github" } ], - "time": "2026-08-14T03:20:47+00:00" + "time": "2026-08-26T05:15:51+00:00" } ], "packages-dev": [ @@ -10901,16 +10908,16 @@ }, { "name": "amphp/dns", - "version": "v2.4.0", + "version": "v2.4.1", "source": { "type": "git", "url": "https://github.com/amphp/dns.git", - "reference": "78eb3db5fc69bf2fc0cb503c4fcba667bc223c71" + "reference": "ec5948bfafac808f410406e18bc7b52a2d4629b7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/dns/zipball/78eb3db5fc69bf2fc0cb503c4fcba667bc223c71", - "reference": "78eb3db5fc69bf2fc0cb503c4fcba667bc223c71", + "url": "https://api.github.com/repos/amphp/dns/zipball/ec5948bfafac808f410406e18bc7b52a2d4629b7", + "reference": "ec5948bfafac808f410406e18bc7b52a2d4629b7", "shasum": "" }, "require": { @@ -10929,7 +10936,7 @@ "amphp/php-cs-fixer-config": "^2", "amphp/phpunit-util": "^3", "phpunit/phpunit": "^9", - "psalm/phar": "5.20" + "psalm/phar": "6.16.1" }, "type": "library", "autoload": { @@ -10978,7 +10985,7 @@ ], "support": { "issues": "https://github.com/amphp/dns/issues", - "source": "https://github.com/amphp/dns/tree/v2.4.0" + "source": "https://github.com/amphp/dns/tree/v2.4.1" }, "funding": [ { @@ -10986,7 +10993,7 @@ "type": "github" } ], - "time": "2025-01-19T15:43:40+00:00" + "time": "2026-07-26T17:53:54+00:00" }, { "name": "amphp/hpack", @@ -11586,16 +11593,16 @@ }, { "name": "amphp/socket", - "version": "v2.4.0", + "version": "v2.4.1", "source": { "type": "git", "url": "https://github.com/amphp/socket.git", - "reference": "dadb63c5d3179fd83803e29dfeac27350e619314" + "reference": "b347be5aff6b2cc025208bb4d896607eb470c018" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/amphp/socket/zipball/dadb63c5d3179fd83803e29dfeac27350e619314", - "reference": "dadb63c5d3179fd83803e29dfeac27350e619314", + "url": "https://api.github.com/repos/amphp/socket/zipball/b347be5aff6b2cc025208bb4d896607eb470c018", + "reference": "b347be5aff6b2cc025208bb4d896607eb470c018", "shasum": "" }, "require": { @@ -11658,7 +11665,7 @@ ], "support": { "issues": "https://github.com/amphp/socket/issues", - "source": "https://github.com/amphp/socket/tree/v2.4.0" + "source": "https://github.com/amphp/socket/tree/v2.4.1" }, "funding": [ { @@ -11666,7 +11673,7 @@ "type": "github" } ], - "time": "2026-04-19T15:09:56+00:00" + "time": "2026-08-22T18:13:20+00:00" }, { "name": "amphp/sync", @@ -11910,16 +11917,16 @@ }, { "name": "barryvdh/laravel-debugbar", - "version": "v4.4.1", + "version": "v4.4.3", "source": { "type": "git", "url": "https://github.com/fruitcake/laravel-debugbar.git", - "reference": "389157fb616e5c5d19d16a88fd9bfcf3e3248e9b" + "reference": "caa08515a2eef6d8137653d3a1f3c3387cadf184" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/fruitcake/laravel-debugbar/zipball/389157fb616e5c5d19d16a88fd9bfcf3e3248e9b", - "reference": "389157fb616e5c5d19d16a88fd9bfcf3e3248e9b", + "url": "https://api.github.com/repos/fruitcake/laravel-debugbar/zipball/caa08515a2eef6d8137653d3a1f3c3387cadf184", + "reference": "caa08515a2eef6d8137653d3a1f3c3387cadf184", "shasum": "" }, "require": { @@ -11994,7 +12001,7 @@ ], "support": { "issues": "https://github.com/fruitcake/laravel-debugbar/issues", - "source": "https://github.com/fruitcake/laravel-debugbar/tree/v4.4.1" + "source": "https://github.com/fruitcake/laravel-debugbar/tree/v4.4.3" }, "funding": [ { @@ -12006,7 +12013,7 @@ "type": "github" } ], - "time": "2026-08-03T06:23:16+00:00" + "time": "2026-09-01T13:46:43+00:00" }, { "name": "brianium/paratest", @@ -12377,19 +12384,20 @@ }, { "name": "justinrainbow/json-schema", - "version": "6.10.0", + "version": "6.12.0", "source": { "type": "git", "url": "https://github.com/jsonrainbow/json-schema.git", - "reference": "8b1308a9d7bdbdb20ce87ef920f82b4564bb2d33" + "reference": "8ef236a1a37df364b518b7411a4b5bb95b040d79" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/8b1308a9d7bdbdb20ce87ef920f82b4564bb2d33", - "reference": "8b1308a9d7bdbdb20ce87ef920f82b4564bb2d33", + "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/8ef236a1a37df364b518b7411a4b5bb95b040d79", + "reference": "8ef236a1a37df364b518b7411a4b5bb95b040d79", "shasum": "" }, "require": { + "ext-filter": "*", "ext-json": "*", "marc-mabe/php-enum": "^4.4", "php": "^7.2 || ^8.0" @@ -12422,20 +12430,9 @@ ], "authors": [ { - "name": "Bruno Prieto Reis", - "email": "bruno.p.reis@gmail.com" - }, - { - "name": "Justin Rainbow", - "email": "justin.rainbow@gmail.com" - }, - { - "name": "Igor Wiedler", - "email": "igor@wiedler.ch" - }, - { - "name": "Robert Schönthal", - "email": "seroscho@googlemail.com" + "name": "Danny van der Sluijs", + "email": "danny.vandersluijs@icloud.com", + "role": "Maintainer" } ], "description": "A library to validate a json schema.", @@ -12446,9 +12443,9 @@ ], "support": { "issues": "https://github.com/jsonrainbow/json-schema/issues", - "source": "https://github.com/jsonrainbow/json-schema/tree/6.10.0" + "source": "https://github.com/jsonrainbow/json-schema/tree/6.12.0" }, - "time": "2026-06-16T20:50:26+00:00" + "time": "2026-09-04T12:54:20+00:00" }, { "name": "kelunik/certificate", @@ -12510,16 +12507,16 @@ }, { "name": "laravel/pint", - "version": "v1.30.5", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "fe4148c503a0e266353d61396b79bbf7f35122df" + "reference": "dea4e95f0f1722b8fec9eaabc8fdd676d71e8841" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/fe4148c503a0e266353d61396b79bbf7f35122df", - "reference": "fe4148c503a0e266353d61396b79bbf7f35122df", + "url": "https://api.github.com/repos/laravel/pint/zipball/dea4e95f0f1722b8fec9eaabc8fdd676d71e8841", + "reference": "dea4e95f0f1722b8fec9eaabc8fdd676d71e8841", "shasum": "" }, "require": { @@ -12531,13 +12528,13 @@ }, "require-dev": { "composer/semver": "^3.4.4", - "friendsofphp/php-cs-fixer": "^3.95.18", - "illuminate/view": "^13.24.0", - "larastan/larastan": "^3.10.0", + "friendsofphp/php-cs-fixer": "^3.95.25", + "illuminate/view": "^13.31.0", + "larastan/larastan": "^3.11.0", "laravel-zero/framework": "^13.0.0", "laravel/agent-detector": "^2.0.2", - "laravel/prompts": "^0.3.22", - "mockery/mockery": "^1.6.12", + "laravel/prompts": "^0.3.24", + "mockery/mockery": "^1.6.15", "nunomaduro/termwind": "^2.4.0", "pestphp/pest": "^4.7.8" }, @@ -12576,7 +12573,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2026-08-10T15:35:50+00:00" + "time": "2026-09-09T16:26:47+00:00" }, { "name": "laravel/tinker", @@ -12806,16 +12803,16 @@ }, { "name": "mockery/mockery", - "version": "1.6.13", + "version": "1.6.15", "source": { "type": "git", "url": "https://github.com/mockery/mockery.git", - "reference": "9cb54414cdcd2ec5ca292e7ba19dba3a3444885d" + "reference": "967a801bd188989a5669bd280f252d51c0fdc9ee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/mockery/mockery/zipball/9cb54414cdcd2ec5ca292e7ba19dba3a3444885d", - "reference": "9cb54414cdcd2ec5ca292e7ba19dba3a3444885d", + "url": "https://api.github.com/repos/mockery/mockery/zipball/967a801bd188989a5669bd280f252d51c0fdc9ee", + "reference": "967a801bd188989a5669bd280f252d51c0fdc9ee", "shasum": "" }, "require": { @@ -12884,7 +12881,7 @@ "security": "https://github.com/mockery/mockery/security/advisories", "source": "https://github.com/mockery/mockery" }, - "time": "2026-08-15T03:07:32+00:00" + "time": "2026-08-19T19:37:52+00:00" }, { "name": "myclabs/deep-copy", @@ -13958,16 +13955,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "2.3.3", + "version": "2.3.5", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "fb19eedd2bb67ff8cf7a5502ad329e701d6398a3" + "reference": "148cefffaf0233e4c08cc13db8a195a56dd6dfe9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fb19eedd2bb67ff8cf7a5502ad329e701d6398a3", - "reference": "fb19eedd2bb67ff8cf7a5502ad329e701d6398a3", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/148cefffaf0233e4c08cc13db8a195a56dd6dfe9", + "reference": "148cefffaf0233e4c08cc13db8a195a56dd6dfe9", "shasum": "" }, "require": { @@ -13999,9 +13996,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.3" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.5" }, - "time": "2026-07-08T07:01:06+00:00" + "time": "2026-08-31T16:05:28+00:00" }, { "name": "phpunit/php-code-coverage", @@ -14093,23 +14090,23 @@ }, { "name": "phpunit/php-file-iterator", - "version": "6.0.1", + "version": "6.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5" + "reference": "a248d1640ab059b075f53a2ef0f9856e864e06b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", - "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a248d1640ab059b075f53a2ef0f9856e864e06b5", + "reference": "a248d1640ab059b075f53a2ef0f9856e864e06b5", "shasum": "" }, "require": { "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^12.5.33" }, "type": "library", "extra": { @@ -14142,7 +14139,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.1" + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.2" }, "funding": [ { @@ -14162,7 +14159,7 @@ "type": "tidelift" } ], - "time": "2026-02-02T14:04:18+00:00" + "time": "2026-08-25T14:40:53+00:00" }, { "name": "phpunit/php-invoker", @@ -14810,24 +14807,24 @@ }, { "name": "sebastian/diff", - "version": "7.0.0", + "version": "7.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "7ab1ea946c012266ca32390913653d844ecd085f" + "reference": "cd4cabe39f8a4e8ee6818ba99f10a05561ea4ad6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f", - "reference": "7ab1ea946c012266ca32390913653d844ecd085f", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/cd4cabe39f8a4e8ee6818ba99f10a05561ea4ad6", + "reference": "cd4cabe39f8a4e8ee6818ba99f10a05561ea4ad6", "shasum": "" }, "require": { "php": ">=8.3" }, "require-dev": { - "phpunit/phpunit": "^12.0", - "symfony/process": "^7.2" + "phpunit/phpunit": "^12.5.33", + "symfony/process": "^7.4.17" }, "type": "library", "extra": { @@ -14865,15 +14862,27 @@ "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", "security": "https://github.com/sebastianbergmann/diff/security/policy", - "source": "https://github.com/sebastianbergmann/diff/tree/7.0.0" + "source": "https://github.com/sebastianbergmann/diff/tree/7.0.1" }, "funding": [ { "url": "https://github.com/sebastianbergmann", "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/diff", + "type": "tidelift" } ], - "time": "2025-02-07T04:55:46+00:00" + "time": "2026-08-25T15:35:54+00:00" }, { "name": "sebastian/environment", @@ -16123,5 +16132,5 @@ "platform-overrides": { "php": "8.4" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.3.0" } diff --git a/config/app.php b/config/app.php index ca2cf8a7c2..9c62530e8c 100644 --- a/config/app.php +++ b/config/app.php @@ -37,7 +37,7 @@ |-------------------------------------------------------------------------- */ - 'version' => 'v2609.0.0', + 'version' => 'v2609.0.1', /* |-------------------------------------------------------------------------- diff --git a/database/migrations/2026_09_07_000001_add_mime_type_to_das_potensi_table.php b/database/migrations/2026_09_07_000001_add_mime_type_to_das_potensi_table.php new file mode 100644 index 0000000000..b94dc4c0fc --- /dev/null +++ b/database/migrations/2026_09_07_000001_add_mime_type_to_das_potensi_table.php @@ -0,0 +1,32 @@ +string('mime_type', 50)->nullable()->after('file_gambar'); + } + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('das_potensi', function (Blueprint $table) { + if (Schema::hasColumn('das_potensi', 'mime_type')) { + $table->dropColumn('mime_type'); + } + }); + } +}; diff --git a/database/migrations/2026_09_07_000001_add_penduduk_id_gabungan_to_suplemen_terdata_table.php b/database/migrations/2026_09_07_000001_add_penduduk_id_gabungan_to_suplemen_terdata_table.php new file mode 100644 index 0000000000..e3317c02a7 --- /dev/null +++ b/database/migrations/2026_09_07_000001_add_penduduk_id_gabungan_to_suplemen_terdata_table.php @@ -0,0 +1,59 @@ +unsignedBigInteger('penduduk_id_gabungan')->nullable()->after('penduduk_id'); + $table->unsignedInteger('penduduk_id')->nullable()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down() + { + Schema::table('das_suplemen_terdata', function (Blueprint $table) { + $table->dropColumn('penduduk_id_gabungan'); + $table->unsignedInteger('penduduk_id')->nullable(false)->change(); + }); + } +} diff --git a/database/migrations/2026_09_07_000002_add_desa_id_to_suplemen_terdata_table.php b/database/migrations/2026_09_07_000002_add_desa_id_to_suplemen_terdata_table.php new file mode 100644 index 0000000000..6790d9b0ea --- /dev/null +++ b/database/migrations/2026_09_07_000002_add_desa_id_to_suplemen_terdata_table.php @@ -0,0 +1,60 @@ +string('desa_id', 13)->nullable()->after('suplemen_id'); + }); + + DB::statement('UPDATE das_suplemen_terdata st JOIN das_penduduk p ON p.id = st.penduduk_id SET st.desa_id = p.desa_id WHERE st.desa_id IS NULL'); + } + + /** + * Reverse the migrations. + */ + public function down() + { + Schema::table('das_suplemen_terdata', function (Blueprint $table) { + $table->dropColumn('desa_id'); + }); + } +} diff --git a/database/migrations/2026_09_08_055444_add_penduduk_id_gabungan_to_lembaga_tables.php b/database/migrations/2026_09_08_055444_add_penduduk_id_gabungan_to_lembaga_tables.php new file mode 100644 index 0000000000..fa3e4c9d3a --- /dev/null +++ b/database/migrations/2026_09_08_055444_add_penduduk_id_gabungan_to_lembaga_tables.php @@ -0,0 +1,40 @@ +unsignedInteger('penduduk_id_gabungan')->nullable()->after('penduduk_id'); + }); + + Schema::table('das_lembaga_anggota', function (Blueprint $table) { + $table->unsignedInteger('penduduk_id_gabungan')->nullable()->after('penduduk_id'); + $table->dropForeign(['penduduk_id']); + $table->unsignedInteger('penduduk_id')->nullable()->change(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('das_lembaga', function (Blueprint $table) { + $table->dropColumn('penduduk_id_gabungan'); + }); + + Schema::table('das_lembaga_anggota', function (Blueprint $table) { + $table->unsignedInteger('penduduk_id')->nullable(false)->change(); + $table->foreign('penduduk_id')->references('id')->on('das_penduduk')->onDelete('cascade'); + $table->dropColumn('penduduk_id_gabungan'); + }); + } +}; diff --git a/helpers/general_helper.php b/helpers/general_helper.php index 8d6fc0f01d..badfabeb92 100644 --- a/helpers/general_helper.php +++ b/helpers/general_helper.php @@ -42,7 +42,7 @@ use voku\helper\AntiXSS; use willvincent\Feeds\Facades\FeedsFacade; -/** +/* * Parsing url image dari rss feed description * * @param string $content @@ -60,9 +60,10 @@ function get_tag_image(string $content) } /** - * { function_description } + * { function_description }. * * @param $parent_id The parent identifier + * * @return ( description_of_the_return_value ) */ function define_child($parent_id) @@ -73,10 +74,11 @@ function define_child($parent_id) } /** - * { function_description } + * { function_description }. * * @param $id The identifier * @param $permission The permission + * * @return ( description_of_the_return_value ) */ function permission_val($id, $permission) @@ -98,9 +100,10 @@ function permission_name($name) } /** - * Generate Password + * Generate Password. * * @param int $length Length Character + * * @return string voucher */ function generate_password($length = 6) @@ -122,7 +125,7 @@ function generate_password($length = 6) } /** - * Respon Meta + * Respon Meta. * * @param $message The message */ @@ -311,7 +314,9 @@ function is_wajib_ktp($umur, $status_kawin) function isThumbnail($url = null, $img = '/img/no-image.png') { - return ! empty($url) && Storage::disk('public')->exists($url) ? Storage::disk('public')->url($url) : asset($img); + $url = trim($url ?? ''); + + return ! empty($url) && Storage::disk('public')->exists($url) ? asset('storage/' . ltrim($url, '/')) : asset($img); } function is_img($url = null, $img = '/img/no-image.png') @@ -389,7 +394,7 @@ function terbilang($angka) if (! function_exists('sudahInstal')) { /** - * Cek apakah sudah install OpenDK atau belum + * Cek apakah sudah install OpenDK atau belum. * * @return bool True jika sudah install, False jika belum install */ @@ -405,7 +410,7 @@ function sudahInstal(): bool if (! function_exists('isActive')) { /** - * Helper function untuk installer views - cek apakah route aktif + * Helper function untuk installer views - cek apakah route aktif. * * @param string $routeName */ @@ -429,7 +434,7 @@ function isActive($routeName): string } } -/** +/* * Cek akses website. * * @param string $url @@ -488,7 +493,7 @@ function parsedown($params = null) if (! function_exists('theme_new')) { /** - * Ambil model tema + * Ambil model tema. * * @return Theme */ @@ -753,3 +758,32 @@ function kembalikanSlug($str): ?string // Ganti '-' dengan spasi dan hilangkan titik '.' return str_replace(['-', '.'], [' ', ''], $str); } + +if (! function_exists('list_desa')) { + function list_desa(bool $all = false) + { + return (new \App\Services\DesaService)->listDesa($all)->pluck('nama', 'desa_id'); + } +} + +if (! function_exists('nama_desa')) { + function nama_desa(string $desaId): ?string + { + return list_desa()[$desaId] ?? null; + } +} + +if (! function_exists('is_pdf')) { + function is_pdf($path = null, $mime = null): bool + { + if ($mime && str_contains(strtolower($mime), 'pdf')) { + return true; + } + + if ($path && str_ends_with(strtolower($path), '.pdf')) { + return true; + } + + return false; + } +} diff --git a/package-lock.json b/package-lock.json index 7cd4482547..9e20c889d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -807,9 +807,9 @@ } }, "node_modules/fast-uri": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", - "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.7.tgz", + "integrity": "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==", "dev": true, "funding": [ { @@ -820,8 +820,7 @@ "type": "opencollective", "url": "https://opencollective.com/fastify" } - ], - "license": "BSD-3-Clause" + ] }, "node_modules/fastq": { "version": "1.20.1", @@ -1630,11 +1629,10 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", "dev": true, - "license": "MIT", "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" diff --git a/public/css/admin-style.css b/public/css/admin-style.css index 187c0bdd15..82fdfae93f 100644 --- a/public/css/admin-style.css +++ b/public/css/admin-style.css @@ -92,12 +92,18 @@ body.sidebar-collapse .sidebar-kabupaten { /* Prevent dropdown clipping inside DataTable responsive wrapper */ .box-body .table-responsive { - overflow: visible !important; + overflow-x: auto; } .box-body .table-responsive .dropdown-menu { z-index: 1050; } +/* Ensure nowrap tables do not wrap cells and preserve header spacing */ +.table.nowrap th, +.table.nowrap td { + white-space: nowrap; +} + /* Simple centered modal for activity detail */ .modal-overlay { position: fixed; diff --git a/public/js/admin.js b/public/js/admin.js index 445a5c445a..1e2cb30252 100644 --- a/public/js/admin.js +++ b/public/js/admin.js @@ -5,6 +5,61 @@ $(function() { 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content') } }); + + // Fix dropdown clipping inside scrollable DataTables and responsive tables + $(document).on('shown.bs.dropdown', '.table-responsive, .dataTables_scrollBody', function(e) { + var $dropdown = $(e.target); + var $menu = $dropdown.find('.dropdown-menu'); + if (!$menu.length) return; + + $('body').append($menu.detach()); + + function updateDropdownPosition() { + if (!$dropdown.is(':visible') || !$dropdown.hasClass('open')) { + $dropdown.trigger('hide.bs.dropdown'); + return; + } + var offset = $dropdown.offset(); + var menuWidth = $menu.outerWidth(); + var left = offset.left; + + if (left + menuWidth > $(window).width()) { + left = offset.left + $dropdown.outerWidth() - menuWidth; + } + + $menu.css({ + display: 'block', + position: 'absolute', + top: (offset.top + $dropdown.outerHeight()) + 'px', + left: left + 'px', + zIndex: 1060 + }); + } + + updateDropdownPosition(); + $dropdown.data('detached-dropdown-menu', $menu); + + $(window).on('scroll.dropdown_fix resize.dropdown_fix', updateDropdownPosition); + $('.dataTables_scrollBody, .table-responsive').on('scroll.dropdown_fix', updateDropdownPosition); + }); + + $(document).on('hide.bs.dropdown', '.table-responsive, .dataTables_scrollBody', function(e) { + var $dropdown = $(e.target); + var $menu = $dropdown.data('detached-dropdown-menu'); + if ($menu && $menu.length) { + $dropdown.append($menu.detach()); + $menu.css({ + display: '', + position: '', + top: '', + left: '', + zIndex: '' + }); + $dropdown.removeData('detached-dropdown-menu'); + } + $(window).off('.dropdown_fix'); + $('.dataTables_scrollBody, .table-responsive').off('.dropdown_fix'); + }); }); function errorValidation(response) { diff --git a/resources/views/backend/event/form_create.blade.php b/resources/views/backend/event/form_create.blade.php index a6656889ca..e72efaa426 100644 --- a/resources/views/backend/event/form_create.blade.php +++ b/resources/views/backend/event/form_create.blade.php @@ -40,32 +40,41 @@ + @endpush +@endonce diff --git a/resources/views/components/penduduk-gabungan-select.blade.php b/resources/views/components/penduduk-gabungan-select.blade.php new file mode 100644 index 0000000000..fcee29704e --- /dev/null +++ b/resources/views/components/penduduk-gabungan-select.blade.php @@ -0,0 +1,77 @@ +@props([ + 'name' => 'penduduk_id', + 'id' => 'penduduk_id_gabungan', + 'selected' => null, + 'selectedText' => null, + 'placeholder' => 'Pilih Ketua Lembaga', + 'required' => false, + 'apiServerDatabase' => null, + 'apiKeyDatabase' => null, + 'kodeKecamatan' => null, +]) + +@php + $apiServerDatabase = $apiServerDatabase ?? ($settings['api_server_database_gabungan'] ?? ''); + $apiKeyDatabase = $apiKeyDatabase ?? ($settings['api_key_database_gabungan'] ?? ''); + $kodeKecamatan = $kodeKecamatan ?? (isset($profil) ? str_replace('.', '', $profil->kecamatan_id) : ''); +@endphp + +
+ +
+ +@push('scripts') + +@endpush diff --git a/resources/views/data/aki_akb/index.blade.php b/resources/views/data/aki_akb/index.blade.php index 1b3f012ee2..f251ca0e2a 100644 --- a/resources/views/data/aki_akb/index.blade.php +++ b/resources/views/data/aki_akb/index.blade.php @@ -55,7 +55,10 @@ url: "{!! route('data.aki-akb.getdata') !!}", type: 'POST', data: function(d) { - d.desa_id = $('#list_desa').val(); + var desaId = $('#list_desa').val(); + if (desaId && desaId != 'Semua') { + d.desa = desaId; + } } }, columns: [{ @@ -94,8 +97,9 @@ class: 'text-center', }); $('#list_desa').on('select2:select', function(e) { - data.columns(1).search(this.value).draw(); + data.ajax.reload(); }); + }); @include('forms.datatable-vertical') diff --git a/resources/views/data/anggaran_desa/index.blade.php b/resources/views/data/anggaran_desa/index.blade.php index 3a445b4c44..38467f4526 100644 --- a/resources/views/data/anggaran_desa/index.blade.php +++ b/resources/views/data/anggaran_desa/index.blade.php @@ -55,7 +55,10 @@ url: "{!! route('data.anggaran-desa.getdata') !!}", type: "POST", data: function(d) { - d.desa = $('#list_desa').val(); + var desaId = $('#list_desa').val(); + if (desaId && desaId != 'Semua') { + d.desa = desaId; + } } }, columns: [{ diff --git a/resources/views/data/data_sarana/index.blade.php b/resources/views/data/data_sarana/index.blade.php index 20f38c3e07..7c981874aa 100644 --- a/resources/views/data/data_sarana/index.blade.php +++ b/resources/views/data/data_sarana/index.blade.php @@ -50,7 +50,7 @@ - + @@ -76,8 +76,11 @@ url: "{{ route('data.data-sarana.getdata') }}", type: "POST", data: function(d) { - d.desa_id = $('#list_desa').val(), - d.kategori = $('#kategori').val() + var desaId = $('#list_desa').val(); + if (desaId && desaId != 'Semua') { + d.desa = desaId; + } + d.kategori = $('#kategori').val() } }, columns: [{ @@ -85,7 +88,7 @@ name: 'aksi', orderable: false, searchable: false, - className: 'text-center' + className: 'text-center text-nowrap' }, { data: 'nama', diff --git a/resources/views/data/data_sarana/partials/action.blade.php b/resources/views/data/data_sarana/partials/action.blade.php index 3cb9ac1eb5..7acc2d872a 100644 --- a/resources/views/data/data_sarana/partials/action.blade.php +++ b/resources/views/data/data_sarana/partials/action.blade.php @@ -1,17 +1,13 @@ -
- - +
+ + -
-
- @csrf - @method('DELETE') - - -
+
+ @csrf + @method('DELETE') + +
diff --git a/resources/views/data/data_suplemen/create.blade.php b/resources/views/data/data_suplemen/create.blade.php index 6b0ff3363f..db9bc47502 100644 --- a/resources/views/data/data_suplemen/create.blade.php +++ b/resources/views/data/data_suplemen/create.blade.php @@ -19,7 +19,7 @@ {!! html()->form('POST', route('data.data-suplemen.store'))->id('form-faq')->class( 'form-horizontal - form-label-left', + form-label-left', )->open() !!}
diff --git a/resources/views/data/data_suplemen/form.blade.php b/resources/views/data/data_suplemen/form.blade.php index 9b80a9c4b2..8c6fed1e8d 100644 --- a/resources/views/data/data_suplemen/form.blade.php +++ b/resources/views/data/data_suplemen/form.blade.php @@ -18,8 +18,8 @@
{!! html()->textarea('keterangan')->class('textarea')->style( 'width: 100%; height: 200px; font-size: 14px; - line-height: 18px; border: 1px solid #dddddd; padding: - 10px;', + line-height: 18px; border: 1px solid #dddddd; padding: + 10px;', )->placeholder('Keterangan')->value(old('keterangan', isset($suplemen) ? $suplemen->keterangan : '')) !!}
diff --git a/resources/views/data/data_suplemen/form_detail.blade.php b/resources/views/data/data_suplemen/form_detail.blade.php index 22239183d3..2b833dbd02 100644 --- a/resources/views/data/data_suplemen/form_detail.blade.php +++ b/resources/views/data/data_suplemen/form_detail.blade.php @@ -1,3 +1,9 @@ +@php + $isDatabaseGabungan = $isDatabaseGabungan ?? false; + $pendudukField = $isDatabaseGabungan ? 'penduduk_id_gabungan' : 'penduduk_id'; + $selectedDesaId = $selectedDesaId ?? null; + $selectedPendudukId = $selectedPendudukId ?? null; +@endphp {{ html()->hidden('suplemen_id', $suplemen->id) }} @if ($suplemen->sasaran == 3)
@@ -7,11 +13,7 @@
@@ -20,16 +22,12 @@
- @foreach ($data as $penduduk) - @if ($anggota == null) - - @else - - @endif + @endforeach
@@ -43,32 +41,25 @@
- +
- @foreach ($data as $penduduk) - @if ($anggota == null) - - @else - - @endif + @endforeach
diff --git a/resources/views/data/data_suplemen/gabungan/create_detail.blade.php b/resources/views/data/data_suplemen/gabungan/create_detail.blade.php new file mode 100644 index 0000000000..0a9f977891 --- /dev/null +++ b/resources/views/data/data_suplemen/gabungan/create_detail.blade.php @@ -0,0 +1,177 @@ +@extends('layouts.dashboard_template') + +@section('content') +
+

+ {{ $page_title ?? 'Page Title' }} + {{ $page_description ?? '' }} +

+ +
+
+
+
+
+ + {!! html()->form('POST', route('data.data-suplemen.storedetail'))->id('form-suiplemen-gabungan')->class('form-horizontal form-label-left')->open() !!} + +
+
+
AksiAksi Nama Sarana Jumlah Kategori
+ + + + + + + + + + + + +
Nama: {{ $suplemen->nama }}
Sasaran: {{ $sasaran[$suplemen->sasaran] }}
Keterangan: {{ $suplemen->keterangan }}
+ +
+ Daftar Anggota Suplemen + + @if (count($errors) > 0) +
+ Ups! Ada beberapa masalah dengan masukan Anda.

+
    + @foreach ($errors->all() as $error) +
  • {{ $error }}
  • + @endforeach +
+
+ @endif + + @include('flash::message') + + {{ html()->hidden('suplemen_id', $suplemen->id) }} + +
+ + +
+ @include('layouts.fragments.select-desa', ['selectAttributes' => ['name' => 'desa_id', 'required' => 'required']]) +
+
+ +
+ + +
+ +
+
+ +
+ + +
+ {!! html()->textarea('keterangan')->class('textarea')->placeholder('Keterangan')->value(old('keterangan'))->style('width: 100%; height: 200px; font-size: 14px; line-height: 18px; border: 1px solid #dddddd; padding: 10px;') !!} +
+
+
+ + + {!! html()->form()->close() !!} + + + + +@endsection + +@include('partials.asset_select2') + +@push('scripts') + +@endpush diff --git a/resources/views/data/data_suplemen/gabungan/edit_detail.blade.php b/resources/views/data/data_suplemen/gabungan/edit_detail.blade.php new file mode 100644 index 0000000000..b5d05f0f7a --- /dev/null +++ b/resources/views/data/data_suplemen/gabungan/edit_detail.blade.php @@ -0,0 +1,187 @@ +@extends('layouts.dashboard_template') + +@section('content') +
+

+ {{ $page_title ?? 'Page Title' }} + {{ $page_description ?? '' }} +

+ +
+
+
+
+
+ + @if (count($errors) > 0) +
+ Ups! Ada beberapa masalah dengan masukan Anda.

+
    + @foreach ($errors->all() as $error) +
  • {{ $error }}
  • + @endforeach +
+
+ @endif + + + {!! html()->form('post', route('data.data-suplemen.updatedetail', $anggota->id))->id('form-suiplemen-gabungan')->class('form-horizontal form-label-left')->open() !!} + +
+
+ + + + + + + + + + + + + +
Nama: {{ $suplemen->nama }}
Sasaran: {{ $sasaran[$suplemen->sasaran] }}
Keterangan: {{ $suplemen->keterangan }}
+
+
+ Daftar Anggota Suplemen + + {{ method_field('PUT') }} + @include('flash::message') + + {{ html()->hidden('suplemen_id', $suplemen->id) }} + +
+ + +
+ @include('layouts.fragments.select-desa', ['selectAttributes' => ['name' => 'desa_id', 'required' => 'required'], 'selectedOption' => $selectedDesaId]) +
+
+ +
+ + +
+ +
+
+ +
+ + +
+ {!! html()->textarea('keterangan')->class('textarea')->placeholder('Keterangan')->value(old('keterangan', $anggota->keterangan))->style('width: 100%; height: 200px; font-size: 14px; line-height: 18px; border: 1px solid #dddddd; padding: 10px;') !!} +
+
+
+ + + {!! html()->form()->close() !!} +
+
+
+
+@endsection + +@include('partials.asset_select2') + +@push('scripts') + +@endpush diff --git a/resources/views/data/data_suplemen/gabungan/show.blade.php b/resources/views/data/data_suplemen/gabungan/show.blade.php new file mode 100644 index 0000000000..4b88bcdbb4 --- /dev/null +++ b/resources/views/data/data_suplemen/gabungan/show.blade.php @@ -0,0 +1,162 @@ +@extends('layouts.dashboard_template') + +@section('content') +
+

+ {{ $page_title ?? 'Page Title' }} + {{ $page_description ?? '' }} +

+ +
+
+ + @include('partials.flash_message') + +
+
+ +  Tambah + + @include('forms.btn-social', [ + 'export_url' => route('data.data-suplemen.export-terdata-excel', $suplemen->id), + ]) +
+
+
+ + + + + + + + + + + + + +
Nama: {{ $suplemen->nama }}
Sasaran: {{ $sasaran[$suplemen->sasaran] }}
Keterangan: {{ $suplemen->keterangan }}
+
+
+ Daftar Anggota Suplemen + @include('layouts.fragments.list-desa') +
+
+ + + + + + + + + + + + + + + +
Aksi{{ config('setting.sebutan_desa') }}No. KKNIK PendudukNama PendudukTempat LahirTanggal LahirJenis KelaminAlamatKeterangan
+
+
+
+
+@endsection + +@include('partials.asset_select2') +@include('partials.asset_datatables') + +@push('scripts') + + + + @include('forms.datatable-vertical') + @include('forms.delete-modal') +@endpush diff --git a/resources/views/data/data_suplemen/show.blade.php b/resources/views/data/data_suplemen/show.blade.php index 6b9361fb7f..a4778e1ccf 100644 --- a/resources/views/data/data_suplemen/show.blade.php +++ b/resources/views/data/data_suplemen/show.blade.php @@ -46,6 +46,7 @@
Daftar Anggota Suplemen + @include('layouts.fragments.list-desa')
@@ -131,6 +132,20 @@ class: 'text-center', }); }); + + @include('forms.datatable-vertical') @include('forms.delete-modal') @endpush diff --git a/resources/views/data/epidemi_penyakit/index.blade.php b/resources/views/data/epidemi_penyakit/index.blade.php index 85530fa9a9..f2a34390bb 100644 --- a/resources/views/data/epidemi_penyakit/index.blade.php +++ b/resources/views/data/epidemi_penyakit/index.blade.php @@ -62,7 +62,10 @@ url: "{!! route('data.epidemi-penyakit.getdata') !!}", type: 'POST', data: function(d) { - d.desa = $('#list_desa').val(); + var desaId = $('#list_desa').val(); + if (desaId && desaId != 'Semua') { + d.desa = desaId; + } } }, columns: [{ @@ -99,7 +102,7 @@ class: 'text-center', }); $('#list_desa').on('select2:select', function(e) { - data.columns(1).search(this.value).draw(); + data.ajax.reload(); }); }); diff --git a/resources/views/data/fasilitas_paud/edit.blade.php b/resources/views/data/fasilitas_paud/edit.blade.php index 1a34d1dcfb..c088134a54 100644 --- a/resources/views/data/fasilitas_paud/edit.blade.php +++ b/resources/views/data/fasilitas_paud/edit.blade.php @@ -29,7 +29,7 @@ @endif - {!! html()->form()->open() !!} + {!! html()->form('PUT', route('data.fasilitas-paud.update', $fasilitas->id))->class('form-horizontal form-label-left')->open() !!}
diff --git a/resources/views/data/fasilitas_paud/import.blade.php b/resources/views/data/fasilitas_paud/import.blade.php index 10ecfde187..b28ec40633 100644 --- a/resources/views/data/fasilitas_paud/import.blade.php +++ b/resources/views/data/fasilitas_paud/import.blade.php @@ -21,7 +21,7 @@ {!! html()->form('POST', route('data.fasilitas-paud.do_import'))->id('form-import')->class( 'form-horizontal - form-label-left', + form-label-left', )->acceptsFiles()->open() !!}
@@ -43,11 +43,7 @@
- + @include('layouts.fragments.select-desa', ['selectAttributes' => ['name' => 'desa_id', 'data-width' => '100%', 'required' => 'required']])
@@ -106,6 +102,7 @@ @push('scripts') diff --git a/resources/views/data/keluarga/index.blade.php b/resources/views/data/keluarga/index.blade.php index 93ec0a1cba..08d1f3e36c 100644 --- a/resources/views/data/keluarga/index.blade.php +++ b/resources/views/data/keluarga/index.blade.php @@ -68,7 +68,10 @@ url: "{!! route('data.keluarga.getdata') !!}", type: "POST", data: function(d) { - d.desa = $('#list_desa').val(); + var desaId = $('#list_desa').val(); + if (desaId && desaId != 'Semua') { + d.desa = desaId; + } } }, columns: [{ diff --git a/resources/views/data/laporan-apbdes/index.blade.php b/resources/views/data/laporan-apbdes/index.blade.php index ba3e81786a..0d3117c376 100644 --- a/resources/views/data/laporan-apbdes/index.blade.php +++ b/resources/views/data/laporan-apbdes/index.blade.php @@ -72,7 +72,10 @@ url: "{!! route('data.laporan-apbdes.getdata') !!}", type: "POST", data: function(d) { - d.desa = $('#list_desa').val(); + var desaId = $('#list_desa').val(); + if (desaId && desaId != 'Semua') { + d.desa = desaId; + } } }, columns: [{ diff --git a/resources/views/data/laporan-penduduk/gabungan/index.blade.php b/resources/views/data/laporan-penduduk/gabungan/index.blade.php index cfca0ad8b9..84a843b7d1 100644 --- a/resources/views/data/laporan-penduduk/gabungan/index.blade.php +++ b/resources/views/data/laporan-penduduk/gabungan/index.blade.php @@ -99,7 +99,7 @@ const _url = data.attributes.path === undefined ? `{{ route('data.laporan-penduduk.export-excel.by-id', ['data' => '__DATA__']) }}` .replace('__DATA__', jsonData) : - `asset('storage/laporan_penduduk')/${data.nama_file}` + data.attributes.path const _disabled = data.attributes.path === undefined ? 'disabled' : '' return ` diff --git a/resources/views/data/laporan-penduduk/index.blade.php b/resources/views/data/laporan-penduduk/index.blade.php index d28337509a..c041616724 100644 --- a/resources/views/data/laporan-penduduk/index.blade.php +++ b/resources/views/data/laporan-penduduk/index.blade.php @@ -68,7 +68,10 @@ url: "{!! route('data.laporan-penduduk.getdata') !!}", type: "POST", data: function(d) { - d.desa = $('#list_desa').val(); + var desaId = $('#list_desa').val(); + if (desaId && desaId != 'Semua') { + d.desa = desaId; + } } }, columns: [{ diff --git a/resources/views/data/lembaga/gabungan/create.blade.php b/resources/views/data/lembaga/gabungan/create.blade.php new file mode 100644 index 0000000000..574224888c --- /dev/null +++ b/resources/views/data/lembaga/gabungan/create.blade.php @@ -0,0 +1,37 @@ +@extends('layouts.dashboard_template') + +@section('content') +
+

+ {{ $page_title ?? 'Page Title' }} + {{ $page_description ?? '' }} +

+
+
+
+
+
+
+ + {!! html()->form('POST', route('data.lembaga.store'))->id('form-lembaga')->class('form-horizontal form-label-left')->open() !!} + @include('layouts.fragments.error_message') +
+ + @include('flash::message') + @include('data.lembaga.gabungan.form_create') + +
+ + {!! html()->form()->close() !!} +
+
+
+
+@endsection + +@include('partials.tinymce_min') diff --git a/resources/views/data/lembaga/gabungan/edit.blade.php b/resources/views/data/lembaga/gabungan/edit.blade.php new file mode 100644 index 0000000000..b9b439f6d0 --- /dev/null +++ b/resources/views/data/lembaga/gabungan/edit.blade.php @@ -0,0 +1,37 @@ +@extends('layouts.dashboard_template') + +@section('content') +
+

+ {{ $page_title ?? 'Page Title' }} + {{ $page_description ?? '' }} +

+ +
+
+
+
+
+ + {!! html()->form('PUT', route('data.lembaga.update', $lembaga->id))->id('form-lembaga')->class('form-horizontal form-label-left')->open() !!} + @include('layouts.fragments.error_message') +
+ + @include('flash::message') + @include('data.lembaga.gabungan.form_edit') + +
+ + {!! html()->form()->close() !!} +
+
+
+
+@endsection + +@include('partials.tinymce_min') diff --git a/resources/views/data/lembaga/gabungan/form_create.blade.php b/resources/views/data/lembaga/gabungan/form_create.blade.php new file mode 100644 index 0000000000..0b8ac0d810 --- /dev/null +++ b/resources/views/data/lembaga/gabungan/form_create.blade.php @@ -0,0 +1,51 @@ +@include('partials.asset_select2') +
+ + +
+ {!! html()->text('nama')->value(old('nama', isset($lembaga) ? $lembaga->nama : ''))->class('form-control')->required()->placeholder('Nama Lembaga') !!} +
+
+ +
+ + +
+ {!! html()->text('kode')->value(old('kode', isset($lembaga) ? $lembaga->kode : ''))->class('form-control')->required()->placeholder('Kode Lembaga') !!} +
+
+ +
+ + +
+ +
+
+ +
+ + +
+ +
+
+ +
+ + +
+ {!! html()->textarea('keterangan')->value(old('keterangan', isset($lembaga) ? $lembaga->keterangan : ''))->class('form-control')->placeholder('Deskripsi Lembaga')->rows(2) !!} +
+
+ +
+ +@include('partials.asset_jqueryvalidation') diff --git a/resources/views/data/lembaga/gabungan/form_edit.blade.php b/resources/views/data/lembaga/gabungan/form_edit.blade.php new file mode 100644 index 0000000000..5a6574fe65 --- /dev/null +++ b/resources/views/data/lembaga/gabungan/form_edit.blade.php @@ -0,0 +1,55 @@ +@include('partials.asset_select2') +
+ + +
+ {!! html()->text('nama')->value(old('nama', isset($lembaga) ? $lembaga->nama : ''))->class('form-control')->required()->placeholder('Nama Lembaga') !!} +
+
+ +
+ + +
+ {!! html()->text('kode')->value(old('kode', isset($lembaga) ? $lembaga->kode : ''))->class('form-control')->required()->placeholder('Kode Lembaga') !!} +
+
+ +
+ + +
+ +
+
+ +
+ + +
+ +
+
+ +
+ + +
+ {!! html()->textarea('keterangan')->value(old('keterangan', isset($lembaga) ? $lembaga->keterangan : ''))->class('form-control')->placeholder('Deskripsi Lembaga')->rows(2) !!} +
+
+ +
+ +@include('partials.asset_jqueryvalidation') + +@push('scripts') + {!! JsValidator::formRequest('App\Http\Requests\UpdateLembagaRequest', '#form-lembaga') !!} +@endpush diff --git a/resources/views/data/lembaga_anggota/gabungan/create.blade.php b/resources/views/data/lembaga_anggota/gabungan/create.blade.php new file mode 100644 index 0000000000..586f5bf4e8 --- /dev/null +++ b/resources/views/data/lembaga_anggota/gabungan/create.blade.php @@ -0,0 +1,41 @@ +@extends('layouts.dashboard_template') + +@section('content') +
+

+ {{ $page_title ?? 'Page Title' }} + {{ $page_description ?? '' }} +

+ +
+
+
+
+
+ + {!! html()->form()->route('data.lembaga_anggota.store', $lembaga->slug)->method('POST')->id('form-lembaga-anggota')->class('form-horizontal form-label-left')->open() !!} + @include('layouts.fragments.error_message') + +
+ + @include('flash::message') + @include('data.lembaga_anggota.gabungan.form_create') + +
+ + {!! html()->form()->close() !!} +
+
+
+
+@endsection + +@include('partials.tinymce_min') diff --git a/resources/views/data/lembaga_anggota/gabungan/edit.blade.php b/resources/views/data/lembaga_anggota/gabungan/edit.blade.php new file mode 100644 index 0000000000..62202df849 --- /dev/null +++ b/resources/views/data/lembaga_anggota/gabungan/edit.blade.php @@ -0,0 +1,48 @@ +@extends('layouts.dashboard_template') + +@section('content') +
+

+ {{ $page_title ?? 'Page Title' }} + {{ $page_description ?? '' }} +

+ +
+
+
+
+
+ + {!! html()->form( + 'PUT', + route('data.lembaga_anggota.update', [ + 'id' => $anggota->id, + 'slug' => $lembaga->slug, + ]), + )->id('form-lembaga-anggota')->class('form-horizontal form-label-left')->open() !!} + + @include('layouts.fragments.error_message') + +
+ + @include('flash::message') + @include('data.lembaga_anggota.gabungan.form_edit') + +
+ + {!! html()->form()->close() !!} +
+
+
+
+@endsection + +@include('partials.tinymce_min') diff --git a/resources/views/data/lembaga_anggota/gabungan/form_create.blade.php b/resources/views/data/lembaga_anggota/gabungan/form_create.blade.php new file mode 100644 index 0000000000..273b10452a --- /dev/null +++ b/resources/views/data/lembaga_anggota/gabungan/form_create.blade.php @@ -0,0 +1,96 @@ +@include('partials.asset_select2') +
+ + +
+ +
+
+
+ + +
+ {!! html()->number('no_anggota')->value(old('no_anggota', isset($anggota) ? $anggota->no_anggota : ''))->class('form-control')->required()->placeholder('Nomor Anggota')->attribute('min', 1) !!} + *Pastikan nomor anggota belum pernah digunakan ! +
+
+ +
+ + +
+
+ {!! html()->select('jabatan_id', [ + 1 => 'Ketua', + 2 => 'Wakil Ketua', + 3 => 'Sekretaris', + 4 => 'Bendahara', + 5 => 'Anggota', + ])->value(old('jabatan_id', isset($anggota) ? $anggota->jabatan : ''))->placeholder('Pilih Jabatan')->class('form-control select2')->required()->style('width:100%;') !!} +
+
+
+ +
+ + +
+ {!! html()->text('no_sk_jabatan')->value(old('no_sk_jabatan', isset($anggota) ? $anggota->no_sk_jabatan : ''))->class('form-control')->placeholder('Nomor SK Jabatan') !!} +
+
+ +
+ + +
+ {!! html()->text('no_sk_pengangkatan')->value(old('no_sk_pengangkatan', isset($anggota) ? $anggota->no_sk_pengangkatan : ''))->class('form-control')->placeholder('Nomor SK Pengangkatan') !!} +
+
+ +
+ + +
+ {!! html()->date('tgl_sk_pengangkatan')->value(old('tgl_sk_pengangkatan', isset($anggota) ? $anggota->tgl_sk_pengangkatan : ''))->class('form-control')->placeholder('Tanggal SK Pengangkatan') !!} +
+
+ +
+ + +
+ {!! html()->text('no_sk_pemberhentian')->value(old('no_sk_pemberhentian', isset($anggota) ? $anggota->no_sk_pemberhentian : ''))->class('form-control')->placeholder('Nomor SK Pemberhentian') !!} +
+
+ +
+ + +
+ {!! html()->date('tgl_sk_pemberhentian')->value(old('tgl_sk_pemberhentian', isset($anggota) ? $anggota->tgl_sk_pemberhentian : ''))->class('form-control')->placeholder('Tanggal SK Pemberhentian') !!} +
+
+ +
+ + +
+ {!! html()->text('periode')->value(old('periode', isset($anggota) ? $anggota->periode : ''))->class('form-control')->placeholder('Contoh: 6 Tahun Periode Pertama (2015 s/d 2021)') !!} +
+
+ +
+ + +
+ {!! html()->textarea('keterangan')->value(old('keterangan', isset($anggota) ? $anggota->keterangan : ''))->class('form-control')->placeholder('Keterangan')->rows(2) !!} +
+
+ +
+ +@include('partials.asset_jqueryvalidation') + +@push('scripts') + {!! JsValidator::formRequest('App\Http\Requests\StoreLembagaAnggotaRequest', '#form-lembaga-anggota') !!} +@endpush diff --git a/resources/views/data/lembaga_anggota/gabungan/form_edit.blade.php b/resources/views/data/lembaga_anggota/gabungan/form_edit.blade.php new file mode 100644 index 0000000000..44d8a95478 --- /dev/null +++ b/resources/views/data/lembaga_anggota/gabungan/form_edit.blade.php @@ -0,0 +1,96 @@ +@include('partials.asset_select2') +
+ + +
+ +
+
+
+ + +
+ {!! html()->number('no_anggota')->value(old('no_anggota', $anggota->no_anggota ?? ''))->class('form-control')->required()->placeholder('Nomor Anggota')->attribute('min', 1) !!} + *Pastikan nomor anggota belum pernah digunakan ! +
+
+ +
+ + +
+
+ {!! html()->select('jabatan_id', [ + 1 => 'Ketua', + 2 => 'Wakil Ketua', + 3 => 'Sekretaris', + 4 => 'Bendahara', + 5 => 'Anggota', + ])->value(old('jabatan_id', isset($anggota) ? $anggota->jabatan : ''))->placeholder('Pilih Jabatan')->class('form-control select2')->required()->style('width:100%;') !!} +
+
+
+ +
+ + +
+ {!! html()->text('no_sk_jabatan')->value(old('no_sk_jabatan', $anggota->no_sk_jabatan ?? ''))->class('form-control')->placeholder('Nomor SK Jabatan') !!} +
+
+ +
+ + +
+ {!! html()->text('no_sk_pengangkatan')->value(old('no_sk_pengangkatan', $anggota->no_sk_pengangkatan ?? ''))->class('form-control')->placeholder('Nomor SK Pengangkatan') !!} +
+
+ +
+ + +
+ {!! html()->date('tgl_sk_pengangkatan')->value(old('tgl_sk_pengangkatan', $anggota->tgl_sk_pengangkatan ?? ''))->class('form-control')->placeholder('Tanggal SK Pengangkatan') !!} +
+
+ +
+ + +
+ {!! html()->text('no_sk_pemberhentian')->value(old('no_sk_pemberhentian', $anggota->no_sk_pemberhentian ?? ''))->class('form-control')->placeholder('Nomor SK Pemberhentian') !!} +
+
+ +
+ + +
+ {!! html()->date('tgl_sk_pemberhentian')->value(old('tgl_sk_pemberhentian', $anggota->tgl_sk_pemberhentian ?? ''))->class('form-control')->placeholder('Tanggal SK Pemberhentian') !!} +
+
+ +
+ + +
+ {!! html()->text('periode')->value(old('periode', $anggota->periode ?? ''))->class('form-control')->placeholder('Contoh: 6 Tahun Periode Pertama (2015 s/d 2021)') !!} +
+
+ +
+ + +
+ {!! html()->textarea('keterangan')->value(old('keterangan', $anggota->keterangan ?? ''))->class('form-control')->placeholder('Keterangan')->rows(2) !!} +
+
+ +
+ +@include('partials.asset_jqueryvalidation') + +@push('scripts') + {!! JsValidator::formRequest('App\Http\Requests\UpdateLembagaAnggotaRequest', '#form-lembaga-anggota') !!} +@endpush diff --git a/resources/views/data/lembaga_anggota/index.blade.php b/resources/views/data/lembaga_anggota/index.blade.php index a211db23d3..f2fe37317f 100644 --- a/resources/views/data/lembaga_anggota/index.blade.php +++ b/resources/views/data/lembaga_anggota/index.blade.php @@ -26,7 +26,7 @@
@include('forms.btn-social', [ - 'create_url' => auth()->user()->can('access.data.lembaga_anggota.create') ? route('data.lembaga_anggota.create', $lembaga->slug) : null, + 'create_url' => auth()->user()->can('access.data.lembaga.create') ? route('data.lembaga_anggota.create', $lembaga->slug) : null, ])
@@ -47,7 +47,7 @@
- + @@ -102,7 +102,6 @@ processing: true, serverSide: false, ordering: false, - scrollX: true, ajax: "{!! route('data.lembaga_anggota.getdata', $lembaga->slug) !!}", columns: [{ data: 'aksi', diff --git a/resources/views/data/pembangunan/index.blade.php b/resources/views/data/pembangunan/index.blade.php index 1e0886754a..544f3c2089 100644 --- a/resources/views/data/pembangunan/index.blade.php +++ b/resources/views/data/pembangunan/index.blade.php @@ -70,7 +70,10 @@ url: "{!! route('data.pembangunan.getdata') !!}", type: "POST", data: function(d) { - d.desa = $('#list_desa').val(); + var desaId = $('#list_desa').val(); + if (desaId && desaId != 'Semua') { + d.desa = desaId; + } } }, columns: [{ diff --git a/resources/views/data/penduduk/index.blade.php b/resources/views/data/penduduk/index.blade.php index 92b8abb723..801ac1fe82 100644 --- a/resources/views/data/penduduk/index.blade.php +++ b/resources/views/data/penduduk/index.blade.php @@ -72,7 +72,10 @@ url: "{!! route('data.penduduk.getdata') !!}", type: "POST", data: function(d) { - d.desa = $('#list_desa').val(); + var desaId = $('#list_desa').val(); + if (desaId && desaId != 'Semua') { + d.desa = desaId; + } } }, columns: [{ diff --git a/resources/views/data/pengurus/index.blade.php b/resources/views/data/pengurus/index.blade.php index c90bbf6381..e3be8e2488 100644 --- a/resources/views/data/pengurus/index.blade.php +++ b/resources/views/data/pengurus/index.blade.php @@ -4,6 +4,14 @@ Data Pengurus @endsection +@push('css') + +@endpush + @section('content')

@@ -44,18 +52,18 @@
-

Ketua Lembaga :{{ $lembaga->penduduk->nama }}{{ $lembaga->penduduk?->nama ?? ($lembaga->pendudukGabungan?->nama ?? '-') }}
Kategori Lembaga
+
- - - - + + + + - + @@ -79,6 +87,8 @@ var data = $('#pengurus-table').DataTable({ processing: true, serverSide: true, + scrollX: true, + autoWidth: false, ajax: { url: "{!! route('data.pengurus.getdata.post') !!}", type: "POST", @@ -130,7 +140,8 @@ class: 'text-center', }, { data: 'status', - name: 'status' + name: 'status', + class: 'text-center' }, { data: 'pendidikan.nama', @@ -163,6 +174,10 @@ class: 'text-center', $('#status').on('change', function(e) { data.ajax.reload(); }); + + $(window).on('resize', function() { + data.columns.adjust(); + }); }); @include('forms.datatable-vertical') diff --git a/resources/views/data/program_bantuan/index.blade.php b/resources/views/data/program_bantuan/index.blade.php index e0044707ec..2519b9123e 100644 --- a/resources/views/data/program_bantuan/index.blade.php +++ b/resources/views/data/program_bantuan/index.blade.php @@ -65,7 +65,10 @@ url: "{!! route('data.program-bantuan.getdata') !!}", type: "POST", data: function(d) { - d.desa = $('#list_desa').val(); + var desaId = $('#list_desa').val(); + if (desaId && desaId != 'Semua') { + d.desa = desaId; + } } }, diff --git a/resources/views/data/putus_sekolah/import.blade.php b/resources/views/data/putus_sekolah/import.blade.php index d8f671dad3..3731fe588b 100644 --- a/resources/views/data/putus_sekolah/import.blade.php +++ b/resources/views/data/putus_sekolah/import.blade.php @@ -20,7 +20,7 @@ {!! html()->form()->route('data.putus-sekolah.do_import')->method('POST')->id('form-import')->class( 'form-horizontal - form-label-left', + form-label-left', )->acceptsFiles()->open() !!}
@@ -43,11 +43,7 @@
- + @include('layouts.fragments.select-desa', ['selectAttributes' => ['name' => 'desa_id', 'data-width' => '100%', 'required' => 'required']])
@@ -108,6 +104,7 @@ @push('scripts') diff --git a/resources/views/forms/btn-social.blade.php b/resources/views/forms/btn-social.blade.php index ddf9bd401a..544b7e0a81 100644 --- a/resources/views/forms/btn-social.blade.php +++ b/resources/views/forms/btn-social.blade.php @@ -15,7 +15,7 @@ @endif @if (isset($export_url)) - + diff --git a/resources/views/informasi/form_dokumen/form_edit.blade.php b/resources/views/informasi/form_dokumen/form_edit.blade.php index 59aad21937..66b9e9992e 100644 --- a/resources/views/informasi/form_dokumen/form_edit.blade.php +++ b/resources/views/informasi/form_dokumen/form_edit.blade.php @@ -28,7 +28,7 @@
- {!! html()->file('file_dokumen')->class('form-control')->id('file_prosedur')->attribute('accept', '.jpeg,.png,.jpg,.gif,.svg,.xlsx,.xls,.doc,.docx,.pdf,.ppt,.pptx') !!} + {!! html()->file('file_dokumen')->class('form-control')->id('file_dokumen')->attribute('accept', '.jpeg,.png,.jpg,.gif,.svg,.xlsx,.xls,.doc,.docx,.pdf,.ppt,.pptx') !!}
@if (!empty($dokumen->file_dokumen))
Download File diff --git a/resources/views/informasi/potensi/create.blade.php b/resources/views/informasi/potensi/create.blade.php index 8b5e1fb5e8..e5c066d261 100644 --- a/resources/views/informasi/potensi/create.blade.php +++ b/resources/views/informasi/potensi/create.blade.php @@ -21,7 +21,7 @@ {!! html()->form('POST', route('informasi.potensi.store'))->acceptsFiles()->id('form-potensi')->class( 'form-horizontal - form-label-left', + form-label-left', )->open() !!} @if (count($errors) > 0) @@ -52,24 +52,39 @@ @push('scripts') - @include('components.modal-preview-surat') @include('forms.datatable-vertical') @include('forms.delete-modal') @endpush diff --git a/resources/views/informasi/prosedur/show.blade.php b/resources/views/informasi/prosedur/show.blade.php index 291de99051..d9a2486f2f 100644 --- a/resources/views/informasi/prosedur/show.blade.php +++ b/resources/views/informasi/prosedur/show.blade.php @@ -18,21 +18,28 @@
- + @if (!empty($prosedur->file_prosedur)) + +   Unduh + + @endif
- @if (isset($prosedur->file_prosedur) && $prosedur->mime_type != 'pdf') - - @endif - - @if (isset($prosedur->file_prosedur) && $prosedur->mime_type == 'pdf') - + @if (!empty($prosedur->file_prosedur)) + @if ($prosedur->is_pdf) + + @else + + @endif @endif
diff --git a/resources/views/informasi/regulasi/form_create.blade.php b/resources/views/informasi/regulasi/form_create.blade.php index 7aacb9cc3e..d9a8ae8cf8 100644 --- a/resources/views/informasi/regulasi/form_create.blade.php +++ b/resources/views/informasi/regulasi/form_create.blade.php @@ -19,20 +19,12 @@
- + + +

- - @if (isset($regulasi->file_regulasi) && $regulasi->mime_type != 'pdf') - - @endif - - @if (isset($regulasi->file_regulasi) && $regulasi->mime_type == 'pdf') - - @endif - - - - + +
@@ -45,34 +37,38 @@ @endpush + + diff --git a/resources/views/vendor/laravel-log-viewer/log.blade.php b/resources/views/vendor/laravel-log-viewer/log.blade.php index 22a1b5801f..f53e807aa5 100644 --- a/resources/views/vendor/laravel-log-viewer/log.blade.php +++ b/resources/views/vendor/laravel-log-viewer/log.blade.php @@ -30,7 +30,7 @@
@@ -43,22 +43,22 @@
@if ($current_file) Unduh Bersihkan File Hapus File @if (count($files) > 1) - + Hapus Semua file @endif diff --git a/routes/web.php b/routes/web.php index 0d00b9ed4c..1f0e59d33d 100644 --- a/routes/web.php +++ b/routes/web.php @@ -235,7 +235,7 @@ Route::group(['prefix' => 'potensi'], function () { Route::permanentRedirect('/', '/'); Route::get('{slug}', 'PageController@PotensiByKategory')->name('potensi.kategori'); - Route::get('{kategori}/{slug}', 'PageController@PotensiShow')->name('potensi.kategori.show'); + Route::get('{kategori}/{id}', 'PageController@PotensiShow')->name('potensi.kategori.show'); }); Route::any('refresh-captcha', 'PageController@refresh_captcha')->name('refresh-captcha'); @@ -346,7 +346,6 @@ Route::put('update/{prosedur}', ['as' => 'informasi.prosedur.update', 'uses' => 'ProsedurController@update']); Route::delete('destroy/{prosedur}', ['as' => 'informasi.prosedur.destroy', 'uses' => 'ProsedurController@destroy']); Route::get('download/{prosedur}', ['as' => 'informasi.prosedur.download', 'uses' => 'ProsedurController@download']); - Route::get('preview/{prosedur}', ['as' => 'informasi.prosedur.preview', 'uses' => 'ProsedurController@preview']); }); // Regulasi diff --git a/tests/Feature/Data/SuplemenControllerRequestTest.php b/tests/Feature/Data/SuplemenControllerRequestTest.php new file mode 100644 index 0000000000..e8b8ba125a --- /dev/null +++ b/tests/Feature/Data/SuplemenControllerRequestTest.php @@ -0,0 +1,186 @@ +withViewErrors([]); + $this->withoutMiddleware([ + Authenticate::class, + RoleMiddleware::class, + PermissionMiddleware::class, + CompleteProfile::class, + GlobalShareMiddleware::class, + ]); + + SettingAplikasi::updateOrCreate( + ['key' => 'sinkronisasi_database_gabungan'], + ['value' => '0'] + ); +}); + +test('store membuat data suplemen dengan data valid', function () { + $response = $this->post(route('data.data-suplemen.store'), [ + 'nama' => 'Suplemen Valid', + 'sasaran' => '1', + 'keterangan' => 'Keterangan valid', + ]); + + $response->assertRedirect(route('data.data-suplemen.index')); + $response->assertSessionHas('success', 'Data Suplemen berhasil ditambah!'); + + $this->assertDatabaseHas('das_suplemen', [ + 'nama' => 'Suplemen Valid', + 'slug' => 'suplemen-valid', + 'sasaran' => '1', + ]); +}); + +test('store gagal ketika nama kosong', function () { + $response = $this->post(route('data.data-suplemen.store'), [ + 'nama' => '', + 'sasaran' => '1', + ]); + + $response->assertSessionHasErrors(['nama']); +}); + +test('store gagal ketika sasaran tidak valid', function () { + $response = $this->post(route('data.data-suplemen.store'), [ + 'nama' => 'Suplemen Sasaran', + 'sasaran' => '3', + ]); + + $response->assertSessionHasErrors(['sasaran']); +}); + +test('update mengubah data suplemen saat data valid', function () { + $suplemen = Suplemen::create([ + 'nama' => 'Suplemen Lama', + 'slug' => 'suplemen-lama', + 'sasaran' => 1, + ]); + + $response = $this->put(route('data.data-suplemen.update', $suplemen->id), [ + 'nama' => 'Suplemen Baru', + 'sasaran' => '2', + 'keterangan' => 'Keterangan baru', + ]); + + $response->assertRedirect(route('data.data-suplemen.index')); + $response->assertSessionHas('success', 'Data Suplemen berhasil diubah!'); + + $this->assertDatabaseHas('das_suplemen', [ + 'id' => $suplemen->id, + 'nama' => 'Suplemen Baru', + 'slug' => 'suplemen-baru', + 'sasaran' => '2', + ]); +}); + +test('storeDetail membuat anggota suplemen saat data valid', function () { + $suplemen = Suplemen::create([ + 'nama' => 'Suplemen Anggota', + 'slug' => 'suplemen-anggota', + 'sasaran' => 1, + ]); + $penduduk = Penduduk::factory()->create(); + + $response = $this->post(route('data.data-suplemen.storedetail'), [ + 'suplemen_id' => $suplemen->id, + 'desa_id' => $penduduk->desa_id, + 'penduduk_id' => $penduduk->id, + 'keterangan' => 'Anggota baru', + ]); + + $response->assertRedirect(route('data.data-suplemen.show', $suplemen->id)); + $response->assertSessionHas('success', 'Anggota Suplemen berhasil ditambah!'); + + expect(SuplemenTerdata::where('suplemen_id', $suplemen->id)->count())->toBe(1); +}); + +test('storeDetail gagal ketika sumber penduduk kosong dengan pesan kustom', function () { + $suplemen = Suplemen::create([ + 'nama' => 'Suplemen Anggota', + 'slug' => 'suplemen-anggota', + 'sasaran' => 1, + ]); + + $response = $this->post(route('data.data-suplemen.storedetail'), [ + 'suplemen_id' => $suplemen->id, + 'keterangan' => 'Tanpa penduduk', + ]); + + $response->assertSessionHasErrors(['penduduk_id']); + + $errors = session('errors'); + expect($errors->first('penduduk_id'))->toBe('isian warga atau penduduk wajib diisi'); +}); + +test('updateDetail mengubah anggota suplemen saat data valid', function () { + $suplemen = Suplemen::create([ + 'nama' => 'Suplemen Anggota', + 'slug' => 'suplemen-anggota', + 'sasaran' => 1, + ]); + $penduduk = Penduduk::factory()->create(); + $terdata = SuplemenTerdata::create([ + 'suplemen_id' => $suplemen->id, + 'penduduk_id' => $penduduk->id, + 'keterangan' => 'Keterangan awal', + ]); + + $response = $this->put(route('data.data-suplemen.updatedetail', $terdata->id), [ + 'suplemen_id' => $suplemen->id, + 'desa_id' => $penduduk->desa_id, + 'penduduk_id' => $penduduk->id, + 'keterangan' => 'Keterangan diubah', + ]); + + $response->assertRedirect(route('data.data-suplemen.show', $suplemen->id)); + $response->assertSessionHas('success', 'Anggota Suplemen berhasil diubah!'); + + $this->assertDatabaseHas('das_suplemen_terdata', [ + 'id' => $terdata->id, + 'keterangan' => 'Keterangan diubah', + ]); +}); diff --git a/tests/Feature/Data/SuplemenGabunganTest.php b/tests/Feature/Data/SuplemenGabunganTest.php new file mode 100644 index 0000000000..65e4d88e04 --- /dev/null +++ b/tests/Feature/Data/SuplemenGabunganTest.php @@ -0,0 +1,572 @@ + 'sinkronisasi_database_gabungan'], + ['value' => $aktif ? '1' : '0'] + ); + + if ($aktif) { + SettingAplikasi::updateOrCreate( + ['key' => 'api_server_database_gabungan'], + ['value' => 'https://api.example.com'] + ); + SettingAplikasi::updateOrCreate( + ['key' => 'api_key_database_gabungan'], + ['value' => 'test-api-key'] + ); + } +} + +/** + * Fake seluruh panggilan API database gabungan. + * + * Constructor controller memanggil DesaService::listDesa() sehingga setiap + * request saat mode gabungan aktif wajib memiliki stub catch-all. + */ +function fakeGabunganApi(array $routes = []): void +{ + Http::fake(array_merge($routes, [ + '*' => Http::response([ + 'data' => [], + 'meta' => ['pagination' => ['total' => 0]], + ], 200), + ])); +} + +beforeEach(function () { + $this->withViewErrors([]); + $this->withoutMiddleware([ + Authenticate::class, + RoleMiddleware::class, + PermissionMiddleware::class, + CompleteProfile::class, + GlobalShareMiddleware::class, + ]); + + aktifkanDatabaseGabungan(false); + + $this->suplemen = Suplemen::create([ + 'nama' => 'Suplemen Gabungan Test', + 'slug' => 'suplemen-gabungan-test', + 'sasaran' => 1, + 'keterangan' => 'Testing database gabungan', + ]); +}); + +test('show menampilkan view gabungan saat database gabungan aktif', function () { + aktifkanDatabaseGabungan(true); + fakeGabunganApi(); + + $response = $this->get(route('data.data-suplemen.show', $this->suplemen->id)); + + $response->assertStatus(200); + $response->assertViewIs('data.data_suplemen.gabungan.show'); + $response->assertViewHas('suplemen', fn (Suplemen $suplemen) => $suplemen->id === $this->suplemen->id); +}); + +test('show menampilkan view lokal saat database gabungan tidak aktif', function () { + $response = $this->get(route('data.data-suplemen.show', $this->suplemen->id)); + + $response->assertStatus(200); + $response->assertViewIs('data.data_suplemen.show'); + $response->assertViewHas('suplemen', fn (Suplemen $suplemen) => $suplemen->id === $this->suplemen->id); +}); + +test('createDetail menampilkan view gabungan saat database gabungan aktif', function () { + aktifkanDatabaseGabungan(true); + fakeGabunganApi(); + + $response = $this->get(route('data.data-suplemen.createdetail', $this->suplemen->id)); + + $response->assertStatus(200); + $response->assertViewIs('data.data_suplemen.gabungan.create_detail'); + $response->assertViewHas('isDatabaseGabungan', true); + $response->assertSee('api.example.com/api/v1/opendk/sync-penduduk-opendk'); + $response->assertSee('filter[kode_desa]'); + $response->assertSee('select2({'); + $response->assertSee('minimumInputLength: 0'); + $response->assertSee("$('#penduduk_id_gabungan')", false); +}); + +test('createDetail menampilkan view lokal saat database gabungan tidak aktif', function () { + $response = $this->get(route('data.data-suplemen.createdetail', $this->suplemen->id)); + + $response->assertStatus(200); + $response->assertViewIs('data.data_suplemen.create_detail'); + $response->assertViewHas('isDatabaseGabungan', false); +}); + +test('storeDetail menyimpan penduduk dari database gabungan saat gabungan aktif', function () { + aktifkanDatabaseGabungan(true); + fakeGabunganApi(); + + $response = $this->post(route('data.data-suplemen.storedetail'), [ + 'suplemen_id' => $this->suplemen->id, + 'desa_id' => '3301010001', + 'penduduk_id_gabungan' => 99, + 'keterangan' => 'Anggota dari gabungan', + ]); + + $response->assertRedirect(route('data.data-suplemen.show', $this->suplemen->id)); + $response->assertSessionHas('success', 'Anggota Suplemen berhasil ditambah!'); + + $this->assertDatabaseHas('das_suplemen_terdata', [ + 'suplemen_id' => $this->suplemen->id, + 'penduduk_id' => null, + 'penduduk_id_gabungan' => 99, + 'desa_id' => '3301010001', + 'keterangan' => 'Anggota dari gabungan', + ]); +}); + +test('storeDetail menyimpan penduduk lokal saat database gabungan tidak aktif', function () { + $penduduk = Penduduk::factory()->create(); + + $response = $this->post(route('data.data-suplemen.storedetail'), [ + 'suplemen_id' => $this->suplemen->id, + 'desa_id' => $penduduk->desa_id, + 'penduduk_id' => $penduduk->id, + 'keterangan' => 'Anggota lokal', + ]); + + $response->assertRedirect(route('data.data-suplemen.show', $this->suplemen->id)); + $response->assertSessionHas('success', 'Anggota Suplemen berhasil ditambah!'); + + $this->assertDatabaseHas('das_suplemen_terdata', [ + 'suplemen_id' => $this->suplemen->id, + 'penduduk_id' => $penduduk->id, + 'penduduk_id_gabungan' => null, + 'desa_id' => $penduduk->desa_id, + ]); +}); + +test('storeDetail gagal ketika penduduk lokal dan gabungan keduanya diisi', function () { + $penduduk = Penduduk::factory()->create(); + + $response = $this->post(route('data.data-suplemen.storedetail'), [ + 'suplemen_id' => $this->suplemen->id, + 'desa_id' => $penduduk->desa_id, + 'penduduk_id' => $penduduk->id, + 'penduduk_id_gabungan' => 99, + ]); + + $response->assertSessionHasErrors(['penduduk_id_gabungan']); +}); + +test('storeDetail gagal ketika desa_id tidak diisi', function () { + $penduduk = Penduduk::factory()->create(); + + $response = $this->post(route('data.data-suplemen.storedetail'), [ + 'suplemen_id' => $this->suplemen->id, + 'penduduk_id' => $penduduk->id, + ]); + + $response->assertSessionHasErrors(['desa_id']); +}); + +test('getDataSuplemenTerdata tidak menggabungkan anggota dari API database gabungan', function () { + aktifkanDatabaseGabungan(true); + fakeGabunganApi([ + 'https://api.example.com/api/v1/opendk/suplemen-terdata-datatable/*' => Http::response([ + 'data' => [ + [ + 'id' => 100, + 'attributes' => [ + 'nama_desa' => 'Desa API', + 'no_kk' => '1234567890123456', + 'nik' => '1234567890123457', + 'nama' => 'Anggota Dari API', + 'tempat_lahir' => 'Jakarta', + 'tanggal_lahir' => '1990-01-01', + 'sex' => 1, + 'alamat' => 'Alamat API', + 'keterangan' => 'Terdata desa', + ], + ], + ], + 'meta' => ['pagination' => ['total' => 1]], + ], 200), + ]); + + $penduduk = Penduduk::factory()->create(['nama' => 'Anggota Lokal']); + SuplemenTerdata::create([ + 'suplemen_id' => $this->suplemen->id, + 'penduduk_id' => $penduduk->id, + 'desa_id' => $penduduk->desa_id, + 'keterangan' => 'Anggota lokal', + ]); + + $response = $this->postJson( + route('data.data-suplemen.getsuplementerdata', $this->suplemen->id), + [], + ['X-Requested-With' => 'XMLHttpRequest'] + ); + + $response->assertStatus(200); + $data = collect($response->json('data')); + + expect($data->pluck('keterangan'))->toContain('Anggota lokal') + ->not->toContain('Terdata desa') + ->and($data->pluck('penduduk.nama'))->toContain('Anggota Lokal') + ->not->toContain('Anggota Dari API'); +}); + +test('getDataSuplemenTerdata menyelesaikan penduduk gabungan lokal dalam satu request batch', function () { + aktifkanDatabaseGabungan(true); + + $batchCalls = 0; + $sentIds = null; + + Http::fake([ + 'https://api.example.com/api/v1/opendk/sync-penduduk-opendk*' => function (\Illuminate\Http\Client\Request $request) use (&$batchCalls, &$sentIds) { + $batchCalls++; + parse_str((string) parse_url($request->url(), PHP_URL_QUERY), $query); + $sentIds = $query['filter']['id_penduduk'] ?? null; + + return Http::response([ + 'data' => [ + ['id' => 500, 'attributes' => ['nama' => 'Penduduk Gabungan 1', 'nik' => '123', 'sex' => 1]], + ['id' => 501, 'attributes' => ['nama' => 'Penduduk Gabungan 2', 'nik' => '124', 'sex' => 2]], + ], + 'meta' => ['pagination' => ['total' => 2]], + ], 200); + }, + 'https://api.example.com/api/v1/opendk/suplemen-terdata-datatable/*' => Http::response([ + 'data' => [], + 'meta' => ['pagination' => ['total' => 0]], + ], 200), + '*' => Http::response([ + 'data' => [], + 'meta' => ['pagination' => ['total' => 0]], + ], 200), + ]); + + SuplemenTerdata::create([ + 'suplemen_id' => $this->suplemen->id, + 'penduduk_id_gabungan' => 500, + 'keterangan' => 'A1', + ]); + SuplemenTerdata::create([ + 'suplemen_id' => $this->suplemen->id, + 'penduduk_id_gabungan' => 501, + 'keterangan' => 'A2', + ]); + + $response = $this->postJson( + route('data.data-suplemen.getsuplementerdata', $this->suplemen->id), + [], + ['X-Requested-With' => 'XMLHttpRequest'] + ); + + $response->assertStatus(200); + $data = collect($response->json('data')); + + expect($batchCalls)->toBe(1) + ->and($sentIds)->toContain('500') + ->and($sentIds)->toContain('501') + ->and($data->pluck('penduduk.nama'))->toContain('Penduduk Gabungan 1') + ->and($data->pluck('penduduk.nama'))->toContain('Penduduk Gabungan 2'); +}); + +test('getDataSuplemenTerdata mengabaikan item non-array pada respons batch', function () { + aktifkanDatabaseGabungan(true); + + Http::fake([ + 'https://api.example.com/api/v1/opendk/sync-penduduk-opendk*' => Http::response([ + 'data' => [ + ['id' => 500, 'attributes' => ['nama' => 'Penduduk Gabungan 1', 'nik' => '123', 'sex' => 1]], + false, + null, + ], + 'meta' => ['pagination' => ['total' => 3]], + ], 200), + 'https://api.example.com/api/v1/opendk/suplemen-terdata-datatable/*' => Http::response([ + 'data' => [], + 'meta' => ['pagination' => ['total' => 0]], + ], 200), + '*' => Http::response([ + 'data' => [], + 'meta' => ['pagination' => ['total' => 0]], + ], 200), + ]); + + SuplemenTerdata::create([ + 'suplemen_id' => $this->suplemen->id, + 'penduduk_id_gabungan' => 500, + 'keterangan' => 'A1', + ]); + + $response = $this->postJson( + route('data.data-suplemen.getsuplementerdata', $this->suplemen->id), + [], + ['X-Requested-With' => 'XMLHttpRequest'] + ); + + $response->assertStatus(200); + $data = collect($response->json('data')); + + expect($data->pluck('penduduk.nama'))->toContain('Penduduk Gabungan 1'); +}); + +test('getDataSuplemenTerdata memfilter anggota lokal berdasarkan desa', function () { + aktifkanDatabaseGabungan(true); + fakeGabunganApi(); + + $desaA = DataDesa::create(['desa_id' => '3301010001', 'nama' => 'Desa A']); + $desaB = DataDesa::create(['desa_id' => '3301010002', 'nama' => 'Desa B']); + + $pendudukA = Penduduk::factory()->create(['desa_id' => $desaA->desa_id, 'nama' => 'Warga Desa A']); + $pendudukB = Penduduk::factory()->create(['desa_id' => $desaB->desa_id, 'nama' => 'Warga Desa B']); + + SuplemenTerdata::create([ + 'suplemen_id' => $this->suplemen->id, + 'penduduk_id' => $pendudukA->id, + 'desa_id' => $pendudukA->desa_id, + 'keterangan' => 'A', + ]); + SuplemenTerdata::create([ + 'suplemen_id' => $this->suplemen->id, + 'penduduk_id' => $pendudukB->id, + 'desa_id' => $pendudukB->desa_id, + 'keterangan' => 'B', + ]); + + $response = $this->postJson( + route('data.data-suplemen.getsuplementerdata', $this->suplemen->id), + ['desa' => $desaA->desa_id], + ['X-Requested-With' => 'XMLHttpRequest'] + ); + + $response->assertStatus(200); + $data = collect($response->json('data')); + + expect($data->pluck('penduduk.nama'))->toContain('Warga Desa A') + ->not->toContain('Warga Desa B'); +}); + +test('getDataSuplemenTerdata tanpa filter desa menampilkan semua anggota lokal', function () { + $desaA = DataDesa::create(['desa_id' => '3301010001', 'nama' => 'Desa A']); + $desaB = DataDesa::create(['desa_id' => '3301010002', 'nama' => 'Desa B']); + + $pendudukA = Penduduk::factory()->create(['desa_id' => $desaA->desa_id, 'nama' => 'Warga Desa A']); + $pendudukB = Penduduk::factory()->create(['desa_id' => $desaB->desa_id, 'nama' => 'Warga Desa B']); + + SuplemenTerdata::create([ + 'suplemen_id' => $this->suplemen->id, + 'penduduk_id' => $pendudukA->id, + 'desa_id' => $pendudukA->desa_id, + ]); + SuplemenTerdata::create([ + 'suplemen_id' => $this->suplemen->id, + 'penduduk_id' => $pendudukB->id, + 'desa_id' => $pendudukB->desa_id, + ]); + + $response = $this->postJson( + route('data.data-suplemen.getsuplementerdata', $this->suplemen->id), + ['desa' => 'Semua'], + ['X-Requested-With' => 'XMLHttpRequest'] + ); + + $response->assertStatus(200); + $data = collect($response->json('data')); + + expect($data->pluck('penduduk.nama'))->toContain('Warga Desa A') + ->toContain('Warga Desa B'); +}); + +test('storeDetail menyimpan desa_id dari penduduk database gabungan', function () { + aktifkanDatabaseGabungan(true); + fakeGabunganApi([ + 'https://api.example.com/api/v1/opendk/sync-penduduk-opendk*' => Http::response([ + 'data' => [ + [ + 'id' => 99, + 'attributes' => [ + 'nama' => 'Penduduk Gabungan', + 'nik' => '123', + 'sex' => 1, + 'config' => ['kode_desa' => '3301010001', 'nama_desa' => 'Desa API'], + ], + ], + ], + 'meta' => ['pagination' => ['total' => 1]], + ], 200), + ]); + + $response = $this->post(route('data.data-suplemen.storedetail'), [ + 'suplemen_id' => $this->suplemen->id, + 'desa_id' => '3301010001', + 'penduduk_id_gabungan' => 99, + 'keterangan' => 'Anggota dari gabungan', + ]); + + $response->assertRedirect(route('data.data-suplemen.show', $this->suplemen->id)); + + $this->assertDatabaseHas('das_suplemen_terdata', [ + 'suplemen_id' => $this->suplemen->id, + 'penduduk_id_gabungan' => 99, + 'desa_id' => '3301010001', + ]); +}); + +test('storeDetail menyimpan desa_id dari penduduk lokal', function () { + $penduduk = Penduduk::factory()->create(['desa_id' => '3301010003', 'nama' => 'Warga Lokal']); + + $response = $this->post(route('data.data-suplemen.storedetail'), [ + 'suplemen_id' => $this->suplemen->id, + 'desa_id' => $penduduk->desa_id, + 'penduduk_id' => $penduduk->id, + 'keterangan' => 'Anggota lokal', + ]); + + $response->assertRedirect(route('data.data-suplemen.show', $this->suplemen->id)); + + $this->assertDatabaseHas('das_suplemen_terdata', [ + 'suplemen_id' => $this->suplemen->id, + 'penduduk_id' => $penduduk->id, + 'desa_id' => '3301010003', + ]); +}); + +test('editDetail menampilkan view gabungan saat database gabungan aktif', function () { + aktifkanDatabaseGabungan(true); + fakeGabunganApi([ + 'https://api.example.com/api/v1/opendk/sync-penduduk-opendk*' => Http::response([ + 'data' => [ + [ + 'id' => 99, + 'attributes' => [ + 'nama' => 'Penduduk Gabungan Edit', + 'nik' => '1234567890123456', + 'sex' => 1, + 'config' => ['nama_desa' => 'Desa Edit'], + ], + ], + ], + 'meta' => ['pagination' => ['total' => 1]], + ], 200), + ]); + + $terdata = SuplemenTerdata::create([ + 'suplemen_id' => $this->suplemen->id, + 'penduduk_id_gabungan' => 99, + 'keterangan' => 'Anggota gabungan', + ]); + + $response = $this->get(route('data.data-suplemen.editdetail', [$terdata->id, $this->suplemen->id])); + + $response->assertStatus(200); + $response->assertViewIs('data.data_suplemen.gabungan.edit_detail'); + $response->assertViewHas('isDatabaseGabungan', true); + $response->assertSee('Penduduk Gabungan Edit'); + $response->assertSee('sync-penduduk-opendk'); + $response->assertSee('filter[kode_desa]'); +}); + +test('editDetail menampilkan view lokal saat database gabungan tidak aktif', function () { + $penduduk = Penduduk::factory()->create(['nama' => 'Anggota Lokal Edit']); + $terdata = SuplemenTerdata::create([ + 'suplemen_id' => $this->suplemen->id, + 'penduduk_id' => $penduduk->id, + 'keterangan' => 'Anggota lokal', + ]); + + $response = $this->get(route('data.data-suplemen.editdetail', [$terdata->id, $this->suplemen->id])); + + $response->assertStatus(200); + $response->assertViewIs('data.data_suplemen.edit_detail'); + $response->assertViewHas('isDatabaseGabungan', false); +}); + +test('updateDetail memperbarui anggota gabungan saat gabungan aktif', function () { + aktifkanDatabaseGabungan(true); + fakeGabunganApi(); + + $terdata = SuplemenTerdata::create([ + 'suplemen_id' => $this->suplemen->id, + 'penduduk_id_gabungan' => 99, + 'keterangan' => 'Keterangan awal', + ]); + + $response = $this->put(route('data.data-suplemen.updatedetail', $terdata->id), [ + 'suplemen_id' => $this->suplemen->id, + 'desa_id' => '3301010001', + 'penduduk_id_gabungan' => 100, + 'keterangan' => 'Keterangan diubah', + ]); + + $response->assertRedirect(route('data.data-suplemen.show', $this->suplemen->id)); + $response->assertSessionHas('success', 'Anggota Suplemen berhasil diubah!'); + + $this->assertDatabaseHas('das_suplemen_terdata', [ + 'id' => $terdata->id, + 'penduduk_id' => null, + 'penduduk_id_gabungan' => 100, + 'keterangan' => 'Keterangan diubah', + ]); +}); + +test('destroyDetail menghapus anggota saat gabungan aktif', function () { + aktifkanDatabaseGabungan(true); + fakeGabunganApi(); + + $terdata = SuplemenTerdata::create([ + 'suplemen_id' => $this->suplemen->id, + 'penduduk_id_gabungan' => 99, + 'keterangan' => 'Anggota gabungan', + ]); + + $response = $this->delete(route('data.data-suplemen.destroydetail', [$terdata->id, $this->suplemen->id])); + + $response->assertRedirect(route('data.data-suplemen.show', $this->suplemen->id)); + $response->assertSessionHas('success', 'Anggota Suplemen berhasil dihapus!'); + + $this->assertDatabaseMissing('das_suplemen_terdata', ['id' => $terdata->id]); +}); diff --git a/tests/Feature/FilePreviewFeatureTest.php b/tests/Feature/FilePreviewFeatureTest.php new file mode 100644 index 0000000000..a952dd2c04 --- /dev/null +++ b/tests/Feature/FilePreviewFeatureTest.php @@ -0,0 +1,152 @@ +withoutMiddleware([CompleteProfile::class]); + + $this->user = User::first(); + $this->actingAs($this->user); + + // Prepare test directory + $this->testDir = public_path('storage/test_previews'); + if (!file_exists($this->testDir)) { + mkdir($this->testDir, 0777, true); + } + + // Create dummy PDF and Image files + $this->pdfPath = $this->testDir . '/sample.pdf'; + file_put_contents($this->pdfPath, "%PDF-1.4\n%test pdf content"); + + $this->imgPath = $this->testDir . '/sample.png'; + // 1x1 transparent PNG + file_put_contents($this->imgPath, base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==')); + }); + + afterEach(function () { + if (file_exists($this->pdfPath)) { + @unlink($this->pdfPath); + } + if (file_exists($this->imgPath)) { + @unlink($this->imgPath); + } + if (is_dir($this->testDir)) { + @rmdir($this->testDir); + } + }); + + test('prosedur show page renders iframe for PDF and img for image', function () { + $prosedurPdf = Prosedur::create([ + 'judul_prosedur' => 'Prosedur Test PDF', + 'file_prosedur' => 'storage/test_previews/sample.pdf', + 'mime_type' => 'application/pdf', + ]); + + $responsePdf = $this->get(route('informasi.prosedur.show', $prosedurPdf->id)); + $responsePdf->assertStatus(200); + $responsePdf->assertSee('assertSee('class="showpdf"', false); + + $prosedurImg = Prosedur::create([ + 'judul_prosedur' => 'Prosedur Test Image', + 'file_prosedur' => 'storage/test_previews/sample.png', + 'mime_type' => 'image/png', + ]); + + $responseImg = $this->get(route('informasi.prosedur.show', $prosedurImg->id)); + $responseImg->assertStatus(200); + $responseImg->assertSee('delete(); + $prosedurImg->delete(); + }); + + test('regulasi show page renders iframe for PDF and img for image', function () { + $tipe = TipeRegulasi::first(); + + $regulasiPdf = Regulasi::create([ + 'tipe_regulasi' => $tipe->id, + 'judul' => 'Regulasi Test PDF', + 'deskripsi' => 'Deskripsi Regulasi PDF', + 'file_regulasi' => 'storage/test_previews/sample.pdf', + 'mime_type' => 'application/pdf', + ]); + + $responsePdf = $this->get(route('informasi.regulasi.show', $regulasiPdf->id)); + $responsePdf->assertStatus(200); + $responsePdf->assertSee('assertSee('class="showpdf"', false); + + $regulasiImg = Regulasi::create([ + 'tipe_regulasi' => $tipe->id, + 'judul' => 'Regulasi Test Image', + 'deskripsi' => 'Deskripsi Regulasi Image', + 'file_regulasi' => 'storage/test_previews/sample.png', + 'mime_type' => 'image/png', + ]); + + $responseImg = $this->get(route('informasi.regulasi.show', $regulasiImg->id)); + $responseImg->assertStatus(200); + $responseImg->assertSee('delete(); + $regulasiImg->delete(); + }); + + test('potensi show page renders iframe for PDF and img for image', function () { + $tipe = TipePotensi::first(); + + $potensiPdf = Potensi::create([ + 'kategori_id' => $tipe->id, + 'nama_potensi' => 'Potensi Wisata PDF', + 'deskripsi' => 'Deskripsi Potensi PDF', + 'lokasi' => 'Lokasi Wisata', + 'file_gambar' => 'storage/test_previews/sample.pdf', + 'mime_type' => 'application/pdf', + ]); + + $responsePdf = $this->get(route('informasi.potensi.show', $potensiPdf->id)); + $responsePdf->assertStatus(200); + $responsePdf->assertSee('assertSee('class="showpdf"', false); + + $potensiImg = Potensi::create([ + 'kategori_id' => $tipe->id, + 'nama_potensi' => 'Potensi Wisata Image', + 'deskripsi' => 'Deskripsi Potensi Image', + 'lokasi' => 'Lokasi Wisata', + 'file_gambar' => 'storage/test_previews/sample.png', + 'mime_type' => 'image/png', + ]); + + $responseImg = $this->get(route('informasi.potensi.show', $potensiImg->id)); + $responseImg->assertStatus(200); + $responseImg->assertSee('delete(); + $potensiImg->delete(); + }); + + test('download rejects path traversal attempts securely', function () { + $prosedurEvil = Prosedur::create([ + 'judul_prosedur' => 'Evil Prosedur', + 'file_prosedur' => '../../../../Windows/win.ini', + 'mime_type' => 'application/pdf', + ]); + + // Download attempt with traversal path should not return the file + $downloadResponse = $this->get(route('informasi.prosedur.download', $prosedurEvil->id)); + expect($downloadResponse->status())->toBeIn([302, 403, 404]); + + $prosedurEvil->delete(); + }); +}); diff --git a/tests/Feature/GaleriControllerTest.php b/tests/Feature/GaleriControllerTest.php new file mode 100644 index 0000000000..0ad0d7dee4 --- /dev/null +++ b/tests/Feature/GaleriControllerTest.php @@ -0,0 +1,287 @@ +withViewErrors([]); + $this->withoutMiddleware([ + Authenticate::class, + RoleMiddleware::class, + PermissionMiddleware::class, + CompleteProfile::class, + ]); +}); + +test('display the galeri index page', function () { + $album = Album::factory()->create(); + + $response = $this->get(route('publikasi.galeri.index', $album->id)); + + $response->assertStatus(200); + $response->assertViewIs('publikasi.galeri.index'); +}); + +test('display the galeri create page without broken image placeholder', function () { + $album = Album::factory()->create(); + Session::put('album_id', $album->id); + + $response = $this->get(route('publikasi.galeri.create')); + + $response->assertStatus(200); + $response->assertViewIs('publikasi.galeri.create'); + // Ensure the old broken placeholder is not displayed + $response->assertDontSee('id="showthumbnail"', false); + $response->assertSee('id="file-galeri"', false); + $response->assertSee('id="preview-container"', false); +}); + +test('display the galeri edit page and render valid thumbnail path', function () { + Storage::fake('public'); + $album = Album::factory()->create(); + Session::put('album_id', $album->id); + + // Put a dummy image in storage + Storage::disk('public')->put('publikasi/galeri/sample.jpg', 'fake content'); + + $galeri = Galeri::create([ + 'album_id' => $album->id, + 'judul' => 'Galeri Edit Test', + 'gambar' => ['sample.jpg'], + 'jenis' => 'file', + 'status' => true, + ]); + + $response = $this->get(route('publikasi.galeri.edit', $galeri->id)); + + $response->assertStatus(200); + $response->assertViewIs('publikasi.galeri.edit'); + // Should see sample.jpg in existing images + $response->assertSee('sample.jpg'); + // Should render modal image trigger and download button + $response->assertSee('data-toggle="modal-image"', false); + $response->assertSee('Unduh'); + $response->assertSee('id="modal-image-preview"', false); + $response->assertDontSee('publikasi/galeri/ sample.jpg'); + $response->assertDontSee('isThumbnail(" publikasi/galeri/'); +}); + +test('create a galeri with file upload', function () { + Storage::fake('public'); + $album = Album::factory()->create(); + Session::put('album_id', $album->id); + + $file = UploadedFile::fake()->image('foto1.jpg', 600, 400); + + $data = [ + 'judul' => 'Galeri Foto Kegiatan', + 'jenis' => 'file', + 'status' => '1', + 'gambar' => [$file], + ]; + + $response = $this->post(route('publikasi.galeri.store'), $data); + + $response->assertRedirect(route('publikasi.galeri.index', $album->id)); + $response->assertSessionHas('success', 'Galeri berhasil disimpan!'); + + $galeri = Galeri::where('judul', 'Galeri Foto Kegiatan')->first(); + expect($galeri)->not->toBeNull(); + expect($galeri->jenis)->toBe('file'); + expect($galeri->gambar)->toBeArray()->toHaveCount(1); + + Storage::disk('public')->assertExists('publikasi/galeri/' . $galeri->gambar[0]); +}); + +test('create a galeri with url link', function () { + $album = Album::factory()->create(); + Session::put('album_id', $album->id); + + $data = [ + 'judul' => 'Galeri Video YouTube', + 'jenis' => 'url', + 'link' => 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', + 'status' => '1', + ]; + + $response = $this->post(route('publikasi.galeri.store'), $data); + + $response->assertRedirect(route('publikasi.galeri.index', $album->id)); + $response->assertSessionHas('success', 'Galeri berhasil disimpan!'); + + $galeri = Galeri::where('judul', 'Galeri Video YouTube')->first(); + expect($galeri)->not->toBeNull(); + expect($galeri->jenis)->toBe('url'); + expect($galeri->link)->toBe('https://www.youtube.com/watch?v=dQw4w9WgXcQ'); + expect($galeri->gambar)->toBeNull(); +}); + +test('update a galeri without changing image preserves existing image', function () { + Storage::fake('public'); + $album = Album::factory()->create(); + Session::put('album_id', $album->id); + + $galeri = Galeri::create([ + 'album_id' => $album->id, + 'judul' => 'Galeri Awal', + 'gambar' => ['existing_image.jpg'], + 'jenis' => 'file', + 'status' => true, + ]); + + $data = [ + 'judul' => 'Galeri Diperbarui', + 'jenis' => 'file', + 'status' => '1', + ]; + + $response = $this->put(route('publikasi.galeri.update', $galeri->id), $data); + + $response->assertRedirect(route('publikasi.galeri.index', $album->id)); + $response->assertSessionHas('success', 'Galeri berhasil diubah!'); + + $galeri->refresh(); + expect($galeri->judul)->toBe('Galeri Diperbarui'); + expect($galeri->gambar)->toBe(['existing_image.jpg']); +}); + +test('update a galeri with new image replaces old image in storage', function () { + Storage::fake('public'); + $album = Album::factory()->create(); + Session::put('album_id', $album->id); + + Storage::disk('public')->put('publikasi/galeri/old_file.jpg', 'old content'); + + $galeri = Galeri::create([ + 'album_id' => $album->id, + 'judul' => 'Galeri File Lama', + 'gambar' => ['old_file.jpg'], + 'jenis' => 'file', + 'status' => true, + ]); + + $newFile = UploadedFile::fake()->image('new_file.jpg', 600, 400); + + $data = [ + 'judul' => 'Galeri File Baru', + 'jenis' => 'file', + 'status' => '1', + 'gambar' => [$newFile], + ]; + + $response = $this->put(route('publikasi.galeri.update', $galeri->id), $data); + + $response->assertRedirect(route('publikasi.galeri.index', $album->id)); + $response->assertSessionHas('success', 'Galeri berhasil diubah!'); + + $galeri->refresh(); + expect($galeri->judul)->toBe('Galeri File Baru'); + expect($galeri->gambar[0])->not->toBe('old_file.jpg'); + + // Observer should have deleted the old file + Storage::disk('public')->assertMissing('publikasi/galeri/old_file.jpg'); + // And new file should exist + Storage::disk('public')->assertExists('publikasi/galeri/' . $galeri->gambar[0]); +}); + +test('delete a galeri removes image from storage', function () { + Storage::fake('public'); + $album = Album::factory()->create(); + Session::put('album_id', $album->id); + + Storage::disk('public')->put('publikasi/galeri/to_delete.jpg', 'content'); + + $galeri = Galeri::create([ + 'album_id' => $album->id, + 'judul' => 'Galeri Hapus', + 'gambar' => ['to_delete.jpg'], + 'jenis' => 'file', + 'status' => true, + ]); + + $response = $this->delete(route('publikasi.galeri.destroy', $galeri->id)); + + $response->assertRedirect(route('publikasi.galeri.index', $album->id)); + $response->assertSessionHas('success', 'Galeri sukses dihapus!'); + + $this->assertDatabaseMissing('galeris', ['id' => $galeri->id]); + Storage::disk('public')->assertMissing('publikasi/galeri/to_delete.jpg'); +}); + +test('toggle galeri status', function () { + $album = Album::factory()->create(); + Session::put('album_id', $album->id); + + $galeri = Galeri::create([ + 'album_id' => $album->id, + 'judul' => 'Galeri Status', + 'jenis' => 'file', + 'status' => false, + ]); + + $response = $this->put(route('publikasi.galeri.status', $galeri->id)); + + $galeri->refresh(); + expect($galeri->status)->toBeTrue(); + $response->assertRedirect(route('publikasi.galeri.index', $album->id)); +}); + +test('getGambarPathAttribute handles file, youtube url, and empty safely', function () { + Storage::fake('public'); + $album = Album::factory()->create(); + + // 1. File that exists + Storage::disk('public')->put('publikasi/galeri/ada.jpg', 'fake content'); + $galeri1 = Galeri::create([ + 'album_id' => $album->id, + 'judul' => 'Galeri Ada File', + 'gambar' => ['ada.jpg'], + 'jenis' => 'file', + 'status' => true, + ]); + expect($galeri1->gambar_path)->toContain('publikasi/galeri/ada.jpg'); + + // 2. File that does not exist in storage -> returns fallback no-image + $galeri2 = Galeri::create([ + 'album_id' => $album->id, + 'judul' => 'Galeri Tak Ada File', + 'gambar' => ['tidak_ada.jpg'], + 'jenis' => 'file', + 'status' => true, + ]); + expect($galeri2->gambar_path)->toContain('no-image.png'); + + // 3. YouTube link -> returns YouTube thumbnail url + $galeri3 = Galeri::create([ + 'album_id' => $album->id, + 'judul' => 'Galeri YouTube', + 'link' => 'https://www.youtube.com/watch?v=dQw4w9WgXcQ', + 'jenis' => 'url', + 'status' => true, + ]); + expect($galeri3->gambar_path)->toBe('https://img.youtube.com/vi/dQw4w9WgXcQ/hqdefault.jpg'); + + // 4. Non-youtube link -> returns fallback no-image + $galeri4 = Galeri::create([ + 'album_id' => $album->id, + 'judul' => 'Galeri Web Biasa', + 'link' => 'https://opendesa.id', + 'jenis' => 'url', + 'status' => true, + ]); + expect($galeri4->gambar_path)->toContain('no-image.png'); + + // 5. Empty / null fields -> does not crash with undefined array key + $galeri5 = new Galeri(); + expect($galeri5->gambar_path)->toContain('no-image.png'); +}); diff --git a/tests/Feature/LembagaAnggotaControllerTest.php b/tests/Feature/LembagaAnggotaControllerTest.php index 828c073d00..4ede05554d 100644 --- a/tests/Feature/LembagaAnggotaControllerTest.php +++ b/tests/Feature/LembagaAnggotaControllerTest.php @@ -60,7 +60,7 @@ ); }); -function getLembagaPenduduk() +function getLembagaPendudukAnggota() { $penduduk = Penduduk::inRandomOrder()->first(); if (!$penduduk) { @@ -70,7 +70,7 @@ function getLembagaPenduduk() return $penduduk; } -function getLembagaKategori() +function getLembagaKategoriAnggota() { $kategori = KategoriLembaga::inRandomOrder()->first(); if (!$kategori) { @@ -81,8 +81,8 @@ function getLembagaKategori() } test('it can create lembaga anggota', function () { - $penduduk = getLembagaPenduduk(); - $kategori = getLembagaKategori(); + $penduduk = getLembagaPendudukAnggota(); + $kategori = getLembagaKategoriAnggota(); $lembaga = Lembaga::factory()->create([ 'lembaga_kategori_id' => $kategori->id, 'penduduk_id' => $penduduk->id, @@ -110,8 +110,8 @@ function getLembagaKategori() }); test('it can update lembaga anggota', function () { - $penduduk = getLembagaPenduduk(); - $kategori = getLembagaKategori(); + $penduduk = getLembagaPendudukAnggota(); + $kategori = getLembagaKategoriAnggota(); $lembaga = Lembaga::factory()->create([ 'lembaga_kategori_id' => $kategori->id, 'penduduk_id' => $penduduk->id, @@ -136,8 +136,8 @@ function getLembagaKategori() }); test('it can delete lembaga anggota', function () { - $penduduk = getLembagaPenduduk(); - $kategori = getLembagaKategori(); + $penduduk = getLembagaPendudukAnggota(); + $kategori = getLembagaKategoriAnggota(); $lembaga = Lembaga::factory()->create([ 'lembaga_kategori_id' => $kategori->id, 'penduduk_id' => $penduduk->id, diff --git a/tests/Feature/LembagaControllerTest.php b/tests/Feature/LembagaControllerTest.php new file mode 100644 index 0000000000..0141b78cb0 --- /dev/null +++ b/tests/Feature/LembagaControllerTest.php @@ -0,0 +1,238 @@ +withViewErrors([]); + $this->withoutMiddleware([ + \App\Http\Middleware\Authenticate::class, + \Spatie\Permission\Middleware\PermissionMiddleware::class, + \Spatie\Permission\Middleware\RoleMiddleware::class, + \App\Http\Middleware\CompleteProfile::class, + \App\Http\Middleware\GlobalShareMiddleware::class, + ]); +}); + +function getLembagaPenduduk() +{ + $penduduk = Penduduk::inRandomOrder()->first(); + if (!$penduduk) { + Penduduk::factory()->create(); + $penduduk = Penduduk::inRandomOrder()->first(); + } + return $penduduk; +} + +function getLembagaKategori() +{ + $kategori = KategoriLembaga::inRandomOrder()->first(); + if (!$kategori) { + KategoriLembaga::factory()->create(); + $kategori = KategoriLembaga::inRandomOrder()->first(); + } + return $kategori; +} + +test('it can show create lembaga page (local mode)', function () { + $penduduk = getLembagaPenduduk(); + $kategori = getLembagaKategori(); + + SettingAplikasi::updateOrCreate( + ['key' => 'sinkronisasi_database_gabungan'], + ['value' => '0'] + ); + + $response = get(route('data.lembaga.create')); + + $response->assertStatus(200); + $response->assertSee('Tambah Lembaga'); + $response->assertSee('penduduk_id'); +}); + +test('it can show create lembaga page (gabungan mode)', function () { + SettingAplikasi::updateOrCreate( + ['key' => 'sinkronisasi_database_gabungan'], + ['value' => '1'] + ); + SettingAplikasi::updateOrCreate( + ['key' => 'api_server_database_gabungan'], + ['value' => 'http://localhost:8000'] + ); + SettingAplikasi::updateOrCreate( + ['key' => 'api_key_database_gabungan'], + ['value' => 'test-key'] + ); + + Http::fake([ + 'http://localhost:8000/api/v1/wilayah/desa*' => Http::response([ + 'data' => [ + [ + 'id' => 1, + 'attributes' => [ + 'kode_desa' => '5102060001', + 'nama_desa' => 'Desa Test', + 'sebutan_desa' => 'Desa', + 'path' => null, + ], + ], + ], + ], 200), + 'http://localhost:8000/*' => Http::response(['data' => []], 200), + ]); + + $response = get(route('data.lembaga.create')); + + $response->assertStatus(200); + $response->assertSee('Tambah Lembaga'); + $response->assertSee('penduduk_id_gabungan'); +}); + +test('it can show edit lembaga page (gabungan mode)', function () { + SettingAplikasi::updateOrCreate( + ['key' => 'sinkronisasi_database_gabungan'], + ['value' => '1'] + ); + SettingAplikasi::updateOrCreate( + ['key' => 'api_server_database_gabungan'], + ['value' => 'http://localhost:8000'] + ); + SettingAplikasi::updateOrCreate( + ['key' => 'api_key_database_gabungan'], + ['value' => 'test-key'] + ); + + Http::fake([ + 'http://localhost:8000/api/v1/wilayah/desa*' => Http::response([ + 'data' => [ + [ + 'id' => 1, + 'attributes' => [ + 'kode_desa' => '5102060001', + 'nama_desa' => 'Desa Test', + 'sebutan_desa' => 'Desa', + 'path' => null, + ], + ], + ], + ], 200), + 'http://localhost:8000/api/v1/opendk/sync-penduduk-opendk*' => Http::response([ + 'data' => [ + [ + 'id' => 1, + 'attributes' => [ + 'nama' => 'John Doe', + 'nik' => '1234567890123456', + ], + ], + ], + ], 200), + 'http://localhost:8000/*' => Http::response(['data' => []], 200), + ]); + + $penduduk = getLembagaPenduduk(); + $kategori = getLembagaKategori(); + $lembaga = Lembaga::factory()->create([ + 'lembaga_kategori_id' => $kategori->id, + 'penduduk_id' => $penduduk->id, + ]); + + $response = get(route('data.lembaga.edit', $lembaga->id)); + + $response->assertStatus(200); + $response->assertSee('Ubah Lembaga'); + $response->assertSee('penduduk_id_gabungan'); +}); + +test('it can create lembaga in gabungan mode with integer penduduk_id', function () { + SettingAplikasi::updateOrCreate( + ['key' => 'sinkronisasi_database_gabungan'], + ['value' => '1'] + ); + + $penduduk = getLembagaPenduduk(); + $kategori = getLembagaKategori(); + + $response = post(route('data.lembaga.store'), [ + 'nama' => 'Lembaga Test Gabungan', + 'kode' => 'LTG-' . uniqid(), + 'lembaga_kategori_id' => $kategori->id, + 'penduduk_id_gabungan' => $penduduk->id, + ]); + + $response->assertRedirect(route('data.lembaga.index')); + $this->assertDatabaseHas('das_lembaga', [ + 'nama' => 'Lembaga Test Gabungan', + 'lembaga_kategori_id' => $kategori->id, + 'penduduk_id_gabungan' => $penduduk->id, + ]); +}); + +test('it can update lembaga in gabungan mode with integer penduduk_id', function () { + SettingAplikasi::updateOrCreate( + ['key' => 'sinkronisasi_database_gabungan'], + ['value' => '1'] + ); + + $penduduk = getLembagaPenduduk(); + $kategori = getLembagaKategori(); + $lembaga = Lembaga::factory()->create([ + 'lembaga_kategori_id' => $kategori->id, + 'penduduk_id_gabungan' => $penduduk->id, + ]); + + $newPenduduk = getLembagaPenduduk(); + + $response = put(route('data.lembaga.update', $lembaga->id), [ + 'nama' => 'Lembaga Updated Gabungan', + 'kode' => $lembaga->kode, + 'lembaga_kategori_id' => $kategori->id, + 'penduduk_id_gabungan' => $newPenduduk->id, + ]); + + $response->assertRedirect(route('data.lembaga.index')); + $this->assertDatabaseHas('das_lembaga', [ + 'id' => $lembaga->id, + 'nama' => 'Lembaga Updated Gabungan', + 'penduduk_id_gabungan' => $newPenduduk->id, + ]); +}); diff --git a/tests/Feature/LogViewerControllerTest.php b/tests/Feature/LogViewerControllerTest.php new file mode 100644 index 0000000000..542ab9c84a --- /dev/null +++ b/tests/Feature/LogViewerControllerTest.php @@ -0,0 +1,74 @@ +withoutMiddleware([ + Authenticate::class, + RoleMiddleware::class, + PermissionMiddleware::class, + CompleteProfile::class, + GlobalShareMiddleware::class, + ]); +}); + +test('user-supplied log params are decrypted without unserialization', function () { + // Payload dibuat seolah-olah oleh penyerang yang memiliki APP_KEY: + // menyerupai Crypt::decrypt() lama (unserialize=true) terhadap input user. + // decryptString() mengembalikan string mentah tanpa unserialize, sehingga + // object tidak pernah diinstansiasi (tidak ada side effect __wakeup()). + $malicious = Crypt::encryptString(serialize(new ObjectInjectionGadget)); + + $this->get(route('setting.info-sistem').'?f='.urlencode($malicious)); + + expect(ObjectInjectionGadget::$wokenUp)->toBeFalse(); +}); diff --git a/tests/Feature/ProfilControllerTest.php b/tests/Feature/ProfilControllerTest.php index 5a39507327..83f0eba656 100644 --- a/tests/Feature/ProfilControllerTest.php +++ b/tests/Feature/ProfilControllerTest.php @@ -32,7 +32,9 @@ use App\Models\DataDesa; use App\Models\DataUmum; use App\Models\Profil; +use App\Models\SettingAplikasi; use Illuminate\Foundation\Testing\DatabaseTransactions; +use Illuminate\Support\Facades\Http; uses(DatabaseTransactions::class); @@ -296,3 +298,54 @@ function createTestProfil(): void expect($rateLimitHeader)->not->toBeNull() ->and(is_numeric($rateLimitHeader))->toBeTrue(); }); + +test('letak geografis page passes data_umum to the view', function () { + $response = $this->get(route('profil.letak-geografis')); + + $response->assertStatus(200); + $response->assertViewHas('data_umum'); +}); + +test('letak geografis gabungan page renders polygon path from data_umum', function () { + Http::fake([ + '*' => Http::response(['data' => []], 200), + ]); + + SettingAplikasi::updateOrCreate( + ['key' => 'sinkronisasi_database_gabungan'], + ['value' => '1'] + ); + + $data_umum = DataUmum::first(); + + $response = $this->get(route('profil.letak-geografis')); + + $response->assertStatus(200); + $response->assertViewIs('pages.profil.gabungan.letakgeografis'); + $response->assertViewHas('data_umum', function ($viewDataUmum) use ($data_umum) { + return $viewDataUmum !== null + && (int) $viewDataUmum->id === (int) $data_umum->id + && $viewDataUmum->path === $data_umum->path; + }); +}); + +test('letak geografis gabungan page renders when polygon path is empty', function () { + Http::fake([ + '*' => Http::response(['data' => []], 200), + ]); + + SettingAplikasi::updateOrCreate( + ['key' => 'sinkronisasi_database_gabungan'], + ['value' => '1'] + ); + + DataUmum::query()->update(['path' => null]); + + $response = $this->get(route('profil.letak-geografis')); + + $response->assertStatus(200); + $response->assertViewIs('pages.profil.gabungan.letakgeografis'); + $response->assertViewHas('data_umum', function ($viewDataUmum) { + return $viewDataUmum === null || $viewDataUmum->path === null; + }); +}); diff --git a/tests/Feature/Security/ThemeUploadSecurityTest.php b/tests/Feature/Security/ThemeUploadSecurityTest.php index 5d3248ad65..4a5f9ad9b1 100644 --- a/tests/Feature/Security/ThemeUploadSecurityTest.php +++ b/tests/Feature/Security/ThemeUploadSecurityTest.php @@ -2,12 +2,11 @@ namespace Tests\Feature\Security; -use App\Models\User; use App\Models\Themes; +use App\Models\User; use Database\Seeders\RoleSpatieSeeder; use Illuminate\Http\UploadedFile; use Illuminate\Support\Facades\Storage; -use Tests\TestCase; use ZipArchive; describe('Theme Upload Security (RCE Prevention)', function () { @@ -218,6 +217,90 @@ @unlink($hooksFile); }); + // ── Issue #57 FIX: Backtick Execution Operator Bypass ── + + test('hooks_with_backtick_execution_is_rejected', function () { + $hooksDir = base_path('themes/default'); + if (! is_dir($hooksDir)) { + mkdir($hooksDir, 0755, true); + } + + $hooksFile = $hooksDir . '/hooks.php'; + $proofFile = $hooksDir . '/rce_proof.txt'; + @unlink($proofFile); + + // PoC payload from Issue #57 + file_put_contents($hooksFile, ' ' . $proofFile . ' 2>&1`;'); + + $theme = Themes::firstOrCreate( + ['name' => 'default'], + [ + 'vendor' => 'opendk', + 'version' => '1.0', + 'description' => 'Default', + 'path' => $hooksDir, + 'system' => true, + 'active' => 0, + ] + ); + + $r = $this->actingAs($this->superAdmin) + ->get(route('setting.themes.activate', $theme)); + + $r->assertStatus(302); + $r->assertSessionHas('error'); + + // Verify that command was NOT executed and proof file was NOT created + expect(file_exists($proofFile))->toBeFalse(); + + @unlink($hooksFile); + @unlink($proofFile); + }); + + test('hooks_with_include_construct_is_rejected', function () { + $hooksDir = base_path('themes/default'); + if (! is_dir($hooksDir)) { + mkdir($hooksDir, 0755, true); + } + + $hooksFile = $hooksDir . '/hooks.php'; + file_put_contents($hooksFile, ' 'default'], + [ + 'vendor' => 'opendk', + 'version' => '1.0', + 'description' => 'Default', + 'path' => $hooksDir, + 'system' => true, + 'active' => 0, + ] + ); + + $r = $this->actingAs($this->superAdmin) + ->get(route('setting.themes.activate', $theme)); + + $r->assertStatus(302); + $r->assertSessionHas('error'); + + @unlink($hooksFile); + }); + + test('upload_with_backtick_in_blade_template_is_rejected', function () { + $file = makeZipWithBacktickBlade(); + $r = $this->actingAs($this->superAdmin) + ->post(route('setting.themes.upload'), ['file' => $file]); + $r->assertJson(['status' => 'error']); + }); + + test('upload_with_misplaced_blade_template_is_rejected', function () { + $file = makeZipWithMisplacedBlade(); + $r = $this->actingAs($this->superAdmin) + ->post(route('setting.themes.upload'), ['file' => $file]); + $r->assertJson(['status' => 'error']); + }); + // ── Helpers ── function makeZipWithPhp(): UploadedFile @@ -233,6 +316,38 @@ function makeZipWithPhp(): UploadedFile return new UploadedFile($path, 'theme.zip', 'application/zip', null, true); } + /** + * Helper — creates a ZIP with backtick in a Blade template. + */ + function makeZipWithBacktickBlade(): UploadedFile + { + $path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid() . '.zip'; + $zip = new ZipArchive(); + $zip->open($path, ZipArchive::CREATE); + $zip->addFromString('evil-theme/composer.json', '{"name":"evil"}'); + $zip->addFromString('evil-theme/theme.json', '{"api_version":"v1"}'); + $zip->addFromString('evil-theme/resources/views/layouts/evil.blade.php', '
{{ `id` }}
'); + $zip->close(); + + return new UploadedFile($path, 'theme.zip', 'application/zip', null, true); + } + + /** + * Helper — creates a ZIP with a Blade template outside resources/views/. + */ + function makeZipWithMisplacedBlade(): UploadedFile + { + $path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid() . '.zip'; + $zip = new ZipArchive(); + $zip->open($path, ZipArchive::CREATE); + $zip->addFromString('evil-theme/composer.json', '{"name":"evil"}'); + $zip->addFromString('evil-theme/theme.json', '{"api_version":"v1"}'); + $zip->addFromString('evil-theme/misplaced.blade.php', '
Bad
'); + $zip->close(); + + return new UploadedFile($path, 'theme.zip', 'application/zip', null, true); + } + /** * BUG 3 FIX: Helper — creates a ZIP with a path traversal entry. */ @@ -250,4 +365,4 @@ function makeZipWithPathTraversal(): UploadedFile return new UploadedFile($path, 'theme.zip', 'application/zip', null, true); } -})->group('security', 'theme-upload'); \ No newline at end of file +})->group('security', 'theme-upload'); diff --git a/tests/Feature/SuplemenExportTest.php b/tests/Feature/SuplemenExportTest.php index d57d48c9d3..fc2854d7ab 100644 --- a/tests/Feature/SuplemenExportTest.php +++ b/tests/Feature/SuplemenExportTest.php @@ -2,11 +2,14 @@ use App\Exports\ExportSuplemen; use App\Exports\ExportSuplemenTerdata; +use App\Exports\ExportSuplemenTerdataGabungan; use App\Models\DataDesa; use App\Models\Penduduk; +use App\Models\SettingAplikasi; use App\Models\Suplemen; use App\Models\SuplemenTerdata; use Illuminate\Foundation\Testing\DatabaseTransactions; +use Illuminate\Support\Facades\Http; use Maatwebsite\Excel\Facades\Excel; uses(DatabaseTransactions::class); @@ -227,17 +230,17 @@ // Arrange: Buat data suplemen dan terdata Suplemen::query()->delete(); SuplemenTerdata::query()->delete(); - + $desa = DataDesa::factory()->create(); $penduduk = Penduduk::factory()->create(['desa_id' => $desa->desa_id]); - + $suplemen = Suplemen::create([ 'nama' => 'Test Suplemen Terdata', 'slug' => 'test-suplemen-terdata', 'sasaran' => 1, 'keterangan' => 'Test Keterangan' ]); - + SuplemenTerdata::create([ 'suplemen_id' => $suplemen->id, 'penduduk_id' => $penduduk->id, @@ -266,23 +269,121 @@ expect($collection)->toHaveCount(0); }); +test('export suplemen terdata with gabungan resolver fills penduduk relation', function () { + Suplemen::query()->delete(); + SuplemenTerdata::query()->delete(); + + Http::fake([ + 'https://api.example.com/api/v1/opendk/sync-penduduk-opendk*' => Http::response([ + 'data' => [ + [ + 'id' => 500, + 'attributes' => [ + 'nama' => 'Penduduk Dari API', + 'nik' => '1234567890123456', + 'sex' => 1, + 'config' => ['kode_desa' => '3301010001', 'nama_desa' => 'Desa API'], + ], + ], + ], + 'meta' => ['pagination' => ['total' => 1]], + ], 200), + ]); + + SettingAplikasi::updateOrCreate(['key' => 'api_server_database_gabungan'], ['value' => 'https://api.example.com']); + SettingAplikasi::updateOrCreate(['key' => 'api_key_database_gabungan'], ['value' => 'test-api-key']); + + $suplemen = Suplemen::create([ + 'nama' => 'Suplemen Gabungan', + 'slug' => 'suplemen-gabungan', + 'sasaran' => 1, + 'keterangan' => 'Test', + ]); + + $terdata = SuplemenTerdata::create([ + 'suplemen_id' => $suplemen->id, + 'penduduk_id_gabungan' => 500, + 'desa_id' => '3301010001', + 'keterangan' => 'A1', + ]); + + expect(SuplemenTerdata::count())->toBe(1) + ->and(SuplemenTerdata::first()->penduduk_id_gabungan)->toBe(500); + + $export = new ExportSuplemenTerdataGabungan($suplemen->id, [], [500]); + + $collection = $export->collection(); + + expect($collection)->toHaveCount(1) + ->and($collection->first()->penduduk_id_gabungan)->toBe(500) + ->and($collection->first()->desa_id)->toBe('3301010001') + ->and($collection->first()->penduduk)->not->toBeNull() + ->and($collection->first()->penduduk->nama)->toBe('Penduduk Dari API'); +}); + +test('export suplemen terdata excel calls gabungan api when gabungan active', function () { + SettingAplikasi::updateOrCreate(['key' => 'sinkronisasi_database_gabungan'], ['value' => '1']); + SettingAplikasi::updateOrCreate(['key' => 'api_server_database_gabungan'], ['value' => 'https://api.example.com']); + SettingAplikasi::updateOrCreate(['key' => 'api_key_database_gabungan'], ['value' => 'test-api-key']); + + Http::fake([ + 'https://api.example.com/api/v1/opendk/sync-penduduk-opendk*' => Http::response([ + 'data' => [ + [ + 'id' => 500, + 'attributes' => [ + 'nama' => 'Penduduk API Export', + 'nik' => '1234567890123456', + 'sex' => 1, + 'config' => ['kode_desa' => '3301010001', 'nama_desa' => 'Desa API'], + ], + ], + ], + 'meta' => ['pagination' => ['total' => 1]], + ], 200), + '*' => Http::response([ + 'data' => [], + 'meta' => ['pagination' => ['total' => 0]], + ], 200), + ]); + + $suplemen = Suplemen::create([ + 'nama' => 'Suplemen Gabungan Export', + 'slug' => 'suplemen-gabungan-export', + 'sasaran' => 1, + 'keterangan' => 'Test', + ]); + + SuplemenTerdata::create([ + 'suplemen_id' => $suplemen->id, + 'penduduk_id_gabungan' => 500, + 'desa_id' => '3301010001', + 'keterangan' => 'A1', + ]); + + $response = $this->get("/data/data-suplemen/export-terdata-excel/{$suplemen->id}"); + + $response->assertSuccessful(); + Http::assertSent(fn ($request) => str_contains($request->url(), 'api/v1/opendk/sync-penduduk-opendk')); +}); + test('export suplemen terdata by id with multiple terdata', function () { // Arrange: Buat data suplemen dengan multiple terdata Suplemen::query()->delete(); SuplemenTerdata::query()->delete(); - + $desa = DataDesa::factory()->create(); $penduduk1 = Penduduk::factory()->create(['desa_id' => $desa->desa_id, 'nama' => 'Penduduk 1']); $penduduk2 = Penduduk::factory()->create(['desa_id' => $desa->desa_id, 'nama' => 'Penduduk 2']); $penduduk3 = Penduduk::factory()->create(['desa_id' => $desa->desa_id, 'nama' => 'Penduduk 3']); - + $suplemen = Suplemen::create([ 'nama' => 'Test Suplemen Multiple', 'slug' => 'test-suplemen-multiple', 'sasaran' => 1, 'keterangan' => 'Test Keterangan' ]); - + SuplemenTerdata::create([ 'suplemen_id' => $suplemen->id, 'penduduk_id' => $penduduk1->id, diff --git a/tests/Unit/FilePreviewSecurityTest.php b/tests/Unit/FilePreviewSecurityTest.php new file mode 100644 index 0000000000..1c91a8264a --- /dev/null +++ b/tests/Unit/FilePreviewSecurityTest.php @@ -0,0 +1,107 @@ +resolveSecureFilePath($path); + } +} + +describe('File Preview Security & Detection Tests', function () { + + test('is_pdf helper correctly identifies PDF paths and MIME types', function () { + expect(is_pdf('doc.pdf'))->toBeTrue(); + expect(is_pdf('DOC.PDF'))->toBeTrue(); + expect(is_pdf('storage/files/prosedur.pdf'))->toBeTrue(); + expect(is_pdf(null, 'application/pdf'))->toBeTrue(); + expect(is_pdf(null, 'pdf'))->toBeTrue(); + expect(is_pdf('doc.png', 'application/pdf'))->toBeTrue(); + expect(is_pdf('photo.png', 'image/png'))->toBeFalse(); + expect(is_pdf('photo.jpg'))->toBeFalse(); + expect(is_pdf(null, null))->toBeFalse(); + }); + + test('models have functional is_pdf attribute', function () { + $prosedurPdf = new Prosedur(['file_prosedur' => 'storage/doc.pdf', 'mime_type' => 'application/pdf']); + expect($prosedurPdf->is_pdf)->toBeTrue(); + + $prosedurImg = new Prosedur(['file_prosedur' => 'storage/pic.jpg', 'mime_type' => 'image/jpeg']); + expect($prosedurImg->is_pdf)->toBeFalse(); + + $regulasiPdf = new Regulasi(['file_regulasi' => 'storage/sk.pdf', 'mime_type' => 'application/pdf']); + expect($regulasiPdf->is_pdf)->toBeTrue(); + + $regulasiImg = new Regulasi(['file_regulasi' => 'storage/banner.png', 'mime_type' => 'image/png']); + expect($regulasiImg->is_pdf)->toBeFalse(); + + $potensiPdf = new Potensi(['file_gambar' => 'storage/wisata.pdf', 'mime_type' => 'application/pdf']); + expect($potensiPdf->is_pdf)->toBeTrue(); + + $potensiImg = new Potensi(['file_gambar' => 'storage/wisata.png', 'mime_type' => 'image/png']); + expect($potensiImg->is_pdf)->toBeFalse(); + }); + + test('HandlesFileUpload rejects path traversal and files outside allowed directories', function () { + $helper = new FileUploadSecurityTestHelper(); + + // Null and empty paths + expect($helper->testResolvePath(null))->toBeNull(); + expect($helper->testResolvePath(''))->toBeNull(); + + // Path traversal attempts + expect($helper->testResolvePath('../../../Windows/win.ini'))->toBeNull(); + expect($helper->testResolvePath('..\\..\\..\\Windows\\win.ini'))->toBeNull(); + expect($helper->testResolvePath('storage/../../../../etc/passwd'))->toBeNull(); + expect($helper->testResolvePath('/etc/passwd'))->toBeNull(); + + // Safe resolution for real file inside public_path + $tempFile = public_path('test_preview_security_probe.txt'); + file_put_contents($tempFile, 'safe content'); + + try { + $resolved = $helper->testResolvePath('test_preview_security_probe.txt'); + expect($resolved)->not->toBeNull(); + expect(realpath($resolved))->toBe(realpath($tempFile)); + } finally { + if (file_exists($tempFile)) { + unlink($tempFile); + } + } + }); + + test('SecurityHeaders middleware enforces secure CSP in production', function () { + app()['env'] = 'production'; + try { + $middleware = new SecurityHeaders(); + $request = Request::create('/', 'GET'); + $response = $middleware->handle($request, function () { + return new Response('Hello'); + }); + + $csp = $response->headers->get('Content-Security-Policy'); + expect($csp)->not->toBeNull(); + + // Verify CSP directives for secure PDF preview + expect($csp)->toContain("object-src 'self' blob:"); + expect($csp)->toContain("frame-src 'self' blob: data:"); + expect($csp)->toContain("img-src 'self' * data: blob:"); + expect($csp)->toContain("frame-ancestors 'self'"); + expect($csp)->not->toContain('report-uri ;'); + } finally { + app()['env'] = 'testing'; + } + }); + +}); diff --git a/tests/Unit/Listeners/ClearProsedurCacheListenerTest.php b/tests/Unit/Listeners/ClearProsedurCacheListenerTest.php new file mode 100644 index 0000000000..a3a6e5d692 --- /dev/null +++ b/tests/Unit/Listeners/ClearProsedurCacheListenerTest.php @@ -0,0 +1,55 @@ +shouldReceive('removeCachePrefix') + ->once() + ->with('prosedur:api') + ->andReturnTrue(); + + $this->app->instance(CacheService::class, $cacheService); + + $listener = new ClearProsedurCacheListener(); + $event = new ProsedurChanged(new Prosedur()); + + $listener->handle($event); +}); + +it('reads correct cache prefix from config', function () { + config(['theme-api.prosedur.cache_prefix' => 'custom:prosedur']); + + $cacheService = Mockery::mock(CacheService::class); + $cacheService->shouldReceive('removeCachePrefix') + ->once() + ->with('custom:prosedur') + ->andReturnTrue(); + + $this->app->instance(CacheService::class, $cacheService); + + $listener = new ClearProsedurCacheListener(); + $event = new ProsedurChanged(new Prosedur()); + + $listener->handle($event); +}); + +it('handles exception gracefully', function () { + $cacheService = Mockery::mock(CacheService::class); + $cacheService->shouldReceive('removeCachePrefix') + ->once() + ->andThrow(new \Exception('Cache error')); + + $this->app->instance(CacheService::class, $cacheService); + + \Illuminate\Support\Facades\Log::shouldReceive('error')->once(); + + $listener = new ClearProsedurCacheListener(); + $event = new ProsedurChanged(new Prosedur()); + + // Should not throw exception + $listener->handle($event); +}); diff --git a/tests/Unit/Services/PendudukServiceTest.php b/tests/Unit/Services/PendudukServiceTest.php index de4fabc913..f67a6648d6 100644 --- a/tests/Unit/Services/PendudukServiceTest.php +++ b/tests/Unit/Services/PendudukServiceTest.php @@ -1,8 +1,8 @@ toBeNull(); }); +it('can fetch multiple penduduk from gabungan by ids', function () { + $sentParams = null; + + Http::fake([ + '*/api/v1/opendk/sync-penduduk-opendk*' => function (\Illuminate\Http\Client\Request $request) use (&$sentParams) { + parse_str((string) parse_url($request->url(), PHP_URL_QUERY), $sentParams); + + return Http::response([ + 'data' => [ + ['id' => 500, 'attributes' => ['nama' => 'John Doe', 'nik' => '1234567890123456']], + ['id' => 501, 'attributes' => ['nama' => 'Jane Doe', 'nik' => '1234567890123457']], + ], + ], 200); + }, + ]); + + $service = new PendudukService(); + $result = $service->pendudukGabunganByIds([500, 501]); + + expect($result)->toHaveCount(2); + expect($result->pluck('id'))->toContain(500); + expect($result->pluck('id'))->toContain(501); + expect($sentParams['filter']['id_penduduk'])->toBe(['500', '501']); +}); + +it('returns empty collection when gabungan ids empty', function () { + $service = new PendudukService(); + + expect($service->pendudukGabunganByIds([]))->toHaveCount(0); +}); + it('can apply filters to jumlah penduduk', function () { Http::fake([ '*/api/v1/opendk/sync-penduduk-opendk*' => Http::response([ @@ -372,4 +403,4 @@ expect($exportData->first()['umur'])->toBe(''); expect($exportData->first()['pekerjaan'])->toBe(''); expect($exportData->first()['status_kawin'])->toBe(''); -}); \ No newline at end of file +}); diff --git a/tests/Unit/ThemesControllerSecurityTest.php b/tests/Unit/ThemesControllerSecurityTest.php index abde0f5e91..6ec5dc10c1 100644 --- a/tests/Unit/ThemesControllerSecurityTest.php +++ b/tests/Unit/ThemesControllerSecurityTest.php @@ -109,4 +109,205 @@ test('validateHooksSource rejects inline HTML', function () { $r = $this->validator->validateSource('

x

toBeFalse(); -}); \ No newline at end of file +}); + +// ── Backtick execution operator tests (Issue #57) ── + +test('validateHooksSource rejects simple backtick execution', function () { + $r = $this->validator->validateSource('toBeFalse() + ->and($r['reason'])->toContain('Backtick execution operator'); +}); + +test('validateHooksSource rejects live PoC backtick execution', function () { + $r = $this->validator->validateSource(' themes/default/rce_proof.txt 2>&1`;', 'evil'); + expect($r['valid'])->toBeFalse() + ->and($r['reason'])->toContain('Backtick execution operator'); +}); + +test('validateHooksSource rejects backtick assigned to variable', function () { + $r = $this->validator->validateSource('toBeFalse() + ->and($r['reason'])->toContain('Backtick execution operator'); +}); + +// ── Language constructs tests (include, require) ── + +test('validateHooksSource rejects include construct', function () { + $r = $this->validator->validateSource('toBeFalse(); +}); + +test('validateHooksSource rejects include_once construct', function () { + $r = $this->validator->validateSource('toBeFalse(); +}); + +test('validateHooksSource rejects require construct', function () { + $r = $this->validator->validateSource('toBeFalse(); +}); + +test('validateHooksSource rejects require_once construct', function () { + $r = $this->validator->validateSource('toBeFalse(); +}); + +// ── Dynamic invocation tests ── + +test('validateHooksSource rejects dynamic class instantiation', function () { + $r = $this->validator->validateSource('toBeFalse(); +}); + +test('validateHooksSource rejects dynamic method access', function () { + $r = $this->validator->validateSource('$method();', 'evil'); + expect($r['valid'])->toBeFalse(); +}); + +test('validateHooksSource rejects dynamic static call', function () { + $r = $this->validator->validateSource('toBeFalse(); +}); + +test('validateHooksSource rejects variable variables', function () { + $r = $this->validator->validateSource('toBeFalse(); +}); + +test('validateHooksSource rejects variable function call with comments and whitespace', function () { + $r = $this->validator->validateSource('toBeFalse(); +}); + +// ── Exotic bypass tests (Security Review findings A–D) ── + +test('validateHooksSource rejects curly-string function call: ${"system"}()', function () { + $r = $this->validator->validateSource('toBeFalse() + ->and($r['reason'])->toContain('curly_expression_function_call'); +}); + +test('validateHooksSource rejects curly-variable function call: ${$a}()', function () { + $r = $this->validator->validateSource('toBeFalse() + ->and($r['reason'])->toContain('curly_expression_function_call'); +}); + +test('validateHooksSource rejects array-subscript function call: $_GET["cmd"]()', function () { + $r = $this->validator->validateSource('toBeFalse() + ->and($r['reason'])->toContain('array_subscript_function_call'); +}); + +test("validateHooksSource rejects curly-string single-quote function call: \${'system'}()", function () { + $r = $this->validator->validateSource("toBeFalse() + ->and($r['reason'])->toContain('curly_expression_function_call'); +}); + +// ── Expanded dangerous functions tests ── + +test('validateHooksSource rejects readfile() call', function () { + $r = $this->validator->validateSource('toBeFalse(); +}); + +test('validateHooksSource rejects forward_static_call() call', function () { + $r = $this->validator->validateSource('toBeFalse(); +}); + +test('validateHooksSource rejects register_shutdown_function() call', function () { + $r = $this->validator->validateSource('toBeFalse(); +}); + +test('validateHooksSource rejects unserialize() call', function () { + $r = $this->validator->validateSource('toBeFalse(); +}); + +test('validateHooksSource rejects scandir() call', function () { + $r = $this->validator->validateSource('toBeFalse(); +}); + +test('validateHooksSource rejects glob() call', function () { + $r = $this->validator->validateSource('toBeFalse(); +}); + +// ── ZIP Blade template scanning tests ── + +test('scanZipForPhp allows valid blade templates inside resources/views/', function () { + $zipPath = sys_get_temp_dir() . '/' . uniqid() . '.zip'; + $z = new ZipArchive(); + $z->open($zipPath, ZipArchive::CREATE); + $z->addFromString('theme.json', '{}'); + $z->addFromString('resources/views/layouts/app.blade.php', '
{{ $page_title }}
'); + $z->close(); + + $z = new ZipArchive(); + $z->open($zipPath); + + $this->validator->scanZipForPhp($z); + + expect(true)->toBeTrue(); + + $z->close(); + @unlink($zipPath); +}); + +test('scanZipForPhp rejects blade template placed outside resources/views/', function () { + $zipPath = sys_get_temp_dir() . '/' . uniqid() . '.zip'; + $z = new ZipArchive(); + $z->open($zipPath, ZipArchive::CREATE); + $z->addFromString('evil.blade.php', '

Bad location

'); + $z->close(); + + $z = new ZipArchive(); + $z->open($zipPath); + + expect(fn () => $this->validator->scanZipForPhp($z)) + ->toThrow(RuntimeException::class); + + $z->close(); + @unlink($zipPath); +}); + +test('scanZipForPhp allows blade template with JavaScript ES6 template literals', function () { + $zipPath = sys_get_temp_dir() . '/' . uniqid() . '.zip'; + $z = new ZipArchive(); + $z->open($zipPath, ZipArchive::CREATE); + $z->addFromString('theme.json', '{}'); + $z->addFromString('resources/views/widgets/custom.blade.php', ''); + $z->close(); + + $z = new ZipArchive(); + $z->open($zipPath); + + $this->validator->scanZipForPhp($z); + + expect(true)->toBeTrue(); + + $z->close(); + @unlink($zipPath); +}); + +test('scanZipForPhp rejects blade template containing backtick operator in PHP/Blade directive', function () { + $zipPath = sys_get_temp_dir() . '/' . uniqid() . '.zip'; + $z = new ZipArchive(); + $z->open($zipPath, ZipArchive::CREATE); + $z->addFromString('resources/views/pages/evil.blade.php', '
{{ `id` }}
'); + $z->close(); + + $z = new ZipArchive(); + $z->open($zipPath); + + expect(fn () => $this->validator->scanZipForPhp($z)) + ->toThrow(RuntimeException::class); + + $z->close(); + @unlink($zipPath); +}); diff --git a/themes/opendk/default/resources/views/pages/potensi/index.blade.php b/themes/opendk/default/resources/views/pages/potensi/index.blade.php index 8b4762071a..92d4d7e7f8 100644 --- a/themes/opendk/default/resources/views/pages/potensi/index.blade.php +++ b/themes/opendk/default/resources/views/pages/potensi/index.blade.php @@ -24,74 +24,74 @@ @endsection @push('scripts') - + }, + error: function(xhr, status, error) { + console.error('Error loading potensi:', error); + var $container = $('#potensi-container .post.clearfix'); + $container.html('

Gagal memuat potensi. Silakan coba lagi nanti.

'); + } + }); + } + + // Load potensi on page load + loadPotensi(); + }); + @endpush diff --git a/themes/opendk/default/resources/views/pages/potensi/show.blade.php b/themes/opendk/default/resources/views/pages/potensi/show.blade.php index 617ef1a1b4..7ea4f1477f 100644 --- a/themes/opendk/default/resources/views/pages/potensi/show.blade.php +++ b/themes/opendk/default/resources/views/pages/potensi/show.blade.php @@ -2,7 +2,7 @@ @section('content')
-
+
@@ -11,7 +11,7 @@

Loading potensi...

-
+
@@ -19,43 +19,47 @@ @endsection @push('scripts') - + }); + } + + // Load potensi detail on page load + loadPotensiDetail(); + }); + @endpush diff --git a/themes/opendk/default/resources/views/pages/profil/gabungan/letakgeografis.blade.php b/themes/opendk/default/resources/views/pages/profil/gabungan/letakgeografis.blade.php index badacea115..bae0e8f825 100644 --- a/themes/opendk/default/resources/views/pages/profil/gabungan/letakgeografis.blade.php +++ b/themes/opendk/default/resources/views/pages/profil/gabungan/letakgeografis.blade.php @@ -13,8 +13,7 @@
- +
@@ -109,7 +108,7 @@ function tampil_peta () { zoom: 13 }); - var path_kec = {!! $data_umum->path ?? '[]' !!}; + var path_kec = {!! ($data_umum->path ?? null) ?: '[]' !!}; // Geolocation IP Route/GPS geoLocation(peta_wilayah); showPolygon(path_kec, peta_wilayah); diff --git a/themes/opendk/default/resources/views/pages/unduhan/form-dokumen.blade.php b/themes/opendk/default/resources/views/pages/unduhan/form-dokumen.blade.php index 07b1e21d86..50efbcad01 100644 --- a/themes/opendk/default/resources/views/pages/unduhan/form-dokumen.blade.php +++ b/themes/opendk/default/resources/views/pages/unduhan/form-dokumen.blade.php @@ -30,15 +30,18 @@ $(document).ready(function() { var table = $('#dokumen-table').DataTable({ processing: true, - serverSide: false, + serverSide: true, ajax: { url: '{!! $urlApi !!}/form-dokumen', cache: false, - dataSrc: 'data', + dataSrc: function(json) { + json.recordsTotal = json.meta?.pagination?.total ?? 0; + json.recordsFiltered = json.meta?.pagination?.total ?? 0; + return json.data; + }, data: function(d) { - // Convert DataTables parameters to API format (use safe defaults to avoid NaN) var start = (typeof d.start !== 'undefined' && d.start !== null) ? d.start : 0; - var length = (typeof d.length !== 'undefined' && d.length) ? d.length : (typeof d.pageLength !== 'undefined' ? d.pageLength : 10); + var length = (typeof d.length !== 'undefined' && d.length) ? d.length : 10; var pageNumber = 1; if (length && !isNaN(length)) { pageNumber = Math.floor(start / length) + 1; @@ -52,6 +55,8 @@ }; } }, + pageLength: 10, + lengthMenu: [5, 10, 25, 50, 100], columns: [{ data: null, name: 'aksi', diff --git a/themes/opendk/default/resources/views/pages/unduhan/prosedur.blade.php b/themes/opendk/default/resources/views/pages/unduhan/prosedur.blade.php index b25a3f8713..bfcc3d1778 100644 --- a/themes/opendk/default/resources/views/pages/unduhan/prosedur.blade.php +++ b/themes/opendk/default/resources/views/pages/unduhan/prosedur.blade.php @@ -30,15 +30,18 @@ $(document).ready(function() { var table = $('#prosedur-table').DataTable({ processing: true, - serverSide: false, + serverSide: true, ajax: { url: '{!! $urlApi !!}/prosedur', cache: false, - dataSrc: 'data', + dataSrc: function(json) { + json.recordsTotal = json.meta?.pagination?.total ?? 0; + json.recordsFiltered = json.meta?.pagination?.total ?? 0; + return json.data; + }, data: function(d) { - // Convert DataTables parameters to API format (use safe defaults to avoid NaN) var start = (typeof d.start !== 'undefined' && d.start !== null) ? d.start : 0; - var length = (typeof d.length !== 'undefined' && d.length) ? d.length : (typeof d.pageLength !== 'undefined' ? d.pageLength : 10); + var length = (typeof d.length !== 'undefined' && d.length) ? d.length : 10; var pageNumber = 1; if (length && !isNaN(length)) { pageNumber = Math.floor(start / length) + 1; @@ -50,6 +53,8 @@ }; } }, + pageLength: 10, + lengthMenu: [5, 10, 25, 50, 100], columns: [{ data: 'attributes.judul_prosedur', name: 'judul_prosedur', diff --git a/themes/opendk/default/resources/views/pages/unduhan/regulasi.blade.php b/themes/opendk/default/resources/views/pages/unduhan/regulasi.blade.php index 567698960f..0b2b90993f 100644 --- a/themes/opendk/default/resources/views/pages/unduhan/regulasi.blade.php +++ b/themes/opendk/default/resources/views/pages/unduhan/regulasi.blade.php @@ -30,15 +30,18 @@ $(document).ready(function() { var table = $('#regulasi-table').DataTable({ processing: true, - serverSide: false, + serverSide: true, ajax: { url: '{!! $urlApi !!}/regulasi', cache: false, - dataSrc: 'data', + dataSrc: function(json) { + json.recordsTotal = json.meta?.pagination?.total ?? 0; + json.recordsFiltered = json.meta?.pagination?.total ?? 0; + return json.data; + }, data: function(d) { - // Convert DataTables parameters to API format (use safe defaults to avoid NaN) var start = (typeof d.start !== 'undefined' && d.start !== null) ? d.start : 0; - var length = (typeof d.length !== 'undefined' && d.length) ? d.length : (typeof d.pageLength !== 'undefined' ? d.pageLength : 10); + var length = (typeof d.length !== 'undefined' && d.length) ? d.length : 10; var pageNumber = 1; if (length && !isNaN(length)) { pageNumber = Math.floor(start / length) + 1; @@ -50,6 +53,8 @@ }; } }, + pageLength: 10, + lengthMenu: [5, 10, 25, 50, 100], columns: [{ data: 'attributes.judul', name: 'judul',
AksiFotoNama, NIP, NIKTempat, Tanggal LahirAksiFotoNama, NIP, NIKTempat, Tanggal Lahir Jenis Kelamin Agama Pangkat/Golongan JabatanStatusStatus Pendidikan Terakhir No SK Pengangkatan Tanggal SK Pengangkatan