1555 lines
82 KiB
PHP
1555 lines
82 KiB
PHP
@extends('pages.index')
|
|
@section('admin_content')
|
|
|
|
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.5/css/jquery.dataTables.min.css">
|
|
<link rel="stylesheet" href="https://cdn.datatables.net/1.13.5/css/dataTables.bootstrap5.min.css">
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
|
<style>
|
|
.table-custom-header th {
|
|
background: linear-gradient(90deg, #365CF5 0%, #6a8ffd 100%) !important;
|
|
color: white !important; font-weight: 600; letter-spacing: 0.5px;
|
|
padding: 14px 12px !important; border: none !important; white-space: nowrap;
|
|
}
|
|
.table tbody tr { transition: background 0.2s; }
|
|
.table tbody tr:nth-child(even) { background: #f4f7ff !important; }
|
|
.table tbody tr:hover { background: #e3eafd !important; }
|
|
.table th, .table td { border: none !important; vertical-align: middle !important; }
|
|
.table { border-collapse: separate !important; border-spacing: 0 !important; font-size: 16px; }
|
|
.summary-card { border-radius: 16px; padding: 18px 24px; color: white; display: flex; align-items: center; gap: 16px; box-shadow: 0 4px 16px rgba(0,0,0,0.10); transition: transform 0.2s; }
|
|
.summary-card:hover { transform: translateY(-3px); }
|
|
.summary-card .icon-wrap { width: 52px; height: 52px; background: rgba(255,255,255,0.2); border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 1.7rem; }
|
|
.summary-card .label { font-size: 0.85rem; opacity: 0.85; }
|
|
.summary-card .count { font-size: 2rem; font-weight: 800; line-height: 1; }
|
|
.btn-tambah-siswa {
|
|
font-weight: bold;
|
|
background-color: #365CF5;
|
|
color: white;
|
|
padding: 8px 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
transition: all 0.3s ease;
|
|
border-radius: 8px;
|
|
}
|
|
.btn-tambah-siswa:hover {
|
|
color: white;
|
|
background-color: #2a4fd1;
|
|
}
|
|
.btn-hapus-siswa {
|
|
font-weight: bold;
|
|
background-color: transparent;
|
|
color: #dc3545;
|
|
border: 2px solid #dc3545;
|
|
padding: 8px 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
transition: all 0.3s ease;
|
|
border-radius: 8px;
|
|
}
|
|
.btn-hapus-siswa:hover {
|
|
background-color: #dc3545;
|
|
color: white;
|
|
}
|
|
.btn-edit-siswa {
|
|
font-weight: bold;
|
|
background-color: transparent;
|
|
color: #ffc107;
|
|
border: 2px solid #ffc107;
|
|
padding: 8px 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
transition: all 0.3s ease;
|
|
border-radius: 8px;
|
|
}
|
|
.btn-edit-siswa:hover {
|
|
background-color: #ffc107;
|
|
color: #212529;
|
|
}
|
|
.btn-dataset-siswa {
|
|
font-weight: bold;
|
|
background-color: transparent;
|
|
color: #06b6d4;
|
|
border: 2px solid #06b6d4;
|
|
padding: 8px 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
transition: all 0.3s ease;
|
|
border-radius: 8px;
|
|
}
|
|
.btn-dataset-siswa:hover {
|
|
background-color: #06b6d4;
|
|
color: #fff;
|
|
}
|
|
.btn-camera-siswa {
|
|
font-weight: bold;
|
|
background-color: transparent;
|
|
color: #17a2b8;
|
|
border: 2px solid #17a2b8;
|
|
padding: 8px 16px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
transition: all 0.3s ease;
|
|
border-radius: 8px;
|
|
}
|
|
.btn-camera-siswa:hover {
|
|
background-color: #17a2b8;
|
|
color: #fff;
|
|
}
|
|
@media (max-width: 767.98px) {
|
|
.btn-tambah-siswa,
|
|
.btn-edit-siswa,
|
|
.btn-hapus-siswa {
|
|
font-size: 15px !important;
|
|
padding: 10px 18px !important;
|
|
width: 100% !important;
|
|
}
|
|
}
|
|
</style>
|
|
|
|
<div class="container-fluid">
|
|
<!-- ========== title-wrapper start ========== -->
|
|
<div class="title-wrapper pt-30">
|
|
<div class="row align-items-start">
|
|
<div class="col-md-6">
|
|
<h2 style="font-weight:500;">
|
|
<i class="fas fa-user-graduate me-2 text-primary"></i>Identitas Siswa
|
|
</h2>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<nav aria-label="breadcrumb">
|
|
<ol class="breadcrumb justify-content-md-end" style="font-size: 0.85rem;">
|
|
<li class="breadcrumb-item"><a href="{{ route('dashboard.index') }}">Dashboard</a></li>
|
|
<li class="breadcrumb-item active" aria-current="page">Identitas Siswa</li>
|
|
</ol>
|
|
</nav>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- ========== title-wrapper end ========== -->
|
|
|
|
@php
|
|
$totalSiswa = $students->count();
|
|
$totalKelas = $classes->count();
|
|
@endphp
|
|
|
|
<!-- Summary Cards -->
|
|
<div class="row g-3 mb-4 mt-3">
|
|
<div class="col-6 col-md-3">
|
|
<div class="summary-card" style="background:linear-gradient(135deg,#365CF5,#6a8ffd);">
|
|
<div class="icon-wrap"><i class="fas fa-user-graduate"></i></div>
|
|
<div>
|
|
<div class="label">Total Siswa</div>
|
|
<div class="count">{{ $totalSiswa }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-6 col-md-3">
|
|
<div class="summary-card" style="background:linear-gradient(135deg,#22c55e,#4ade80);">
|
|
<div class="icon-wrap"><i class="fas fa-school"></i></div>
|
|
<div>
|
|
<div class="label">Total Kelas</div>
|
|
<div class="count">{{ $totalKelas }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Card Wrapper -->
|
|
<div class="card border-0 shadow-sm rounded-4">
|
|
<div class="card-header bg-white border-0 rounded-top-4 d-flex flex-column flex-md-row justify-content-between align-items-md-center align-items-start py-3 px-4">
|
|
<h5 class="mb-2 mb-md-0 fw-bold text-primary">
|
|
<i class="fas fa-list me-2"></i>Daftar Identitas Siswa
|
|
</h5>
|
|
<div class="d-flex gap-2 flex-column flex-md-row w-100 w-md-auto justify-content-md-end mt-2 mt-md-0">
|
|
<button class="btn btn-tambah-siswa btn-sm" style="font-size:14px;padding:7px 14px;" data-bs-toggle="modal" data-bs-target="#modalTambahSiswa">
|
|
<i class="fas fa-plus"></i> Tambah Siswa
|
|
</button>
|
|
<button class="btn btn-camera-siswa btn-sm" style="font-size:14px;padding:7px 14px;" id="btn-camera-siswa" type="button">
|
|
<i class="fas fa-camera me-1"></i> Kamera
|
|
</button>
|
|
<button class="btn btn-dataset-siswa btn-sm" style="font-size:14px;padding:7px 14px;" id="btn-dataset-siswa" type="button">
|
|
<i class="fas fa-database me-1"></i> Dataset
|
|
</button>
|
|
<button class="btn btn-edit-siswa btn-sm" style="font-size:14px;padding:7px 14px;" id="btn-edit-siswa" type="button">
|
|
<i class="fas fa-edit"></i> Edit
|
|
</button>
|
|
<button class="btn btn-hapus-siswa btn-sm" style="font-size:14px;padding:7px 14px;" id="btn-hapus-siswa" type="button">
|
|
<i class="fas fa-trash"></i> Hapus
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="card-body px-4 py-3">
|
|
<div class="table-responsive rounded-3">
|
|
<table id="example" class="table table-hover align-middle w-100">
|
|
<thead class="table-custom-header">
|
|
<tr>
|
|
<th class="text-center" style="width:40px;border-radius:12px 0 0 0;">
|
|
<input type="checkbox" id="select-all" />
|
|
</th>
|
|
<th class="text-center" style="width:60px;">Foto Profil</th>
|
|
<th class="text-center" style="width:60px;">Foto Webcam</th>
|
|
<th>Nama</th>
|
|
<th>Email</th>
|
|
<th>NISN</th>
|
|
<th>Kelas</th>
|
|
<th>Tahun Masuk</th>
|
|
<th>Password</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@foreach($students as $student)
|
|
<tr data-id="{{ $student->id }}">
|
|
<td class="text-center">
|
|
<input type="checkbox" class="row-checkbox" />
|
|
</td>
|
|
<td class="text-center">
|
|
@if($student->user->profile_picture)
|
|
<img src="{{ asset('storage/profile_pictures/student/' . $student->user->profile_picture) }}" alt="Foto Profil" width="36" height="36" class="rounded-circle border border-2 border-primary shadow-sm">
|
|
@else
|
|
<img src="https://ui-avatars.com/api/?name={{ urlencode($student->name) }}" alt="Foto Profil" width="36" height="36" class="rounded-circle border border-2 border-primary shadow-sm">
|
|
@endif
|
|
</td>
|
|
<td class="text-center">
|
|
@if($student->pictures)
|
|
<div style="display: flex; gap: 5px; justify-content: center; flex-wrap: wrap;">
|
|
@php
|
|
$photos = explode(',', $student->pictures);
|
|
@endphp
|
|
@foreach($photos as $photo)
|
|
@if(!empty($photo))
|
|
<img src="{{ asset('storage/photo-webcam/' . $photo) }}" alt="Foto Webcam" width="32" height="32" class="rounded border border-1 border-success shadow-sm" style="object-fit: cover;">
|
|
@endif
|
|
@endforeach
|
|
</div>
|
|
@else
|
|
<span class="text-muted"><i class="fas fa-camera"></i></span>
|
|
@endif
|
|
</td>
|
|
<td><span class="fw-semibold text-dark d-block">{{ $student->name }}</span></td>
|
|
<td><span class="text-muted">{{ $student->user->email }}</span></td>
|
|
<td>
|
|
<span class="badge rounded-pill px-3 py-2" style="background:#e3eafd;color:#365CF5;font-weight:600;">
|
|
{{ $student->nisn }}
|
|
</span>
|
|
</td>
|
|
<td data-class-id="{{ $student->class->id ?? '' }}">
|
|
<span class="badge rounded-pill px-3 py-2" style="background:#dcfce7;color:#16a34a;font-weight:600;">
|
|
{{ $student->class->name ?? '-' }}
|
|
</span>
|
|
</td>
|
|
<td>{{ $student->entry_year }}</td>
|
|
<td>starbaks</td>
|
|
</tr>
|
|
@endforeach
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- End Card Wrapper -->
|
|
|
|
<!-- Modal Tambah Siswa -->
|
|
<div class="modal fade" id="modalTambahSiswa" tabindex="-1" aria-labelledby="modalTambahSiswaLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content shadow-lg border-0 rounded-4">
|
|
<div class="modal-header bg-primary border-0 rounded-top-4">
|
|
<h5 class="modal-title fw-bold text-white" id="modalTambahSiswaLabel" style="color: white;">
|
|
Tambah Data Siswa
|
|
</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<form id="formTambahSiswa" method="POST" action="{{ route('register.student.post') }}" enctype="multipart/form-data">
|
|
@csrf
|
|
<div class="modal-body p-4 bg-light">
|
|
<!-- Baris 1: Nama Lengkap dan Email -->
|
|
<div class="row mb-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label fw-semibold text-dark">
|
|
Nama Lengkap
|
|
</label>
|
|
<input type="text" name="name" class="form-control border-2" placeholder="Masukkan nama lengkap" value="{{ old('name') }}" required>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label fw-semibold text-dark">
|
|
Email
|
|
</label>
|
|
<input type="email" name="email" class="form-control border-2" placeholder="contoh@email.com" value="{{ old('email') }}" required>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Baris 2: Password dan NISN -->
|
|
<div class="row mb-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label fw-semibold text-dark">
|
|
Password
|
|
</label>
|
|
<input type="text" name="password" class="form-control border-2" value="starbaks" readonly>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label fw-semibold text-dark">
|
|
NISN
|
|
</label>
|
|
<input type="number" name="nisn" class="form-control border-2" placeholder="Nomor Induk Siswa" value="{{ old('nisn') }}" required>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Baris 3: Kelas dan Tahun Masuk -->
|
|
<div class="row mb-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label fw-semibold text-dark">
|
|
Kelas
|
|
</label>
|
|
<select name="id_class" class="form-select border-2" required>
|
|
<option value="">-- Pilih Kelas --</option>
|
|
@foreach($classes as $kelas)
|
|
<option value="{{ $kelas->id }}" {{ (string)old('id_class') === (string)$kelas->id ? 'selected' : '' }}>{{ $kelas->name }}</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label fw-semibold text-dark">
|
|
Tahun Masuk
|
|
</label>
|
|
<input type="number" name="entry_year" class="form-control border-2" placeholder="2024" min="2015" max="2030" value="{{ old('entry_year') }}" required>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Baris 4: Foto Profil (Full Width) -->
|
|
<div class="row mb-3">
|
|
<div class="col-12">
|
|
<label class="form-label fw-semibold text-dark">
|
|
Foto Profil
|
|
<small class="text-muted">(opsional)</small>
|
|
</label>
|
|
<input type="file" name="profile_picture" class="form-control border-2" accept="image/*">
|
|
<div class="form-text text-muted">
|
|
Format: JPG, PNG, GIF. Maksimal 2MB.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Baris 4: Webcam Capture -->
|
|
<div class="row mb-3">
|
|
<div class="col-12">
|
|
<label class="form-label fw-semibold text-dark">
|
|
<i class="fas fa-camera me-2"></i>Ambil Foto via Webcam
|
|
</label>
|
|
<div class="card border-light bg-light p-3">
|
|
<div id="initial-container" class="text-center">
|
|
<button type="button" class="btn btn-outline-primary" id="btn-start-webcam">
|
|
<i class="fas fa-video"></i> Aktifkan Webcam
|
|
</button>
|
|
</div>
|
|
|
|
<div id="webcam-container" class="text-center" style="display: none;">
|
|
<video id="webcam-video" style="width: 100%; max-width: 400px; border-radius: 8px; border: 2px solid #365CF5; margin-bottom: 10px;"></video>
|
|
<div class="d-flex gap-2 justify-content-center mb-3">
|
|
<button type="button" class="btn btn-primary btn-sm" id="btn-capture">
|
|
<i class="fas fa-camera"></i> Ambil Foto
|
|
</button>
|
|
<button type="button" class="btn btn-secondary btn-sm" id="btn-stop-webcam">
|
|
<i class="fas fa-stop"></i> Tutup
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="preview-container" class="text-center" style="display: none;">
|
|
<canvas id="webcam-canvas" style="width: 100%; max-width: 400px; border-radius: 8px; border: 2px solid #365CF5; margin-bottom: 10px;"></canvas>
|
|
<div class="d-flex gap-2 justify-content-center">
|
|
<button type="button" class="btn btn-warning btn-sm" id="btn-retake">
|
|
<i class="fas fa-redo"></i> Ambil Ulang
|
|
</button>
|
|
<button type="button" class="btn btn-success btn-sm" id="btn-use-photo">
|
|
<i class="fas fa-check"></i> Gunakan
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="accepted-container" class="text-center" style="display: none;">
|
|
<div style="position: relative; display: inline-block; margin-bottom: 15px;">
|
|
<img id="accepted-photo-preview" src="" alt="Foto Diterima" style="width: 150px; height: 150px; border-radius: 8px; border: 2px solid #22c55e; object-fit: cover;">
|
|
<button type="button" class="btn btn-sm btn-danger" id="btn-delete-accepted-photo" style="position: absolute; top: -10px; right: -10px; border-radius: 50%; width: 30px; height: 30px; padding: 0; display: flex; align-items: center; justify-content: center;">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
<div class="alert alert-success mb-3" role="alert">
|
|
<i class="fas fa-check-circle me-2"></i> <span id="photos-progress">Foto 1 dari 3</span>
|
|
</div>
|
|
<button type="button" class="btn btn-primary btn-sm" id="btn-next-photo">
|
|
<i class="fas fa-arrow-right me-2"></i> Lanjut ke Foto Berikutnya
|
|
</button>
|
|
</div>
|
|
|
|
<div id="photos-summary-container" class="text-center" style="display: none;">
|
|
<div class="alert alert-success mb-3" role="alert">
|
|
<i class="fas fa-check-circle me-2"></i> Semua 3 foto berhasil diambil!
|
|
</div>
|
|
<div style="display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; margin-bottom: 15px;">
|
|
<div style="position: relative; display: inline-block;">
|
|
<img id="summary-photo-1" src="" alt="Foto 1" style="width: 100px; height: 100px; border-radius: 8px; border: 2px solid #22c55e; object-fit: cover;">
|
|
<button type="button" class="btn btn-sm btn-danger" data-photo="1" style="position: absolute; top: -10px; right: -10px; border-radius: 50%; width: 25px; height: 25px; padding: 0; display: flex; align-items: center; justify-content: center; font-size: 12px;" onclick="deletePhotoFromSummary(this)">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
<div style="position: relative; display: inline-block;">
|
|
<img id="summary-photo-2" src="" alt="Foto 2" style="width: 100px; height: 100px; border-radius: 8px; border: 2px solid #22c55e; object-fit: cover;">
|
|
<button type="button" class="btn btn-sm btn-danger" data-photo="2" style="position: absolute; top: -10px; right: -10px; border-radius: 50%; width: 25px; height: 25px; padding: 0; display: flex; align-items: center; justify-content: center; font-size: 12px;" onclick="deletePhotoFromSummary(this)">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
<div style="position: relative; display: inline-block;">
|
|
<img id="summary-photo-3" src="" alt="Foto 3" style="width: 100px; height: 100px; border-radius: 8px; border: 2px solid #22c55e; object-fit: cover;">
|
|
<button type="button" class="btn btn-sm btn-danger" data-photo="3" style="position: absolute; top: -10px; right: -10px; border-radius: 50%; width: 25px; height: 25px; padding: 0; display: flex; align-items: center; justify-content: center; font-size: 12px;" onclick="deletePhotoFromSummary(this)">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<input type="hidden" id="webcam-photo-data" name="pictures" value="">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer bg-white border-0 p-4 gap-2">
|
|
<button type="button" class="btn btn-danger px-4 fw-semibold shadow-sm" data-bs-dismiss="modal">
|
|
<i class="fas fa-times me-2"></i>Batal
|
|
</button>
|
|
<button type="submit" class="btn btn-primary px-4 fw-semibold shadow-sm" id="btn-submit-form">
|
|
<i class="fas fa-save me-2"></i>Simpan Data
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal Kamera Absensi -->
|
|
<div class="modal fade" id="modalKameraAbsensi" tabindex="-1" aria-labelledby="modalKameraAbsensiLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-dialog-centered" style="max-width:520px;">
|
|
<div class="modal-content shadow-lg border-0 rounded-4">
|
|
<div class="modal-header border-0 rounded-top-4" style="background:linear-gradient(135deg,#17a2b8,#0e7d91);">
|
|
<h5 class="modal-title fw-bold text-white" id="modalKameraAbsensiLabel">
|
|
<i class="fas fa-camera me-2"></i>Kamera Absensi
|
|
</h5>
|
|
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<div class="modal-body p-4 bg-light text-center">
|
|
<!-- Live Camera Feed -->
|
|
<div id="absensi-camera-container">
|
|
<div style="position:relative;display:inline-block;width:100%;">
|
|
<video id="absensi-video"
|
|
autoplay playsinline
|
|
style="width:100%;max-width:460px;border-radius:12px;border:3px solid #17a2b8;background:#000;"></video>
|
|
<!-- Overlay loading -->
|
|
<div id="absensi-loading" style="position:absolute;inset:0;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.55);border-radius:12px;">
|
|
<div class="spinner-border text-info" role="status"></div>
|
|
</div>
|
|
</div>
|
|
<canvas id="absensi-canvas" style="display:none;"></canvas>
|
|
|
|
<div class="mt-3 d-flex gap-2 justify-content-center">
|
|
<button type="button" class="btn btn-info fw-semibold px-4" id="btn-absensi-capture" disabled>
|
|
<i class="fas fa-camera me-2"></i>Capture & Absen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Result Area -->
|
|
<div id="absensi-result" class="mt-3" style="display:none;">
|
|
<div id="absensi-result-content"></div>
|
|
</div>
|
|
|
|
<!-- Sending Spinner -->
|
|
<div id="absensi-sending" class="mt-3" style="display:none;">
|
|
<div class="d-flex align-items-center justify-content-center gap-2 text-info">
|
|
<div class="spinner-border spinner-border-sm" role="status"></div>
|
|
<span class="fw-semibold">Memproses pengenalan wajah...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer bg-white border-0 px-4 pb-4 pt-0 gap-2 justify-content-center">
|
|
<button type="button" class="btn btn-secondary px-4 fw-semibold" data-bs-dismiss="modal">
|
|
<i class="fas fa-times me-2"></i>Tutup
|
|
</button>
|
|
<button type="button" class="btn btn-outline-info px-4 fw-semibold" id="btn-absensi-retake" style="display:none;">
|
|
<i class="fas fa-redo me-2"></i>Ambil Ulang
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- End Modal Kamera Absensi -->
|
|
|
|
<!-- Modal Edit Siswa -->
|
|
<div class="modal fade" id="modalEditSiswa" tabindex="-1" aria-labelledby="modalEditSiswaLabel" aria-hidden="true">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content shadow-lg border-0 rounded-4">
|
|
<div class="modal-header bg-warning border-0 rounded-top-4">
|
|
<h5 class="modal-title fw-bold text-dark" id="modalEditSiswaLabel">
|
|
Edit Data Siswa
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<form id="formEditSiswa" method="POST" enctype="multipart/form-data">
|
|
@csrf
|
|
@method('PUT')
|
|
<div class="modal-body p-4 bg-light">
|
|
<div class="row mb-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label fw-semibold text-dark">
|
|
Nama Lengkap
|
|
</label>
|
|
<input type="text" name="name" id="edit-name" class="form-control border-2" required>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label fw-semibold text-dark">
|
|
Email
|
|
</label>
|
|
<input type="email" name="email" id="edit-email" class="form-control border-2" required>
|
|
</div>
|
|
</div>
|
|
<div class="row mb-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label fw-semibold text-dark">
|
|
Password
|
|
</label>
|
|
<input type="text" name="password" id="edit-password" class="form-control border-2" value="starbaks" readonly>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label fw-semibold text-dark">
|
|
NISN
|
|
</label>
|
|
<input type="number" name="nisn" id="edit-nisn" class="form-control border-2" required>
|
|
</div>
|
|
</div>
|
|
<div class="row mb-3">
|
|
<div class="col-md-6">
|
|
<label class="form-label fw-semibold text-dark">
|
|
Kelas
|
|
</label>
|
|
<select name="id_class" id="edit-id_class" class="form-select border-2" required>
|
|
<option value="">-- Pilih Kelas --</option>
|
|
@foreach($classes as $kelas)
|
|
<option value="{{ $kelas->id }}">{{ $kelas->name }}</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label fw-semibold text-dark">
|
|
Tahun Masuk
|
|
</label>
|
|
<input type="number" name="entry_year" id="edit-entry_year" class="form-control border-2" min="2015" max="2030" required>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Foto Profil -->
|
|
<div class="row mb-3">
|
|
<div class="col-12">
|
|
<label class="form-label fw-semibold text-dark">
|
|
Foto Profil
|
|
<small class="text-muted">(opsional, kosongkan jika tidak ingin mengganti)</small>
|
|
</label>
|
|
<div class="d-flex align-items-center gap-3 mb-2">
|
|
<img id="edit-profile-preview" src="" alt="Foto Profil" width="64" height="64" class="rounded-circle border border-2 border-primary shadow-sm" style="object-fit: cover;">
|
|
<input type="file" name="profile_picture" id="edit-profile_picture" class="form-control border-2" accept="image/*">
|
|
</div>
|
|
<div class="form-text text-muted">
|
|
Format: JPG, PNG, GIF. Maksimal 2MB.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mb-3">
|
|
<div class="col-12">
|
|
<label class="form-label fw-semibold text-dark">
|
|
Foto Webcam yang Ada Saat Ini
|
|
<small class="text-muted">(akan diganti jika upload baru)</small>
|
|
</label>
|
|
<div id="existing-photos-container" class="card border-light bg-light p-3 mb-3">
|
|
<div style="display: flex; gap: 10px; justify-content: center; flex-wrap: wrap;">
|
|
<div style="text-align: center;">
|
|
<img id="existing-photo-1" src="" alt="Tidak ada" style="width: 100px; height: 100px; border-radius: 8px; border: 2px solid #dee2e6; object-fit: cover; display: none;">
|
|
<p class="text-muted small mt-2" id="no-photo-1">-</p>
|
|
</div>
|
|
<div style="text-align: center;">
|
|
<img id="existing-photo-2" src="" alt="Tidak ada" style="width: 100px; height: 100px; border-radius: 8px; border: 2px solid #dee2e6; object-fit: cover; display: none;">
|
|
<p class="text-muted small mt-2" id="no-photo-2">-</p>
|
|
</div>
|
|
<div style="text-align: center;">
|
|
<img id="existing-photo-3" src="" alt="Tidak ada" style="width: 100px; height: 100px; border-radius: 8px; border: 2px solid #dee2e6; object-fit: cover; display: none;">
|
|
<p class="text-muted small mt-2" id="no-photo-3">-</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mb-3">
|
|
<div class="col-12">
|
|
<label class="form-label fw-semibold text-dark">
|
|
Ambil Foto via Webcam
|
|
<small class="text-muted">(harus 3 foto)</small>
|
|
</label>
|
|
<div class="card border-light bg-light p-3">
|
|
<!-- Initial Container -->
|
|
<div id="initial-container-edit" class="text-center">
|
|
<button type="button" id="btn-start-webcam-edit" class="btn btn-primary btn-sm mb-2">
|
|
<i class="fas fa-camera me-2"></i>Mulai Webcam
|
|
</button>
|
|
<p class="text-muted small mb-0">Klik tombol untuk memulai capture foto dari webcam</p>
|
|
</div>
|
|
|
|
<!-- Webcam Container -->
|
|
<div id="webcam-container-edit" style="display: none;" class="text-center mb-3">
|
|
<video id="webcam-video-edit" style="width: 100%; max-width: 400px; border: 2px solid #dee2e6; border-radius: 8px;"></video>
|
|
<div class="mt-2">
|
|
<button type="button" id="btn-capture-edit" class="btn btn-success btn-sm me-2">
|
|
<i class="fas fa-camera me-1"></i>Ambil Foto
|
|
</button>
|
|
<button type="button" id="btn-stop-webcam-edit" class="btn btn-secondary btn-sm">
|
|
<i class="fas fa-stop me-1"></i>Tutup
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Preview Container -->
|
|
<div id="preview-container-edit" style="display: none;" class="text-center mb-3">
|
|
<canvas id="webcam-canvas-edit" style="max-width: 100%; border: 2px solid #dee2e6; border-radius: 8px; display: none;"></canvas>
|
|
<img id="preview-img-edit" src="" style="max-width: 100%; max-height: 400px; border: 2px solid #dee2e6; border-radius: 8px;">
|
|
<div class="mt-2">
|
|
<button type="button" id="btn-use-photo-edit" class="btn btn-success btn-sm me-2">
|
|
<i class="fas fa-check me-1"></i>Terima
|
|
</button>
|
|
<button type="button" id="btn-retake-edit" class="btn btn-warning btn-sm">
|
|
<i class="fas fa-redo me-1"></i>Ulangi
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Accepted Container -->
|
|
<div id="accepted-container-edit" style="display: none;">
|
|
<div style="position: relative; display: inline-block; margin-bottom: 15px;">
|
|
<img id="accepted-photo-preview-edit" src="" alt="Foto Diterima" style="width: 150px; height: 150px; border-radius: 8px; border: 2px solid #22c55e; object-fit: cover;">
|
|
<button type="button" class="btn btn-sm btn-danger" id="btn-delete-accepted-photo-edit" style="position: absolute; top: -10px; right: -10px; border-radius: 50%; width: 30px; height: 30px; padding: 0; display: flex; align-items: center; justify-content: center;">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
<div class="alert alert-success mb-3" role="alert">
|
|
<i class="fas fa-check-circle me-2"></i> <span id="photos-progress-edit">Foto 1 dari 3</span>
|
|
</div>
|
|
<button type="button" class="btn btn-primary btn-sm" id="btn-next-photo-edit">
|
|
<i class="fas fa-arrow-right me-2"></i> Lanjut ke Foto Berikutnya
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Photos Summary Container -->
|
|
<div id="photos-summary-container-edit" class="text-center" style="display: none;">
|
|
<div class="alert alert-success mb-3" role="alert">
|
|
<i class="fas fa-check-circle me-2"></i> Semua 3 foto berhasil diambil!
|
|
</div>
|
|
<div style="display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; margin-bottom: 15px;">
|
|
<div style="position: relative; display: inline-block;">
|
|
<img id="summary-photo-1-edit" src="" alt="Foto 1" style="width: 100px; height: 100px; border-radius: 8px; border: 2px solid #22c55e; object-fit: cover;">
|
|
<button type="button" class="btn btn-sm btn-danger" data-photo="1" style="position: absolute; top: -10px; right: -10px; border-radius: 50%; width: 25px; height: 25px; padding: 0; display: flex; align-items: center; justify-content: center; font-size: 12px;" onclick="deletePhotoFromSummaryEdit(this)">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
<div style="position: relative; display: inline-block;">
|
|
<img id="summary-photo-2-edit" src="" alt="Foto 2" style="width: 100px; height: 100px; border-radius: 8px; border: 2px solid #22c55e; object-fit: cover;">
|
|
<button type="button" class="btn btn-sm btn-danger" data-photo="2" style="position: absolute; top: -10px; right: -10px; border-radius: 50%; width: 25px; height: 25px; padding: 0; display: flex; align-items: center; justify-content: center; font-size: 12px;" onclick="deletePhotoFromSummaryEdit(this)">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
<div style="position: relative; display: inline-block;">
|
|
<img id="summary-photo-3-edit" src="" alt="Foto 3" style="width: 100px; height: 100px; border-radius: 8px; border: 2px solid #22c55e; object-fit: cover;">
|
|
<button type="button" class="btn btn-sm btn-danger" data-photo="3" style="position: absolute; top: -10px; right: -10px; border-radius: 50%; width: 25px; height: 25px; padding: 0; display: flex; align-items: center; justify-content: center; font-size: 12px;" onclick="deletePhotoFromSummaryEdit(this)">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<input type="hidden" id="webcam-photo-data-edit" name="pictures_edit" value="">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer bg-white border-0 p-4 gap-2">
|
|
<button type="button" class="btn btn-danger px-4 fw-semibold shadow-sm" data-bs-dismiss="modal">
|
|
<i class="fas fa-times me-2"></i>Batal
|
|
</button>
|
|
<button type="submit" class="btn btn-warning px-4 fw-semibold shadow-sm text-white">
|
|
<i class="fas fa-save me-2"></i>Update Data
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal Edit Kelas Massal -->
|
|
<div class="modal fade" id="modalEditKelasMassal" tabindex="-1" aria-labelledby="modalEditKelasMassalLabel" aria-hidden="true">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content shadow-lg border-0 rounded-4">
|
|
<div class="modal-header bg-warning border-0 rounded-top-4">
|
|
<h5 class="modal-title fw-bold text-dark" id="modalEditKelasMassalLabel">
|
|
Edit Kelas Siswa Terpilih
|
|
</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
</div>
|
|
<form id="formEditKelasMassal" method="POST">
|
|
@csrf
|
|
@method('PUT')
|
|
<div class="modal-body p-4 bg-light">
|
|
<div class="mb-3">
|
|
<label class="form-label fw-semibold text-dark">Kelas Baru</label>
|
|
<select name="id_class" id="mass-edit-class" class="form-select border-2" required>
|
|
<option value="">-- Pilih Kelas --</option>
|
|
@foreach($classes as $kelas)
|
|
<option value="{{ $kelas->id }}">{{ $kelas->name }}</option>
|
|
@endforeach
|
|
</select>
|
|
</div>
|
|
<div class="alert alert-info mb-0">
|
|
Semua siswa yang dipilih akan dipindahkan ke kelas ini.
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer bg-white border-0 p-4 gap-2">
|
|
<button type="button" class="btn btn-danger px-4 fw-semibold shadow-sm" data-bs-dismiss="modal">
|
|
<i class="fas fa-times me-2"></i>Batal
|
|
</button>
|
|
<button type="submit" class="btn btn-warning px-4 fw-semibold shadow-sm text-white">
|
|
<i class="fas fa-save me-2"></i>Update Kelas
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
|
|
<script src="https://cdn.datatables.net/1.13.5/js/jquery.dataTables.min.js"></script>
|
|
<script src="https://cdn.datatables.net/1.13.5/js/dataTables.bootstrap5.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
|
<script>
|
|
let webcamStream = null;
|
|
let webcamCanvas = null;
|
|
let collectedPhotos = [null, null, null];
|
|
let currentPhotoIndex = 0;
|
|
let collectedPhotosEdit = [null, null, null];
|
|
let currentPhotoIndexEdit = 0;
|
|
|
|
// Add Modal Event Listeners
|
|
document.getElementById('btn-start-webcam').addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
startWebcam();
|
|
});
|
|
document.getElementById('btn-capture').addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
capturePhoto();
|
|
});
|
|
document.getElementById('btn-stop-webcam').addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
stopWebcam();
|
|
});
|
|
document.getElementById('btn-retake').addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
retakePhoto();
|
|
});
|
|
document.getElementById('btn-use-photo').addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
usePhoto();
|
|
});
|
|
document.getElementById('btn-delete-accepted-photo').addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
deleteAcceptedPhoto();
|
|
});
|
|
document.getElementById('btn-next-photo').addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
nextPhoto();
|
|
});
|
|
|
|
// Edit Modal Event Listeners
|
|
document.getElementById('btn-start-webcam-edit').addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
startWebcamEdit();
|
|
}); document.getElementById('btn-capture-edit').addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
capturePhotoEdit();
|
|
});
|
|
document.getElementById('btn-stop-webcam-edit').addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
stopWebcamEdit();
|
|
});
|
|
document.getElementById('btn-retake-edit').addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
retakePhotoEdit();
|
|
});
|
|
document.getElementById('btn-use-photo-edit').addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
usePhotoEdit();
|
|
});
|
|
document.getElementById('btn-delete-accepted-photo-edit').addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
deleteAcceptedPhotoEdit();
|
|
});
|
|
document.getElementById('btn-next-photo-edit').addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
nextPhotoEdit();
|
|
});
|
|
|
|
// Live preview foto profil saat memilih file baru (modal edit)
|
|
document.getElementById('edit-profile_picture').addEventListener('change', function() {
|
|
const file = this.files[0];
|
|
if (file) {
|
|
const reader = new FileReader();
|
|
reader.onload = function(e) {
|
|
document.getElementById('edit-profile-preview').src = e.target.result;
|
|
};
|
|
reader.readAsDataURL(file);
|
|
}
|
|
});
|
|
|
|
// Add Modal Functions
|
|
function startWebcam() {
|
|
const video = document.getElementById('webcam-video');
|
|
const constraints = {
|
|
video: { width: { ideal: 400 }, height: { ideal: 400 } },
|
|
audio: false
|
|
};
|
|
navigator.mediaDevices.getUserMedia(constraints)
|
|
.then(function(stream) {
|
|
webcamStream = stream;
|
|
video.srcObject = stream;
|
|
video.onloadedmetadata = function() {
|
|
video.play();
|
|
document.getElementById('initial-container').style.display = 'none';
|
|
document.getElementById('webcam-container').style.display = 'block';
|
|
};
|
|
})
|
|
.catch(function(err) {
|
|
alert('Gagal akses webcam: ' + err.message);
|
|
});
|
|
}
|
|
|
|
function capturePhoto() {
|
|
const video = document.getElementById('webcam-video');
|
|
const canvas = document.getElementById('webcam-canvas');
|
|
const context = canvas.getContext('2d');
|
|
canvas.width = video.videoWidth;
|
|
canvas.height = video.videoHeight;
|
|
context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
webcamCanvas = canvas;
|
|
stopWebcam();
|
|
document.getElementById('webcam-container').style.display = 'none';
|
|
document.getElementById('preview-container').style.display = 'block';
|
|
}
|
|
|
|
function stopWebcam() {
|
|
if (webcamStream) {
|
|
webcamStream.getTracks().forEach(track => track.stop());
|
|
webcamStream = null;
|
|
}
|
|
}
|
|
|
|
function retakePhoto() {
|
|
document.getElementById('preview-container').style.display = 'none';
|
|
document.getElementById('initial-container').style.display = 'block';
|
|
}
|
|
|
|
function usePhoto() {
|
|
if (webcamCanvas) {
|
|
const photoData = webcamCanvas.toDataURL('image/png');
|
|
collectedPhotos[currentPhotoIndex] = photoData;
|
|
document.getElementById('accepted-photo-preview').src = photoData;
|
|
document.getElementById('photos-progress').textContent = `Foto ${currentPhotoIndex + 1} dari 3`;
|
|
document.getElementById('preview-container').style.display = 'none';
|
|
document.getElementById('accepted-container').style.display = 'block';
|
|
}
|
|
}
|
|
|
|
function deleteAcceptedPhoto() {
|
|
collectedPhotos[currentPhotoIndex] = null;
|
|
document.getElementById('accepted-container').style.display = 'none';
|
|
document.getElementById('initial-container').style.display = 'block';
|
|
Swal.fire({
|
|
toast: true,
|
|
position: 'bottom-end',
|
|
icon: 'info',
|
|
title: 'Foto dihapus',
|
|
showConfirmButton: false,
|
|
timer: 2000
|
|
});
|
|
}
|
|
|
|
function nextPhoto() {
|
|
if (currentPhotoIndex < 2) {
|
|
currentPhotoIndex++;
|
|
document.getElementById('accepted-container').style.display = 'none';
|
|
document.getElementById('initial-container').style.display = 'block';
|
|
} else {
|
|
showPhotosSummary();
|
|
}
|
|
}
|
|
|
|
function showPhotosSummary() {
|
|
for (let i = 0; i < 3; i++) {
|
|
if (collectedPhotos[i]) {
|
|
document.getElementById(`summary-photo-${i + 1}`).src = collectedPhotos[i];
|
|
}
|
|
}
|
|
const photosString = collectedPhotos.map(photo => photo ? photo : '').join('|||');
|
|
document.getElementById('webcam-photo-data').value = photosString;
|
|
document.getElementById('accepted-container').style.display = 'none';
|
|
document.getElementById('photos-summary-container').style.display = 'block';
|
|
}
|
|
|
|
function deletePhotoFromSummary(btn) {
|
|
const photoNum = parseInt(btn.getAttribute('data-photo')) - 1;
|
|
collectedPhotos[photoNum] = null;
|
|
document.getElementById(`summary-photo-${photoNum + 1}`).style.opacity = '0.5';
|
|
btn.style.display = 'none';
|
|
const photosString = collectedPhotos.map(photo => photo ? photo : '').join('|||');
|
|
document.getElementById('webcam-photo-data').value = photosString;
|
|
Swal.fire({
|
|
toast: true,
|
|
position: 'bottom-end',
|
|
icon: 'info',
|
|
title: 'Foto dihapus',
|
|
showConfirmButton: false,
|
|
timer: 2000
|
|
});
|
|
}
|
|
|
|
// Edit Modal Functions
|
|
function startWebcamEdit() {
|
|
const video = document.getElementById('webcam-video-edit');
|
|
const constraints = {
|
|
video: { width: { ideal: 400 }, height: { ideal: 400 } },
|
|
audio: false
|
|
};
|
|
navigator.mediaDevices.getUserMedia(constraints)
|
|
.then(function(stream) {
|
|
webcamStream = stream;
|
|
video.srcObject = stream;
|
|
video.onloadedmetadata = function() {
|
|
video.play();
|
|
document.getElementById('initial-container-edit').style.display = 'none';
|
|
document.getElementById('webcam-container-edit').style.display = 'block';
|
|
};
|
|
})
|
|
.catch(function(err) {
|
|
alert('Gagal akses webcam: ' + err.message);
|
|
});
|
|
}
|
|
|
|
function capturePhotoEdit() {
|
|
const video = document.getElementById('webcam-video-edit');
|
|
const canvas = document.getElementById('webcam-canvas-edit');
|
|
const context = canvas.getContext('2d');
|
|
canvas.width = video.videoWidth;
|
|
canvas.height = video.videoHeight;
|
|
context.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
webcamCanvas = canvas;
|
|
stopWebcamEdit();
|
|
const previewImg = document.getElementById('preview-img-edit');
|
|
previewImg.src = canvas.toDataURL('image/png');
|
|
document.getElementById('webcam-container-edit').style.display = 'none';
|
|
document.getElementById('preview-container-edit').style.display = 'block';
|
|
}
|
|
|
|
function stopWebcamEdit() {
|
|
if (webcamStream) {
|
|
webcamStream.getTracks().forEach(track => track.stop());
|
|
webcamStream = null;
|
|
}
|
|
}
|
|
|
|
function retakePhotoEdit() {
|
|
document.getElementById('preview-container-edit').style.display = 'none';
|
|
document.getElementById('initial-container-edit').style.display = 'block';
|
|
}
|
|
|
|
function usePhotoEdit() {
|
|
if (webcamCanvas) {
|
|
const photoData = webcamCanvas.toDataURL('image/png');
|
|
collectedPhotosEdit[currentPhotoIndexEdit] = photoData;
|
|
document.getElementById('accepted-photo-preview-edit').src = photoData;
|
|
document.getElementById('photos-progress-edit').textContent = `Foto ${currentPhotoIndexEdit + 1} dari 3`;
|
|
document.getElementById('preview-container-edit').style.display = 'none';
|
|
document.getElementById('accepted-container-edit').style.display = 'block';
|
|
}
|
|
}
|
|
|
|
function deleteAcceptedPhotoEdit() {
|
|
collectedPhotosEdit[currentPhotoIndexEdit] = null;
|
|
document.getElementById('accepted-container-edit').style.display = 'none';
|
|
document.getElementById('initial-container-edit').style.display = 'block';
|
|
Swal.fire({
|
|
toast: true,
|
|
position: 'bottom-end',
|
|
icon: 'info',
|
|
title: 'Foto dihapus',
|
|
showConfirmButton: false,
|
|
timer: 2000
|
|
});
|
|
}
|
|
|
|
function nextPhotoEdit() {
|
|
if (currentPhotoIndexEdit < 2) {
|
|
currentPhotoIndexEdit++;
|
|
document.getElementById('accepted-container-edit').style.display = 'none';
|
|
document.getElementById('initial-container-edit').style.display = 'block';
|
|
} else {
|
|
showPhotosSummaryEdit();
|
|
}
|
|
}
|
|
|
|
function showPhotosSummaryEdit() {
|
|
for (let i = 0; i < 3; i++) {
|
|
if (collectedPhotosEdit[i]) {
|
|
document.getElementById(`summary-photo-${i + 1}-edit`).src = collectedPhotosEdit[i];
|
|
}
|
|
}
|
|
const photosString = collectedPhotosEdit.map(photo => photo ? photo : '').join('|||');
|
|
document.getElementById('webcam-photo-data-edit').value = photosString;
|
|
document.getElementById('accepted-container-edit').style.display = 'none';
|
|
document.getElementById('photos-summary-container-edit').style.display = 'block';
|
|
}
|
|
|
|
function deletePhotoFromSummaryEdit(btn) {
|
|
const photoNum = parseInt(btn.getAttribute('data-photo')) - 1;
|
|
collectedPhotosEdit[photoNum] = null;
|
|
document.getElementById(`summary-photo-${photoNum + 1}-edit`).style.opacity = '0.5';
|
|
btn.style.display = 'none';
|
|
const photosString = collectedPhotosEdit.map(photo => photo ? photo : '').join('|||');
|
|
document.getElementById('webcam-photo-data-edit').value = photosString;
|
|
Swal.fire({
|
|
toast: true,
|
|
position: 'bottom-end',
|
|
icon: 'info',
|
|
title: 'Foto dihapus',
|
|
showConfirmButton: false,
|
|
timer: 2000
|
|
});
|
|
}
|
|
|
|
$(document).ready(function () {
|
|
// Validasi form submit add - pastikan 3 foto sudah diambil
|
|
$('#btn-submit-form').on('click', function(e) {
|
|
// Webcam photos adalah opsional - boleh submit tanpa foto
|
|
// Validation dihapus untuk membuat webcam truly optional seperti profile picture
|
|
});
|
|
|
|
// Form edit siswa - webcam photos opsional
|
|
$('#formEditSiswa').on('submit', function(e) {
|
|
// Tidak ada validasi - webcam opsional seperti profile picture
|
|
});
|
|
$('#example').DataTable({
|
|
lengthChange: false,
|
|
language: {
|
|
search: "Cari:",
|
|
info: "Menampilkan _START_ sampai _END_ dari _TOTAL_ data",
|
|
paginate: {
|
|
first: "Awal",
|
|
last: "Akhir",
|
|
next: "Berikutnya",
|
|
previous: "Sebelumnya"
|
|
}
|
|
},
|
|
pageLength: 10
|
|
});
|
|
|
|
$('#select-all').on('click', function () {
|
|
$('.row-checkbox').prop('checked', this.checked);
|
|
});
|
|
|
|
$('#btn-hapus-siswa').on('click', function () {
|
|
let checked = [];
|
|
$('#example tbody input.row-checkbox:checked').each(function () {
|
|
checked.push($(this).closest('tr').data('id'));
|
|
});
|
|
|
|
if (checked.length === 0) {
|
|
Swal.fire({
|
|
toast: true,
|
|
position: 'bottom-end',
|
|
icon: 'warning',
|
|
title: 'Pilih data yang ingin dihapus.',
|
|
showConfirmButton: false,
|
|
timer: 2000
|
|
});
|
|
return;
|
|
}
|
|
|
|
Swal.fire({
|
|
title: 'Yakin ingin menghapus ' + checked.length + ' siswa?',
|
|
icon: 'warning',
|
|
showCancelButton: true,
|
|
confirmButtonText: 'Ya, hapus!',
|
|
cancelButtonText: 'Batal',
|
|
confirmButtonColor: '#dc3545',
|
|
reverseButtons: true
|
|
}).then((result) => {
|
|
if (result.isConfirmed) {
|
|
checked.forEach(function(id) {
|
|
let form = document.createElement('form');
|
|
form.action = '{{ url("/pages/akun/indentitas_siswa") }}/' + id;
|
|
form.method = 'POST';
|
|
form.style.display = 'none';
|
|
|
|
let csrf = document.createElement('input');
|
|
csrf.type = 'hidden';
|
|
csrf.name = '_token';
|
|
csrf.value = '{{ csrf_token() }}';
|
|
form.appendChild(csrf);
|
|
|
|
let method = document.createElement('input');
|
|
method.type = 'hidden';
|
|
method.name = '_method';
|
|
method.value = 'DELETE';
|
|
form.appendChild(method);
|
|
|
|
document.body.appendChild(form);
|
|
form.submit();
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
// ===== Kamera Absensi Modal =====
|
|
let absensiStream = null;
|
|
|
|
function startAbsensiCamera() {
|
|
const video = document.getElementById('absensi-video');
|
|
const loading = document.getElementById('absensi-loading');
|
|
const captureBtn = document.getElementById('btn-absensi-capture');
|
|
|
|
navigator.mediaDevices.getUserMedia({ video: { facingMode: 'user' }, audio: false })
|
|
.then(function(stream) {
|
|
absensiStream = stream;
|
|
video.srcObject = stream;
|
|
video.onloadedmetadata = function() {
|
|
video.play();
|
|
loading.style.display = 'none';
|
|
captureBtn.disabled = false;
|
|
};
|
|
})
|
|
.catch(function(err) {
|
|
loading.style.display = 'none';
|
|
document.getElementById('absensi-result').style.display = 'block';
|
|
document.getElementById('absensi-result-content').innerHTML =
|
|
'<div class="alert alert-danger"><i class="fas fa-exclamation-circle me-2"></i>Tidak dapat mengakses kamera: ' + err.message + '</div>';
|
|
});
|
|
}
|
|
|
|
function stopAbsensiCamera() {
|
|
if (absensiStream) {
|
|
absensiStream.getTracks().forEach(function(track) { track.stop(); });
|
|
absensiStream = null;
|
|
}
|
|
const video = document.getElementById('absensi-video');
|
|
if (video) video.srcObject = null;
|
|
}
|
|
|
|
$('#btn-camera-siswa').on('click', function () {
|
|
// Reset state
|
|
document.getElementById('absensi-result').style.display = 'none';
|
|
document.getElementById('absensi-result-content').innerHTML = '';
|
|
document.getElementById('absensi-sending').style.display = 'none';
|
|
document.getElementById('btn-absensi-retake').style.display = 'none';
|
|
document.getElementById('absensi-loading').style.display = 'flex';
|
|
document.getElementById('btn-absensi-capture').disabled = true;
|
|
$('#modalKameraAbsensi').modal('show');
|
|
});
|
|
|
|
document.getElementById('modalKameraAbsensi').addEventListener('shown.bs.modal', function () {
|
|
startAbsensiCamera();
|
|
});
|
|
|
|
document.getElementById('btn-absensi-capture').addEventListener('click', function () {
|
|
const video = document.getElementById('absensi-video');
|
|
const canvas = document.getElementById('absensi-canvas');
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
canvas.width = video.videoWidth || 640;
|
|
canvas.height = video.videoHeight || 480;
|
|
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
|
|
// Tampilkan spinner kirim
|
|
document.getElementById('absensi-sending').style.display = 'block';
|
|
document.getElementById('absensi-result').style.display = 'none';
|
|
document.getElementById('btn-absensi-capture').disabled = true;
|
|
|
|
canvas.toBlob(function(blob) {
|
|
const formData = new FormData();
|
|
formData.append('image', blob, 'capture.jpg');
|
|
|
|
$.ajax({
|
|
url: 'http://202.10.47.101:5000/attendance/recognize',
|
|
type: 'POST',
|
|
data: formData,
|
|
processData: false,
|
|
contentType: false,
|
|
success: function(response) {
|
|
document.getElementById('absensi-sending').style.display = 'none';
|
|
document.getElementById('btn-absensi-retake').style.display = 'inline-block';
|
|
|
|
let html = '';
|
|
const isSuccess = response && response.status === 'success';
|
|
const alertClass = isSuccess ? 'alert-success' : 'alert-warning';
|
|
const iconClass = isSuccess ? 'fa-check-circle' : 'fa-exclamation-triangle';
|
|
|
|
const nama = response.student_name || response.name || '-';
|
|
const statusDisp = response.attendance_status_display || '-';
|
|
const confidence = response.confidence != null
|
|
? (parseFloat(response.confidence) * 100).toFixed(1) + '%'
|
|
: '-';
|
|
const message = response.message || '-';
|
|
|
|
html = '<div class="alert ' + alertClass + ' text-start mb-0">'
|
|
+ '<div class="fw-bold mb-2"><i class="fas ' + iconClass + ' me-2"></i>' + message + '</div>'
|
|
+ '<table class="table table-sm table-borderless mb-0" style="font-size:0.9rem;">'
|
|
+ '<tr><td class="text-muted pe-2" style="width:130px;">Nama Siswa</td>'
|
|
+ '<td><strong>' + nama + '</strong></td></tr>'
|
|
+ '<tr><td class="text-muted pe-2">Status Kehadiran</td>'
|
|
+ '<td><strong>' + statusDisp + '</strong></td></tr>'
|
|
+ '<tr><td class="text-muted pe-2">Confidence</td>'
|
|
+ '<td><strong>' + confidence + '</strong></td></tr>'
|
|
+ '</table>'
|
|
+ '</div>';
|
|
document.getElementById('absensi-result-content').innerHTML = html;
|
|
document.getElementById('absensi-result').style.display = 'block';
|
|
},
|
|
error: function(xhr) {
|
|
document.getElementById('absensi-sending').style.display = 'none';
|
|
document.getElementById('btn-absensi-capture').disabled = false;
|
|
document.getElementById('btn-absensi-retake').style.display = 'inline-block';
|
|
|
|
let errMsg = 'Gagal menghubungi server absensi.';
|
|
if (xhr.responseJSON && xhr.responseJSON.message) errMsg = xhr.responseJSON.message;
|
|
else if (xhr.responseText) errMsg = xhr.responseText.substring(0, 200);
|
|
|
|
document.getElementById('absensi-result-content').innerHTML =
|
|
'<div class="alert alert-danger"><i class="fas fa-exclamation-circle me-2"></i>' + errMsg + '</div>';
|
|
document.getElementById('absensi-result').style.display = 'block';
|
|
}
|
|
});
|
|
}, 'image/jpeg', 0.92);
|
|
});
|
|
|
|
document.getElementById('btn-absensi-retake').addEventListener('click', function () {
|
|
document.getElementById('absensi-result').style.display = 'none';
|
|
document.getElementById('absensi-result-content').innerHTML = '';
|
|
document.getElementById('btn-absensi-retake').style.display = 'none';
|
|
document.getElementById('btn-absensi-capture').disabled = false;
|
|
});
|
|
// ===== End Kamera Absensi Modal =====
|
|
|
|
$('#btn-dataset-siswa').on('click', function () {
|
|
Swal.fire({
|
|
title: 'Latih Model CNN?',
|
|
text: 'Apakah Anda yakin ingin melatih model CNN? Proses ini mungkin memakan waktu beberapa saat.',
|
|
icon: 'question',
|
|
showCancelButton: true,
|
|
confirmButtonText: 'Ya, Latih',
|
|
cancelButtonText: 'Batal',
|
|
confirmButtonColor: '#06b6d4',
|
|
reverseButtons: true
|
|
}).then((result) => {
|
|
if (result.isConfirmed) {
|
|
$.ajax({
|
|
url: 'http://presensiku.site/flaskpresensiku/pipeline/run',
|
|
type: 'POST',
|
|
contentType: 'application/json',
|
|
success: function(response) {
|
|
Swal.fire({
|
|
toast: true,
|
|
position: 'bottom-end',
|
|
icon: 'success',
|
|
title: 'Latih model cnn berhasil dijalankan',
|
|
showConfirmButton: false,
|
|
timer: 2000
|
|
});
|
|
},
|
|
error: function(error) {
|
|
Swal.fire({
|
|
toast: true,
|
|
position: 'bottom-end',
|
|
icon: 'error',
|
|
title: 'Gagal menjalankan latih model',
|
|
showConfirmButton: false,
|
|
timer: 2000
|
|
});
|
|
}
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
$('#btn-edit-siswa').on('click', function () {
|
|
const checked = $('.row-checkbox:checked');
|
|
if (checked.length === 0) {
|
|
alert('Pilih data yang ingin diedit.');
|
|
} else if (checked.length === 1) {
|
|
const tr = checked.closest('tr');
|
|
const id = tr.data('id');
|
|
const name = tr.find('td').eq(3).text().trim();
|
|
const email = tr.find('td').eq(4).text().trim();
|
|
const nisn = tr.find('td').eq(5).text().trim();
|
|
const kelasId = tr.find('td').eq(6).data('class-id') || '';
|
|
const entryYear = tr.find('td').eq(7).text().trim();
|
|
|
|
$('#edit-name').val(name);
|
|
$('#edit-email').val(email);
|
|
$('#edit-nisn').val(nisn);
|
|
$('#edit-id_class').val(kelasId);
|
|
$('#edit-entry_year').val(entryYear);
|
|
$('#formEditSiswa').attr('action', '{{ url("/pages/akun/indentitas_siswa") }}/' + id);
|
|
|
|
// Set preview foto profil dari baris tabel & reset input file
|
|
const profileImg = tr.find('td').eq(1).find('img').attr('src');
|
|
$('#edit-profile-preview').attr('src', profileImg || 'https://ui-avatars.com/api/?name=' + encodeURIComponent(name));
|
|
$('#edit-profile_picture').val('');
|
|
|
|
document.getElementById('initial-container-edit').style.display = 'block';
|
|
document.getElementById('webcam-container-edit').style.display = 'none';
|
|
document.getElementById('preview-container-edit').style.display = 'none';
|
|
document.getElementById('accepted-container-edit').style.display = 'none';
|
|
document.getElementById('photos-summary-container-edit').style.display = 'none';
|
|
document.getElementById('webcam-photo-data-edit').value = '';
|
|
|
|
// Display existing photos
|
|
const existingPhotos = tr.find('td').eq(2).find('img');
|
|
document.getElementById('existing-photo-1').style.display = 'none';
|
|
document.getElementById('existing-photo-2').style.display = 'none';
|
|
document.getElementById('existing-photo-3').style.display = 'none';
|
|
document.getElementById('no-photo-1').style.display = 'block';
|
|
document.getElementById('no-photo-2').style.display = 'block';
|
|
document.getElementById('no-photo-3').style.display = 'block';
|
|
|
|
existingPhotos.each(function(index) {
|
|
if (index < 3) {
|
|
const src = $(this).attr('src');
|
|
document.getElementById(`existing-photo-${index + 1}`).src = src;
|
|
document.getElementById(`existing-photo-${index + 1}`).style.display = 'block';
|
|
document.getElementById(`no-photo-${index + 1}`).style.display = 'none';
|
|
}
|
|
});
|
|
|
|
collectedPhotosEdit = [null, null, null];
|
|
currentPhotoIndexEdit = 0;
|
|
collectedPhotosEdit = [null, null, null];
|
|
currentPhotoIndexEdit = 0;
|
|
|
|
$('#modalEditSiswa').modal('show');
|
|
} else {
|
|
$('#mass-edit-class').val('');
|
|
$('#modalEditKelasMassal').modal('show');
|
|
|
|
$('#formEditKelasMassal').off('submit').on('submit', function(e) {
|
|
e.preventDefault();
|
|
const kelasBaru = $('#mass-edit-class').val();
|
|
if (!kelasBaru) {
|
|
Swal.fire('Pilih kelas terlebih dahulu!', '', 'warning');
|
|
return;
|
|
}
|
|
checked.each(function () {
|
|
const id = $(this).closest('tr').data('id');
|
|
let form = document.createElement('form');
|
|
form.action = '{{ url("/pages/akun/indentitas_siswa") }}/' + id;
|
|
form.method = 'POST';
|
|
form.style.display = 'none';
|
|
|
|
let csrf = document.createElement('input');
|
|
csrf.type = 'hidden';
|
|
csrf.name = '_token';
|
|
csrf.value = '{{ csrf_token() }}';
|
|
form.appendChild(csrf);
|
|
|
|
let method = document.createElement('input');
|
|
method.type = 'hidden';
|
|
method.name = '_method';
|
|
method.value = 'PUT';
|
|
form.appendChild(method);
|
|
|
|
let kelas = document.createElement('input');
|
|
kelas.type = 'hidden';
|
|
kelas.name = 'id_class';
|
|
kelas.value = kelasBaru;
|
|
form.appendChild(kelas);
|
|
|
|
document.body.appendChild(form);
|
|
form.submit();
|
|
});
|
|
$('#modalEditKelasMassal').modal('hide');
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
// Modal Cleanup
|
|
document.getElementById('modalTambahSiswa').addEventListener('hidden.bs.modal', function() {
|
|
stopWebcam();
|
|
document.getElementById('initial-container').style.display = 'block';
|
|
document.getElementById('webcam-container').style.display = 'none';
|
|
document.getElementById('preview-container').style.display = 'none';
|
|
document.getElementById('accepted-container').style.display = 'none';
|
|
document.getElementById('photos-summary-container').style.display = 'none';
|
|
document.getElementById('webcam-photo-data').value = '';
|
|
collectedPhotos = [null, null, null];
|
|
currentPhotoIndex = 0;
|
|
});
|
|
|
|
document.getElementById('modalEditSiswa').addEventListener('hidden.bs.modal', function() {
|
|
stopWebcamEdit();
|
|
document.getElementById('initial-container-edit').style.display = 'block';
|
|
document.getElementById('webcam-container-edit').style.display = 'none';
|
|
document.getElementById('preview-container-edit').style.display = 'none';
|
|
document.getElementById('accepted-container-edit').style.display = 'none';
|
|
document.getElementById('photos-summary-container-edit').style.display = 'none';
|
|
document.getElementById('webcam-photo-data-edit').value = '';
|
|
collectedPhotosEdit = [null, null, null];
|
|
currentPhotoIndexEdit = 0;
|
|
|
|
// Reset existing photos display
|
|
document.getElementById('existing-photo-1').style.display = 'none';
|
|
document.getElementById('existing-photo-2').style.display = 'none';
|
|
document.getElementById('existing-photo-3').style.display = 'none';
|
|
document.getElementById('no-photo-1').style.display = 'block';
|
|
document.getElementById('no-photo-2').style.display = 'block';
|
|
document.getElementById('no-photo-3').style.display = 'block';
|
|
});
|
|
|
|
// Cleanup modal kamera absensi saat ditutup
|
|
document.getElementById('modalKameraAbsensi').addEventListener('hidden.bs.modal', function() {
|
|
stopAbsensiCamera();
|
|
document.getElementById('absensi-result').style.display = 'none';
|
|
document.getElementById('absensi-result-content').innerHTML = '';
|
|
document.getElementById('absensi-sending').style.display = 'none';
|
|
document.getElementById('btn-absensi-retake').style.display = 'none';
|
|
document.getElementById('absensi-loading').style.display = 'flex';
|
|
document.getElementById('btn-absensi-capture').disabled = true;
|
|
});
|
|
</script>
|
|
|
|
{{-- Flash & validation feedback (single source) --}}
|
|
<div id="page-flash"
|
|
data-success="{{ session('success') ?? '' }}"
|
|
data-error="{{ session('error') ?? '' }}"
|
|
data-open-modal="{{ session('open_modal') ?? '' }}"
|
|
data-edit-id="{{ session('edit_id') ?? '' }}"
|
|
data-errors='@json($errors->all())'
|
|
style="display:none;"></div>
|
|
|
|
<script>
|
|
(function () {
|
|
const flashEl = document.getElementById('page-flash');
|
|
if (!flashEl) return;
|
|
|
|
const successMsg = flashEl.dataset.success || '';
|
|
const errorMsg = flashEl.dataset.error || '';
|
|
const openModal = flashEl.dataset.openModal || '';
|
|
let validationErrors = [];
|
|
try {
|
|
validationErrors = JSON.parse(flashEl.dataset.errors || '[]');
|
|
} catch (e) {
|
|
validationErrors = [];
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
// Toast sukses
|
|
if (successMsg) {
|
|
Swal.fire({
|
|
toast: true,
|
|
position: 'bottom-end',
|
|
icon: 'success',
|
|
title: successMsg,
|
|
showConfirmButton: false,
|
|
timer: 2500,
|
|
timerProgressBar: true
|
|
});
|
|
}
|
|
|
|
// Dialog validasi (centered) — prioritaskan ini dulu
|
|
if (validationErrors.length > 0) {
|
|
validationErrors = validationErrors.map(function(msg) {
|
|
switch (msg) {
|
|
case 'The password field must be at least 6 characters.':
|
|
return 'Password minimal 6 karakter.';
|
|
|
|
case 'The nisn field must be at least 10 characters.':
|
|
case 'The nisn field must be 10 digits.':
|
|
case 'The nisn must be at least 10 characters.':
|
|
case 'The nisn must be 10 digits.':
|
|
return 'NISN minimal 10 digit.';
|
|
|
|
case 'The email has already been taken.':
|
|
return 'Email sudah digunakan.';
|
|
|
|
case 'The nisn has already been taken.':
|
|
return 'NISN sudah terdaftar.';
|
|
|
|
case 'The profile picture field must be an image.':
|
|
return 'File yang dipilih harus berupa gambar.';
|
|
|
|
default:
|
|
return msg;
|
|
}
|
|
});
|
|
|
|
const listHtml = '<ul class="list-unstyled text-center mb-0 p-0">' +
|
|
validationErrors.map(function (msg) {
|
|
return '<li class="mb-1">' + msg + '</li>';
|
|
}).join('') + '</ul>';
|
|
|
|
Swal.fire({
|
|
icon: 'error',
|
|
title: 'Gagal Menyimpan Data',
|
|
html: listHtml,
|
|
confirmButtonText: 'Mengerti',
|
|
confirmButtonColor: '#365CF5',
|
|
customClass: {
|
|
popup: 'rounded-4 shadow-lg',
|
|
title: 'fw-bold text-danger',
|
|
confirmButton: 'fw-semibold px-4'
|
|
}
|
|
}).then(function () {
|
|
reopenModalIfNeeded();
|
|
});
|
|
} else if (errorMsg) {
|
|
// Toast error umum (bukan validasi)
|
|
Swal.fire({
|
|
toast: true,
|
|
position: 'bottom-end',
|
|
icon: 'error',
|
|
title: errorMsg,
|
|
showConfirmButton: false,
|
|
timer: 2500,
|
|
timerProgressBar: true
|
|
}).then(function () {
|
|
reopenModalIfNeeded();
|
|
});
|
|
} else {
|
|
reopenModalIfNeeded();
|
|
}
|
|
});
|
|
|
|
function reopenModalIfNeeded() {
|
|
if (openModal === 'tambah') {
|
|
const el = document.getElementById('modalTambahSiswa');
|
|
if (el && typeof bootstrap !== 'undefined') {
|
|
bootstrap.Modal.getOrCreateInstance(el).show();
|
|
}
|
|
}
|
|
// Catatan: modal edit tidak otomatis dibuka kembali karena membutuhkan
|
|
// data dinamis (foto, kelas, dsb) yang dimuat saat tombol edit diklik.
|
|
}
|
|
})();
|
|
</script>
|
|
@endsection
|