1501 lines
70 KiB
PHP
1501 lines
70 KiB
PHP
<html lang="id">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Report Data</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/flowbite/2.2.0/flowbite.min.css" rel="stylesheet" />
|
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/apexcharts"></script>
|
|
<style>
|
|
.table-responsive {
|
|
overflow-x: auto;
|
|
}
|
|
.badge {
|
|
margin-right: 5px;
|
|
}
|
|
#loading-indicator {
|
|
display: none;
|
|
position: fixed;
|
|
top: 10px;
|
|
right: 10px;
|
|
z-index: 1000;
|
|
}
|
|
.highlight {
|
|
animation: highlight-row 2s ease-in-out;
|
|
}
|
|
@keyframes highlight-row {
|
|
0% { background-color: #fff; }
|
|
50% { background-color: #d1ecf1; }
|
|
100% { background-color: #fff; }
|
|
}
|
|
/* Styling untuk multiple select */
|
|
select[multiple] {
|
|
max-height: 200px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
select[multiple] optgroup {
|
|
font-weight: 600;
|
|
color: #374151;
|
|
padding: 0.25rem 0;
|
|
}
|
|
|
|
select[multiple] option {
|
|
padding: 0.25rem 0.5rem;
|
|
margin: 0.125rem 0;
|
|
}
|
|
|
|
select[multiple] option:checked {
|
|
background-color: #2563eb;
|
|
color: white;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="bg-gray-50">
|
|
|
|
<div class="container mx-auto px-4 py-8">
|
|
<div class="flex justify-between items-center mb-6">
|
|
<h2 class="text-2xl font-bold text-gray-800">Laporan Keamanan dan Monitoring</h2>
|
|
<a href="{{route('welcome')}}" class="inline-flex items-center px-4 py-2 text-sm font-medium text-white bg-blue-700 rounded-lg hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300">
|
|
<svg class="w-4 h-4 mr-2" 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="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
|
|
</svg>
|
|
Kembali ke Dashboard
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Dashboard Overview - Single Comprehensive Chart -->
|
|
<div class="p-4 bg-white border border-gray-200 rounded-lg shadow-sm mb-6">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h5 class="text-lg font-medium text-gray-900">Ringkasan Status Sistem</h5>
|
|
<span class="text-sm text-gray-500" id="system-chart-count"></span>
|
|
</div>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div id="systemOverviewChart" class="w-full h-64 md:h-80"></div>
|
|
<div class="p-4 bg-gray-50 rounded-lg">
|
|
<h6 class="text-base font-medium text-gray-900 mb-3">Penjelasan Status</h6>
|
|
<ul class="space-y-2 text-sm">
|
|
<li class="flex items-center">
|
|
<span class="w-3 h-3 rounded-full bg-green-500 mr-2"></span>
|
|
<span><strong>Fan:</strong> Perubahan pada status kipas</span>
|
|
</li>
|
|
<li class="flex items-center">
|
|
<span class="w-3 h-3 rounded-full bg-red-500 mr-2"></span>
|
|
<span><strong>Status:</strong> Perubahan status keamanan (aman/bahaya)</span>
|
|
</li>
|
|
<li class="flex items-center">
|
|
<span class="w-3 h-3 rounded-full bg-yellow-500 mr-2"></span>
|
|
<span><strong>Motion:</strong> Perubahan status gerakan</span>
|
|
</li>
|
|
<li class="flex items-center">
|
|
<span class="w-3 h-3 rounded-full bg-cyan-500 mr-2"></span>
|
|
<span><strong>Servo Status:</strong> Perubahan status servo (terbuka/terkunci)</span>
|
|
</li>
|
|
<li class="flex items-center">
|
|
<span class="w-3 h-3 rounded-full bg-violet-400 mr-2"></span>
|
|
<span><strong>Last Access:</strong> Perubahan pada akses terakhir</span>
|
|
</li>
|
|
<li class="flex items-center">
|
|
<span class="w-3 h-3 rounded-full bg-orange-500 mr-2"></span>
|
|
<span><strong>Restart ESP:</strong> Perangkat ESP direstart</span>
|
|
</li>
|
|
<li class="flex items-center">
|
|
<span class="w-3 h-3 rounded-full bg-amber-500 mr-2"></span>
|
|
<span><strong>Restart Wemos:</strong> Perangkat Wemos direstart</span>
|
|
</li>
|
|
<li class="flex items-center">
|
|
<span class="w-3 h-3 rounded-full bg-indigo-500 mr-2"></span>
|
|
<span><strong>RFID:</strong> Perubahan pada status RFID</span>
|
|
</li>
|
|
<li class="flex items-center">
|
|
<span class="w-3 h-3 rounded-full bg-lime-500 mr-2"></span>
|
|
<span><strong>DHT:</strong> Perubahan pada sensor DHT</span>
|
|
</li>
|
|
<li class="flex items-center">
|
|
<span class="w-3 h-3 rounded-full bg-teal-500 mr-2"></span>
|
|
<span><strong>MPU:</strong> Perubahan pada sensor MPU</span>
|
|
</li>
|
|
<li class="flex items-center">
|
|
<span class="w-3 h-3 rounded-full bg-sky-500 mr-2"></span>
|
|
<span><strong>Servo Log:</strong> Perubahan pada log servo</span>
|
|
</li>
|
|
<li class="flex items-center">
|
|
<span class="w-3 h-3 rounded-full bg-rose-500 mr-2"></span>
|
|
<span><strong>System ESP:</strong> Perubahan status sistem ESP</span>
|
|
</li>
|
|
<li class="flex items-center">
|
|
<span class="w-3 h-3 rounded-full bg-pink-500 mr-2"></span>
|
|
<span><strong>System Wemos:</strong> Perubahan status sistem Wemos</span>
|
|
</li>
|
|
<li class="flex items-center">
|
|
<span class="w-3 h-3 rounded-full bg-gray-500 mr-2"></span>
|
|
<span><strong>Device Status:</strong> Perubahan status perangkat lainnya</span>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filter Panel Flowbite Style -->
|
|
<div class="p-4 mb-6 border border-gray-200 rounded-lg bg-white shadow-sm">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h5 class="text-lg font-medium text-gray-900">Filter Data</h5>
|
|
<button id="toggleFilterBtn" class="text-sm px-3 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 focus:ring-2 focus:ring-gray-300 flex items-center">
|
|
<svg class="w-4 h-4 mr-2" 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 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"></path>
|
|
</svg>
|
|
Filter
|
|
</button>
|
|
</div>
|
|
|
|
<div id="filterPanel" class="hidden">
|
|
<form id="filterForm" class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<!-- Tanggal -->
|
|
<div>
|
|
<label for="filterDate" class="block mb-2 text-sm font-medium text-gray-700">Tanggal</label>
|
|
<div class="relative">
|
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
|
|
<svg class="w-4 h-4 text-gray-500" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
|
<path d="M20 4a2 2 0 0 0-2-2h-2V1a1 1 0 0 0-2 0v1h-3V1a1 1 0 0 0-2 0v1H6V1a1 1 0 0 0-2 0v1H2a2 2 0 0 0-2 2v2h20V4ZM0 18a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V8H0v10Zm5-8h10a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2Z"/>
|
|
</svg>
|
|
</div>
|
|
<input type="date" id="filterDate" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Waktu / Jam dengan Time Range Picker Flowbite -->
|
|
<div>
|
|
<label for="filterTimeToggle" class="block mb-2 text-sm font-medium text-gray-700">Waktu</label>
|
|
|
|
<div class="flex items-center">
|
|
<!-- Toggle -->
|
|
<label class="relative inline-flex items-center cursor-pointer mr-3">
|
|
<input type="checkbox" id="filterTimeToggle" class="sr-only peer">
|
|
<div class="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
|
|
<span class="ms-2 text-sm font-medium text-gray-700" id="timeToggleStatus">Semua Waktu</span>
|
|
</label>
|
|
</div>
|
|
|
|
<!-- Time range picker (shown when toggle is active) -->
|
|
<div id="timeRangePicker" class="mt-2 hidden">
|
|
<div class="grid grid-cols-2 gap-3">
|
|
<div>
|
|
<label for="startTime" class="block mb-1 text-xs text-gray-500">Dari</label>
|
|
<div class="relative">
|
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
|
|
<svg class="w-4 h-4 text-gray-500" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
|
<path d="M10 0a10 10 0 1 0 10 10A10.011 10.011 0 0 0 10 0Zm3.982 13.982a1 1 0 0 1-1.414 0l-3.274-3.274A1.012 1.012 0 0 1 9 10V6a1 1 0 0 1 2 0v3.586l2.982 2.982a1 1 0 0 1 0 1.414Z"/>
|
|
</svg>
|
|
</div>
|
|
<input type="time" id="startTime" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5">
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label for="endTime" class="block mb-1 text-xs text-gray-500">Sampai</label>
|
|
<div class="relative">
|
|
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
|
|
<svg class="w-4 h-4 text-gray-500" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
|
<path d="M10 0a10 10 0 1 0 10 10A10.011 10.011 0 0 0 10 0Zm3.982 13.982a1 1 0 0 1-1.414 0l-3.274-3.274A1.012 1.012 0 0 1 9 10V6a1 1 0 0 1 2 0v3.586l2.982 2.982a1 1 0 0 1 0 1.414Z"/>
|
|
</svg>
|
|
</div>
|
|
<input type="time" id="endTime" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full ps-10 p-2.5">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tambahkan ini di dalam form filter, setelah Time Range Picker -->
|
|
<div class="md:col-span-2">
|
|
<label for="categories" class="block mb-2 text-sm font-medium text-gray-700">Filter Kategori</label>
|
|
<select id="categories" data-te-select-init multiple
|
|
class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5">
|
|
<optgroup label="Keamanan">
|
|
<option value="motion">Motion</option>
|
|
<option value="status">Status Keamanan</option>
|
|
<option value="fan">Fan</option>
|
|
</optgroup>
|
|
<optgroup label="Perangkat">
|
|
<option value="servo-status">Servo Status</option>
|
|
<option value="last-access">Last Access</option>
|
|
<option value="device">Device Status</option>
|
|
</optgroup>
|
|
<optgroup label="Kontrol">
|
|
<option value="restart-esp">Restart ESP</option>
|
|
<option value="restart-wemos">Restart Wemos</option>
|
|
</optgroup>
|
|
<optgroup label="Sensor">
|
|
<option value="rfid">RFID</option>
|
|
<option value="dht">DHT</option>
|
|
<option value="mpu">MPU</option>
|
|
</optgroup>
|
|
<optgroup label="Log">
|
|
<option value="servo-log">Servo Log</option>
|
|
<option value="system-esp">System ESP</option>
|
|
<option value="system-wemos">System Wemos</option>
|
|
</optgroup>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="md:col-span-2 flex justify-end space-x-2">
|
|
<button type="button" id="applyFilter" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5">
|
|
Terapkan Filter
|
|
</button>
|
|
<button type="button" id="resetFilter" class="py-2.5 px-5 text-sm font-medium text-gray-900 focus:outline-none bg-white rounded-lg border border-gray-200 hover:bg-gray-100 hover:text-blue-700 focus:z-10 focus:ring-4 focus:ring-gray-200">
|
|
Reset
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="loading-indicator" class="text-sm px-4 py-2 rounded-md bg-blue-100 text-blue-800">
|
|
<svg class="inline w-4 h-4 me-2 animate-spin" 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>
|
|
Memperbarui data...
|
|
</div>
|
|
|
|
<div id="reports-container">
|
|
<!-- Data akan diisi oleh AJAX -->
|
|
</div>
|
|
|
|
<!-- Pagination -->
|
|
<div id="pagination-container" class="flex items-center justify-between mt-4">
|
|
<div>
|
|
<span class="text-sm text-gray-700">
|
|
Menampilkan <span id="pagination-range">0-0</span> dari <span id="pagination-total">0</span> data
|
|
</span>
|
|
</div>
|
|
<div class="inline-flex mt-2 xs:mt-0">
|
|
<button id="prev-page" class="flex items-center justify-center px-3 h-8 text-sm font-medium text-white bg-gray-800 rounded-s hover:bg-gray-900 disabled:opacity-50 disabled:cursor-not-allowed">
|
|
<svg class="w-3.5 h-3.5 me-2 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 10">
|
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 5H1m0 0 4 4M1 5l4-4"/>
|
|
</svg>
|
|
Sebelumnya
|
|
</button>
|
|
<button id="next-page" class="flex items-center justify-center px-3 h-8 text-sm font-medium text-white bg-gray-800 border-0 border-s border-gray-700 rounded-e hover:bg-gray-900 disabled:opacity-50 disabled:cursor-not-allowed">
|
|
Selanjutnya
|
|
<svg class="w-3.5 h-3.5 ms-2 rtl:rotate-180" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 10">
|
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 5h12m0 0L9 1m4 4L9 9"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modal Detail Data -->
|
|
<div id="detailModal" tabindex="-1" aria-hidden="true" class="fixed top-0 left-0 right-0 z-50 hidden w-full p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-[calc(100%-1rem)] max-h-full">
|
|
<div class="relative w-full max-w-4xl max-h-full">
|
|
<div class="relative bg-white rounded-lg shadow">
|
|
<div class="flex items-center justify-between p-4 md:p-5 border-b rounded-t">
|
|
<h3 class="text-xl font-medium text-gray-900" id="detailModalLabel">
|
|
Detail Laporan
|
|
</h3>
|
|
<button type="button" class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center" data-modal-hide="detailModal">
|
|
<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>
|
|
<span class="sr-only">Close modal</span>
|
|
</button>
|
|
</div>
|
|
<div class="p-4 md:p-5 space-y-4" id="modalBodyContent">
|
|
<!-- Modal content akan diisi oleh JavaScript -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Template untuk tabel reports -->
|
|
<template id="reports-template">
|
|
<div class="relative overflow-x-auto shadow-md sm:rounded-lg">
|
|
<table id="reports-table" class="w-full text-sm text-left text-gray-500">
|
|
<thead class="text-xs text-gray-700 uppercase bg-gray-50">
|
|
<tr>
|
|
<th scope="col" class="px-6 py-3">#</th>
|
|
<th scope="col" class="px-6 py-3">Tanggal</th>
|
|
<th scope="col" class="px-6 py-3">Perubahan</th>
|
|
<th scope="col" class="px-6 py-3">Status</th>
|
|
<th scope="col" class="px-6 py-3">Info Selengkapnya</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="reports-body">
|
|
<!-- Rows will be inserted here by JavaScript -->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div id="no-data-alert" style="display: none;" class="p-4 mb-4 text-sm text-yellow-800 rounded-lg bg-yellow-50 mt-4 text-center">
|
|
Tidak ada data laporan.
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Template untuk baris report -->
|
|
<template id="report-row-template">
|
|
<tr class="bg-white border-b hover:bg-gray-50">
|
|
<td class="px-6 py-4 report-index"></td>
|
|
<td class="px-6 py-4 report-date"></td>
|
|
<td class="px-6 py-4 report-changes"></td>
|
|
<td class="px-6 py-4 report-status"></td>
|
|
<td class="px-6 py-4 report-actions">
|
|
<button class="view-detail text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-xs px-3 py-1.5">
|
|
Lihat Detail
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/flowbite/2.2.0/flowbite.min.js"></script>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Global variables
|
|
let allReports = [];
|
|
let currentPage = 1;
|
|
const pageSize = 10; // Jumlah item per halaman
|
|
let filteredReports = []; // Menyimpan hasil filter
|
|
let isFilterActive = false; // Flag untuk menandai apakah filter aktif
|
|
let selectedCategories = [];
|
|
|
|
// Variabel untuk menyimpan instance chart
|
|
let systemOverviewChart = null;
|
|
|
|
// Toggle filter panel
|
|
document.getElementById('toggleFilterBtn').addEventListener('click', function() {
|
|
const filterPanel = document.getElementById('filterPanel');
|
|
filterPanel.classList.toggle('hidden');
|
|
});
|
|
|
|
// Event listener for time filter toggle
|
|
const timeToggle = document.getElementById('filterTimeToggle');
|
|
const timeRangePicker = document.getElementById('timeRangePicker');
|
|
const timeToggleStatus = document.getElementById('timeToggleStatus');
|
|
|
|
timeToggle.addEventListener('change', function() {
|
|
if (this.checked) {
|
|
timeRangePicker.classList.remove('hidden');
|
|
timeToggleStatus.textContent = 'Rentang Waktu';
|
|
|
|
// Set default times if empty
|
|
if (!document.getElementById('startTime').value) {
|
|
document.getElementById('startTime').value = '00:00';
|
|
}
|
|
if (!document.getElementById('endTime').value) {
|
|
document.getElementById('endTime').value = '23:59';
|
|
}
|
|
} else {
|
|
timeRangePicker.classList.add('hidden');
|
|
timeToggleStatus.textContent = 'Semua Waktu';
|
|
}
|
|
});
|
|
|
|
// Inisialisasi - ambil data pertama kali
|
|
fetchReports();
|
|
|
|
// Gunakan interval polling yang lebih singkat dan tersembunyi
|
|
setInterval(function() {
|
|
fetchReportsQuietly();
|
|
}, 2000);
|
|
|
|
// Event listener untuk filter dan pagination
|
|
document.getElementById('applyFilter').addEventListener('click', applyFilters);
|
|
document.getElementById('resetFilter').addEventListener('click', resetFilters);
|
|
document.getElementById('prev-page').addEventListener('click', goToPrevPage);
|
|
document.getElementById('next-page').addEventListener('click', goToNextPage);
|
|
|
|
// Fungsi untuk halaman sebelumnya
|
|
function goToPrevPage() {
|
|
if (currentPage > 1) {
|
|
currentPage--;
|
|
renderPaginatedReports(false);
|
|
|
|
// Scroll ke atas tabel
|
|
document.getElementById('reports-table').scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'start'
|
|
});
|
|
}
|
|
}
|
|
|
|
// Fungsi untuk halaman selanjutnya
|
|
function goToNextPage() {
|
|
const totalPages = Math.ceil((filteredReports.length || allReports.length) / pageSize);
|
|
if (currentPage < totalPages) {
|
|
currentPage++;
|
|
renderPaginatedReports(false);
|
|
|
|
// Scroll ke atas tabel
|
|
document.getElementById('reports-table').scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'start'
|
|
});
|
|
}
|
|
}
|
|
|
|
// Fungsi untuk menerapkan filter
|
|
function applyFilters() {
|
|
const filterDate = document.getElementById('filterDate').value;
|
|
const timeFilterEnabled = document.getElementById('filterTimeToggle').checked;
|
|
const categories = Array.from(document.getElementById('categories').selectedOptions).map(opt => opt.value);
|
|
|
|
// Set flag filter aktif
|
|
isFilterActive = filterDate || timeFilterEnabled || categories.length > 0;
|
|
selectedCategories = categories;
|
|
|
|
let filteredData = allReports;
|
|
|
|
// Filter berdasarkan tanggal dan waktu
|
|
if (filterDate || timeFilterEnabled) {
|
|
filteredData = filteredData.filter(report => {
|
|
const reportDate = new Date(report.timestamp);
|
|
|
|
// Filter tanggal
|
|
if (filterDate) {
|
|
const dateStr = reportDate.toISOString().split('T')[0];
|
|
if (dateStr !== filterDate) return false;
|
|
}
|
|
|
|
// Filter waktu
|
|
if (timeFilterEnabled) {
|
|
const startTime = document.getElementById('startTime').value;
|
|
const endTime = document.getElementById('endTime').value;
|
|
const [startHour, startMinute] = startTime.split(':').map(Number);
|
|
const [endHour, endMinute] = endTime.split(':').map(Number);
|
|
|
|
const startTotalMinutes = startHour * 60 + startMinute;
|
|
const endTotalMinutes = endHour * 60 + endMinute;
|
|
|
|
const hour = reportDate.getHours();
|
|
const minute = reportDate.getMinutes();
|
|
const totalMinutes = hour * 60 + minute;
|
|
|
|
if (startTotalMinutes <= endTotalMinutes) {
|
|
if (totalMinutes < startTotalMinutes || totalMinutes > endTotalMinutes) return false;
|
|
} else {
|
|
if (totalMinutes < startTotalMinutes && totalMinutes > endTotalMinutes) return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
// Filter berdasarkan kategori
|
|
if (categories.length > 0) {
|
|
filteredData = filteredData.filter(report => {
|
|
// Deteksi perubahan untuk report ini
|
|
const changes = detectChanges(report, allReports, allReports.indexOf(report));
|
|
// Cek apakah ada perubahan yang masuk dalam kategori yang dipilih
|
|
return changes.some(change => categories.includes(change.badge));
|
|
});
|
|
}
|
|
|
|
// Update data terfilter
|
|
filteredReports = filteredData;
|
|
|
|
// Reset ke halaman pertama
|
|
currentPage = 1;
|
|
|
|
// Update chart
|
|
renderSystemOverviewChart(filteredReports);
|
|
|
|
// Render data
|
|
renderPaginatedReports(true);
|
|
|
|
// Tampilkan notifikasi hasil filter
|
|
const filterCount = filteredReports.length;
|
|
const totalCount = allReports.length;
|
|
|
|
let filterMessage = `Ditemukan ${filterCount} dari ${totalCount} data`;
|
|
if (categories.length > 0) {
|
|
filterMessage += ` dengan kategori: ${categories.join(', ')}`;
|
|
}
|
|
|
|
showFilterNotification(filterCount === 0 ? 'Tidak ada data yang cocok dengan filter' : filterMessage);
|
|
|
|
// Update status filter pada chart
|
|
updateChartFilterStatus();
|
|
}
|
|
|
|
// Fungsi untuk menampilkan notifikasi hasil filter
|
|
function showFilterNotification(message) {
|
|
// Cek apakah notifikasi sudah ada
|
|
let notification = document.getElementById('filter-notification');
|
|
|
|
// Jika belum ada, buat elemen baru
|
|
if (!notification) {
|
|
notification = document.createElement('div');
|
|
notification.id = 'filter-notification';
|
|
notification.className = 'p-2 mb-4 text-sm text-blue-800 rounded-lg bg-blue-50 mt-2';
|
|
document.getElementById('filterPanel').appendChild(notification);
|
|
}
|
|
|
|
// Isi pesan notifikasi
|
|
notification.textContent = message;
|
|
|
|
// Hapus notifikasi setelah 5 detik
|
|
setTimeout(() => {
|
|
notification.remove();
|
|
}, 5000);
|
|
}
|
|
|
|
// Fungsi untuk mereset filter
|
|
function resetFilters() {
|
|
document.getElementById('filterForm').reset();
|
|
document.getElementById('filterTimeToggle').checked = false;
|
|
document.getElementById('timeRangePicker').classList.add('hidden');
|
|
document.getElementById('timeToggleStatus').textContent = 'Semua Waktu';
|
|
document.getElementById('categories').selectedIndex = -1; // Reset multiple select
|
|
selectedCategories = [];
|
|
|
|
// Reset flag filter
|
|
isFilterActive = false;
|
|
|
|
// Reset ke semua data dan halaman pertama
|
|
filteredReports = allReports;
|
|
currentPage = 1;
|
|
|
|
// Update chart dengan semua data
|
|
renderSystemOverviewChart(allReports);
|
|
|
|
// Update chart filter status
|
|
document.getElementById('system-chart-count').textContent = `Total: ${allReports.length} laporan`;
|
|
|
|
// Render reports
|
|
renderPaginatedReports();
|
|
}
|
|
|
|
// Fungsi untuk mengambil data report terbaru secara diam-diam
|
|
function fetchReportsQuietly() {
|
|
fetch('/api/reports', {
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
'X-Requested-With': 'XMLHttpRequest'
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
// Cek apakah ada data baru
|
|
const hasNewData = checkForNewData(allReports, data);
|
|
|
|
// Perbarui data global
|
|
allReports = data;
|
|
|
|
// Jika filter aktif, terapkan filter tanpa mereset halaman
|
|
if (isFilterActive) {
|
|
// Simpan halaman saat ini
|
|
const savedPage = currentPage;
|
|
|
|
// Terapkan filter ulang dengan data baru
|
|
const filterDate = document.getElementById('filterDate').value;
|
|
const timeFilterEnabled = document.getElementById('filterTimeToggle').checked;
|
|
let startTime = null;
|
|
let endTime = null;
|
|
|
|
if (timeFilterEnabled) {
|
|
startTime = document.getElementById('startTime').value;
|
|
endTime = document.getElementById('endTime').value;
|
|
|
|
// Convert to minutes for easier comparison
|
|
const [startHour, startMinute] = startTime.split(':').map(Number);
|
|
const [endHour, endMinute] = endTime.split(':').map(Number);
|
|
|
|
const startTotalMinutes = startHour * 60 + startMinute;
|
|
const endTotalMinutes = endHour * 60 + endMinute;
|
|
|
|
filteredReports = allReports.filter(report => {
|
|
const reportDate = new Date(report.timestamp);
|
|
|
|
// Filter berdasarkan tanggal
|
|
if (filterDate) {
|
|
const dateStr = reportDate.toISOString().split('T')[0];
|
|
if (dateStr !== filterDate) return false;
|
|
}
|
|
|
|
// Filter berdasarkan rentang waktu
|
|
if (timeFilterEnabled) {
|
|
const hour = reportDate.getHours();
|
|
const minute = reportDate.getMinutes();
|
|
const totalMinutes = hour * 60 + minute;
|
|
|
|
if (startTotalMinutes <= endTotalMinutes) {
|
|
if (totalMinutes < startTotalMinutes || totalMinutes > endTotalMinutes) return false;
|
|
} else {
|
|
if (totalMinutes < startTotalMinutes && totalMinutes > endTotalMinutes) return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
});
|
|
} else {
|
|
filteredReports = allReports.filter(report => {
|
|
const reportDate = new Date(report.timestamp);
|
|
|
|
if (filterDate) {
|
|
const dateStr = reportDate.toISOString().split('T')[0];
|
|
if (dateStr !== filterDate) return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
// Perbarui chart
|
|
if (hasNewData) {
|
|
renderSystemOverviewChart(filteredReports);
|
|
}
|
|
|
|
// Kembalikan halaman ke posisi sebelumnya
|
|
currentPage = savedPage;
|
|
|
|
// Periksa apakah halaman saat ini masih valid
|
|
const totalPages = Math.ceil(filteredReports.length / pageSize);
|
|
if (currentPage > totalPages && totalPages > 0) {
|
|
currentPage = totalPages;
|
|
}
|
|
|
|
// Render ulang dengan flag false untuk tidak mereset halaman
|
|
renderPaginatedReports(false);
|
|
|
|
// Update filter badge
|
|
updateChartFilterStatus();
|
|
} else {
|
|
// Jika tidak ada filter, gunakan semua data
|
|
filteredReports = allReports;
|
|
|
|
// Update chart dengan semua data
|
|
if (hasNewData) {
|
|
renderSystemOverviewChart(allReports);
|
|
updateReportsIfChanged(false);
|
|
}
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error fetching reports:', error);
|
|
});
|
|
}
|
|
|
|
// Fungsi untuk memeriksa apakah ada data baru
|
|
function checkForNewData(oldData, newData) {
|
|
if (!oldData || !newData) return false;
|
|
|
|
// Cek apakah jumlah data berubah
|
|
if (oldData.length !== newData.length) return true;
|
|
|
|
// Cek apakah ada ID baru
|
|
const oldIds = new Set(oldData.map(item => item.id));
|
|
return newData.some(item => !oldIds.has(item.id));
|
|
}
|
|
|
|
// Fungsi untuk membandingkan data baru dengan yang sudah ada,
|
|
// dan hanya memperbarui jika ada perubahan
|
|
function updateReportsIfChanged(resetPage = true) {
|
|
// Perbarui data terfilter
|
|
if (!isFilterActive) {
|
|
filteredReports = allReports;
|
|
}
|
|
|
|
// Hitung total halaman berdasarkan data terfilter
|
|
const totalPages = Math.ceil(filteredReports.length / pageSize);
|
|
|
|
// Jika halaman saat ini lebih besar dari total halaman, reset ke halaman terakhir
|
|
if (currentPage > totalPages && totalPages > 0 && resetPage) {
|
|
currentPage = totalPages;
|
|
}
|
|
|
|
// Render ulang halaman dengan data yang sudah difilter
|
|
renderPaginatedReports(resetPage);
|
|
}
|
|
|
|
// Fungsi untuk mengambil data report terbaru dengan indikator loading
|
|
function fetchReports() {
|
|
document.getElementById('loading-indicator').style.display = 'block';
|
|
|
|
fetch('/api/reports', {
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
'X-Requested-With': 'XMLHttpRequest'
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
allReports = data; // Simpan semua data untuk filtering
|
|
filteredReports = data; // Inisialisasi data yang difilter dengan semua data
|
|
|
|
// Render dashboard chart
|
|
renderSystemOverviewChart(data);
|
|
|
|
renderPaginatedReports();
|
|
document.getElementById('loading-indicator').style.display = 'none';
|
|
})
|
|
.catch(error => {
|
|
console.error('Error fetching reports:', error);
|
|
document.getElementById('loading-indicator').style.display = 'none';
|
|
});
|
|
}
|
|
|
|
// Render reports dengan pagination
|
|
function renderPaginatedReports(resetPage = true) {
|
|
// Hitung data untuk halaman saat ini
|
|
const dataToShow = filteredReports.length > 0 ? filteredReports : allReports;
|
|
const totalPages = Math.ceil(dataToShow.length / pageSize);
|
|
|
|
// Pastikan currentPage tidak melebihi totalPages
|
|
if ((currentPage > totalPages && totalPages > 0) && resetPage) {
|
|
currentPage = totalPages;
|
|
}
|
|
|
|
const startIndex = (currentPage - 1) * pageSize;
|
|
const endIndex = Math.min(startIndex + pageSize, dataToShow.length);
|
|
const paginatedData = dataToShow.slice(startIndex, endIndex);
|
|
|
|
// Perbarui informasi pagination
|
|
document.getElementById('pagination-range').textContent =
|
|
dataToShow.length > 0 ? `${startIndex + 1}-${endIndex}` : '0-0';
|
|
document.getElementById('pagination-total').textContent = dataToShow.length;
|
|
|
|
// Aktifkan/nonaktifkan tombol pagination
|
|
document.getElementById('prev-page').disabled = currentPage === 1;
|
|
document.getElementById('next-page').disabled = currentPage >= totalPages;
|
|
|
|
// Render data yang telah dipaginasi
|
|
renderReports(paginatedData);
|
|
}
|
|
|
|
// Render reports table dengan data yang diterima
|
|
function renderReports(reports) {
|
|
const container = document.getElementById('reports-container');
|
|
|
|
// Clone template tabel jika belum ada
|
|
if (!document.getElementById('reports-table')) {
|
|
const template = document.getElementById('reports-template');
|
|
container.innerHTML = '';
|
|
container.appendChild(template.content.cloneNode(true));
|
|
}
|
|
|
|
const tableBody = document.getElementById('reports-body');
|
|
const noDataAlert = document.getElementById('no-data-alert');
|
|
|
|
// Tampilkan pesan jika tidak ada data
|
|
if (!reports || reports.length === 0) {
|
|
tableBody.innerHTML = '';
|
|
noDataAlert.style.display = 'block';
|
|
document.getElementById('pagination-container').style.display = 'none';
|
|
return;
|
|
}
|
|
|
|
noDataAlert.style.display = 'none';
|
|
document.getElementById('pagination-container').style.display = 'flex';
|
|
|
|
// Bersihkan tabel
|
|
tableBody.innerHTML = '';
|
|
|
|
// Tambahkan baris untuk setiap report dengan indeks mulai dari (currentPage-1) * pageSize + 1
|
|
reports.forEach((report, index) => {
|
|
const template = document.getElementById('report-row-template');
|
|
const row = template.content.cloneNode(true).querySelector('tr');
|
|
|
|
// Set attributes dan data report
|
|
row.dataset.reportId = report.id;
|
|
row.dataset.reportData = JSON.stringify(report);
|
|
|
|
// Isi konten baris, indeks dimulai dari indeks halaman saat ini
|
|
row.querySelector('.report-index').textContent = (currentPage - 1) * pageSize + index + 1;
|
|
row.querySelector('.report-date').textContent = formatDate(report.timestamp);
|
|
|
|
// Isi badges untuk perubahan
|
|
const changesCell = row.querySelector('.report-changes');
|
|
changesCell.innerHTML = '';
|
|
|
|
// Deteksi perubahan
|
|
const changes = detectChanges(report, allReports, allReports.indexOf(report));
|
|
changes.forEach(change => {
|
|
const badge = document.createElement('span');
|
|
|
|
// Flowbite badge styles dengan warna khusus untuk setiap jenis
|
|
const badgeClasses = {
|
|
'primary': 'bg-blue-100 text-blue-800',
|
|
'motion': 'bg-yellow-100 text-yellow-800',
|
|
'status': 'bg-red-100 text-red-800',
|
|
'fan': 'bg-green-100 text-green-800',
|
|
'last-access': 'bg-violet-100 text-violet-800',
|
|
'servo-status': 'bg-cyan-100 text-cyan-800',
|
|
'restart-esp': 'bg-orange-100 text-orange-800',
|
|
'restart-wemos': 'bg-amber-100 text-amber-800',
|
|
'rfid': 'bg-indigo-100 text-indigo-800',
|
|
'dht': 'bg-lime-100 text-lime-800',
|
|
'mpu': 'bg-teal-100 text-teal-800',
|
|
'servo-log': 'bg-sky-100 text-sky-800',
|
|
'system-esp': 'bg-rose-100 text-rose-800',
|
|
'system-wemos': 'bg-pink-100 text-pink-800',
|
|
'device': 'bg-gray-100 text-gray-800',
|
|
'light': 'bg-gray-100 text-gray-400'
|
|
};
|
|
|
|
badge.className = `px-2 py-0.5 rounded text-xs font-medium me-2 ${badgeClasses[change.badge] || badgeClasses['light']}`;
|
|
badge.textContent = change.type;
|
|
changesCell.appendChild(badge);
|
|
});
|
|
|
|
// Isi status
|
|
const statusCell = row.querySelector('.report-status');
|
|
statusCell.innerHTML = '';
|
|
|
|
if (report.security && report.security.motion) {
|
|
const motionText = document.createElement('div');
|
|
motionText.className = 'mb-1';
|
|
motionText.innerHTML = `<strong>Gerakan:</strong> ${firstUpper(report.security.motion)}`;
|
|
statusCell.appendChild(motionText);
|
|
}
|
|
|
|
if (report.security && report.security.status) {
|
|
const securityStatus = document.createElement('div');
|
|
securityStatus.className = 'mb-1';
|
|
securityStatus.innerHTML = `<strong>Keamanan:</strong> ${firstUpper(report.security.status)}`;
|
|
statusCell.appendChild(securityStatus);
|
|
}
|
|
|
|
if (report.smartcab && report.smartcab.servo_status) {
|
|
const servoStatus = document.createElement('div');
|
|
servoStatus.className = 'mb-1';
|
|
servoStatus.innerHTML = `<strong>Servo:</strong> ${report.smartcab.servo_status}`;
|
|
statusCell.appendChild(servoStatus);
|
|
}
|
|
|
|
// Set action untuk detail
|
|
const viewBtn = row.querySelector('.view-detail');
|
|
viewBtn.onclick = function() {
|
|
showDetail(report);
|
|
};
|
|
|
|
tableBody.appendChild(row);
|
|
});
|
|
}
|
|
|
|
// Fungsi untuk mendeteksi perubahan pada report
|
|
function detectChanges(report, reports, index) {
|
|
const changes = [];
|
|
|
|
if (index === 0 || !reports[index + 1]) {
|
|
changes.push({ type: 'Data awal', badge: 'primary' });
|
|
return changes;
|
|
}
|
|
|
|
const prevReport = reports[index + 1]; // Baris sebelumnya (karena data sorted terbaru dulu)
|
|
|
|
// Security changes
|
|
if (report.security && prevReport.security) {
|
|
if (report.security.motion !== prevReport.security.motion) {
|
|
changes.push({ type: 'Motion', badge: 'motion' });
|
|
}
|
|
|
|
if (report.security.status !== prevReport.security.status) {
|
|
changes.push({ type: 'Status', badge: 'status' });
|
|
}
|
|
|
|
if (report.security.fan !== prevReport.security.fan) {
|
|
changes.push({ type: 'Fan', badge: 'fan' });
|
|
}
|
|
}
|
|
|
|
// Smartcab changes
|
|
if (report.smartcab && prevReport.smartcab) {
|
|
if (report.smartcab.last_access !== prevReport.smartcab.last_access) {
|
|
changes.push({ type: 'Last Access', badge: 'last-access' });
|
|
}
|
|
|
|
if (report.smartcab.servo_status !== prevReport.smartcab.servo_status) {
|
|
changes.push({ type: 'Servo Status', badge: 'servo-status' });
|
|
}
|
|
}
|
|
|
|
// Control changes
|
|
if (report.control && prevReport.control) {
|
|
if (report.control.restartESP !== prevReport.control.restartESP) {
|
|
changes.push({ type: 'Restart ESP', badge: 'restart-esp' });
|
|
}
|
|
|
|
if (report.control.restartWemos !== prevReport.control.restartWemos) {
|
|
changes.push({ type: 'Restart Wemos', badge: 'restart-wemos' });
|
|
}
|
|
}
|
|
|
|
// Logs changes
|
|
if (report.logs && prevReport.logs) {
|
|
// RFID logs
|
|
if (report.logs.RFID && prevReport.logs.RFID &&
|
|
JSON.stringify(report.logs.RFID) !== JSON.stringify(prevReport.logs.RFID)) {
|
|
changes.push({ type: 'RFID', badge: 'rfid' });
|
|
}
|
|
|
|
// DHT logs
|
|
if (report.logs.dht && prevReport.logs.dht &&
|
|
JSON.stringify(report.logs.dht) !== JSON.stringify(prevReport.logs.dht)) {
|
|
changes.push({ type: 'DHT', badge: 'dht' });
|
|
}
|
|
|
|
// MPU logs
|
|
if (report.logs.mpu && prevReport.logs.mpu &&
|
|
JSON.stringify(report.logs.mpu) !== JSON.stringify(prevReport.logs.mpu)) {
|
|
changes.push({ type: 'MPU', badge: 'mpu' });
|
|
}
|
|
|
|
// Servo logs
|
|
if (report.logs.servo && prevReport.logs.servo &&
|
|
JSON.stringify(report.logs.servo) !== JSON.stringify(prevReport.logs.servo)) {
|
|
changes.push({ type: 'Servo Log', badge: 'servo-log' });
|
|
}
|
|
|
|
// System ESP logs
|
|
if (report.logs.systemESP !== prevReport.logs.systemESP) {
|
|
changes.push({ type: 'System ESP', badge: 'system-esp' });
|
|
}
|
|
|
|
// System Wemos logs
|
|
if (report.logs.systemWemos !== prevReport.logs.systemWemos) {
|
|
changes.push({ type: 'System Wemos', badge: 'system-wemos' });
|
|
}
|
|
}
|
|
|
|
// Device changes - general fallback if needed
|
|
if (report.device && prevReport.device &&
|
|
JSON.stringify(report.device) !== JSON.stringify(prevReport.device)) {
|
|
changes.push({ type: 'Device Status', badge: 'device' });
|
|
}
|
|
|
|
if (changes.length === 0) {
|
|
changes.push({ type: 'Tidak ada perubahan', badge: 'light' });
|
|
}
|
|
|
|
return changes;
|
|
}
|
|
|
|
// Utility function to format date
|
|
function formatDate(dateString) {
|
|
const date = new Date(dateString);
|
|
return date.toLocaleDateString('id-ID', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit'
|
|
}).replace(',', '');
|
|
}
|
|
|
|
// First letter uppercase
|
|
function firstUpper(string) {
|
|
if (!string) return '';
|
|
return string.charAt(0).toUpperCase() + string.slice(1);
|
|
}
|
|
|
|
// Fungsi untuk menampilkan detail dalam modal
|
|
function showDetail(report) {
|
|
let formattedDate = new Date(report.timestamp).toLocaleString('id-ID');
|
|
|
|
// Security section
|
|
let securityHtml = '';
|
|
if (report.security) {
|
|
securityHtml = `
|
|
<div class="mb-4">
|
|
<h5 class="text-lg font-medium text-gray-900 mb-2">Keamanan:</h5>
|
|
<ul class="space-y-2">
|
|
<li class="p-3 bg-gray-50 rounded-lg"><strong class="text-gray-700">Gerakan:</strong> ${report.security.motion || 'N/A'}</li>
|
|
<li class="p-3 bg-gray-50 rounded-lg"><strong class="text-gray-700">Status:</strong> ${report.security.status || 'N/A'}</li>
|
|
<li class="p-3 bg-gray-50 rounded-lg"><strong class="text-gray-700">Fan:</strong> ${report.security.fan || 'N/A'}</li>
|
|
</ul>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Smartcab section
|
|
let smartcabHtml = '';
|
|
if (report.smartcab) {
|
|
smartcabHtml = `
|
|
<div class="mb-4">
|
|
<h5 class="text-lg font-medium text-gray-900 mb-2">Smartcab:</h5>
|
|
<ul class="space-y-2">
|
|
<li class="p-3 bg-gray-50 rounded-lg"><strong class="text-gray-700">Akses Terakhir:</strong> ${report.smartcab.last_access || 'N/A'}</li>
|
|
<li class="p-3 bg-gray-50 rounded-lg"><strong class="text-gray-700">Status Servo:</strong> ${report.smartcab.servo_status || 'N/A'}</li>
|
|
<li class="p-3 bg-gray-50 rounded-lg"><strong class="text-gray-700">Status Perangkat:</strong> ${report.smartcab.status_device || 'N/A'}</li>
|
|
</ul>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// DHT11 section
|
|
let dht11Html = '';
|
|
if (report.dht11) {
|
|
dht11Html = `
|
|
<div class="mb-4">
|
|
<h5 class="text-lg font-medium text-gray-900 mb-2">Sensor DHT11:</h5>
|
|
<ul class="space-y-2">
|
|
<li class="p-3 bg-gray-50 rounded-lg"><strong class="text-gray-700">Kelembaban:</strong> ${report.dht11.humidity || 'N/A'}%</li>
|
|
<li class="p-3 bg-gray-50 rounded-lg"><strong class="text-gray-700">Suhu:</strong> ${report.dht11.temperature || 'N/A'}°C</li>
|
|
</ul>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Control section
|
|
let controlHtml = '';
|
|
if (report.control) {
|
|
controlHtml = `
|
|
<div class="mb-4">
|
|
<h5 class="text-lg font-medium text-gray-900 mb-2">Kontrol:</h5>
|
|
<ul class="space-y-2">
|
|
<li class="p-3 bg-gray-50 rounded-lg"><strong class="text-gray-700">Restart ESP:</strong> ${report.control.restartESP ? 'Ya' : 'Tidak'}</li>
|
|
<li class="p-3 bg-gray-50 rounded-lg"><strong class="text-gray-700">Restart Wemos:</strong> ${report.control.restartWemos ? 'Ya' : 'Tidak'}</li>
|
|
</ul>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Device section
|
|
let deviceHtml = '';
|
|
if (report.device) {
|
|
deviceHtml = `
|
|
<div class="mb-4">
|
|
<h5 class="text-lg font-medium text-gray-900 mb-2">Status Perangkat:</h5>
|
|
<ul class="space-y-2">
|
|
<li class="p-3 bg-gray-50 rounded-lg"><strong class="text-gray-700">Last Active:</strong> ${report.device.lastActive || 'N/A'}</li>
|
|
<li class="p-3 bg-gray-50 rounded-lg"><strong class="text-gray-700">Last Active Wemos:</strong> ${report.device.lastActiveWemos || 'N/A'}</li>
|
|
</ul>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Logs section
|
|
let logsHtml = '';
|
|
if (report.logs) {
|
|
logsHtml = `
|
|
<div class="mb-4">
|
|
<h5 class="text-lg font-medium text-gray-900 mb-2">Logs Sistem:</h5>
|
|
<ul class="space-y-2">
|
|
`;
|
|
|
|
// RFID
|
|
if (report.logs.RFID) {
|
|
logsHtml += `<li class="p-3 bg-gray-50 rounded-lg"><strong class="text-gray-700">RFID:</strong> ${report.logs.RFID.status || 'N/A'}</li>`;
|
|
}
|
|
|
|
// DHT
|
|
if (report.logs.dht) {
|
|
logsHtml += `
|
|
<li class="p-3 bg-gray-50 rounded-lg">
|
|
<strong class="text-gray-700">DHT:</strong> ${report.logs.dht.status || 'N/A'}<br>
|
|
<span class="text-xs text-gray-500">${report.logs.dht.message || ''}</span>
|
|
</li>
|
|
`;
|
|
}
|
|
|
|
// MPU
|
|
if (report.logs.mpu) {
|
|
logsHtml += `
|
|
<li class="p-3 bg-gray-50 rounded-lg">
|
|
<strong class="text-gray-700">MPU:</strong> ${report.logs.mpu.status || 'N/A'}<br>
|
|
<span class="text-xs text-gray-500">${report.logs.mpu.message || ''}</span>
|
|
</li>
|
|
`;
|
|
}
|
|
|
|
// Servo
|
|
if (report.logs.servo) {
|
|
logsHtml += `<li class="p-3 bg-gray-50 rounded-lg"><strong class="text-gray-700">Servo:</strong> ${report.logs.servo.status || 'N/A'}</li>`;
|
|
}
|
|
|
|
// System status
|
|
if (report.logs.systemESP) {
|
|
logsHtml += `<li class="p-3 bg-gray-50 rounded-lg"><strong class="text-gray-700">System ESP:</strong> ${report.logs.systemESP || 'N/A'}</li>`;
|
|
}
|
|
|
|
if (report.logs.systemWemos) {
|
|
logsHtml += `<li class="p-3 bg-gray-50 rounded-lg"><strong class="text-gray-700">System Wemos:</strong> ${report.logs.systemWemos || 'N/A'}</li>`;
|
|
}
|
|
|
|
logsHtml += `</ul></div>`;
|
|
}
|
|
|
|
// Combine all HTML sections
|
|
let detailHtml = `
|
|
<div class="p-4 mb-4 text-sm text-blue-800 rounded-lg bg-blue-50">
|
|
<div class="font-medium">ID: ${report.id}</div>
|
|
<div>Tanggal Waktu: ${formattedDate}</div>
|
|
</div>
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<div>
|
|
${securityHtml}
|
|
${smartcabHtml}
|
|
${dht11Html}
|
|
</div>
|
|
<div>
|
|
${controlHtml}
|
|
${deviceHtml}
|
|
${logsHtml}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
document.getElementById('modalBodyContent').innerHTML = detailHtml;
|
|
|
|
// Gunakan Flowbite modal sebagai gantinya
|
|
const modalElement = document.getElementById('detailModal');
|
|
const modalOptions = {
|
|
placement: 'center',
|
|
backdrop: 'dynamic',
|
|
backdropClasses: 'bg-gray-900/50 fixed inset-0 z-40',
|
|
closable: true
|
|
};
|
|
|
|
// Buka modal menggunakan Flowbite
|
|
const modal = new Modal(modalElement, modalOptions);
|
|
modal.show();
|
|
}
|
|
|
|
// Fungsi untuk analisis keseluruhan sistem
|
|
function analyzeSystemOverview(reports) {
|
|
// Siapkan variabel untuk menyimpan hitungan status
|
|
const statusCounts = {
|
|
'motion': 0, // Motion status
|
|
'status': 0, // Security status
|
|
'fan': 0, // Fan status
|
|
'servoStatus': 0, // Servo status
|
|
'lastAccess': 0, // Last access
|
|
'restartEsp': 0, // Restart ESP
|
|
'restartWemos': 0, // Restart Wemos
|
|
'rfid': 0, // RFID
|
|
'dht': 0, // DHT sensor
|
|
'mpu': 0, // MPU sensor
|
|
'servoLog': 0, // Servo log
|
|
'systemEsp': 0, // System ESP
|
|
'systemWemos': 0, // System Wemos
|
|
'deviceStatus': 0 // Device status
|
|
};
|
|
|
|
// Total untuk persentase
|
|
let totalCounts = 0;
|
|
|
|
// Fungsi untuk menghitung perubahan antar report
|
|
function countChanges(report, index) {
|
|
if (index === 0 || index >= reports.length - 1) return [];
|
|
|
|
const changes = [];
|
|
const prevReport = reports[index + 1]; // Data baris sebelumnya (karena data sorted terbaru dulu)
|
|
|
|
// Security changes
|
|
if (report.security && prevReport.security) {
|
|
if (report.security.motion !== prevReport.security.motion) {
|
|
changes.push('motion');
|
|
}
|
|
|
|
if (report.security.status !== prevReport.security.status) {
|
|
changes.push('status');
|
|
}
|
|
|
|
if (report.security.fan !== prevReport.security.fan) {
|
|
changes.push('fan');
|
|
}
|
|
}
|
|
|
|
// Smartcab changes
|
|
if (report.smartcab && prevReport.smartcab) {
|
|
if (report.smartcab.last_access !== prevReport.smartcab.last_access) {
|
|
changes.push('lastAccess');
|
|
}
|
|
|
|
if (report.smartcab.servo_status !== prevReport.smartcab.servo_status) {
|
|
changes.push('servoStatus');
|
|
}
|
|
}
|
|
|
|
// Control changes
|
|
if (report.control && prevReport.control) {
|
|
if (report.control.restartESP !== prevReport.control.restartESP) {
|
|
changes.push('restartEsp');
|
|
}
|
|
|
|
if (report.control.restartWemos !== prevReport.control.restartWemos) {
|
|
changes.push('restartWemos');
|
|
}
|
|
}
|
|
|
|
// Logs changes
|
|
if (report.logs && prevReport.logs) {
|
|
// RFID logs
|
|
if (report.logs.RFID && prevReport.logs.RFID &&
|
|
JSON.stringify(report.logs.RFID) !== JSON.stringify(prevReport.logs.RFID)) {
|
|
changes.push('rfid');
|
|
}
|
|
|
|
// DHT logs
|
|
if (report.logs.dht && prevReport.logs.dht &&
|
|
JSON.stringify(report.logs.dht) !== JSON.stringify(prevReport.logs.dht)) {
|
|
changes.push('dht');
|
|
}
|
|
|
|
// MPU logs
|
|
if (report.logs.mpu && prevReport.logs.mpu &&
|
|
JSON.stringify(report.logs.mpu) !== JSON.stringify(prevReport.logs.mpu)) {
|
|
changes.push('mpu');
|
|
}
|
|
|
|
// Servo logs
|
|
if (report.logs.servo && prevReport.logs.servo &&
|
|
JSON.stringify(report.logs.servo) !== JSON.stringify(prevReport.logs.servo)) {
|
|
changes.push('servoLog');
|
|
}
|
|
|
|
// System ESP logs
|
|
if (report.logs.systemESP !== prevReport.logs.systemESP) {
|
|
changes.push('systemEsp');
|
|
}
|
|
|
|
// System Wemos logs
|
|
if (report.logs.systemWemos !== prevReport.logs.systemWemos) {
|
|
changes.push('systemWemos');
|
|
}
|
|
}
|
|
|
|
// Device changes - general fallback if needed
|
|
if (report.device && prevReport.device &&
|
|
JSON.stringify(report.device) !== JSON.stringify(prevReport.device)) {
|
|
changes.push('deviceStatus');
|
|
}
|
|
|
|
return changes;
|
|
}
|
|
|
|
// Periksa setiap laporan untuk mengisi kategori
|
|
reports.forEach((report, index) => {
|
|
const changes = countChanges(report, index);
|
|
|
|
changes.forEach(change => {
|
|
statusCounts[change]++;
|
|
totalCounts++;
|
|
});
|
|
|
|
// Jika tidak ada perubahan, tambahkan satu ke total untuk report ini
|
|
if (changes.length === 0 && index > 0 && index < reports.length - 1) {
|
|
totalCounts++;
|
|
}
|
|
});
|
|
|
|
// Update counter
|
|
document.getElementById('system-chart-count').textContent =
|
|
`Total: ${reports.length} laporan dengan ${totalCounts} perubahan status`;
|
|
|
|
// Kembalikan array untuk chart dengan hanya nilai yang bukan nol
|
|
const resultData = [
|
|
statusCounts['fan'],
|
|
statusCounts['status'],
|
|
statusCounts['motion'],
|
|
statusCounts['servoStatus'],
|
|
statusCounts['lastAccess'],
|
|
statusCounts['restartEsp'],
|
|
statusCounts['restartWemos'],
|
|
statusCounts['rfid'],
|
|
statusCounts['dht'],
|
|
statusCounts['mpu'],
|
|
statusCounts['servoLog'],
|
|
statusCounts['systemEsp'],
|
|
statusCounts['systemWemos'],
|
|
statusCounts['deviceStatus']
|
|
];
|
|
|
|
// Label yang sesuai dengan resultData
|
|
const resultLabels = [
|
|
'Fan',
|
|
'Status Keamanan',
|
|
'Motion',
|
|
'Servo Status',
|
|
'Last Access',
|
|
'Restart ESP',
|
|
'Restart Wemos',
|
|
'RFID',
|
|
'DHT',
|
|
'MPU',
|
|
'Servo Log',
|
|
'System ESP',
|
|
'System Wemos',
|
|
'Device Status'
|
|
];
|
|
|
|
// Warna yang sesuai dengan resultData (sesuai dengan badge colors)
|
|
const resultColors = [
|
|
'#22c55e', // Fan (Hijau)
|
|
'#ef4444', // Status (Merah)
|
|
'#eab308', // Motion (Kuning)
|
|
'#06b6d4', // Servo Status (Cyan)
|
|
'#8b5cf6', // Last Access (Ungu Muda)
|
|
'#f97316', // Restart ESP (Oranye)
|
|
'#f59e0b', // Restart Wemos (Amber)
|
|
'#6366f1', // RFID (Indigo)
|
|
'#84cc16', // DHT (Lime)
|
|
'#14b8a6', // MPU (Teal)
|
|
'#0ea5e9', // Servo Log (Sky)
|
|
'#e11d48', // System ESP (Rose)
|
|
'#ec4899', // System Wemos (Pink)
|
|
'#6b7280' // Device Status (Gray)
|
|
];
|
|
|
|
// Filter untuk menghilangkan kategori dengan nilai nol
|
|
const filteredData = [];
|
|
const filteredLabels = [];
|
|
const filteredColors = [];
|
|
|
|
for (let i = 0; i < resultData.length; i++) {
|
|
if (resultData[i] > 0) {
|
|
filteredData.push(resultData[i]);
|
|
filteredLabels.push(resultLabels[i]);
|
|
filteredColors.push(resultColors[i]);
|
|
}
|
|
}
|
|
|
|
return {
|
|
series: filteredData,
|
|
labels: filteredLabels,
|
|
colors: filteredColors
|
|
};
|
|
}
|
|
|
|
// Render donut chart untuk overview sistem
|
|
function renderSystemOverviewChart(data) {
|
|
const chartInfo = analyzeSystemOverview(data);
|
|
|
|
const options = {
|
|
series: chartInfo.series,
|
|
chart: {
|
|
type: 'donut',
|
|
height: 350
|
|
},
|
|
labels: chartInfo.labels,
|
|
colors: chartInfo.colors,
|
|
legend: {
|
|
position: 'bottom',
|
|
fontSize: '14px',
|
|
offsetY: 10,
|
|
itemMargin: {
|
|
horizontal: 5,
|
|
vertical: 3
|
|
}
|
|
},
|
|
plotOptions: {
|
|
pie: {
|
|
donut: {
|
|
size: '65%',
|
|
labels: {
|
|
show: true,
|
|
name: {
|
|
show: true,
|
|
fontSize: '18px',
|
|
fontFamily: 'Helvetica, Arial, sans-serif',
|
|
fontWeight: 600,
|
|
offsetY: -10
|
|
},
|
|
value: {
|
|
show: true,
|
|
fontSize: '16px',
|
|
fontFamily: 'Helvetica, Arial, sans-serif',
|
|
fontWeight: 400,
|
|
offsetY: 5
|
|
},
|
|
total: {
|
|
show: true,
|
|
showAlways: true,
|
|
label: 'Total Status',
|
|
fontSize: '18px',
|
|
fontFamily: 'Helvetica, Arial, sans-serif',
|
|
fontWeight: 600,
|
|
formatter: function (w) {
|
|
return w.globals.seriesTotals.reduce((a, b) => a + b, 0);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
},
|
|
responsive: [{
|
|
breakpoint: 480,
|
|
options: {
|
|
chart: {
|
|
height: 280
|
|
},
|
|
legend: {
|
|
position: 'bottom',
|
|
offsetY: 0
|
|
}
|
|
}
|
|
}],
|
|
dataLabels: {
|
|
enabled: true,
|
|
formatter: function(val) {
|
|
return val.toFixed(1) + "%";
|
|
}
|
|
}
|
|
};
|
|
|
|
// Jika chart sudah ada, update data saja
|
|
if (systemOverviewChart) {
|
|
systemOverviewChart.updateOptions({
|
|
labels: chartInfo.labels,
|
|
colors: chartInfo.colors
|
|
});
|
|
systemOverviewChart.updateSeries(chartInfo.series);
|
|
} else {
|
|
// Buat chart baru
|
|
systemOverviewChart = new ApexCharts(document.querySelector("#systemOverviewChart"), options);
|
|
systemOverviewChart.render();
|
|
}
|
|
}
|
|
|
|
// Tambahkan ke fungsi updateChartFilterStatus
|
|
function updateChartFilterStatus() {
|
|
const chartCountElement = document.getElementById('system-chart-count');
|
|
const chartFilterBadge = document.getElementById('chart-filter-badge');
|
|
const chartFilterText = document.getElementById('chart-filter-text');
|
|
const filterDate = document.getElementById('filterDate').value;
|
|
const timeFilterEnabled = document.getElementById('filterTimeToggle').checked;
|
|
|
|
let filterInfo = [];
|
|
|
|
if (filterDate) {
|
|
const formattedDate = new Date(filterDate).toLocaleDateString('id-ID', {
|
|
day: '2-digit',
|
|
month: '2-digit',
|
|
year: 'numeric'
|
|
});
|
|
filterInfo.push(`Tanggal: ${formattedDate}`);
|
|
}
|
|
|
|
if (timeFilterEnabled) {
|
|
const startTime = document.getElementById('startTime').value;
|
|
const endTime = document.getElementById('endTime').value;
|
|
filterInfo.push(`Waktu: ${startTime} - ${endTime}`);
|
|
}
|
|
|
|
if (selectedCategories.length > 0) {
|
|
filterInfo.push(`Kategori: ${selectedCategories.join(', ')}`);
|
|
}
|
|
|
|
// Update text pada chart count
|
|
if (filterInfo.length > 0) {
|
|
chartCountElement.innerHTML = `
|
|
<span class="font-medium">Data Terfilter:</span> ${filteredReports.length} dari ${allReports.length}
|
|
`;
|
|
|
|
// Tampilkan badge filter
|
|
chartFilterBadge.classList.remove('hidden');
|
|
chartFilterText.textContent = filterInfo.join(' | ');
|
|
} else {
|
|
chartCountElement.textContent = `Total: ${allReports.length} laporan dengan ${analyzeSystemOverview(allReports).series.reduce((a, b) => a + b, 0)} status`;
|
|
|
|
// Sembunyikan badge filter
|
|
chartFilterBadge.classList.add('hidden');
|
|
}
|
|
}
|
|
});
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|