diff --git a/app/Http/Controllers/Surat/PermohonanController.php b/app/Http/Controllers/Surat/PermohonanController.php index d101b9a0e0..92b8d95830 100644 --- a/app/Http/Controllers/Surat/PermohonanController.php +++ b/app/Http/Controllers/Surat/PermohonanController.php @@ -38,26 +38,34 @@ use App\Models\DataDesa; use App\Models\LogTte; use App\Models\Surat; +use Endroid\QrCode\Builder\Builder; +use Endroid\QrCode\Encoding\Encoding; +use Endroid\QrCode\ErrorCorrectionLevel; +use Endroid\QrCode\RoundBlockSizeMode; +use Endroid\QrCode\Writer\PngWriter; use GuzzleHttp\Exception\ClientException; use GuzzleHttp\Psr7; +use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Storage; +use Illuminate\View\View; +use setasign\Fpdi\Fpdi; +use Symfony\Component\HttpFoundation\BinaryFileResponse; use Yajra\DataTables\DataTables; class PermohonanController extends Controller { - public function index() + public function index(): View { - // dd(!$this->isDatabaseGabungan(), DataDesa::pluck('desa_id')); $page_title = 'Permohonan Surat'; $page_description = 'Daftar Permohonan Surat'; return view('surat.permohonan.index', compact('page_title', 'page_description')); } - public function getData() + public function getData(): JsonResponse { $desa = request('kode_desa'); @@ -127,9 +135,8 @@ public function getData() ->rawColumns(['aksi', 'nama', 'log_verifikasi'])->make(); } - public function show($id) + public function show(Surat $surat): View { - $surat = Surat::findOrFail($id); $page_title = 'Detail Surat'; $page_description = "Detail Data Surat: {$surat->nama}"; @@ -153,27 +160,24 @@ public function show($id) return view('surat.permohonan.show', compact('page_title', 'page_description', 'surat')); } - public function download($id) + public function download(Surat $surat): BinaryFileResponse { try { - $surat = Surat::findOrFail($id); - return Storage::download('public/surat/' . $surat->file); } catch (\Exception $e) { Log::error('Permohonan Surat download failed', [ 'error' => $e->getMessage(), 'user_id' => auth()->id(), - 'surat_id' => $id, + 'surat_id' => $surat->id, ]); return back()->with('error', 'Dokumen tidak ditemukan'); } } - public function setujui($id) + public function setujui(Surat $surat): JsonResponse { try { - $surat = Surat::findOrFail($id); $log_sekarang = $surat->log_verifikasi; if ($log_sekarang == LogVerifikasiSurat::Operator) { @@ -193,35 +197,38 @@ public function setujui($id) Log::error('Permohonan Surat approval failed', [ 'error' => $e->getMessage(), 'user_id' => auth()->id(), - 'surat_id' => $id, + 'surat_id' => $surat->id, ]); } return response()->json(); } - public function tolak(Request $request, $id) + public function tolak(Request $request, Surat $surat): JsonResponse { try { - Surat::findOrFail($id)->update([ + $request->validate(['keterangan' => 'required|string|max:500']); + + $surat->update([ 'log_verifikasi' => LogVerifikasiSurat::Ditolak, 'status' => StatusSurat::Ditolak, - 'keterangan' => $request['keterangan'], + 'keterangan' => $request->input('keterangan'), ]); } catch (\Exception $e) { Log::error('Permohonan Surat rejection failed', [ 'error' => $e->getMessage(), 'user_id' => auth()->id(), - 'surat_id' => $id, + 'surat_id' => $surat->id, ]); } return response()->json(); } - public function passphrase(Request $request, $id) + public function passphrase(Request $request, Surat $surat): JsonResponse { - $surat = Surat::findOrFail($id); + $request->validate(['passphrase' => 'required|string']); + DB::beginTransaction(); @@ -244,7 +251,7 @@ public function passphrase(Request $request, $id) 'multipart' => [ ['name' => 'file', 'contents' => Psr7\Utils::tryFopen($file_path, 'r')], ['name' => 'nik', 'contents' => $surat->pengurus->nik], - ['name' => 'passphrase', 'contents' => $request['passphrase']], + ['name' => 'passphrase', 'contents' => $request->input('passphrase')], ['name' => 'tampilan', 'contents' => 'visible'], ['name' => 'linkQR', 'contents' => route('surat.arsip.qrcode', $surat->id)], ['name' => 'width', 'contents' => $width], @@ -271,35 +278,140 @@ public function passphrase(Request $request, $id) 'status' => true, 'pesan_error' => 'success', 'jenis' => 'success', - ]); + ], $surat->id); } catch (ClientException $e) { Log::error('Permohonan Surat TTE signing failed', [ 'error' => $e->getMessage(), 'user_id' => auth()->id(), - 'surat_id' => $id, + 'surat_id' => $surat->id, ]); DB::rollback(); return $this->response([ 'status' => false, - 'pesan_error' => $e->getMessage(), + 'pesan_error' => 'Terjadi kesalahan saat menandatangani surat.', 'jenis' => 'ClientException', + ], $surat->id); + } + } + + public function tandatanganQr(Surat $surat): JsonResponse + { + if ($surat->log_verifikasi != LogVerifikasiSurat::ProsesTTE) { + return response()->json(['status' => false, 'pesan_error' => 'Surat tidak dalam tahap penandatanganan.'], 400); + } + + $user = auth()->user()->pengurus_id; + if ($user != $this->akun_camat->id) { + return response()->json(['status' => false, 'pesan_error' => 'Hanya camat yang dapat menandatangani surat.'], 403); + } + + DB::beginTransaction(); + + try { + $file_path = public_path('storage/surat/' . basename($surat->file)); + $file_info = pathinfo($file_path); + $signed_path = public_path('storage/surat/' . $file_info['filename'] . '_signed.pdf'); + + $verificationUrl = route('surat.arsip.qrcode', $surat->id); + + $qrCode = Builder::create() + ->writer(new PngWriter()) + ->data($verificationUrl) + ->encoding(new Encoding('UTF-8')) + ->errorCorrectionLevel(ErrorCorrectionLevel::High) + ->size(200) + ->margin(10) + ->roundBlockSizeMode(RoundBlockSizeMode::Margin) + ->build(); + + $qrTempPath = public_path('storage/surat/qr_temp_' . uniqid() . '_' . $surat->id . '.png'); + $qrCode->saveToFile($qrTempPath); + + $pdf = new Fpdi(); + $pageCount = $pdf->setSourceFile($file_path); + + for ($i = 1; $i <= $pageCount; $i++) { + $templateId = $pdf->importPage($i); + $size = $pdf->getTemplateSize($templateId); + $pdf->AddPage($size['orientation'], [$size['width'], $size['height']]); + $pdf->useTemplate($templateId); + + if ($i === $pageCount) { + $qrSize = 40; + $margin = 10; + $pdf->Image($qrTempPath, $size['width'] - $qrSize - $margin, $size['height'] - $qrSize - $margin, $qrSize, $qrSize); + } + } + + $pdf->Output('F', $signed_path); + + if (file_exists($qrTempPath)) { + unlink($qrTempPath); + } + + $fileHash = hash_file('sha256', $signed_path); + + $surat->update([ + 'status' => StatusSurat::Arsip, + 'log_verifikasi' => LogVerifikasiSurat::SudahTTE, + 'file_hash' => $fileHash, + ]); + + DB::commit(); + + if (!rename($signed_path, $file_path)) { + throw new \RuntimeException('Gagal memindahkan file signed ke lokasi asal.'); + } + + LogTte::create([ + 'pesan_error' => 'success', + 'jenis' => 'QRCode', + 'surat_id' => $surat->id, + ]); + + return response()->json([ + 'status' => true, + 'pesan_error' => 'success', + 'jenis' => 'success', + ]); + } catch (\Exception $e) { + DB::rollback(); + + if (isset($signed_path) && file_exists($signed_path)) { + unlink($signed_path); + } + if (isset($qrTempPath) && file_exists($qrTempPath)) { + unlink($qrTempPath); + } + + Log::error('QR Code signing failed', [ + 'error' => $e->getMessage(), + 'user_id' => auth()->id(), + 'surat_id' => $surat->id, + ]); + + return response()->json([ + 'status' => false, + 'pesan_error' => 'Terjadi kesalahan saat menandatangani surat.', + 'jenis' => 'Exception', ]); } } - protected function response($notif = []) + protected function response(array $notif = [], $suratId = null): JsonResponse { - LogTte::create([ - 'pesan_error' => $notif['pesan_error'], - 'jenis' => $notif['jenis'], - ]); + LogTte::create(array_filter([ + 'pesan_error' => $notif['pesan_error'] ?? '', + 'jenis' => $notif['jenis'] ?? '', + 'surat_id' => $suratId, + ])); return response()->json($notif); } - public function ditolak() + public function ditolak(): View { $page_title = 'Permohonan Surat Ditolak'; $page_description = 'Daftar Permohonan Surat Ditolak'; @@ -307,7 +419,7 @@ public function ditolak() return view('surat.permohonan.ditolak', compact('page_title', 'page_description')); } - public function getDataDitolak() + public function getDataDitolak(): JsonResponse { $desa = request('kode_desa'); $surat = Surat::ditolak() diff --git a/app/Http/Controllers/Surat/SuratController.php b/app/Http/Controllers/Surat/SuratController.php index 91bee0f39a..7e20568879 100644 --- a/app/Http/Controllers/Surat/SuratController.php +++ b/app/Http/Controllers/Surat/SuratController.php @@ -37,13 +37,18 @@ use App\Models\Profil; use App\Models\SettingAplikasi; use App\Models\Surat; +use Illuminate\Http\JsonResponse; +use Illuminate\Http\RedirectResponse; +use Illuminate\Http\Request; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Storage; +use Illuminate\View\View; +use Symfony\Component\HttpFoundation\BinaryFileResponse; use Yajra\DataTables\DataTables; class SuratController extends Controller { - public function arsip() + public function arsip(): View { $page_title = 'Arsip Surat'; $page_description = 'Daftar Arsip Surat'; @@ -51,7 +56,7 @@ public function arsip() return view('surat.arsip', compact('page_title', 'page_description')); } - public function getData() + public function getData(): JsonResponse { $desa = request()->get('kode_desa'); return DataTables::of(Surat::arsip() @@ -79,27 +84,31 @@ public function getData() } return $row->nama_penduduk; }) - ->rawColumns(['aksi'])->make(); + ->addColumn('hash', function ($row) { + if ($row->file_hash) { + return '' . e(substr($row->file_hash, 0, 16)) . '...'; + } + return '-'; + }) + ->rawColumns(['aksi', 'hash'])->make(); } - public function download($id) + public function download(Surat $surat): BinaryFileResponse { try { - $surat = Surat::findOrFail($id); - return Storage::download('public/surat/' . $surat->file); } catch (\Exception $e) { Log::error('Surat download failed', [ 'error' => $e->getMessage(), 'user_id' => auth()->id(), - 'surat_id' => $id, + 'surat_id' => $surat->id, ]); return back()->with('error', 'Dokumen tidak ditemukan'); } } - public function pengaturan() + public function pengaturan(): View { $formAction = route('surat.pengaturan.update'); $camat = $this->akun_camat; @@ -111,7 +120,7 @@ public function pengaturan() return view('surat.pengaturan', compact('page_title', 'page_description', 'formAction', 'camat', 'sekretaris')); } - public function pengaturan_update(PengaturanSuratRequest $request) + public function pengaturan_update(PengaturanSuratRequest $request): RedirectResponse { try { foreach ($request->all() as $key => $value) { @@ -129,11 +138,45 @@ public function pengaturan_update(PengaturanSuratRequest $request) return redirect()->route('surat.pengaturan')->with('success', 'Pengaturan Surat berhasil diubah!'); } - public function qrcode($id) + public function qrcode(Surat $surat): View { - $surat = Surat::where('id', '=', $id)->where('status', '=', StatusSurat::Arsip)->firstOrFail(); + abort_if($surat->status !== StatusSurat::Arsip, 404); $profil = Profil::first(); return view('surat.qrcode', compact('surat', 'profil')); } + + public function verifikasi(): View + { + $page_title = 'Verifikasi Surat'; + $page_description = 'Verifikasi keaslian surat digital'; + + return view('surat.verifikasi.index', compact('page_title', 'page_description')); + } + + public function verifikasiStore(Request $request): View|RedirectResponse + { + $request->validate([ + 'file' => 'required|mimes:pdf|max:5120', + ]); + + try { + $uploadedFile = $request->file('file'); + $uploadedHash = hash_file('sha256', $uploadedFile->getRealPath()); + + $surat = Surat::where('file_hash', $uploadedHash)->where('status', StatusSurat::Arsip)->first(); + + if (!$surat) { + return back()->with('error', 'Surat tidak ditemukan atau file tidak sesuai dengan surat yang diterbitkan.'); + } + + return view('surat.verifikasi.hasil', compact('surat')); + } catch (\Exception $e) { + Log::error('Verifikasi surat failed', [ + 'error' => $e->getMessage(), + ]); + + return back()->with('error', 'Terjadi kesalahan saat memverifikasi surat.'); + } + } } diff --git a/app/Models/LogTte.php b/app/Models/LogTte.php index c550341772..edb6836e37 100644 --- a/app/Models/LogTte.php +++ b/app/Models/LogTte.php @@ -37,5 +37,5 @@ class LogTte extends Model { protected $table = 'das_log_tte'; - protected $fillable = ['pesan_error', 'jenis']; + protected $fillable = ['pesan_error', 'jenis', 'surat_id']; } diff --git a/app/Models/Surat.php b/app/Models/Surat.php index de192b530b..ecc3bea612 100644 --- a/app/Models/Surat.php +++ b/app/Models/Surat.php @@ -50,6 +50,7 @@ class Surat extends Model 'nomor', 'nama', 'file', + 'file_hash', 'keterangan', 'log_verifikasi', 'verifikasi_operator', diff --git a/composer.json b/composer.json index f777cb4fc6..6c9c15c030 100644 --- a/composer.json +++ b/composer.json @@ -13,6 +13,7 @@ "bensampo/laravel-enum": "^6.0", "cocur/slugify": "4.6.0", "cviebrock/eloquent-sluggable": "^13.0", + "endroid/qr-code": "^6.1", "guzzlehttp/guzzle": "^7.2", "hexadog/laravel-themes-manager": "^1.13", "intervention/image": "3.11.3", @@ -30,6 +31,7 @@ "proengsoft/laravel-jsvalidation": "^4.4", "rap2hpoutre/laravel-log-viewer": "^3.0", "sentry/sentry-laravel": "^4.0", + "setasign/fpdi": "^2.6", "simplepie/simplepie": "^1.0", "spatie/laravel-backup": "^10.0", "spatie/laravel-fractal": "*", @@ -111,4 +113,4 @@ }, "minimum-stability": "stable", "prefer-stable": true -} \ No newline at end of file +} diff --git a/composer.lock b/composer.lock index 9df1b18e2d..cdde5e5be0 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,63 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "af7dbd4efc87c76122a09f5d75843c39", + "content-hash": "f65ee89e64960bfa404b0112140ce22a", "packages": [ + { + "name": "bacon/bacon-qr-code", + "version": "v3.1.1", + "source": { + "type": "git", + "url": "https://github.com/Bacon/BaconQrCode.git", + "reference": "4da2233e72eeecd9be3b62e0dc2cc9ed8e2e31c2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Bacon/BaconQrCode/zipball/4da2233e72eeecd9be3b62e0dc2cc9ed8e2e31c2", + "reference": "4da2233e72eeecd9be3b62e0dc2cc9ed8e2e31c2", + "shasum": "" + }, + "require": { + "dasprid/enum": "^1.0.3", + "ext-iconv": "*", + "php": "^8.1" + }, + "require-dev": { + "phly/keep-a-changelog": "^2.12", + "phpunit/phpunit": "^10.5.11 || ^11.0.4", + "spatie/phpunit-snapshot-assertions": "^5.1.5", + "spatie/pixelmatch-php": "^1.2.0", + "squizlabs/php_codesniffer": "^3.9" + }, + "suggest": { + "ext-imagick": "to generate QR code images" + }, + "type": "library", + "autoload": { + "psr-4": { + "BaconQrCode\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "https://dasprids.de/", + "role": "Developer" + } + ], + "description": "BaconQrCode is a QR code generator for PHP.", + "homepage": "https://github.com/Bacon/BaconQrCode", + "support": { + "issues": "https://github.com/Bacon/BaconQrCode/issues", + "source": "https://github.com/Bacon/BaconQrCode/tree/v3.1.1" + }, + "time": "2026-04-05T21:06:35+00:00" + }, { "name": "bensampo/laravel-enum", "version": "v6.14.0", @@ -595,6 +650,56 @@ ], "time": "2026-03-19T14:42:10+00:00" }, + { + "name": "dasprid/enum", + "version": "1.0.7", + "source": { + "type": "git", + "url": "https://github.com/DASPRiD/Enum.git", + "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/DASPRiD/Enum/zipball/b5874fa9ed0043116c72162ec7f4fb50e02e7cce", + "reference": "b5874fa9ed0043116c72162ec7f4fb50e02e7cce", + "shasum": "" + }, + "require": { + "php": ">=7.1 <9.0" + }, + "require-dev": { + "phpunit/phpunit": "^7 || ^8 || ^9 || ^10 || ^11", + "squizlabs/php_codesniffer": "*" + }, + "type": "library", + "autoload": { + "psr-4": { + "DASPRiD\\Enum\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "authors": [ + { + "name": "Ben Scholzen 'DASPRiD'", + "email": "mail@dasprids.de", + "homepage": "https://dasprids.de/", + "role": "Developer" + } + ], + "description": "PHP 7.1 enum implementation", + "keywords": [ + "enum", + "map" + ], + "support": { + "issues": "https://github.com/DASPRiD/Enum/issues", + "source": "https://github.com/DASPRiD/Enum/tree/1.0.7" + }, + "time": "2025-09-16T12:23:56+00:00" + }, { "name": "dflydev/dot-access-data", "version": "v3.0.3", @@ -968,6 +1073,78 @@ ], "time": "2025-03-06T22:45:56+00:00" }, + { + "name": "endroid/qr-code", + "version": "6.1.3", + "source": { + "type": "git", + "url": "https://github.com/endroid/qr-code.git", + "reference": "5fa534856ed95649d67c0eab0cabc03ab1d8e0e2" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/endroid/qr-code/zipball/5fa534856ed95649d67c0eab0cabc03ab1d8e0e2", + "reference": "5fa534856ed95649d67c0eab0cabc03ab1d8e0e2", + "shasum": "" + }, + "require": { + "bacon/bacon-qr-code": "^3.0", + "php": "^8.4" + }, + "require-dev": { + "endroid/quality": "dev-main", + "ext-gd": "*", + "khanamiryan/qrcode-detector-decoder": "^2.0.3", + "setasign/fpdf": "^1.8.2" + }, + "suggest": { + "ext-gd": "Enables you to write PNG images", + "khanamiryan/qrcode-detector-decoder": "Enables you to use the image validator", + "roave/security-advisories": "Makes sure package versions with known security issues are not installed", + "setasign/fpdf": "Enables you to use the PDF writer" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.x-dev" + } + }, + "autoload": { + "psr-4": { + "Endroid\\QrCode\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jeroen van den Enden", + "email": "info@endroid.nl" + } + ], + "description": "Endroid QR Code", + "homepage": "https://github.com/endroid/qr-code", + "keywords": [ + "code", + "endroid", + "php", + "qr", + "qrcode" + ], + "support": { + "issues": "https://github.com/endroid/qr-code/issues", + "source": "https://github.com/endroid/qr-code/tree/6.1.3" + }, + "funding": [ + { + "url": "https://github.com/endroid", + "type": "github" + } + ], + "time": "2026-02-05T07:01:58+00:00" + }, { "name": "ezyang/htmlpurifier", "version": "v4.19.0", @@ -6063,6 +6240,78 @@ ], "time": "2026-06-11T13:22:14+00:00" }, + { + "name": "setasign/fpdi", + "version": "v2.6.8", + "source": { + "type": "git", + "url": "https://github.com/Setasign/FPDI.git", + "reference": "881945be29a4996ad3d008eb18ddc01fa3df890c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Setasign/FPDI/zipball/881945be29a4996ad3d008eb18ddc01fa3df890c", + "reference": "881945be29a4996ad3d008eb18ddc01fa3df890c", + "shasum": "" + }, + "require": { + "ext-zlib": "*", + "php": ">=7.2 <=8.5.99999" + }, + "conflict": { + "setasign/tfpdf": "<1.31" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.52", + "setasign/fpdf": "^1.9.0", + "setasign/tfpdf": "~1.33", + "squizlabs/php_codesniffer": "^3.5", + "tecnickcom/tcpdf": "^6.8" + }, + "suggest": { + "setasign/fpdf": "FPDI will extend this class but as it is also possible to use TCPDF or tFPDF as an alternative. There's no fixed dependency configured." + }, + "type": "library", + "autoload": { + "psr-4": { + "setasign\\Fpdi\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Slabon", + "email": "jan.slabon@setasign.com", + "homepage": "https://www.setasign.com" + }, + { + "name": "Maximilian Kresse", + "email": "maximilian.kresse@setasign.com", + "homepage": "https://www.setasign.com" + } + ], + "description": "FPDI is a collection of PHP classes facilitating developers to read pages from existing PDF documents and use them as templates in FPDF. Because it is also possible to use FPDI with TCPDF, there are no fixed dependencies defined. Please see suggestions for packages which evaluates the dependencies automatically.", + "homepage": "https://www.setasign.com/fpdi", + "keywords": [ + "fpdf", + "fpdi", + "pdf" + ], + "support": { + "issues": "https://github.com/Setasign/FPDI/issues", + "source": "https://github.com/Setasign/FPDI/tree/v2.6.8" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/setasign/fpdi", + "type": "tidelift" + } + ], + "time": "2026-06-11T10:37:24+00:00" + }, { "name": "shalvah/upgrader", "version": "0.6.0", @@ -15981,5 +16230,5 @@ "platform-overrides": { "php": "8.4" }, - "plugin-api-version": "2.6.0" + "plugin-api-version": "2.9.0" } diff --git a/database/factories/SuratFactory.php b/database/factories/SuratFactory.php index 30af488607..452f0b746a 100644 --- a/database/factories/SuratFactory.php +++ b/database/factories/SuratFactory.php @@ -3,46 +3,123 @@ namespace Database\Factories; use App\Models\DataDesa; +use App\Models\JenisSurat; +use App\Models\Penduduk; use App\Models\Surat; -use App\Models\User; +use App\Enums\LogVerifikasiSurat; +use App\Enums\StatusSurat; +use App\Enums\StatusVerifikasiSurat; use Illuminate\Support\Str; use Illuminate\Database\Eloquent\Factories\Factory; -/** - * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Model> - */ class SuratFactory extends Factory { protected $model = Surat::class; - /** - * Define the model's default state. - * - * @return array - */ + public function definition() { + $jenisSurat = JenisSurat::inRandomOrder()->first(); + return [ - 'desa_id' => function () { - return DataDesa::firstOrCreate(['nama' => 'Desa Contoh'], ['desa_id' => '3301011234567', 'nama' => 'Desa Contoh', 'website' => 'https://example.com', 'luas_wilayah' => 10.5])->desa_id; + 'desa_id' => function () { + return DataDesa::firstOrCreate(['nama' => 'Desa Contoh'], [ + 'desa_id' => '3301011234567', + 'nama' => 'Desa Contoh', + 'website' => 'https://example.com', + 'luas_wilayah' => 10.5, + ])->desa_id; + }, + 'nik' => function () { + $penduduk = Penduduk::factory()->create(); + return $penduduk->nik; }, - 'nik' => $this->faker->numerify('################'), // 16 digit char - 'pengurus_id' => function () { - return User::factory()->create()->id; + 'pengurus_id' => function () { + return \App\Models\Pengurus::factory()->create()->id; }, - 'tanggal' => $this->faker->date(), - 'nomor' => strtoupper(Str::random(10)), - 'nama' => $this->faker->name(), - 'file' => 'dummy.pdf', - 'keterangan' => $this->faker->sentence(), - 'log_verifikasi' => $this->faker->numberBetween(0, 3), // sesuaikan enum - 'verifikasi_operator' => $this->faker->numberBetween(0, 3), // angka, bukan tanggal - 'verifikasi_sekretaris' => $this->faker->numberBetween(0, 3), // angka - 'verifikasi_camat' => $this->faker->numberBetween(0, 3), // angka - 'status_tte' => $this->faker->boolean(), - 'status' => $this->faker->numberBetween(0, 3), // angka - 'created_at' => now(), - 'updated_at' => now(), + 'tanggal' => $this->faker->date(), + 'nomor' => strtoupper(Str::random(10)), + 'nama' => $jenisSurat->nama ?? $this->faker->randomElement([ + 'Surat Pengantar RT/RW', + 'Fotokopi KK', + 'Surat Keterangan Kematian', + 'Surat Keterangan Cerai', + 'Surat Pindah Datang', + 'Fotokopi Ijasah Terakhir', + ]), + 'nama_penduduk' => $this->faker->name(), + 'file' => 'dummy.pdf', + 'keterangan' => $this->faker->sentence(), + 'log_verifikasi' => LogVerifikasiSurat::Operator, + 'verifikasi_operator' => StatusVerifikasiSurat::MenungguVerifikasi, + 'verifikasi_sekretaris' => StatusVerifikasiSurat::TidakAktif, + 'verifikasi_camat' => StatusVerifikasiSurat::TidakAktif, + 'status_tte' => true, + 'status' => StatusSurat::Permohonan, + 'created_at' => now(), + 'updated_at' => now(), ]; + } + public function menungguSekretaris() + { + return $this->state(function (array $attributes) { + return [ + 'log_verifikasi' => LogVerifikasiSurat::Sekretaris, + 'verifikasi_operator' => StatusVerifikasiSurat::TelahDiverifikasi, + 'verifikasi_sekretaris' => StatusVerifikasiSurat::MenungguVerifikasi, + ]; + }); + } + + public function menungguCamat() + { + return $this->state(function (array $attributes) { + return [ + 'log_verifikasi' => LogVerifikasiSurat::Camat, + 'verifikasi_operator' => StatusVerifikasiSurat::TelahDiverifikasi, + 'verifikasi_sekretaris' => StatusVerifikasiSurat::TelahDiverifikasi, + 'verifikasi_camat' => StatusVerifikasiSurat::MenungguVerifikasi, + ]; + }); + } + + public function menungguTTE() + { + return $this->state(function (array $attributes) { + return [ + 'log_verifikasi' => LogVerifikasiSurat::ProsesTTE, + 'verifikasi_operator' => StatusVerifikasiSurat::TelahDiverifikasi, + 'verifikasi_sekretaris' => StatusVerifikasiSurat::TelahDiverifikasi, + 'verifikasi_camat' => StatusVerifikasiSurat::TelahDiverifikasi, + ]; + }); + } + + public function arsip() + { + return $this->state(function (array $attributes) { + return [ + 'log_verifikasi' => LogVerifikasiSurat::SudahTTE, + 'verifikasi_operator' => StatusVerifikasiSurat::TelahDiverifikasi, + 'verifikasi_sekretaris' => StatusVerifikasiSurat::TelahDiverifikasi, + 'verifikasi_camat' => StatusVerifikasiSurat::TelahDiverifikasi, + 'status' => StatusSurat::Arsip, + 'status_tte' => true, + ]; + }); + } + + public function ditolak() + { + return $this->state(function (array $attributes) { + return [ + 'log_verifikasi' => LogVerifikasiSurat::Ditolak, + 'verifikasi_operator' => StatusVerifikasiSurat::TelahDiverifikasi, + 'verifikasi_sekretaris' => StatusVerifikasiSurat::TidakAktif, + 'verifikasi_camat' => StatusVerifikasiSurat::TidakAktif, + 'status' => StatusSurat::Ditolak, + 'keterangan' => 'Dokumen tidak lengkap', + ]; + }); } } diff --git a/database/migrations/2026_06_12_000001_add_file_hash_to_log_surat_table.php b/database/migrations/2026_06_12_000001_add_file_hash_to_log_surat_table.php new file mode 100644 index 0000000000..613a7f92db --- /dev/null +++ b/database/migrations/2026_06_12_000001_add_file_hash_to_log_surat_table.php @@ -0,0 +1,22 @@ +string('file_hash', 64)->nullable()->after('file'); + }); + } + + public function down() + { + Schema::table('das_log_surat', function (Blueprint $table) { + $table->dropColumn('file_hash'); + }); + } +} diff --git a/database/migrations/2026_07_19_000001_add_surat_id_to_log_tte_table.php b/database/migrations/2026_07_19_000001_add_surat_id_to_log_tte_table.php new file mode 100644 index 0000000000..0bdf51e762 --- /dev/null +++ b/database/migrations/2026_07_19_000001_add_surat_id_to_log_tte_table.php @@ -0,0 +1,22 @@ +unsignedBigInteger('surat_id')->nullable()->after('id'); + }); + } + + public function down() + { + Schema::table('das_log_tte', function (Blueprint $table) { + $table->dropColumn('surat_id'); + }); + } +} diff --git a/database/seeders/SuratPermohonanSeeder.php b/database/seeders/SuratPermohonanSeeder.php new file mode 100644 index 0000000000..32680e9a7a --- /dev/null +++ b/database/seeders/SuratPermohonanSeeder.php @@ -0,0 +1,239 @@ + 'Desa Contoh'], + [ + 'desa_id' => '3301011234567', + 'nama' => 'Desa Contoh', + 'website' => 'https://example.com', + 'luas_wilayah' => 10.5, + ] + ); + + $this->seedVerifikasiChain($desa->desa_id); + + $this->seedMenungguSekretaris($desa->desa_id); + + $this->seedMenungguCamat($desa->desa_id); + + $this->seedMenungguTTE($desa->desa_id); + + $this->seedArsip($desa->desa_id); + + $this->seedDitolak($desa->desa_id); + } + + private function seedVerifikasiChain(string $desaId): void + { + Surat::updateOrCreate( + ['nomor' => '001/' . now()->year . '/DPRD', 'desa_id' => $desaId], + [ + 'nik' => '3275010101010001', + 'nama_penduduk' => 'Budi Santoso', + 'pengurus_id' => $this->getOperator()->id, + 'tanggal' => now()->subDays(5), + 'nama' => 'Surat Pengantar RT/RW', + 'file' => 'dummy.pdf', + 'keterangan' => 'Permohonan surat pengantar untuk keperluan administrasi kependudukan', + 'log_verifikasi' => LogVerifikasiSurat::Operator, + 'verifikasi_operator' => StatusVerifikasiSurat::MenungguVerifikasi, + 'verifikasi_sekretaris' => StatusVerifikasiSurat::TidakAktif, + 'verifikasi_camat' => StatusVerifikasiSurat::TidakAktif, + 'status_tte' => true, + 'status' => StatusSurat::Permohonan, + ] + ); + + Surat::updateOrCreate( + ['nomor' => '002/' . now()->year . '/DPRD', 'desa_id' => $desaId], + [ + 'nik' => '3275010101010002', + 'nama_penduduk' => 'Siti Aminah', + 'pengurus_id' => $this->getOperator()->id, + 'tanggal' => now()->subDays(4), + 'nama' => 'Fotokopi KK', + 'file' => 'dummy.pdf', + 'keterangan' => 'Permohonan fotokopi Kartu Keluarga untuk keperluan pembuatan KTP', + 'log_verifikasi' => LogVerifikasiSurat::Operator, + 'verifikasi_operator' => StatusVerifikasiSurat::MenungguVerifikasi, + 'verifikasi_sekretaris' => StatusVerifikasiSurat::TidakAktif, + 'verifikasi_camat' => StatusVerifikasiSurat::TidakAktif, + 'status_tte' => true, + 'status' => StatusSurat::Permohonan, + ] + ); + } + + private function seedMenungguSekretaris(string $desaId): void + { + Surat::updateOrCreate( + ['nomor' => '003/' . now()->year . '/DPRD', 'desa_id' => $desaId], + [ + 'nik' => '3275010101010003', + 'nama_penduduk' => 'Ahmad Hidayat', + 'pengurus_id' => $this->getOperator()->id, + 'tanggal' => now()->subDays(3), + 'nama' => 'Surat Keterangan Kematian', + 'file' => 'dummy.pdf', + 'keterangan' => 'Permohonan surat keterangan kematian dari keluarga', + 'log_verifikasi' => LogVerifikasiSurat::Sekretaris, + 'verifikasi_operator' => StatusVerifikasiSurat::TelahDiverifikasi, + 'verifikasi_sekretaris' => StatusVerifikasiSurat::MenungguVerifikasi, + 'verifikasi_camat' => StatusVerifikasiSurat::TidakAktif, + 'status_tte' => true, + 'status' => StatusSurat::Permohonan, + ] + ); + } + + private function seedMenungguCamat(string $desaId): void + { + Surat::updateOrCreate( + ['nomor' => '004/' . now()->year . '/DPRD', 'desa_id' => $desaId], + [ + 'nik' => '3275010101010004', + 'nama_penduduk' => 'Dewi Lestari', + 'pengurus_id' => $this->getOperator()->id, + 'tanggal' => now()->subDays(2), + 'nama' => 'Surat Pindah Datang', + 'file' => 'dummy.pdf', + 'keterangan' => 'Permohonan surat pindah datang dari luar kota', + 'log_verifikasi' => LogVerifikasiSurat::Camat, + 'verifikasi_operator' => StatusVerifikasiSurat::TelahDiverifikasi, + 'verifikasi_sekretaris' => StatusVerifikasiSurat::TelahDiverifikasi, + 'verifikasi_camat' => StatusVerifikasiSurat::MenungguVerifikasi, + 'status_tte' => true, + 'status' => StatusSurat::Permohonan, + ] + ); + } + + private function seedMenungguTTE(string $desaId): void + { + Surat::updateOrCreate( + ['nomor' => '005/' . now()->year . '/DPRD', 'desa_id' => $desaId], + [ + 'nik' => '3275010101010005', + 'nama_penduduk' => 'Rizky Ramadhan', + 'pengurus_id' => $this->getOperator()->id, + 'tanggal' => now()->subDay(), + 'nama' => 'Surat Keterangan Cerai', + 'file' => 'dummy.pdf', + 'keterangan' => 'Permohonan surat keterangan cerai untuk keperluan pengadilan', + 'log_verifikasi' => LogVerifikasiSurat::ProsesTTE, + 'verifikasi_operator' => StatusVerifikasiSurat::TelahDiverifikasi, + 'verifikasi_sekretaris' => StatusVerifikasiSurat::TelahDiverifikasi, + 'verifikasi_camat' => StatusVerifikasiSurat::TelahDiverifikasi, + 'status_tte' => true, + 'status' => StatusSurat::Permohonan, + ] + ); + } + + private function seedArsip(string $desaId): void + { + Surat::updateOrCreate( + ['nomor' => '006/' . now()->year . '/DPRD', 'desa_id' => $desaId], + [ + 'nik' => '3275010101010006', + 'nama_penduduk' => 'Maya Putri', + 'pengurus_id' => $this->getOperator()->id, + 'tanggal' => now()->subDays(10), + 'nama' => 'Fotokopi Ijasah Terakhir', + 'file' => 'dummy.pdf', + 'keterangan' => 'Permohonan fotokopi ijasah untuk melamar pekerjaan', + 'log_verifikasi' => LogVerifikasiSurat::SudahTTE, + 'verifikasi_operator' => StatusVerifikasiSurat::TelahDiverifikasi, + 'verifikasi_sekretaris' => StatusVerifikasiSurat::TelahDiverifikasi, + 'verifikasi_camat' => StatusVerifikasiSurat::TelahDiverifikasi, + 'status_tte' => true, + 'status' => StatusSurat::Arsip, + ] + ); + } + + private function seedDitolak(string $desaId): void + { + Surat::updateOrCreate( + ['nomor' => '007/' . now()->year . '/DPRD', 'desa_id' => $desaId], + [ + 'nik' => '3275010101010007', + 'nama_penduduk' => 'Andi Wijaya', + 'pengurus_id' => $this->getOperator()->id, + 'tanggal' => now()->subDays(7), + 'nama' => 'Surat Keterangan Kematian dari Kepala Desa', + 'file' => 'dummy.pdf', + 'keterangan' => 'Dokumen pendukung tidak lengkap, NIK tidak terdata', + 'log_verifikasi' => LogVerifikasiSurat::Ditolak, + 'verifikasi_operator' => StatusVerifikasiSurat::TelahDiverifikasi, + 'verifikasi_sekretaris' => StatusVerifikasiSurat::TidakAktif, + 'verifikasi_camat' => StatusVerifikasiSurat::TidakAktif, + 'status_tte' => true, + 'status' => StatusSurat::Ditolak, + ] + ); + } + + private function getOperator(): Pengurus + { + if ($this->operator === null) { + $jabatan = Jabatan::factory()->create([ + 'nama' => 'Operator', + ]); + + $this->operator = Pengurus::factory()->create([ + 'jabatan_id' => $jabatan->id, + 'status' => 1, + 'pangkat' => 'Operator', + ]); + } + + return $this->operator; + } +} diff --git a/resources/views/layouts/fragments/sidebar.blade.php b/resources/views/layouts/fragments/sidebar.blade.php index 7190fecaf4..8bb64b3cd6 100644 --- a/resources/views/layouts/fragments/sidebar.blade.php +++ b/resources/views/layouts/fragments/sidebar.blade.php @@ -385,6 +385,8 @@
  • Arsip
  • +
  • Verifikasi +
  • Pengaturan
  • diff --git a/resources/views/surat/arsip.blade.php b/resources/views/surat/arsip.blade.php index c303985160..c3507de3ea 100644 --- a/resources/views/surat/arsip.blade.php +++ b/resources/views/surat/arsip.blade.php @@ -38,6 +38,7 @@ Nama Penduduk Ditandatangani oleh Tanggal + Hash @@ -105,6 +106,12 @@ class: 'text-center', data: 'tanggal', name: 'tanggal' }, + { + data: 'hash', + name: 'hash', + orderable: false, + searchable: false + }, ] }); diff --git a/resources/views/surat/permohonan/show.blade.php b/resources/views/surat/permohonan/show.blade.php index 8b35e72669..9530a19348 100644 --- a/resources/views/surat/permohonan/show.blade.php +++ b/resources/views/surat/permohonan/show.blade.php @@ -38,8 +38,11 @@