1010 lines
46 KiB
PHP
1010 lines
46 KiB
PHP
<!DOCTYPE html>
|
|
<html lang="id">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Dashboard - Smart Rack Security</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script src="https://unpkg.com/feather-icons"></script>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
<style>
|
|
body { font-family: 'Inter', sans-serif; }
|
|
.font-display { font-family: 'Space Grotesk', sans-serif; }
|
|
.glass { backdrop-filter: blur(16px); background: rgba(255, 255, 255, 0.9); border: 1px solid rgba(255, 255, 255, 0.2); }
|
|
.animate-pulse-slow { animation: pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite; }
|
|
.animate-bounce-slow { animation: bounce 2s infinite; }
|
|
|
|
/* LED & Buzzer Animations */
|
|
@keyframes led-blink {
|
|
0%, 100% { opacity: 1; box-shadow: 0 0 12px 4px currentColor; }
|
|
50% { opacity: 0.3; box-shadow: none; }
|
|
}
|
|
@keyframes buzzer-wave {
|
|
0% { transform: scale(1); opacity: 1; }
|
|
100% { transform: scale(2.5); opacity: 0; }
|
|
}
|
|
.led-on-red {
|
|
animation: led-blink 0.6s ease-in-out infinite;
|
|
color: #ef4444;
|
|
}
|
|
.led-on-green {
|
|
box-shadow: 0 0 12px 4px #22c55e;
|
|
color: #22c55e;
|
|
}
|
|
.led-off {
|
|
color: #6b7280;
|
|
opacity: 0.4;
|
|
}
|
|
.buzzer-active::before {
|
|
content: '';
|
|
position: absolute;
|
|
inset: 0;
|
|
border-radius: 50%;
|
|
border: 3px solid #ef4444;
|
|
animation: buzzer-wave 1s ease-out infinite;
|
|
}
|
|
.buzzer-active::after {
|
|
content: '';
|
|
position: absolute;
|
|
inset: 0;
|
|
border-radius: 50%;
|
|
border: 3px solid #ef4444;
|
|
animation: buzzer-wave 1s ease-out 0.4s infinite;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body class="bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-100 font-sans overflow-x-hidden">
|
|
|
|
<div class="flex h-screen overflow-hidden">
|
|
|
|
<!-- OVERLAY untuk mobile -->
|
|
<div id="sidebar-overlay" class="fixed inset-0 bg-black/50 z-30 hidden md:hidden" onclick="toggleSidebar()"></div>
|
|
|
|
<!-- SIDEBAR -->
|
|
<aside id="sidebar" class="fixed md:static z-40 top-0 left-0 h-full w-72 bg-gradient-to-b from-slate-900 via-gray-900 to-slate-800 text-gray-200 shadow-2xl p-6 transform -translate-x-full md:translate-x-0 transition-all duration-300 border-r border-gray-700/50 overflow-y-auto">
|
|
|
|
<!-- LOGO -->
|
|
<div class="flex items-center gap-4 mb-12 p-3 bg-gradient-to-r from-indigo-600/20 to-purple-600/20 rounded-2xl border border-indigo-500/20">
|
|
<div class="w-12 h-12 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center shadow-lg">
|
|
<i data-feather="shield" class="w-6 h-6 text-white"></i>
|
|
</div>
|
|
<div>
|
|
<h2 class="text-xl font-display font-bold text-white leading-none">Smart Rack</h2>
|
|
<p class="text-sm text-indigo-300">Security System</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- MENU -->
|
|
<nav class="space-y-3">
|
|
<a href="/" class="group flex items-center gap-4 px-4 py-3 rounded-xl hover:bg-gradient-to-r hover:from-slate-700/50 hover:to-slate-600/50 hover:text-white transition-all duration-300 text-gray-300">
|
|
<div class="w-10 h-10 bg-gray-700/50 group-hover:bg-indigo-500/20 rounded-lg flex items-center justify-center transition-all duration-300">
|
|
<i data-feather="arrow-left" class="w-5 h-5"></i>
|
|
</div>
|
|
<span class="font-medium">Home</span>
|
|
</a>
|
|
|
|
<a href="/dashboard" class="group flex items-center gap-4 px-4 py-3 rounded-xl bg-gradient-to-r from-indigo-600 to-purple-600 text-white shadow-lg">
|
|
<div class="w-10 h-10 bg-white/20 rounded-lg flex items-center justify-center">
|
|
<i data-feather="home" class="w-5 h-5"></i>
|
|
</div>
|
|
<span class="font-medium">Dashboard</span>
|
|
</a>
|
|
|
|
<a href="/monitoring" class="group flex items-center gap-4 px-4 py-3 rounded-xl hover:bg-gradient-to-r hover:from-slate-700/50 hover:to-slate-600/50 hover:text-white transition-all duration-300 text-gray-300">
|
|
<div class="w-10 h-10 bg-gray-700/50 group-hover:bg-blue-500/20 rounded-lg flex items-center justify-center transition-all duration-300">
|
|
<i data-feather="activity" class="w-5 h-5"></i>
|
|
</div>
|
|
<span class="font-medium">Monitoring Sensor</span>
|
|
</a>
|
|
|
|
<a href="/log" class="group flex items-center gap-4 px-4 py-3 rounded-xl hover:bg-gradient-to-r hover:from-slate-700/50 hover:to-slate-600/50 hover:text-white transition-all duration-300 text-gray-300">
|
|
<div class="w-10 h-10 bg-gray-700/50 group-hover:bg-green-500/20 rounded-lg flex items-center justify-center transition-all duration-300">
|
|
<i data-feather="file-text" class="w-5 h-5"></i>
|
|
</div>
|
|
<span class="font-medium">Log Aktivitas</span>
|
|
</a>
|
|
|
|
<a href="/device" class="group flex items-center gap-4 px-4 py-3 rounded-xl hover:bg-gradient-to-r hover:from-slate-700/50 hover:to-slate-600/50 hover:text-white transition-all duration-300 text-gray-300">
|
|
<div class="w-10 h-10 bg-gray-700/50 group-hover:bg-purple-500/20 rounded-lg flex items-center justify-center transition-all duration-300">
|
|
<i data-feather="cpu" class="w-5 h-5"></i>
|
|
</div>
|
|
<span class="font-medium">Status Device</span>
|
|
</a>
|
|
</nav>
|
|
|
|
<!-- DIVIDER -->
|
|
<div class="border-t border-gray-700/50 my-8"></div>
|
|
|
|
<!-- USER -->
|
|
<div class="flex items-center gap-4 p-4 bg-gradient-to-r from-gray-800/50 to-slate-700/50 rounded-2xl border border-gray-700/30">
|
|
<div class="relative cursor-pointer" onclick="openProfileModal()" title="Ubah foto profil">
|
|
@if(auth()->user()->profile_photo)
|
|
<img src="{{ auth()->user()->profile_photo_url }}" class="w-12 h-12 rounded-xl shadow-lg object-cover hover:opacity-80 transition-opacity">
|
|
@else
|
|
<div class="w-12 h-12 rounded-xl shadow-lg bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center hover:opacity-80 transition-opacity">
|
|
<span class="text-white font-bold text-sm">{{ auth()->user()->initials }}</span>
|
|
</div>
|
|
@endif
|
|
<div class="absolute -bottom-1 -right-1 w-4 h-4 bg-green-500 rounded-full border-2 border-slate-800"></div>
|
|
</div>
|
|
|
|
<div class="flex-1">
|
|
<p class="font-semibold text-white">{{ auth()->user()->name }}</p>
|
|
<p class="text-sm text-gray-400">System Operator</p>
|
|
</div>
|
|
|
|
<a href="{{ route('logout') }}" class="w-10 h-10 bg-red-500/20 hover:bg-red-500/30 rounded-lg flex items-center justify-center text-red-400 hover:text-red-300 transition-all duration-300" title="Logout">
|
|
<i data-feather="log-out" class="w-5 h-5"></i>
|
|
</a>
|
|
</div>
|
|
|
|
</aside>
|
|
|
|
<!-- MAIN -->
|
|
<div class="flex-1 flex flex-col">
|
|
|
|
<!-- TOPBAR -->
|
|
<header class="glass border-b border-white/20 flex justify-between items-center px-4 md:px-8 py-4 md:py-6">
|
|
<div class="flex items-center gap-3">
|
|
<!-- Hamburger button mobile -->
|
|
<button onclick="toggleSidebar()" class="md:hidden w-10 h-10 bg-gray-100 rounded-xl flex items-center justify-center text-gray-600 hover:bg-gray-200 transition-all">
|
|
<i data-feather="menu" class="w-5 h-5"></i>
|
|
</button>
|
|
<div>
|
|
<h1 class="text-lg md:text-2xl font-display font-bold text-gray-800">Dashboard Monitoring</h1>
|
|
<p class="text-gray-600 mt-0.5 text-xs md:text-sm hidden sm:block">Sistem keamanan rak berbasis IoT</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-2 md:gap-6">
|
|
<div id="system-status-badge" class="flex items-center gap-2 {{ $sistemOnline ? 'bg-gradient-to-r from-green-500/10 to-emerald-500/10 border-green-200' : 'bg-gradient-to-r from-red-500/10 to-orange-500/10 border-red-200' }} px-3 py-2 rounded-xl border transition-all duration-500 cursor-default" title="Status koneksi ESP32">
|
|
<div id="system-status-dot" class="w-2 h-2 md:w-3 md:h-3 {{ $sistemOnline ? 'bg-green-500 animate-pulse-slow' : 'bg-red-500' }} rounded-full transition-all duration-500"></div>
|
|
<span id="system-status-text" class="{{ $sistemOnline ? 'text-green-700' : 'text-red-700' }} font-semibold text-xs md:text-sm hidden sm:inline transition-all duration-500">
|
|
{{ $sistemOnline ? 'Sistem Online' : 'Sistem Offline' }}
|
|
</span>
|
|
</div>
|
|
|
|
<div class="flex items-center gap-2">
|
|
<div class="relative cursor-pointer" onclick="openProfileModal()" title="Klik untuk ubah foto profil">
|
|
@if(auth()->user()->profile_photo)
|
|
<img src="{{ auth()->user()->profile_photo_url }}" class="w-9 h-9 md:w-11 md:h-11 rounded-xl shadow-lg object-cover hover:opacity-80 transition-opacity">
|
|
@else
|
|
<div class="w-9 h-9 md:w-11 md:h-11 rounded-xl shadow-lg bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center hover:opacity-80 transition-opacity">
|
|
<span class="text-white font-bold text-xs md:text-sm">{{ auth()->user()->initials }}</span>
|
|
</div>
|
|
@endif
|
|
<div class="absolute -bottom-1 -right-1 w-3 h-3 md:w-4 md:h-4 bg-green-500 rounded-full border-2 border-white"></div>
|
|
</div>
|
|
<div class="hidden sm:block">
|
|
<span class="text-gray-800 font-semibold text-sm">{{ auth()->user()->name }}</span>
|
|
<p class="text-gray-500 text-xs">Online</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
|
|
<!-- CONTENT -->
|
|
<main class="p-4 md:p-8 space-y-6 md:space-y-10 overflow-y-auto">
|
|
|
|
<div class="mb-4 md:mb-8">
|
|
<h2 class="text-2xl md:text-3xl font-display font-bold text-gray-800 mb-1 md:mb-2">Ringkasan Sistem</h2>
|
|
<p class="text-gray-600 text-sm md:text-base">Overview monitoring keamanan rak secara realtime</p>
|
|
</div>
|
|
|
|
<!-- STATISTICS -->
|
|
<div class="grid grid-cols-2 lg:grid-cols-4 gap-3 md:gap-6">
|
|
|
|
<div class="group bg-white p-4 md:p-8 rounded-2xl md:rounded-3xl shadow-lg hover:shadow-2xl hover:scale-105 transition-all duration-300 border border-gray-100 relative overflow-hidden">
|
|
<div class="absolute inset-0 bg-gradient-to-br from-indigo-500/5 to-purple-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
|
<div class="relative z-10">
|
|
<div class="w-10 h-10 md:w-16 md:h-16 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl md:rounded-2xl flex items-center justify-center mb-3 md:mb-4">
|
|
<i data-feather="layers" class="w-5 h-5 md:w-8 md:h-8 text-white"></i>
|
|
</div>
|
|
<p class="text-gray-600 font-medium mb-1 md:mb-2 text-xs md:text-base">Total Sensor</p>
|
|
<h3 class="text-2xl md:text-4xl font-display font-bold text-indigo-600">{{ $totalSensor }}</h3>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="group bg-white p-4 md:p-8 rounded-2xl md:rounded-3xl shadow-lg hover:shadow-2xl hover:scale-105 transition-all duration-300 border border-gray-100 relative overflow-hidden">
|
|
<div class="absolute inset-0 bg-gradient-to-br from-green-500/5 to-emerald-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
|
<div class="relative z-10">
|
|
<div class="w-10 h-10 md:w-16 md:h-16 bg-gradient-to-br from-green-500 to-emerald-600 rounded-xl md:rounded-2xl flex items-center justify-center mb-3 md:mb-4">
|
|
<i data-feather="check-circle" class="w-5 h-5 md:w-8 md:h-8 text-white"></i>
|
|
</div>
|
|
<p class="text-gray-600 font-medium mb-1 md:mb-2 text-xs md:text-base">Sensor Aktif</p>
|
|
<h3 class="text-2xl md:text-4xl font-display font-bold text-green-600">{{ $sensorAktif }}</h3>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="group bg-white p-4 md:p-8 rounded-2xl md:rounded-3xl shadow-lg hover:shadow-2xl hover:scale-105 transition-all duration-300 border border-gray-100 relative overflow-hidden">
|
|
<div class="absolute inset-0 bg-gradient-to-br from-red-500/5 to-pink-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
|
<div class="relative z-10">
|
|
<div class="w-10 h-10 md:w-16 md:h-16 bg-gradient-to-br from-red-500 to-pink-600 rounded-xl md:rounded-2xl flex items-center justify-center mb-3 md:mb-4">
|
|
<i data-feather="alert-triangle" class="w-5 h-5 md:w-8 md:h-8 text-white"></i>
|
|
</div>
|
|
<p class="text-gray-600 font-medium mb-1 md:mb-2 text-xs md:text-base">Peringatan</p>
|
|
<h3 class="text-2xl md:text-4xl font-display font-bold text-red-600">{{ $peringatanHariIni }}</h3>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="group bg-white p-4 md:p-8 rounded-2xl md:rounded-3xl shadow-lg hover:shadow-2xl hover:scale-105 transition-all duration-300 border border-gray-100 relative overflow-hidden">
|
|
<div class="absolute inset-0 bg-gradient-to-br from-purple-500/5 to-blue-500/5 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
|
<div class="relative z-10">
|
|
<div class="w-10 h-10 md:w-16 md:h-16 bg-gradient-to-br from-purple-500 to-blue-600 rounded-xl md:rounded-2xl flex items-center justify-center mb-3 md:mb-4">
|
|
<i data-feather="shield-check" class="w-5 h-5 md:w-8 md:h-8 text-white"></i>
|
|
</div>
|
|
<p class="text-gray-600 font-medium mb-1 md:mb-2 text-xs md:text-base">Status</p>
|
|
<h3 class="text-xl md:text-3xl font-display font-bold text-purple-600">{{ $alertAktif > 0 ? 'Waspada' : 'Normal' }}</h3>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- ALERT REALTIME -->
|
|
@if($alertTerbaru->count() > 0 || $pirAktif || $vibAktif || $reedAktif)
|
|
<div class="bg-gradient-to-br from-red-50 to-orange-50 rounded-3xl shadow-lg p-8 border border-red-200">
|
|
<div class="flex items-center justify-between mb-6">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-12 h-12 bg-gradient-to-br from-red-500 to-orange-600 rounded-xl flex items-center justify-center">
|
|
<i data-feather="alert-triangle" class="w-6 h-6 text-white"></i>
|
|
</div>
|
|
<div>
|
|
<h3 class="text-xl font-display font-bold text-red-800">⚠ Alert Aktif</h3>
|
|
<p class="text-red-600 text-sm">{{ $alertTerbaru->count() }} peringatan memerlukan perhatian</p>
|
|
</div>
|
|
</div>
|
|
<a href="/log" class="text-red-600 hover:text-red-800 text-sm font-semibold flex items-center gap-1">
|
|
Lihat semua <i data-feather="arrow-right" class="w-4 h-4"></i>
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Deteksi sensor aktif saat ini -->
|
|
@if($pirAktif || $vibAktif || $reedAktif)
|
|
<div class="mb-4 p-4 bg-red-500 rounded-2xl text-white flex items-center gap-3">
|
|
<div class="w-3 h-3 bg-white rounded-full animate-pulse flex-shrink-0"></div>
|
|
<p class="font-semibold text-sm">
|
|
DETEKSI AKTIF SEKARANG:
|
|
@if($pirAktif) <span class="bg-white/20 px-2 py-0.5 rounded-lg ml-1">PIR</span> @endif
|
|
@if($vibAktif) <span class="bg-white/20 px-2 py-0.5 rounded-lg ml-1">SW-420</span> @endif
|
|
@if($reedAktif) <span class="bg-white/20 px-2 py-0.5 rounded-lg ml-1">Reed Switch</span> @endif
|
|
</p>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Daftar alert -->
|
|
<div class="space-y-3">
|
|
@foreach($alertTerbaru as $alert)
|
|
@php
|
|
$priorityColor = match($alert->priority) {
|
|
'critical' => 'bg-red-100 border-red-300 text-red-800',
|
|
'high' => 'bg-orange-100 border-orange-300 text-orange-800',
|
|
'medium' => 'bg-yellow-100 border-yellow-300 text-yellow-800',
|
|
default => 'bg-blue-100 border-blue-300 text-blue-800',
|
|
};
|
|
$dotColor = match($alert->priority) {
|
|
'critical' => 'bg-red-500',
|
|
'high' => 'bg-orange-500',
|
|
'medium' => 'bg-yellow-500',
|
|
default => 'bg-blue-500',
|
|
};
|
|
@endphp
|
|
<div class="flex items-start gap-4 p-4 {{ $priorityColor }} rounded-2xl border">
|
|
<div class="w-3 h-3 {{ $dotColor }} rounded-full mt-1 flex-shrink-0 animate-pulse"></div>
|
|
<div class="flex-1">
|
|
<p class="font-semibold text-sm">{{ $alert->title }}</p>
|
|
<p class="text-xs mt-0.5 opacity-80">{{ $alert->message }}</p>
|
|
<p class="text-xs mt-1 opacity-60">
|
|
{{ $alert->device?->name ?? 'Sistem' }} ·
|
|
{{ $alert->triggered_at?->diffForHumans() ?? '-' }}
|
|
</p>
|
|
</div>
|
|
<span class="text-xs font-bold uppercase px-2 py-1 bg-white/50 rounded-lg flex-shrink-0">
|
|
{{ $alert->priority }}
|
|
</span>
|
|
</div>
|
|
@endforeach
|
|
</div>
|
|
</div>
|
|
@endif
|
|
|
|
<!-- BUZZER & LED STATUS -->
|
|
<div class="bg-white/80 backdrop-blur-sm rounded-3xl shadow-lg p-8 border border-gray-100">
|
|
|
|
<div class="flex items-center justify-between mb-8">
|
|
<div class="flex items-center gap-3">
|
|
<div class="w-12 h-12 bg-gradient-to-br from-red-500 to-orange-600 rounded-xl flex items-center justify-center">
|
|
<i data-feather="bell" class="w-6 h-6 text-white"></i>
|
|
</div>
|
|
<div>
|
|
<h3 class="text-xl font-display font-bold text-gray-800">Status Buzzer & LED Lokal</h3>
|
|
<p class="text-gray-600 text-sm">Indikator perangkat keras saat sensor terdeteksi</p>
|
|
</div>
|
|
</div>
|
|
<div class="flex items-center gap-2 text-xs text-gray-400">
|
|
<div id="polling-dot" class="w-2 h-2 bg-green-400 rounded-full animate-pulse-slow"></div>
|
|
<span id="last-update-text">Memuat...</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ALERT BANNER -->
|
|
<div id="alert-banner" class="hidden mb-6 p-4 bg-gradient-to-r from-red-500 to-orange-500 rounded-2xl text-white flex items-center gap-4 shadow-lg">
|
|
<div class="w-10 h-10 bg-white/20 rounded-xl flex items-center justify-center flex-shrink-0">
|
|
<i data-feather="alert-triangle" class="w-5 h-5"></i>
|
|
</div>
|
|
<div class="flex-1">
|
|
<p class="font-bold text-sm">⚠ PERINGATAN AKTIF</p>
|
|
<p id="alert-banner-text" class="text-red-100 text-xs mt-0.5">Sensor mendeteksi aktivitas mencurigakan</p>
|
|
</div>
|
|
<button onclick="dismissAlert()" class="w-8 h-8 bg-white/20 hover:bg-white/30 rounded-lg flex items-center justify-center transition-all">
|
|
<i data-feather="x" class="w-4 h-4"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
|
|
|
<!-- BUZZER -->
|
|
<div class="bg-gradient-to-br from-gray-50 to-slate-100 rounded-2xl p-6 border border-gray-200">
|
|
<p class="text-xs font-semibold text-gray-500 uppercase tracking-widest mb-5">Buzzer</p>
|
|
<div class="flex items-center gap-6">
|
|
<!-- Buzzer Icon -->
|
|
<div class="relative flex items-center justify-center w-20 h-20 flex-shrink-0">
|
|
<div id="buzzer-icon" class="relative w-16 h-16 bg-gray-200 rounded-full flex items-center justify-center transition-all duration-300">
|
|
<i data-feather="volume-x" class="w-7 h-7 text-gray-400" id="buzzer-icon-inner"></i>
|
|
</div>
|
|
</div>
|
|
<div class="flex-1">
|
|
<div class="flex items-center gap-3 mb-2">
|
|
<div id="buzzer-status-dot" class="w-4 h-4 rounded-full bg-gray-300 transition-all duration-300"></div>
|
|
<p id="buzzer-status-text" class="text-xl font-display font-bold text-gray-400">Tidak Aktif</p>
|
|
</div>
|
|
<p id="buzzer-trigger-text" class="text-gray-500 text-sm">Menunggu deteksi sensor...</p>
|
|
<div class="mt-3 flex flex-wrap gap-2" id="buzzer-triggers">
|
|
<span class="text-xs px-2 py-1 bg-gray-200 text-gray-500 rounded-lg">PIR</span>
|
|
<span class="text-xs px-2 py-1 bg-gray-200 text-gray-500 rounded-lg">SW-420</span>
|
|
<span class="text-xs px-2 py-1 bg-gray-200 text-gray-500 rounded-lg">Reed Switch</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- LED -->
|
|
<div class="bg-gradient-to-br from-gray-50 to-slate-100 rounded-2xl p-6 border border-gray-200">
|
|
<p class="text-xs font-semibold text-gray-500 uppercase tracking-widest mb-5">LED Indikator</p>
|
|
<div class="flex items-center gap-6">
|
|
<!-- LED Icon -->
|
|
<div class="flex flex-col items-center gap-3 flex-shrink-0">
|
|
<!-- LED Merah -->
|
|
<div class="flex items-center gap-2">
|
|
<div id="led-red" class="w-8 h-8 rounded-full bg-gray-200 led-off transition-all duration-300 flex items-center justify-center">
|
|
<div class="w-4 h-4 rounded-full bg-gray-400"></div>
|
|
</div>
|
|
<span class="text-xs text-gray-500 font-medium">MERAH</span>
|
|
</div>
|
|
<!-- LED Hijau -->
|
|
<div class="flex items-center gap-2">
|
|
<div id="led-green" class="w-8 h-8 rounded-full bg-green-100 transition-all duration-300 flex items-center justify-center" style="box-shadow: 0 0 10px 3px #22c55e;">
|
|
<div class="w-4 h-4 rounded-full bg-green-500"></div>
|
|
</div>
|
|
<span class="text-xs text-gray-500 font-medium">HIJAU</span>
|
|
</div>
|
|
</div>
|
|
<div class="flex-1">
|
|
<div class="flex items-center gap-3 mb-2">
|
|
<p id="led-status-text" class="text-xl font-display font-bold text-green-600">Aman</p>
|
|
</div>
|
|
<p id="led-detail-text" class="text-gray-500 text-sm">LED hijau menyala — sistem normal</p>
|
|
<div class="mt-3 p-2 bg-white rounded-xl border border-gray-100">
|
|
<div class="flex items-center gap-2 text-xs text-gray-500">
|
|
<div class="w-3 h-3 rounded-full bg-green-400"></div>
|
|
<span>Hijau = Aman | </span>
|
|
<div class="w-3 h-3 rounded-full bg-red-400"></div>
|
|
<span>Merah = Bahaya</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- SENSOR TRIGGER STATUS -->
|
|
<div class="mt-6 grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
|
|
<!-- PIR -->
|
|
<div id="card-pir" class="flex items-center gap-4 p-4 rounded-2xl border-2 border-gray-100 bg-white transition-all duration-500">
|
|
<div id="icon-pir" class="w-12 h-12 bg-gray-100 rounded-xl flex items-center justify-center transition-all duration-300">
|
|
<i data-feather="eye" class="w-6 h-6 text-gray-400"></i>
|
|
</div>
|
|
<div>
|
|
<p class="font-semibold text-gray-700 text-sm">Sensor PIR</p>
|
|
<p id="status-pir" class="text-xs text-gray-400 mt-0.5">Tidak ada gerakan</p>
|
|
</div>
|
|
<div id="dot-pir" class="ml-auto w-3 h-3 rounded-full bg-gray-300"></div>
|
|
</div>
|
|
|
|
<!-- SW-420 -->
|
|
<div id="card-sw420" class="flex items-center gap-4 p-4 rounded-2xl border-2 border-gray-100 bg-white transition-all duration-500">
|
|
<div id="icon-sw420" class="w-12 h-12 bg-gray-100 rounded-xl flex items-center justify-center transition-all duration-300">
|
|
<i data-feather="zap" class="w-6 h-6 text-gray-400"></i>
|
|
</div>
|
|
<div>
|
|
<p class="font-semibold text-gray-700 text-sm">Sensor SW-420</p>
|
|
<p id="status-sw420" class="text-xs text-gray-400 mt-0.5">Tidak ada getaran</p>
|
|
</div>
|
|
<div id="dot-sw420" class="ml-auto w-3 h-3 rounded-full bg-gray-300"></div>
|
|
</div>
|
|
|
|
<!-- Reed Switch -->
|
|
<div id="card-reed" class="flex items-center gap-4 p-4 rounded-2xl border-2 border-gray-100 bg-white transition-all duration-500">
|
|
<div id="icon-reed" class="w-12 h-12 bg-gray-100 rounded-xl flex items-center justify-center transition-all duration-300">
|
|
<i data-feather="unlock" class="w-6 h-6 text-gray-400"></i>
|
|
</div>
|
|
<div>
|
|
<p class="font-semibold text-gray-700 text-sm">Reed Switch</p>
|
|
<p id="status-reed" class="text-xs text-gray-400 mt-0.5">Pintu tertutup</p>
|
|
</div>
|
|
<div id="dot-reed" class="ml-auto w-3 h-3 rounded-full bg-gray-300"></div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- STATUS -->
|
|
<div class="bg-white/80 backdrop-blur-sm rounded-3xl shadow-lg p-8 border border-gray-100">
|
|
|
|
<div class="flex items-center gap-3 mb-8">
|
|
<div class="w-12 h-12 bg-gradient-to-br from-blue-500 to-indigo-600 rounded-xl flex items-center justify-center">
|
|
<i data-feather="activity" class="w-6 h-6 text-white"></i>
|
|
</div>
|
|
<div>
|
|
<h3 class="text-xl font-display font-bold text-gray-800">Status Sistem</h3>
|
|
<p class="text-gray-600">Kondisi operasional saat ini</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
|
|
<div class="group bg-gradient-to-br from-green-50 to-emerald-50 p-6 rounded-2xl border border-green-200 hover:shadow-lg transition-all duration-300">
|
|
<div class="flex items-center gap-4">
|
|
<div class="w-12 h-12 bg-green-500 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform">
|
|
<i data-feather="check" class="w-6 h-6 text-white"></i>
|
|
</div>
|
|
<div>
|
|
<p class="font-semibold text-green-800">Semua sensor aktif</p>
|
|
<p class="text-green-600 text-sm">Monitoring berjalan normal</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="group bg-gradient-to-br from-blue-50 to-indigo-50 p-6 rounded-2xl border border-blue-200 hover:shadow-lg transition-all duration-300">
|
|
<div class="flex items-center gap-4">
|
|
<div class="w-12 h-12 bg-blue-500 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform">
|
|
<i data-feather="radio" class="w-6 h-6 text-white"></i>
|
|
</div>
|
|
<div>
|
|
<p class="font-semibold text-blue-800">Komunikasi LoRa stabil</p>
|
|
<p class="text-blue-600 text-sm">Koneksi optimal</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="group bg-gradient-to-br from-yellow-50 to-orange-50 p-6 rounded-2xl border border-yellow-200 hover:shadow-lg transition-all duration-300">
|
|
<div class="flex items-center gap-4">
|
|
<div class="w-12 h-12 bg-yellow-500 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform">
|
|
<i data-feather="alert-circle" class="w-6 h-6 text-white"></i>
|
|
</div>
|
|
<div>
|
|
<p class="font-semibold text-yellow-800">Aktivitas terdeteksi hari ini</p>
|
|
<p class="text-yellow-600 text-sm">2 kejadian tercatat</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="group bg-gradient-to-br from-purple-50 to-pink-50 p-6 rounded-2xl border border-purple-200 hover:shadow-lg transition-all duration-300">
|
|
<div class="flex items-center gap-4">
|
|
<div class="w-12 h-12 bg-purple-500 rounded-xl flex items-center justify-center group-hover:scale-110 transition-transform">
|
|
<i data-feather="clock" class="w-6 h-6 text-white"></i>
|
|
</div>
|
|
<div>
|
|
<p class="font-semibold text-purple-800">Update terakhir</p>
|
|
<p class="text-purple-600 text-sm">Update: {{ $lastUpdate ? $lastUpdate->diffForHumans() : 'Belum ada data' }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<!-- QUICK ACCESS -->
|
|
<div class="bg-white/80 backdrop-blur-sm rounded-3xl shadow-lg p-8 border border-gray-100">
|
|
|
|
<div class="flex items-center gap-3 mb-8">
|
|
<div class="w-12 h-12 bg-gradient-to-br from-indigo-500 to-purple-600 rounded-xl flex items-center justify-center">
|
|
<i data-feather="zap" class="w-6 h-6 text-white"></i>
|
|
</div>
|
|
<div>
|
|
<h3 class="text-xl font-display font-bold text-gray-800">Menu Cepat</h3>
|
|
<p class="text-gray-600">Akses fitur utama sistem</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
|
|
<a href="/monitoring" class="group bg-gradient-to-br from-blue-500 to-indigo-600 p-8 rounded-2xl text-white text-center font-semibold shadow-lg hover:shadow-2xl hover:scale-105 transition-all duration-300 relative overflow-hidden">
|
|
<div class="absolute inset-0 bg-white/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
|
<div class="relative z-10">
|
|
<div class="w-16 h-16 bg-white/20 rounded-2xl flex items-center justify-center mx-auto mb-4 group-hover:scale-110 transition-transform">
|
|
<i data-feather="activity" class="w-8 h-8"></i>
|
|
</div>
|
|
<h4 class="text-lg font-display font-bold mb-2">Monitoring Sensor</h4>
|
|
<p class="text-blue-100 text-sm">Pantau status sensor realtime</p>
|
|
</div>
|
|
</a>
|
|
|
|
<a href="/log" class="group bg-gradient-to-br from-gray-600 to-slate-700 p-8 rounded-2xl text-white text-center font-semibold shadow-lg hover:shadow-2xl hover:scale-105 transition-all duration-300 relative overflow-hidden">
|
|
<div class="absolute inset-0 bg-white/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
|
<div class="relative z-10">
|
|
<div class="w-16 h-16 bg-white/20 rounded-2xl flex items-center justify-center mx-auto mb-4 group-hover:scale-110 transition-transform">
|
|
<i data-feather="file-text" class="w-8 h-8"></i>
|
|
</div>
|
|
<h4 class="text-lg font-display font-bold mb-2">Log Aktivitas</h4>
|
|
<p class="text-gray-200 text-sm">Riwayat kejadian sistem</p>
|
|
</div>
|
|
</a>
|
|
|
|
<a href="/device" class="group bg-gradient-to-br from-green-500 to-emerald-600 p-8 rounded-2xl text-white text-center font-semibold shadow-lg hover:shadow-2xl hover:scale-105 transition-all duration-300 relative overflow-hidden">
|
|
<div class="absolute inset-0 bg-white/10 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
|
<div class="relative z-10">
|
|
<div class="w-16 h-16 bg-white/20 rounded-2xl flex items-center justify-center mx-auto mb-4 group-hover:scale-110 transition-transform">
|
|
<i data-feather="cpu" class="w-8 h-8"></i>
|
|
</div>
|
|
<h4 class="text-lg font-display font-bold mb-2">Status Device</h4>
|
|
<p class="text-green-100 text-sm">Kondisi perangkat IoT</p>
|
|
</div>
|
|
</a>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</main>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
feather.replace()
|
|
</script>
|
|
|
|
<!-- MODAL FOTO PROFIL -->
|
|
<div id="profile-modal" class="fixed inset-0 z-50 hidden flex items-center justify-center p-4">
|
|
<!-- Backdrop -->
|
|
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm" onclick="closeProfileModal()"></div>
|
|
|
|
<!-- Modal Card -->
|
|
<div class="relative bg-white rounded-3xl shadow-2xl w-full max-w-md p-8 z-10">
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between mb-6">
|
|
<div>
|
|
<h3 class="text-xl font-display font-bold text-gray-800">Foto Profil</h3>
|
|
<p class="text-gray-500 text-sm mt-0.5">Upload atau hapus foto profil Anda</p>
|
|
</div>
|
|
<button onclick="closeProfileModal()" class="w-10 h-10 bg-gray-100 hover:bg-gray-200 rounded-xl flex items-center justify-center text-gray-500 transition-all">
|
|
<i data-feather="x" class="w-5 h-5"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Preview Foto -->
|
|
<div class="flex flex-col items-center mb-6">
|
|
<div class="relative group">
|
|
@if(auth()->user()->profile_photo)
|
|
<img id="photo-preview" src="{{ auth()->user()->profile_photo_url }}" class="w-28 h-28 rounded-2xl object-cover shadow-lg border-4 border-indigo-100">
|
|
@else
|
|
<div id="photo-preview-placeholder" class="w-28 h-28 rounded-2xl bg-gradient-to-br from-indigo-500 to-purple-600 flex items-center justify-center shadow-lg border-4 border-indigo-100">
|
|
<span class="text-white font-bold text-3xl">{{ auth()->user()->initials }}</span>
|
|
</div>
|
|
<img id="photo-preview" src="" class="w-28 h-28 rounded-2xl object-cover shadow-lg border-4 border-indigo-100 hidden">
|
|
@endif
|
|
<label for="photo-input" class="absolute inset-0 bg-black/40 rounded-2xl flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity cursor-pointer">
|
|
<i data-feather="camera" class="w-7 h-7 text-white"></i>
|
|
</label>
|
|
</div>
|
|
<p class="text-gray-400 text-xs mt-3">Klik foto untuk memilih gambar baru</p>
|
|
</div>
|
|
|
|
<!-- Flash messages -->
|
|
@if(session('success'))
|
|
<div class="mb-4 p-3 bg-green-50 border border-green-200 rounded-xl text-green-700 text-sm flex items-center gap-2">
|
|
<i data-feather="check-circle" class="w-4 h-4 flex-shrink-0"></i>
|
|
{{ session('success') }}
|
|
</div>
|
|
@endif
|
|
|
|
<!-- Form Upload -->
|
|
<form action="{{ route('profile.photo.upload') }}" method="POST" enctype="multipart/form-data" id="upload-form">
|
|
@csrf
|
|
<input type="file" id="photo-input" name="photo" accept="image/jpeg,image/png,image/jpg,image/gif" class="hidden" onchange="previewPhoto(this)">
|
|
|
|
<div id="upload-area" class="border-2 border-dashed border-gray-200 rounded-2xl p-6 text-center mb-4 hover:border-indigo-300 transition-colors cursor-pointer" onclick="document.getElementById('photo-input').click()">
|
|
<i data-feather="upload-cloud" class="w-8 h-8 text-gray-400 mx-auto mb-2"></i>
|
|
<p class="text-gray-500 text-sm font-medium">Klik atau drag foto ke sini</p>
|
|
<p class="text-gray-400 text-xs mt-1">JPEG, PNG, JPG, GIF — Maks. 2MB</p>
|
|
</div>
|
|
|
|
<div id="selected-file-info" class="hidden mb-4 p-3 bg-indigo-50 border border-indigo-200 rounded-xl flex items-center gap-3">
|
|
<i data-feather="image" class="w-5 h-5 text-indigo-500 flex-shrink-0"></i>
|
|
<div class="flex-1 min-w-0">
|
|
<p id="selected-file-name" class="text-indigo-700 text-sm font-medium truncate"></p>
|
|
<p id="selected-file-size" class="text-indigo-400 text-xs"></p>
|
|
</div>
|
|
<button type="button" onclick="clearPhotoSelection()" class="text-indigo-400 hover:text-indigo-600">
|
|
<i data-feather="x" class="w-4 h-4"></i>
|
|
</button>
|
|
</div>
|
|
|
|
@error('photo')
|
|
<p class="text-red-500 text-sm mb-3 flex items-center gap-1">
|
|
<i data-feather="alert-circle" class="w-4 h-4"></i> {{ $message }}
|
|
</p>
|
|
@enderror
|
|
|
|
<button type="submit" id="upload-btn" class="w-full bg-gradient-to-r from-indigo-500 to-purple-600 text-white py-3 rounded-xl font-semibold hover:shadow-lg hover:scale-[1.02] transition-all duration-300 flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed disabled:scale-100" disabled>
|
|
<i data-feather="upload" class="w-5 h-5"></i>
|
|
Simpan Foto Profil
|
|
</button>
|
|
</form>
|
|
|
|
<!-- Hapus Foto (hanya tampil jika ada foto) -->
|
|
@if(auth()->user()->profile_photo)
|
|
<form action="{{ route('profile.photo.delete') }}" method="POST" class="mt-3" onsubmit="return confirm('Hapus foto profil?')">
|
|
@csrf
|
|
@method('DELETE')
|
|
<button type="submit" class="w-full bg-red-50 hover:bg-red-100 text-red-600 py-3 rounded-xl font-semibold transition-all duration-300 flex items-center justify-center gap-2 border border-red-200">
|
|
<i data-feather="trash-2" class="w-5 h-5"></i>
|
|
Hapus Foto Profil
|
|
</button>
|
|
</form>
|
|
@endif
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Toggle sidebar mobile
|
|
function toggleSidebar() {
|
|
const sidebar = document.getElementById('sidebar');
|
|
const overlay = document.getElementById('sidebar-overlay');
|
|
const isOpen = !sidebar.classList.contains('-translate-x-full');
|
|
if (isOpen) {
|
|
sidebar.classList.add('-translate-x-full');
|
|
overlay.classList.add('hidden');
|
|
} else {
|
|
sidebar.classList.remove('-translate-x-full');
|
|
overlay.classList.remove('hidden');
|
|
}
|
|
}
|
|
|
|
// Auto refresh halaman setiap 30 detik untuk update alert
|
|
setTimeout(function() {
|
|
window.location.reload();
|
|
}, 30000);
|
|
</script>
|
|
|
|
<script>
|
|
// =============================================
|
|
// BUZZER & LED REALTIME POLLING
|
|
// =============================================
|
|
|
|
let alertDismissed = false;
|
|
let pollingInterval = null;
|
|
|
|
// Threshold waktu deteksi dianggap "aktif" (dalam detik)
|
|
const DETECTION_WINDOW = 30;
|
|
|
|
async function fetchSensorStatus() {
|
|
try {
|
|
const [pirRes, vibRes, reedRes] = await Promise.all([
|
|
fetch('/api/pir/readings?limit=1').then(r => r.json()).catch(() => null),
|
|
fetch('/api/vibration/readings?limit=1').then(r => r.json()).catch(() => null),
|
|
fetch('/api/door-access/readings?limit=1').then(r => r.json()).catch(() => null),
|
|
]);
|
|
|
|
const now = Date.now();
|
|
|
|
// Cek PIR
|
|
let pirActive = false;
|
|
if (pirRes?.success && pirRes.data?.length > 0) {
|
|
const latest = pirRes.data[0];
|
|
const age = (now - new Date(latest.recorded_at).getTime()) / 1000;
|
|
pirActive = latest.motion_detected && age <= DETECTION_WINDOW;
|
|
}
|
|
|
|
// Cek Vibration (SW-420)
|
|
let sw420Active = false;
|
|
if (vibRes?.success && vibRes.data?.length > 0) {
|
|
const latest = vibRes.data[0];
|
|
const age = (now - new Date(latest.recorded_at).getTime()) / 1000;
|
|
sw420Active = latest.is_abnormal && age <= DETECTION_WINDOW;
|
|
}
|
|
|
|
// Cek Reed Switch
|
|
let reedActive = false;
|
|
if (reedRes?.success && reedRes.data?.length > 0) {
|
|
const latest = reedRes.data[0];
|
|
const age = (now - new Date(latest.recorded_at).getTime()) / 1000;
|
|
reedActive = latest.door_open && age <= DETECTION_WINDOW;
|
|
}
|
|
|
|
const anyActive = pirActive || sw420Active || reedActive;
|
|
|
|
updateUI(pirActive, sw420Active, reedActive, anyActive);
|
|
updateLastUpdateText();
|
|
|
|
} catch (err) {
|
|
console.error('Polling error:', err);
|
|
}
|
|
}
|
|
|
|
function updateUI(pirActive, sw420Active, reedActive, anyActive) {
|
|
// --- SENSOR CARDS ---
|
|
updateSensorCard('pir', pirActive, 'Gerakan terdeteksi!', 'Tidak ada gerakan');
|
|
updateSensorCard('sw420', sw420Active, 'Getaran terdeteksi!', 'Tidak ada getaran');
|
|
updateSensorCard('reed', reedActive, 'Pintu terbuka!', 'Pintu tertutup');
|
|
|
|
// --- BUZZER ---
|
|
const buzzerIcon = document.getElementById('buzzer-icon');
|
|
const buzzerDot = document.getElementById('buzzer-status-dot');
|
|
const buzzerText = document.getElementById('buzzer-status-text');
|
|
const buzzerTrigger = document.getElementById('buzzer-trigger-text');
|
|
const buzzerIconInner = document.getElementById('buzzer-icon-inner');
|
|
|
|
if (anyActive) {
|
|
buzzerIcon.className = 'relative w-16 h-16 bg-red-500 rounded-full flex items-center justify-center transition-all duration-300 buzzer-active';
|
|
buzzerDot.className = 'w-4 h-4 rounded-full bg-red-500 animate-pulse transition-all duration-300';
|
|
buzzerText.className = 'text-xl font-display font-bold text-red-600';
|
|
buzzerText.textContent = 'BERBUNYI!';
|
|
buzzerIconInner.setAttribute('data-feather', 'volume-2');
|
|
|
|
const triggers = [];
|
|
if (pirActive) triggers.push('PIR');
|
|
if (sw420Active) triggers.push('SW-420');
|
|
if (reedActive) triggers.push('Reed Switch');
|
|
buzzerTrigger.textContent = 'Dipicu oleh: ' + triggers.join(', ');
|
|
|
|
// Update trigger badges
|
|
document.getElementById('buzzer-triggers').innerHTML = triggers.map(t =>
|
|
`<span class="text-xs px-2 py-1 bg-red-100 text-red-600 rounded-lg font-semibold animate-pulse">${t}</span>`
|
|
).join('');
|
|
} else {
|
|
buzzerIcon.className = 'relative w-16 h-16 bg-gray-200 rounded-full flex items-center justify-center transition-all duration-300';
|
|
buzzerDot.className = 'w-4 h-4 rounded-full bg-gray-300 transition-all duration-300';
|
|
buzzerText.className = 'text-xl font-display font-bold text-gray-400';
|
|
buzzerText.textContent = 'Tidak Aktif';
|
|
buzzerIconInner.setAttribute('data-feather', 'volume-x');
|
|
buzzerTrigger.textContent = 'Menunggu deteksi sensor...';
|
|
document.getElementById('buzzer-triggers').innerHTML = `
|
|
<span class="text-xs px-2 py-1 bg-gray-200 text-gray-500 rounded-lg">PIR</span>
|
|
<span class="text-xs px-2 py-1 bg-gray-200 text-gray-500 rounded-lg">SW-420</span>
|
|
<span class="text-xs px-2 py-1 bg-gray-200 text-gray-500 rounded-lg">Reed Switch</span>
|
|
`;
|
|
}
|
|
|
|
// --- LED ---
|
|
const ledRed = document.getElementById('led-red');
|
|
const ledGreen = document.getElementById('led-green');
|
|
const ledStatusText = document.getElementById('led-status-text');
|
|
const ledDetailText = document.getElementById('led-detail-text');
|
|
|
|
if (anyActive) {
|
|
// LED Merah menyala, hijau mati
|
|
ledRed.className = 'w-8 h-8 rounded-full bg-red-100 transition-all duration-300 flex items-center justify-center';
|
|
ledRed.style.boxShadow = '0 0 14px 5px #ef4444';
|
|
ledRed.innerHTML = '<div class="w-4 h-4 rounded-full bg-red-500 animate-pulse"></div>';
|
|
|
|
ledGreen.className = 'w-8 h-8 rounded-full bg-gray-100 transition-all duration-300 flex items-center justify-center';
|
|
ledGreen.style.boxShadow = 'none';
|
|
ledGreen.innerHTML = '<div class="w-4 h-4 rounded-full bg-gray-300"></div>';
|
|
|
|
ledStatusText.className = 'text-xl font-display font-bold text-red-600';
|
|
ledStatusText.textContent = 'BAHAYA!';
|
|
ledDetailText.textContent = 'LED merah menyala — aktivitas mencurigakan terdeteksi';
|
|
} else {
|
|
// LED Hijau menyala, merah mati
|
|
ledRed.className = 'w-8 h-8 rounded-full bg-gray-100 transition-all duration-300 flex items-center justify-center';
|
|
ledRed.style.boxShadow = 'none';
|
|
ledRed.innerHTML = '<div class="w-4 h-4 rounded-full bg-gray-300"></div>';
|
|
|
|
ledGreen.className = 'w-8 h-8 rounded-full bg-green-100 transition-all duration-300 flex items-center justify-center';
|
|
ledGreen.style.boxShadow = '0 0 10px 3px #22c55e';
|
|
ledGreen.innerHTML = '<div class="w-4 h-4 rounded-full bg-green-500"></div>';
|
|
|
|
ledStatusText.className = 'text-xl font-display font-bold text-green-600';
|
|
ledStatusText.textContent = 'Aman';
|
|
ledDetailText.textContent = 'LED hijau menyala — sistem normal';
|
|
}
|
|
|
|
// --- ALERT BANNER ---
|
|
const banner = document.getElementById('alert-banner');
|
|
if (anyActive && !alertDismissed) {
|
|
const msgs = [];
|
|
if (pirActive) msgs.push('gerakan (PIR)');
|
|
if (sw420Active) msgs.push('getaran (SW-420)');
|
|
if (reedActive) msgs.push('pintu terbuka (Reed Switch)');
|
|
document.getElementById('alert-banner-text').textContent =
|
|
'Terdeteksi: ' + msgs.join(', ') + ' — Buzzer & LED merah aktif!';
|
|
banner.classList.remove('hidden');
|
|
} else if (!anyActive) {
|
|
banner.classList.add('hidden');
|
|
alertDismissed = false;
|
|
}
|
|
|
|
// Re-render feather icons
|
|
feather.replace();
|
|
}
|
|
|
|
function updateSensorCard(id, active, activeText, inactiveText) {
|
|
const card = document.getElementById('card-' + id);
|
|
const icon = document.getElementById('icon-' + id);
|
|
const status = document.getElementById('status-' + id);
|
|
const dot = document.getElementById('dot-' + id);
|
|
|
|
if (active) {
|
|
card.className = 'flex items-center gap-4 p-4 rounded-2xl border-2 border-red-300 bg-red-50 transition-all duration-500';
|
|
icon.className = 'w-12 h-12 bg-red-500 rounded-xl flex items-center justify-center transition-all duration-300';
|
|
icon.querySelector('i').className = icon.querySelector('i').className.replace('text-gray-400', 'text-white');
|
|
status.textContent = activeText;
|
|
status.className = 'text-xs text-red-600 font-semibold mt-0.5';
|
|
dot.className = 'ml-auto w-3 h-3 rounded-full bg-red-500 animate-pulse';
|
|
} else {
|
|
card.className = 'flex items-center gap-4 p-4 rounded-2xl border-2 border-gray-100 bg-white transition-all duration-500';
|
|
icon.className = 'w-12 h-12 bg-gray-100 rounded-xl flex items-center justify-center transition-all duration-300';
|
|
status.textContent = inactiveText;
|
|
status.className = 'text-xs text-gray-400 mt-0.5';
|
|
dot.className = 'ml-auto w-3 h-3 rounded-full bg-gray-300';
|
|
}
|
|
}
|
|
|
|
function updateLastUpdateText() {
|
|
const now = new Date();
|
|
const timeStr = now.toLocaleTimeString('id-ID', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
document.getElementById('last-update-text').textContent = 'Update: ' + timeStr;
|
|
}
|
|
|
|
function dismissAlert() {
|
|
alertDismissed = true;
|
|
document.getElementById('alert-banner').classList.add('hidden');
|
|
}
|
|
|
|
// Mulai polling setiap 5 detik
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
fetchSensorStatus();
|
|
pollingInterval = setInterval(fetchSensorStatus, 5000);
|
|
});
|
|
</script>
|
|
|
|
<script>
|
|
// =============================================
|
|
// POLLING STATUS SISTEM (ESP32 ONLINE/OFFLINE)
|
|
// =============================================
|
|
async function pollSystemStatus() {
|
|
try {
|
|
const res = await fetch('/api/device/status').then(r => r.json()).catch(() => null);
|
|
if (!res) return;
|
|
|
|
const badge = document.getElementById('system-status-badge');
|
|
const dot = document.getElementById('system-status-dot');
|
|
const text = document.getElementById('system-status-text');
|
|
|
|
if (res.online) {
|
|
badge.className = badge.className
|
|
.replace(/from-red-500\/10 to-orange-500\/10 border-red-200/g, '')
|
|
.replace(/from-green-500\/10 to-emerald-500\/10 border-green-200/g, '');
|
|
badge.className = 'flex items-center gap-2 bg-gradient-to-r from-green-500/10 to-emerald-500/10 border-green-200 px-3 py-2 rounded-xl border transition-all duration-500 cursor-default';
|
|
|
|
dot.className = 'w-2 h-2 md:w-3 md:h-3 bg-green-500 animate-pulse-slow rounded-full transition-all duration-500';
|
|
text.className = 'text-green-700 font-semibold text-xs md:text-sm hidden sm:inline transition-all duration-500';
|
|
text.textContent = 'Sistem Online';
|
|
} else {
|
|
badge.className = 'flex items-center gap-2 bg-gradient-to-r from-red-500/10 to-orange-500/10 border-red-200 px-3 py-2 rounded-xl border transition-all duration-500 cursor-default';
|
|
dot.className = 'w-2 h-2 md:w-3 md:h-3 bg-red-500 rounded-full transition-all duration-500';
|
|
text.className = 'text-red-700 font-semibold text-xs md:text-sm hidden sm:inline transition-all duration-500';
|
|
text.textContent = 'Sistem Offline';
|
|
}
|
|
|
|
// Update tooltip dengan info last seen
|
|
badge.title = res.online
|
|
? 'ESP32 aktif — terakhir kirim data: ' + (res.last_seen_human ?? '-')
|
|
: 'ESP32 tidak merespons — terakhir online: ' + (res.last_seen_human ?? 'Belum pernah');
|
|
|
|
} catch (e) {
|
|
// Diam saja jika gagal
|
|
}
|
|
}
|
|
|
|
// Jalankan polling status sistem setiap 10 detik
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
pollSystemStatus();
|
|
setInterval(pollSystemStatus, 10000);
|
|
});
|
|
</script>
|
|
|
|
<script>
|
|
// =============================================
|
|
// MODAL FOTO PROFIL
|
|
// =============================================
|
|
function openProfileModal() {
|
|
const modal = document.getElementById('profile-modal');
|
|
modal.classList.remove('hidden');
|
|
modal.classList.add('flex');
|
|
feather.replace();
|
|
}
|
|
|
|
function closeProfileModal() {
|
|
const modal = document.getElementById('profile-modal');
|
|
modal.classList.add('hidden');
|
|
modal.classList.remove('flex');
|
|
}
|
|
|
|
function previewPhoto(input) {
|
|
if (!input.files || !input.files[0]) return;
|
|
|
|
const file = input.files[0];
|
|
const reader = new FileReader();
|
|
|
|
reader.onload = function (e) {
|
|
const preview = document.getElementById('photo-preview');
|
|
const placeholder = document.getElementById('photo-preview-placeholder');
|
|
|
|
preview.src = e.target.result;
|
|
preview.classList.remove('hidden');
|
|
if (placeholder) placeholder.classList.add('hidden');
|
|
|
|
// Info file
|
|
const info = document.getElementById('selected-file-info');
|
|
document.getElementById('selected-file-name').textContent = file.name;
|
|
document.getElementById('selected-file-size').textContent = (file.size / 1024).toFixed(1) + ' KB';
|
|
info.classList.remove('hidden');
|
|
|
|
// Enable tombol upload
|
|
document.getElementById('upload-btn').disabled = false;
|
|
|
|
feather.replace();
|
|
};
|
|
|
|
reader.readAsDataURL(file);
|
|
}
|
|
|
|
function clearPhotoSelection() {
|
|
document.getElementById('photo-input').value = '';
|
|
document.getElementById('selected-file-info').classList.add('hidden');
|
|
document.getElementById('upload-btn').disabled = true;
|
|
|
|
// Kembalikan preview ke kondisi awal
|
|
@if(auth()->user()->profile_photo)
|
|
document.getElementById('photo-preview').src = '{{ auth()->user()->profile_photo_url }}';
|
|
@else
|
|
const preview = document.getElementById('photo-preview');
|
|
preview.src = '';
|
|
preview.classList.add('hidden');
|
|
const placeholder = document.getElementById('photo-preview-placeholder');
|
|
if (placeholder) placeholder.classList.remove('hidden');
|
|
@endif
|
|
}
|
|
|
|
// Buka modal otomatis jika ada flash message dari upload/hapus foto
|
|
@if(session('success') && (str_contains(session('success'), 'foto') || str_contains(session('success'), 'Foto')))
|
|
document.addEventListener('DOMContentLoaded', function () {
|
|
openProfileModal();
|
|
});
|
|
@endif
|
|
|
|
// Tutup modal dengan Escape
|
|
document.addEventListener('keydown', function (e) {
|
|
if (e.key === 'Escape') closeProfileModal();
|
|
});
|
|
</script>
|
|
|
|
</body>
|
|
</html> |