feat: laporan rekap pelanggaran PDF (per periode/kelas, scoping wali kelas) + infrastruktur test MySQL (phpunit, fix migrasi 1093, UserFactory username)
This commit is contained in:
@@ -0,0 +1,96 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Classes;
|
||||||
|
use App\Models\Setting;
|
||||||
|
use App\Models\Violation;
|
||||||
|
use Barryvdh\DomPDF\Facade\Pdf;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\View\View;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Laporan Rekap Pelanggaran (PDF) — untuk BK / Kepala Sekolah.
|
||||||
|
*/
|
||||||
|
class ViolationReportController extends Controller
|
||||||
|
{
|
||||||
|
public function index(): View
|
||||||
|
{
|
||||||
|
$classes = Classes::orderBy('name')->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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.3",
|
"php": "^8.3",
|
||||||
|
"barryvdh/laravel-dompdf": "^3.1",
|
||||||
"laravel/framework": "^13.8",
|
"laravel/framework": "^13.8",
|
||||||
"laravel/reverb": "^1.10",
|
"laravel/reverb": "^1.10",
|
||||||
"laravel/tinker": "^3.0",
|
"laravel/tinker": "^3.0",
|
||||||
|
|||||||
Generated
+526
-4
@@ -4,8 +4,85 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "201a8d1d916055d461ce40221c866999",
|
"content-hash": "afdc1f6e4dfd50c7b92edd5a03dbc47e",
|
||||||
"packages": [
|
"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",
|
"name": "brick/math",
|
||||||
"version": "0.18.0",
|
"version": "0.18.0",
|
||||||
@@ -582,6 +659,161 @@
|
|||||||
],
|
],
|
||||||
"time": "2024-02-05T11:56:58+00:00"
|
"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",
|
"name": "dragonmantank/cron-expression",
|
||||||
"version": "v3.6.0",
|
"version": "v3.6.0",
|
||||||
@@ -2549,6 +2781,73 @@
|
|||||||
},
|
},
|
||||||
"time": "2022-12-02T22:17:43+00:00"
|
"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",
|
"name": "monolog/monolog",
|
||||||
"version": "3.10.0",
|
"version": "3.10.0",
|
||||||
@@ -4581,6 +4880,86 @@
|
|||||||
],
|
],
|
||||||
"time": "2024-06-11T12:45:25+00:00"
|
"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",
|
"name": "symfony/clock",
|
||||||
"version": "v8.1.0",
|
"version": "v8.1.0",
|
||||||
@@ -7060,6 +7439,149 @@
|
|||||||
],
|
],
|
||||||
"time": "2026-06-09T10:54:51+00:00"
|
"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",
|
"name": "tijsverkoyen/css-to-inline-styles",
|
||||||
"version": "v2.4.0",
|
"version": "v2.4.0",
|
||||||
@@ -9561,12 +10083,12 @@
|
|||||||
],
|
],
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"minimum-stability": "stable",
|
"minimum-stability": "stable",
|
||||||
"stability-flags": [],
|
"stability-flags": {},
|
||||||
"prefer-stable": true,
|
"prefer-stable": true,
|
||||||
"prefer-lowest": false,
|
"prefer-lowest": false,
|
||||||
"platform": {
|
"platform": {
|
||||||
"php": "^8.3"
|
"php": "^8.3"
|
||||||
},
|
},
|
||||||
"platform-dev": [],
|
"platform-dev": {},
|
||||||
"plugin-api-version": "2.6.0"
|
"plugin-api-version": "2.9.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class UserFactory extends Factory
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'name' => fake()->name(),
|
'name' => fake()->name(),
|
||||||
|
'username' => fake()->unique()->userName(),
|
||||||
'email' => fake()->unique()->safeEmail(),
|
'email' => fake()->unique()->safeEmail(),
|
||||||
'email_verified_at' => now(),
|
'email_verified_at' => now(),
|
||||||
'password' => static::$password ??= Hash::make('password'),
|
'password' => static::$password ??= Hash::make('password'),
|
||||||
|
|||||||
@@ -13,13 +13,14 @@ return new class extends Migration
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Mark existing alpha & terlambat types as system
|
// Mark existing alpha & terlambat types as system
|
||||||
DB::table('violation_types')
|
// (ambil id dulu — MySQL 8 menolak UPDATE..SELECT dari tabel yang sama)
|
||||||
->whereIn('id', function ($q) {
|
$systemIds = DB::table('violation_types')
|
||||||
$q->select('id')
|
|
||||||
->from('violation_types')
|
|
||||||
->where('name', 'like', '%alpha%')
|
->where('name', 'like', '%alpha%')
|
||||||
->orWhere('slug', 'terlambat-datang-ke-sekolah-1');
|
->orWhere('slug', 'terlambat-datang-ke-sekolah-1')
|
||||||
})
|
->pluck('id');
|
||||||
|
|
||||||
|
DB::table('violation_types')
|
||||||
|
->whereIn('id', $systemIds)
|
||||||
->update(['is_system' => true]);
|
->update(['is_system' => true]);
|
||||||
|
|
||||||
// Set clean slugs for system types
|
// Set clean slugs for system types
|
||||||
|
|||||||
+6
-2
@@ -23,8 +23,12 @@
|
|||||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||||
<env name="BROADCAST_CONNECTION" value="null"/>
|
<env name="BROADCAST_CONNECTION" value="null"/>
|
||||||
<env name="CACHE_STORE" value="array"/>
|
<env name="CACHE_STORE" value="array"/>
|
||||||
<env name="DB_CONNECTION" value="sqlite"/>
|
<env name="DB_CONNECTION" value="mysql"/>
|
||||||
<env name="DB_DATABASE" value=":memory:"/>
|
<env name="DB_HOST" value="127.0.0.1"/>
|
||||||
|
<env name="DB_PORT" value="3306"/>
|
||||||
|
<env name="DB_USERNAME" value="ketertiban"/>
|
||||||
|
<env name="DB_PASSWORD" value="P4ssw0rd"/>
|
||||||
|
<env name="DB_DATABASE" value="aplikasi_ketertiban_test"/>
|
||||||
<env name="DB_URL" value=""/>
|
<env name="DB_URL" value=""/>
|
||||||
<env name="MAIL_MAILER" value="array"/>
|
<env name="MAIL_MAILER" value="array"/>
|
||||||
<env name="QUEUE_CONNECTION" value="sync"/>
|
<env name="QUEUE_CONNECTION" value="sync"/>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ $faIcons = [
|
|||||||
'users' => 'fa-solid fa-users',
|
'users' => 'fa-solid fa-users',
|
||||||
'clipboard-check' => 'fa-solid fa-clipboard-check',
|
'clipboard-check' => 'fa-solid fa-clipboard-check',
|
||||||
'document-text' => 'fa-solid fa-file-lines',
|
'document-text' => 'fa-solid fa-file-lines',
|
||||||
|
'file-lines' => 'fa-solid fa-file-lines',
|
||||||
'tag' => 'fa-solid fa-tag',
|
'tag' => 'fa-solid fa-tag',
|
||||||
'list' => 'fa-solid fa-list',
|
'list' => 'fa-solid fa-list',
|
||||||
'chart-bar' => 'fa-solid fa-chart-simple',
|
'chart-bar' => 'fa-solid fa-chart-simple',
|
||||||
|
|||||||
@@ -167,6 +167,10 @@
|
|||||||
<x-nav-item href="{{ route('sp-letters.index') }}" icon="document-text" :active="request()->routeIs('sp-letters.*')">Surat Peringatan</x-nav-item>
|
<x-nav-item href="{{ route('sp-letters.index') }}" icon="document-text" :active="request()->routeIs('sp-letters.*')">Surat Peringatan</x-nav-item>
|
||||||
@endcanPermission
|
@endcanPermission
|
||||||
|
|
||||||
|
@canPermission('violations-export')
|
||||||
|
<x-nav-item href="{{ route('reports.violations') }}" icon="file-lines" :active="request()->routeIs('reports.violations*')">Laporan</x-nav-item>
|
||||||
|
@endcanPermission
|
||||||
|
|
||||||
@canPermission('categories-manage')
|
@canPermission('categories-manage')
|
||||||
<div class="pt-4 mt-4 border-t border-gray-200">
|
<div class="pt-4 mt-4 border-t border-gray-200">
|
||||||
<p class="px-3 text-xs font-semibold text-gray-500 uppercase tracking-wider">Master Data</p>
|
<p class="px-3 text-xs font-semibold text-gray-500 uppercase tracking-wider">Master Data</p>
|
||||||
|
|||||||
@@ -0,0 +1,180 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="id">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Rekap Pelanggaran — {{ $school['name'] }}</title>
|
||||||
|
<style>
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { font-family: 'DejaVu Sans', sans-serif; font-size: 9px; color: #1a1a1a; margin: 0; padding: 0; }
|
||||||
|
.kop { display: flex; align-items: center; gap: 12px; border-bottom: 3px double #1e3a8a; padding-bottom: 10px; margin-bottom: 14px; }
|
||||||
|
.kop img { width: 62px; height: 62px; object-fit: contain; }
|
||||||
|
.kop .nama-sekolah { font-size: 15px; font-weight: bold; color: #1e3a8a; text-transform: uppercase; letter-spacing: 0.5px; }
|
||||||
|
.kop .alamat { font-size: 8px; color: #444; margin-top: 2px; }
|
||||||
|
.kop .sub { font-size: 8px; color: #666; }
|
||||||
|
.judul { text-align: center; margin: 12px 0 4px; }
|
||||||
|
.judul h2 { font-size: 13px; margin: 0; text-transform: uppercase; letter-spacing: 1px; }
|
||||||
|
.judul p { font-size: 9px; margin: 3px 0 0; color: #444; }
|
||||||
|
.meta { width: 100%; border-collapse: collapse; margin: 10px 0 6px; }
|
||||||
|
.meta td { padding: 2px 0; }
|
||||||
|
.summary { width: 100%; border-collapse: collapse; margin: 8px 0 14px; }
|
||||||
|
.summary td { border: 1px solid #cbd5e1; padding: 7px 8px; text-align: center; background: #f8fafc; }
|
||||||
|
.summary .angka { font-size: 16px; font-weight: bold; color: #1e3a8a; display: block; }
|
||||||
|
.summary .label { font-size: 8px; color: #555; text-transform: uppercase; }
|
||||||
|
table.detail { width: 100%; border-collapse: collapse; margin: 4px 0 14px; }
|
||||||
|
table.detail th { background: #1e3a8a; color: #fff; padding: 5px 6px; text-align: left; font-size: 8.5px; text-transform: uppercase; letter-spacing: 0.3px; }
|
||||||
|
table.detail td { border: 1px solid #cbd5e1; padding: 4px 6px; }
|
||||||
|
table.detail tr:nth-child(even) td { background: #f8fafc; }
|
||||||
|
h3 { font-size: 10px; margin: 12px 0 4px; color: #1e3a8a; text-transform: uppercase; }
|
||||||
|
.center { text-align: center; }
|
||||||
|
.right { text-align: right; }
|
||||||
|
.ttd { width: 100%; margin-top: 30px; }
|
||||||
|
.ttd td { width: 50%; text-align: center; font-size: 9px; vertical-align: top; }
|
||||||
|
.ttd .nama { margin-top: 70px; font-weight: bold; text-decoration: underline; }
|
||||||
|
.footer { margin-top: 16px; text-align: center; font-size: 7.5px; color: #888; border-top: 1px solid #e2e8f0; padding-top: 6px; }
|
||||||
|
.badge { display: inline-block; padding: 1px 6px; border-radius: 8px; font-size: 7.5px; font-weight: bold; }
|
||||||
|
.badge-selesai { background: #dcfce7; color: #166534; }
|
||||||
|
.badge-proses { background: #fef3c7; color: #92400e; }
|
||||||
|
.badge-baru { background: #dbeafe; color: #1e40af; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
{{-- Kop Sekolah --}}
|
||||||
|
<div class="kop">
|
||||||
|
@if (! empty($school['logo']) && file_exists(public_path('storage/'.$school['logo'])))
|
||||||
|
<img src="{{ public_path('storage/'.$school['logo']) }}" alt="logo">
|
||||||
|
@endif
|
||||||
|
<div>
|
||||||
|
<div class="nama-sekolah">{{ $school['name'] }}</div>
|
||||||
|
<div class="alamat">{{ $school['address'] }}</div>
|
||||||
|
<div class="sub">Telp. {{ $school['phone'] }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Judul --}}
|
||||||
|
<div class="judul">
|
||||||
|
<h2>Rekap Laporan Pelanggaran Siswa</h2>
|
||||||
|
<p>Periode: {{ $periode }} • Kelas: {{ $filterKelas }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Ringkasan --}}
|
||||||
|
<table class="summary">
|
||||||
|
<tr>
|
||||||
|
<td><span class="angka">{{ $summary['total_kasus'] }}</span><span class="label">Total Kasus</span></td>
|
||||||
|
<td><span class="angka">{{ $summary['total_poin'] }}</span><span class="label">Total Poin</span></td>
|
||||||
|
<td><span class="angka">{{ $summary['siswa_terlibat'] }}</span><span class="label">Siswa Terlibat</span></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{{-- Rekap per Jenis --}}
|
||||||
|
<h3>A. Rekap per Jenis Pelanggaran</h3>
|
||||||
|
<table class="detail">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width:30px">No</th>
|
||||||
|
<th>Jenis Pelanggaran</th>
|
||||||
|
<th>Kategori</th>
|
||||||
|
<th class="center" style="width:60px">Jumlah</th>
|
||||||
|
<th class="center" style="width:70px">Total Poin</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse ($perJenis as $i => $item)
|
||||||
|
<tr>
|
||||||
|
<td>{{ $i + 1 }}</td>
|
||||||
|
<td>{{ $item['jenis'] }}</td>
|
||||||
|
<td>{{ $item['kategori'] }}</td>
|
||||||
|
<td class="center">{{ $item['jumlah'] }}</td>
|
||||||
|
<td class="center">{{ $item['poin'] }}</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr><td colspan="5" class="center">Tidak ada data</td></tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{{-- Rekap per Kelas --}}
|
||||||
|
<h3>B. Rekap per Kelas</h3>
|
||||||
|
<table class="detail">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width:30px">No</th>
|
||||||
|
<th>Kelas</th>
|
||||||
|
<th class="center" style="width:80px">Jumlah</th>
|
||||||
|
<th class="center" style="width:90px">Total Poin</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@forelse ($perKelas as $i => $item)
|
||||||
|
<tr>
|
||||||
|
<td>{{ $i + 1 }}</td>
|
||||||
|
<td>{{ $item['kelas'] }}</td>
|
||||||
|
<td class="center">{{ $item['jumlah'] }}</td>
|
||||||
|
<td class="center">{{ $item['poin'] }}</td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr><td colspan="4" class="center">Tidak ada data</td></tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{{-- Rincian --}}
|
||||||
|
<h3>C. Rincian Pelanggaran</h3>
|
||||||
|
<table class="detail">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width:24px">No</th>
|
||||||
|
<th style="width:64px">Tanggal</th>
|
||||||
|
<th style="width:70px">NISN</th>
|
||||||
|
<th>Nama Siswa</th>
|
||||||
|
<th style="width:56px">Kelas</th>
|
||||||
|
<th>Jenis Pelanggaran</th>
|
||||||
|
<th class="center" style="width:34px">Poin</th>
|
||||||
|
<th style="width:66px">Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@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
|
||||||
|
<tr>
|
||||||
|
<td>{{ $i + 1 }}</td>
|
||||||
|
<td>{{ $v->violation_date?->format('d/m/Y') }}</td>
|
||||||
|
<td>{{ $v->student?->nisn }}</td>
|
||||||
|
<td>{{ $v->student?->full_name }}</td>
|
||||||
|
<td>{{ $v->student?->class?->name }}</td>
|
||||||
|
<td>{{ $v->violationType?->name }} ({{ $v->violationType?->category?->name ?? '-' }})</td>
|
||||||
|
<td class="center">{{ $v->points }}</td>
|
||||||
|
<td class="center"><span class="badge {{ $badge }}">{{ $status }}</span></td>
|
||||||
|
</tr>
|
||||||
|
@empty
|
||||||
|
<tr><td colspan="8" class="center">Tidak ada data pelanggaran pada periode ini</td></tr>
|
||||||
|
@endforelse
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
{{-- TTD --}}
|
||||||
|
<table class="ttd">
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div>Petugas / Guru BK</div>
|
||||||
|
<div class="nama"> </div>
|
||||||
|
<div>______________________</div>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div>Mengetahui,<br>Kepala {{ $school['name'] }}</div>
|
||||||
|
<div class="nama">{{ $school['kepala_sekolah'] }}</div>
|
||||||
|
<div>NIP. {{ $school['kepala_sekolah_nip'] }}</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
Dicetak {{ now()->locale('id')->isoFormat('dddd, D MMMM YYYY, HH:mm') }} WIB • E-TATIB {{ $school['name'] }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', 'Laporan Pelanggaran')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<div>
|
||||||
|
{{-- Header --}}
|
||||||
|
<div class="mb-6">
|
||||||
|
<h1 class="text-2xl font-bold text-gray-900 tracking-tight">Laporan Rekap Pelanggaran</h1>
|
||||||
|
<p class="text-sm text-gray-500 mt-1">Cetak rekap pelanggaran per periode untuk BK & Kepala Sekolah</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Form Filter --}}
|
||||||
|
<div class="bg-white rounded-2xl shadow-sm border border-gray-200 max-w-3xl">
|
||||||
|
<form method="GET" action="{{ route('reports.violations.pdf') }}" target="_blank" class="p-6 space-y-5">
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-700 mb-1.5">Dari Tanggal</label>
|
||||||
|
<input type="date" name="date_from" value="{{ request('date_from') }}"
|
||||||
|
class="w-full px-4 py-2.5 border border-gray-200 rounded-xl text-sm bg-gray-50/50 focus:bg-white focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-semibold text-gray-700 mb-1.5">Sampai Tanggal</label>
|
||||||
|
<input type="date" name="date_to" value="{{ request('date_to') }}"
|
||||||
|
class="w-full px-4 py-2.5 border border-gray-200 rounded-xl text-sm bg-gray-50/50 focus:bg-white focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition">
|
||||||
|
</div>
|
||||||
|
<div class="sm:col-span-2">
|
||||||
|
<label class="block text-sm font-semibold text-gray-700 mb-1.5">Filter Kelas (opsional)</label>
|
||||||
|
<select name="class_id"
|
||||||
|
class="w-full px-4 py-2.5 border border-gray-200 rounded-xl text-sm bg-gray-50/50 focus:bg-white focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition">
|
||||||
|
<option value="">— Semua Kelas —</option>
|
||||||
|
@foreach ($classes as $class)
|
||||||
|
<option value="{{ $class->id }}" @selected(request('class_id') == $class->id)>{{ $class->name }}</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-3 pt-2">
|
||||||
|
<button type="submit"
|
||||||
|
class="inline-flex items-center gap-2 px-5 py-2.5 text-sm font-semibold text-white bg-blue-600 rounded-xl hover:bg-blue-700 transition shadow-sm">
|
||||||
|
<i class="fa-solid fa-file-pdf text-xs"></i>
|
||||||
|
Cetak Laporan PDF
|
||||||
|
</button>
|
||||||
|
<a href="{{ route('violations.export', request()->only(['date_from', 'date_to'])) }}"
|
||||||
|
class="inline-flex items-center gap-2 px-5 py-2.5 text-sm font-semibold text-emerald-600 bg-emerald-50 border border-emerald-200 rounded-xl hover:bg-emerald-100 transition shadow-sm">
|
||||||
|
<i class="fa-solid fa-file-excel text-xs"></i>
|
||||||
|
Export Data Excel
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{-- Preview ringkas --}}
|
||||||
|
<div class="mt-6 grid grid-cols-1 sm:grid-cols-3 gap-4 max-w-3xl">
|
||||||
|
<div class="bg-gradient-to-br from-blue-600 to-blue-700 text-white rounded-2xl p-5 shadow-sm">
|
||||||
|
<div class="text-3xl font-bold">{{ $totalKasus }}</div>
|
||||||
|
<div class="text-sm text-blue-100 mt-1">Total Kasus Pelanggaran</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gradient-to-br from-amber-500 to-orange-600 text-white rounded-2xl p-5 shadow-sm">
|
||||||
|
<div class="text-3xl font-bold">{{ $totalPoin }}</div>
|
||||||
|
<div class="text-sm text-amber-100 mt-1">Total Poin</div>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gradient-to-br from-rose-500 to-rose-700 text-white rounded-2xl p-5 shadow-sm">
|
||||||
|
<div class="text-3xl font-bold">{{ $siswaTerlibat }}</div>
|
||||||
|
<div class="text-sm text-rose-100 mt-1">Siswa Terlibat</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
@@ -132,6 +132,8 @@ Route::middleware('auth')->group(function () {
|
|||||||
|
|
||||||
Route::middleware('permission:violations-export')->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('/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) =====
|
// ===== Administrasi (permission-based) =====
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ class ExampleTest extends TestCase
|
|||||||
*/
|
*/
|
||||||
public function test_the_application_returns_a_successful_response(): void
|
public function test_the_application_returns_a_successful_response(): void
|
||||||
{
|
{
|
||||||
$response = $this->get('/');
|
// Tamu diarahkan ke halaman login
|
||||||
|
$this->get('/')->assertRedirect(route('login'));
|
||||||
$response->assertStatus(200);
|
$this->get('/login')->assertOk();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use App\Models\Classes;
|
||||||
|
use App\Models\Setting;
|
||||||
|
use App\Models\Student;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Violation;
|
||||||
|
use App\Models\ViolationType;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class ViolationReportTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
parent::setUp();
|
||||||
|
// Role 'kepala_sekolah' punya permission violations-export (di-seed lewat migrasi)
|
||||||
|
$this->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');
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user