686 lines
38 KiB
PHP
686 lines
38 KiB
PHP
<!DOCTYPE html>
|
|
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<meta name="csrf-token" content="{{ csrf_token() }}">
|
|
|
|
<title>florAura</title>
|
|
|
|
<!-- Fonts -->
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
|
|
<!-- Tailwind CSS via CDN -->
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
|
|
<!-- Flowbite - Untuk komponen UI seperti dropdown -->
|
|
<link href="https://cdnjs.cloudflare.com/ajax/libs/flowbite/2.2.0/flowbite.min.css" rel="stylesheet" />
|
|
|
|
<!-- Firebase SDK -->
|
|
<script src="https://www.gstatic.com/firebasejs/8.10.1/firebase-app.js"></script>
|
|
<script src="https://www.gstatic.com/firebasejs/8.10.1/firebase-database.js"></script>
|
|
|
|
<script>
|
|
// Konfigurasi Firebase
|
|
const firebaseConfig = {
|
|
apiKey: "AIzaSyDf2QTksasAup4pzsNs9_JDpCXmBbUbywY",
|
|
authDomain: "sensoranggrek-3d9ac.firebaseapp.com",
|
|
databaseURL: "https://sensoranggrek-3d9ac-default-rtdb.firebaseio.com",
|
|
projectId: "sensoranggrek-3d9ac",
|
|
storageBucket: "sensoranggrek-3d9ac.firebasestorage.app",
|
|
messagingSenderId: "16998798790",
|
|
appId: "1:16998798790:web:885e1155255b24ab98cec7"
|
|
};
|
|
|
|
// Inisialisasi Firebase
|
|
firebase.initializeApp(firebaseConfig);
|
|
const database = firebase.database();
|
|
</script>
|
|
</head>
|
|
<body class="font-sans antialiased bg-gray-100">
|
|
<div class="min-h-screen flex flex-col">
|
|
<!-- Navbar -->
|
|
<nav class="bg-white border-gray-200 dark:bg-gray-900">
|
|
<div class="max-w-screen-xl flex flex-wrap items-center justify-between mx-auto p-4">
|
|
<a href="{{ route('dashboard') }}" class="flex items-center space-x-3 rtl:space-x-reverse">
|
|
<span class="self-center text-2xl font-semibold whitespace-nowrap dark:text-white">florAura</span>
|
|
</a>
|
|
|
|
<!-- Menu navigasi diletakkan sebelum profile menu (di sebelah kanan) -->
|
|
<div class="hidden md:flex items-center space-x-1 md:me-4">
|
|
<a href="{{ route('dashboard') }}" class="py-2 px-3 {{ Request::routeIs('dashboard') ? 'text-blue-700 border-b-2 border-blue-700' : 'text-gray-900 hover:text-blue-700' }}">Dashboard</a>
|
|
<a href="{{ route('history') }}" class="py-2 px-3 {{ Request::routeIs('history') ? 'text-blue-700 border-b-2 border-blue-700' : 'text-gray-900 hover:text-blue-700' }}">History</a>
|
|
</div>
|
|
|
|
<div class="flex items-center md:order-2">
|
|
<button type="button" class="flex text-sm bg-gray-800 rounded-full md:me-0 focus:ring-4 focus:ring-gray-300 dark:focus:ring-gray-600" id="user-menu-button" aria-expanded="false" data-dropdown-toggle="user-dropdown" data-dropdown-placement="bottom">
|
|
<span class="sr-only">Open user menu</span>
|
|
<span class="inline-flex h-8 w-8 rounded-full bg-gray-500 text-white justify-center items-center">
|
|
<span class="text-sm font-medium leading-none">{{ substr(session('user_email') ?? 'User', 0, 1) }}</span>
|
|
</span>
|
|
</button>
|
|
<!-- Dropdown menu -->
|
|
<div class="z-50 hidden my-4 text-base list-none bg-white divide-y divide-gray-100 rounded-lg shadow dark:bg-gray-700 dark:divide-gray-600" id="user-dropdown">
|
|
<div class="px-4 py-3">
|
|
<span class="block text-sm text-gray-900 dark:text-white">{{ session('user_email') ?? 'User' }}</span>
|
|
</div>
|
|
<ul class="py-2" aria-labelledby="user-menu-button">
|
|
<li>
|
|
<form method="POST" action="{{ route('logout') }}">
|
|
@csrf
|
|
<button type="submit" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:hover:bg-gray-600 dark:text-gray-200 dark:hover:text-white w-full text-left">Logout</button>
|
|
</form>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<button data-collapse-toggle="navbar-user" type="button" class="inline-flex items-center p-2 w-10 h-10 justify-center text-sm text-gray-500 rounded-lg md:hidden hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600 ml-2" aria-controls="navbar-user" aria-expanded="false" id="navbar-toggle">
|
|
<span class="sr-only">Open main menu</span>
|
|
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 14">
|
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 1h15M1 7h15M1 13h15"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Navbar untuk tampilan mobile -->
|
|
<div class="items-center justify-between hidden w-full md:hidden" id="navbar-user">
|
|
<ul class="flex flex-col font-medium p-4 mt-4 border border-gray-100 rounded-lg bg-gray-50 dark:bg-gray-800 dark:border-gray-700">
|
|
<li>
|
|
<a href="{{ route('dashboard') }}" class="block py-2 px-3 {{ Request::routeIs('dashboard') ? 'text-white bg-blue-700 rounded' : 'text-gray-900 rounded hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700' }}">Dashboard</a>
|
|
</li>
|
|
<li>
|
|
<a href="{{ route('history') }}" class="block py-2 px-3 {{ Request::routeIs('history') ? 'text-white bg-blue-700 rounded' : 'text-gray-900 rounded hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700' }}">History</a>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</nav>
|
|
|
|
<!-- Main content -->
|
|
<div class="flex flex-col flex-1">
|
|
<!-- Sensor Cards Bar -->
|
|
<div class="sticky top-0 z-10 flex-shrink-0 bg-white shadow">
|
|
<!-- Toggle button untuk sensor cards di mobile -->
|
|
<div class="flex items-center justify-between bg-white p-3 border-t border-gray-200 md:hidden">
|
|
<h3 class="text-sm font-medium text-gray-700">Status Sensor</h3>
|
|
<button id="toggleSensorCards" class="flex items-center bg-gray-100 hover:bg-gray-200 rounded-md px-3 py-1 text-xs text-gray-700 transition-all">
|
|
<span id="toggleSensorText">Lihat Detail</span>
|
|
<svg id="toggleSensorIcon" class="w-4 h-4 ml-1 transform transition-transform" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Sensor Cards Bar -->
|
|
<div id="sensorCardsContainer" class="hidden md:grid grid-cols-1 xs:grid-cols-2 md:grid-cols-4 gap-3 sm:gap-4 p-3 sm:p-4 bg-gray-50 border-t border-gray-200">
|
|
<!-- Card Suhu -->
|
|
<div class="bg-white p-3 sm:p-4 rounded-lg shadow-sm border border-gray-200 transition-all duration-300 hover:shadow-md">
|
|
<div class="flex items-start sm:items-center">
|
|
<div class="flex-shrink-0 bg-red-100 rounded-full p-2 sm:p-3 mr-3 sm:mr-4">
|
|
<svg class="h-5 w-5 sm:h-6 sm:w-6 text-red-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
|
</svg>
|
|
</div>
|
|
<div class="flex-1">
|
|
<div class="text-xs font-medium text-gray-500 uppercase">Suhu</div>
|
|
<div class="text-xl sm:text-2xl font-semibold text-gray-800 transition-all duration-300" id="nilai-suhu">--°C</div>
|
|
<div class="flex flex-col sm:flex-row sm:justify-between mt-1 sm:mt-2">
|
|
<div class="mb-1 sm:mb-0">
|
|
<span class="text-xs font-medium text-gray-500">Status:</span>
|
|
<span class="text-xs sm:text-sm font-medium ml-1" id="status-suhu">--</span>
|
|
</div>
|
|
<div>
|
|
<span class="text-xs font-medium text-gray-500">Aksi:</span>
|
|
<span class="text-xs sm:text-sm font-medium ml-1" id="aksi-suhu">--</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Card Kelembaban -->
|
|
<div class="bg-white p-3 sm:p-4 rounded-lg shadow-sm border border-gray-200 transition-all duration-300 hover:shadow-md">
|
|
<div class="flex items-start sm:items-center">
|
|
<div class="flex-shrink-0 bg-blue-100 rounded-full p-2 sm:p-3 mr-3 sm:mr-4">
|
|
<svg class="h-5 w-5 sm:h-6 sm:w-6 text-blue-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1115 0z" />
|
|
</svg>
|
|
</div>
|
|
<div class="flex-1">
|
|
<div class="text-xs font-medium text-gray-500 uppercase">Kelembapan Tanah</div>
|
|
<div class="text-xl sm:text-2xl font-semibold text-gray-800 transition-all duration-300" id="nilai-kelembaban">--%</div>
|
|
<div class="flex flex-col sm:flex-row sm:justify-between mt-1 sm:mt-2">
|
|
<div class="mb-1 sm:mb-0">
|
|
<span class="text-xs font-medium text-gray-500">Status:</span>
|
|
<span class="text-xs sm:text-sm font-medium ml-1" id="status-kelembaban">--</span>
|
|
</div>
|
|
<div>
|
|
<span class="text-xs font-medium text-gray-500">Aksi:</span>
|
|
<span class="text-xs sm:text-sm font-medium ml-1" id="aksi-kelembaban">--</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Card Cahaya -->
|
|
<div class="bg-white p-3 sm:p-4 rounded-lg shadow-sm border border-gray-200 transition-all duration-300 hover:shadow-md">
|
|
<div class="flex items-start sm:items-center">
|
|
<div class="flex-shrink-0 bg-yellow-100 rounded-full p-2 sm:p-3 mr-3 sm:mr-4">
|
|
<svg class="h-5 w-5 sm:h-6 sm:w-6 text-yellow-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" />
|
|
</svg>
|
|
</div>
|
|
<div class="flex-1">
|
|
<div class="text-xs font-medium text-gray-500 uppercase">Cahaya</div>
|
|
<div class="text-xl sm:text-2xl font-semibold text-gray-800 transition-all duration-300" id="nilai-cahaya">-- lux</div>
|
|
<div class="flex flex-col sm:flex-row sm:justify-between mt-1 sm:mt-2">
|
|
<div class="mb-1 sm:mb-0">
|
|
<span class="text-xs font-medium text-gray-500">Status:</span>
|
|
<span class="text-xs sm:text-sm font-medium ml-1" id="status-cahaya">--</span>
|
|
</div>
|
|
<div>
|
|
<span class="text-xs font-medium text-gray-500">Aksi:</span>
|
|
<span class="text-xs sm:text-sm font-medium ml-1" id="aksi-cahaya">--</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Card Status ESP8266 -->
|
|
<div class="bg-white p-3 sm:p-4 rounded-lg shadow-sm border border-gray-200 transition-all duration-300 hover:shadow-md">
|
|
<div class="flex items-start sm:items-center">
|
|
<div class="flex-shrink-0 bg-gray-100 rounded-full p-2 sm:p-3 mr-3 sm:mr-4">
|
|
<svg class="h-5 w-5 sm:h-6 sm:w-6 text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" />
|
|
</svg>
|
|
</div>
|
|
<div class="flex-1">
|
|
<div class="text-xs font-medium text-gray-500 uppercase">Status ESP8266</div>
|
|
<div class="flex items-center">
|
|
<span class="h-2.5 w-2.5 sm:h-3 sm:w-3 rounded-full mr-1 sm:mr-2 bg-gray-300 transition-all duration-300" id="status-dot"></span>
|
|
<span class="text-sm sm:text-lg font-semibold text-gray-800 transition-all duration-300" id="status-text">Menghubungkan...</span>
|
|
</div>
|
|
<div class="mt-2 flex items-center">
|
|
<button id="restart-esp" class="p-1.5 sm:p-2 bg-blue-500 text-white rounded-full hover:bg-blue-600 transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
|
<svg class="h-3.5 w-3.5 sm:h-4 sm:w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
|
</svg>
|
|
</button>
|
|
<span id="restart-status" class="text-xs text-gray-500 ml-2 flex-grow truncate"></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<main class="flex-1">
|
|
<div class="py-6">
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 md:px-8">
|
|
@yield('content')
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Script untuk Firebase Realtime -->
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
// Referensi ke path data sensor
|
|
const sensorRef = database.ref('sensor');
|
|
|
|
// Referensi ke status ESP8266
|
|
const statusRef = database.ref('status/connected');
|
|
|
|
// Referensi untuk restart
|
|
const restartRef = database.ref('system/restart');
|
|
|
|
// Tombol restart ESP8266
|
|
const restartButton = document.getElementById('restart-esp');
|
|
const restartStatus = document.getElementById('restart-status');
|
|
|
|
// Timestamp saat terakhir update
|
|
let lastUpdateTime = Date.now();
|
|
|
|
// Token CSRF untuk request API
|
|
let csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
|
|
|
|
// Tambahkan variabel untuk menyimpan status terakhir
|
|
let lastStatus = {
|
|
suhu: null,
|
|
kelembaban: null,
|
|
cahaya: null
|
|
};
|
|
|
|
// Menyimpan nilai sensor terakhir untuk dibandingkan
|
|
let lastSensorValues = {
|
|
suhu: null,
|
|
kelembapan_tanah: null,
|
|
cahaya: null
|
|
};
|
|
|
|
// Tambahkan variabel untuk menyimpan status terakhir yang dikirim ke history
|
|
let lastSentStatus = {
|
|
suhu: null,
|
|
kelembaban: null,
|
|
cahaya: null
|
|
};
|
|
|
|
// Flag untuk tracking status pengiriman data ke server
|
|
let isDataSending = false;
|
|
|
|
// Listener untuk data sensor (menggunakan snapshot)
|
|
sensorRef.on('value', (snapshot) => {
|
|
const data = snapshot.val();
|
|
|
|
if (data) {
|
|
// Suhu
|
|
if (data.suhu !== undefined && data.suhu !== lastSensorValues.suhu) {
|
|
saveSensorHistory('suhu', lastSensorValues.suhu, data.suhu);
|
|
lastSensorValues.suhu = data.suhu;
|
|
}
|
|
updateSensorValue('nilai-suhu', data.suhu ? `${data.suhu}°C` : '--°C');
|
|
if (data.suhu !== undefined) evaluateSuhu(data.suhu);
|
|
|
|
// Kelembapan Tanah
|
|
if (data.kelembapan_tanah !== undefined && data.kelembapan_tanah !== lastSensorValues.kelembapan_tanah) {
|
|
saveSensorHistory('kelembapan_tanah', lastSensorValues.kelembapan_tanah, data.kelembapan_tanah);
|
|
lastSensorValues.kelembapan_tanah = data.kelembapan_tanah;
|
|
}
|
|
updateSensorValue('nilai-kelembaban', data.kelembapan_tanah ? `${data.kelembapan_tanah}%` : '--%');
|
|
if (data.kelembapan_tanah !== undefined) evaluateKelembaban(data.kelembapan_tanah);
|
|
|
|
// Cahaya
|
|
if (data.cahaya !== undefined && data.cahaya !== lastSensorValues.cahaya) {
|
|
saveSensorHistory('cahaya', lastSensorValues.cahaya, data.cahaya);
|
|
lastSensorValues.cahaya = data.cahaya;
|
|
}
|
|
updateSensorValue('nilai-cahaya', data.cahaya ? `${data.cahaya} lux` : '-- lux');
|
|
if (data.cahaya !== undefined) evaluateCahaya(data.cahaya);
|
|
|
|
lastUpdateTime = Date.now();
|
|
}
|
|
});
|
|
|
|
// Fungsi untuk menyimpan riwayat perubahan sensor ke server API
|
|
async function saveSensorHistory(sensorType, oldValue, newValue) {
|
|
// Hindari pengiriman berulang jika masih dalam proses pengiriman
|
|
if (isDataSending) return;
|
|
|
|
// Cek jika nilainya kosong atau null
|
|
if (newValue === null || newValue === undefined) return;
|
|
|
|
// Jika tidak ada perubahan nilai, tidak perlu mengirim data
|
|
if (oldValue === newValue) return;
|
|
|
|
try {
|
|
// Tentukan status dan aksi berdasarkan nilai baru
|
|
let currentStatus = '';
|
|
let currentAction = '';
|
|
|
|
if (sensorType === 'suhu') {
|
|
if (newValue < 24) {
|
|
currentStatus = 'Rendah';
|
|
currentAction = 'Kipas Off';
|
|
} else if (newValue >= 25 && newValue <= 27) {
|
|
currentStatus = 'Normal';
|
|
currentAction = 'Kipas Off';
|
|
} else if (newValue >= 28) {
|
|
currentStatus = 'Tinggi';
|
|
currentAction = 'Kipas On';
|
|
}
|
|
} else if (sensorType === 'kelembapan_tanah') {
|
|
if (newValue < 40) {
|
|
currentStatus = 'Kering';
|
|
currentAction = 'Pompa Air On';
|
|
} else if (newValue >= 40 && newValue < 50) {
|
|
currentStatus = 'Cukup';
|
|
currentAction = 'Pompa Air Off';
|
|
} else if (newValue >= 50 && newValue < 60) {
|
|
currentStatus = 'Optimal';
|
|
currentAction = 'Pompa Air Off';
|
|
} else if (newValue >= 60 && newValue <= 70) {
|
|
currentStatus = 'Lembap';
|
|
currentAction = 'Pompa Air Off';
|
|
} else if (newValue > 70) {
|
|
currentStatus = 'Sangat Lembap';
|
|
currentAction = 'Pompa Air Off';
|
|
}
|
|
} else if (sensorType === 'cahaya') {
|
|
if (newValue < 8000) {
|
|
currentStatus = 'Rendah';
|
|
currentAction = 'Lampu On';
|
|
} else if (newValue >= 8001 && newValue <= 11000) {
|
|
currentStatus = 'Normal';
|
|
currentAction = 'Lampu Off';
|
|
} else if (newValue > 11001) {
|
|
currentStatus = 'Tinggi';
|
|
currentAction = 'Lampu Off';
|
|
}
|
|
}
|
|
|
|
// Siapkan data untuk dikirim ke server
|
|
const sensorData = {
|
|
sensor: sensorType === 'kelembapan_tanah' ? 'kelembapan_tanah' : sensorType,
|
|
oldValue: oldValue,
|
|
newValue: newValue,
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
|
|
console.log('Mengirim data sensor ke API:', sensorData);
|
|
|
|
// Set flag pengiriman data
|
|
isDataSending = true;
|
|
|
|
// Kirim data ke API
|
|
const response = await fetch('/api/history', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRF-TOKEN': csrfToken
|
|
},
|
|
body: JSON.stringify(sensorData)
|
|
});
|
|
|
|
// Reset flag pengiriman data
|
|
isDataSending = false;
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Gagal mengirim data ke server');
|
|
}
|
|
|
|
// Update status terakhir yang dikirim
|
|
lastSentStatus[sensorType] = currentStatus;
|
|
|
|
console.log('Data sensor berhasil dikirim ke API');
|
|
} catch (error) {
|
|
console.error('Error mengirim data sensor ke API:', error);
|
|
isDataSending = false;
|
|
}
|
|
}
|
|
|
|
// Event listener untuk tombol restart
|
|
restartButton.addEventListener('click', function() {
|
|
if (confirm('Apakah Anda yakin ingin merestart ESP8266?')) {
|
|
// Nonaktifkan tombol saat proses restart
|
|
restartButton.disabled = true;
|
|
restartButton.classList.add('bg-gray-400');
|
|
restartButton.classList.remove('bg-blue-500', 'hover:bg-blue-600');
|
|
restartStatus.textContent = 'Mengirim perintah restart...';
|
|
|
|
// Set nilai restart ke true
|
|
database.ref('system/restart').set(true)
|
|
.then(() => {
|
|
restartStatus.textContent = 'Perintah restart dikirim';
|
|
restartStatus.className = 'text-xs text-green-600 ml-2';
|
|
|
|
// Setelah 3 detik, reset ke false
|
|
setTimeout(() => {
|
|
database.ref('system/restart').set(false)
|
|
.catch(() => {
|
|
console.log('Reset restart flag gagal, tetapi ESP mungkin telah membacanya');
|
|
});
|
|
}, 3000);
|
|
|
|
// Setelah 5 detik, aktifkan kembali tombol
|
|
setTimeout(() => {
|
|
restartButton.disabled = false;
|
|
restartButton.classList.remove('bg-gray-400');
|
|
restartButton.classList.add('bg-blue-500', 'hover:bg-blue-600');
|
|
restartStatus.textContent = '';
|
|
}, 5000);
|
|
})
|
|
.catch((error) => {
|
|
restartStatus.textContent = 'Gagal mengirim restart. Perlu cek aturan Firebase.';
|
|
restartStatus.className = 'text-xs text-red-600 ml-2';
|
|
console.error('Gagal mengirim restart:', error);
|
|
|
|
// Aktifkan kembali tombol
|
|
restartButton.disabled = false;
|
|
restartButton.classList.remove('bg-gray-400');
|
|
restartButton.classList.add('bg-blue-500', 'hover:bg-blue-600');
|
|
});
|
|
}
|
|
});
|
|
|
|
// Listener untuk status koneksi ESP8266
|
|
statusRef.on('value', (snapshot) => {
|
|
const isConnected = snapshot.val();
|
|
if (isConnected === true) {
|
|
lastUpdateTime = Date.now(); // Update waktu terakhir terlihat
|
|
}
|
|
updateConnectionStatus();
|
|
});
|
|
|
|
// Juga pantau timestamp last_seen untuk deteksi koneksi yang lebih akurat
|
|
const lastSeenRef = database.ref('status/last_seen');
|
|
lastSeenRef.on('value', (snapshot) => {
|
|
const lastSeenTimestamp = snapshot.val();
|
|
if (lastSeenTimestamp) {
|
|
// Parse timestamp (format: "DD-MM-YYYY HH:MM:SS")
|
|
try {
|
|
const lastSeen = parseFirebaseTimestamp(lastSeenTimestamp);
|
|
if (!isNaN(lastSeen.getTime())) {
|
|
// Hanya update jika timestamp valid
|
|
lastUpdateTime = Date.now();
|
|
updateConnectionStatus();
|
|
}
|
|
} catch (e) {
|
|
console.error("Error parsing timestamp:", e);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Fungsi untuk memperbarui status koneksi
|
|
function updateConnectionStatus() {
|
|
const statusDot = document.getElementById('status-dot');
|
|
const statusText = document.getElementById('status-text');
|
|
|
|
// Jika terakhir update kurang dari 60 detik, anggap terhubung
|
|
const isConnected = (Date.now() - lastUpdateTime) < 60000;
|
|
|
|
if (isConnected) {
|
|
statusDot.className = 'h-3 w-3 rounded-full mr-2 bg-green-500 transition-all duration-300';
|
|
statusText.textContent = 'Terhubung';
|
|
statusText.className = 'text-lg font-semibold text-green-600 transition-all duration-300';
|
|
} else {
|
|
statusDot.className = 'h-3 w-3 rounded-full mr-2 bg-red-500 transition-all duration-300';
|
|
statusText.textContent = 'Terputus';
|
|
statusText.className = 'text-lg font-semibold text-red-600 transition-all duration-300';
|
|
}
|
|
}
|
|
|
|
// Fungsi untuk mengubah string timestamp Firebase menjadi objek Date
|
|
function parseFirebaseTimestamp(timestampStr) {
|
|
// Format: "DD-MM-YYYY HH:MM:SS"
|
|
const [datePart, timePart] = timestampStr.split(' ');
|
|
const [day, month, year] = datePart.split('-');
|
|
const [hour, minute, second] = timePart.split(':');
|
|
|
|
return new Date(year, month-1, day, hour, minute, second);
|
|
}
|
|
|
|
// Cek koneksi setiap 5 detik untuk memperbarui status UI
|
|
setInterval(updateConnectionStatus, 5000);
|
|
|
|
// Fungsi untuk update nilai dengan animasi smooth
|
|
function updateSensorValue(elementId, newValue) {
|
|
const element = document.getElementById(elementId);
|
|
|
|
// Tambahkan kelas untuk animasi
|
|
element.classList.add('scale-110', 'text-indigo-600');
|
|
element.textContent = newValue;
|
|
|
|
// Hapus kelas setelah animasi selesai
|
|
setTimeout(() => {
|
|
element.classList.remove('scale-110', 'text-indigo-600');
|
|
}, 300);
|
|
}
|
|
|
|
// Fungsi untuk mengevaluasi status dan aksi sensor suhu
|
|
function evaluateSuhu(suhu) {
|
|
let status = '--';
|
|
let aksi = '--';
|
|
let statusClass = '';
|
|
|
|
if (suhu < 24) {
|
|
status = 'Rendah';
|
|
aksi = 'Kipas Off';
|
|
statusClass = 'text-blue-600';
|
|
} else if (suhu >= 25 && suhu <= 27) {
|
|
status = 'Normal';
|
|
aksi = 'Kipas Off';
|
|
statusClass = 'text-green-600';
|
|
} else if (suhu >= 28) {
|
|
status = 'Tinggi';
|
|
aksi = 'Kipas On';
|
|
statusClass = 'text-red-600';
|
|
}
|
|
|
|
const statusElement = document.getElementById('status-suhu');
|
|
const aksiElement = document.getElementById('aksi-suhu');
|
|
|
|
statusElement.textContent = status;
|
|
statusElement.className = `text-sm font-medium ml-1 ${statusClass}`;
|
|
|
|
aksiElement.textContent = aksi;
|
|
aksiElement.className = `text-sm font-medium ml-1 ${aksi.includes('On') ? 'text-green-600' : 'text-gray-600'}`;
|
|
}
|
|
|
|
// Fungsi untuk mengevaluasi status dan aksi sensor kelembapan tanah
|
|
function evaluateKelembaban(kelembapanTanah) {
|
|
let status = '--';
|
|
let aksi = '--';
|
|
let statusClass = '';
|
|
|
|
if (kelembapanTanah < 40) {
|
|
status = 'Kering';
|
|
aksi = 'Pompa Air On';
|
|
statusClass = 'text-yellow-600';
|
|
} else if (kelembapanTanah >= 40 && kelembapanTanah < 50) {
|
|
status = 'Cukup';
|
|
aksi = 'Pompa Air Off';
|
|
statusClass = 'text-green-600';
|
|
} else if (kelembapanTanah >= 50 && kelembapanTanah < 60) {
|
|
status = 'Optimal';
|
|
aksi = 'Pompa Air Off';
|
|
statusClass = 'text-green-600';
|
|
} else if (kelembapanTanah >= 60 && kelembapanTanah <= 70) {
|
|
status = 'Lembap';
|
|
aksi = 'Pompa Air Off';
|
|
statusClass = 'text-blue-600';
|
|
} else if (kelembapanTanah > 70) {
|
|
status = 'Sangat Lembap';
|
|
aksi = 'Pompa Air Off';
|
|
statusClass = 'text-blue-400';
|
|
}
|
|
|
|
const statusElement = document.getElementById('status-kelembaban');
|
|
const aksiElement = document.getElementById('aksi-kelembaban');
|
|
|
|
statusElement.textContent = status;
|
|
statusElement.className = `text-sm font-medium ml-1 ${statusClass}`;
|
|
|
|
aksiElement.textContent = aksi;
|
|
aksiElement.className = `text-sm font-medium ml-1 ${aksi === 'Pompa Air On' ? 'text-green-600' : 'text-gray-600'}`;
|
|
}
|
|
|
|
// Fungsi untuk mengevaluasi status dan aksi sensor cahaya
|
|
function evaluateCahaya(cahaya) {
|
|
let status = '--';
|
|
let aksi = '--';
|
|
let statusClass = '';
|
|
|
|
if (cahaya < 8000) {
|
|
status = 'Rendah';
|
|
aksi = 'Lampu On';
|
|
statusClass = 'text-blue-600';
|
|
} else if (cahaya >= 8001 && cahaya <= 11000) {
|
|
status = 'Normal';
|
|
aksi = 'Lampu Off';
|
|
statusClass = 'text-green-600';
|
|
} else if (cahaya > 11001) {
|
|
status = 'Tinggi';
|
|
aksi = 'Lampu Off';
|
|
statusClass = 'text-yellow-600';
|
|
}
|
|
|
|
const statusElement = document.getElementById('status-cahaya');
|
|
const aksiElement = document.getElementById('aksi-cahaya');
|
|
|
|
statusElement.textContent = status;
|
|
statusElement.className = `text-xs sm:text-sm font-medium ml-1 ${statusClass}`;
|
|
|
|
aksiElement.textContent = aksi;
|
|
aksiElement.className = `text-xs sm:text-sm font-medium ml-1 ${aksi.includes('On') ? 'text-green-600' : 'text-gray-600'}`;
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<!-- Script untuk Flowbite (dropdown, dll) -->
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/flowbite/2.2.0/flowbite.min.js"></script>
|
|
|
|
<!-- Script untuk User Dropdown dan Sensor Cards Toggle -->
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const userMenuButton = document.getElementById('user-menu-button');
|
|
const userDropdown = document.getElementById('user-dropdown');
|
|
const navbarToggle = document.getElementById('navbar-toggle');
|
|
const navbarUser = document.getElementById('navbar-user');
|
|
|
|
// Toggle Sensor Cards pada Mobile
|
|
const toggleSensorCards = document.getElementById('toggleSensorCards');
|
|
const sensorCardsContainer = document.getElementById('sensorCardsContainer');
|
|
const toggleSensorIcon = document.getElementById('toggleSensorIcon');
|
|
const toggleSensorText = document.getElementById('toggleSensorText');
|
|
|
|
if (toggleSensorCards && sensorCardsContainer) {
|
|
toggleSensorCards.addEventListener('click', function() {
|
|
sensorCardsContainer.classList.toggle('hidden');
|
|
toggleSensorIcon.classList.toggle('rotate-180');
|
|
toggleSensorText.textContent = sensorCardsContainer.classList.contains('hidden') ? 'Lihat Detail' : 'Sembunyikan';
|
|
});
|
|
|
|
// Restore state dari localStorage jika ada
|
|
const sensorCardsState = localStorage.getItem('sensorCardsVisible');
|
|
if (sensorCardsState === 'true') {
|
|
sensorCardsContainer.classList.remove('hidden');
|
|
toggleSensorIcon.classList.add('rotate-180');
|
|
toggleSensorText.textContent = 'Sembunyikan';
|
|
}
|
|
|
|
// Save state ke localStorage saat toggle
|
|
toggleSensorCards.addEventListener('click', function() {
|
|
localStorage.setItem('sensorCardsVisible', !sensorCardsContainer.classList.contains('hidden'));
|
|
});
|
|
}
|
|
|
|
// Fallback jika flowbite tidak berfungsi
|
|
if (userMenuButton && userDropdown && typeof flowbite === 'undefined') {
|
|
// Toggle dropdown saat tombol profil diklik
|
|
userMenuButton.addEventListener('click', function() {
|
|
userDropdown.classList.toggle('hidden');
|
|
});
|
|
|
|
// Tutup dropdown saat klik di luar dropdown
|
|
document.addEventListener('click', function(e) {
|
|
if (!userMenuButton.contains(e.target) && !userDropdown.contains(e.target)) {
|
|
userDropdown.classList.add('hidden');
|
|
}
|
|
});
|
|
}
|
|
|
|
if (navbarToggle && navbarUser) {
|
|
// Toggle navbar mobile
|
|
navbarToggle.addEventListener('click', function() {
|
|
navbarUser.classList.toggle('hidden');
|
|
});
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html> |