Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
co2ampel2
Web
Commits
0cfce44e
Commit
0cfce44e
authored
Jun 25, 2025
by
Gezer
Browse files
Add Device Dashboard component and integrate with routing; create DeviceDashboardView
parent
667b5b56
Changes
4
Hide whitespace changes
Inline
Side-by-side
frontend/src/components/DeviceDashboard.vue
0 → 100644
View file @
0cfce44e
<
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
frontend/src/components/NavBar.vue
View file @
0cfce44e
...
...
@@ -4,7 +4,7 @@ import { RouterLink, RouterView } from 'vue-router'
<
template
>
<header>
<div
class=
"container"
>
<div
class=
"container
-navbar
"
>
<div
class=
"image"
>
<RouterLink
to=
"/"
>
<img
alt=
"Vue logo"
class=
"logo"
src=
"@/assets/logo.svg"
width=
"100"
height=
"100"
/>
...
...
@@ -17,6 +17,7 @@ import { RouterLink, RouterView } from 'vue-router'
<RouterLink
to=
"/login"
>
Login
</RouterLink>
<RouterLink
to=
"/register"
>
Registrieren
</RouterLink>
<RouterLink
to=
"/buildingsView"
>
Gebäude
</RouterLink>
<RouterLink
to=
"/dashboard"
>
Device Dashboard
</RouterLink>
</nav>
</div>
</header>
...
...
@@ -37,7 +38,7 @@ header {
box-shadow
:
0
2px
4px
rgba
(
0
,
0
,
0
,
0.1
);
}
.container
{
.container
-navbar
{
display
:
flex
;
justify-content
:
space-between
;
align-items
:
center
;
...
...
frontend/src/router/index.ts
View file @
0cfce44e
...
...
@@ -5,6 +5,7 @@ import RegisterView from '../views/RegisterView.vue'
import
RoomNavView
from
'
../views/RoomNavView.vue
'
import
BuildingsView
from
'
../views/BuildingsView.vue
'
import
ChartView
from
'
../views/ChartView.vue
'
import
DeviceDashboard
from
'
../components/DeviceDashboard.vue
'
const
router
=
createRouter
({
history
:
createWebHistory
(
import
.
meta
.
env
.
BASE_URL
),
...
...
@@ -39,12 +40,17 @@ const router = createRouter({
name
:
'
charts
'
,
component
:
ChartView
},
{
path
:
'
/dashboard
'
,
name
:
'
dashboard
'
,
component
:
DeviceDashboard
,
},
{
path
:
'
/rooms/:room
'
,
name
:
'
RoomChart
'
,
component
:
()
=>
import
(
'
../views/ChartView.vue
'
),
props
:
true
},
},
{
path
:
'
/about
'
,
name
:
'
about
'
,
...
...
frontend/src/views/DeviceDashboardView.vue
0 → 100644
View file @
0cfce44e
<
template
>
<Dashboard
/>
</
template
>
<
script
>
import
Dashboard
from
'
../components/DeviceDashboard.vue
'
;
export
default
{
components
:
{
Dashboard
}
}
</
script
>
\ No newline at end of file
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment