feat: reset selektif + import Excel jenis pelanggaran
- Reset selektif: admin bisa pilih data mana yang akan dikosongkan - Auto-seed kategori & SP threshold default setelah direset - Deteksi admin via role (tidak hardcoded ID 1) - UI reset full-width dengan checkbox card - Import Excel jenis pelanggaran (.xlsx) dengan template download - Drag & drop upload, validasi header, auto-match kategori, skip duplikat - Tombol Import + Download Template di halaman Jenis Pelanggaran
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\ViolationCategory;
|
||||
use App\Models\ViolationType;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||
|
||||
class ImportController extends Controller
|
||||
{
|
||||
public function downloadTemplate()
|
||||
{
|
||||
$spreadsheet = new \PhpOffice\PhpSpreadsheet\Spreadsheet();
|
||||
$sheet = $spreadsheet->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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user