From c5a2ad6913c895e41f390721eb081857398db566 Mon Sep 17 00:00:00 2001 From: Sari Date: Mon, 3 Aug 2026 08:50:52 +0700 Subject: [PATCH] feat: laporan rekap pelanggaran PDF (per periode/kelas, scoping wali kelas) + infrastruktur test MySQL (phpunit, fix migrasi 1093, UserFactory username) --- .../Admin/ViolationReportController.php | 96 ++++ composer.json | 1 + composer.lock | 530 +++++++++++++++++- database/factories/UserFactory.php | 1 + ...add_is_system_to_violation_types_table.php | 13 +- phpunit.xml | 8 +- resources/views/components/nav-item.blade.php | 1 + resources/views/layouts/app.blade.php | 4 + .../reports/violation-report-pdf.blade.php | 180 ++++++ resources/views/reports/violations.blade.php | 70 +++ routes/web.php | 2 + tests/Feature/ExampleTest.php | 6 +- tests/Feature/ViolationReportTest.php | 104 ++++ 13 files changed, 1001 insertions(+), 15 deletions(-) create mode 100644 app/Http/Controllers/Admin/ViolationReportController.php create mode 100644 resources/views/reports/violation-report-pdf.blade.php create mode 100644 resources/views/reports/violations.blade.php create mode 100644 tests/Feature/ViolationReportTest.php diff --git a/app/Http/Controllers/Admin/ViolationReportController.php b/app/Http/Controllers/Admin/ViolationReportController.php new file mode 100644 index 0000000..e095fbd --- /dev/null +++ b/app/Http/Controllers/Admin/ViolationReportController.php @@ -0,0 +1,96 @@ +get(); + + $query = Violation::query() + ->when(auth()->user()->isScopedWaliKelas(), fn ($q) => $q->whereHas('student', fn ($qq) => $qq->whereIn('class_id', auth()->user()->homeroomClassIds()))); + + $totalKasus = (clone $query)->count(); + $totalPoin = (clone $query)->sum('points'); + $siswaTerlibat = (clone $query)->distinct('student_id')->count('student_id'); + + return view('reports.violations', compact('classes', 'totalKasus', 'totalPoin', 'siswaTerlibat')); + } + + public function pdf(Request $request) + { + $data = $request->validate([ + 'date_from' => ['nullable', 'date'], + 'date_to' => ['nullable', 'date', 'after_or_equal:date_from'], + 'class_id' => ['nullable', 'exists:classes,id'], + ]); + + $query = Violation::with(['student.class', 'violationType.category', 'handlings']) + ->when($data['date_from'] ?? null, fn ($q) => $q->whereDate('violation_date', '>=', $data['date_from'])) + ->when($data['date_to'] ?? null, fn ($q) => $q->whereDate('violation_date', '<=', $data['date_to'])) + ->when($data['class_id'] ?? null, fn ($q) => $q->whereHas('student', fn ($qq) => $qq->where('class_id', $data['class_id']))) + // Wali kelas murni: hanya kelas yang diwalikan + ->when(auth()->user()->isScopedWaliKelas(), fn ($q) => $q->whereHas('student', fn ($qq) => $qq->whereIn('class_id', auth()->user()->homeroomClassIds()))); + + $violations = $query->orderBy('violation_date')->get(); + + $summary = [ + 'total_kasus' => $violations->count(), + 'total_poin' => $violations->sum('points'), + 'siswa_terlibat' => $violations->pluck('student_id')->unique()->count(), + ]; + + $perJenis = $violations->groupBy(fn ($v) => $v->violationType?->name ?? 'Tanpa Jenis') + ->map(fn ($group) => [ + 'jenis' => $group->first()->violationType?->name ?? 'Tanpa Jenis', + 'kategori' => $group->first()->violationType?->category?->name ?? '-', + 'jumlah' => $group->count(), + 'poin' => $group->sum('points'), + ]) + ->sortByDesc('jumlah') + ->values(); + + $perKelas = $violations->groupBy(fn ($v) => $v->student?->class?->name ?? 'Tanpa Kelas') + ->map(fn ($group) => [ + 'kelas' => $group->first()->student?->class?->name ?? 'Tanpa Kelas', + 'jumlah' => $group->count(), + 'poin' => $group->sum('points'), + ]) + ->sortByDesc('jumlah') + ->values(); + + $school = [ + 'name' => Setting::getValue('school_name', 'SMK'), + 'address' => Setting::getValue('school_address', ''), + 'phone' => Setting::getValue('school_phone', ''), + 'logo' => Setting::getValue('school_logo', ''), + 'kepala_sekolah' => Setting::getValue('kepala_sekolah_name', ''), + 'kepala_sekolah_nip' => Setting::getValue('kepala_sekolah_nip', ''), + ]; + + $periode = ($data['date_from'] ?? 'Awal').' s/d '.($data['date_to'] ?? 'Sekarang'); + $filterKelas = $data['class_id'] ?? null + ? Classes::find($data['class_id'])?->name + : 'Semua Kelas'; + + $pdf = Pdf::loadView('reports.violation-report-pdf', compact( + 'violations', 'summary', 'perJenis', 'perKelas', 'school', 'periode', 'filterKelas' + ))->setPaper('a4', 'landscape'); + + $filename = 'rekap-pelanggaran-'.now()->format('Ymd-His').'.pdf'; + + return $pdf->download($filename); + } +} diff --git a/composer.json b/composer.json index fc78dc4..6314533 100644 --- a/composer.json +++ b/composer.json @@ -7,6 +7,7 @@ "license": "MIT", "require": { "php": "^8.3", + "barryvdh/laravel-dompdf": "^3.1", "laravel/framework": "^13.8", "laravel/reverb": "^1.10", "laravel/tinker": "^3.0", diff --git a/composer.lock b/composer.lock index da4fbd8..855f113 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,85 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "201a8d1d916055d461ce40221c866999", + "content-hash": "afdc1f6e4dfd50c7b92edd5a03dbc47e", "packages": [ + { + "name": "barryvdh/laravel-dompdf", + "version": "v3.1.2", + "source": { + "type": "git", + "url": "https://github.com/barryvdh/laravel-dompdf.git", + "reference": "ee3b72b19ccdf57d0243116ecb2b90261344dedc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/barryvdh/laravel-dompdf/zipball/ee3b72b19ccdf57d0243116ecb2b90261344dedc", + "reference": "ee3b72b19ccdf57d0243116ecb2b90261344dedc", + "shasum": "" + }, + "require": { + "dompdf/dompdf": "^3.0", + "illuminate/support": "^9|^10|^11|^12|^13.0", + "php": "^8.1" + }, + "require-dev": { + "larastan/larastan": "^2.7|^3.0", + "orchestra/testbench": "^7|^8|^9.16|^10|^11.0", + "phpro/grumphp": "^2.5", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "PDF": "Barryvdh\\DomPDF\\Facade\\Pdf", + "Pdf": "Barryvdh\\DomPDF\\Facade\\Pdf" + }, + "providers": [ + "Barryvdh\\DomPDF\\ServiceProvider" + ] + }, + "branch-alias": { + "dev-master": "3.0-dev" + } + }, + "autoload": { + "psr-4": { + "Barryvdh\\DomPDF\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Barry vd. Heuvel", + "email": "barryvdh@gmail.com" + } + ], + "description": "A DOMPDF Wrapper for Laravel", + "keywords": [ + "dompdf", + "laravel", + "pdf" + ], + "support": { + "issues": "https://github.com/barryvdh/laravel-dompdf/issues", + "source": "https://github.com/barryvdh/laravel-dompdf/tree/v3.1.2" + }, + "funding": [ + { + "url": "https://fruitcake.nl", + "type": "custom" + }, + { + "url": "https://github.com/barryvdh", + "type": "github" + } + ], + "time": "2026-02-21T08:51:10+00:00" + }, { "name": "brick/math", "version": "0.18.0", @@ -582,6 +659,161 @@ ], "time": "2024-02-05T11:56:58+00:00" }, + { + "name": "dompdf/dompdf", + "version": "v3.1.6", + "source": { + "type": "git", + "url": "https://github.com/dompdf/dompdf.git", + "reference": "6d4b4eb8500f7a786da8868ba463a71b725a4005" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/dompdf/zipball/6d4b4eb8500f7a786da8868ba463a71b725a4005", + "reference": "6d4b4eb8500f7a786da8868ba463a71b725a4005", + "shasum": "" + }, + "require": { + "dompdf/php-font-lib": "^1.0.0", + "dompdf/php-svg-lib": "^1.0.0", + "ext-dom": "*", + "ext-mbstring": "*", + "masterminds/html5": "^2.0", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "ext-gd": "*", + "ext-json": "*", + "ext-zip": "*", + "mockery/mockery": "^1.3", + "phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11", + "squizlabs/php_codesniffer": "^3.5", + "symfony/process": "^4.4 || ^5.4 || ^6.2 || ^7.0" + }, + "suggest": { + "ext-gd": "Needed to process images", + "ext-gmagick": "Improves image processing performance", + "ext-imagick": "Improves image processing performance", + "ext-zlib": "Needed for pdf stream compression" + }, + "type": "library", + "autoload": { + "psr-4": { + "Dompdf\\": "src/" + }, + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1" + ], + "authors": [ + { + "name": "The Dompdf Community", + "homepage": "https://github.com/dompdf/dompdf/blob/master/AUTHORS.md" + } + ], + "description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter", + "homepage": "https://github.com/dompdf/dompdf", + "support": { + "issues": "https://github.com/dompdf/dompdf/issues", + "source": "https://github.com/dompdf/dompdf/tree/v3.1.6" + }, + "time": "2026-07-20T12:29:38+00:00" + }, + { + "name": "dompdf/php-font-lib", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/dompdf/php-font-lib.git", + "reference": "a6e9a688a2a80016ac080b97be73d3e10c444c9a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/a6e9a688a2a80016ac080b97be73d3e10c444c9a", + "reference": "a6e9a688a2a80016ac080b97be73d3e10c444c9a", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11 || ^12" + }, + "type": "library", + "autoload": { + "psr-4": { + "FontLib\\": "src/FontLib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-2.1-or-later" + ], + "authors": [ + { + "name": "The FontLib Community", + "homepage": "https://github.com/dompdf/php-font-lib/blob/master/AUTHORS.md" + } + ], + "description": "A library to read, parse, export and make subsets of different types of font files.", + "homepage": "https://github.com/dompdf/php-font-lib", + "support": { + "issues": "https://github.com/dompdf/php-font-lib/issues", + "source": "https://github.com/dompdf/php-font-lib/tree/1.0.2" + }, + "time": "2026-01-20T14:10:26+00:00" + }, + { + "name": "dompdf/php-svg-lib", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/dompdf/php-svg-lib.git", + "reference": "8259ffb930817e72b1ff1caef5d226501f3dfeb1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/8259ffb930817e72b1ff1caef5d226501f3dfeb1", + "reference": "8259ffb930817e72b1ff1caef5d226501f3dfeb1", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^7.1 || ^8.0", + "sabberworm/php-css-parser": "^8.4 || ^9.0" + }, + "require-dev": { + "phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11" + }, + "type": "library", + "autoload": { + "psr-4": { + "Svg\\": "src/Svg" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "The SvgLib Community", + "homepage": "https://github.com/dompdf/php-svg-lib/blob/master/AUTHORS.md" + } + ], + "description": "A library to read, parse and export to PDF SVG files.", + "homepage": "https://github.com/dompdf/php-svg-lib", + "support": { + "issues": "https://github.com/dompdf/php-svg-lib/issues", + "source": "https://github.com/dompdf/php-svg-lib/tree/1.0.2" + }, + "time": "2026-01-02T16:01:13+00:00" + }, { "name": "dragonmantank/cron-expression", "version": "v3.6.0", @@ -2549,6 +2781,73 @@ }, "time": "2022-12-02T22:17:43+00:00" }, + { + "name": "masterminds/html5", + "version": "2.10.1", + "source": { + "type": "git", + "url": "https://github.com/Masterminds/html5-php.git", + "reference": "fd5018f6815fff903946d0564977b44ce8010e29" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fd5018f6815fff903946d0564977b44ce8010e29", + "reference": "fd5018f6815fff903946d0564977b44ce8010e29", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9 || ^10" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.7-dev" + } + }, + "autoload": { + "psr-4": { + "Masterminds\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Matt Butcher", + "email": "technosophos@gmail.com" + }, + { + "name": "Matt Farina", + "email": "matt@mattfarina.com" + }, + { + "name": "Asmir Mustafic", + "email": "goetas@gmail.com" + } + ], + "description": "An HTML5 parser and serializer.", + "homepage": "http://masterminds.github.io/html5-php", + "keywords": [ + "HTML5", + "dom", + "html", + "parser", + "querypath", + "serializer", + "xml" + ], + "support": { + "issues": "https://github.com/Masterminds/html5-php/issues", + "source": "https://github.com/Masterminds/html5-php/tree/2.10.1" + }, + "time": "2026-06-23T18:43:15+00:00" + }, { "name": "monolog/monolog", "version": "3.10.0", @@ -4581,6 +4880,86 @@ ], "time": "2024-06-11T12:45:25+00:00" }, + { + "name": "sabberworm/php-css-parser", + "version": "v9.4.0", + "source": { + "type": "git", + "url": "https://github.com/MyIntervals/PHP-CSS-Parser.git", + "reference": "fd3bf9fb173e0df649bc4e3e0d088a1b2417c08f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/fd3bf9fb173e0df649bc4e3e0d088a1b2417c08f", + "reference": "fd3bf9fb173e0df649bc4e3e0d088a1b2417c08f", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "php": "^7.2.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0", + "thecodingmachine/safe": "^1.3 || ^2.5 || ^3.4" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "1.4.0", + "phpstan/extension-installer": "1.4.3", + "phpstan/phpstan": "1.12.33 || 2.2.2", + "phpstan/phpstan-phpunit": "1.4.2 || 2.0.16", + "phpstan/phpstan-strict-rules": "1.6.2 || 2.0.11", + "phpunit/phpunit": "8.5.52", + "rawr/phpunit-data-provider": "3.3.1", + "rector/rector": "1.2.10 || 2.4.6", + "rector/type-perfect": "1.0.0 || 2.1.3", + "squizlabs/php_codesniffer": "4.0.1", + "thecodingmachine/phpstan-safe-rule": "1.2.0 || 1.4.3" + }, + "suggest": { + "ext-mbstring": "for parsing UTF-8 CSS" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "9.5.x-dev" + } + }, + "autoload": { + "files": [ + "src/Rule/Rule.php", + "src/RuleSet/RuleContainer.php" + ], + "psr-4": { + "Sabberworm\\CSS\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Raphael Schweikert" + }, + { + "name": "Oliver Klee", + "email": "github@oliverklee.de" + }, + { + "name": "Jake Hotson", + "email": "jake.github@qzdesign.co.uk" + } + ], + "description": "Parser for CSS Files written in PHP", + "homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser", + "keywords": [ + "css", + "parser", + "stylesheet" + ], + "support": { + "issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues", + "source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v9.4.0" + }, + "time": "2026-06-18T15:10:53+00:00" + }, { "name": "symfony/clock", "version": "v8.1.0", @@ -7060,6 +7439,149 @@ ], "time": "2026-06-09T10:54:51+00:00" }, + { + "name": "thecodingmachine/safe", + "version": "v3.4.0", + "source": { + "type": "git", + "url": "https://github.com/thecodingmachine/safe.git", + "reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/705683a25bacf0d4860c7dea4d7947bfd09eea19", + "reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19", + "shasum": "" + }, + "require": { + "php": "^8.1" + }, + "require-dev": { + "php-parallel-lint/php-parallel-lint": "^1.4", + "phpstan/phpstan": "^2", + "phpunit/phpunit": "^10", + "squizlabs/php_codesniffer": "^3.2" + }, + "type": "library", + "autoload": { + "files": [ + "lib/special_cases.php", + "generated/apache.php", + "generated/apcu.php", + "generated/array.php", + "generated/bzip2.php", + "generated/calendar.php", + "generated/classobj.php", + "generated/com.php", + "generated/cubrid.php", + "generated/curl.php", + "generated/datetime.php", + "generated/dir.php", + "generated/eio.php", + "generated/errorfunc.php", + "generated/exec.php", + "generated/fileinfo.php", + "generated/filesystem.php", + "generated/filter.php", + "generated/fpm.php", + "generated/ftp.php", + "generated/funchand.php", + "generated/gettext.php", + "generated/gmp.php", + "generated/gnupg.php", + "generated/hash.php", + "generated/ibase.php", + "generated/ibmDb2.php", + "generated/iconv.php", + "generated/image.php", + "generated/imap.php", + "generated/info.php", + "generated/inotify.php", + "generated/json.php", + "generated/ldap.php", + "generated/libxml.php", + "generated/lzf.php", + "generated/mailparse.php", + "generated/mbstring.php", + "generated/misc.php", + "generated/mysql.php", + "generated/mysqli.php", + "generated/network.php", + "generated/oci8.php", + "generated/opcache.php", + "generated/openssl.php", + "generated/outcontrol.php", + "generated/pcntl.php", + "generated/pcre.php", + "generated/pgsql.php", + "generated/posix.php", + "generated/ps.php", + "generated/pspell.php", + "generated/readline.php", + "generated/rnp.php", + "generated/rpminfo.php", + "generated/rrd.php", + "generated/sem.php", + "generated/session.php", + "generated/shmop.php", + "generated/sockets.php", + "generated/sodium.php", + "generated/solr.php", + "generated/spl.php", + "generated/sqlsrv.php", + "generated/ssdeep.php", + "generated/ssh2.php", + "generated/stream.php", + "generated/strings.php", + "generated/swoole.php", + "generated/uodbc.php", + "generated/uopz.php", + "generated/url.php", + "generated/var.php", + "generated/xdiff.php", + "generated/xml.php", + "generated/xmlrpc.php", + "generated/yaml.php", + "generated/yaz.php", + "generated/zip.php", + "generated/zlib.php" + ], + "classmap": [ + "lib/DateTime.php", + "lib/DateTimeImmutable.php", + "lib/Exceptions/", + "generated/Exceptions/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHP core functions that throw exceptions instead of returning FALSE on error", + "support": { + "issues": "https://github.com/thecodingmachine/safe/issues", + "source": "https://github.com/thecodingmachine/safe/tree/v3.4.0" + }, + "funding": [ + { + "url": "https://github.com/OskarStark", + "type": "github" + }, + { + "url": "https://github.com/shish", + "type": "github" + }, + { + "url": "https://github.com/silasjoisten", + "type": "github" + }, + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2026-02-04T18:08:13+00:00" + }, { "name": "tijsverkoyen/css-to-inline-styles", "version": "v2.4.0", @@ -9561,12 +10083,12 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { "php": "^8.3" }, - "platform-dev": [], - "plugin-api-version": "2.6.0" + "platform-dev": {}, + "plugin-api-version": "2.9.0" } diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php index c4ceb07..d4e1faa 100644 --- a/database/factories/UserFactory.php +++ b/database/factories/UserFactory.php @@ -26,6 +26,7 @@ class UserFactory extends Factory { return [ 'name' => fake()->name(), + 'username' => fake()->unique()->userName(), 'email' => fake()->unique()->safeEmail(), 'email_verified_at' => now(), 'password' => static::$password ??= Hash::make('password'), diff --git a/database/migrations/2026_07_22_164059_add_is_system_to_violation_types_table.php b/database/migrations/2026_07_22_164059_add_is_system_to_violation_types_table.php index d07eb5e..2c29367 100644 --- a/database/migrations/2026_07_22_164059_add_is_system_to_violation_types_table.php +++ b/database/migrations/2026_07_22_164059_add_is_system_to_violation_types_table.php @@ -13,13 +13,14 @@ return new class extends Migration }); // Mark existing alpha & terlambat types as system + // (ambil id dulu — MySQL 8 menolak UPDATE..SELECT dari tabel yang sama) + $systemIds = DB::table('violation_types') + ->where('name', 'like', '%alpha%') + ->orWhere('slug', 'terlambat-datang-ke-sekolah-1') + ->pluck('id'); + DB::table('violation_types') - ->whereIn('id', function ($q) { - $q->select('id') - ->from('violation_types') - ->where('name', 'like', '%alpha%') - ->orWhere('slug', 'terlambat-datang-ke-sekolah-1'); - }) + ->whereIn('id', $systemIds) ->update(['is_system' => true]); // Set clean slugs for system types diff --git a/phpunit.xml b/phpunit.xml index e7f0a48..cdd1588 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -23,8 +23,12 @@ - - + + + + + + diff --git a/resources/views/components/nav-item.blade.php b/resources/views/components/nav-item.blade.php index 450ee4a..5809030 100644 --- a/resources/views/components/nav-item.blade.php +++ b/resources/views/components/nav-item.blade.php @@ -13,6 +13,7 @@ $faIcons = [ 'users' => 'fa-solid fa-users', 'clipboard-check' => 'fa-solid fa-clipboard-check', 'document-text' => 'fa-solid fa-file-lines', + 'file-lines' => 'fa-solid fa-file-lines', 'tag' => 'fa-solid fa-tag', 'list' => 'fa-solid fa-list', 'chart-bar' => 'fa-solid fa-chart-simple', diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 6f84bce..933ee6d 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -167,6 +167,10 @@ Surat Peringatan @endcanPermission + @canPermission('violations-export') + Laporan + @endcanPermission + @canPermission('categories-manage')

Master Data

diff --git a/resources/views/reports/violation-report-pdf.blade.php b/resources/views/reports/violation-report-pdf.blade.php new file mode 100644 index 0000000..77cfa4f --- /dev/null +++ b/resources/views/reports/violation-report-pdf.blade.php @@ -0,0 +1,180 @@ + + + + + Rekap Pelanggaran — {{ $school['name'] }} + + + + + {{-- Kop Sekolah --}} +
+ @if (! empty($school['logo']) && file_exists(public_path('storage/'.$school['logo']))) + logo + @endif +
+
{{ $school['name'] }}
+
{{ $school['address'] }}
+
Telp. {{ $school['phone'] }}
+
+
+ + {{-- Judul --}} +
+

Rekap Laporan Pelanggaran Siswa

+

Periode: {{ $periode }}  •  Kelas: {{ $filterKelas }}

+
+ + {{-- Ringkasan --}} + + + + + + +
{{ $summary['total_kasus'] }}Total Kasus{{ $summary['total_poin'] }}Total Poin{{ $summary['siswa_terlibat'] }}Siswa Terlibat
+ + {{-- Rekap per Jenis --}} +

A. Rekap per Jenis Pelanggaran

+ + + + + + + + + + + + @forelse ($perJenis as $i => $item) + + + + + + + + @empty + + @endforelse + +
NoJenis PelanggaranKategoriJumlahTotal Poin
{{ $i + 1 }}{{ $item['jenis'] }}{{ $item['kategori'] }}{{ $item['jumlah'] }}{{ $item['poin'] }}
Tidak ada data
+ + {{-- Rekap per Kelas --}} +

B. Rekap per Kelas

+ + + + + + + + + + + @forelse ($perKelas as $i => $item) + + + + + + + @empty + + @endforelse + +
NoKelasJumlahTotal Poin
{{ $i + 1 }}{{ $item['kelas'] }}{{ $item['jumlah'] }}{{ $item['poin'] }}
Tidak ada data
+ + {{-- Rincian --}} +

C. Rincian Pelanggaran

+ + + + + + + + + + + + + + + @forelse ($violations as $i => $v) + @php + $status = $v->handlings->where('status', 'selesai')->count() > 0 ? 'Selesai' + : ($v->handlings->count() > 0 ? 'Proses' : 'Baru'); + $badge = $status === 'Selesai' ? 'badge-selesai' : ($status === 'Proses' ? 'badge-proses' : 'badge-baru'); + @endphp + + + + + + + + + + + @empty + + @endforelse + +
NoTanggalNISNNama SiswaKelasJenis PelanggaranPoinStatus
{{ $i + 1 }}{{ $v->violation_date?->format('d/m/Y') }}{{ $v->student?->nisn }}{{ $v->student?->full_name }}{{ $v->student?->class?->name }}{{ $v->violationType?->name }} ({{ $v->violationType?->category?->name ?? '-' }}){{ $v->points }}{{ $status }}
Tidak ada data pelanggaran pada periode ini
+ + {{-- TTD --}} + + + + + +
+
Petugas / Guru BK
+
 
+
______________________
+
+
Mengetahui,
Kepala {{ $school['name'] }}
+
{{ $school['kepala_sekolah'] }}
+
NIP. {{ $school['kepala_sekolah_nip'] }}
+
+ + + + + diff --git a/resources/views/reports/violations.blade.php b/resources/views/reports/violations.blade.php new file mode 100644 index 0000000..7c130f6 --- /dev/null +++ b/resources/views/reports/violations.blade.php @@ -0,0 +1,70 @@ +@extends('layouts.app') + +@section('title', 'Laporan Pelanggaran') + +@section('content') +
+ {{-- Header --}} +
+

Laporan Rekap Pelanggaran

+

Cetak rekap pelanggaran per periode untuk BK & Kepala Sekolah

+
+ + {{-- Form Filter --}} +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + + + Export Data Excel + +
+
+
+ + {{-- Preview ringkas --}} +
+
+
{{ $totalKasus }}
+
Total Kasus Pelanggaran
+
+
+
{{ $totalPoin }}
+
Total Poin
+
+
+
{{ $siswaTerlibat }}
+
Siswa Terlibat
+
+
+
+@endsection diff --git a/routes/web.php b/routes/web.php index be5d585..b777b7a 100644 --- a/routes/web.php +++ b/routes/web.php @@ -132,6 +132,8 @@ Route::middleware('auth')->group(function () { Route::middleware('permission:violations-export')->group(function () { Route::get('/export/violations', [\App\Http\Controllers\Admin\ViolationExportController::class, 'export'])->name('violations.export'); + Route::get('/reports/violations', [\App\Http\Controllers\Admin\ViolationReportController::class, 'index'])->name('reports.violations'); + Route::get('/reports/violations/pdf', [\App\Http\Controllers\Admin\ViolationReportController::class, 'pdf'])->name('reports.violations.pdf'); }); // ===== Administrasi (permission-based) ===== diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php index 8364a84..3b1cb93 100644 --- a/tests/Feature/ExampleTest.php +++ b/tests/Feature/ExampleTest.php @@ -12,8 +12,8 @@ class ExampleTest extends TestCase */ public function test_the_application_returns_a_successful_response(): void { - $response = $this->get('/'); - - $response->assertStatus(200); + // Tamu diarahkan ke halaman login + $this->get('/')->assertRedirect(route('login')); + $this->get('/login')->assertOk(); } } diff --git a/tests/Feature/ViolationReportTest.php b/tests/Feature/ViolationReportTest.php new file mode 100644 index 0000000..6fd267e --- /dev/null +++ b/tests/Feature/ViolationReportTest.php @@ -0,0 +1,104 @@ +admin = User::factory()->create(['roles' => ['kepala_sekolah']]); + } + + protected function seedData(): void + { + Setting::setValue('school_name', 'SMKN 1 WONOREJO'); + Setting::setValue('school_address', 'Jl. Pesantren'); + Setting::setValue('school_phone', '(0343) 410064'); + Setting::setValue('kepala_sekolah_name', 'Dr. H. Ahmad Sobirin'); + Setting::setValue('kepala_sekolah_nip', '197606042008011014'); + + $class = Classes::create(['name' => 'X-TKJ 1', 'level' => 'X']); + $student = Student::create([ + 'full_name' => 'Budi Santoso', + 'nisn' => '1234567890', + 'class_id' => $class->id, + 'class_name' => 'X-TKJ 1', + ]); + $category = \App\Models\ViolationCategory::create(['name' => 'Kedisiplinan', 'slug' => 'kedisiplinan']); + $type = ViolationType::create(['name' => 'Terlambat', 'slug' => 'terlambat', 'points' => 5, 'category_id' => $category->id]); + + Violation::create([ + 'student_id' => $student->id, + 'student_class' => 'X-TKJ 1', + 'violation_type_id' => $type->id, + 'violation_date' => '2026-07-20', + 'points' => 5, + 'recorded_by' => $this->admin->id, + ]); + Violation::create([ + 'student_id' => $student->id, + 'student_class' => 'X-TKJ 1', + 'violation_type_id' => $type->id, + 'violation_date' => '2026-07-21', + 'points' => 5, + 'recorded_by' => $this->admin->id, + ]); + } + + public function test_halaman_laporan_membutuhkan_permission(): void + { + $user = User::factory()->create(); + + $this->actingAs($user)->get('/reports/violations')->assertForbidden(); + } + + public function test_halaman_laporan_terbuka_untuk_yang_punya_permission(): void + { + $this->seedData(); + + $this->actingAs($this->admin) + ->get('/reports/violations') + ->assertOk() + ->assertSee('Laporan Rekap Pelanggaran') + ->assertSee('Cetak Laporan PDF'); + } + + public function test_pdf_laporan_valid(): void + { + $this->seedData(); + + $response = $this->actingAs($this->admin) + ->get('/reports/violations/pdf?date_from=2026-07-01&date_to=2026-07-31'); + + $response->assertOk(); + $response->assertHeader('content-type', 'application/pdf'); + + $content = $response->getContent(); + $this->assertStringStartsWith('%PDF-', $content); + $this->assertGreaterThan(1000, strlen($content)); // PDF terisi, bukan kosong + } + + public function test_pdf_laporan_dengan_filter_kelas(): void + { + $this->seedData(); + $class = Classes::first(); + + $this->actingAs($this->admin) + ->get("/reports/violations/pdf?class_id={$class->id}") + ->assertOk() + ->assertHeader('content-type', 'application/pdf'); + } +}