projectTA/resources/views/dashboard.blade.php

1077 lines
57 KiB
PHP

@extends('layouts.app')
@section('title', 'Dashboard')
@section('content')
<!-- SweetAlert2 CSS dan JS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11/dist/sweetalert2.min.css">
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- Sensor Status Section - Card layout yang lebih responsif -->
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4 mt-4 mb-6">
<!-- Card BH1750 - Dioptimalkan untuk layar kecil -->
<div class="bg-white rounded-lg shadow p-4 flex flex-col items-center hover:shadow-md transition-all duration-300">
<div class="w-16 h-16 sm:w-20 sm:h-20 rounded-full bg-blue-100 flex items-center justify-center mb-3">
<img src="{{ asset('images/bh1750-sensor.png') }}" alt="BH1750" class="w-12 h-12 sm:w-14 sm:h-14 object-contain">
</div>
<h3 class="text-base sm:text-lg font-semibold mb-1">Sensor BH1750</h3>
<div id="status-bh1750" class="text-gray-500 text-center">Memuat...</div>
</div>
<!-- Card DHT11 - Dioptimalkan untuk layar kecil -->
<div class="bg-white rounded-lg shadow p-4 flex flex-col items-center hover:shadow-md transition-all duration-300">
<div class="w-16 h-16 sm:w-20 sm:h-20 rounded-full bg-green-100 flex items-center justify-center mb-3">
<img src="{{ asset('images/dht11-sensor.png') }}" alt="DHT11" class="w-12 h-12 sm:w-14 sm:h-14 object-contain">
</div>
<h3 class="text-base sm:text-lg font-semibold mb-1">Sensor DHT11</h3>
<div id="status-dht11" class="text-gray-500 text-center">Memuat...</div>
</div>
<!-- Card Soil Moisture - Dioptimalkan untuk layar kecil -->
<div class="bg-white rounded-lg shadow p-4 flex flex-col items-center hover:shadow-md transition-all duration-300">
<div class="w-16 h-16 sm:w-20 sm:h-20 rounded-full bg-yellow-100 flex items-center justify-center mb-3">
<img src="{{ asset('images/soil-moisture-sensor.png') }}" alt="Soil Moisture" class="w-12 h-12 sm:w-14 sm:h-14 object-contain">
</div>
<h3 class="text-base sm:text-lg font-semibold mb-1">Soil Moisture</h3>
<div id="status-soil" class="text-gray-500 text-center">Memuat...</div>
</div>
</div>
<!-- ESP32-CAM Viewer Section - Layout yang lebih baik untuk mobile -->
<div class="mb-6">
<div class="bg-white rounded-lg shadow-md overflow-hidden">
<div class="p-4 sm:p-6">
<h2 class="text-xl sm:text-2xl font-bold text-gray-800 mb-4">ESP32-CAM Monitoring</h2>
<!-- Status ESP32-CAM - Form yang lebih responsif -->
<div class="mb-4 bg-gray-50 rounded-lg p-3 sm:p-4">
<h3 class="text-base sm:text-lg font-semibold text-gray-700 mb-2">Status ESP32-CAM</h3>
<div class="flex flex-col space-y-3">
<form id="checkStatusForm" class="flex flex-col sm:flex-row items-start sm:items-center space-y-2 sm:space-y-0">
<div class="w-full sm:w-auto flex">
<input type="text" id="cameraIp" placeholder="192.168.240.201" value="192.168.240.201"
class="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-l-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
<button type="submit" class="px-3 py-2 bg-blue-500 text-white rounded-r-md hover:bg-blue-600 transition duration-300 whitespace-nowrap">
Cek Status
</button>
</div>
<div id="cameraStatus" class="flex items-center sm:ml-3 mt-2 sm:mt-0">
<span class="inline-block h-3 w-3 rounded-full mr-2 bg-gray-300"></span>
<span class="text-sm text-gray-500">Belum diperiksa</span>
</div>
</form>
</div>
</div>
<!-- Live Streaming Controls -->
<div class="mb-4 bg-blue-50 rounded-lg p-3 sm:p-4">
<h3 class="text-base sm:text-lg font-semibold text-gray-700 mb-2 flex items-center">
<svg class="w-5 h-5 mr-2 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
Live Streaming
</h3>
<div class="flex flex-col space-y-3">
<div class="flex flex-col sm:flex-row items-start sm:items-center">
<div class="w-full sm:w-auto flex mb-2 sm:mb-0">
<input type="text" id="streamIp" placeholder="192.168.240.201" value="192.168.240.201"
class="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-l-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
<button id="startStreamBtn" type="button" class="px-3 py-2 bg-green-600 text-white rounded-r-md hover:bg-green-700 transition duration-300 whitespace-nowrap">
Mulai Stream
</button>
</div>
<button id="stopStreamBtn" type="button" class="px-3 py-2 sm:ml-2 bg-red-600 text-white rounded-md hover:bg-red-700 transition duration-300 whitespace-nowrap disabled:opacity-50 disabled:cursor-not-allowed" disabled>
Hentikan Stream
</button>
</div>
<div id="streamStatus" class="text-sm text-gray-600">
Live stream tidak aktif
</div>
<!-- Pengaturan Kamera -->
<div class="pt-2 border-t border-gray-200 mt-2">
<h4 class="text-sm font-medium text-gray-700 mb-2">Pengaturan Kamera</h4>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<div>
<label for="cameraResolution" class="block text-xs font-medium text-gray-700 mb-1">Resolusi</label>
<select id="cameraResolution" class="w-full px-3 py-2 text-sm border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
<option value="UXGA">UXGA (1600x1200)</option>
<option value="SXGA">SXGA (1280x1024)</option>
<option value="XGA">XGA (1024x768)</option>
<option value="SVGA" selected>SVGA (800x600)</option>
<option value="VGA">VGA (640x480)</option>
<option value="CIF">CIF (400x296)</option>
<option value="QVGA">QVGA (320x240)</option>
<option value="HQVGA">HQVGA (240x176)</option>
<option value="QQVGA">QQVGA (160x120)</option>
</select>
</div>
<div>
<label for="cameraQuality" class="block text-xs font-medium text-gray-700 mb-1">Kualitas (0-63, rendah=kualitas tinggi)</label>
<input type="range" id="cameraQuality" min="0" max="63" value="10" class="w-full">
<div class="flex justify-between text-xs text-gray-500">
<span>Tinggi</span>
<span id="qualityValue">10</span>
<span>Rendah</span>
</div>
</div>
</div>
<button id="applyCameraSettings" type="button" class="mt-2 px-3 py-1.5 bg-blue-500 text-white text-sm rounded-md hover:bg-blue-600 transition duration-300">
Terapkan Pengaturan
</button>
</div>
</div>
</div>
<!-- Stream Container -->
<div id="streamContainer" class="mb-4 hidden">
<div class="relative w-full">
<img id="streamImage" src="" alt="Live Stream" class="w-full h-auto rounded-lg shadow">
<div class="absolute bottom-0 left-0 right-0 bg-black bg-opacity-60 text-white p-2 rounded-b-lg">
<p class="text-xs sm:text-sm" id="streamInfo">
Live Stream - Aktif
</p>
</div>
</div>
</div>
<!-- Image Container - Responsif untuk berbagai ukuran layar -->
<div class="flex flex-col items-center mb-4">
<?php
$uploadDir = 'storage/esp32cam/';
$files = glob(public_path($uploadDir) . '*.jpg');
if (count($files) > 0) {
usort($files, function($a, $b) {
return filemtime($b) - filemtime($a);
});
$latestImage = $uploadDir . basename($files[0]);
$imageTime = date("d-m-Y H:i:s", filemtime($files[0]));
$fileName = basename($files[0]);
// Mendapatkan timestamp dari nama file (format: esp32cam_YmdHis.jpg)
$timestampFromFilename = null;
if (preg_match('/esp32cam_(\d{8})_(\d{6})/', $fileName, $matches)) {
$dateStr = $matches[1];
$timeStr = $matches[2];
$timestampFromFilename = strtotime(
substr($dateStr, 0, 4) . '-' .
substr($dateStr, 4, 2) . '-' .
substr($dateStr, 6, 2) . ' ' .
substr($timeStr, 0, 2) . ':' .
substr($timeStr, 2, 2) . ':' .
substr($timeStr, 4, 2)
);
}
// Menggunakan timestamp dari nama file jika tersedia
if ($timestampFromFilename) {
$imageTime = date("d-m-Y H:i:s", $timestampFromFilename);
}
// Mendapatkan timestamp dari EXIF data jika tersedia
$exifData = @exif_read_data($files[0]);
$captureTime = $imageTime; // Default menggunakan waktu file
if ($exifData && isset($exifData['DateTimeOriginal'])) {
// Format EXIF datetime: YYYY:MM:DD HH:MM:SS
$exifTime = strtotime($exifData['DateTimeOriginal']);
if ($exifTime) {
$captureTime = date("d-m-Y H:i:s", $exifTime);
}
}
?>
<div class="relative w-full">
<img src="{{ asset($latestImage) }}"
alt="ESP32-CAM Image"
class="w-full h-auto rounded-lg shadow">
<div class="absolute bottom-0 left-0 right-0 bg-black bg-opacity-60 text-white p-2 rounded-b-lg">
<p class="text-xs sm:text-sm">
Foto terakhir: {{ $captureTime }}
</p>
</div>
</div>
<?php } else { ?>
<div class="flex flex-col items-center justify-center w-full h-48 sm:h-64 bg-gray-100 rounded-lg">
<svg class="w-12 h-12 sm:w-16 sm:h-16 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<p class="mt-3 text-sm text-gray-500">Belum ada foto yang tersedia</p>
</div>
<?php } ?>
<!-- Tombol untuk mengambil gambar - Dioptimalkan untuk mobile -->
<div class="mt-3 w-full">
<form id="captureImageForm" class="flex flex-col sm:flex-row space-y-2 sm:space-y-0">
<div class="flex flex-1">
<input type="text" id="captureIp" placeholder="192.168.240.201" value="192.168.240.201"
class="flex-1 px-3 py-2 text-sm border border-gray-300 rounded-l-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
<button type="submit" class="px-3 py-2 bg-green-600 text-white rounded-r-md hover:bg-green-700 transition duration-300 flex items-center">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"></path>
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z"></path>
</svg>
Ambil Foto
</button>
</div>
</form>
<div id="captureResult" class="mt-2 text-xs text-gray-600 hidden"></div>
</div>
</div>
<!-- Penjadwalan dan Pembersihan - Tampilan lebih compact -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 mb-4">
<!-- Penjadwalan -->
<div class="p-3 bg-gray-50 rounded-lg">
<h3 class="text-base font-semibold text-gray-700 mb-2 flex items-center">
<svg class="w-5 h-5 mr-1 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Penjadwalan
</h3>
<p class="text-xs sm:text-sm text-gray-600 mb-2">
Sistem mengambil gambar otomatis setiap jam 6 pagi.
</p>
<div class="flex items-center">
<div class="h-3 w-3 rounded-full bg-green-500 mr-2"></div>
<span class="text-xs sm:text-sm text-gray-700">Aktif</span>
</div>
</div>
<!-- Pembersihan Manual -->
<div class="p-3 bg-gray-50 rounded-lg">
<h3 class="text-base font-semibold text-gray-700 mb-2 flex items-center">
<svg class="w-5 h-5 mr-1 text-red-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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>
Pembersihan Foto
</h3>
<p class="text-xs sm:text-sm text-gray-600 mb-2">
Menghapus foto lama (>7 hari).
</p>
<button id="cleanupButton" class="px-3 py-1.5 text-xs sm:text-sm bg-red-500 text-white rounded hover:bg-red-600 transition duration-300">
Bersihkan Foto Lama
</button>
</div>
</div>
<!-- Image Gallery - Grid yang lebih responsif -->
<div>
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-3 gap-2">
<h3 class="text-lg font-semibold text-gray-700 flex items-center">
<svg class="w-5 h-5 mr-2 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
Riwayat Foto
</h3>
<!-- Filter Foto Mini -->
<div class="flex flex-wrap items-center gap-2">
<?php
// Mengumpulkan tanggal unik dari file foto
$uniqueDates = [];
$uniqueMonths = [];
foreach ($files as $file) {
$fileName = basename($file);
$timestamp = filemtime($file);
// Mendapatkan timestamp dari nama file (format: esp32cam_YmdHis.jpg)
if (preg_match('/esp32cam_(\d{8})_(\d{6})/', $fileName, $matches)) {
$dateStr = $matches[1];
$timeStr = $matches[2];
$timestamp = strtotime(
substr($dateStr, 0, 4) . '-' .
substr($dateStr, 4, 2) . '-' .
substr($dateStr, 6, 2) . ' ' .
substr($timeStr, 0, 2) . ':' .
substr($timeStr, 2, 2) . ':' .
substr($timeStr, 4, 2)
);
}
// Mendapatkan timestamp dari EXIF data jika tersedia
$exifData = @exif_read_data($file);
if ($exifData && isset($exifData['DateTimeOriginal'])) {
$exifTime = strtotime($exifData['DateTimeOriginal']);
if ($exifTime) {
$timestamp = $exifTime;
}
}
$date = date('Y-m-d', $timestamp);
$month = date('Y-m', $timestamp);
$uniqueDates[$date] = date('d M Y', $timestamp);
$uniqueMonths[$month] = date('M Y', $timestamp);
}
// Urutkan tanggal dan bulan (terbaru dulu)
krsort($uniqueDates);
krsort($uniqueMonths);
?>
<select id="dashFilterDate" class="text-xs px-2 py-1 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
<option value="">Semua Tanggal</option>
<?php foreach(array_slice($uniqueDates, 0, 7) as $date => $displayDate): ?>
<option value="<?php echo $date; ?>"><?php echo $displayDate; ?></option>
<?php endforeach; ?>
</select>
<select id="dashFilterMonth" class="text-xs px-2 py-1 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500">
<option value="">Semua Bulan</option>
<?php foreach($uniqueMonths as $month => $displayMonth): ?>
<option value="<?php echo $month; ?>"><?php echo $displayMonth; ?></option>
<?php endforeach; ?>
</select>
<div class="flex space-x-1">
<button id="resetFilterBtn" onclick="resetDashboardFilters()" class="text-xs px-2 py-1 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300 transition duration-300 flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
</button>
<a href="{{ route('photos.all') }}" class="text-xs px-2 py-1 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition duration-300">
Lihat Semua
</a>
</div>
</div>
</div>
<?php if (count($files) > 0) { ?>
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-3" id="photosGrid">
<?php
foreach (array_slice($files, 0, 12) as $file) {
$fileName = basename($file);
$fileTime = date("d-m-Y H:i:s", filemtime($file));
$filePath = $uploadDir . $fileName;
$fileSize = round(filesize($file) / 1024, 2) . ' KB';
// Mendapatkan timestamp dari nama file (format: esp32cam_YmdHis.jpg)
$timestampFromFilename = null;
if (preg_match('/esp32cam_(\d{8})_(\d{6})/', $fileName, $matches)) {
$dateStr = $matches[1];
$timeStr = $matches[2];
$timestampFromFilename = strtotime(
substr($dateStr, 0, 4) . '-' .
substr($dateStr, 4, 2) . '-' .
substr($dateStr, 6, 2) . ' ' .
substr($timeStr, 0, 2) . ':' .
substr($timeStr, 2, 2) . ':' .
substr($timeStr, 4, 2)
);
}
// Menggunakan timestamp dari nama file jika tersedia
if ($timestampFromFilename) {
$fileTime = date("d-m-Y H:i:s", $timestampFromFilename);
}
// Mendapatkan timestamp dari EXIF data jika tersedia
$exifData = @exif_read_data($file);
$captureTime = $fileTime; // Default menggunakan waktu file
if ($exifData && isset($exifData['DateTimeOriginal'])) {
// Format EXIF datetime: YYYY:MM:DD HH:MM:SS
$exifTime = strtotime($exifData['DateTimeOriginal']);
if ($exifTime) {
$captureTime = date("d-m-Y H:i:s", $exifTime);
}
}
?>
<div class="bg-gray-50 rounded-lg overflow-hidden shadow hover:shadow-md transition duration-300 relative group">
<img src="{{ asset($filePath) }}"
alt="<?php echo $fileName; ?>"
class="w-full h-24 sm:h-28 object-cover rounded-t-lg cursor-pointer"
onclick="openPhotoModal('{{ asset($filePath) }}', '<?php echo $captureTime; ?>', '<?php echo $fileSize; ?>', '<?php echo $fileName; ?>')">
<div class="p-1.5 bg-white">
<p class="text-xs text-gray-500 truncate" title="<?php echo $captureTime; ?>"><?php echo $captureTime; ?></p>
<p class="text-xs text-gray-500"><?php echo $fileSize; ?></p>
</div>
<!-- Tombol hapus yang lebih mudah diakses di mobile -->
<button class="delete-photo absolute top-1 right-1 p-1 bg-red-500 text-white rounded-full opacity-80 sm:opacity-0 sm:group-hover:opacity-100 transition-opacity duration-300 hover:bg-red-600 z-10"
data-filename="<?php echo $fileName; ?>">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</button>
</div>
<?php } ?>
</div>
<?php if (count($files) > 12) { ?>
<div class="mt-4 text-center">
<a href="{{ route('photos.all') }}" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition duration-300 inline-block text-sm">
Lihat Semua Foto
</a>
</div>
<?php } ?>
<?php } else { ?>
<div class="text-center py-6 bg-gray-50 rounded-lg">
<p class="text-gray-500 text-sm">Belum ada riwayat foto</p>
</div>
<?php } ?>
</div>
</div>
</div>
</div>
<!-- Modal untuk Photo Zoom -->
<div id="photoModal" class="fixed inset-0 z-50 hidden overflow-auto bg-black bg-opacity-80 flex items-center justify-center p-4">
<div class="relative bg-white rounded-lg shadow-xl max-w-4xl w-full">
<div class="flex justify-between items-center p-4 border-b">
<h3 class="text-lg font-semibold text-gray-900" id="modal-title">Detail Foto</h3>
<button type="button" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm p-1.5 ml-auto inline-flex items-center" onclick="closePhotoModal()">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path>
</svg>
</button>
</div>
<div class="p-4 space-y-6">
<div class="relative">
<div id="zoom-container" class="overflow-hidden relative">
<img id="modal-photo" src="" alt="Photo" class="w-full transform transition-transform duration-300">
</div>
<div class="flex justify-center mt-4 space-x-2">
<button id="zoom-in" class="bg-gray-200 p-2 rounded-full hover:bg-gray-300 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 5a1 1 0 011 1v3h3a1 1 0 110 2h-3v3a1 1 0 11-2 0v-3H6a1 1 0 110-2h3V6a1 1 0 011-1z" clip-rule="evenodd" />
</svg>
</button>
<button id="zoom-out" class="bg-gray-200 p-2 rounded-full hover:bg-gray-300 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M5 10a1 1 0 011-1h8a1 1 0 110 2H6a1 1 0 01-1-1z" clip-rule="evenodd" />
</svg>
</button>
<button id="zoom-reset" class="bg-gray-200 p-2 rounded-full hover:bg-gray-300 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" clip-rule="evenodd" />
</svg>
</button>
</div>
</div>
<div class="mt-4">
<p class="text-sm text-gray-600"><span class="font-medium">Nama File:</span> <span id="modal-filename"></span></p>
<p class="text-sm text-gray-600"><span class="font-medium">Waktu Pengambilan:</span> <span id="modal-timestamp"></span></p>
<p class="text-sm text-gray-600"><span class="font-medium">Ukuran File:</span> <span id="modal-filesize"></span></p>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
const logsRef = firebase.database().ref('logs');
// Konfigurasi SweetAlert2 untuk menggunakan warna Tailwind
const mySwal = Swal.mixin({
customClass: {
confirmButton: 'bg-blue-600 text-white font-medium px-4 py-2 rounded-md shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-500 mr-2',
cancelButton: 'bg-red-500 text-white font-medium px-4 py-2 rounded-md shadow-sm hover:bg-red-600 focus:outline-none focus:ring-2 focus:ring-red-500'
},
buttonsStyling: false
});
function updateStatus(sensor, elementId) {
logsRef.child(sensor).once('value', function(snapshot) {
const data = snapshot.val();
const status = data && data.message ? data.message : 'Tidak ada data';
const el = document.getElementById(elementId);
el.textContent = status;
el.className = status.toLowerCase().includes('terhubung') ? 'text-green-600' : 'text-red-600';
});
}
// Real-time listener (utama)
logsRef.child('bh1750').on('value', function(snapshot) {
const data = snapshot.val();
const status = data && data.message ? data.message : 'Tidak ada data';
const el = document.getElementById('status-bh1750');
el.textContent = status;
el.className = status.toLowerCase().includes('terhubung') ? 'text-green-600' : 'text-red-600';
});
logsRef.child('dht11').on('value', function(snapshot) {
const data = snapshot.val();
const status = data && data.message ? data.message : 'Tidak ada data';
const el = document.getElementById('status-dht11');
el.textContent = status;
el.className = status.toLowerCase().includes('terhubung') ? 'text-green-600' : 'text-red-600';
});
logsRef.child('soil_moisture').on('value', function(snapshot) {
const data = snapshot.val();
const status = data && data.message ? data.message : 'Tidak ada data';
const el = document.getElementById('status-soil');
el.textContent = status;
el.className = status.toLowerCase().includes('terhubung') ? 'text-green-600' : 'text-red-600';
});
// Polling interval (cadangan, misal setiap 10 detik)
setInterval(function() {
updateStatus('bh1750', 'status-bh1750');
updateStatus('dht11', 'status-dht11');
updateStatus('soil_moisture', 'status-soil');
}, 10000); // 10000 ms = 10 detik
// Form cek status ESP32-CAM
document.getElementById('checkStatusForm').addEventListener('submit', function(e) {
e.preventDefault();
const cameraIp = document.getElementById('cameraIp').value;
const statusElement = document.getElementById('cameraStatus');
if (!cameraIp) {
mySwal.fire({
title: 'Error!',
text: 'Masukkan IP address ESP32-CAM',
icon: 'error'
});
return;
}
// Tampilkan loading
mySwal.fire({
title: 'Memeriksa status...',
html: 'Mohon tunggu sebentar...',
allowOutsideClick: false,
didOpen: () => {
mySwal.showLoading();
}
});
fetch(`https://${cameraIp}/status`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error: ${response.status}`);
}
return response.json();
})
.then(data => {
statusElement.innerHTML = `
<span class="inline-block h-3 w-3 rounded-full mr-2 bg-green-500"></span>
<span class="text-green-600">ESP32-CAM terhubung!</span>
<span class="ml-2 text-sm text-gray-500">
RSSI: ${data.wifi.rssi} dBm,
Uptime: ${data.uptime} detik
</span>
`;
mySwal.fire({
title: 'Terhubung!',
html: `ESP32-CAM terhubung dengan baik.<br>
<small>RSSI: ${data.wifi.rssi} dBm, Uptime: ${data.uptime} detik</small>`,
icon: 'success'
});
})
.catch(error => {
statusElement.innerHTML = `
<span class="inline-block h-3 w-3 rounded-full mr-2 bg-red-500"></span>
<span class="text-red-600">Tidak dapat terhubung ke ESP32-CAM</span>
<span class="ml-2 text-sm text-gray-500">${error.message}</span>
`;
mySwal.fire({
title: 'Gagal Terhubung!',
text: `Tidak dapat terhubung ke ESP32-CAM: ${error.message}`,
icon: 'error'
});
});
});
// Tombol cleanup
document.getElementById('cleanupButton').addEventListener('click', function() {
mySwal.fire({
title: 'Hapus foto lama?',
text: "Foto lama akan dihapus secara permanen!",
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Ya, bersihkan!',
cancelButtonText: 'Batal'
}).then((result) => {
if (result.isConfirmed) {
// Tampilkan loading
mySwal.fire({
title: 'Membersihkan...',
html: 'Mohon tunggu sebentar...',
allowOutsideClick: false,
didOpen: () => {
mySwal.showLoading();
}
});
fetch('/api/esp32cam/cleanup')
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
mySwal.fire({
title: 'Berhasil!',
text: data.message,
icon: 'success',
timer: 1500,
showConfirmButton: false
});
} else {
mySwal.fire({
title: 'Gagal!',
text: data.message,
icon: 'error'
});
}
})
.catch(error => {
mySwal.fire({
title: 'Error!',
text: error.message,
icon: 'error'
});
});
}
});
});
// Form pengambilan gambar
document.getElementById('captureImageForm').addEventListener('submit', function(e) {
e.preventDefault();
const cameraIp = document.getElementById('captureIp').value;
if (!cameraIp) {
mySwal.fire({
title: 'Error!',
text: 'Masukkan IP address ESP32-CAM',
icon: 'error'
});
return;
}
// Tampilkan loading
mySwal.fire({
title: 'Mengambil gambar...',
html: 'Mohon tunggu sebentar...',
allowOutsideClick: false,
didOpen: () => {
mySwal.showLoading();
}
});
// Debug payload
const payload = {
camera_ip: cameraIp
};
console.log('Sending request with payload:', payload);
fetch('/api/esp32cam/fetch-from-camera', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken
},
body: JSON.stringify(payload)
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
mySwal.fire({
title: 'Berhasil!',
text: `Gambar telah disimpan dengan nama ${data.filename}`,
icon: 'success',
showConfirmButton: true,
confirmButtonText: 'Muat Ulang Halaman',
showCancelButton: true,
cancelButtonText: 'Tetap di Halaman Ini'
}).then((result) => {
if (result.isConfirmed) {
window.location.reload();
}
});
} else {
mySwal.fire({
title: 'Gagal!',
text: data.message,
icon: 'error'
});
}
})
.catch(error => {
mySwal.fire({
title: 'Error!',
text: error.message,
icon: 'error'
});
});
});
// Tombol hapus foto
document.querySelectorAll('.delete-photo').forEach(button => {
button.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation(); // Prevent triggering photo click
const filename = this.getAttribute('data-filename');
const photoElement = this.closest('.bg-gray-50');
mySwal.fire({
title: 'Apakah Anda yakin?',
text: "Foto yang dihapus tidak dapat dikembalikan!",
icon: 'warning',
showCancelButton: true,
confirmButtonText: 'Ya, hapus!',
cancelButtonText: 'Batal'
}).then((result) => {
if (result.isConfirmed) {
// Tampilkan loading
mySwal.fire({
title: 'Menghapus...',
html: 'Mohon tunggu sebentar...',
allowOutsideClick: false,
didOpen: () => {
mySwal.showLoading();
}
});
fetch('/api/esp32cam/delete', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken
},
body: JSON.stringify({
filename: filename,
delete_storage: true // Tambahkan flag untuk menghapus file di storage
})
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
// Tambahkan animasi sebelum menghapus
photoElement.style.transition = 'all 0.5s ease';
photoElement.style.opacity = '0';
photoElement.style.transform = 'scale(0.8)';
setTimeout(() => {
photoElement.remove();
mySwal.fire({
title: 'Terhapus!',
text: 'Foto berhasil dihapus dari sistem.',
icon: 'success',
timer: 1500,
showConfirmButton: false
});
}, 500);
} else {
mySwal.fire({
title: 'Error!',
text: 'Gagal menghapus foto: ' + data.message,
icon: 'error'
});
}
})
.catch(error => {
console.error('Error:', error);
mySwal.fire({
title: 'Error!',
text: 'Terjadi kesalahan saat menghapus foto.',
icon: 'error'
});
});
}
});
});
});
// Live Streaming Functionality
const startStreamBtn = document.getElementById('startStreamBtn');
const stopStreamBtn = document.getElementById('stopStreamBtn');
const streamIp = document.getElementById('streamIp');
const streamStatus = document.getElementById('streamStatus');
const streamContainer = document.getElementById('streamContainer');
const streamImage = document.getElementById('streamImage');
const streamInfo = document.getElementById('streamInfo');
// Kamera Settings
const cameraResolution = document.getElementById('cameraResolution');
const cameraQuality = document.getElementById('cameraQuality');
const qualityValue = document.getElementById('qualityValue');
const applyCameraSettings = document.getElementById('applyCameraSettings');
// Update quality value display when slider changes
cameraQuality.addEventListener('input', function() {
qualityValue.textContent = this.value;
});
// Apply camera settings
applyCameraSettings.addEventListener('click', function() {
const ip = streamIp.value;
if (!ip) {
mySwal.fire({
title: 'Error!',
text: 'Masukkan IP address ESP32-CAM',
icon: 'error'
});
return;
}
const resolution = cameraResolution.value;
const quality = cameraQuality.value;
// Kirim permintaan ke ESP32-CAM untuk mengubah pengaturan
fetch(`https://${ip}/camera-settings?resolution=${resolution}&quality=${quality}`)
.then(response => response.json())
.then(data => {
mySwal.fire({
title: 'Berhasil!',
text: `Pengaturan kamera diubah: ${data.resolution}, kualitas ${data.quality}`,
icon: 'success'
});
})
.catch(error => {
console.error('Error mengubah pengaturan kamera:', error);
mySwal.fire({
title: 'Gagal!',
text: 'Tidak dapat mengubah pengaturan kamera. Periksa koneksi ke ESP32-CAM.',
icon: 'error'
});
});
});
let isStreaming = false;
let streamStartTime;
// Timer untuk memperbarui durasi streaming
let streamTimer = null;
function updateStreamDuration() {
if (isStreaming && streamStartTime) {
const duration = Math.floor((Date.now() - streamStartTime) / 1000);
const minutes = Math.floor(duration / 60);
const seconds = duration % 60;
streamInfo.textContent = `Live Stream - Aktif (${minutes}:${seconds.toString().padStart(2, '0')})`;
}
}
startStreamBtn.addEventListener('click', function() {
const ip = streamIp.value;
if (!ip) {
mySwal.fire({
title: 'Error!',
text: 'Masukkan IP address ESP32-CAM',
icon: 'error'
});
return;
}
// Update UI
streamStatus.textContent = 'Menghubungkan ke stream...';
startStreamBtn.disabled = true;
// Set source untuk stream MJPEG
streamImage.src = `https://${ip}/stream`;
streamContainer.classList.remove('hidden');
// Periksa apakah streaming berhasil atau tidak
streamImage.onload = function() {
// Stream berhasil dimulai
isStreaming = true;
streamStatus.textContent = 'Live stream aktif';
streamStatus.className = 'text-sm text-green-600';
stopStreamBtn.disabled = false;
streamStartTime = Date.now();
// Mulai timer untuk durasi streaming
streamTimer = setInterval(updateStreamDuration, 1000);
};
streamImage.onerror = function() {
// Stream gagal dimulai
streamStatus.textContent = 'Gagal terhubung ke stream';
streamStatus.className = 'text-sm text-red-600';
startStreamBtn.disabled = false;
streamContainer.classList.add('hidden');
mySwal.fire({
title: 'Gagal!',
text: `Tidak dapat terhubung ke stream. Pastikan ESP32-CAM aktif dan dapat diakses di ${ip}`,
icon: 'error'
});
};
});
stopStreamBtn.addEventListener('click', function() {
const ip = streamIp.value;
// Hentikan stream dari sisi server ESP32-CAM
fetch(`https://${ip}/stopstream`)
.then(response => {
console.log('Stream dihentikan dari server:', response.ok);
})
.catch(error => {
console.error('Error menghentikan stream:', error);
});
// Update UI
isStreaming = false;
streamImage.src = '';
streamContainer.classList.add('hidden');
streamStatus.textContent = 'Live stream dihentikan';
streamStatus.className = 'text-sm text-gray-600';
startStreamBtn.disabled = false;
stopStreamBtn.disabled = true;
// Hentikan timer
if (streamTimer) {
clearInterval(streamTimer);
streamTimer = null;
}
});
// Tambahkan filter untuk riwayat foto di dashboard
const dashFilterDate = document.getElementById('dashFilterDate');
const dashFilterMonth = document.getElementById('dashFilterMonth');
const photosGrid = document.getElementById('photosGrid');
// Fungsi filter foto berdasarkan tanggal atau bulan
function filterDashboardPhotos() {
const selectedDate = dashFilterDate.value;
const selectedMonth = dashFilterMonth.value;
const photoItems = photosGrid.querySelectorAll('.bg-gray-50');
let visibleCount = 0;
photoItems.forEach(item => {
const timeElement = item.querySelector('.text-xs.text-gray-500[title]');
const displayTime = timeElement.getAttribute('title');
// Mengkonversi format tanggal dari dd-mm-yyyy menjadi yyyy-mm-dd untuk pembandingan
let photoDate = '';
let photoMonth = '';
if (displayTime) {
const parts = displayTime.split(' ');
if (parts.length === 2) {
const dateParts = parts[0].split('-');
if (dateParts.length === 3) {
// Format dari dd-mm-yyyy ke yyyy-mm-dd
photoDate = `${dateParts[2]}-${dateParts[1]}-${dateParts[0]}`;
photoMonth = `${dateParts[2]}-${dateParts[1]}`;
}
}
}
let shouldShow = true;
if (selectedDate && photoDate !== selectedDate) {
shouldShow = false;
}
if (selectedMonth && photoMonth !== selectedMonth) {
shouldShow = false;
}
item.classList.toggle('hidden', !shouldShow);
if (shouldShow) {
visibleCount++;
}
});
// Jika tidak ada foto yang terlihat, tampilkan pesan
const noPhotosMessage = document.getElementById('noFilteredPhotos');
if (visibleCount === 0 && (selectedDate || selectedMonth)) {
if (!noPhotosMessage) {
const message = document.createElement('div');
message.id = 'noFilteredPhotos';
message.className = 'text-center py-8 bg-gray-50 rounded-lg col-span-full';
message.innerHTML = `
<p class="text-gray-500">Tidak ada foto untuk filter yang dipilih</p>
<button class="mt-2 px-3 py-1.5 text-sm bg-blue-500 text-white rounded-md hover:bg-blue-600" onclick="resetDashboardFilters()">
Reset Filter
</button>
`;
photosGrid.appendChild(message);
}
} else if (noPhotosMessage) {
noPhotosMessage.remove();
}
}
// Reset filter
function resetDashboardFilters() {
dashFilterDate.value = '';
dashFilterMonth.value = '';
filterDashboardPhotos();
}
// Tambahkan event listener
dashFilterDate.addEventListener('change', filterDashboardPhotos);
dashFilterMonth.addEventListener('change', filterDashboardPhotos);
});
// Variabel untuk zoom
let currentScale = 1;
// Fungsi untuk modal foto
function openPhotoModal(src, timestamp, filesize, filename) {
const modal = document.getElementById('photoModal');
const modalPhoto = document.getElementById('modal-photo');
const modalTimestamp = document.getElementById('modal-timestamp');
const modalFilesize = document.getElementById('modal-filesize');
const modalFilename = document.getElementById('modal-filename');
modalPhoto.src = src;
modalTimestamp.textContent = timestamp;
modalFilesize.textContent = filesize;
modalFilename.textContent = filename;
// Reset zoom setiap kali membuka modal
currentScale = 1;
modalPhoto.style.transform = 'scale(1)';
modal.classList.remove('hidden');
// Tambahkan event listeners untuk kontrol zoom
const zoomIn = document.getElementById('zoom-in');
const zoomOut = document.getElementById('zoom-out');
const zoomReset = document.getElementById('zoom-reset');
zoomIn.addEventListener('click', function() {
currentScale += 0.25;
modalPhoto.style.transform = `scale(${currentScale})`;
});
zoomOut.addEventListener('click', function() {
if (currentScale > 0.5) {
currentScale -= 0.25;
modalPhoto.style.transform = `scale(${currentScale})`;
}
});
zoomReset.addEventListener('click', function() {
currentScale = 1;
modalPhoto.style.transform = 'scale(1)';
});
}
function closePhotoModal() {
const modal = document.getElementById('photoModal');
modal.classList.add('hidden');
}
// Tutup modal dengan escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
closePhotoModal();
}
});
// Tutup modal dengan click di luar content
document.getElementById('photoModal')?.addEventListener('click', function(e) {
if (e.target === this) {
closePhotoModal();
}
});
</script>
@endsection