first commit
This commit is contained in:
commit
fb3bcc6565
|
@ -0,0 +1,155 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Dashboard Admin - Monitoring Parkir</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
<script src="https://unpkg.com/mqtt/dist/mqtt.min.js"></script>
|
||||
<script type="module" src="script.js"></script>
|
||||
<style>
|
||||
.back-btn {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
margin-bottom: 25px;
|
||||
transition: background-color 0.3s ease;
|
||||
box-shadow: 0 4px 8px rgba(76, 175, 80, 0.4);
|
||||
display: inline-block;
|
||||
}
|
||||
.back-btn:hover {
|
||||
background-color: #388E3C;
|
||||
box-shadow: 0 6px 12px rgba(56, 142, 60, 0.6);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.parking-history {
|
||||
background-color: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 25px 30px;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
|
||||
max-width: 480px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 450px;
|
||||
}
|
||||
.parking-history h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 20px;
|
||||
color: #4CAF50;
|
||||
font-weight: 700;
|
||||
font-size: 1.7em;
|
||||
border-bottom: 3px solid #4CAF50;
|
||||
padding-bottom: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
#history-container {
|
||||
flex-grow: 1;
|
||||
overflow-y: auto;
|
||||
padding-right: 10px;
|
||||
}
|
||||
.history-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.history-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 14px 18px;
|
||||
margin-bottom: 12px;
|
||||
border-radius: 8px;
|
||||
border-left: 6px solid;
|
||||
background-color: #f9f9f9;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.04);
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
.history-item:hover {
|
||||
background-color: #e6f7e6;
|
||||
}
|
||||
.history-item.entry {
|
||||
border-left-color: #f44336;
|
||||
}
|
||||
.history-item.exit {
|
||||
border-left-color: #4CAF50;
|
||||
}
|
||||
.history-info {
|
||||
font-weight: 600;
|
||||
font-size: 1.1em;
|
||||
color: #333;
|
||||
}
|
||||
.history-info small {
|
||||
display: block;
|
||||
font-weight: normal;
|
||||
color: #666;
|
||||
font-size: 0.8em;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.history-time {
|
||||
color: #666;
|
||||
font-size: 0.9em;
|
||||
font-style: italic;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.loading-message {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
padding: 20px;
|
||||
}
|
||||
/* Scrollbar custom */
|
||||
#history-container::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
#history-container::-webkit-scrollbar-track {
|
||||
background: #eee;
|
||||
border-radius: 10px;
|
||||
}
|
||||
#history-container::-webkit-scrollbar-thumb {
|
||||
background-color: #4CAF50;
|
||||
border-radius: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="admin-view">
|
||||
<div class="admin-header">
|
||||
<h1>Dashboard Admin - Monitoring Parkir</h1>
|
||||
</div>
|
||||
<a href="index.html"><button class="back-btn">Kembali ke Mode Pengguna</button></a>
|
||||
<div class="admin-content">
|
||||
<div class="parking-status">
|
||||
<h2>Status Parkir Saat Ini</h2>
|
||||
<div id="sensor_1" class="sensor">
|
||||
<img src="images/suv-car.png" alt="Logo Kendaraan">
|
||||
<h3>Parkir Slot 1</h3>
|
||||
<p id="status_1" class="status">Tersedia</p>
|
||||
</div>
|
||||
<div id="sensor_2" class="sensor">
|
||||
<img src="images/suv-car.png" alt="Logo Kendaraan">
|
||||
<h3>Parkir Slot 2</h3>
|
||||
<p id="status_2" class="status">Tersedia</p>
|
||||
</div>
|
||||
<div id="sensor_3" class="sensor">
|
||||
<img src="images/suv-car.png" alt="Logo Kendaraan">
|
||||
<h3>Parkir Slot 3</h3>
|
||||
<p id="status_3" class="status">Tersedia</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="parking-history">
|
||||
<h3>Riwayat Parkir Realtime</h3>
|
||||
<div id="history-container">
|
||||
<div class="loading-message">
|
||||
🔄 Memuat riwayat parkir...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
|
@ -0,0 +1,74 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Monitoring Slot Parkir</title>
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
<script src="https://unpkg.com/mqtt/dist/mqtt.min.js"></script>
|
||||
<script type="module" src="script.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<button class="mode-btn" onclick="showAdminLogin()">Mode Developer/Admin</button>
|
||||
</div>
|
||||
<div id="user-view">
|
||||
<h1>Monitoring Slot Parkir</h1>
|
||||
<!-- Sensor Slot 1 -->
|
||||
<div id="sensor_1" class="sensor">
|
||||
<img src="images/suv-car.png" alt="Logo Kendaraan" />
|
||||
<h3>Parkir Slot 1</h3>
|
||||
<p class="status" id="status_1">Status: Tersedia</p>
|
||||
</div>
|
||||
<!-- Sensor Slot 2 -->
|
||||
<div id="sensor_2" class="sensor">
|
||||
<img src="images/suv-car.png" alt="Logo Kendaraan" />
|
||||
<h3>Parkir Slot 2</h3>
|
||||
<p class="status" id="status_2">Status: Tersedia</p>
|
||||
</div>
|
||||
<!-- Sensor Slot 3 -->
|
||||
<div id="sensor_3" class="sensor">
|
||||
<img src="images/suv-car.png" alt="Logo Kendaraan" />
|
||||
<h3>Parkir Slot 3</h3>
|
||||
<p class="status" id="status_3">Status: Tersedia</p>
|
||||
</div>
|
||||
|
||||
<div class="map-container">
|
||||
<h2>Denah Lokasi Parkir</h2>
|
||||
<div class="map-indicators">
|
||||
<img src="images/Denah2.png" alt="Denah Lokasi Parkir" style="max-width: 80%; height: auto;" />
|
||||
<div id="indicator_1" class="slot-indicator available" title="Slot 1">1</div>
|
||||
<div id="indicator_2" class="slot-indicator available" title="Slot 2">2</div>
|
||||
<div id="indicator_3" class="slot-indicator available" title="Slot 3">3</div>
|
||||
</div>
|
||||
|
||||
<div class="map-legend">
|
||||
<div class="legend-item">
|
||||
<div class="legend-color legend-available"></div>
|
||||
<span>Tersedia</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color legend-occupied"></div>
|
||||
<span>Terisi</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="direction">
|
||||
<p style="font-size: 15px;">Notes: Tempat parkir berada di sebelah kanan, dekat dengan palang parkir.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Admin Login Modal -->
|
||||
<div id="admin-modal" class="modal">
|
||||
<div class="modal-content">
|
||||
<h2>Login Admin</h2>
|
||||
<p>Masukkan password untuk mengakses mode developer:</p>
|
||||
<input type="password" id="admin-password" placeholder="Password" onkeypress="handlePasswordKeypress(event)" />
|
||||
<br />
|
||||
<button onclick="loginAdmin()">Login</button>
|
||||
<button class="cancel-btn" onclick="closeAdminModal()">Batal</button>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,577 @@
|
|||
// FIREBASE KONFIGURASI
|
||||
import { initializeApp } from "https://www.gstatic.com/firebasejs/9.6.1/firebase-app.js";
|
||||
import { getDatabase, ref, set, onValue, push } from "https://www.gstatic.com/firebasejs/9.6.1/firebase-database.js";
|
||||
|
||||
// Firebase Configuration
|
||||
const firebaseConfig = {
|
||||
apiKey: "AIzaSyAS1qxaUCFRZ1Fe25qGK27JtwCWEPuaYys",
|
||||
authDomain: "parkingslot-c5fba.firebaseapp.com",
|
||||
databaseURL: "https://parkingslot-c5fba-default-rtdb.firebaseio.com",
|
||||
projectId: "parkingslot-c5fba",
|
||||
storageBucket: "parkingslot-c5fba.firebasestorage.app",
|
||||
messagingSenderId: "675813222621",
|
||||
appId: "1:675813222621:web:2986b33a1e5fd10c5b9334"
|
||||
};
|
||||
|
||||
// Initialize Firebase
|
||||
const app = initializeApp(firebaseConfig);
|
||||
const db = getDatabase(app);
|
||||
console.log("Firebase berhasil terhubung:", db);
|
||||
|
||||
// Global Variables
|
||||
let isAdminMode = false;
|
||||
let previousSlotStatus = {
|
||||
slot_1: null, // null berarti belum pernah di-set
|
||||
slot_2: null,
|
||||
slot_3: null
|
||||
};
|
||||
let slotStatus = {
|
||||
slot_1: "Tersedia",
|
||||
slot_2: "Tersedia",
|
||||
slot_3: "Tersedia"
|
||||
};
|
||||
let isSystemInitialized = false;
|
||||
|
||||
// Set isAdminMode true jika di halaman admin.html
|
||||
if (window.location.pathname.includes('admin.html')) {
|
||||
isAdminMode = true;
|
||||
console.log("Mode Admin aktif");
|
||||
} else {
|
||||
console.log("Mode User aktif");
|
||||
}
|
||||
|
||||
// Initialize previous status dari Firebase saat startup
|
||||
function initializePreviousStatus() {
|
||||
console.log("🔄 Menginisialisasi status sebelumnya dari Firebase...");
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let loadedSlots = 0;
|
||||
const totalSlots = 3;
|
||||
|
||||
// Load status sebelumnya dari Firebase untuk setiap slot
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const slotKey = `slot_${i}`;
|
||||
|
||||
onValue(ref(db, `parkir/slot_${i}`), function(snapshot) {
|
||||
const data = snapshot.val();
|
||||
if (data && data.status) {
|
||||
previousSlotStatus[slotKey] = data.status;
|
||||
console.log(`✅ Status awal slot ${i}: ${data.status}`);
|
||||
} else {
|
||||
// Jika tidak ada data, set default
|
||||
previousSlotStatus[slotKey] = "Tersedia";
|
||||
console.log(`⚠️ Slot ${i} tidak ada data, set default: Tersedia`);
|
||||
}
|
||||
|
||||
loadedSlots++;
|
||||
if (loadedSlots === totalSlots) {
|
||||
isSystemInitialized = true;
|
||||
console.log("✅ Semua slot berhasil diinisialisasi");
|
||||
console.log("Previous Status:", previousSlotStatus);
|
||||
resolve();
|
||||
}
|
||||
}, { onlyOnce: true });
|
||||
}
|
||||
|
||||
// Timeout fallback jika Firebase lambat
|
||||
setTimeout(() => {
|
||||
if (!isSystemInitialized) {
|
||||
console.log("⏰ Timeout inisialisasi, menggunakan default values");
|
||||
previousSlotStatus = {
|
||||
slot_1: "Tersedia",
|
||||
slot_2: "Tersedia",
|
||||
slot_3: "Tersedia"
|
||||
};
|
||||
isSystemInitialized = true;
|
||||
resolve();
|
||||
}
|
||||
}, 5000);
|
||||
});
|
||||
}
|
||||
|
||||
// Setup MQTT
|
||||
const client = mqtt.connect('ws://broker.hivemq.com:8000/mqtt');
|
||||
client.on('connect', function () {
|
||||
console.log("🔗 Terhubung ke Broker MQTT");
|
||||
client.subscribe('parkir/slot_1');
|
||||
client.subscribe('parkir/slot_2');
|
||||
client.subscribe('parkir/slot_3');
|
||||
client.subscribe('parkir/status');
|
||||
|
||||
// Initialize previous status setelah MQTT connect
|
||||
setTimeout(async () => {
|
||||
await initializePreviousStatus();
|
||||
console.log("🚀 Sistem siap menerima data MQTT");
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
client.on('message', function (topic, message) {
|
||||
// Jangan proses jika sistem belum diinisialisasi
|
||||
if (!isSystemInitialized) {
|
||||
console.log("⏳ Sistem belum siap, mengabaikan pesan MQTT");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`📨 Pesan MQTT diterima: ${message.toString()} di topik: ${topic}`);
|
||||
const distance = parseInt(message.toString());
|
||||
|
||||
if (isNaN(distance)) {
|
||||
console.error('❌ Data tidak valid diterima:', message.toString());
|
||||
return;
|
||||
}
|
||||
|
||||
// Process each slot
|
||||
if (topic === 'parkir/slot_1') {
|
||||
processSlotUpdate(1, distance, 'mqtt');
|
||||
}
|
||||
if (topic === 'parkir/slot_2') {
|
||||
processSlotUpdate(2, distance, 'mqtt');
|
||||
}
|
||||
if (topic === 'parkir/slot_3') {
|
||||
processSlotUpdate(3, distance, 'mqtt');
|
||||
}
|
||||
|
||||
checkParkingStatus();
|
||||
});
|
||||
|
||||
// Fungsi utama untuk memproses update slot - DIPERBAIKI
|
||||
function processSlotUpdate(slotNumber, distance, source = 'unknown') {
|
||||
const newStatus = distance > 5 ? "Tersedia" : "Terisi";
|
||||
const slotKey = `slot_${slotNumber}`;
|
||||
|
||||
console.log(`🔄 Processing slot ${slotNumber}: distance=${distance}, newStatus=${newStatus}, source=${source}`);
|
||||
console.log(`📊 Previous status slot ${slotNumber}: ${previousSlotStatus[slotKey]}`);
|
||||
|
||||
// Update status slot di UI
|
||||
updateSlotStatus(slotNumber, distance);
|
||||
|
||||
// Cek perubahan status - LOGIKA DIPERBAIKI
|
||||
const prevStatus = previousSlotStatus[slotKey];
|
||||
|
||||
// Jika ini adalah data pertama (previous status null), langsung log
|
||||
if (prevStatus === null) {
|
||||
console.log(`🆕 DATA PERTAMA - Slot ${slotNumber}: null -> ${newStatus}`);
|
||||
logParkingHistory(slotNumber, newStatus, isAdminMode, source);
|
||||
previousSlotStatus[slotKey] = newStatus;
|
||||
}
|
||||
// Jika ada perubahan status dari status sebelumnya
|
||||
else if (prevStatus !== newStatus) {
|
||||
console.log(`🔥 PERUBAHAN TERDETEKSI - Slot ${slotNumber}: ${prevStatus} -> ${newStatus}`);
|
||||
console.log(`📝 Mode saat ini: ${isAdminMode ? 'Admin' : 'User'}`);
|
||||
|
||||
// SELALU log parking history baik di user maupun admin mode
|
||||
logParkingHistory(slotNumber, newStatus, isAdminMode, source);
|
||||
|
||||
// Update previous status SETELAH logging
|
||||
previousSlotStatus[slotKey] = newStatus;
|
||||
console.log(`✅ Previous status slot ${slotNumber} diupdate ke: ${newStatus}`);
|
||||
} else {
|
||||
console.log(`➡️ Tidak ada perubahan status slot ${slotNumber}: ${newStatus}`);
|
||||
}
|
||||
|
||||
// Update current status
|
||||
slotStatus[slotKey] = newStatus;
|
||||
}
|
||||
|
||||
// Fungsi untuk update indikator pada denah
|
||||
function updateMapIndicator(slot, status) {
|
||||
const indicator = document.getElementById('indicator_' + slot);
|
||||
if (indicator) {
|
||||
indicator.classList.remove('available', 'occupied');
|
||||
if (status === "Tersedia") {
|
||||
indicator.classList.add('available');
|
||||
indicator.style.backgroundColor = '#4CAF50';
|
||||
} else {
|
||||
indicator.classList.add('occupied');
|
||||
indicator.style.backgroundColor = '#f44336';
|
||||
}
|
||||
indicator.title = `Slot ${slot} - ${status}`;
|
||||
console.log(`🗺️ Indikator denah slot ${slot} diupdate: ${status}`);
|
||||
}
|
||||
}
|
||||
|
||||
function updateSlotStatus(slot, distance) {
|
||||
const statusElement = document.getElementById('status_' + slot);
|
||||
const slotElement = document.getElementById('sensor_' + slot);
|
||||
|
||||
let status;
|
||||
if (distance > 5) {
|
||||
status = "Tersedia";
|
||||
if (statusElement) {
|
||||
statusElement.textContent = "Status: Tersedia";
|
||||
statusElement.className = "status empty";
|
||||
}
|
||||
if (slotElement) {
|
||||
slotElement.style.backgroundColor = "lightgreen";
|
||||
}
|
||||
} else {
|
||||
status = "Terisi";
|
||||
if (statusElement) {
|
||||
statusElement.textContent = "Status: Terisi";
|
||||
statusElement.className = "status occupied";
|
||||
}
|
||||
if (slotElement) {
|
||||
slotElement.style.backgroundColor = "lightcoral";
|
||||
}
|
||||
}
|
||||
|
||||
updateMapIndicator(slot, status);
|
||||
// Simpan ke Firebase tanpa memanggil processSlotUpdate lagi untuk menghindari loop
|
||||
updateFirebaseStatusOnly(slot, distance);
|
||||
}
|
||||
|
||||
function checkParkingStatus() {
|
||||
const allFull = slotStatus.slot_1 === "Terisi" && slotStatus.slot_2 === "Terisi" && slotStatus.slot_3 === "Terisi";
|
||||
if (allFull) {
|
||||
console.log("🚫 Parkir Penuh - Palang tetap tertutup.");
|
||||
client.publish('palang/status', 'Tutup');
|
||||
} else {
|
||||
console.log("✅ Ada slot tersedia - Palang dibuka.");
|
||||
client.publish('palang/status', 'Buka');
|
||||
}
|
||||
}
|
||||
|
||||
// Fungsi khusus untuk update Firebase tanpa trigger logging
|
||||
function updateFirebaseStatusOnly(slot, distance) {
|
||||
const status = distance > 5 ? "Tersedia" : "Terisi";
|
||||
console.log(`💾 Menyimpan ke Firebase - Slot ${slot}: distance=${distance}, status=${status}`);
|
||||
|
||||
set(ref(db, 'parkir/slot_' + slot), {
|
||||
distance: distance,
|
||||
status: status,
|
||||
lastUpdate: new Date().toISOString(),
|
||||
updatedBy: isAdminMode ? 'admin' : 'user'
|
||||
})
|
||||
.then(() => {
|
||||
console.log(`✅ Status slot ${slot} berhasil disimpan ke Firebase`);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("❌ Gagal menyimpan status ke Firebase:", error);
|
||||
});
|
||||
}
|
||||
|
||||
// Fungsi log parking history - DIPERBAIKI DENGAN LOGGING DETAIL
|
||||
function logParkingHistory(slot, status, isAdmin, source = 'unknown') {
|
||||
const timestamp = new Date().toLocaleString('id-ID');
|
||||
const historyData = {
|
||||
slot: slot,
|
||||
status: status,
|
||||
timestamp: timestamp,
|
||||
date: new Date().toISOString(),
|
||||
source: isAdmin ? 'admin' : 'user',
|
||||
dataSource: source, // mqtt, firebase, manual
|
||||
sessionId: generateSessionId()
|
||||
};
|
||||
|
||||
console.log(`📊 MENYIMPAN RIWAYAT PARKIR:`);
|
||||
console.log(` - Slot: ${slot}`);
|
||||
console.log(` - Status: ${status}`);
|
||||
console.log(` - Mode: ${historyData.source}`);
|
||||
console.log(` - Data Source: ${source}`);
|
||||
console.log(` - Timestamp: ${timestamp}`);
|
||||
|
||||
push(ref(db, 'parking_history'), historyData)
|
||||
.then(() => {
|
||||
console.log(`✅ SUKSES! Riwayat parkir slot ${slot} tersimpan (${historyData.source})`);
|
||||
|
||||
// Hanya update tampilan jika di mode admin
|
||||
if (isAdmin && typeof updateHistoryDisplay === 'function') {
|
||||
console.log(`🔄 Mengupdate tampilan riwayat admin...`);
|
||||
updateHistoryDisplay();
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(`❌ GAGAL! Menyimpan riwayat parkir slot ${slot}:`, error);
|
||||
});
|
||||
}
|
||||
|
||||
// Generate session ID untuk tracking
|
||||
function generateSessionId() {
|
||||
return Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
|
||||
// Fungsi untuk update tampilan riwayat (hanya di admin mode)
|
||||
function updateHistoryDisplay() {
|
||||
// Hanya jalankan jika di admin mode
|
||||
if (!isAdminMode) {
|
||||
console.log("updateHistoryDisplay diabaikan - bukan mode admin");
|
||||
return;
|
||||
}
|
||||
|
||||
onValue(ref(db, 'parking_history'), function(snapshot) {
|
||||
const historyContainer = document.getElementById('history-container');
|
||||
if (!historyContainer) {
|
||||
console.log("Element history-container tidak ditemukan");
|
||||
return;
|
||||
}
|
||||
|
||||
const data = snapshot.val();
|
||||
if (!data) {
|
||||
historyContainer.innerHTML = '<div class="loading-message">📝 Belum ada riwayat parkir</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const historyArray = Object.values(data).sort((a, b) => new Date(b.date) - new Date(a.date));
|
||||
let historyHTML = '<div class="history-list">';
|
||||
|
||||
historyArray.forEach(record => {
|
||||
const statusClass = record.status === 'Terisi' ? 'entry' : 'exit';
|
||||
const statusText = record.status === 'Terisi' ? 'Masuk' : 'Keluar';
|
||||
const sourceIcon = record.source === 'admin' ? '🔧' : '👤';
|
||||
const dataSourceIcon = record.dataSource === 'mqtt' ? '📡' : '💾';
|
||||
|
||||
historyHTML += `
|
||||
<div class="history-item ${statusClass}">
|
||||
<div class="history-info">
|
||||
<strong>Slot ${record.slot}</strong> - ${statusText}
|
||||
<small>${sourceIcon} ${record.source} ${dataSourceIcon} ${record.dataSource || 'unknown'}</small>
|
||||
</div>
|
||||
<div class="history-time">${record.timestamp}</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
historyHTML += '</div>';
|
||||
historyContainer.innerHTML = historyHTML;
|
||||
console.log(`📋 Tampilan riwayat diupdate dengan ${historyArray.length} record`);
|
||||
});
|
||||
}
|
||||
|
||||
// Listener realtime Firebase untuk semua slot - DIPERBAIKI
|
||||
function setupRealtimeListeners() {
|
||||
console.log("🔥 Setting up Firebase realtime listeners...");
|
||||
|
||||
// Listener untuk slot 1
|
||||
onValue(ref(db, 'parkir/slot_1'), function(snapshot) {
|
||||
const data = snapshot.val();
|
||||
if (data) {
|
||||
console.log(`🔥 Firebase realtime update slot 1: distance=${data.distance}, status=${data.status}`);
|
||||
|
||||
// Hanya update UI, jangan trigger processSlotUpdate untuk menghindari double logging
|
||||
const statusElement = document.getElementById('status_1');
|
||||
const slotElement = document.getElementById('sensor_1');
|
||||
|
||||
if (data.distance > 5) {
|
||||
if (statusElement) {
|
||||
statusElement.textContent = "Status: Tersedia";
|
||||
statusElement.className = "status empty";
|
||||
}
|
||||
if (slotElement) {
|
||||
slotElement.style.backgroundColor = "lightgreen";
|
||||
}
|
||||
} else {
|
||||
if (statusElement) {
|
||||
statusElement.textContent = "Status: Terisi";
|
||||
statusElement.className = "status occupied";
|
||||
}
|
||||
if (slotElement) {
|
||||
slotElement.style.backgroundColor = "lightcoral";
|
||||
}
|
||||
}
|
||||
|
||||
updateMapIndicator(1, data.status);
|
||||
slotStatus.slot_1 = data.status;
|
||||
}
|
||||
});
|
||||
|
||||
// Listener untuk slot 2
|
||||
onValue(ref(db, 'parkir/slot_2'), function(snapshot) {
|
||||
const data = snapshot.val();
|
||||
if (data) {
|
||||
console.log(`🔥 Firebase realtime update slot 2: distance=${data.distance}, status=${data.status}`);
|
||||
|
||||
const statusElement = document.getElementById('status_2');
|
||||
const slotElement = document.getElementById('sensor_2');
|
||||
|
||||
if (data.distance > 5) {
|
||||
if (statusElement) {
|
||||
statusElement.textContent = "Status: Tersedia";
|
||||
statusElement.className = "status empty";
|
||||
}
|
||||
if (slotElement) {
|
||||
slotElement.style.backgroundColor = "lightgreen";
|
||||
}
|
||||
} else {
|
||||
if (statusElement) {
|
||||
statusElement.textContent = "Status: Terisi";
|
||||
statusElement.className = "status occupied";
|
||||
}
|
||||
if (slotElement) {
|
||||
slotElement.style.backgroundColor = "lightcoral";
|
||||
}
|
||||
}
|
||||
|
||||
updateMapIndicator(2, data.status);
|
||||
slotStatus.slot_2 = data.status;
|
||||
}
|
||||
});
|
||||
|
||||
// Listener untuk slot 3
|
||||
onValue(ref(db, 'parkir/slot_3'), function(snapshot) {
|
||||
const data = snapshot.val();
|
||||
if (data) {
|
||||
console.log(`🔥 Firebase realtime update slot 3: distance=${data.distance}, status=${data.status}`);
|
||||
|
||||
const statusElement = document.getElementById('status_3');
|
||||
const slotElement = document.getElementById('sensor_3');
|
||||
|
||||
if (data.distance > 5) {
|
||||
if (statusElement) {
|
||||
statusElement.textContent = "Status: Tersedia";
|
||||
statusElement.className = "status empty";
|
||||
}
|
||||
if (slotElement) {
|
||||
slotElement.style.backgroundColor = "lightcoral";
|
||||
}
|
||||
} else {
|
||||
if (statusElement) {
|
||||
statusElement.textContent = "Status: Terisi";
|
||||
statusElement.className = "status occupied";
|
||||
}
|
||||
if (slotElement) {
|
||||
slotElement.style.backgroundColor = "lightcoral";
|
||||
}
|
||||
}
|
||||
|
||||
updateMapIndicator(3, data.status);
|
||||
slotStatus.slot_3 = data.status;
|
||||
}
|
||||
});
|
||||
|
||||
console.log("✅ Realtime listeners untuk Firebase telah diatur");
|
||||
}
|
||||
|
||||
// Mode switching functions (Admin login modal etc.)
|
||||
window.showAdminLogin = function () {
|
||||
const modal = document.getElementById('admin-modal');
|
||||
if (modal) modal.style.display = 'flex';
|
||||
}
|
||||
|
||||
window.closeAdminModal = function () {
|
||||
const modal = document.getElementById('admin-modal');
|
||||
const input = document.getElementById('admin-password');
|
||||
if (modal) modal.style.display = 'none';
|
||||
if (input) input.value = '';
|
||||
}
|
||||
|
||||
window.loginAdmin = function () {
|
||||
const input = document.getElementById('admin-password');
|
||||
if (input && input.value === 'admin123') {
|
||||
window.location.href = 'admin.html';
|
||||
} else {
|
||||
alert('Password salah!');
|
||||
}
|
||||
}
|
||||
|
||||
window.handlePasswordKeypress = function (event) {
|
||||
if (event.key === 'Enter') {
|
||||
loginAdmin();
|
||||
}
|
||||
}
|
||||
|
||||
// Update admin status display
|
||||
function updateAdminStatus() {
|
||||
if (isAdminMode) {
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
const userStatus = document.getElementById('status_' + i)?.textContent;
|
||||
const adminElement = document.getElementById('admin-status-' + i);
|
||||
if (adminElement && userStatus) {
|
||||
adminElement.textContent = userStatus.replace('Status: ', '');
|
||||
adminElement.style.color = userStatus.includes('Tersedia') ? '#4CAF50' : '#f44336';
|
||||
adminElement.style.fontWeight = 'bold';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FUNGSI DEBUG YANG DITINGKATKAN
|
||||
window.debugParkingSystem = function() {
|
||||
console.log("=== 🔍 DEBUG PARKING SYSTEM ===");
|
||||
console.log("Mode:", isAdminMode ? "Admin" : "User");
|
||||
console.log("System Initialized:", isSystemInitialized);
|
||||
console.log("Current Slot Status:", slotStatus);
|
||||
console.log("Previous Slot Status:", previousSlotStatus);
|
||||
console.log("Firebase DB Connected:", !!db);
|
||||
console.log("MQTT Client Connected:", client.connected);
|
||||
console.log("===============================");
|
||||
}
|
||||
|
||||
// Fungsi untuk testing manual - DIPERBAIKI
|
||||
window.testHistoryLogging = function(slot, status) {
|
||||
console.log(`🧪 MANUAL TEST - Logging history for slot ${slot} with status ${status}`);
|
||||
|
||||
// Simulasi perubahan status
|
||||
const slotKey = `slot_${slot}`;
|
||||
const currentPrev = previousSlotStatus[slotKey];
|
||||
|
||||
console.log(`📊 Before test - Previous: ${currentPrev}, New: ${status}`);
|
||||
|
||||
// Force log tanpa cek perubahan
|
||||
logParkingHistory(slot, status, isAdminMode, 'manual-test');
|
||||
|
||||
// Update previous status
|
||||
previousSlotStatus[slotKey] = status;
|
||||
console.log(`✅ Test completed - Previous status updated to: ${status}`);
|
||||
}
|
||||
|
||||
// FUNGSI KHUSUS UNTUK FORCE LOGGING (Testing)
|
||||
window.forceLogHistory = function(slot, status) {
|
||||
console.log(`🚨 FORCE LOGGING - Slot ${slot}, Status: ${status}`);
|
||||
logParkingHistory(slot, status, isAdminMode, 'force-test');
|
||||
}
|
||||
|
||||
// Inisialisasi saat DOM ready
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
console.log(`🚀 Sistem parkir dimulai - Mode: ${isAdminMode ? '🔧 Admin' : '👤 User'}`);
|
||||
|
||||
// Inisialisasi indikator denah
|
||||
for (let i = 1; i <= 3; i++) {
|
||||
updateMapIndicator(i, "Tersedia");
|
||||
}
|
||||
console.log("🗺️ Indikator denah telah diinisialisasi");
|
||||
|
||||
// Setup realtime listeners
|
||||
setupRealtimeListeners();
|
||||
|
||||
// Jika admin mode, setup tampilan riwayat dan update berkala
|
||||
if (isAdminMode) {
|
||||
console.log("🔧 Mengaktifkan fitur admin...");
|
||||
updateHistoryDisplay();
|
||||
|
||||
// Update admin status setiap detik
|
||||
setInterval(updateAdminStatus, 1000);
|
||||
|
||||
console.log("✅ Admin mode: Pemantauan riwayat realtime aktif");
|
||||
} else {
|
||||
console.log("👤 User mode: Siap mengirim data riwayat ke Firebase");
|
||||
|
||||
// Test functions untuk user mode
|
||||
window.testUserMode = function() {
|
||||
console.log("🧪 Testing user mode logging...");
|
||||
console.log("🔍 Current state:", {
|
||||
initialized: isSystemInitialized,
|
||||
previousStatus: previousSlotStatus,
|
||||
currentStatus: slotStatus
|
||||
});
|
||||
|
||||
// Test dengan force logging
|
||||
forceLogHistory(1, "Terisi");
|
||||
setTimeout(() => forceLogHistory(1, "Tersedia"), 3000);
|
||||
};
|
||||
|
||||
// Fungsi untuk simulasi sensor
|
||||
window.simulateSensor = function(slot, distance) {
|
||||
console.log(`🎯 Simulasi sensor slot ${slot} dengan jarak ${distance}cm`);
|
||||
processSlotUpdate(slot, distance, 'simulation');
|
||||
};
|
||||
}
|
||||
|
||||
console.log("✅ Inisialisasi sistem parkir selesai");
|
||||
console.log("💡 Fungsi debugging:");
|
||||
console.log(" - debugParkingSystem() : Info sistem");
|
||||
console.log(" - forceLogHistory(slot, status) : Force log history");
|
||||
if (!isAdminMode) {
|
||||
console.log(" - testUserMode() : Test user mode logging");
|
||||
console.log(" - simulateSensor(slot, distance) : Simulasi sensor");
|
||||
}
|
||||
});
|
|
@ -0,0 +1,236 @@
|
|||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f4f4f9;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
margin: 0;
|
||||
}
|
||||
h1 {
|
||||
color: #4CAF50;
|
||||
font-size: 2.5em;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.mode-buttons {
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
.mode-btn {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
margin: 0 10px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
.mode-btn:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
.sensor {
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
margin: 20px;
|
||||
padding: 20px;
|
||||
display: inline-block;
|
||||
width: 250px;
|
||||
position: relative;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
.sensor h3 {
|
||||
margin-top: 0;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
.sensor img {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 1px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
}
|
||||
.status {
|
||||
font-weight: bold;
|
||||
padding: 10px;
|
||||
margin-top: 10px;
|
||||
border-radius: 5px;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
.status.empty {
|
||||
background-color: lightgreen;
|
||||
color: white;
|
||||
}
|
||||
.status.occupied {
|
||||
background-color: lightcoral;
|
||||
color: white;
|
||||
}
|
||||
p {
|
||||
font-size: 1.1em;
|
||||
margin: 0;
|
||||
}
|
||||
.map-container {
|
||||
margin-top: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
.map-container img {
|
||||
width: 80%;
|
||||
max-width: 600px;
|
||||
height: auto;
|
||||
}
|
||||
.direction {
|
||||
margin-top: 10px;
|
||||
font-size: 1.2em;
|
||||
color: #333;
|
||||
}
|
||||
/* Modal styles */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 1000;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0,0,0,0.5);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.modal-content {
|
||||
background-color: white;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
text-align: center;
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||||
min-width: 300px;
|
||||
}
|
||||
.modal h2 {
|
||||
margin-top: 0;
|
||||
color: #4CAF50;
|
||||
}
|
||||
.modal input {
|
||||
width: 200px;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.modal button {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
margin: 5px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
}
|
||||
.modal button:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
.modal .cancel-btn {
|
||||
background-color: #f44336;
|
||||
}
|
||||
.modal .cancel-btn:hover {
|
||||
background-color: #da190b;
|
||||
}
|
||||
|
||||
/* Perbaikan tampilan parking-history di admin */
|
||||
|
||||
.back-btn {
|
||||
background-color: #2196F3;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.back-btn:hover {
|
||||
background-color: #0b7dda;
|
||||
}
|
||||
|
||||
/* Styling untuk indikator slot pada denah */
|
||||
.map-indicators {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.slot-indicator {
|
||||
position: absolute;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-radius: 3px;
|
||||
background-color: #4CAF50;
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 8px;
|
||||
font-weight: bold;
|
||||
color: white;
|
||||
text-shadow: 1px 1px 1px rgba(0,0,0,0.5);
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.slot-indicator.occupied {
|
||||
background-color: #f44336;
|
||||
}
|
||||
|
||||
.slot-indicator.available {
|
||||
background-color: #4CAF50;
|
||||
}
|
||||
|
||||
/* Posisi indikator pada denah - sesuaikan dengan lokasi parkir Anda */
|
||||
#indicator_1 {
|
||||
top: 39%;
|
||||
right: 27%;
|
||||
}
|
||||
|
||||
#indicator_2 {
|
||||
top: 51%;
|
||||
right: 27%;
|
||||
}
|
||||
|
||||
#indicator_3 {
|
||||
top: 63%;
|
||||
right: 27%;
|
||||
}
|
||||
|
||||
/* Efek hover untuk indikator */
|
||||
.slot-indicator:hover {
|
||||
transform: scale(1.2);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* Legend untuk indikator */
|
||||
.map-legend {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.legend-color {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.legend-available {
|
||||
background-color: #4CAF50;
|
||||
}
|
||||
|
||||
.legend-occupied {
|
||||
background-color: #f44336;
|
||||
}
|
Loading…
Reference in New Issue