Commit 0cfce44e authored by Gezer's avatar Gezer
Browse files

Add Device Dashboard component and integrate with routing; create DeviceDashboardView

parent 667b5b56
<template>
<div class="container">
<div class="header">
<h1>Device Status Dashboard</h1>
<p>Übersicht aller Geräte und deren Verfügbarkeit</p>
</div>
<div class="filter-bar">
<label for="status-filter">Filter:</label>
<select id="status-filter" v-model="selectedFilter" @change="applyFilter">
<option value="all">Alle</option>
<option value="true">Verfügbar</option>
<option value="loading">Lädt...</option>
<option value="false">Nicht verfügbar</option>
</select>
</div>
<div class="table-container" v-if="!loading">
<table class="status-table">
<thead>
<tr>
<th>Raum</th>
<th>MAC-Adresse</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr v-for="device in filteredDevices" :key="device['mac-adress']">
<td class="room-cell">{{ device.room }}</td>
<td><span class="mac-cell">{{ device['mac-adress'] }}</span></td>
<td>
<div class="status-indicator">
<div class="status-light" :class="getStatusClass(device.availabil)"></div>
<span class="status-text">{{ getStatusText(device.availabil) }}</span>
</div>
</td>
</tr>
</tbody>
</table>
</div>
<div v-else class="loading-spinner">
<p>Lade Gerätedaten...</p>
</div>
<div class="stats-bar" v-if="!loading">
<div class="stat-item">
<div class="stat-number stat-available">{{ stats.available }}</div>
<div class="stat-label">Verfügbar</div>
</div>
<div class="stat-item">
<div class="stat-number stat-loading">{{ stats.loading }}</div>
<div class="stat-label">Laden</div>
</div>
<div class="stat-item">
<div class="stat-number stat-unavailable">{{ stats.unavailable }}</div>
<div class="stat-label">Nicht verfügbar</div>
</div>
<div class="stat-item">
<div class="stat-number">{{ devices.length }}</div>
<div class="stat-label">Gesamt</div>
</div>
</div>
<button class="refresh-btn" @click="refreshData">
🔄 Aktualisieren
</button>
</div>
</template>
<script>
export default {
data() {
return {
devices: [],
loading: true,
selectedFilter: 'all',
};
},
computed: {
stats() {
return {
available: this.devices.filter(d => d.availabil === 'true').length,
loading: this.devices.filter(d => d.availabil === 'loading').length,
unavailable: this.devices.filter(d => d.availabil === 'false').length,
};
},
filteredDevices() {
if (this.selectedFilter === 'all') {
return this.devices;
}
return this.devices.filter(d => d.availabil === this.selectedFilter);
},},
methods: {
async loadDevices() {
this.loading = true;
try {
// ✨ Beispielgeräte (verschiedene Räume & Stati)
const data = [
{ "mac-adress": "00:11:22:33:44:01", room: "Büro A", availabil: "true" },
{ "mac-adress": "00:11:22:33:44:02", room: "Büro B", availabil: "false" },
{ "mac-adress": "00:11:22:33:44:03", room: "Lager", availabil: "loading" },
{ "mac-adress": "00:11:22:33:44:04", room: "Küche", availabil: "true" },
{ "mac-adress": "00:11:22:33:44:05", room: "Serverraum", availabil: "false" },
{ "mac-adress": "00:11:22:33:44:06", room: "Eingang", availabil: "loading" },
{ "mac-adress": "00:11:22:33:44:07", room: "Konferenz 1", availabil: "true" },
{ "mac-adress": "00:11:22:33:44:08", room: "Konferenz 2", availabil: "false" }
];
await new Promise(resolve => setTimeout(resolve, 500)); // künstliche Ladezeit
this.devices = data;
} catch (err) {
console.error('Mock-Daten-Fehler:', err);
this.devices = [];
} finally {
this.loading = false;
}
},
async refreshData() {
await this.loadDevices();
},
applyFilter() {
// Auswahl-Änderung triggert automatisch filteredDevices
},
getStatusClass(status) {
switch (status) {
case 'true': return 'available';
case 'loading': return 'loading';
case 'false': return 'unavailable';
default: return 'unavailable';
}
},
getStatusText(status) {
switch (status) {
case 'true': return 'Verfügbar';
case 'loading': return 'Lädt...';
case 'false': return 'Nicht verfügbar';
default: return 'Unbekannt';
}
},
},
async refreshData() {
await this.loadDevices();
},
mounted() {
this.loadDevices();
},
};
</script>
<style>
.container {
display: flex;
flex-direction: column; /* Stellt sicher, dass Kinder untereinander stehen */
background: white;
border-radius: 15px;
box-shadow: 0 20px 40px rgba(0,0,0,0.1);
overflow: hidden;
padding: 30px;
}
.stats-bar {
display: flex;
justify-content: space-around;
align-items: center;
flex-wrap: wrap;
gap: 1rem;
padding: 20px 30px;
background: #f8f9fa;
border-top: 1px solid #ecf0f1;
}
.header {
background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
color: white;
padding: 30px;
text-align: center;
}
.header h1 {
font-size: 2.5rem;
margin-bottom: 10px;
font-weight: 300;
}
.header p {
opacity: 0.8;
font-size: 1.1rem;
}
.table-container {
padding: 30px;
overflow-x: auto;
}
.status-table {
width: 100%;
border-collapse: collapse;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 8px 25px rgba(0,0,0,0.08);
}
.status-table th {
background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);
color: white;
padding: 20px 15px;
text-align: left;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
font-size: 0.9rem;
}
.status-table td {
padding: 18px 15px;
border-bottom: 1px solid #ecf0f1;
transition: background-color 0.3s ease;
}
.status-table tbody tr:hover {
background-color: #f8f9fa;
transform: translateY(-1px);
transition: all 0.3s ease;
}
.status-table tbody tr:last-child td {
border-bottom: none;
}
.room-cell {
font-weight: 600;
color: #2c3e50;
font-size: 1.1rem;
}
.mac-cell {
font-family: 'Courier New', monospace;
background: #f8f9fa;
padding: 8px 12px;
border-radius: 6px;
color: #555;
font-size: 0.9rem;
}
.status-indicator {
display: inline-flex;
align-items: center;
gap: 10px;
font-weight: 600;
}
.status-light {
width: 20px;
height: 20px;
border-radius: 50%;
position: relative;
box-shadow: 0 0 10px rgba(0,0,0,0.2);
}
.status-light.available {
background: linear-gradient(135deg, #2ecc71 0%, #27ae60 100%);
box-shadow: 0 0 15px rgba(46, 204, 113, 0.5);
}
.status-light.loading {
background: linear-gradient(135deg, #f39c12 0%, #e67e22 100%);
box-shadow: 0 0 15px rgba(243, 156, 18, 0.5);
animation: pulse 2s infinite;
}
.status-light.unavailable {
background: linear-gradient(135deg, #e74c3c 0%, #c0392b 100%);
box-shadow: 0 0 15px rgba(231, 76, 60, 0.5);
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
.status-text {
color: #2c3e50;
}
.loading-spinner {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
color: #7f8c8d;
}
.refresh-btn {
position: fixed;
bottom: 30px;
right: 30px;
background: linear-gradient(135deg, #3498db 0%, #2980b9 100%);
color: white;
border: none;
padding: 15px 20px;
border-radius: 50px;
cursor: pointer;
font-size: 1rem;
font-weight: 600;
box-shadow: 0 8px 25px rgba(52, 152, 219, 0.3);
transition: all 0.3s ease;
}
.refresh-btn:hover {
transform: translateY(-3px);
box-shadow: 0 12px 35px rgba(52, 152, 219, 0.4);
}
.stats-bar {
display: flex;
justify-content: space-around;
padding: 20px 30px;
background: #f8f9fa;
border-top: 1px solid #ecf0f1;
}
.stat-item {
text-align: center;
}
.stat-number {
font-size: 2rem;
font-weight: bold;
color: #2c3e50;
}
.stat-label {
color: #7f8c8d;
font-size: 0.9rem;
text-transform: uppercase;
letter-spacing: 1px;
}
.stat-available { color: #27ae60; }
.stat-loading { color: #f39c12; }
.stat-unavailable { color: #e74c3c; }
@media (max-width: 600px) {
.stats-bar {
flex-direction: column;
align-items: stretch;
}
}
</style>
\ No newline at end of file
...@@ -4,7 +4,7 @@ import { RouterLink, RouterView } from 'vue-router' ...@@ -4,7 +4,7 @@ import { RouterLink, RouterView } from 'vue-router'
<template> <template>
<header> <header>
<div class="container"> <div class="container-navbar">
<div class="image"> <div class="image">
<RouterLink to="/"> <RouterLink to="/">
<img alt="Vue logo" class="logo" src="@/assets/logo.svg" width="100" height="100" /> <img alt="Vue logo" class="logo" src="@/assets/logo.svg" width="100" height="100" />
...@@ -17,6 +17,7 @@ import { RouterLink, RouterView } from 'vue-router' ...@@ -17,6 +17,7 @@ import { RouterLink, RouterView } from 'vue-router'
<RouterLink to="/login">Login</RouterLink> <RouterLink to="/login">Login</RouterLink>
<RouterLink to="/register">Registrieren</RouterLink> <RouterLink to="/register">Registrieren</RouterLink>
<RouterLink to="/buildingsView">Gebäude</RouterLink> <RouterLink to="/buildingsView">Gebäude</RouterLink>
<RouterLink to="/dashboard">Device Dashboard</RouterLink>
</nav> </nav>
</div> </div>
</header> </header>
...@@ -37,7 +38,7 @@ header { ...@@ -37,7 +38,7 @@ header {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
} }
.container { .container-navbar {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
......
...@@ -5,6 +5,7 @@ import RegisterView from '../views/RegisterView.vue' ...@@ -5,6 +5,7 @@ import RegisterView from '../views/RegisterView.vue'
import RoomNavView from '../views/RoomNavView.vue' import RoomNavView from '../views/RoomNavView.vue'
import BuildingsView from '../views/BuildingsView.vue' import BuildingsView from '../views/BuildingsView.vue'
import ChartView from '../views/ChartView.vue' import ChartView from '../views/ChartView.vue'
import DeviceDashboard from '../components/DeviceDashboard.vue'
const router = createRouter({ const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), history: createWebHistory(import.meta.env.BASE_URL),
...@@ -39,6 +40,11 @@ const router = createRouter({ ...@@ -39,6 +40,11 @@ const router = createRouter({
name: 'charts', name: 'charts',
component: ChartView component: ChartView
}, },
{
path: '/dashboard',
name: 'dashboard',
component: DeviceDashboard,
},
{ {
path: '/rooms/:room', path: '/rooms/:room',
name: 'RoomChart', name: 'RoomChart',
......
<template>
<Dashboard />
</template>
<script>
import Dashboard from '../components/DeviceDashboard.vue';
export default {
components: { Dashboard }
}
</script>
\ No newline at end of file
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment