diff --git a/app/Enums/PpidStatusEnum.php b/app/Enums/PpidStatusEnum.php
new file mode 100644
index 000000000..60ef84ec2
--- /dev/null
+++ b/app/Enums/PpidStatusEnum.php
@@ -0,0 +1,76 @@
+ 'Terbit',
+ self::TidakTerbit => 'Tidak',
+ self::Aktif => 'Aktif',
+ self::TidakAktif => 'Tidak Aktif',
+ ];
+ }
+
+ public static function labels(): array
+ {
+ return [
+ self::Terbit => 'success',
+ self::TidakTerbit => 'danger',
+ self::Aktif => 'success',
+ self::TidakAktif => 'danger',
+ ];
+ }
+
+ /**
+ * Label khusus untuk tombol toggle
+ */
+ public static function buttonLabels(): array
+ {
+ return [
+ self::Terbit => 'Terbit',
+ self::TidakTerbit => 'Tidak',
+ ];
+ }
+}
diff --git a/app/Enums/PpidTipeDokumenEnum.php b/app/Enums/PpidTipeDokumenEnum.php
new file mode 100644
index 000000000..1829646f1
--- /dev/null
+++ b/app/Enums/PpidTipeDokumenEnum.php
@@ -0,0 +1,60 @@
+ 'Unggah Dokumen',
+ self::Url => 'Link/URL',
+ ];
+ }
+
+ /**
+ * Label untuk tampilan tabel (uppercase)
+ */
+ public static function labelTable(): array
+ {
+ return [
+ self::File => 'FILE',
+ self::Url => 'URL',
+ ];
+ }
+}
diff --git a/app/Http/Controllers/Informasi/PpidDokumenController.php b/app/Http/Controllers/Informasi/PpidDokumenController.php
new file mode 100644
index 000000000..4fb9eae1a
--- /dev/null
+++ b/app/Http/Controllers/Informasi/PpidDokumenController.php
@@ -0,0 +1,294 @@
+get();
+
+ return view('ppid.dokumen.index', compact('page_title', 'page_description', 'jenis_dokumen'));
+ }
+
+ public function getData(Request $request)
+ {
+ $query = PpidDokumen::with('jenisDokumen');
+
+ // Filters
+ if ($request->filled('jenis_dokumen_id')) {
+ $query->where('jenis_dokumen_id', $request->jenis_dokumen_id);
+ }
+ if ($request->filled('status')) {
+ $query->where('status', $request->status);
+ }
+ if ($request->filled('tipe_dokumen')) {
+ $query->where('tipe_dokumen', $request->tipe_dokumen);
+ }
+ if ($request->filled('tanggal_mulai') && $request->filled('tanggal_selesai')) {
+ $query->whereBetween('tanggal_publikasi', [$request->tanggal_mulai, $request->tanggal_selesai]);
+ }
+
+ return DataTables::of($query)
+ ->addColumn('aksi', function ($row) {
+ $data['show_url'] = route('ppid.dokumen.show', $row->id);
+ $data['edit_url'] = route('ppid.dokumen.edit', $row->id);
+ $data['delete_url'] = route('ppid.dokumen.destroy', $row->id);
+
+ if ($row->tipe_dokumen === PpidTipeDokumenEnum::File) {
+ $data['download_url'] = route('ppid.dokumen.download', $row->id);
+ }
+
+ return view('forms.aksi', $data);
+ })
+ ->editColumn('status', function ($row) {
+ $badgeClass = $row->status === PpidStatusEnum::Terbit ? 'label-success' : 'label-danger';
+
+ return "".PpidStatusEnum::options()[$row->status].'';
+ })
+ ->editColumn('tipe_dokumen', function ($row) {
+ $badgeClass = $row->tipe_dokumen === PpidTipeDokumenEnum::File ? 'label-info' : 'label-warning';
+
+ return "".strtoupper($row->tipe_dokumen).'';
+ })
+ ->editColumn('jenis_dokumen', function ($row) {
+ return $row->jenisDokumen->nama ?? '-';
+ })
+ ->editColumn('tanggal_publikasi', function ($row) {
+ return $row->tanggal_publikasi ? $row->tanggal_publikasi->format('d/m/Y') : '-';
+ })
+ ->filterColumn('tanggal_publikasi', function ($query, $keyword) {
+ $query->whereRaw("DATE_FORMAT(tanggal_publikasi, '%d/%m/%Y') LIKE ?", ["%{$keyword}%"]);
+ })
+ ->rawColumns(['aksi', 'status', 'tipe_dokumen'])
+ ->make(true);
+ }
+
+ public function create()
+ {
+ $page_title = 'Dokumen PPID';
+ $page_description = 'Tambah Dokumen PPID';
+ $jenis_dokumen = PpidJenisDokumen::aktif()->pluck('nama', 'id');
+ $tipe_dokumen_options = PpidTipeDokumenEnum::options();
+ $status_options = PpidStatusEnum::options();
+
+ return view('ppid.dokumen.create', compact(
+ 'page_title',
+ 'page_description',
+ 'jenis_dokumen',
+ 'tipe_dokumen_options',
+ 'status_options'
+ ));
+ }
+
+ public function store(PpidDokumenRequest $request)
+ {
+ try {
+ $input = $request->validated();
+
+ // Handle file upload if tipe_dokumen is file
+ if ($input['tipe_dokumen'] === PpidTipeDokumenEnum::File) {
+ $this->handleFileUpload($request, $input, 'file_path', 'ppid/dokumen');
+ $input['url'] = null;
+ } else {
+ $input['file_path'] = null;
+ }
+
+ PpidDokumen::create($input);
+
+ return redirect()->route('ppid.dokumen.index')
+ ->with('success', 'Dokumen PPID berhasil ditambahkan!');
+ } catch (\Exception $e) {
+ report($e);
+
+ return back()->withInput()
+ ->with('error', 'Dokumen PPID gagal ditambahkan!');
+ }
+ }
+
+ public function show(PpidDokumen $dokumen)
+ {
+ $page_title = 'Dokumen PPID';
+ $page_description = 'Detail Dokumen PPID';
+
+ return view('ppid.dokumen.show', compact('page_title', 'page_description', 'dokumen'));
+ }
+
+ public function edit(PpidDokumen $dokumen)
+ {
+ $page_title = 'Dokumen PPID';
+ $page_description = 'Ubah Dokumen PPID';
+ $jenis_dokumen = PpidJenisDokumen::aktif()->pluck('nama', 'id');
+ $tipe_dokumen_options = PpidTipeDokumenEnum::options();
+ $status_options = PpidStatusEnum::options();
+
+ return view('ppid.dokumen.edit', compact(
+ 'page_title',
+ 'page_description',
+ 'dokumen',
+ 'jenis_dokumen',
+ 'tipe_dokumen_options',
+ 'status_options'
+ ));
+ }
+
+ public function update(PpidDokumenRequest $request, PpidDokumen $dokumen)
+ {
+ try {
+ $input = $request->validated();
+
+ // BUG-001: Cleanup old file when switching from FILE to URL
+ if ($dokumen->tipe_dokumen === PpidTipeDokumenEnum::File && $input['tipe_dokumen'] === PpidTipeDokumenEnum::Url) {
+ if (! empty($dokumen->file_path) && file_exists(public_path($dokumen->file_path))) {
+ unlink(public_path($dokumen->file_path));
+ }
+ $input['file_path'] = null;
+ }
+
+ // BUG-001: Cleanup old URL when switching from URL to FILE
+ if ($dokumen->tipe_dokumen === PpidTipeDokumenEnum::Url && $input['tipe_dokumen'] === PpidTipeDokumenEnum::File) {
+ $input['url'] = null;
+ }
+
+ // BUG-002: Handle file upload with old file cleanup
+ if ($input['tipe_dokumen'] === PpidTipeDokumenEnum::File) {
+ if ($request->hasFile('file_path')) {
+ // Delete old file before uploading new one
+ if (! empty($dokumen->file_path) && file_exists(public_path($dokumen->file_path))) {
+ unlink(public_path($dokumen->file_path));
+ }
+ $this->handleFileUpload($request, $input, 'file_path', 'ppid/dokumen');
+ $input['url'] = null;
+ }
+ // If no new file uploaded but tipe is still FILE, keep existing file
+ elseif (! empty($dokumen->file_path)) {
+ $input['file_path'] = $dokumen->file_path;
+ }
+ }
+
+ $dokumen->update($input);
+
+ return redirect()->route('ppid.dokumen.index')
+ ->with('success', 'Dokumen PPID berhasil diubah!');
+ } catch (\Exception $e) {
+ report($e);
+
+ return back()->withInput()
+ ->with('error', 'Dokumen PPID gagal diubah!');
+ }
+ }
+
+ public function destroy(PpidDokumen $dokumen)
+ {
+ try {
+ $dokumen->delete();
+
+ return redirect()->route('ppid.dokumen.index')
+ ->with('success', 'Dokumen PPID berhasil dihapus!');
+ } catch (\Exception $e) {
+ report($e);
+
+ return redirect()->route('ppid.dokumen.index')
+ ->with('error', 'Dokumen PPID gagal dihapus!');
+ }
+ }
+
+ /**
+ * Download file dokumen PPID.
+ *
+ * BUG-003: Validate tipe dokumen must be FILE
+ * BUG-004: Path traversal protection with file exists check
+ *
+ * @return \Symfony\Component\HttpFoundation\BinaryFileResponse|\Illuminate\Http\RedirectResponse
+ */
+ public function download(PpidDokumen $dokumen)
+ {
+ try {
+ // BUG-003: Validate tipe dokumen harus FILE
+ if ($dokumen->tipe_dokumen !== PpidTipeDokumenEnum::File) {
+ return back()->with('error', 'Dokumen ini bukan tipe file.');
+ }
+
+ // Validate file_path tidak kosong
+ if (empty($dokumen->file_path)) {
+ return back()->with('error', 'File tidak ditemukan.');
+ }
+
+ // BUG-004: Path traversal protection - validate file path
+ $filePath = public_path($dokumen->file_path);
+
+ // Validate file ada di dalam storage yang diizinkan
+ $realPath = realpath($filePath);
+ $storagePath = realpath(storage_path('app/public'));
+
+ if ($realPath === false || strpos($realPath, $storagePath) !== 0) {
+ report(new \Exception("PPID Dokumen path traversal attempt: {$dokumen->file_path}"));
+
+ return back()->with('error', 'File tidak valid.');
+ }
+
+ // Validate file exists
+ if (! file_exists($filePath)) {
+ report(new \Exception("PPID Dokumen file not found: {$filePath}"));
+
+ return back()->with('error', 'File tidak ditemukan atau telah dihapus.');
+ }
+
+ // Validate file readable
+ if (! is_readable($filePath)) {
+ report(new \Exception("PPID Dokumen file not readable: {$filePath}"));
+
+ return back()->with('error', 'File tidak dapat dibaca.');
+ }
+
+ return response()->download($filePath);
+ } catch (\Exception $e) {
+ report($e);
+
+ return back()->with('error', 'Terjadi kesalahan saat mengunduh file.');
+ }
+ }
+}
diff --git a/app/Http/Controllers/Informasi/PpidJenisDokumenController.php b/app/Http/Controllers/Informasi/PpidJenisDokumenController.php
new file mode 100644
index 000000000..c5321e103
--- /dev/null
+++ b/app/Http/Controllers/Informasi/PpidJenisDokumenController.php
@@ -0,0 +1,217 @@
+addColumn('aksi', function ($row) {
+ $data['show_url'] = route('ppid.jenis-dokumen.show', $row->id);
+ $data['edit_url'] = route('ppid.jenis-dokumen.edit', $row->id);
+ $data['delete_url'] = route('ppid.jenis-dokumen.destroy', $row->id);
+ $data['lock_url'] = route('ppid.jenis-dokumen.toggle-kunci', $row->id);
+ $data['is_locked'] = $row->is_kunci;
+
+ return view('forms.aksi', $data);
+ })
+ ->editColumn('status', function ($row) {
+ $badgeClass = $row->status === PpidStatusEnum::Aktif ? 'label-success' : 'label-danger';
+ $label = PpidStatusEnum::options()[$row->status] ?? $row->status;
+ $lockIcon = $row->is_kunci ? ' ' : '';
+
+ return "{$label}{$lockIcon}";
+ })
+ ->editColumn('urutan', function ($row) {
+ return ' '.$row->urutan.'';
+ })
+ ->addColumn('jumlah_dokumen', function ($row) {
+ return $row->dokumen()->count();
+ })
+ ->filterColumn('status', function ($query, $keyword) {
+ if (strtolower($keyword) === 'aktif') {
+ $query->where('status', 'aktif');
+ } elseif (strtolower($keyword) === 'tidak aktif') {
+ $query->where('status', 'tidak_aktif');
+ }
+ })
+ ->rawColumns(['aksi', 'status', 'urutan'])
+ ->make(true);
+ }
+
+ public function create()
+ {
+ $page_title = 'Jenis Dokumen PPID';
+ $page_description = 'Tambah Jenis Dokumen PPID';
+ $status_options = PpidStatusEnum::options();
+
+ return view('ppid.jenis-dokumen.create', compact('page_title', 'page_description', 'status_options'));
+ }
+
+ public function store(PpidJenisDokumenRequest $request)
+ {
+ try {
+ $input = $request->validated();
+
+ // Generate slug from nama if not provided
+ if (empty($input['slug'])) {
+ $input['slug'] = Str::slug($input['nama']);
+ }
+
+ // Get the highest urutan and add 1
+ $maxUrutan = PpidJenisDokumen::max('urutan') ?? 0;
+ $input['urutan'] = $input['urutan'] ?? ($maxUrutan + 1);
+
+ PpidJenisDokumen::create($input);
+
+ return redirect()->route('ppid.jenis-dokumen.index')
+ ->with('success', 'Jenis dokumen PPID berhasil ditambahkan!');
+ } catch (\Exception $e) {
+ report($e);
+
+ return back()->withInput()
+ ->with('error', 'Jenis dokumen PPID gagal ditambahkan!');
+ }
+ }
+
+ public function show(PpidJenisDokumen $jenis_dokumen)
+ {
+ $page_title = 'Jenis Dokumen PPID';
+ $page_description = 'Detail Jenis Dokumen PPID';
+
+ return view('ppid.jenis-dokumen.show', compact('page_title', 'page_description', 'jenis_dokumen'));
+ }
+
+ public function edit(PpidJenisDokumen $jenis_dokumen)
+ {
+ $page_title = 'Jenis Dokumen PPID';
+ $page_description = 'Ubah Jenis Dokumen PPID';
+ $status_options = PpidStatusEnum::options();
+
+ return view('ppid.jenis-dokumen.edit', compact('page_title', 'page_description', 'jenis_dokumen', 'status_options'));
+ }
+
+ public function update(PpidJenisDokumenRequest $request, PpidJenisDokumen $jenis_dokumen)
+ {
+ try {
+ $input = $request->validated();
+
+ // Prevent editing slug for locked records
+ if ($jenis_dokumen->is_kunci) {
+ unset($input['slug']);
+ }
+
+ $jenis_dokumen->update($input);
+
+ return redirect()->route('ppid.jenis-dokumen.index')
+ ->with('success', 'Jenis dokumen PPID berhasil diubah!');
+ } catch (\Exception $e) {
+ report($e);
+
+ return back()->withInput()
+ ->with('error', 'Jenis dokumen PPID gagal diubah!');
+ }
+ }
+
+ public function destroy(PpidJenisDokumen $jenis_dokumen)
+ {
+ try {
+ // Prevent deleting locked records
+ if ($jenis_dokumen->is_kunci) {
+ return redirect()->route('ppid.jenis-dokumen.index')
+ ->with('error', 'Jenis dokumen ini terkunci dan tidak dapat dihapus!');
+ }
+
+ $jenis_dokumen->delete();
+
+ return redirect()->route('ppid.jenis-dokumen.index')
+ ->with('success', 'Jenis dokumen PPID berhasil dihapus!');
+ } catch (\Exception $e) {
+ report($e);
+
+ return redirect()->route('ppid.jenis-dokumen.index')
+ ->with('error', 'Jenis dokumen PPID gagal dihapus!');
+ }
+ }
+
+ public function toggleKunci(PpidJenisDokumen $jenis_dokumen)
+ {
+ try {
+ $jenis_dokumen->update([
+ 'is_kunci' => ! $jenis_dokumen->is_kunci,
+ ]);
+
+ return redirect()->route('ppid.jenis-dokumen.index')
+ ->with('success', 'Status kunci berhasil diubah!');
+ } catch (\Exception $e) {
+ report($e);
+
+ return back()->with('error', 'Status kunci gagal diubah!');
+ }
+ }
+
+ public function reorder(Request $request)
+ {
+ try {
+ $orders = $request->input('order', []);
+
+ foreach ($orders as $index => $id) {
+ PpidJenisDokumen::where('id', $id)->update(['urutan' => $index + 1]);
+ }
+
+ return response()->json(['success' => true]);
+ } catch (\Exception $e) {
+ report($e);
+
+ return response()->json(['success' => false], 500);
+ }
+ }
+}
diff --git a/app/Http/Controllers/Informasi/PpidPengaturanController.php b/app/Http/Controllers/Informasi/PpidPengaturanController.php
new file mode 100644
index 000000000..b57394d73
--- /dev/null
+++ b/app/Http/Controllers/Informasi/PpidPengaturanController.php
@@ -0,0 +1,91 @@
+ PpidPengaturan::getValue('banner', 'default-ppid-banner.jpg'),
+ 'memperoleh_informasi' => PpidPengaturan::getValue('memperoleh_informasi', [
+ 'Mengambil informasi di kantor desa (hardcopy)',
+ 'Dikirim melalui email',
+ 'Melalui Whatsapp pengguna (Softcopy)',
+ ]),
+ 'alasan_keberatan' => PpidPengaturan::getValue('alasan_keberatan', [
+ 'Permohonan Informasi ditolak',
+ 'Informasi berkala tidak tersedia',
+ 'Permintaan informasi ditanggapi tidak sebagaimana yang diminta',
+ 'Permintaan informasi tidak dipenuhi',
+ 'Biaya yang dikenakan tidak wajar',
+ 'Informasi disampaikan melebihi jangka waktu yang ditentukan',
+ ]),
+ 'salinan_informasi' => PpidPengaturan::getValue('salinan_informasi', [
+ 'Mengambil Langsung',
+ 'Email',
+ 'Faksimili',
+ 'Kurir',
+ 'Pos',
+ ]),
+ ];
+
+ return view('ppid.pengaturan.index', compact('page_title', 'page_description', 'pengaturan'));
+ }
+
+ public function update(PpidPengaturanRequest $request)
+ {
+ try {
+ $input = $request->validated();
+
+ PpidPengaturan::setValue('banner', $input['banner'] ?? 'default-ppid-banner.jpg', 'Banner PPID');
+ PpidPengaturan::setValue('memperoleh_informasi', $input['memperoleh_informasi'] ?? [], 'Opsi memperoleh informasi');
+ PpidPengaturan::setValue('alasan_keberatan', $input['alasan_keberatan'] ?? [], 'Opsi pemohonan keberatan');
+ PpidPengaturan::setValue('salinan_informasi', $input['salinan_informasi'] ?? [], 'Opsi salinan informasi');
+
+ return redirect()->route('ppid.pengaturan.index')
+ ->with('success', 'Pengaturan PPID berhasil diperbarui!');
+ } catch (\Exception $e) {
+ report($e);
+
+ return back()->withInput()
+ ->with('error', 'Pengaturan PPID gagal diperbarui!');
+ }
+ }
+}
diff --git a/app/Http/Requests/PpidDokumenRequest.php b/app/Http/Requests/PpidDokumenRequest.php
new file mode 100644
index 000000000..df21974a9
--- /dev/null
+++ b/app/Http/Requests/PpidDokumenRequest.php
@@ -0,0 +1,120 @@
+ 'required|string|max:255',
+ 'jenis_dokumen_id' => 'required|exists:das_ppid_jenis_dokumen,id',
+ 'tipe_dokumen' => 'required|in:file,url',
+ 'ringkasan' => 'nullable|string',
+ 'status' => 'required|in:terbit,tidak_terbit',
+ 'tanggal_publikasi' => 'nullable|date',
+ ];
+
+ // BUG-006: Get the tipe_dokumen value from request or existing model
+ $tipeDokumen = $this->input('tipe_dokumen');
+ if ($this->ppid_dokumen && empty($tipeDokumen)) {
+ $tipeDokumen = $this->ppid_dokumen->tipe_dokumen;
+ }
+
+ // Conditional validation based on tipe_dokumen
+ if ($tipeDokumen === 'file') {
+ // For FILE type: file required if creating, nullable if updating
+ if ($this->ppid_dokumen) {
+ // Updating: file is nullable (keep existing if not changed)
+ $rules['file_path'] = 'nullable|file|mimes:pdf,doc,docx,xls,xlsx,ppt,pptx,jpg,jpeg,png|max:10240';
+ } else {
+ // Creating: file is required
+ $rules['file_path'] = 'required|file|mimes:pdf,doc,docx,xls,xlsx,ppt,pptx,jpg,jpeg,png|max:10240';
+ }
+ $rules['url'] = 'nullable|max:255';
+ } elseif ($tipeDokumen === 'url') {
+ // For URL type: url required, file nullable
+ $rules['file_path'] = 'nullable|file|mimes:pdf,doc,docx,xls,xlsx,ppt,pptx,jpg,jpeg,png|max:10240';
+ $rules['url'] = 'required|url|max:255';
+ } else {
+ // Default: both nullable (when tipe_dokumen not yet selected)
+ $rules['file_path'] = 'nullable|file|mimes:pdf,doc,docx,xls,xlsx,ppt,pptx,jpg,jpeg,png|max:10240';
+ $rules['url'] = 'nullable|url|max:255';
+ }
+
+ return $rules;
+ }
+
+ /**
+ * Get custom messages for validator errors.
+ *
+ * @return array
+ */
+ public function messages()
+ {
+ return [
+ 'judul.required' => 'Judul wajib diisi.',
+ 'judul.max' => 'Judul maksimal 255 karakter.',
+ 'jenis_dokumen_id.required' => 'Jenis dokumen wajib dipilih.',
+ 'jenis_dokumen_id.exists' => 'Jenis dokumen tidak valid.',
+ 'tipe_dokumen.required' => 'Tipe dokumen wajib dipilih.',
+ 'tipe_dokumen.in' => 'Tipe dokumen harus file atau url.',
+ 'file_path.required' => 'File wajib diunggah saat tipe dokumen adalah file.',
+ 'file_path.mimes' => 'Format file harus: pdf, doc, docx, xls, xlsx, ppt, pptx, jpg, jpeg, png.',
+ 'file_path.max' => 'Ukuran file maksimal 10MB.',
+ 'url.required' => 'URL wajib diisi saat tipe dokumen adalah url.',
+ 'url.url' => 'Format URL tidak valid.',
+ 'url.max' => 'URL maksimal 255 karakter.',
+ 'status.required' => 'Status wajib dipilih.',
+ 'status.in' => 'Status harus terbit atau tidak_terbit.',
+ 'tanggal_publikasi.date' => 'Format tanggal publikasi tidak valid.',
+ ];
+ }
+}
diff --git a/app/Http/Requests/PpidJenisDokumenRequest.php b/app/Http/Requests/PpidJenisDokumenRequest.php
new file mode 100644
index 000000000..2dc94a110
--- /dev/null
+++ b/app/Http/Requests/PpidJenisDokumenRequest.php
@@ -0,0 +1,88 @@
+jenis_dokumen) {
+ $uniqueSlug = $uniqueSlug->ignore($this->jenis_dokumen->id);
+ }
+
+ return [
+ 'slug' => ['required', 'string', 'max:255', $uniqueSlug],
+ 'nama' => 'required|string|max:255',
+ 'urutan' => 'nullable|integer|min:0',
+ 'status' => 'required|in:aktif,tidak_aktif',
+ ];
+ }
+
+ /**
+ * Get custom messages for validator errors.
+ *
+ * @return array
+ */
+ public function messages()
+ {
+ return [
+ 'slug.required' => 'Slug wajib diisi.',
+ 'slug.unique' => 'Slug sudah digunakan.',
+ 'nama.required' => 'Nama jenis dokumen wajib diisi.',
+ 'nama.max' => 'Nama jenis dokumen maksimal 255 karakter.',
+ 'status.required' => 'Status wajib dipilih.',
+ 'status.in' => 'Status harus aktif atau tidak_aktif.',
+ 'urutan.integer' => 'Urutan harus berupa angka.',
+ 'urutan.min' => 'Urutan minimal 0.',
+ ];
+ }
+}
diff --git a/app/Http/Requests/PpidPengaturanRequest.php b/app/Http/Requests/PpidPengaturanRequest.php
new file mode 100644
index 000000000..394299895
--- /dev/null
+++ b/app/Http/Requests/PpidPengaturanRequest.php
@@ -0,0 +1,83 @@
+ 'nullable|string|max:255',
+ 'memperoleh_informasi' => 'nullable|array',
+ 'memperoleh_informasi.*' => 'string|max:255',
+ 'alasan_keberatan' => 'nullable|array',
+ 'alasan_keberatan.*' => 'string|max:255',
+ 'salinan_informasi' => 'nullable|array',
+ 'salinan_informasi.*' => 'string|max:255',
+ ];
+ }
+
+ /**
+ * Get custom messages for validator errors.
+ *
+ * @return array
+ */
+ public function messages()
+ {
+ return [
+ 'banner.max' => 'Nama file banner maksimal 255 karakter.',
+ 'memperoleh_informasi.array' => 'Format memperoleh informasi tidak valid.',
+ 'memperoleh_informasi.*.max' => 'Opsi memperoleh informasi maksimal 255 karakter.',
+ 'alasan_keberatan.array' => 'Format alasan keberatan tidak valid.',
+ 'alasan_keberatan.*.max' => 'Opsi alasan keberatan maksimal 255 karakter.',
+ 'salinan_informasi.array' => 'Format salinan informasi tidak valid.',
+ 'salinan_informasi.*.max' => 'Opsi salinan informasi maksimal 255 karakter.',
+ ];
+ }
+}
diff --git a/app/Models/PpidDokumen.php b/app/Models/PpidDokumen.php
new file mode 100644
index 000000000..93dbc598a
--- /dev/null
+++ b/app/Models/PpidDokumen.php
@@ -0,0 +1,87 @@
+ 'date',
+ ];
+
+ protected $resources = [
+ 'file_path',
+ ];
+
+ public function jenisDokumen()
+ {
+ return $this->belongsTo(PpidJenisDokumen::class, 'jenis_dokumen_id');
+ }
+
+ public function scopeTerbit($query)
+ {
+ return $query->where('status', 'terbit');
+ }
+
+ public function scopeTidakTerbit($query)
+ {
+ return $query->where('status', 'tidak_terbit');
+ }
+
+ public function scopeFile($query)
+ {
+ return $query->where('tipe_dokumen', 'file');
+ }
+
+ public function scopeUrl($query)
+ {
+ return $query->where('tipe_dokumen', 'url');
+ }
+}
diff --git a/app/Models/PpidJenisDokumen.php b/app/Models/PpidJenisDokumen.php
new file mode 100644
index 000000000..82932ce45
--- /dev/null
+++ b/app/Models/PpidJenisDokumen.php
@@ -0,0 +1,84 @@
+ 'boolean',
+ ];
+
+ public function dokumen()
+ {
+ return $this->hasMany(PpidDokumen::class, 'jenis_dokumen_id');
+ }
+
+ public function scopeAktif($query)
+ {
+ return $query->where('status', 'aktif');
+ }
+
+ public function scopeTidakAktif($query)
+ {
+ return $query->where('status', 'tidak_aktif');
+ }
+
+ public function scopeTerkunci($query)
+ {
+ return $query->where('is_kunci', true);
+ }
+
+ public function scopeTidakTerkunci($query)
+ {
+ return $query->where('is_kunci', false);
+ }
+
+ public function scopeUrut($query)
+ {
+ return $query->orderBy('urutan');
+ }
+}
diff --git a/app/Models/PpidPengaturan.php b/app/Models/PpidPengaturan.php
new file mode 100644
index 000000000..c031d4d82
--- /dev/null
+++ b/app/Models/PpidPengaturan.php
@@ -0,0 +1,87 @@
+ 'array',
+ ];
+
+ /**
+ * Get value by key
+ */
+ public static function getValue(string $key, $default = null)
+ {
+ $setting = static::where('kunci', $key)->first();
+
+ if ($setting) {
+ return $setting->nilai;
+ }
+
+ return $default;
+ }
+
+ /**
+ * Set value by key
+ */
+ public static function setValue(string $key, $value, ?string $keterangan = null): void
+ {
+ $setting = static::where('kunci', $key)->first();
+
+ if ($setting) {
+ $setting->update([
+ 'nilai' => $value,
+ 'keterangan' => $keterangan,
+ ]);
+ } else {
+ static::create([
+ 'kunci' => $key,
+ 'nilai' => $value,
+ 'keterangan' => $keterangan,
+ ]);
+ }
+ }
+}
diff --git a/database/migrations/2026_02_17_115634_create_das_ppid_pengaturan_table.php b/database/migrations/2026_02_17_115634_create_das_ppid_pengaturan_table.php
new file mode 100644
index 000000000..7ca2f3d4d
--- /dev/null
+++ b/database/migrations/2026_02_17_115634_create_das_ppid_pengaturan_table.php
@@ -0,0 +1,31 @@
+id();
+ $table->string('kunci', 255)->unique();
+ $table->text('nilai');
+ $table->string('keterangan', 255)->nullable();
+ $table->timestamps();
+ $table->softDeletes('deleted_at', 0);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('das_ppid_pengaturan');
+ }
+};
diff --git a/database/migrations/2026_02_17_115635_create_das_ppid_jenis_dokumen_table.php b/database/migrations/2026_02_17_115635_create_das_ppid_jenis_dokumen_table.php
new file mode 100644
index 000000000..5767685fc
--- /dev/null
+++ b/database/migrations/2026_02_17_115635_create_das_ppid_jenis_dokumen_table.php
@@ -0,0 +1,37 @@
+id();
+ $table->string('slug', 255)->unique();
+ $table->string('nama', 255);
+ $table->integer('urutan')->default(0);
+ $table->enum('status', ['aktif', 'tidak_aktif'])->default('aktif');
+ $table->boolean('is_kunci')->default(false);
+ $table->timestamps();
+ $table->softDeletes('deleted_at', 0);
+
+ // Indexes
+ $table->index('status');
+ $table->index('urutan');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('das_ppid_jenis_dokumen');
+ }
+};
diff --git a/database/migrations/2026_02_17_115636_create_das_ppid_dokumen_table.php b/database/migrations/2026_02_17_115636_create_das_ppid_dokumen_table.php
new file mode 100644
index 000000000..3d433626d
--- /dev/null
+++ b/database/migrations/2026_02_17_115636_create_das_ppid_dokumen_table.php
@@ -0,0 +1,44 @@
+id();
+ $table->string('judul', 255);
+ $table->unsignedBigInteger('jenis_dokumen_id');
+ $table->enum('tipe_dokumen', ['file', 'url'])->default('file');
+ $table->string('file_path', 255)->nullable();
+ $table->string('url', 255)->nullable();
+ $table->text('ringkasan')->nullable();
+ $table->enum('status', ['terbit', 'tidak_terbit'])->default('terbit');
+ $table->date('tanggal_publikasi')->nullable();
+ $table->timestamps();
+ $table->softDeletes('deleted_at', 0);
+
+ $table->foreign('jenis_dokumen_id')->references('id')->on('das_ppid_jenis_dokumen');
+
+ // Indexes for filtering
+ $table->index('jenis_dokumen_id');
+ $table->index('status');
+ $table->index('tipe_dokumen');
+ $table->index('tanggal_publikasi');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('das_ppid_dokumen');
+ }
+};
diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php
index 484a6adec..c08cddfa3 100644
--- a/database/seeders/DatabaseSeeder.php
+++ b/database/seeders/DatabaseSeeder.php
@@ -74,6 +74,8 @@ public function run()
$this->call(DasProfilTableSeeder::class);
$this->call(DasDataUmumTableSeeder::class);
$this->call(PendudukSexSeeder::class);
+ // PPID
+ $this->call(PpidSeeder::class);
DB::statement('SET FOREIGN_KEY_CHECKS=1;');
}
diff --git a/database/seeders/PpidSeeder.php b/database/seeders/PpidSeeder.php
new file mode 100644
index 000000000..f3be7367b
--- /dev/null
+++ b/database/seeders/PpidSeeder.php
@@ -0,0 +1,145 @@
+seedPengaturan();
+ $this->seedJenisDokumen();
+ }
+
+ /**
+ * Seed default PPID settings
+ */
+ private function seedPengaturan(): void
+ {
+ $pengaturan = [
+ [
+ 'kunci' => 'banner',
+ 'nilai' => 'default-ppid-banner.jpg',
+ 'keterangan' => 'Banner PPID',
+ ],
+ [
+ 'kunci' => 'memperoleh_informasi',
+ 'nilai' => json_encode([
+ 'Mengambil informasi di kantor desa (hardcopy)',
+ 'Dikirim melalui email',
+ 'Melalui Whatsapp pengguna (Softcopy)',
+ ]),
+ 'keterangan' => 'Opsi memperoleh informasi',
+ ],
+ [
+ 'kunci' => 'alasan_keberatan',
+ 'nilai' => json_encode([
+ 'Permohonan Informasi ditolak',
+ 'Informasi berkala tidak tersedia',
+ 'Permintaan informasi ditanggapi tidak sebagaimana yang diminta',
+ 'Permintaan informasi tidak dipenuhi',
+ 'Biaya yang dikenakan tidak wajar',
+ 'Informasi disampaikan melebihi jangka waktu yang ditentukan',
+ ]),
+ 'keterangan' => 'Opsi pemohonan keberatan',
+ ],
+ [
+ 'kunci' => 'salinan_informasi',
+ 'nilai' => json_encode([
+ 'Mengambil Langsung',
+ 'Email',
+ 'Faksimili',
+ 'Kurir',
+ 'Pos',
+ ]),
+ 'keterangan' => 'Opsi salinan informasi',
+ ],
+ ];
+
+ foreach ($pengaturan as $data) {
+ PpidPengaturan::updateOrCreate(
+ ['kunci' => $data['kunci']],
+ [
+ 'nilai' => $data['nilai'],
+ 'keterangan' => $data['keterangan'],
+ ]
+ );
+ }
+
+ $this->command->info('PPID Pengaturan seeded successfully.');
+ }
+
+ /**
+ * Seed default jenis dokumen PPID
+ */
+ private function seedJenisDokumen(): void
+ {
+ $jenisDokumen = [
+ [
+ 'slug' => 'berkala',
+ 'nama' => 'Berkala',
+ 'urutan' => 1,
+ 'status' => 'aktif',
+ 'is_kunci' => false,
+ ],
+ [
+ 'slug' => 'serta_merta',
+ 'nama' => 'Serta Merta',
+ 'urutan' => 2,
+ 'status' => 'aktif',
+ 'is_kunci' => false,
+ ],
+ [
+ 'slug' => 'setiap_saat',
+ 'nama' => 'Setiap Saat',
+ 'urutan' => 3,
+ 'status' => 'aktif',
+ 'is_kunci' => false,
+ ],
+ ];
+
+ foreach ($jenisDokumen as $data) {
+ PpidJenisDokumen::updateOrCreate(
+ ['slug' => $data['slug']],
+ $data
+ );
+ }
+
+ $this->command->info('PPID Jenis Dokumen seeded successfully.');
+ }
+}
diff --git a/resources/views/ppid/dokumen/create.blade.php b/resources/views/ppid/dokumen/create.blade.php
new file mode 100644
index 000000000..060b1b53e
--- /dev/null
+++ b/resources/views/ppid/dokumen/create.blade.php
@@ -0,0 +1,74 @@
+@extends('layouts.dashboard_template')
+
+@section('content')
+
+
+
+ @foreach ($errors->all() as $error)
+
+
+
+ @foreach ($errors->all() as $error)
+
+
Format yang diperbolehkan: PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX, JPG, JPEG, PNG. Maksimal 10MB.
+File saat ini: Lihat File
+ @endif +Format yang diperbolehkan: PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX, JPG, JPEG, PNG. Maksimal 10MB.
+| Aksi | +Judul | +Jenis Dokumen | +Tipe Dokumen | +Status | +Tanggal Publikasi | +
|---|
Data jenis dokumen belum tersedia. Silakan tambah data jenis dokumen PPID terlebih dahulu.
+ +| Judul | +{{ $dokumen->judul }} | +
|---|---|
| Jenis Dokumen | +{{ $dokumen->jenisDokumen->nama ?? '-' }} | +
| Tipe Dokumen | +{{ strtoupper($dokumen->tipe_dokumen) }} | +
| Status | ++ @if($dokumen->status === 'terbit') + Terbit + @else + Tidak Terbit + @endif + | +
| Tanggal Publikasi | +{{ $dokumen->tanggal_publikasi ? $dokumen->tanggal_publikasi->format('d/m/Y') : '-' }} | +
| Ringkasan | +{{ $dokumen->ringkasan ?: '-' }} | +
| URL | +{{ $dokumen->url }} | +
Preview tidak tersedia untuk tipe file ini. Unduh file untuk melihat.
+ @endif +Slug akan digenerate otomatis dari nama jika dikosongkan.
+Slug terkunci dan tidak dapat diubah.
+ @else + {!! html()->text('slug', old('slug', $jenis_dokumen->slug)) + ->class('form-control') + ->placeholder('Slug (otomatis dari nama jika kosong)') !!} + @endif +| Aksi | +Urutan | +Nama | +Slug | +Status | +Jumlah Dokumen | +
|---|
| Nama | +{{ $jenis_dokumen->nama }} | +
|---|---|
| Slug | +{{ $jenis_dokumen->slug }} | +
| Urutan | +{{ $jenis_dokumen->urutan }} | +
| Status | ++ @if($jenis_dokumen->status === 'aktif') + Aktif + @else + Tidak Aktif + @endif + | +
| Terkunci | ++ @if($jenis_dokumen->is_kunci) + Ya + @else + Tidak + @endif + | +
| Jumlah Dokumen | +{{ $jenis_dokumen->dokumen()->count() }} | +
Nama file banner untuk halaman PPID.
+