TKK_E32232338/kontrol.php

583 lines
17 KiB
PHP

<?php
require_once 'functions/auth.php';
if(!isLoggedIn()) {
header("Location: login.php");
exit;
}
if (!defined('NODERED_URL')) {
define('NODERED_URL', 'https://node-red.aquamonn.online');
}
require_once 'partials/header.php';
$noderedUrl = rtrim(NODERED_URL, '/');
?>
<div class="dashboard-layout">
<?php require_once 'partials/sidebar.php'; ?>
<div class="main-content page-enter">
<style>
.sensor-bar-wrap {
width: 100%;
height: 5px;
background: rgba(255,255,255,.15);
border-radius: 99px;
margin-top: 8px;
overflow: hidden;
}
.sensor-bar-fill {
height: 100%;
border-radius: 99px;
background: #22c55e;
width: 0%;
transition: width .6s ease, background .3s;
}
#conn-status {
display: inline-block;
width: 9px; height: 9px;
border-radius: 50%;
background: #6b7280;
margin-right: 6px;
transition: background .3s;
}
#conn-status.online { background: #22c55e; box-shadow: 0 0 6px #22c55e; }
#conn-status.offline { background: #ef4444; box-shadow: 0 0 6px #ef4444; }
#last-update { font-size: 11px; color: rgba(255,255,255,.55); margin-left: 6px; }
.device-badge {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 3px 10px;
border-radius: 99px;
font-size: 11px;
font-weight: 700;
background: rgba(255,255,255,.1);
color: rgba(255,255,255,.5);
transition: background .3s, color .3s;
}
.device-badge.on { background: rgba(34,197,94,.2); color: #22c55e; }
.device-badge.off { background: rgba(239,68,68,.15); color: #ef4444; }
</style>
<div class="dashboard-layout">
<?php require_once 'partials/sidebar.php'; ?>
<div class="main-content page-enter">
<!-- TOPBAR -->
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:20px; flex-wrap:wrap; gap:10px;">
<div style="display:flex; align-items:center; gap:10px; flex-wrap:wrap;">
<div style="display:flex; align-items:center; color:rgba(25, 22, 22, 0.7); font-size:13px;">
<span id="conn-status"></span>
<span id="conn-label">Menghubungkan…</span>
<span id="last-update"></span>
</div>
<span class="device-badge" id="badge-relay">
</span>
<span class="device-badge" id="badge-lampu">
</span>
</div>
<div class="topbar-icons">
<div class="notif" id="notifBtn">
<i class="fas fa-bell"></i>
<span class="notif-badge">0</span>
<div class="notif-dropdown" id="notifDropdown">
<p><b>Notifikasi</b></p>
<ul><li>Memuat data…</li></ul>
</div>
</div>
<div class="user" id="userBtn">
<i class="fas fa-user"></i>
<div class="user-dropdown" id="userDropdown">
<p><?= htmlspecialchars($_SESSION['email'] ?? 'User') ?></p>
<a href="logout.php">Logout</a>
</div>
</div>
</div>
</div>
<!-- GRID -->
<div class="ctrl-main-grid">
<!-- BOX SISTEM OTOMATIS -->
<div class="ctrl-box">
<div class="ctrl-box-title">Sistem Otomatis</div>
<!-- AMONIA -->
<div class="ctrl-item">
<span>Batas Amonia</span>
<input type="number" id="input-amonia" step="0.1">
<button class="btn-mulai" onclick="setAmonia()">Simpan</button>
</div>
<!-- KEKERUHAN -->
<div class="ctrl-item">
<span>Batas Kekeruhan</span>
<input type="number" id="input-kekeruhan" step="0.1" >
<button class="btn-mulai" onclick="setBatasKekeruhan()">Simpan</button>
</div>
<!-- TINGGI AIR -->
<div class="ctrl-item">
<span>Batas Tinggi Air</span>
<input type="number" id="input-tinggi-air" step="1">
<button class="btn-mulai" onclick="setBatasTinggiAir()">Simpan</button>
</div>
<!-- KONTROL TINGGI AQUARIUM -->
<div class="ctrl-item">
<span>Tinggi Aquarium</span>
<input type="number" id="input-tinggi-aquarium" step="1">
<button class="btn-mulai" onclick="setTinggiAquarium()">Simpan</button>
</div>
<!-- MODE POMPA -->
<div class="ctrl-item">
</div>
</div>
<!-- BOX LAMPU -->
<div class="ctrl-box">
<div class="ctrl-box-title">Set Lampu</div>
<div class="ctrl-time">
<input type="text" id="jam-mulai">
<span style="font-weight:bold;">—</span>
<input type="text" id="jam-selesai">
</div>
<div style="display:flex; gap:10px; margin-top:10px; flex-wrap:wrap;">
<button class="btn-mulai" style="background:#e222;" onclick="setJadwalLampu()">Simpan Jadwal</button>
</div>
<!-- MODE LAMPU -->
<div class="ctrl-item" style="margin-top:12px;">
<span>Lampu</span>
<label class="switch">
<input type="checkbox" id="toggle-lampu" onchange="kontrolLampu(this.checked)">
<span class="slider"></span>
</label>
<span id="label-lampu" style="font-size:12px; color:#aaa;">OFF</span>
</div>
</div>
</div>
</div>
</div>
<script>
const NR = "<?= $noderedUrl ?>";
/* ===================== JADWAL LAMPU ===================== */
async function setJadwalLampu() {
const onTime = document.getElementById("jam-mulai").value.trim();
const offTime = document.getElementById("jam-selesai").value.trim();
if (!onTime || !offTime) {
showToast("Isi jam nyala dan jam mati terlebih dahulu", "error");
return;
}
const re = /^([01]\d|2[0-3]):([0-5]\d)$/;
if (!re.test(onTime) || !re.test(offTime)) {
showToast("Format jam harus HH:MM", "error");
return;
}
// Menggunakan toggle yang ada di HTML
const isAuto = document.getElementById("toggle-lampu")?.checked ?? false;
try {
const response = await fetch(NR + "/api-schedule", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
on_time: onTime,
off_time: offTime,
enabled: isAuto
})
});
if (!response.ok) {
throw new Error("HTTP " + response.status);
}
await response.text();
showToast("Jadwal lampu berhasil disimpan");
} catch (err) {
console.error(err);
showToast("Gagal menyimpan jadwal lampu", "error");
}
}
/* ===================== KONTROL LAMPU ===================== */
async function kontrolLampu(isOn) {
try {
const response = await fetch(NR + "/api-lampu", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
lampu: isOn ? "on" : "off"
})
});
if (!response.ok) {
throw new Error("HTTP " + response.status);
}
await response.text();
document.getElementById("label-lampu").textContent = isOn ? "ON" : "OFF";
showToast("Lampu " + (isOn ? "ON" : "OFF"));
} catch (err) {
console.error(err);
showToast("Gagal mengontrol lampu", "error");
}
}
/* ===================== BATAS AMONIA ===================== */
async function setAmonia() {
const nilai = parseFloat(document.getElementById("input-amonia").value);
if (isNaN(nilai)) {
showToast("Masukkan nilai amonia", "error");
return;
}
try {
const response = await fetch(NR + "/set-pmaks", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
maks: nilai
})
});
if (!response.ok) {
throw new Error("HTTP " + response.status);
}
showToast("Batas amonia berhasil disimpan", "success");
} catch (error) {
console.error(error);
showToast("Gagal menyimpan batas amonia", "error");
}
}
/* ===================== BATAS KEKERUHAN ===================== */
async function setBatasKekeruhan() {
const nilai = parseFloat(document.getElementById("input-kekeruhan").value);
if (isNaN(nilai)) {
showToast("Masukkan nilai kekeruhan", "error");
return;
}
try {
const response = await fetch(NR + "/set-batas_kekeruhan", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
max_kekeruhan: nilai
})
});
if (!response.ok) {
throw new Error("HTTP " + response.status);
}
await response.text();
showToast("Batas kekeruhan berhasil disimpan");
} catch (err) {
console.error(err);
showToast("Gagal menyimpan batas kekeruhan", "error");
}
}
/* ===================== BATAS TINGGI AIR ===================== */
async function setBatasTinggiAir() {
const nilai = parseFloat(document.getElementById("input-tinggi-air").value);
if (isNaN(nilai)) {
showToast("Masukkan tinggi air", "error");
return;
}
try {
const response = await fetch(NR + "/set-batas_distance", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
min_jarak: nilai
})
});
if (!response.ok) {
throw new Error("HTTP " + response.status);
}
await response.text();
showToast("Batas tinggi air berhasil disimpan");
} catch (err) {
console.error(err);
showToast("Gagal menyimpan batas tinggi air", "error");
}
}
/* ===================== TINGGI AQUARIUM ===================== */
async function setTinggiAquarium() {
const nilai = parseFloat(document.getElementById("input-tinggi-aquarium").value);
if (isNaN(nilai)) {
showToast("Masukkan tinggi aquarium", "error");
return;
}
try {
const response = await fetch(NR + "/set-tinggi_aquarium", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
tinggi_aquarium: nilai
})
});
if (!response.ok) {
throw new Error("HTTP " + response.status);
}
await response.text();
showToast("Tinggi aquarium berhasil disimpan");
} catch (err) {
console.error(err);
showToast("Gagal menyimpan tinggi aquarium", "error");
}
}
/* ── Load status terkini dari Node-RED /get-status ────── */
async function loadStatus() {
try {
const res = await fetch(NR + '/get-status', { cache: 'no-store' });
if (!res.ok) return;
const d = await res.json();
if (!d) return;
// Pompa
if (d.relay !== undefined) {
const on = d.relay == 1;
const statusPompa = document.getElementById('status-pompa');
if (statusPompa) {
statusPompa.textContent = on ? 'ON' : 'OFF';
statusPompa.style.color = on ? '#22c55e' : '#ef4444';
}
}
if (d.pompa_mode !== undefined) {
const isAuto = d.pompa_mode === 'AUTO';
const togglePompa = document.getElementById('toggle-pompa-auto');
if (togglePompa) togglePompa.checked = isAuto;
const labelPompa = document.getElementById('label-pompa-mode');
if (labelPompa) labelPompa.textContent = isAuto ? 'Auto' : 'Manual';
}
if (d.batas_amonia !== undefined) {
document.getElementById('input-amonia').value = d.batas_amonia;
}
if (d.batas_turbidity !== undefined) {
document.getElementById('input-kekeruhan').value = d.batas_turbidity;
}
if (d.batas_distance !== undefined) {
document.getElementById('input-tinggi-air').value = d.batas_distance;
}
if (d.tinggi_aquarium !== undefined) {
document.getElementById('input-tinggi-aquarium').value = d.tinggi_aquarium;
}
// Lampu
if (d.lampu !== undefined) {
const on = d.lampu == 1;
const labelLampu = document.getElementById('label-lampu');
if (labelLampu) {
labelLampu.textContent = on ? 'ON' : 'OFF';
}
const toggleLampu = document.getElementById('toggle-lampu');
if (toggleLampu) {
toggleLampu.checked = on;
}
}
const jamMulai = document.getElementById("jam-mulai");
if (jamMulai && !jamMulai.value && d.lampu_on_time) {
jamMulai.value = d.lampu_on_time.substring(0,5);
}
const jamSelesai = document.getElementById("jam-selesai");
if (jamSelesai && !jamSelesai.value && d.lampu_off_time) {
jamSelesai.value = d.lampu_off_time.substring(0,5);
}
} catch(e) {
console.warn('[kontrol] Gagal load status:', e.message);
}
}
/* ── Toast ────────────────────────────────────────────── */
function showToast(message, type = "success") {
let toast = document.getElementById("nr-toast");
if (!toast) {
toast = document.createElement("div");
toast.id = "nr-toast";
toast.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
min-width: 320px;
padding: 15px 20px;
border-radius: 8px;
color: #fff;
font-size: 14px;
font-weight: 600;
z-index: 99999;
opacity: 0;
transform: translateX(100%);
transition: all .35s ease;
box-shadow: 0 8px 20px rgba(0,0,0,.25);
`;
document.body.appendChild(toast);
}
if (type === "success") {
toast.style.background = "#22c55e";
toast.innerHTML = "✅ " + message;
} else {
toast.style.background = "#ef4444";
toast.innerHTML = "❌ " + message;
}
toast.style.opacity = "1";
toast.style.transform = "translateX(0)";
clearTimeout(toast.timer);
toast.timer = setTimeout(() => {
toast.style.opacity = "0";
toast.style.transform = "translateX(100%)";
}, 3000);
}
/* ── Dropdown ─────────────────────────────────────────── */
const notifBtn = document.getElementById('notifBtn');
const notifDropdown = document.getElementById('notifDropdown');
const userBtn = document.getElementById('userBtn');
const userDropdown = document.getElementById('userDropdown');
notifBtn.onclick = e => { e.stopPropagation(); notifDropdown.classList.toggle('show'); userDropdown.classList.remove('show'); };
userBtn.onclick = e => { e.stopPropagation(); userDropdown.classList.toggle('show'); notifDropdown.classList.remove('show'); };
document.onclick = () => { notifDropdown.classList.remove('show'); userDropdown.classList.remove('show'); };
document.addEventListener('DOMContentLoaded', () => {
loadStatus();
setInterval(loadStatus, 10000); // refresh status tiap 10 detik
});
</script>
<script>
/**
* NODERED_URL = URL server Node-RED (misal http://192.168.1.x:1880)
* node-red.js akan panggil NODERED_URL + "/get-sensor"
*
* FIREBASE_DB_URL dipakai sebagai fallback jika NODERED_URL tidak diset.
*
* Ganti IP sesuai server Node-RED kamu.
*/
window.NODERED_URL = "https://node-red.aquamonn.online";
window.FIREBASE_DB_URL = "<?= rtrim(FIREBASE_DB_URL, '/') ?>";
// Jika NODERED_URL tidak dikonfigurasi di PHP, kosongkan agar
// node-red.js fallback ke Firebase langsung.
if (!window.NODERED_URL) {
// Firebase path sesuai flow:
// https://aquascape-67707-default-rtdb.firebaseio.com/sensor.json
window.FIREBASE_DB_URL = window.FIREBASE_DB_URL || "https://aquascape-67707-default-rtdb.firebaseio.com";
}
window.onSensorUpdate = function(ok) {
const dot = document.getElementById("conn-status");
const label = document.getElementById("conn-label");
const ts = document.getElementById("last-update");
if (!dot) return;
dot.className = ok ? "online" : "offline";
label.textContent = ok ? "Live" : "Offline";
if (ok) ts.textContent = " · " + new Date().toLocaleTimeString("id-ID");
};
</script>
<script src="assets/js/node-red.js"></script>
<?php require_once 'partials/footer.php'; ?>