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:
Novy San87
2026-08-02 20:57:11 +07:00
parent 0a5f471854
commit 6e256661dd
76 changed files with 5335 additions and 888 deletions
@@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('handling_types', function (Blueprint $table) {
$table->id();
$table->string('name')->unique();
$table->string('icon')->default('fa-clipboard-list');
$table->string('color')->default('#64748b');
$table->integer('sort_order')->default(0);
$table->boolean('is_active')->default(true);
$table->boolean('is_system')->default(false);
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('handling_types');
}
};
@@ -0,0 +1,26 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('classes', function (Blueprint $table) {
$table->foreignId('homeroom_teacher_id')
->nullable()
->after('academic_year_name')
->constrained('users')
->nullOnDelete();
});
}
public function down(): void
{
Schema::table('classes', function (Blueprint $table) {
$table->dropConstrainedForeignId('homeroom_teacher_id');
});
}
};
@@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->json('roles')->nullable()->after('role');
});
}
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('roles');
});
}
};
@@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('face_verification_logs', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->foreignId('student_id')->nullable()->constrained('students')->nullOnDelete();
$table->foreignId('violation_id')->nullable()->constrained('violations')->nullOnDelete();
$table->boolean('faceid_matched')->default(false);
$table->boolean('faceid_ambiguous')->default(false);
$table->string('faceid_reason')->nullable(); // cocok / tidak_cocok / kembar_terdeteksi / skor_berdekatan
$table->decimal('faceid_score', 5, 4)->nullable();
$table->string('photo_hash', 64)->nullable(); // sha256 foto, tanpa menyimpan foto asli
$table->json('top_candidates')->nullable();
$table->timestamp('created_at')->useCurrent();
$table->index(['user_id', 'created_at']);
$table->index(['student_id', 'created_at']);
});
}
public function down(): void
{
Schema::dropIfExists('face_verification_logs');
}
};
@@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('students', function (Blueprint $table) {
$table->string('parent_phone', 30)->nullable()->after('phone_number');
});
}
public function down(): void
{
Schema::table('students', function (Blueprint $table) {
$table->dropColumn('parent_phone');
});
}
};
@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('violation_notifications', function (Blueprint $table) {
$table->id();
$table->foreignId('violation_id')->constrained('violations')->cascadeOnDelete();
$table->foreignId('student_id')->nullable()->constrained('students')->nullOnDelete();
$table->string('channel', 20)->default('whatsapp');
$table->string('recipient', 30)->nullable();
$table->text('message')->nullable();
$table->string('status', 20)->default('sent');
$table->foreignId('user_id')->nullable()->constrained('users')->nullOnDelete();
$table->timestamp('created_at')->useCurrent();
$table->index(['violation_id', 'created_at']);
});
}
public function down(): void
{
Schema::dropIfExists('violation_notifications');
}
};
@@ -0,0 +1,87 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
$now = now();
// ===== 1) Permission baru untuk fitur Face ID & Notifikasi WA =====
$newPermissions = [
// [key, group, label, description]
['face-scan', 'wajah', 'Scan Wajah', 'Scan wajah siswa untuk verifikasi identitas'],
['face-register', 'wajah', 'Registrasi Wajah', 'Registrasi/perbarui foto wajah siswa'],
['notify-parent-wa', 'pelanggaran', 'Notifikasi WA Orang Tua', 'Kirim notifikasi WhatsApp ke orang tua/wali'],
];
$permIds = [];
foreach ($newPermissions as [$key, $group, $label, $desc]) {
$id = DB::table('permissions')->insertGetId([
'key' => $key,
'group' => $group,
'label' => $label,
'description' => $desc,
'created_at' => $now,
'updated_at' => $now,
]);
$permIds[$key] = $id;
}
// ===== 2) Default grant: role yang punya akses terkait ikut dapat =====
// face-scan & face-register → role yang punya input-violations
$inputRoles = DB::table('role_permissions')
->join('permissions', 'permissions.id', '=', 'role_permissions.permission_id')
->where('permissions.key', 'input-violations')
->pluck('role_permissions.role')
->unique();
foreach (['face-scan', 'face-register'] as $key) {
foreach ($inputRoles as $role) {
DB::table('role_permissions')->insertOrIgnore([
'role' => $role,
'permission_id' => $permIds[$key],
'created_at' => $now,
'updated_at' => $now,
]);
}
}
// notify-parent-wa → role yang punya view-violations
$viewRoles = DB::table('role_permissions')
->join('permissions', 'permissions.id', '=', 'role_permissions.permission_id')
->where('permissions.key', 'view-violations')
->pluck('role_permissions.role')
->unique();
foreach ($viewRoles as $role) {
DB::table('role_permissions')->insertOrIgnore([
'role' => $role,
'permission_id' => $permIds['notify-parent-wa'],
'created_at' => $now,
'updated_at' => $now,
]);
}
// Admin selalu full — pastikan 3 permission baru masuk admin
foreach ($permIds as $pid) {
DB::table('role_permissions')->insertOrIgnore([
'role' => 'admin',
'permission_id' => $pid,
'created_at' => $now,
'updated_at' => $now,
]);
}
}
public function down(): void
{
DB::table('permissions')
->whereIn('key', ['face-scan', 'face-register', 'notify-parent-wa'])
->delete();
}
};
@@ -0,0 +1,61 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
return new class extends Migration
{
public function up(): void
{
$now = now();
$pid = fn (string $key) => DB::table('permissions')->where('key', $key)->value('id');
$grant = function (string $role, array $keys) use ($now, $pid) {
foreach ($keys as $key) {
$id = $pid($key);
if ($id) {
DB::table('role_permissions')->insertOrIgnore([
'role' => $role,
'permission_id' => $id,
'created_at' => $now,
'updated_at' => $now,
]);
}
}
};
// ===== Kepala Sekolah: view + export + scan (bukan input harian) =====
$grant('kepala_sekolah', [
'access-dashboard',
'view-violations',
'view-students',
'view-sp-letters',
'view-notifications',
'violations-export',
'face-scan',
]);
// ===== Waka Kesiswaan: setara BK =====
$bkPerms = DB::table('role_permissions')
->join('permissions', 'permissions.id', '=', 'role_permissions.permission_id')
->where('role_permissions.role', 'bk')
->pluck('permissions.key')
->toArray();
$grant('waka_kesiswaan', $bkPerms);
// ===== Ketua Tim: setara Staff =====
$staffPerms = DB::table('role_permissions')
->join('permissions', 'permissions.id', '=', 'role_permissions.permission_id')
->where('role_permissions.role', 'staff')
->pluck('permissions.key')
->toArray();
$grant('ketua_tim', $staffPerms);
}
public function down(): void
{
DB::table('role_permissions')
->whereIn('role', ['kepala_sekolah', 'waka_kesiswaan', 'ketua_tim'])
->delete();
}
};