diff --git a/app/Http/Controllers/Admin/ImportController.php b/app/Http/Controllers/Admin/ImportController.php new file mode 100644 index 0000000..1a47d16 --- /dev/null +++ b/app/Http/Controllers/Admin/ImportController.php @@ -0,0 +1,181 @@ +getActiveSheet(); + + // Header + $sheet->setCellValue('A1', 'Kategori'); + $sheet->setCellValue('B1', 'Nama Pelanggaran'); + $sheet->setCellValue('C1', 'Poin'); + $sheet->setCellValue('D1', 'Sanksi Default'); + $sheet->setCellValue('E1', 'Deskripsi'); + + // Bold header + $sheet->getStyle('A1:E1')->getFont()->setBold(true); + $sheet->getStyle('A1:E1')->getFill() + ->setFillType(\PhpOffice\PhpSpreadsheet\Style\Fill::FILL_SOLID) + ->getStartColor()->setARGB('FFE2E8F0'); + + // Contoh baris + $sheet->setCellValue('A2', 'Ringan'); + $sheet->setCellValue('B2', 'Terlambat masuk kelas'); + $sheet->setCellValue('C2', 5); + $sheet->setCellValue('D2', 'Teguran lisan'); + $sheet->setCellValue('E2', 'Terlambat masuk kelas setelah bel berbunyi'); + + $sheet->setCellValue('A3', 'Sedang'); + $sheet->setCellValue('B3', 'Membuang sampah sembarangan'); + $sheet->setCellValue('C3', 20); + $sheet->setCellValue('D3', 'Membersihkan lingkungan selama 1 jam'); + + // Autofit column width + foreach (range('A', 'E') as $col) { + $sheet->getColumnDimension($col)->setAutoSize(true); + } + + // Data validation for kategori column + $categories = ViolationCategory::where('is_active', true)->pluck('name')->toArray(); + if (!empty($categories)) { + $validation = $sheet->getCell('A2')->getDataValidation(); + $validation->setType(\PhpOffice\PhpSpreadsheet\Cell\DataValidation::TYPE_LIST); + $validation->setFormula1('"' . implode(',', $categories) . '"'); + $validation->setAllowBlank(false); + $validation->setShowDropDown(true); + } + + $writer = new Xlsx($spreadsheet); + + return response()->streamDownload(function () use ($writer) { + $writer->save('php://output'); + }, 'template-jenis-pelanggaran.xlsx', [ + 'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + ]); + } + + public function importViolationTypes(Request $request): RedirectResponse + { + $request->validate([ + 'file' => ['required', 'file', 'mimes:xlsx,xls', 'max:2048'], + ]); + + try { + $file = $request->file('file'); + $spreadsheet = IOFactory::load($file->getPathname()); + $worksheet = $spreadsheet->getActiveSheet(); + $rows = $worksheet->toArray(); + + if (count($rows) < 2) { + return back()->with('error', 'File kosong. Isi minimal 1 baris data.'); + } + + // Validate header + $header = array_map('trim', $rows[0]); + $expectedHeader = ['Kategori', 'Nama Pelanggaran', 'Poin', 'Sanksi Default', 'Deskripsi']; + if (array_slice($header, 0, 4) !== array_slice($expectedHeader, 0, 4)) { + return back()->with('error', 'Format kolom tidak sesuai. Gunakan template yang disediakan.'); + } + + $categories = ViolationCategory::where('is_active', true)->get()->keyBy(fn($c) => strtolower(trim($c->name))); + $imported = 0; + $skipped = 0; + $errors = []; + + foreach (array_slice($rows, 1) as $index => $row) { + $rowNum = $index + 2; + + $categoryName = trim($row[0] ?? ''); + $typeName = trim($row[1] ?? ''); + $points = trim($row[2] ?? ''); + $sanction = trim($row[3] ?? ''); + $description = trim($row[4] ?? ''); + + // Skip baris kosong + if (empty($categoryName) && empty($typeName)) { + continue; + } + + // Validasi: nama pelanggaran wajib + if (empty($typeName)) { + $errors[] = "Baris {$rowNum}: Nama Pelanggaran tidak boleh kosong."; + $skipped++; + continue; + } + + // Validasi: poin harus angka + if (!is_numeric($points) || $points < 0) { + $errors[] = "Baris {$rowNum} ('{$typeName}'): Poin harus angka positif."; + $skipped++; + continue; + } + + // Cari kategori + $catKey = strtolower($categoryName); + if (empty($categoryName) || !isset($categories[$catKey])) { + $errors[] = "Baris {$rowNum} ('{$typeName}'): Kategori '{$categoryName}' tidak ditemukan. Buat kategori terlebih dahulu."; + $skipped++; + continue; + } + + $category = $categories[$catKey]; + + // Cek duplikat nama + $exists = ViolationType::where('name', $typeName) + ->where('category_id', $category->id) + ->exists(); + + if ($exists) { + $skipped++; + continue; + } + + ViolationType::create([ + 'category_id' => $category->id, + 'name' => $typeName, + 'slug' => Str::slug($typeName . '-' . $category->id), + 'points' => (int) $points, + 'default_sanction' => $sanction ?: null, + 'description' => $description ?: null, + 'is_active' => true, + ]); + + $imported++; + } + + $message = "Berhasil mengimpor {$imported} jenis pelanggaran."; + if ($skipped > 0) { + $message .= " {$skipped} dilewati (duplikat/error)."; + } + + if (!empty($errors)) { + $message .= ' ' . implode(', ', array_slice($errors, 0, 5)); + if (count($errors) > 5) { + $message .= ' (+' . (count($errors) - 5) . ' error lainnya)'; + } + } + + if ($imported > 0) { + return back()->with('success', $message); + } else { + return back()->with('error', 'Tidak ada data baru yang diimpor. ' . implode(', ', array_slice($errors, 0, 3))); + } + + } catch (\Exception $e) { + return back()->with('error', 'Gagal membaca file: ' . $e->getMessage()); + } + } +} diff --git a/app/Http/Controllers/Admin/ResetController.php b/app/Http/Controllers/Admin/ResetController.php index 87ab5e0..35a5839 100644 --- a/app/Http/Controllers/Admin/ResetController.php +++ b/app/Http/Controllers/Admin/ResetController.php @@ -4,6 +4,9 @@ namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; use App\Models\User; +use App\Models\ViolationCategory; +use App\Models\ViolationType; +use App\Models\SpThreshold; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; @@ -22,7 +25,10 @@ class ResetController extends Controller 'students' => DB::table('students')->count(), 'classes' => DB::table('classes')->count(), 'settings' => DB::table('settings')->count(), - 'users_other' => DB::table('users')->where('id', '!=', 1)->count(), + 'categories' => DB::table('violation_categories')->count(), + 'types' => DB::table('violation_types')->count(), + 'thresholds' => DB::table('sp_thresholds')->count(), + 'users_other' => DB::table('users')->where('role', '!=', 'admin')->count(), ]; return view('settings.reset', compact('stats')); @@ -32,43 +38,127 @@ class ResetController extends Controller { $request->validate([ 'confirm_password' => ['required', 'string'], + 'reset_items' => ['required', 'array', 'min:1'], + 'reset_items.*' => ['in:violations,evidences,sp_letters,notifications,students,classes,categories,types,thresholds,settings,users'], ]); if (!Hash::check($request->confirm_password, $request->user()->password)) { return back()->with('error', 'Password salah. Reset dibatalkan.'); } + $items = $request->reset_items; + $admin = User::where('role', 'admin')->orderBy('id')->first(); + DB::statement('SET FOREIGN_KEY_CHECKS=0'); - DB::table('violation_evidences')->delete(); - DB::table('violations')->delete(); - DB::table('sp_letters')->delete(); - DB::table('notifications')->delete(); - DB::table('students')->delete(); - DB::table('classes')->delete(); - DB::table('violation_types')->delete(); - DB::table('violation_categories')->delete(); - DB::table('sp_thresholds')->delete(); - DB::table('settings')->delete(); + $cleared = []; - User::where('id', '!=', 1)->delete(); - User::where('id', 1)->update(['is_active' => true, 'role' => 'admin']); + if (in_array('evidences', $items)) { + DB::table('violation_evidences')->delete(); + $cleared[] = 'Foto bukti'; + } + + if (in_array('violations', $items)) { + DB::table('violations')->delete(); + $cleared[] = 'Pelanggaran'; + } + + if (in_array('sp_letters', $items)) { + DB::table('sp_letters')->delete(); + $cleared[] = 'Surat Peringatan'; + } + + if (in_array('notifications', $items)) { + DB::table('notifications')->delete(); + $cleared[] = 'Notifikasi'; + } + + if (in_array('students', $items)) { + DB::table('students')->delete(); + $cleared[] = 'Data siswa'; + } + + if (in_array('classes', $items)) { + DB::table('classes')->delete(); + $cleared[] = 'Data kelas'; + } + + if (in_array('types', $items)) { + DB::table('violation_types')->delete(); + $cleared[] = 'Jenis pelanggaran'; + } + + if (in_array('categories', $items)) { + DB::table('violation_categories')->delete(); + $cleared[] = 'Kategori pelanggaran'; + } + + if (in_array('thresholds', $items)) { + DB::table('sp_thresholds')->delete(); + $cleared[] = 'Ambang SP'; + } + + if (in_array('settings', $items)) { + DB::table('settings')->delete(); + $cleared[] = 'Pengaturan sekolah'; + } + + if (in_array('users', $items)) { + DB::table('users')->where('role', '!=', 'admin')->delete(); + $cleared[] = 'User lain'; + } DB::statement('SET FOREIGN_KEY_CHECKS=1'); - $violationsDir = storage_path('app/public/violations'); - if (is_dir($violationsDir)) { - $it = new \RecursiveIteratorIterator( - new \RecursiveDirectoryIterator($violationsDir, \RecursiveDirectoryIterator::SKIP_DOTS), - \RecursiveIteratorIterator::CHILD_FIRST - ); - foreach ($it as $file) { - if ($file->isDir()) rmdir($file->getRealPath()); - else unlink($file->getRealPath()); + // Auto-seed defaults jika kategori/tipe/threshold dihapus + if (in_array('categories', $items) && ViolationCategory::count() === 0) { + $this->seedDefaultCategories(); + } + if (in_array('thresholds', $items) && SpThreshold::count() === 0) { + $this->seedDefaultThresholds(); + } + + // Hapus file foto jika evidences dihapus + if (in_array('evidences', $items)) { + $violationsDir = storage_path('app/public/violations'); + if (is_dir($violationsDir)) { + $it = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($violationsDir, \RecursiveDirectoryIterator::SKIP_DOTS), + \RecursiveIteratorIterator::CHILD_FIRST + ); + foreach ($it as $file) { + if ($file->isDir()) rmdir($file->getRealPath()); + else unlink($file->getRealPath()); + } } } - return redirect()->route('users.index') - ->with('success', 'Reset berhasil! Semua data pelanggaran, siswa, master data, dan user lain dihapus. Hanya akun admin yang tersisa.'); + $msg = 'Reset berhasil! Data yang dibersihkan: ' . implode(', ', $cleared) . '.'; + + if ($admin) { + $admin->update(['is_active' => true]); + } + + return redirect()->route('settings.reset')->with('success', $msg); + } + + private function seedDefaultCategories(): void + { + $categories = [ + ['name' => 'Ringan', 'description' => 'Pelanggaran ringan (poin 1-15)', 'color' => '#eab308', 'min_points' => 1, 'max_points' => 15, 'is_active' => true, 'sort_order' => 1, 'created_at' => now(), 'updated_at' => now()], + ['name' => 'Sedang', 'description' => 'Pelanggaran sedang (poin 15-50)', 'color' => '#f97316', 'min_points' => 15, 'max_points' => 50, 'is_active' => true, 'sort_order' => 2, 'created_at' => now(), 'updated_at' => now()], + ['name' => 'Berat', 'description' => 'Pelanggaran berat (poin 50-100)', 'color' => '#ef4444', 'min_points' => 50, 'max_points' => 100, 'is_active' => true, 'sort_order' => 3, 'created_at' => now(), 'updated_at' => now()], + ]; + DB::table('violation_categories')->insert($categories); + } + + private function seedDefaultThresholds(): void + { + $thresholds = [ + ['name' => 'SP 1', 'slug' => 'sp-1', 'min_points' => 50, 'max_points' => 99, 'color' => '#eab308', 'is_active' => true, 'description' => 'Surat Peringatan 1 — poin mencapai 50', 'created_at' => now(), 'updated_at' => now()], + ['name' => 'SP 2', 'slug' => 'sp-2', 'min_points' => 100, 'max_points' => 149, 'color' => '#f97316', 'is_active' => true, 'description' => 'Surat Peringatan 2 — poin mencapai 100', 'created_at' => now(), 'updated_at' => now()], + ['name' => 'SP 3', 'slug' => 'sp-3', 'min_points' => 150, 'max_points' => null, 'color' => '#ef4444', 'is_active' => true, 'description' => 'Surat Peringatan 3 — poin mencapai 150', 'created_at' => now(), 'updated_at' => now()], + ]; + DB::table('sp_thresholds')->insert($thresholds); } } diff --git a/resources/views/settings/reset.blade.php b/resources/views/settings/reset.blade.php index 26e3ab6..2848dd2 100644 --- a/resources/views/settings/reset.blade.php +++ b/resources/views/settings/reset.blade.php @@ -2,114 +2,266 @@ @section('title', 'Reset Aplikasi') +@push('styles') + +@endpush + @section('content') -
Kembalikan aplikasi ke kondisi awal pabrik
-Kosongkan data aplikasi secara selektif
Semua data berikut akan DIHAPUS PERMANEN:
-Yang tetap tersimpan:
-Data yang dipilih akan dihapus permanen dan tidak bisa dikembalikan. Pastikan Anda telah membackup data penting sebelum melanjutkan.
Pelanggaran
-{{ $stats['violations'] }}
+