added that sonderraume also update
This commit is contained in:
187
dashboard.py
187
dashboard.py
@@ -3,6 +3,7 @@ from flask import Flask, render_template_string, jsonify
|
|||||||
from datetime import datetime, date
|
from datetime import datetime, date
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
|
# Updated path for Docker volume
|
||||||
DB_FILE = "/app/data/cleaning_logs.db"
|
DB_FILE = "/app/data/cleaning_logs.db"
|
||||||
|
|
||||||
# Room definitions
|
# Room definitions
|
||||||
@@ -25,7 +26,6 @@ BUILDINGS = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def get_all_defined_rooms():
|
def get_all_defined_rooms():
|
||||||
"""Get a set of all room numbers defined in buildings"""
|
|
||||||
rooms = set()
|
rooms = set()
|
||||||
for floors in BUILDINGS.values():
|
for floors in BUILDINGS.values():
|
||||||
for floor in floors:
|
for floor in floors:
|
||||||
@@ -33,9 +33,8 @@ def get_all_defined_rooms():
|
|||||||
return rooms
|
return rooms
|
||||||
|
|
||||||
def get_room_statuses():
|
def get_room_statuses():
|
||||||
"""Returns dict: {room_number: (last_cleaned_time, is_today)}"""
|
|
||||||
today = date.today().strftime("%Y-%m-%d")
|
today = date.today().strftime("%Y-%m-%d")
|
||||||
|
try:
|
||||||
with sqlite3.connect(DB_FILE) as conn:
|
with sqlite3.connect(DB_FILE) as conn:
|
||||||
cursor = conn.execute("""
|
cursor = conn.execute("""
|
||||||
SELECT room_number, MAX(cleaned_at) as last_cleaned
|
SELECT room_number, MAX(cleaned_at) as last_cleaned
|
||||||
@@ -43,6 +42,8 @@ def get_room_statuses():
|
|||||||
GROUP BY room_number
|
GROUP BY room_number
|
||||||
""")
|
""")
|
||||||
rows = cursor.fetchall()
|
rows = cursor.fetchall()
|
||||||
|
except sqlite3.OperationalError:
|
||||||
|
return {}
|
||||||
|
|
||||||
statuses = {}
|
statuses = {}
|
||||||
for room, last_cleaned in rows:
|
for room, last_cleaned in rows:
|
||||||
@@ -51,18 +52,11 @@ def get_room_statuses():
|
|||||||
"is_today": is_today,
|
"is_today": is_today,
|
||||||
"last_cleaned": last_cleaned
|
"last_cleaned": last_cleaned
|
||||||
}
|
}
|
||||||
|
|
||||||
return statuses
|
return statuses
|
||||||
|
|
||||||
def get_special_rooms(statuses):
|
def get_special_rooms(statuses):
|
||||||
"""Get rooms that aren't in any defined building"""
|
|
||||||
defined_rooms = get_all_defined_rooms()
|
defined_rooms = get_all_defined_rooms()
|
||||||
special_rooms = []
|
special_rooms = [r for r in statuses.keys() if r not in defined_rooms]
|
||||||
|
|
||||||
for room_number in statuses.keys():
|
|
||||||
if room_number not in defined_rooms:
|
|
||||||
special_rooms.append(room_number)
|
|
||||||
|
|
||||||
return sorted(special_rooms)
|
return sorted(special_rooms)
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
@@ -78,7 +72,6 @@ def dashboard():
|
|||||||
|
|
||||||
@app.route('/api/status')
|
@app.route('/api/status')
|
||||||
def api_status():
|
def api_status():
|
||||||
"""API endpoint for live updates"""
|
|
||||||
statuses = get_room_statuses()
|
statuses = get_room_statuses()
|
||||||
special_rooms = get_special_rooms(statuses)
|
special_rooms = get_special_rooms(statuses)
|
||||||
return jsonify({
|
return jsonify({
|
||||||
@@ -95,46 +88,46 @@ TEMPLATE = '''
|
|||||||
body { font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }
|
body { font-family: Arial, sans-serif; margin: 20px; background: #f5f5f5; }
|
||||||
h1 { color: #333; }
|
h1 { color: #333; }
|
||||||
.controls { margin-bottom: 20px; }
|
.controls { margin-bottom: 20px; }
|
||||||
.toggle { padding: 10px 20px; font-size: 16px; cursor: pointer; }
|
.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; }
|
.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; }
|
.building h2 { margin-top: 0; color: #555; border-bottom: 2px solid #eee; padding-bottom: 5px; }
|
||||||
.floor { margin-bottom: 15px; }
|
.floor { margin-bottom: 15px; }
|
||||||
.floor-label { font-weight: bold; margin-bottom: 5px; color: #777; }
|
.floor-label { font-weight: bold; margin-bottom: 8px; color: #777; }
|
||||||
.rooms { display: flex; flex-wrap: wrap; gap: 8px; }
|
.rooms { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||||
.room {
|
.room {
|
||||||
padding: 10px 15px;
|
padding: 10px 15px;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
min-width: 80px;
|
min-width: 90px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
transition: background 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
.room.cleaned { background: #4CAF50; color: white; }
|
.room.cleaned { background: #4CAF50; color: white; }
|
||||||
.room.not-cleaned { background: #f44336; color: white; }
|
.room.not-cleaned { background: #f44336; color: white; }
|
||||||
.room.cleaned .time-display, .room.not-cleaned .time-display {
|
.time-display {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
margin-top: 6px;
|
margin-top: 6px;
|
||||||
line-height: 1.4;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
.hidden { display: none !important; }
|
.hidden { display: none !important; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>🧹 Room Cleaning Status</h1>
|
<h1>🧹 Room Cleaning Status</h1>
|
||||||
|
|
||||||
<div class="controls">
|
<div class="controls">
|
||||||
<button class="toggle" onclick="toggleFilter()">
|
<button class="toggle" onclick="toggleFilter()">
|
||||||
<span id="toggle-text">Show Only Cleaned Today</span>
|
<span id="toggle-text">Show Only Cleaned Today</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="buildings-container">
|
||||||
{% for building_name, floors in buildings.items() %}
|
{% for building_name, floors in buildings.items() %}
|
||||||
<div class="building" data-building="{{ building_name }}">
|
<div class="building" data-building="{{ building_name }}">
|
||||||
<h2>{{ building_name }}</h2>
|
<h2>{{ building_name }}</h2>
|
||||||
{% for floor in floors %}
|
{% for floor in floors %}
|
||||||
<div class="floor" data-floor="{{ building_name }}-{{ loop.index }}">
|
<div class="floor">
|
||||||
<div class="floor-label">Etage {{ loop.index }}</div>
|
<div class="floor-label">Etage {{ loop.index }}</div>
|
||||||
<div class="rooms">
|
<div class="rooms">
|
||||||
{% for room in floor %}
|
{% for room in floor %}
|
||||||
@@ -145,9 +138,7 @@ TEMPLATE = '''
|
|||||||
data-room="{{ room }}">
|
data-room="{{ room }}">
|
||||||
{{ room }}
|
{{ room }}
|
||||||
<span class="time-display">
|
<span class="time-display">
|
||||||
{% if status.last_cleaned %}
|
{% if status.last_cleaned %}{{ status.last_cleaned[5:10] }}<br>{{ status.last_cleaned[11:16] }}{% else %}Never{% endif %}
|
||||||
{{ status.last_cleaned[5:10] }}<br>{{ status.last_cleaned[11:16] }}
|
|
||||||
{% else %}Never{% endif %}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -157,88 +148,70 @@ TEMPLATE = '''
|
|||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% if special_rooms %}
|
<div id="sonderraeume-section" class="building {{ 'hidden' if not special_rooms }}" data-building="Sonderräume">
|
||||||
<div class="building" data-building="Sonderräume">
|
|
||||||
<h2>Sonderräume</h2>
|
<h2>Sonderräume</h2>
|
||||||
<div class="floor" data-floor="Sonderräume-1">
|
<div class="floor">
|
||||||
<div class="rooms" id="special-rooms-container">
|
<div class="rooms" id="special-rooms-container">
|
||||||
{% for room in special_rooms %}
|
{% for room in special_rooms %}
|
||||||
{% set status = statuses.get(room, {"is_today": False, "last_cleaned": None}) %}
|
{% set status = statuses.get(room, {"is_today": False, "last_cleaned": None}) %}
|
||||||
<div class="room {{ 'cleaned' if status.is_today else 'not-cleaned' }}"
|
<div class="room {{ 'cleaned' if status.is_today else 'not-cleaned' }}"
|
||||||
data-cleaned="{{ 'yes' if status.is_today else 'no' }}"
|
data-cleaned="{{ 'yes' if status.is_today else 'no' }}"
|
||||||
data-room="{{ room }}"
|
data-room="{{ room }}">
|
||||||
data-special="true">
|
|
||||||
{{ room }}
|
{{ room }}
|
||||||
<span class="time-display">
|
<span class="time-display">{{ status.last_cleaned[5:10] }}<br>{{ status.last_cleaned[11:16] }}</span>
|
||||||
{% if status.last_cleaned %}
|
|
||||||
{{ status.last_cleaned[5:10] }}<br>{{ status.last_cleaned[11:16] }}
|
|
||||||
{% else %}Never{% endif %}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let filterActive = false;
|
let filterActive = false;
|
||||||
|
|
||||||
function formatDateTime(datetimeStr) {
|
function formatDateTime(ts) {
|
||||||
if (!datetimeStr) return 'Never';
|
if (!ts) return 'Never';
|
||||||
const date = datetimeStr.slice(5, 10);
|
return ts.slice(5, 10) + '<br>' + ts.slice(11, 16);
|
||||||
const time = datetimeStr.slice(11, 16);
|
|
||||||
return date + '<br>' + time;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyFilter() {
|
function applyFilter() {
|
||||||
const rooms = document.querySelectorAll('.room');
|
|
||||||
const floors = document.querySelectorAll('.floor');
|
|
||||||
const buildings = document.querySelectorAll('.building');
|
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');
|
||||||
|
|
||||||
if (filterActive) {
|
|
||||||
rooms.forEach(room => {
|
rooms.forEach(room => {
|
||||||
if (room.dataset.cleaned === 'no') {
|
const isCleaned = room.dataset.cleaned === 'yes';
|
||||||
|
if (filterActive && !isCleaned) {
|
||||||
room.classList.add('hidden');
|
room.classList.add('hidden');
|
||||||
} else {
|
} else {
|
||||||
room.classList.remove('hidden');
|
room.classList.remove('hidden');
|
||||||
|
floorHasVisibleRoom = true;
|
||||||
|
buildingHasVisibleRoom = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
floors.forEach(floor => {
|
if (filterActive && !floorHasVisibleRoom) floor.classList.add('hidden');
|
||||||
const visibleRooms = floor.querySelectorAll('.room:not(.hidden)');
|
else floor.classList.remove('hidden');
|
||||||
if (visibleRooms.length === 0) {
|
|
||||||
floor.classList.add('hidden');
|
|
||||||
} else {
|
|
||||||
floor.classList.remove('hidden');
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
buildings.forEach(building => {
|
if (filterActive && !buildingHasVisibleRoom) building.classList.add('hidden');
|
||||||
const visibleFloors = building.querySelectorAll('.floor:not(.hidden)');
|
else building.classList.remove('hidden');
|
||||||
if (visibleFloors.length === 0) {
|
|
||||||
|
// 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');
|
building.classList.add('hidden');
|
||||||
} else {
|
|
||||||
building.classList.remove('hidden');
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
rooms.forEach(room => room.classList.remove('hidden'));
|
|
||||||
floors.forEach(floor => floor.classList.remove('hidden'));
|
|
||||||
buildings.forEach(building => building.classList.remove('hidden'));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleFilter() {
|
function toggleFilter() {
|
||||||
filterActive = !filterActive;
|
filterActive = !filterActive;
|
||||||
const toggleText = document.getElementById('toggle-text');
|
document.getElementById('toggle-text').textContent = filterActive ? 'Show All Rooms' : 'Show Only Cleaned Today';
|
||||||
|
|
||||||
if (filterActive) {
|
|
||||||
toggleText.textContent = 'Show All Rooms';
|
|
||||||
} else {
|
|
||||||
toggleText.textContent = 'Show Only Cleaned Today';
|
|
||||||
}
|
|
||||||
|
|
||||||
applyFilter();
|
applyFilter();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,68 +222,36 @@ TEMPLATE = '''
|
|||||||
const statuses = data.statuses;
|
const statuses = data.statuses;
|
||||||
const specialRooms = data.special_rooms;
|
const specialRooms = data.special_rooms;
|
||||||
|
|
||||||
// Update existing rooms
|
// Update All existing rooms in standard buildings
|
||||||
document.querySelectorAll('.room').forEach(roomDiv => {
|
document.querySelectorAll('.room').forEach(roomDiv => {
|
||||||
const roomNumber = roomDiv.dataset.room;
|
const roomNo = roomDiv.dataset.room;
|
||||||
const status = statuses[roomNumber] || { is_today: false, last_cleaned: null };
|
const s = statuses[roomNo];
|
||||||
|
if (s) {
|
||||||
if (status.is_today) {
|
roomDiv.classList.toggle('cleaned', s.is_today);
|
||||||
roomDiv.classList.remove('not-cleaned');
|
roomDiv.classList.toggle('not-cleaned', !s.is_today);
|
||||||
roomDiv.classList.add('cleaned');
|
roomDiv.dataset.cleaned = s.is_today ? 'yes' : 'no';
|
||||||
roomDiv.dataset.cleaned = 'yes';
|
roomDiv.querySelector('.time-display').innerHTML = formatDateTime(s.last_cleaned);
|
||||||
} else {
|
|
||||||
roomDiv.classList.remove('cleaned');
|
|
||||||
roomDiv.classList.add('not-cleaned');
|
|
||||||
roomDiv.dataset.cleaned = 'no';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeDisplay = roomDiv.querySelector('.time-display');
|
|
||||||
timeDisplay.innerHTML = formatDateTime(status.last_cleaned);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update special rooms section
|
// Rebuild Sonderräume container
|
||||||
const specialContainer = document.getElementById('special-rooms-container');
|
const specialContainer = document.getElementById('special-rooms-container');
|
||||||
if (specialContainer && specialRooms.length > 0) {
|
const sonderSection = document.getElementById('sonderraeume-section');
|
||||||
const existingSpecialRooms = new Set(
|
|
||||||
Array.from(specialContainer.querySelectorAll('.room'))
|
|
||||||
.map(r => r.dataset.room)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Add new special rooms
|
if (specialRooms.length > 0) {
|
||||||
specialRooms.forEach(roomNumber => {
|
sonderSection.classList.remove('hidden');
|
||||||
if (!existingSpecialRooms.has(roomNumber)) {
|
let html = '';
|
||||||
const status = statuses[roomNumber];
|
specialRooms.forEach(room => {
|
||||||
const roomDiv = document.createElement('div');
|
const s = statuses[room];
|
||||||
roomDiv.className = 'room ' + (status.is_today ? 'cleaned' : 'not-cleaned');
|
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>`;
|
||||||
roomDiv.dataset.cleaned = status.is_today ? 'yes' : 'no';
|
|
||||||
roomDiv.dataset.room = roomNumber;
|
|
||||||
roomDiv.dataset.special = 'true';
|
|
||||||
roomDiv.innerHTML = `
|
|
||||||
${roomNumber}
|
|
||||||
<span class="time-display">${formatDateTime(status.last_cleaned)}</span>
|
|
||||||
`;
|
|
||||||
specialContainer.appendChild(roomDiv);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
specialContainer.innerHTML = html;
|
||||||
// Show Sonderräume section if it was hidden
|
} else {
|
||||||
const sonderraumeBuilding = document.querySelector('[data-building="Sonderräume"]');
|
sonderSection.classList.add('hidden');
|
||||||
if (sonderraumeBuilding) {
|
|
||||||
sonderraumeBuilding.classList.remove('hidden');
|
|
||||||
}
|
|
||||||
} else if (specialRooms.length === 0) {
|
|
||||||
// Hide Sonderräume if no special rooms exist
|
|
||||||
const sonderraumeBuilding = document.querySelector('[data-building="Sonderräume"]');
|
|
||||||
if (sonderraumeBuilding) {
|
|
||||||
sonderraumeBuilding.classList.add('hidden');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
applyFilter();
|
applyFilter();
|
||||||
|
} catch (e) { console.error("Update failed", e); }
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to update statuses:', error);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setInterval(updateStatuses, 1000);
|
setInterval(updateStatuses, 1000);
|
||||||
@@ -320,4 +261,4 @@ TEMPLATE = '''
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app.run(host='0.0.0.0', port=5001, debug=True)
|
app.run(host='0.0.0.0', port=5001)
|
||||||
|
|||||||
Reference in New Issue
Block a user