feat: Face ID (scan, register, liveness) + role baru KS/Waka/Ketua Tim + notifikasi WA ortu + scoping wali kelas + error pages premium
This commit is contained in:
@@ -11,7 +11,7 @@ class Classes extends Model
|
||||
protected $fillable = [
|
||||
'source_id', 'name', 'level', 'department_code',
|
||||
'department_name', 'academic_year_name', 'is_active',
|
||||
'metadata',
|
||||
'metadata', 'homeroom_teacher_id',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
@@ -26,4 +26,9 @@ class Classes extends Model
|
||||
{
|
||||
return $this->hasMany(Student::class, 'class_id');
|
||||
}
|
||||
|
||||
public function homeroomTeacher()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'homeroom_teacher_id');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class FaceVerificationLog extends Model
|
||||
{
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'student_id',
|
||||
'violation_id',
|
||||
'faceid_matched',
|
||||
'faceid_ambiguous',
|
||||
'faceid_reason',
|
||||
'faceid_score',
|
||||
'photo_hash',
|
||||
'top_candidates',
|
||||
'created_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'faceid_matched' => 'boolean',
|
||||
'faceid_ambiguous' => 'boolean',
|
||||
'faceid_score' => 'float',
|
||||
'top_candidates' => 'array',
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function student(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Student::class);
|
||||
}
|
||||
|
||||
public function violation(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Violation::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class HandlingType extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $fillable = [
|
||||
'name', 'icon', 'color', 'sort_order', 'is_active', 'is_system',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'is_active' => 'boolean',
|
||||
'is_system' => 'boolean',
|
||||
'sort_order' => 'integer',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Daftar peran penanganan (dari settings, dengan default).
|
||||
*/
|
||||
public static function roles(): array
|
||||
{
|
||||
$defaults = ['Wakil Ketua Tim', 'Anggota Tim', 'Wali Kelas', 'Guru BK', 'Saksi'];
|
||||
$raw = \App\Models\Setting::getValue('handling_roles');
|
||||
if (!$raw) {
|
||||
return $defaults;
|
||||
}
|
||||
$roles = json_decode($raw, true);
|
||||
return is_array($roles) && count($roles) ? $roles : $defaults;
|
||||
}
|
||||
|
||||
public static function saveRoles(array $roles): void
|
||||
{
|
||||
$roles = array_values(array_filter(array_map('trim', $roles)));
|
||||
\App\Models\Setting::setValue('handling_roles', json_encode($roles), 'general', 'Daftar peran penanganan');
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ class Student extends Model
|
||||
protected $fillable = [
|
||||
'source_id', 'nisn', 'student_number', 'full_name',
|
||||
'gender', 'place_of_birth', 'date_of_birth', 'address',
|
||||
'phone_number', 'email', 'class_name', 'class_level',
|
||||
'phone_number', 'parent_phone', 'email', 'class_name', 'class_level',
|
||||
'department_code', 'department_name', 'academic_year_name',
|
||||
'status', 'photo_path', 'class_id', 'is_active',
|
||||
'metadata', 'synced_at',
|
||||
|
||||
+99
-15
@@ -10,8 +10,10 @@ class User extends Authenticatable
|
||||
{
|
||||
use HasFactory, Notifiable;
|
||||
|
||||
public const ROLES = ['admin', 'bk', 'wali_kelas', 'staff', 'other', 'kepala_sekolah', 'waka_kesiswaan', 'ketua_tim'];
|
||||
|
||||
protected $fillable = [
|
||||
'name', 'username', 'email', 'password', 'role', 'is_active',
|
||||
'name', 'username', 'email', 'password', 'role', 'roles', 'is_active',
|
||||
];
|
||||
|
||||
protected $hidden = ['password', 'remember_token'];
|
||||
@@ -22,29 +24,111 @@ class User extends Authenticatable
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'is_active' => 'boolean',
|
||||
'roles' => 'array',
|
||||
];
|
||||
}
|
||||
|
||||
public function isAdmin(): bool { return $this->role === 'admin'; }
|
||||
public function isBK(): bool { return $this->role === 'bk'; }
|
||||
public function isWaliKelas(): bool { return $this->role === 'wali_kelas'; }
|
||||
public function isStaff(): bool { return $this->role === 'staff'; }
|
||||
public function isOther(): bool { return $this->role === 'other'; }
|
||||
/**
|
||||
* Daftar role user (multi-role). Kolom `role` lama tetap sebagai fallback.
|
||||
*/
|
||||
public function roleList(): array
|
||||
{
|
||||
$roles = $this->roles ?? [];
|
||||
if (!is_array($roles) || empty($roles)) {
|
||||
return [$this->role];
|
||||
}
|
||||
return array_values(array_unique(array_filter($roles)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Role utama (pertama) — untuk kompatibilitas kolom `role`.
|
||||
*/
|
||||
public function primaryRole(): string
|
||||
{
|
||||
return $this->roleList()[0] ?? $this->role;
|
||||
}
|
||||
|
||||
public function hasRole(string $role): bool
|
||||
{
|
||||
return in_array($role, $this->roleList(), true);
|
||||
}
|
||||
|
||||
public function hasAnyRole(array $roles): bool
|
||||
{
|
||||
return count(array_intersect($roles, $this->roleList())) > 0;
|
||||
}
|
||||
|
||||
public function isAdmin(): bool { return $this->hasRole('admin'); }
|
||||
public function isBK(): bool { return $this->hasRole('bk'); }
|
||||
public function isWaliKelas(): bool { return $this->hasRole('wali_kelas'); }
|
||||
public function isStaff(): bool { return $this->hasRole('staff'); }
|
||||
public function isOther(): bool { return $this->hasRole('other'); }
|
||||
public function isKepalaSekolah(): bool { return $this->hasRole('kepala_sekolah'); }
|
||||
public function isWakaKesiswaan(): bool { return $this->hasRole('waka_kesiswaan'); }
|
||||
public function isKetuaTim(): bool { return $this->hasRole('ketua_tim'); }
|
||||
|
||||
/**
|
||||
* Role dengan visibilitas data GLOBAL (semua kelas/siswa).
|
||||
* Guru ber-role ganda (mis. staff + wali_kelas) masuk kategori ini.
|
||||
*/
|
||||
public function isGlobalRole(): bool
|
||||
{
|
||||
return $this->hasAnyRole(['admin', 'bk', 'staff', 'waka_kesiswaan', 'kepala_sekolah', 'ketua_tim']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wali kelas MURNI (tanpa role global) → datanya dibatasi ke kelas yang diwalikan.
|
||||
*/
|
||||
public function isScopedWaliKelas(): bool
|
||||
{
|
||||
return $this->hasRole('wali_kelas') && ! $this->isGlobalRole();
|
||||
}
|
||||
|
||||
/** ID kelas yang diwalikan ke user ini. */
|
||||
public function homeroomClassIds(): array
|
||||
{
|
||||
return \App\Models\Classes::where('homeroom_teacher_id', $this->id)->pluck('id')->all();
|
||||
}
|
||||
|
||||
/** Boleh tidak user ini mengakses data siswa tertentu? */
|
||||
public function canViewStudent(\App\Models\Student|int $student): bool
|
||||
{
|
||||
if (! $this->isScopedWaliKelas()) {
|
||||
return true;
|
||||
}
|
||||
$classId = is_int($student)
|
||||
? \App\Models\Student::find($student)?->class_id
|
||||
: $student->class_id;
|
||||
|
||||
return in_array($classId, $this->homeroomClassIds(), true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission dari SEMUA role user (digabung).
|
||||
*/
|
||||
public function allPermissions(): array
|
||||
{
|
||||
$perms = [];
|
||||
foreach ($this->roleList() as $role) {
|
||||
$perms = array_merge($perms, \Illuminate\Support\Facades\Cache::remember(
|
||||
'role_permissions:' . $role,
|
||||
3600,
|
||||
fn () => \DB::table('role_permissions')
|
||||
->join('permissions', 'role_permissions.permission_id', '=', 'permissions.id')
|
||||
->where('role_permissions.role', $role)
|
||||
->pluck('permissions.key')
|
||||
->toArray()
|
||||
));
|
||||
}
|
||||
return array_values(array_unique($perms));
|
||||
}
|
||||
|
||||
public function canPermission(string $permission): bool
|
||||
{
|
||||
if ($this->isAdmin()) {
|
||||
return true;
|
||||
}
|
||||
return in_array($permission, \Illuminate\Support\Facades\Cache::remember(
|
||||
'role_permissions:' . $this->role,
|
||||
3600,
|
||||
fn () => \DB::table('role_permissions')
|
||||
->join('permissions', 'role_permissions.permission_id', '=', 'permissions.id')
|
||||
->where('role_permissions.role', $this->role)
|
||||
->pluck('permissions.key')
|
||||
->toArray()
|
||||
));
|
||||
return in_array($permission, $this->allPermissions(), true);
|
||||
}
|
||||
|
||||
public function recordedViolations()
|
||||
|
||||
@@ -56,6 +56,63 @@ class Violation extends Model
|
||||
return $this->hasMany(ViolationEvidence::class);
|
||||
}
|
||||
|
||||
public function notifications(): HasMany
|
||||
{
|
||||
return $this->hasMany(ViolationNotification::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bangun notifikasi WA untuk orang tua/wali.
|
||||
*
|
||||
* @return array{0: string, 1: string}|null [nomor_wa, pesan] — null bila HP ortu belum diisi
|
||||
*/
|
||||
public function buildWaNotification(): ?array
|
||||
{
|
||||
$student = $this->student;
|
||||
if (! $student || ! $student->parent_phone) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Normalisasi nomor Indonesia: 08xx / 8xx → 628xx
|
||||
$phone = preg_replace('/\D/', '', $student->parent_phone);
|
||||
if (str_starts_with($phone, '0')) {
|
||||
$phone = '62'.substr($phone, 1);
|
||||
} elseif (str_starts_with($phone, '8')) {
|
||||
$phone = '62'.$phone;
|
||||
}
|
||||
if (strlen($phone) < 10) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$template = trim((string) \App\Models\Setting::getValue('wa_notification_template', ''));
|
||||
if ($template === '') {
|
||||
$template = "Yth. Bapak/Ibu Wali dari {nama_siswa} ({kelas}),\n\n"
|
||||
."Dengan hormat, kami informasikan bahwa ananda tercatat melakukan pelanggaran:\n"
|
||||
."• Jenis: {jenis}\n"
|
||||
."• Tanggal: {tanggal}\n"
|
||||
."• Poin: {poin}\n\n"
|
||||
."Total poin ananda saat ini: {total_poin}.\n\n"
|
||||
."Demikian disampaikan, atas perhatian dan kerja samanya kami ucapkan terima kasih.\n\n"
|
||||
."Tim Ketertiban\n{sekolah}";
|
||||
}
|
||||
|
||||
$placeholders = [
|
||||
'{nama_siswa}' => $student->full_name,
|
||||
'{nisn}' => $student->nisn,
|
||||
'{kelas}' => $student->class_name,
|
||||
'{jenis}' => $this->violationType?->name ?? '-',
|
||||
'{tanggal}' => $this->violation_date ? $this->violation_date->format('d/m/Y') : '-',
|
||||
'{waktu}' => $this->violation_time ? $this->violation_time->format('H:i') : '-',
|
||||
'{poin}' => (string) $this->points,
|
||||
'{total_poin}' => (string) ($student->total_points ?? 0),
|
||||
'{lokasi}' => $this->location ?: '-',
|
||||
'{deskripsi}' => $this->description ?: '-',
|
||||
'{sekolah}' => (string) \App\Models\Setting::getValue('school_name', 'SMKN 1 Wonorejo'),
|
||||
];
|
||||
|
||||
return [$phone, strtr($template, $placeholders)];
|
||||
}
|
||||
|
||||
public function handlings(): HasMany
|
||||
{
|
||||
return $this->hasMany(ViolationHandling::class);
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class ViolationNotification extends Model
|
||||
{
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'violation_id',
|
||||
'student_id',
|
||||
'channel',
|
||||
'recipient',
|
||||
'message',
|
||||
'status',
|
||||
'user_id',
|
||||
'created_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function violation(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Violation::class);
|
||||
}
|
||||
|
||||
public function student(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Student::class);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user