Files
Raumreinigung-Logger/dashboard.py
2026-01-30 01:00:23 +01:00

265 lines
9.8 KiB
Python

import sqlite3
from flask import Flask, render_template_string, jsonify
from datetime import datetime, date
app = Flask(__name__)
# Updated path for Docker volume
DB_FILE = "/app/data/cleaning_logs.db"
# Room definitions
BUILDINGS = {
"Haus A-C": [
list(range(101, 131)),
list(range(201, 231)),
list(range(301, 331))
],
"Haus D-F": [
list(range(131, 151)),
list(range(231, 251)),
list(range(331, 351))
],
"Haus O": [
list(range(401, 413)),
list(range(420, 436)),
list(range(440, 457))
]
}
def get_all_defined_rooms():
rooms = set()
for floors in BUILDINGS.values():
for floor in floors:
rooms.update(str(r) for r in floor)
return rooms
def get_room_statuses():
today = date.today().strftime("%Y-%m-%d")
try:
with sqlite3.connect(DB_FILE) as conn:
cursor = conn.execute("""
SELECT room_number, MAX(cleaned_at) as last_cleaned
FROM cleaning_events
GROUP BY room_number
""")
rows = cursor.fetchall()
except sqlite3.OperationalError:
return {}
statuses = {}
for room, last_cleaned in rows:
is_today = last_cleaned.startswith(today) if last_cleaned else False
statuses[room] = {
"is_today": is_today,
"last_cleaned": last_cleaned
}
return statuses
def get_special_rooms(statuses):
defined_rooms = get_all_defined_rooms()
special_rooms = [r for r in statuses.keys() if r not in defined_rooms]
return sorted(special_rooms)
@app.route('/')
def dashboard():
statuses = get_room_statuses()
special_rooms = get_special_rooms(statuses)
return render_template_string(
TEMPLATE,
buildings=BUILDINGS,
statuses=statuses,
special_rooms=special_rooms
)
@app.route('/api/status')
def api_status():
statuses = get_room_statuses()
special_rooms = get_special_rooms(statuses)
return jsonify({
"statuses": statuses,
"special_rooms": special_rooms
})
TEMPLATE = '''
<!DOCTYPE html>
<html>
<head>
<title>Room Cleaning Dashboard</title>
<style>
body { font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }
h1 { color: #333; }
.controls { margin-bottom: 20px; }
.toggle { padding: 10px 20px; font-size: 16px; cursor: pointer; border-radius: 5px; border: 1px solid #ccc; }
.building { margin-bottom: 30px; background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.building h2 { margin-top: 0; color: #555; border-bottom: 2px solid #eee; padding-bottom: 5px; }
.floor { margin-bottom: 15px; }
.floor-label { font-weight: bold; margin-bottom: 8px; color: #777; }
.rooms { display: flex; flex-wrap: wrap; gap: 8px; }
.room {
padding: 10px 15px;
border-radius: 5px;
min-width: 90px;
text-align: center;
font-weight: bold;
transition: all 0.3s ease;
}
.room.cleaned { background: #4CAF50; color: white; }
.room.not-cleaned { background: #f44336; color: white; }
.time-display {
display: block;
font-size: 14px;
font-weight: normal;
margin-top: 6px;
line-height: 1.2;
}
.hidden { display: none !important; }
</style>
</head>
<body>
<h1>🧹 Room Cleaning Status</h1>
<div class="controls">
<button class="toggle" onclick="toggleFilter()">
<span id="toggle-text">Show Only Cleaned Today</span>
</button>
</div>
<div id="buildings-container">
{% for building_name, floors in buildings.items() %}
<div class="building" data-building="{{ building_name }}">
<h2>{{ building_name }}</h2>
{% for floor in floors %}
<div class="floor">
<div class="floor-label">Etage {{ loop.index }}</div>
<div class="rooms">
{% for room in floor %}
{% set room_str = room|string %}
{% set status = statuses.get(room_str, {"is_today": False, "last_cleaned": None}) %}
<div class="room {{ 'cleaned' if status.is_today else 'not-cleaned' }}"
data-cleaned="{{ 'yes' if status.is_today else 'no' }}"
data-room="{{ room }}">
{{ room }}
<span class="time-display">
{% if status.last_cleaned %}{{ status.last_cleaned[5:10] }}<br>{{ status.last_cleaned[11:16] }}{% else %}Never{% endif %}
</span>
</div>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
{% endfor %}
<div id="sonderraeume-section" class="building {{ 'hidden' if not special_rooms }}" data-building="Sonderräume">
<h2>Sonderräume</h2>
<div class="floor">
<div class="rooms" id="special-rooms-container">
{% for room in special_rooms %}
{% set status = statuses.get(room, {"is_today": False, "last_cleaned": None}) %}
<div class="room {{ 'cleaned' if status.is_today else 'not-cleaned' }}"
data-cleaned="{{ 'yes' if status.is_today else 'no' }}"
data-room="{{ room }}">
{{ room }}
<span class="time-display">{{ status.last_cleaned[5:10] }}<br>{{ status.last_cleaned[11:16] }}</span>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
<script>
let filterActive = false;
function formatDateTime(ts) {
if (!ts) return 'Never';
return ts.slice(5, 10) + '<br>' + ts.slice(11, 16);
}
function applyFilter() {
const buildings = document.querySelectorAll('.building');
buildings.forEach(building => {
let buildingHasVisibleRoom = false;
const floors = building.querySelectorAll('.floor');
floors.forEach(floor => {
let floorHasVisibleRoom = false;
const rooms = floor.querySelectorAll('.room');
rooms.forEach(room => {
const isCleaned = room.dataset.cleaned === 'yes';
if (filterActive && !isCleaned) {
room.classList.add('hidden');
} else {
room.classList.remove('hidden');
floorHasVisibleRoom = true;
buildingHasVisibleRoom = true;
}
});
if (filterActive && !floorHasVisibleRoom) floor.classList.add('hidden');
else floor.classList.remove('hidden');
});
if (filterActive && !buildingHasVisibleRoom) building.classList.add('hidden');
else building.classList.remove('hidden');
// Special override: if building is Sonderräume and empty, always hide
if (building.dataset.building === 'Sonderräume' && building.querySelectorAll('.room').length === 0) {
building.classList.add('hidden');
}
});
}
function toggleFilter() {
filterActive = !filterActive;
document.getElementById('toggle-text').textContent = filterActive ? 'Show All Rooms' : 'Show Only Cleaned Today';
applyFilter();
}
async function updateStatuses() {
try {
const response = await fetch('/api/status');
const data = await response.json();
const statuses = data.statuses;
const specialRooms = data.special_rooms;
// Update All existing rooms in standard buildings
document.querySelectorAll('.room').forEach(roomDiv => {
const roomNo = roomDiv.dataset.room;
const s = statuses[roomNo];
if (s) {
roomDiv.classList.toggle('cleaned', s.is_today);
roomDiv.classList.toggle('not-cleaned', !s.is_today);
roomDiv.dataset.cleaned = s.is_today ? 'yes' : 'no';
roomDiv.querySelector('.time-display').innerHTML = formatDateTime(s.last_cleaned);
}
});
// Rebuild Sonderräume container
const specialContainer = document.getElementById('special-rooms-container');
const sonderSection = document.getElementById('sonderraeume-section');
if (specialRooms.length > 0) {
sonderSection.classList.remove('hidden');
let html = '';
specialRooms.forEach(room => {
const s = statuses[room];
html += `<div class="room ${s.is_today ? 'cleaned' : 'not-cleaned'}" data-cleaned="${s.is_today ? 'yes' : 'no'}" data-room="${room}">${room}<span class="time-display">${formatDateTime(s.last_cleaned)}</span></div>`;
});
specialContainer.innerHTML = html;
} else {
sonderSection.classList.add('hidden');
}
applyFilter();
} catch (e) { console.error("Update failed", e); }
}
setInterval(updateStatuses, 1000);
</script>
</body>
</html>
'''
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5001)