1339 lines
63 KiB
PHP
1339 lines
63 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; }
|
|
}
|
|
</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>Keamanan Normal:</strong> Sistem keamanan berjalan dengan baik</span>
|
|
</li>
|
|
<li class="flex items-center">
|
|
<span class="w-3 h-3 rounded-full bg-red-500 mr-2"></span>
|
|
<span><strong>Status Bahaya:</strong> Sistem keamanan mendeteksi potensi bahaya</span>
|
|
</li>
|
|
<li class="flex items-center">
|
|
<span class="w-3 h-3 rounded-full bg-yellow-500 mr-2"></span>
|
|
<span><strong>Gerakan Terdeteksi:</strong> Sensor gerakan mendeteksi aktivitas</span>
|
|
</li>
|
|
<li class="flex items-center">
|
|
<span class="w-3 h-3 rounded-full bg-lime-500 mr-2"></span>
|
|
<span><strong>Tidak Ada Gerakan:</strong> Tidak ada gerakan yang terdeteksi</span>
|
|
</li>
|
|
<li class="flex items-center">
|
|
<span class="w-3 h-3 rounded-full bg-blue-500 mr-2"></span>
|
|
<span><strong>Servo Terkunci:</strong> Servo dalam keadaan terkunci</span>
|
|
</li>
|
|
<li class="flex items-center">
|
|
<span class="w-3 h-3 rounded-full bg-cyan-500 mr-2"></span>
|
|
<span><strong>Servo Terbuka:</strong> Servo dalam keadaan terbuka</span>
|
|
</li>
|
|
<li class="flex items-center">
|
|
<span class="w-3 h-3 rounded-full bg-violet-400 mr-2"></span>
|
|
<span><strong>Akses Terakhir:</strong> Perubahan pada akses terakhir</span>
|
|
</li>
|
|
<li class="flex items-center">
|
|
<span class="w-3 h-3 rounded-full bg-purple-500 mr-2"></span>
|
|
<span><strong>Kontrol Diubah:</strong> Terjadi perubahan pada kontrol perangkat</span>
|
|
</li>
|
|
<li class="flex items-center">
|
|
<span class="w-3 h-3 rounded-full bg-pink-500 mr-2"></span>
|
|
<span><strong>Status Perangkat:</strong> Perubahan pada status perangkat</span>
|
|
</li>
|
|
<li class="flex items-center">
|
|
<span class="w-3 h-3 rounded-full bg-orange-500 mr-2"></span>
|
|
<span><strong>Error/Warning:</strong> Terdapat kesalahan atau peringatan dalam sistem</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>
|
|
|
|
<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
|
|
|
|
// 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;
|
|
|
|
// Simpan halaman sebelumnya
|
|
const prevPage = currentPage;
|
|
|
|
// Set flag filter aktif
|
|
isFilterActive = filterDate || timeFilterEnabled;
|
|
|
|
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;
|
|
|
|
// Filter data berdasarkan kriteria
|
|
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;
|
|
|
|
// Handle case where time range crosses midnight
|
|
if (startTotalMinutes <= endTotalMinutes) {
|
|
// Normal case (e.g., 08:00 to 17:00)
|
|
if (totalMinutes < startTotalMinutes || totalMinutes > endTotalMinutes) return false;
|
|
} else {
|
|
// Overnight case (e.g., 22:00 to 06:00)
|
|
if (totalMinutes < startTotalMinutes && totalMinutes > endTotalMinutes) return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
});
|
|
} else {
|
|
// Jika filter waktu tidak aktif, hanya filter berdasarkan tanggal
|
|
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;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
}
|
|
|
|
// Reset halaman ke 1 HANYA ketika menerapkan filter baru (karena ini eksplisit pengguna memfilter)
|
|
currentPage = 1;
|
|
|
|
// PENTING: Update chart dengan data yang sudah difilter
|
|
renderSystemOverviewChart(filteredReports);
|
|
|
|
// Render data yang telah difilter
|
|
renderPaginatedReports(true);
|
|
|
|
// Tampilkan notifikasi hasil filter
|
|
const filterCount = filteredReports.length;
|
|
const totalCount = allReports.length;
|
|
if (filterCount === 0) {
|
|
showFilterNotification(`Tidak ada data yang cocok dengan filter`);
|
|
} else {
|
|
showFilterNotification(`Ditemukan ${filterCount} dari ${totalCount} data`);
|
|
}
|
|
|
|
// Tambahkan badge untuk menampilkan 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';
|
|
|
|
// Reset flag filter
|
|
isFilterActive = false;
|
|
|
|
// Reset ke semua data dan halaman pertama
|
|
filteredReports = allReports;
|
|
currentPage = 1;
|
|
|
|
// PENTING: 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
|
|
const badgeClasses = {
|
|
'primary': 'bg-blue-100 text-blue-800',
|
|
'secondary': 'bg-gray-100 text-gray-800',
|
|
'danger': 'bg-red-100 text-red-800',
|
|
'warning': 'bg-yellow-100 text-yellow-800',
|
|
'info': 'bg-indigo-100 text-indigo-800',
|
|
'light': 'bg-gray-100 text-gray-800',
|
|
'dark': 'bg-gray-700 text-gray-300'
|
|
};
|
|
|
|
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: 'Gerakan', badge: 'danger' });
|
|
}
|
|
|
|
if (report.security.status !== prevReport.security.status) {
|
|
changes.push({ type: 'Status Keamanan', badge: 'danger' });
|
|
}
|
|
}
|
|
|
|
// Smartcab changes
|
|
if (report.smartcab && prevReport.smartcab) {
|
|
if (report.smartcab.last_access !== prevReport.smartcab.last_access) {
|
|
changes.push({ type: 'Akses Terakhir', badge: 'info' });
|
|
}
|
|
|
|
if (report.smartcab.servo_status !== prevReport.smartcab.servo_status) {
|
|
changes.push({ type: 'Status Servo', badge: 'warning' });
|
|
}
|
|
}
|
|
|
|
// Control changes
|
|
if (report.control && prevReport.control &&
|
|
JSON.stringify(report.control) !== JSON.stringify(prevReport.control)) {
|
|
changes.push({ type: 'Kontrol Perangkat', badge: 'secondary' });
|
|
}
|
|
|
|
// Device changes
|
|
if (report.device && prevReport.device &&
|
|
JSON.stringify(report.device) !== JSON.stringify(prevReport.device)) {
|
|
changes.push({ type: 'Status Perangkat', badge: 'secondary' });
|
|
}
|
|
|
|
// Logs changes
|
|
if (report.logs && prevReport.logs &&
|
|
JSON.stringify(report.logs) !== JSON.stringify(prevReport.logs)) {
|
|
changes.push({ type: 'Log Sistem', badge: 'dark' });
|
|
}
|
|
|
|
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 = {
|
|
'securityNormal': 0, // Status keamanan normal
|
|
'securityDanger': 0, // Status keamanan bahaya
|
|
'motionDetected': 0, // Gerakan terdeteksi
|
|
'motionNotDetected': 0, // Gerakan tidak terdeteksi
|
|
'servoLocked': 0, // Servo terkunci
|
|
'servoUnlocked': 0, // Servo terbuka
|
|
'accessChanged': 0, // Perubahan akses terakhir
|
|
'controlChanged': 0, // Perubahan kontrol perangkat
|
|
'deviceChanged': 0, // Perubahan status perangkat
|
|
'logError': 0 // Log error
|
|
};
|
|
|
|
// Total untuk persentase dan statusCounts
|
|
let totalCounts = 0;
|
|
|
|
// Periksa setiap laporan untuk mengisi kategori
|
|
reports.forEach(report => {
|
|
let categoriesAdded = 0;
|
|
|
|
// Cek status keamanan
|
|
if (report.security) {
|
|
if (report.security.status) {
|
|
if (report.security.status.toLowerCase() === 'bahaya') {
|
|
statusCounts['securityDanger']++;
|
|
categoriesAdded++;
|
|
} else if (report.security.status.toLowerCase() === 'aman') {
|
|
statusCounts['securityNormal']++;
|
|
categoriesAdded++;
|
|
}
|
|
}
|
|
|
|
if (report.security.motion) {
|
|
if (report.security.motion.toLowerCase() === 'detected') {
|
|
statusCounts['motionDetected']++;
|
|
categoriesAdded++;
|
|
} else if (report.security.motion.toLowerCase() === 'not detected') {
|
|
statusCounts['motionNotDetected']++;
|
|
categoriesAdded++;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Cek status smartcab
|
|
if (report.smartcab) {
|
|
if (report.smartcab.servo_status) {
|
|
if (report.smartcab.servo_status.toLowerCase() === 'locked') {
|
|
statusCounts['servoLocked']++;
|
|
categoriesAdded++;
|
|
} else if (report.smartcab.servo_status.toLowerCase() === 'unlocked') {
|
|
statusCounts['servoUnlocked']++;
|
|
categoriesAdded++;
|
|
}
|
|
}
|
|
|
|
if (report.smartcab.last_access) {
|
|
statusCounts['accessChanged']++;
|
|
categoriesAdded++;
|
|
}
|
|
}
|
|
|
|
// Cek perubahan kontrol
|
|
if (report.control && Object.keys(report.control).length > 0) {
|
|
statusCounts['controlChanged']++;
|
|
categoriesAdded++;
|
|
}
|
|
|
|
// Cek perubahan device
|
|
if (report.device && Object.keys(report.device).length > 0) {
|
|
statusCounts['deviceChanged']++;
|
|
categoriesAdded++;
|
|
}
|
|
|
|
// Cek logs untuk error
|
|
if (report.logs) {
|
|
let hasError = false;
|
|
for (const logKey in report.logs) {
|
|
if (report.logs[logKey] && report.logs[logKey].status) {
|
|
const status = report.logs[logKey].status.toLowerCase();
|
|
if (status.includes('error') || status.includes('warning')) {
|
|
hasError = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (hasError) {
|
|
statusCounts['logError']++;
|
|
categoriesAdded++;
|
|
}
|
|
}
|
|
|
|
// Tambahkan jumlah kategori ke total
|
|
totalCounts += categoriesAdded > 0 ? categoriesAdded : 1;
|
|
});
|
|
|
|
// Update counter
|
|
document.getElementById('system-chart-count').textContent =
|
|
`Total: ${reports.length} laporan dengan ${totalCounts} status`;
|
|
|
|
// Kembalikan array untuk chart dengan hanya nilai yang bukan nol
|
|
const resultData = [
|
|
statusCounts['securityNormal'],
|
|
statusCounts['securityDanger'],
|
|
statusCounts['motionDetected'],
|
|
statusCounts['motionNotDetected'],
|
|
statusCounts['servoLocked'],
|
|
statusCounts['servoUnlocked'],
|
|
statusCounts['accessChanged'],
|
|
statusCounts['controlChanged'],
|
|
statusCounts['deviceChanged'],
|
|
statusCounts['logError']
|
|
];
|
|
|
|
// Label yang sesuai dengan resultData
|
|
const resultLabels = [
|
|
'Keamanan Normal',
|
|
'Status Bahaya',
|
|
'Gerakan Terdeteksi',
|
|
'Tidak Ada Gerakan',
|
|
'Servo Terkunci',
|
|
'Servo Terbuka',
|
|
'Akses Terakhir',
|
|
'Kontrol Diubah',
|
|
'Status Perangkat',
|
|
'Error/Warning'
|
|
];
|
|
|
|
// Warna yang sesuai dengan resultData
|
|
const resultColors = [
|
|
'#22c55e', // Keamanan Normal (Hijau)
|
|
'#ef4444', // Status Bahaya (Merah)
|
|
'#eab308', // Gerakan Terdeteksi (Kuning)
|
|
'#84cc16', // Tidak Ada Gerakan (Hijau Muda)
|
|
'#3b82f6', // Servo Terkunci (Biru)
|
|
'#06b6d4', // Servo Terbuka (Cyan)
|
|
'#8b5cf6', // Akses Terakhir (Ungu Muda)
|
|
'#a855f7', // Kontrol Diubah (Ungu)
|
|
'#ec4899', // Status Perangkat (Pink)
|
|
'#f97316' // Error/Warning (Oranye)
|
|
];
|
|
|
|
// 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 += `Tanggal: ${formattedDate}`;
|
|
}
|
|
|
|
if (timeFilterEnabled) {
|
|
const startTime = document.getElementById('startTime').value;
|
|
const endTime = document.getElementById('endTime').value;
|
|
if (filterInfo) filterInfo += ' | ';
|
|
filterInfo += `Waktu: ${startTime} - ${endTime}`;
|
|
}
|
|
|
|
// Update text pada chart count
|
|
if (filterInfo) {
|
|
chartCountElement.innerHTML = `
|
|
<span class="font-medium">Data Terfilter:</span> ${filteredReports.length} dari ${allReports.length}
|
|
`;
|
|
|
|
// Tampilkan badge filter
|
|
chartFilterBadge.classList.remove('hidden');
|
|
chartFilterText.textContent = filterInfo;
|
|
} 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>
|