Initial commit: Aplikasi Ketertiban Siswa
- Fitur: input pelanggaran, sistem poin, surat peringatan otomatis, SP progress - Realtime notifikasi via Laravel Reverb + Echo - Dashboard statistik, manajemen user, manajemen master data - Login 2 kolom premium, user dropdown, pengaturan akun - Tema biru kalem, logo sekolah dinamis, sidebar brand dinamis - Sinkronisasi siswa dari Database Kesiswaan via API
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Classes;
|
||||
use App\Models\Student;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class StudentSyncService
|
||||
{
|
||||
public function sync(string $baseUrl, string $token): array
|
||||
{
|
||||
$results = [
|
||||
'students_created' => 0,
|
||||
'students_updated' => 0,
|
||||
'classes_created' => 0,
|
||||
'classes_updated' => 0,
|
||||
'errors' => [],
|
||||
];
|
||||
|
||||
try {
|
||||
$response = Http::acceptJson()
|
||||
->withToken($token)
|
||||
->timeout(60)
|
||||
->get(rtrim($baseUrl, '/') . '/api/integration/students');
|
||||
|
||||
if (!$response->successful()) {
|
||||
return ['error' => 'Gagal terhubung: ' . $response->status()];
|
||||
}
|
||||
|
||||
$payload = $response->json();
|
||||
$items = Arr::get($payload, 'data', []);
|
||||
|
||||
DB::transaction(function () use ($items, &$results) {
|
||||
foreach ($items as $item) {
|
||||
// Only sync active students
|
||||
$status = $item['status'] ?? 'active';
|
||||
if ($status !== 'active') {
|
||||
$results['errors'][] = 'Skipped (status: ' . $status . '): ' . ($item['full_name'] ?? 'unknown');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use NISN as primary key, fallback to student_number if NISN is null
|
||||
$nisn = $item['nisn'] ?? null;
|
||||
if (!$nisn) {
|
||||
$studentNumber = $item['student_number'] ?? null;
|
||||
if ($studentNumber) {
|
||||
$nisn = 'NIS_' . preg_replace('/[^A-Za-z0-9\-]/', '_', $studentNumber);
|
||||
$results['errors'][] = 'Using student_number as ID: ' . ($item['full_name'] ?? 'unknown') . ' (' . $studentNumber . ')';
|
||||
} else {
|
||||
$results['errors'][] = 'Skipped (no NISN/NIS): ' . ($item['full_name'] ?? 'unknown');
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Sync class
|
||||
$classData = $item['classroom'] ?? [];
|
||||
$className = $item['class_name'] ?? '-';
|
||||
|
||||
$class = Classes::updateOrCreate(
|
||||
['source_id' => (string) ($classData['id'] ?? $className)],
|
||||
[
|
||||
'name' => $className,
|
||||
'level' => $classData['level'] ?? $item['class_level'] ?? 'X',
|
||||
'department_code' => $item['department_code'] ?? '',
|
||||
'department_name' => $item['department_name'] ?? '',
|
||||
'academic_year_name' => $item['academic_year_name'] ?? '',
|
||||
'is_active' => true,
|
||||
]
|
||||
);
|
||||
|
||||
if ($class->wasRecentlyCreated) $results['classes_created']++;
|
||||
elseif ($class->wasChanged()) $results['classes_updated']++;
|
||||
|
||||
// Sync student
|
||||
$student = Student::updateOrCreate(
|
||||
['nisn' => $nisn],
|
||||
[
|
||||
'source_id' => (string) ($item['id'] ?? ''),
|
||||
'student_number' => $item['student_number'],
|
||||
'full_name' => $item['full_name'],
|
||||
'gender' => $item['gender'],
|
||||
'place_of_birth' => $item['place_of_birth'],
|
||||
'date_of_birth' => $item['date_of_birth'] ?: null,
|
||||
'address' => $item['address'],
|
||||
'phone_number' => $item['phone_number'],
|
||||
'email' => $item['email'],
|
||||
'class_name' => $className,
|
||||
'class_level' => $classData['level'] ?? $item['class_level'] ?? 'X',
|
||||
'department_code' => $item['department_code'] ?? '',
|
||||
'department_name' => $item['department_name'] ?? '',
|
||||
'academic_year_name' => $item['academic_year_name'] ?? '',
|
||||
'status' => 'active',
|
||||
'photo_path' => $item['photo_path'],
|
||||
'class_id' => $class->id,
|
||||
'is_active' => true,
|
||||
'synced_at' => now(),
|
||||
'metadata' => [
|
||||
'source_updated_at' => $item['updated_at'],
|
||||
'synced_from' => 'database-kesiswaan',
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
if ($student->wasRecentlyCreated) $results['students_created']++;
|
||||
else $results['students_updated']++;
|
||||
}
|
||||
});
|
||||
|
||||
return $results;
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Student sync failed: ' . $e->getMessage());
|
||||
return ['error' => 'Sinkron gagal: ' . $e->getMessage()];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Events\ViolationRecorded;
|
||||
use App\Models\SpLetter;
|
||||
use App\Models\SpThreshold;
|
||||
use App\Models\Student;
|
||||
use App\Models\User;
|
||||
use App\Models\Violation;
|
||||
use App\Models\AppNotification;
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class ViolationService
|
||||
{
|
||||
public function recordViolation(array $data, int $userId): Violation
|
||||
{
|
||||
$violation = DB::transaction(function () use ($data, $userId) {
|
||||
$violation = Violation::create([
|
||||
'student_id' => $data['student_id'],
|
||||
'violation_type_id' => $data['violation_type_id'],
|
||||
'recorded_by' => $userId,
|
||||
'description' => $data['description'] ?? null,
|
||||
'points' => $data['points'],
|
||||
'sanction' => $data['sanction'] ?? null,
|
||||
'location' => $data['location'] ?? null,
|
||||
'violation_date' => $data['violation_date'],
|
||||
'violation_time' => $data['violation_time'] ?? null,
|
||||
'notes' => $data['notes'] ?? null,
|
||||
]);
|
||||
|
||||
// Save evidence photos
|
||||
if (isset($data['evidences']) && is_array($data['evidences'])) {
|
||||
foreach ($data['evidences'] as $file) {
|
||||
if (!$file || !$file->isValid()) continue;
|
||||
$path = $file->store('violations/' . $violation->id, 'public');
|
||||
$violation->evidences()->create([
|
||||
'file_path' => $path,
|
||||
'original_name' => $file->getClientOriginalName(),
|
||||
'file_size' => $file->getSize(),
|
||||
'mime_type' => $file->getMimeType(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
return $violation;
|
||||
});
|
||||
|
||||
// Check SP thresholds
|
||||
$student = $violation->student;
|
||||
$totalPoints = $student->total_points;
|
||||
|
||||
$thresholdReached = SpThreshold::where('min_points', '<=', $totalPoints)
|
||||
->where('is_active', true)
|
||||
->orderByDesc('min_points')
|
||||
->first();
|
||||
|
||||
if ($thresholdReached) {
|
||||
$existingSp = SpLetter::where('student_id', $student->id)
|
||||
->where('sp_threshold_id', $thresholdReached->id)
|
||||
->exists();
|
||||
|
||||
if (!$existingSp) {
|
||||
$this->generateSpLetter($student, $thresholdReached, $userId);
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast realtime event
|
||||
$this->notifyRealtime($violation);
|
||||
|
||||
return $violation;
|
||||
}
|
||||
|
||||
protected function generateSpLetter(Student $student, SpThreshold $threshold, int $userId): SpLetter
|
||||
{
|
||||
$schoolName = Setting::getValue('school_name', 'SMK');
|
||||
$letterCount = SpLetter::where('sp_threshold_id', $threshold->id)->count() + 1;
|
||||
|
||||
$letter = SpLetter::create([
|
||||
'student_id' => $student->id,
|
||||
'sp_threshold_id' => $threshold->id,
|
||||
'generated_by' => $userId,
|
||||
'letter_number' => sprintf('%s/%s/%s/%s',
|
||||
$threshold->slug,
|
||||
str_pad((string) $letterCount, 3, '0', STR_PAD_LEFT),
|
||||
date('m'),
|
||||
date('Y')
|
||||
),
|
||||
'title' => $threshold->name . ' - ' . $student->full_name,
|
||||
'total_points_at_time' => $student->total_points,
|
||||
'violations_included' => $student->violations()
|
||||
->whereNull('deleted_at')
|
||||
->get(['id', 'violation_date', 'points', 'description'])
|
||||
->toArray(),
|
||||
'status' => 'draft',
|
||||
]);
|
||||
|
||||
return $letter;
|
||||
}
|
||||
|
||||
protected function notifyRealtime(Violation $violation): void
|
||||
{
|
||||
try {
|
||||
$student = $violation->student;
|
||||
|
||||
// Create notifications for BK users
|
||||
$bkUsers = User::whereIn('role', ['admin', 'bk'])->get();
|
||||
|
||||
foreach ($bkUsers as $user) {
|
||||
AppNotification::create([
|
||||
'user_id' => $user->id,
|
||||
'type' => 'violation_recorded',
|
||||
'title' => 'Pelanggaran Baru: ' . $student->full_name,
|
||||
'body' => $violation->violationType->name . ' (+' . $violation->points . ' poin)',
|
||||
'data' => [
|
||||
'violation_id' => $violation->id,
|
||||
'student_name' => $student->full_name,
|
||||
'points' => $violation->points,
|
||||
'total_points' => $student->total_points,
|
||||
],
|
||||
'icon' => 'exclamation-triangle',
|
||||
'color' => $violation->violationType->category->color ?? '#ef4444',
|
||||
'action_url' => route('violations.show', $violation->id),
|
||||
'violation_id' => $violation->id,
|
||||
]);
|
||||
}
|
||||
|
||||
// Broadcast via Reverb
|
||||
broadcast(new ViolationRecorded($violation))->toOthers();
|
||||
} catch (\Exception $e) {
|
||||
\Illuminate\Support\Facades\Log::error('Notify realtime failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user