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:
@@ -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();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Models\HandlingType;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class HandlingTypeDefaultsSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$defaults = [
|
||||
['name' => 'Teguran Lisan', 'icon' => 'fa-comment', 'color' => '#3b82f6', 'sort_order' => 1],
|
||||
['name' => 'Teguran Tertulis', 'icon' => 'fa-file-pen', 'color' => '#4f46e5', 'sort_order' => 2],
|
||||
['name' => 'Pembinaan BK', 'icon' => 'fa-hand-holding-heart', 'color' => '#14b8a6', 'sort_order' => 3],
|
||||
['name' => 'Panggilan Orang Tua', 'icon' => 'fa-phone-volume', 'color' => '#f97316', 'sort_order' => 4],
|
||||
['name' => 'Home Visit', 'icon' => 'fa-house-chimney', 'color' => '#8b5cf6', 'sort_order' => 5],
|
||||
['name' => 'Skorsing', 'icon' => 'fa-ban', 'color' => '#ef4444', 'sort_order' => 6],
|
||||
['name' => 'Tugas Sosial', 'icon' => 'fa-handshake-angle', 'color' => '#10b981', 'sort_order' => 7],
|
||||
['name' => 'SP-1', 'icon' => 'fa-file-signature', 'color' => '#f59e0b', 'sort_order' => 8],
|
||||
['name' => 'SP-2', 'icon' => 'fa-file-signature', 'color' => '#ea580c', 'sort_order' => 9],
|
||||
['name' => 'SP-3', 'icon' => 'fa-file-circle-exclamation','color' => '#dc2626', 'sort_order' => 10],
|
||||
['name' => 'Lainnya', 'icon' => 'fa-clipboard-list', 'color' => '#64748b', 'sort_order' => 99],
|
||||
];
|
||||
|
||||
foreach ($defaults as $data) {
|
||||
HandlingType::updateOrCreate(
|
||||
['name' => $data['name']],
|
||||
$data + ['is_system' => true, 'is_active' => true]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user