update : hubungin kamera ke web, dan kirim data absensi setelah terdeteksi ke database sesuai tgl. next : sesuaikan dengan matkul
This commit is contained in:
parent
d2bc200b8c
commit
a7e981f8c7
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\AbsensiSiswa;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class AbsensiLaporanController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request)
|
||||
{
|
||||
// Mulai query builder dengan relasi yang dibutuhkan
|
||||
$query = AbsensiSiswa::with(['siswa.jurusan', 'siswa.kelas', 'devices']);
|
||||
|
||||
// 1. Filter berdasarkan Tanggal (Wajib)
|
||||
$tanggal = $request->input('tanggal', Carbon::today()->toDateString());
|
||||
$query->whereDate('waktu', $tanggal);
|
||||
|
||||
// 2. Filter berdasarkan Jurusan
|
||||
if ($request->filled('jurusan_id') && $request->jurusan_id != 'all') {
|
||||
$query->whereHas('siswa', function ($q) use ($request) {
|
||||
$q->where('id_jurusan', $request->jurusan_id);
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Filter berdasarkan Kelas
|
||||
if ($request->filled('kelas_id') && $request->kelas_id != 'all') {
|
||||
$query->whereHas('siswa', function ($q) use ($request) {
|
||||
$q->where('id_kelas', $request->kelas_id);
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Filter berdasarkan Device
|
||||
if ($request->filled('device_id') && $request->device_id != 'all') {
|
||||
$query->where('id_devices', $request->device_id);
|
||||
}
|
||||
|
||||
// Eksekusi query
|
||||
$absensi = $query->latest('waktu')->get();
|
||||
|
||||
// Ubah data menjadi format JSON yang rapi untuk dikirim
|
||||
$formattedData = $absensi->map(function ($item, $key) {
|
||||
return [
|
||||
'no' => $key + 1,
|
||||
'nama_siswa' => $item->siswa->nama_siswa ?? 'Siswa Dihapus',
|
||||
'jurusan' => $item->siswa->jurusan->nama_jurusan ?? 'N/A',
|
||||
'kelas' => $item->siswa->kelas->nama_kelas ?? 'N/A',
|
||||
'waktu' => Carbon::parse($item->waktu)->format('H:i:s'),
|
||||
'ruangan' => $item->devices->nama_device ?? 'Device Dihapus',
|
||||
'status' => strtolower($item->status),
|
||||
];
|
||||
});
|
||||
|
||||
return response()->json($formattedData);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Resources\SiswaResource;
|
||||
use App\Models\Devices;
|
||||
use App\Models\Siswa;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class DeviceStudentController extends Controller
|
||||
{
|
||||
public function index(Devices $device)
|
||||
{
|
||||
// Eager load relasi 'kelas' untuk memastikan data tersedia.
|
||||
// Ini akan mencegah error jika relasi belum didefinisikan di model.
|
||||
// Jika Anda melihat error di sini, berarti relasi di model Devices salah.
|
||||
$device->loadMissing('kelas');
|
||||
|
||||
// Jika device tidak terhubung dengan kelas, kembalikan pesan error yang jelas.
|
||||
if (!$device->kelas) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Device ini tidak terhubung dengan kelas manapun.'
|
||||
], 404);
|
||||
}
|
||||
|
||||
// Ambil semua siswa dari kelas yang sama dengan device tersebut
|
||||
// dan pastikan kita juga memuat relasi 'fotos' untuk setiap siswa.
|
||||
$students = Siswa::where('id_kelas', $device->id_kelas)
|
||||
->with('fotos') // Eager load relasi 'fotos' dari model Siswa
|
||||
->get();
|
||||
|
||||
// Kembalikan data siswa menggunakan SiswaResource untuk format yang rapi.
|
||||
return SiswaResource::collection($students)
|
||||
->additional([
|
||||
'success' => true,
|
||||
'message' => 'Berhasil mengambil data siswa untuk device ' . $device->nama_device,
|
||||
'device' => [
|
||||
'id' => $device->id,
|
||||
'nama' => $device->nama_device,
|
||||
'kelas' => $device->kelas->nama_kelas
|
||||
]
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Devices;
|
||||
use App\Models\Kelas;
|
||||
use App\Models\Ruangan;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class DevicesController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$devices = Devices::with(['kelas', 'ruangan'])->latest()->paginate(10);
|
||||
$kelas = Kelas::orderBy('nama_kelas')->get();
|
||||
$ruangan = Ruangan::orderBy('nama_ruangan')->get();
|
||||
|
||||
return view('admin.devices.index', compact('devices', 'kelas', 'ruangan'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'nama_device' => 'required|string|max:255|unique:devices,nama_device',
|
||||
'ip_address' => 'nullable|ipv4',
|
||||
'id_kelas' => 'required|exists:kelas,id',
|
||||
'id_ruangan' => 'required|exists:ruangan,id',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return redirect()->back()->withErrors($validator)->withInput();
|
||||
}
|
||||
|
||||
Devices::create($request->all());
|
||||
|
||||
return redirect()->route('admin.devices.index')->with('success', 'Device baru berhasil ditambahkan.');
|
||||
}
|
||||
|
||||
public function update(Request $request, Devices $device)
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'nama_device' => 'required|string|max:255|unique:devices,nama_device,' . $device->id,
|
||||
'ip_address' => 'nullable|ipv4',
|
||||
'id_kelas' => 'required|exists:kelas,id',
|
||||
'id_ruangan' => 'required|exists:ruangan,id',
|
||||
]);
|
||||
|
||||
if ($validator->fails()) {
|
||||
return redirect()->back()->withErrors($validator)->withInput();
|
||||
}
|
||||
|
||||
$device->update($request->all());
|
||||
|
||||
return redirect()->route('admin.devices.index')->with('success', 'Data device berhasil diperbarui.');
|
||||
}
|
||||
|
||||
public function destroy(Devices $device)
|
||||
{
|
||||
$device->delete();
|
||||
return redirect()->route('admin.devices.index')->with('success', 'Device berhasil dihapus.');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class FotoSiswaResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
// 'url' akan berisi link lengkap ke foto di storage
|
||||
'url' => Storage::url($this->path),
|
||||
];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class SiswaResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource into an array.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function toArray(Request $request): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'nama_siswa' => $this->nama_siswa,
|
||||
'nisn' => $this->nisn,
|
||||
// 'fotos' akan menjadi array berisi URL foto dari FotoSiswaResource
|
||||
'fotos' => FotoSiswaResource::collection($this->whenLoaded('fotos')),
|
||||
];
|
||||
}
|
||||
}
|
|
@ -21,7 +21,7 @@ class AbsensiGuru extends Model
|
|||
//Relasi ke tabel devices
|
||||
public function devices()
|
||||
{
|
||||
return $this->belongsTo(Device::class, 'id_devices');
|
||||
return $this->belongsTo(Devices::class, 'id_devices');
|
||||
}
|
||||
|
||||
// Relasi ke tabel siswa
|
||||
|
|
|
@ -6,13 +6,34 @@
|
|||
|
||||
class Devices extends Model
|
||||
{
|
||||
protected $fillable = ['nama_device', 'id_kelas', 'id_ruangan'];
|
||||
|
||||
public function kelas(){
|
||||
return $this->belongsTo(Kelas::class);
|
||||
protected $table = 'devices';
|
||||
|
||||
/**
|
||||
* Kolom yang bisa diisi secara massal (mass assignable).
|
||||
* Pastikan semua kolom dari form ada di sini.
|
||||
*/
|
||||
protected $fillable = [
|
||||
'nama_device',
|
||||
'ip_address',
|
||||
'id_kelas',
|
||||
'id_ruangan',
|
||||
];
|
||||
|
||||
/**
|
||||
* Mendefinisikan relasi "belongsTo" ke model Kelas.
|
||||
* Ini memberitahu Laravel bahwa satu device "milik" satu kelas.
|
||||
*/
|
||||
public function kelas()
|
||||
{
|
||||
return $this->belongsTo(Kelas::class, 'id_kelas');
|
||||
}
|
||||
|
||||
public function ruangan(){
|
||||
return $this->belongsTo(Ruangan::class);
|
||||
|
||||
/**
|
||||
* Mendefinisikan relasi "belongsTo" ke model Ruangan.
|
||||
* Ini memberitahu Laravel bahwa satu device "milik" satu ruangan.
|
||||
*/
|
||||
public function ruangan()
|
||||
{
|
||||
return $this->belongsTo(Ruangan::class, 'id_ruangan');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,4 +39,9 @@ public function absensi()
|
|||
{
|
||||
return $this->hasMany(AbsensiSiswa::class, 'id_siswa');
|
||||
}
|
||||
|
||||
public function fotos()
|
||||
{
|
||||
return $this->hasMany(FotoSiswa::class, 'id_siswa');
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@ public function up(): void
|
|||
$table->string('nama_device')->unique();
|
||||
$table->foreignId('id_kelas')->constrained('kelas')->onDelete('cascade');
|
||||
$table->foreignId('id_ruangan')->constrained('ruangan')->onDelete('cascade');
|
||||
$table->string('ip_address')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -44,7 +44,7 @@ public function run(): void
|
|||
'alamat' => 'Jl. Kenanga No. 3',
|
||||
'no_hp' => '081234567892',
|
||||
'email' => 'raihan@example.com',
|
||||
'id_jurusan' => 2,
|
||||
'id_jurusan' => 2,
|
||||
'id_kelas' => 3,
|
||||
],
|
||||
]);
|
||||
|
|
|
@ -52,6 +52,9 @@ class="block p-2 rounded-lg {{ request()->is('admin/jurusan/index') ? 'bg-blue-1
|
|||
<li><a href="{{ route('admin.ruangan.index') }}"
|
||||
class="block p-2 rounded-lg {{ request()->is('admin/ruangan/index') ? 'bg-blue-100 text-blue-600' : 'bg-gray-50 text-gray-800' }}">Ruangan</a>
|
||||
</li>
|
||||
<li><a href="{{ route('admin.devices.index') }}"
|
||||
class="block p-2 rounded-lg {{ request()->is('admin/devices/index') ? 'bg-blue-100 text-blue-600' : 'bg-gray-50 text-gray-800' }}">Devices</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
|
||||
|
|
|
@ -0,0 +1,183 @@
|
|||
@extends('layouts.dashboard')
|
||||
|
||||
@section('title', 'Smart School | Manajemen Device')
|
||||
|
||||
@section('content')
|
||||
<div class="container mx-auto px-4 py-6 relative">
|
||||
|
||||
<!-- Toast Notification Container -->
|
||||
<div class="absolute top-4 right-4 space-y-3 z-50">
|
||||
@if (session('success'))
|
||||
<div id="toast-success" class="flex items-center w-full max-w-xs p-4 text-gray-500 bg-white rounded-lg shadow-lg" role="alert">
|
||||
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-green-500 bg-green-100 rounded-lg"><svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20"><path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 8.207-4 4a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L9 10.586l3.293-3.293a1 1 0 0 1 1.414 1.414Z"/></svg></div>
|
||||
<div class="ml-3 text-sm font-normal">{{ session('success') }}</div>
|
||||
<button type="button" class="ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg p-1.5" data-dismiss-target="#toast-success" aria-label="Close"><svg class="w-3 h-3" fill="none" viewBox="0 0 14 14" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/></svg></button>
|
||||
</div>
|
||||
@endif
|
||||
@if ($errors->any())
|
||||
<div id="toast-error" class="flex items-center w-full max-w-xs p-4 text-gray-500 bg-white rounded-lg shadow-lg" role="alert">
|
||||
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-red-500 bg-red-100 rounded-lg"><svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20"><path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM10 15a1 1 0 1 1 0-2 1 1 0 0 1 0 2Zm1-4a1 1 0 0 1-2 0V6a1 1 0 0 1 2 0v5Z"/></svg></div>
|
||||
<div class="ml-3 text-sm font-normal">Gagal! Periksa kembali data Anda.</div>
|
||||
<button type="button" class="ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg p-1.5" data-dismiss-target="#toast-error" aria-label="Close"><svg class="w-3 h-3" fill="none" viewBox="0 0 14 14" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/></svg></button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-2xl shadow-xl overflow-hidden">
|
||||
<!-- Header Card -->
|
||||
<div class="bg-gradient-to-br from-blue-600 to-cyan-500 px-8 py-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-white flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 mr-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 18h.01M7 21h10a2 2 0 002-2V5a2 2 0 00-2-2H7a2 2 0 00-2 2v14a2 2 0 002 2z"/></svg>
|
||||
Manajemen Device
|
||||
</h1>
|
||||
<p class="text-blue-100 mt-1">Total {{ $devices->total() }} device terpasang</p>
|
||||
</div>
|
||||
<button onclick="openModal('createModal')" class="flex items-center px-4 py-2 bg-white text-blue-600 rounded-lg shadow hover:bg-gray-50 transition">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/></svg>
|
||||
Tambah Device
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table Section -->
|
||||
<div class="p-6">
|
||||
<div class="overflow-x-auto rounded-lg border border-gray-200 shadow-sm">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">No</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Nama Device</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">IP Address</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Kelas</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Ruangan</th>
|
||||
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
@forelse($devices as $key => $device)
|
||||
<tr class="hover:bg-gray-50 transition">
|
||||
<td class="px-6 py-4">{{ $devices->firstItem() + $key }}</td>
|
||||
<td class="px-6 py-4 font-medium text-gray-900">{{ $device->nama_device }}</td>
|
||||
<td class="px-6 py-4 text-gray-500 font-mono">{{ $device->ip_address ?? 'N/A' }}</td>
|
||||
<td class="px-6 py-4 text-gray-500">{{ $device->kelas->nama_kelas ?? '-' }}</td>
|
||||
<td class="px-6 py-4 text-gray-500">{{ $device->ruangan->nama_ruangan ?? '-' }}</td>
|
||||
<td class="px-6 py-4 text-right">
|
||||
<div class="flex justify-end space-x-2">
|
||||
<button onclick="openModal('editModal-{{$device->id}}')" class="text-indigo-600 hover:text-indigo-900 p-1">Edit</button>
|
||||
<form id="delete-form-{{ $device->id }}" action="{{ route('admin.devices.destroy', $device->id) }}" method="POST">
|
||||
@csrf @method('DELETE')
|
||||
<button type="button" onclick="confirmDelete('{{ $device->id }}', '{{ $device->nama_device }}')" class="text-red-600 hover:text-red-900 p-1">Hapus</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="6" class="px-6 py-4 text-center text-gray-500">Belum ada device yang ditambahkan.</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="mt-4">{{ $devices->links() }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create Modal -->
|
||||
<div id="createModal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||
<div class="relative top-20 mx-auto p-5 border w-full max-w-lg shadow-lg rounded-md bg-white">
|
||||
<form action="{{ route('admin.devices.store') }}" method="POST">
|
||||
@csrf
|
||||
<div class="p-4">
|
||||
<h3 class="text-lg font-medium leading-6 text-gray-900 mb-4">Tambah Device Baru</h3>
|
||||
<div class="space-y-4">
|
||||
{{-- Form fields --}}
|
||||
<div>
|
||||
<label for="nama_device_create" class="block text-sm font-medium text-gray-700">Nama Device</label>
|
||||
<input type="text" name="nama_device" id="nama_device_create" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="ip_address_create" class="block text-sm font-medium text-gray-700">IP Address</label>
|
||||
<input type="text" name="ip_address" id="ip_address_create" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm" placeholder="Contoh: 192.168.1.101">
|
||||
</div>
|
||||
<div>
|
||||
<label for="id_kelas_create" class="block text-sm font-medium text-gray-700">Kelas</label>
|
||||
<select name="id_kelas" id="id_kelas_create" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm" required>
|
||||
@foreach($kelas as $item) <option value="{{ $item->id }}">{{ $item->nama_kelas }}</option> @endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="id_ruangan_create" class="block text-sm font-medium text-gray-700">Ruangan</label>
|
||||
<select name="id_ruangan" id="id_ruangan_create" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm" required>
|
||||
@foreach($ruangan as $item) <option value="{{ $item->id }}">{{ $item->nama_ruangan }}</option> @endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 flex justify-end space-x-2">
|
||||
<button type="button" onclick="closeModal('createModal')" class="px-4 py-2 bg-gray-200 text-gray-800 rounded-md">Batal</button>
|
||||
<button type="submit" class="px-4 py-2 bg-indigo-600 text-white rounded-md">Simpan</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Modals -->
|
||||
@foreach($devices as $device)
|
||||
<div id="editModal-{{$device->id}}" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
||||
<div class="relative top-20 mx-auto p-5 border w-full max-w-lg shadow-lg rounded-md bg-white">
|
||||
<form action="{{ route('admin.devices.update', $device->id) }}" method="POST">
|
||||
@csrf @method('PUT')
|
||||
<div class="p-4">
|
||||
<h3 class="text-lg font-medium leading-6 text-gray-900 mb-4">Edit Device</h3>
|
||||
<div class="space-y-4">
|
||||
{{-- Form fields --}}
|
||||
<div>
|
||||
<label for="nama_device_edit_{{$device->id}}" class="block text-sm font-medium text-gray-700">Nama Device</label>
|
||||
<input type="text" name="nama_device" id="nama_device_edit_{{$device->id}}" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm" value="{{ $device->nama_device }}" required>
|
||||
</div>
|
||||
<div>
|
||||
<label for="ip_address_edit_{{$device->id}}" class="block text-sm font-medium text-gray-700">IP Address</label>
|
||||
<input type="text" name="ip_address" id="ip_address_edit_{{$device->id}}" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm" value="{{ $device->ip_address }}" placeholder="Contoh: 192.168.1.101">
|
||||
</div>
|
||||
<div>
|
||||
<label for="id_kelas_edit_{{$device->id}}" class="block text-sm font-medium text-gray-700">Kelas</label>
|
||||
<select name="id_kelas" id="id_kelas_edit_{{$device->id}}" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm" required>
|
||||
@foreach($kelas as $item) <option value="{{ $item->id }}" {{ $device->id_kelas == $item->id ? 'selected' : '' }}>{{ $item->nama_kelas }}</option> @endforeach
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label for="id_ruangan_edit_{{$device->id}}" class="block text-sm font-medium text-gray-700">Ruangan</label>
|
||||
<select name="id_ruangan" id="id_ruangan_edit_{{$device->id}}" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm" required>
|
||||
@foreach($ruangan as $item) <option value="{{ $item->id }}" {{ $device->id_ruangan == $item->id ? 'selected' : '' }}>{{ $item->nama_ruangan }}</option> @endforeach
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6 flex justify-end space-x-2">
|
||||
<button type="button" onclick="closeModal('editModal-{{$device->id}}')" class="px-4 py-2 bg-gray-200 text-gray-800 rounded-md">Batal</button>
|
||||
<button type="submit" class="px-4 py-2 bg-indigo-600 text-white rounded-md">Perbarui</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
<script>
|
||||
function openModal(modalId) { document.getElementById(modalId).style.display = 'block'; }
|
||||
function closeModal(modalId) { document.getElementById(modalId).style.display = 'none'; }
|
||||
window.setTimeout(() => document.querySelectorAll('[id^="toast-"]').forEach(toast => toast.remove()), 5000);
|
||||
function confirmDelete(id, name) {
|
||||
Swal.fire({
|
||||
title: 'Hapus Device?',
|
||||
html: `Anda akan menghapus device: <strong>${name}</strong>`,
|
||||
icon: 'warning',
|
||||
showCancelButton: true,
|
||||
confirmButtonColor: '#d33',
|
||||
confirmButtonText: 'Ya, Hapus!',
|
||||
}).then((result) => result.isConfirmed && document.getElementById(`delete-form-${id}`).submit());
|
||||
}
|
||||
</script>
|
||||
@endsection
|
|
@ -13,62 +13,48 @@
|
|||
|
||||
<!-- Grid Layout Utama -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
|
||||
<!-- Kolom Kiri: Filter & Tabel Presensi -->
|
||||
<div class="lg:col-span-2 space-y-6">
|
||||
|
||||
<!-- Card untuk Filter -->
|
||||
<div class="bg-white p-6 rounded-xl shadow-md border border-gray-200">
|
||||
<h2 class="text-xl font-semibold text-gray-700 mb-4">Filter Data</h2>
|
||||
|
||||
{{-- Bungkus filter dalam form GET agar bisa dibaca controller --}}
|
||||
<form id="filterForm" action="{{ url()->current() }}" method="GET">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<!-- Filter Jurusan -->
|
||||
<div>
|
||||
<label for="jurusan" class="block text-sm font-medium text-gray-600 mb-1">Pilih Jurusan</label>
|
||||
{{-- Tambahkan atribut 'name' agar bisa dibaca oleh Request --}}
|
||||
<select id="jurusan" name="jurusan_id" class="filter-input w-full p-2 border border-gray-300 rounded-lg shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition">
|
||||
<option value="all">Semua Jurusan</option>
|
||||
@foreach($jurusans as $jurusan)
|
||||
{{-- Jaga agar nilai filter tetap terpilih setelah halaman di-refresh --}}
|
||||
<option value="{{ $jurusan->id }}" {{ request('jurusan_id') == $jurusan->id ? 'selected' : '' }}>
|
||||
{{ $jurusan->nama_jurusan }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<!-- Filter Kelas -->
|
||||
<div>
|
||||
<label for="kelas" class="block text-sm font-medium text-gray-600 mb-1">Pilih Kelas</label>
|
||||
<select id="kelas" name="kelas_id" class="filter-input w-full p-2 border border-gray-300 rounded-lg shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition">
|
||||
<option value="all">Semua Kelas</option>
|
||||
@foreach($kelases as $kelas)
|
||||
<option value="{{ $kelas->id }}" {{ request('kelas_id') == $kelas->id ? 'selected' : '' }}>
|
||||
{{ $kelas->nama_kelas }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<!-- Filter Ruangan/Device -->
|
||||
<div>
|
||||
<label for="device" class="block text-sm font-medium text-gray-600 mb-1">Pilih Device</label>
|
||||
<select id="device" name="device_id" class="filter-input w-full p-2 border border-gray-300 rounded-lg shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition">
|
||||
<option value="all">Semua Device</option>
|
||||
@foreach($devices as $device)
|
||||
<option value="{{ $device->id }}" {{ request('device_id') == $device->id ? 'selected' : '' }}>
|
||||
{{ $device->nama_device }}
|
||||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<!-- Filter Tanggal -->
|
||||
<div>
|
||||
<label for="tanggal" class="block text-sm font-medium text-gray-600 mb-1">Pilih Tanggal</label>
|
||||
<input type="date" id="tanggal" name="tanggal" value="{{ request('tanggal', date('Y-m-d')) }}" class="filter-input w-full p-2 border border-gray-300 rounded-lg shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition">
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<!-- Filter Jurusan -->
|
||||
<div>
|
||||
<label for="jurusan" class="block text-sm font-medium text-gray-600 mb-1">Pilih Jurusan</label>
|
||||
<select id="jurusan" class="filter-input w-full p-2 border border-gray-300 rounded-lg shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition">
|
||||
<option value="all">Semua Jurusan</option>
|
||||
@foreach($jurusans as $jurusan)
|
||||
<option value="{{ $jurusan->id }}">{{ $jurusan->nama_jurusan }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
<!-- Filter Kelas -->
|
||||
<div>
|
||||
<label for="kelas" class="block text-sm font-medium text-gray-600 mb-1">Pilih Kelas</label>
|
||||
<select id="kelas" class="filter-input w-full p-2 border border-gray-300 rounded-lg shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition">
|
||||
<option value="all">Semua Kelas</option>
|
||||
@foreach($kelases as $kelas)
|
||||
<option value="{{ $kelas->id }}">{{ $kelas->nama_kelas }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<!-- Filter Ruangan/Device -->
|
||||
<div>
|
||||
<label for="device" class="block text-sm font-medium text-gray-600 mb-1">Pilih Device</label>
|
||||
<select id="device" class="filter-input w-full p-2 border border-gray-300 rounded-lg shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition">
|
||||
<option value="all" data-ip="">Semua Device</option>
|
||||
@foreach($devices as $device)
|
||||
<option value="{{ $device->id }}" data-ip="{{ $device->ip_address }}">{{ $device->nama_device }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
<!-- Filter Tanggal -->
|
||||
<div>
|
||||
<label for="tanggal" class="block text-sm font-medium text-gray-600 mb-1">Pilih Tanggal</label>
|
||||
<input type="date" id="tanggal" value="{{ date('Y-m-d') }}" class="filter-input w-full p-2 border border-gray-300 rounded-lg shadow-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 transition">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card untuk Tabel Presensi -->
|
||||
|
@ -78,41 +64,17 @@
|
|||
<table class="w-full text-sm text-left text-gray-500">
|
||||
<thead class="text-xs text-gray-700 uppercase bg-gray-100">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 rounded-l-lg">No</th>
|
||||
<th scope="col" class="px-6 py-3">Nama Siswa</th>
|
||||
<th scope="col" class="px-6 py-3">Jurusan</th>
|
||||
<th scope="col" class="px-6 py-3">Kelas</th>
|
||||
<th scope="col" class="px-6 py-3">Waktu Presensi</th>
|
||||
<th scope="col" class="px-6 py-3">Ruangan</th>
|
||||
<th scope="col" class="px-6 py-3 rounded-r-lg">Status</th>
|
||||
<th class="px-6 py-3 rounded-l-lg">No</th>
|
||||
<th class="px-6 py-3">Nama Siswa</th>
|
||||
<th class="px-6 py-3">Jurusan</th>
|
||||
<th class="px-6 py-3">Kelas</th>
|
||||
<th class="px-6 py-3">Waktu Presensi</th>
|
||||
<th class="px-6 py-3">Ruangan</th>
|
||||
<th class="px-6 py-3 rounded-r-lg">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="attendance-table-body">
|
||||
@forelse ($absensi as $key => $item)
|
||||
<tr class="bg-white border-b hover:bg-gray-50">
|
||||
<td class="px-6 py-4 font-medium text-gray-900">{{ $key + 1 }}</td>
|
||||
<td class="px-6 py-4">{{ $item->siswa->nama_siswa ?? 'Siswa Dihapus' }}</td>
|
||||
<td class="px-6 py-4">{{ $item->siswa->jurusan->nama_jurusan ?? 'N/A' }}</td>
|
||||
<td class="px-6 py-4">{{ $item->siswa->kelas->nama_kelas ?? 'N/A' }}</td>
|
||||
<td class="px-6 py-4">{{ \Carbon\Carbon::parse($item->waktu)->format('H:i:s') }}</td>
|
||||
<td class="px-6 py-4">{{ $item->devices->nama_device ?? 'Device Dihapus' }}</td>
|
||||
<td class="px-6 py-4">
|
||||
@if(strtolower($item->status) == 'hadir')
|
||||
<span class="px-2 py-1 font-semibold leading-tight text-green-700 bg-green-100 rounded-full">Hadir</span>
|
||||
@elseif(strtolower($item->status) == 'terlambat')
|
||||
<span class="px-2 py-1 font-semibold leading-tight text-yellow-700 bg-yellow-100 rounded-full">Terlambat</span>
|
||||
@else
|
||||
<span class="px-2 py-1 font-semibold leading-tight text-red-700 bg-red-100 rounded-full">{{ ucfirst($item->status) }}</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr class="bg-white border-b">
|
||||
<td colspan="7" class="px-6 py-4 text-center text-gray-500">
|
||||
Tidak ada data yang cocok dengan filter yang dipilih.
|
||||
</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
<tr><td colspan="7" class="p-4 text-center text-gray-500">Memuat data...</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
@ -131,51 +93,135 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const filterInputs = document.querySelectorAll('.filter-input');
|
||||
const filterForm = document.getElementById('filterForm');
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
const filterInputs = document.querySelectorAll('.filter-input');
|
||||
const tableBody = document.getElementById('attendance-table-body');
|
||||
const laporanTitle = document.getElementById('laporan-title');
|
||||
const deviceSelect = document.getElementById("device");
|
||||
const cameraFeed = document.getElementById("cameraFeed");
|
||||
const cameraStatus = document.getElementById("cameraStatus");
|
||||
const cameraTitle = document.getElementById("camera-title");
|
||||
|
||||
// Otomatis submit form ketika nilai filter diubah
|
||||
filterInputs.forEach(input => {
|
||||
input.addEventListener('change', function() {
|
||||
filterForm.submit();
|
||||
});
|
||||
let fetchInterval; // Variabel untuk menyimpan interval
|
||||
|
||||
function updateCameraFeed() {
|
||||
const selectedOption = deviceSelect.options[deviceSelect.selectedIndex];
|
||||
const deviceIp = selectedOption.dataset.ip;
|
||||
const deviceName = selectedOption.text;
|
||||
|
||||
cameraFeed.classList.add('hidden');
|
||||
cameraFeed.src = '';
|
||||
|
||||
if (!deviceIp || deviceSelect.value === 'all') {
|
||||
cameraTitle.innerText = "Live Camera Feed";
|
||||
cameraStatus.innerHTML = '<p class="flex items-center justify-center h-full">Pilih device untuk melihat live feed</p>';
|
||||
cameraStatus.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
cameraTitle.innerText = `Live: ${deviceName}`;
|
||||
cameraStatus.innerHTML = `<div class="flex flex-col items-center justify-center h-full"><svg class="animate-spin h-8 w-8 text-blue-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path></svg><p class="mt-2">Menghubungkan...</p></div>`;
|
||||
cameraStatus.style.display = 'block';
|
||||
|
||||
// PERBAIKAN: Gunakan variabel deviceIp yang dinamis
|
||||
cameraFeed.src = `http://${deviceIp}:5000/video_feed`;
|
||||
}
|
||||
|
||||
cameraFeed.onload = function() {
|
||||
cameraFeed.classList.remove("hidden");
|
||||
cameraStatus.style.display = "none";
|
||||
};
|
||||
|
||||
cameraFeed.onerror = function() {
|
||||
cameraFeed.classList.add("hidden");
|
||||
cameraStatus.style.display = 'block';
|
||||
cameraStatus.innerHTML = '<p class="flex items-center justify-center h-full text-red-500 font-semibold">Kamera tidak aktif atau gagal terhubung.</p>';
|
||||
};
|
||||
|
||||
async function fetchAttendanceData() {
|
||||
const jurusanId = document.getElementById('jurusan').value;
|
||||
const kelasId = document.getElementById('kelas').value;
|
||||
const deviceId = deviceSelect.value;
|
||||
const tanggal = document.getElementById('tanggal').value;
|
||||
|
||||
laporanTitle.innerText = `Laporan Kehadiran ${formatIndonesianDate(tanggal)}`;
|
||||
const url = `/api/laporan-absensi?tanggal=${tanggal}&jurusan_id=${jurusanId}&kelas_id=${kelasId}&device_id=${deviceId}`;
|
||||
|
||||
try {
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||||
const data = await response.json();
|
||||
renderTable(data);
|
||||
} catch (error) {
|
||||
console.error('Error fetching data:', error);
|
||||
tableBody.innerHTML = `<tr><td colspan="7" class="p-4 text-center text-red-500">Gagal memuat data.</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderTable(data) {
|
||||
tableBody.innerHTML = '';
|
||||
if (data.length === 0) {
|
||||
tableBody.innerHTML = `<tr><td colspan="7" class="p-4 text-center text-gray-500">Tidak ada data absensi untuk filter ini.</td></tr>`;
|
||||
return;
|
||||
}
|
||||
data.forEach(item => {
|
||||
let statusBadge = '';
|
||||
switch (item.status) {
|
||||
case 'hadir': statusBadge = `<span class="px-2 py-1 font-semibold leading-tight text-green-700 bg-green-100 rounded-full">Hadir</span>`; break;
|
||||
case 'terlambat': statusBadge = `<span class="px-2 py-1 font-semibold leading-tight text-yellow-700 bg-yellow-100 rounded-full">Terlambat</span>`; break;
|
||||
default: statusBadge = `<span class="px-2 py-1 font-semibold leading-tight text-red-700 bg-red-100 rounded-full">${item.status}</span>`;
|
||||
}
|
||||
const row = `
|
||||
<tr class="bg-white border-b hover:bg-gray-50">
|
||||
<td class="px-6 py-4 font-medium text-gray-900">${item.no}</td>
|
||||
<td class="px-6 py-4">${item.nama_siswa}</td>
|
||||
<td class="px-6 py-4">${item.jurusan}</td>
|
||||
<td class="px-6 py-4">${item.kelas}</td>
|
||||
<td class="px-6 py-4">${item.waktu}</td>
|
||||
<td class="px-6 py-4">${item.ruangan}</td>
|
||||
<td class="px-6 py-4">${statusBadge}</td>
|
||||
</tr>`;
|
||||
tableBody.insertAdjacentHTML('beforeend', row);
|
||||
});
|
||||
}
|
||||
|
||||
// Bagian untuk judul dan kamera
|
||||
const laporanTitle = document.getElementById("laporan-title");
|
||||
const tanggalInput = document.getElementById("tanggal");
|
||||
const cameraFeed = document.getElementById("cameraFeed");
|
||||
const cameraStatus = document.getElementById("cameraStatus");
|
||||
const cameraTitle = document.getElementById("camera-title");
|
||||
const deviceSelect = document.getElementById("device");
|
||||
const devicesData = @json($devices);
|
||||
function formatIndonesianDate(dateString) {
|
||||
if (!dateString) return '';
|
||||
const days = ['Minggu', 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu'];
|
||||
const months = ['Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni', 'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember'];
|
||||
const date = new Date(dateString + 'T00:00:00');
|
||||
return `${date.getDate()} ${months[date.getMonth()]} ${date.getFullYear()} (${days[date.getDay()]})`;
|
||||
}
|
||||
|
||||
function formatIndonesianDate(dateString) {
|
||||
if (!dateString) return 'Hari Ini';
|
||||
const days = ['Minggu', 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu'];
|
||||
const months = ['Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni', 'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember'];
|
||||
const parts = dateString.match(/(\d{4})-(\d{2})-(\d{2})/);
|
||||
if (!parts) return 'Hari Ini';
|
||||
const date = new Date(parts[1], parts[2] - 1, parts[3]);
|
||||
return `${date.getDate()} ${months[date.getMonth()]} ${date.getFullYear()} (${days[date.getDay()]})`;
|
||||
}
|
||||
// Fungsi baru untuk memulai/mengatur ulang interval
|
||||
function startAutoRefresh() {
|
||||
// Hapus interval lama jika ada, untuk mencegah duplikasi
|
||||
clearInterval(fetchInterval);
|
||||
|
||||
// Panggil data segera saat filter diubah
|
||||
fetchAttendanceData();
|
||||
|
||||
// Atur interval baru untuk memanggil data setiap 15 detik
|
||||
fetchInterval = setInterval(fetchAttendanceData, 15000); // 15000 ms = 15 detik
|
||||
}
|
||||
|
||||
function updateDynamicElements() {
|
||||
laporanTitle.innerText = `Laporan Kehadiran ${formatIndonesianDate(tanggalInput.value)}`;
|
||||
}
|
||||
|
||||
// Jalankan saat halaman pertama kali dimuat
|
||||
updateDynamicElements();
|
||||
|
||||
// Sisa JavaScript untuk kamera...
|
||||
// ... (Kode untuk kamera sama seperti sebelumnya) ...
|
||||
filterInputs.forEach(input => {
|
||||
input.addEventListener('change', startAutoRefresh);
|
||||
});
|
||||
|
||||
deviceSelect.addEventListener('change', function() {
|
||||
// startAutoRefresh() sudah dipanggil oleh event listener di atas
|
||||
updateCameraFeed();
|
||||
});
|
||||
|
||||
// === INISIALISASI ===
|
||||
// Panggil fungsi saat halaman pertama kali dimuat untuk memulai semuanya
|
||||
startAutoRefresh();
|
||||
});
|
||||
</script>
|
||||
@endsection
|
||||
|
|
|
@ -3,26 +3,59 @@
|
|||
@section('title', 'Smart School | Data Siswa')
|
||||
|
||||
@section('content')
|
||||
<div class="container mx-auto px-4 py-6">
|
||||
<div class="container mx-auto px-4 py-6 relative">
|
||||
|
||||
<!-- Toast Notification Container -->
|
||||
<div class="absolute top-4 right-4 space-y-3 z-50">
|
||||
<!-- Toast for Success -->
|
||||
@if (session('success'))
|
||||
<div id="toast-success" class="flex items-center w-full max-w-xs p-4 mb-4 text-gray-500 bg-white rounded-lg shadow-lg" role="alert">
|
||||
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-green-500 bg-green-100 rounded-lg">
|
||||
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 8.207-4 4a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L9 10.586l3.293-3.293a1 1 0 0 1 1.414 1.414Z"/>
|
||||
</svg>
|
||||
<span class="sr-only">Check icon</span>
|
||||
</div>
|
||||
<div class="ml-3 text-sm font-normal">{{ session('success') }}</div>
|
||||
<button type="button" class="ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex items-center justify-center h-8 w-8" data-dismiss-target="#toast-success" aria-label="Close">
|
||||
<span class="sr-only">Close</span>
|
||||
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<!-- Toast for General Errors -->
|
||||
@if (session('error'))
|
||||
<div id="toast-danger" class="flex items-center w-full max-w-xs p-4 mb-4 text-gray-500 bg-white rounded-lg shadow-lg" role="alert">
|
||||
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-red-500 bg-red-100 rounded-lg">
|
||||
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20"><path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5ZM10 15a1 1 0 1 1 0-2 1 1 0 0 1 0 2Zm1-4a1 1 0 0 1-2 0V6a1 1 0 0 1 2 0v5Z"/></svg>
|
||||
<span class="sr-only">Error icon</span>
|
||||
</div>
|
||||
<div class="ml-3 text-sm font-normal">{{ session('error') }}</div>
|
||||
<button type="button" class="ml-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex items-center justify-center h-8 w-8" data-dismiss-target="#toast-danger" aria-label="Close">
|
||||
<span class="sr-only">Close</span>
|
||||
<svg class="w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14"><path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-2xl shadow-xl overflow-hidden">
|
||||
<!-- Header Card -->
|
||||
<div class="bg-gradient-to-br from-indigo-600 to-purple-600 px-8 py-6">
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-between">
|
||||
<div class="mb-4 md:mb-0">
|
||||
<h1 class="text-2xl font-bold text-white flex items-center">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 mr-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 mr-3" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" /></svg>
|
||||
Data Siswa
|
||||
</h1>
|
||||
<p class="text-indigo-100 mt-1">Total {{ $siswa->total() }} siswa terdaftar</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{{ route('admin.siswa.create') }}"
|
||||
class="flex items-center px-4 py-2 bg-white text-indigo-600 rounded-lg shadow hover:bg-gray-50 transition">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
<a href="{{ route('admin.siswa.create') }}" class="flex items-center px-4 py-2 bg-white text-indigo-600 rounded-lg shadow hover:bg-gray-50 transition">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" /></svg>
|
||||
Tambah Siswa
|
||||
</a>
|
||||
</div>
|
||||
|
@ -33,23 +66,18 @@ class="flex items-center px-4 py-2 bg-white text-indigo-600 rounded-lg shadow ho
|
|||
<div class="p-6">
|
||||
<!-- Search and Filter -->
|
||||
<div class="mb-6 flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div class="relative w-full md:w-64">
|
||||
<form action="{{ route('admin.siswa.index') }}" method="GET">
|
||||
<input type="text" name="search" placeholder="Cari siswa..."
|
||||
value="{{ request('search') }}"
|
||||
{{-- Form untuk Search, Kelas, dan Jurusan --}}
|
||||
<form action="{{ route('admin.siswa.index') }}" method="GET" class="flex flex-col md:flex-row md:items-center gap-4 w-full">
|
||||
<div class="relative w-full md:w-64">
|
||||
<input type="text" name="search" placeholder="Cari siswa..." value="{{ request('search') }}"
|
||||
class="w-full pl-10 pr-4 py-2 rounded-lg border border-gray-300 focus:border-indigo-500 focus:ring focus:ring-indigo-200 transition">
|
||||
<div class="absolute left-3 top-2.5 text-gray-400">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<form action="{{ route('admin.siswa.index') }}" method="GET" class="flex gap-2">
|
||||
<select name="kelas" onchange="this.form.submit()"
|
||||
class="rounded-lg border-gray-300 shadow-sm focus:border-indigo-500 focus:ring focus:ring-indigo-200 transition">
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<select name="kelas" class="rounded-lg border-gray-300 shadow-sm focus:border-indigo-500 focus:ring focus:ring-indigo-200 transition">
|
||||
<option value="">Semua Kelas</option>
|
||||
@foreach($kelas as $item)
|
||||
<option value="{{ $item->id }}" {{ request('kelas') == $item->id ? 'selected' : '' }}>
|
||||
|
@ -58,8 +86,7 @@ class="rounded-lg border-gray-300 shadow-sm focus:border-indigo-500 focus:ring f
|
|||
@endforeach
|
||||
</select>
|
||||
|
||||
<select name="jurusan" onchange="this.form.submit()"
|
||||
class="rounded-lg border-gray-300 shadow-sm focus:border-indigo-500 focus:ring focus:ring-indigo-200 transition">
|
||||
<select name="jurusan" class="rounded-lg border-gray-300 shadow-sm focus:border-indigo-500 focus:ring focus:ring-indigo-200 transition">
|
||||
<option value="">Semua Jurusan</option>
|
||||
@foreach($jurusan as $item)
|
||||
<option value="{{ $item->id }}" {{ request('jurusan') == $item->id ? 'selected' : '' }}>
|
||||
|
@ -67,15 +94,16 @@ class="rounded-lg border-gray-300 shadow-sm focus:border-indigo-500 focus:ring f
|
|||
</option>
|
||||
@endforeach
|
||||
</select>
|
||||
|
||||
|
||||
<button type="submit" class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition">Filter</button>
|
||||
|
||||
@if(request('search') || request('kelas') || request('jurusan'))
|
||||
<a href="{{ route('admin.siswa.index') }}"
|
||||
class="px-3 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition">
|
||||
<a href="{{ route('admin.siswa.index') }}" class="px-3 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transition">
|
||||
Reset
|
||||
</a>
|
||||
@endif
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Students Table -->
|
||||
|
@ -83,24 +111,12 @@ class="px-3 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transiti
|
|||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Foto
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Nama Siswa
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
NISN
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Kelas/Jurusan
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Kontak
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Aksi
|
||||
</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Foto</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Nama Siswa</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">NISN</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Kelas/Jurusan</th>
|
||||
<th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Kontak</th>
|
||||
<th scope="col" class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
|
@ -108,27 +124,21 @@ class="px-3 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transiti
|
|||
<tr class="hover:bg-gray-50 transition">
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="flex-shrink-0 h-10 w-10 rounded-full overflow-hidden border border-gray-200">
|
||||
@if($item->foto_siswa)
|
||||
<img class="h-full w-full object-cover" src="{{ asset('storage/' . $item->foto_siswa) }}" alt="Foto siswa">
|
||||
{{-- Mengambil foto pertama dari relasi 'fotos' --}}
|
||||
@if($item->fotos && $item->fotos->isNotEmpty())
|
||||
<img class="h-full w-full object-cover" src="{{ asset('storage/' . $item->fotos->first()->path) }}" alt="Foto {{ $item->nama_siswa }}">
|
||||
@else
|
||||
<div class="bg-gray-200 h-full w-full flex items-center justify-center text-gray-400">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" /></svg>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm font-medium text-gray-900">{{ $item->nama_siswa }}</div>
|
||||
<div class="text-sm text-gray-500">
|
||||
{{ $item->jenis_kelamin == 'L' ? 'Laki-laki' : 'Perempuan' }} •
|
||||
{{ \Carbon\Carbon::parse($item->tanggal_lahir)->age }} tahun
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
|
||||
{{ $item->nisn }}
|
||||
<div class="text-sm text-gray-500">{{ $item->jenis_kelamin == 'L' ? 'Laki-laki' : 'Perempuan' }} • {{ \Carbon\Carbon::parse($item->tanggal_lahir)->age }} tahun</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ $item->nisn }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<div class="text-sm text-gray-900">{{ $item->kelas->nama_kelas ?? '-' }}</div>
|
||||
<div class="text-sm text-gray-500">{{ $item->jurusan->nama_jurusan ?? '-' }}</div>
|
||||
|
@ -139,20 +149,14 @@ class="px-3 py-2 bg-gray-100 text-gray-700 rounded-lg hover:bg-gray-200 transiti
|
|||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<div class="flex justify-end space-x-2">
|
||||
<a href="{{ route('admin.siswa.edit', $item->id) }}"
|
||||
class="text-indigo-600 hover:text-indigo-900 p-1 rounded hover:bg-indigo-50 transition">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
|
||||
</svg>
|
||||
<a href="{{ route('admin.siswa.edit', $item->id) }}" class="text-indigo-600 hover:text-indigo-900 p-1 rounded hover:bg-indigo-50 transition" title="Edit">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /></svg>
|
||||
</a>
|
||||
<form action="{{ route('admin.siswa.destroy', $item->id) }}" method="POST" class="inline">
|
||||
<form id="delete-form-{{ $item->id }}" action="{{ route('admin.siswa.destroy', $item->id) }}" method="POST" class="inline">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="button" onclick="confirmDelete('{{ $item->id }}', '{{ $item->nama_siswa }}')"
|
||||
class="text-red-600 hover:text-red-900 p-1 rounded hover:bg-red-50 transition">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
|
||||
</svg>
|
||||
<button type="button" onclick="confirmDelete('{{ $item->id }}', '{{ $item->nama_siswa }}')" class="text-red-600 hover:text-red-900 p-1 rounded hover:bg-red-50 transition" title="Hapus">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -160,9 +164,7 @@ class="text-red-600 hover:text-red-900 p-1 rounded hover:bg-red-50 transition">
|
|||
</tr>
|
||||
@empty
|
||||
<tr>
|
||||
<td colspan="6" class="px-6 py-4 text-center text-gray-500">
|
||||
Tidak ada data siswa ditemukan
|
||||
</td>
|
||||
<td colspan="6" class="px-6 py-4 text-center text-gray-500">Tidak ada data siswa ditemukan.</td>
|
||||
</tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
|
@ -170,8 +172,8 @@ class="text-red-600 hover:text-red-900 p-1 rounded hover:bg-red-50 transition">
|
|||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="mt-4">
|
||||
{{ $siswa->links() }}
|
||||
<div class="p-6">
|
||||
{{ $siswa->appends(request()->query())->links() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -181,7 +183,18 @@ class="text-red-600 hover:text-red-900 p-1 rounded hover:bg-red-50 transition">
|
|||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
|
||||
|
||||
<script>
|
||||
// Confirm delete function
|
||||
// Hilangkan toast setelah 5 detik
|
||||
window.setTimeout(function() {
|
||||
const toastElements = document.querySelectorAll('[id^="toast-"]');
|
||||
toastElements.forEach(function(toast) {
|
||||
const closeButton = toast.querySelector('[data-dismiss-target]');
|
||||
if (closeButton) {
|
||||
closeButton.click();
|
||||
}
|
||||
});
|
||||
}, 5000);
|
||||
|
||||
// Konfirmasi hapus dengan SweetAlert2
|
||||
function confirmDelete(id, name) {
|
||||
Swal.fire({
|
||||
title: 'Hapus Siswa?',
|
||||
|
@ -194,10 +207,9 @@ function confirmDelete(id, name) {
|
|||
cancelButtonText: 'Batal'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
// Submit the form
|
||||
document.querySelector(`form[action="{{ route('admin.siswa.destroy', '') }}/${id}"]`).submit();
|
||||
document.getElementById(`delete-form-${id}`).submit();
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@endsection
|
||||
@endsection
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<?php
|
||||
|
||||
use App\Http\Controllers\Api\AbsensiLaporanController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
use App\Http\Controllers\BelController;
|
||||
use App\Http\Controllers\PresensiController;
|
||||
|
@ -9,7 +10,7 @@
|
|||
use App\Http\Controllers\Api\DevicesApiController;
|
||||
use App\Http\Controllers\Api\SiswaApiController;
|
||||
use App\Http\Controllers\Api\AbsensiSiswaApiController;
|
||||
|
||||
use App\Http\Controllers\Api\DeviceStudentController;
|
||||
|
||||
Route::prefix('bel')->group(function () {
|
||||
Route::post('/ring', [BelController::class, 'ring'])->name('api.bel.ring');
|
||||
|
@ -50,4 +51,8 @@
|
|||
Route::get('/siswa', [SiswaApiController::class, 'index']);
|
||||
Route::get('/devices', [DevicesApiController::class, 'index']);
|
||||
|
||||
Route::post('/absensi-siswa', [AbsensiSiswaApiController::class, 'store']);
|
||||
Route::post('/absensi-siswa', [AbsensiSiswaApiController::class, 'store']);
|
||||
|
||||
Route::get('/devices/{device}/students', [DeviceStudentController::class, 'index']);
|
||||
|
||||
Route::get('/laporan-absensi', AbsensiLaporanController::class);
|
|
@ -16,6 +16,7 @@
|
|||
use App\Http\Controllers\AnnouncementController;
|
||||
use App\Http\Controllers\RuanganController;
|
||||
use App\Http\Controllers\BellHistoryController;
|
||||
use App\Http\Controllers\DevicesController;
|
||||
use App\Models\AbsensiGuru;
|
||||
use App\Models\AbsensiSiswa;
|
||||
|
||||
|
@ -91,6 +92,16 @@
|
|||
'update' => 'admin.ruangan.update',
|
||||
'destroy' => 'admin.ruangan.destroy',
|
||||
]);
|
||||
|
||||
Route::resource('devices', DevicesController::class)->names([
|
||||
'index' => 'admin.devices.index',
|
||||
'create' => 'admin.devices.create',
|
||||
'store' => 'admin.devices.store',
|
||||
'edit' => 'admin.devices.edit',
|
||||
'update' => 'admin.devices.update',
|
||||
'destroy' => 'admin.devices.destroy',
|
||||
]);
|
||||
|
||||
// Presensi Siswa
|
||||
Route::controller(AbsensiSiswaController::class)->group(function () {
|
||||
Route::get('/presensi/siswa', 'index')->name('admin.presensi.siswa');
|
||||
|
|
Loading…
Reference in New Issue