format
This commit is contained in:
@@ -30,7 +30,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Grid utilities
|
// Grid utilities
|
||||||
@mixin grid-responsive($columns-mobile: 1, $columns-tablet: 2, $columns-desktop: 3, $gap: $spacing-md) {
|
@mixin grid-responsive(
|
||||||
|
$columns-mobile: 1,
|
||||||
|
$columns-tablet: 2,
|
||||||
|
$columns-desktop: 3,
|
||||||
|
$gap: $spacing-md
|
||||||
|
) {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: $gap;
|
gap: $gap;
|
||||||
grid-template-columns: repeat($columns-mobile, 1fr);
|
grid-template-columns: repeat($columns-mobile, 1fr);
|
||||||
|
|||||||
@@ -66,9 +66,15 @@ $radius-2xl: 1.5rem;
|
|||||||
|
|
||||||
// Shadows
|
// Shadows
|
||||||
$shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
$shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||||
$shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
$shadow-md:
|
||||||
$shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||||
$shadow-xl: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
$shadow-lg:
|
||||||
|
0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||||
|
0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
$shadow-xl:
|
||||||
|
0 20px 25px -5px rgba(0, 0, 0, 0.1),
|
||||||
|
0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||||
|
|
||||||
// Transitions
|
// Transitions
|
||||||
$transition-fast: 150ms ease-in-out;
|
$transition-fast: 150ms ease-in-out;
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
// Typography Styles
|
// Typography Styles
|
||||||
h1, h2, h3, h4, h5, h6 {
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
@@ -40,17 +45,41 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Text utilities
|
// Text utilities
|
||||||
.text-xs { font-size: 0.75rem; }
|
.text-xs {
|
||||||
.text-sm { font-size: 0.875rem; }
|
font-size: 0.75rem;
|
||||||
.text-base { font-size: 1rem; }
|
}
|
||||||
.text-lg { font-size: 1.125rem; }
|
.text-sm {
|
||||||
.text-xl { font-size: 1.25rem; }
|
font-size: 0.875rem;
|
||||||
.text-2xl { font-size: 1.5rem; }
|
}
|
||||||
|
.text-base {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
.text-lg {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
}
|
||||||
|
.text-xl {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
.text-2xl {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.font-medium { font-weight: 500; }
|
.font-medium {
|
||||||
.font-semibold { font-weight: 600; }
|
font-weight: 500;
|
||||||
.font-bold { font-weight: 700; }
|
}
|
||||||
|
.font-semibold {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.font-bold {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
.text-center { text-align: center; }
|
.text-center {
|
||||||
.text-left { text-align: left; }
|
text-align: center;
|
||||||
.text-right { text-align: right; }
|
}
|
||||||
|
.text-left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.text-right {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,14 +2,28 @@
|
|||||||
.grid {
|
.grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
||||||
&--1 { grid-template-columns: 1fr; }
|
&--1 {
|
||||||
&--2 { grid-template-columns: repeat(2, 1fr); }
|
grid-template-columns: 1fr;
|
||||||
&--3 { grid-template-columns: repeat(3, 1fr); }
|
}
|
||||||
&--4 { grid-template-columns: repeat(4, 1fr); }
|
&--2 {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
}
|
||||||
|
&--3 {
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
}
|
||||||
|
&--4 {
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
&--gap-2 { gap: $spacing-sm; }
|
&--gap-2 {
|
||||||
&--gap-4 { gap: $spacing-md; }
|
gap: $spacing-sm;
|
||||||
&--gap-6 { gap: $spacing-lg; }
|
}
|
||||||
|
&--gap-4 {
|
||||||
|
gap: $spacing-md;
|
||||||
|
}
|
||||||
|
&--gap-6 {
|
||||||
|
gap: $spacing-lg;
|
||||||
|
}
|
||||||
|
|
||||||
// Responsive grids
|
// Responsive grids
|
||||||
&--responsive-simple {
|
&--responsive-simple {
|
||||||
@@ -45,8 +59,16 @@
|
|||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--gap-1 { gap: $spacing-xs; }
|
&--gap-1 {
|
||||||
&--gap-2 { gap: $spacing-sm; }
|
gap: $spacing-xs;
|
||||||
&--gap-3 { gap: $spacing-md; }
|
}
|
||||||
&--gap-4 { gap: $spacing-lg; }
|
&--gap-2 {
|
||||||
|
gap: $spacing-sm;
|
||||||
|
}
|
||||||
|
&--gap-3 {
|
||||||
|
gap: $spacing-md;
|
||||||
|
}
|
||||||
|
&--gap-4 {
|
||||||
|
gap: $spacing-lg;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -12,98 +12,226 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Spacing utilities
|
// Spacing utilities
|
||||||
.space-y-2 > * + * { margin-top: $spacing-sm; }
|
.space-y-2 > * + * {
|
||||||
.space-y-3 > * + * { margin-top: $spacing-md; }
|
margin-top: $spacing-sm;
|
||||||
.space-y-4 > * + * { margin-top: $spacing-lg; }
|
}
|
||||||
.space-y-6 > * + * { margin-top: $spacing-xl; }
|
.space-y-3 > * + * {
|
||||||
|
margin-top: $spacing-md;
|
||||||
|
}
|
||||||
|
.space-y-4 > * + * {
|
||||||
|
margin-top: $spacing-lg;
|
||||||
|
}
|
||||||
|
.space-y-6 > * + * {
|
||||||
|
margin-top: $spacing-xl;
|
||||||
|
}
|
||||||
|
|
||||||
.space-x-2 > * + * { margin-left: $spacing-sm; }
|
.space-x-2 > * + * {
|
||||||
.space-x-3 > * + * { margin-left: $spacing-md; }
|
margin-left: $spacing-sm;
|
||||||
.space-x-4 > * + * { margin-left: $spacing-lg; }
|
}
|
||||||
|
.space-x-3 > * + * {
|
||||||
|
margin-left: $spacing-md;
|
||||||
|
}
|
||||||
|
.space-x-4 > * + * {
|
||||||
|
margin-left: $spacing-lg;
|
||||||
|
}
|
||||||
|
|
||||||
// Margin utilities
|
// Margin utilities
|
||||||
.m-0 { margin: 0; }
|
.m-0 {
|
||||||
.mb-1 { margin-bottom: $spacing-xs; }
|
margin: 0;
|
||||||
.mb-2 { margin-bottom: $spacing-sm; }
|
}
|
||||||
.mb-3 { margin-bottom: $spacing-md; }
|
.mb-1 {
|
||||||
.mb-4 { margin-bottom: $spacing-lg; }
|
margin-bottom: $spacing-xs;
|
||||||
|
}
|
||||||
|
.mb-2 {
|
||||||
|
margin-bottom: $spacing-sm;
|
||||||
|
}
|
||||||
|
.mb-3 {
|
||||||
|
margin-bottom: $spacing-md;
|
||||||
|
}
|
||||||
|
.mb-4 {
|
||||||
|
margin-bottom: $spacing-lg;
|
||||||
|
}
|
||||||
|
|
||||||
.mt-1 { margin-top: $spacing-xs; }
|
.mt-1 {
|
||||||
.mt-2 { margin-top: $spacing-sm; }
|
margin-top: $spacing-xs;
|
||||||
.mt-3 { margin-top: $spacing-md; }
|
}
|
||||||
.mt-4 { margin-top: $spacing-lg; }
|
.mt-2 {
|
||||||
|
margin-top: $spacing-sm;
|
||||||
|
}
|
||||||
|
.mt-3 {
|
||||||
|
margin-top: $spacing-md;
|
||||||
|
}
|
||||||
|
.mt-4 {
|
||||||
|
margin-top: $spacing-lg;
|
||||||
|
}
|
||||||
|
|
||||||
// Padding utilities
|
// Padding utilities
|
||||||
.p-0 { padding: 0; }
|
.p-0 {
|
||||||
.p-1 { padding: $spacing-xs; }
|
padding: 0;
|
||||||
.p-2 { padding: $spacing-sm; }
|
}
|
||||||
.p-3 { padding: $spacing-md; }
|
.p-1 {
|
||||||
.p-4 { padding: $spacing-lg; }
|
padding: $spacing-xs;
|
||||||
|
}
|
||||||
|
.p-2 {
|
||||||
|
padding: $spacing-sm;
|
||||||
|
}
|
||||||
|
.p-3 {
|
||||||
|
padding: $spacing-md;
|
||||||
|
}
|
||||||
|
.p-4 {
|
||||||
|
padding: $spacing-lg;
|
||||||
|
}
|
||||||
|
|
||||||
// Width utilities
|
// Width utilities
|
||||||
.w-full { width: 100%; }
|
.w-full {
|
||||||
.w-auto { width: auto; }
|
width: 100%;
|
||||||
|
}
|
||||||
|
.w-auto {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
// Height utilities
|
// Height utilities
|
||||||
.h-full { height: 100%; }
|
.h-full {
|
||||||
.h-auto { height: auto; }
|
height: 100%;
|
||||||
|
}
|
||||||
|
.h-auto {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
// Color utilities
|
// Color utilities
|
||||||
.text-primary { color: $primary; }
|
.text-primary {
|
||||||
.text-secondary { color: $secondary; }
|
color: $primary;
|
||||||
.text-success { color: $success; }
|
}
|
||||||
.text-warning { color: $warning; }
|
.text-secondary {
|
||||||
.text-danger { color: $danger; }
|
color: $secondary;
|
||||||
|
}
|
||||||
|
.text-success {
|
||||||
|
color: $success;
|
||||||
|
}
|
||||||
|
.text-warning {
|
||||||
|
color: $warning;
|
||||||
|
}
|
||||||
|
.text-danger {
|
||||||
|
color: $danger;
|
||||||
|
}
|
||||||
|
|
||||||
.text-gray-400 { color: $gray-400; }
|
.text-gray-400 {
|
||||||
.text-gray-500 { color: $gray-500; }
|
color: $gray-400;
|
||||||
.text-gray-600 { color: $gray-600; }
|
}
|
||||||
.text-gray-700 { color: $gray-700; }
|
.text-gray-500 {
|
||||||
.text-gray-900 { color: $gray-900; }
|
color: $gray-500;
|
||||||
|
}
|
||||||
|
.text-gray-600 {
|
||||||
|
color: $gray-600;
|
||||||
|
}
|
||||||
|
.text-gray-700 {
|
||||||
|
color: $gray-700;
|
||||||
|
}
|
||||||
|
.text-gray-900 {
|
||||||
|
color: $gray-900;
|
||||||
|
}
|
||||||
|
|
||||||
// Background utilities
|
// Background utilities
|
||||||
.bg-primary { background-color: $primary; }
|
.bg-primary {
|
||||||
.bg-white { background-color: white; }
|
background-color: $primary;
|
||||||
.bg-gray-50 { background-color: $gray-50; }
|
}
|
||||||
.bg-gray-100 { background-color: $gray-100; }
|
.bg-white {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
.bg-gray-50 {
|
||||||
|
background-color: $gray-50;
|
||||||
|
}
|
||||||
|
.bg-gray-100 {
|
||||||
|
background-color: $gray-100;
|
||||||
|
}
|
||||||
|
|
||||||
// Border utilities
|
// Border utilities
|
||||||
.border { border: 1px solid $gray-200; }
|
.border {
|
||||||
.border-0 { border: 0; }
|
border: 1px solid $gray-200;
|
||||||
.border-gray-100 { border-color: $gray-100; }
|
}
|
||||||
.border-gray-200 { border-color: $gray-200; }
|
.border-0 {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
.border-gray-100 {
|
||||||
|
border-color: $gray-100;
|
||||||
|
}
|
||||||
|
.border-gray-200 {
|
||||||
|
border-color: $gray-200;
|
||||||
|
}
|
||||||
|
|
||||||
// Border radius utilities
|
// Border radius utilities
|
||||||
.rounded { border-radius: $radius-md; }
|
.rounded {
|
||||||
.rounded-lg { border-radius: $radius-lg; }
|
border-radius: $radius-md;
|
||||||
.rounded-xl { border-radius: $radius-xl; }
|
}
|
||||||
.rounded-2xl { border-radius: $radius-2xl; }
|
.rounded-lg {
|
||||||
.rounded-full { border-radius: 9999px; }
|
border-radius: $radius-lg;
|
||||||
|
}
|
||||||
|
.rounded-xl {
|
||||||
|
border-radius: $radius-xl;
|
||||||
|
}
|
||||||
|
.rounded-2xl {
|
||||||
|
border-radius: $radius-2xl;
|
||||||
|
}
|
||||||
|
.rounded-full {
|
||||||
|
border-radius: 9999px;
|
||||||
|
}
|
||||||
|
|
||||||
// Shadow utilities
|
// Shadow utilities
|
||||||
.shadow-sm { box-shadow: $shadow-sm; }
|
.shadow-sm {
|
||||||
.shadow-md { box-shadow: $shadow-md; }
|
box-shadow: $shadow-sm;
|
||||||
.shadow-lg { box-shadow: $shadow-lg; }
|
}
|
||||||
.shadow-xl { box-shadow: $shadow-xl; }
|
.shadow-md {
|
||||||
|
box-shadow: $shadow-md;
|
||||||
|
}
|
||||||
|
.shadow-lg {
|
||||||
|
box-shadow: $shadow-lg;
|
||||||
|
}
|
||||||
|
.shadow-xl {
|
||||||
|
box-shadow: $shadow-xl;
|
||||||
|
}
|
||||||
|
|
||||||
// Position utilities
|
// Position utilities
|
||||||
.relative { position: relative; }
|
.relative {
|
||||||
.absolute { position: absolute; }
|
position: relative;
|
||||||
.fixed { position: fixed; }
|
}
|
||||||
|
.absolute {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
.fixed {
|
||||||
|
position: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
// Z-index utilities
|
// Z-index utilities
|
||||||
.z-10 { z-index: 10; }
|
.z-10 {
|
||||||
.z-20 { z-index: 20; }
|
z-index: 10;
|
||||||
.z-50 { z-index: 50; }
|
}
|
||||||
|
.z-20 {
|
||||||
|
z-index: 20;
|
||||||
|
}
|
||||||
|
.z-50 {
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
// Overflow utilities
|
// Overflow utilities
|
||||||
.overflow-hidden { overflow: hidden; }
|
.overflow-hidden {
|
||||||
.overflow-y-auto { overflow-y: auto; }
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.overflow-y-auto {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
// Cursor utilities
|
// Cursor utilities
|
||||||
.cursor-pointer { cursor: pointer; }
|
.cursor-pointer {
|
||||||
.cursor-not-allowed { cursor: not-allowed; }
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.cursor-not-allowed {
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
// Opacity utilities
|
// Opacity utilities
|
||||||
.opacity-50 { opacity: 0.5; }
|
.opacity-50 {
|
||||||
.opacity-75 { opacity: 0.75; }
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
.opacity-75 {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,11 +13,28 @@
|
|||||||
Building Average: {{ overallCO2.toFixed(0) }} ppm
|
Building Average: {{ overallCO2.toFixed(0) }} ppm
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-12 h-12 rounded-full flex items-center justify-center" :class="getOverallStatusIconBg()">
|
<div
|
||||||
<svg class="w-6 h-6" :class="getOverallStatusText()" fill="currentColor" viewBox="0 0 24 24">
|
class="w-12 h-12 rounded-full flex items-center justify-center"
|
||||||
<path d="M12,2C6.5,2 2,6.5 2,12C2,17.5 6.5,22 12,22C17.5,22 22,17.5 22,12C22,6.5 17.5,2 12,2M10,16.5L6,12.5L7.5,11L10,13.5L16.5,7L18,8.5L10,16.5Z" v-if="overallStatus === 'good'"/>
|
:class="getOverallStatusIconBg()"
|
||||||
<path d="M12,2C6.5,2 2,6.5 2,12C2,17.5 6.5,22 12,22C17.5,22 22,17.5 22,12C22,6.5 17.5,2 12,2M12,7L17,12L12,17L7,12L12,7Z" v-else-if="overallStatus === 'moderate'"/>
|
>
|
||||||
<path d="M12,2C6.5,2 2,6.5 2,12C2,17.5 6.5,22 12,22C17.5,22 22,17.5 22,12C22,6.5 17.5,2 12,2M12,7L17,12L12,17L7,12L12,7Z" v-else/>
|
<svg
|
||||||
|
class="w-6 h-6"
|
||||||
|
:class="getOverallStatusText()"
|
||||||
|
fill="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M12,2C6.5,2 2,6.5 2,12C2,17.5 6.5,22 12,22C17.5,22 22,17.5 22,12C22,6.5 17.5,2 12,2M10,16.5L6,12.5L7.5,11L10,13.5L16.5,7L18,8.5L10,16.5Z"
|
||||||
|
v-if="overallStatus === 'good'"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M12,2C6.5,2 2,6.5 2,12C2,17.5 6.5,22 12,22C17.5,22 22,17.5 22,12C22,6.5 17.5,2 12,2M12,7L17,12L12,17L7,12L12,7Z"
|
||||||
|
v-else-if="overallStatus === 'moderate'"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M12,2C6.5,2 2,6.5 2,12C2,17.5 6.5,22 12,22C17.5,22 22,17.5 22,12C22,6.5 17.5,2 12,2M12,7L17,12L12,17L7,12L12,7Z"
|
||||||
|
v-else
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -29,7 +46,11 @@
|
|||||||
No air quality data available
|
No air quality data available
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-for="room in roomsList" :key="room.room" class="flex items-center justify-between p-2 rounded">
|
<div
|
||||||
|
v-for="room in roomsList"
|
||||||
|
:key="room.room"
|
||||||
|
class="flex items-center justify-between p-2 rounded"
|
||||||
|
>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div
|
<div
|
||||||
class="w-3 h-3 rounded-full"
|
class="w-3 h-3 rounded-full"
|
||||||
@@ -76,9 +97,9 @@ const roomStore = useRoomStore()
|
|||||||
|
|
||||||
const roomsList = computed(() => {
|
const roomsList = computed(() => {
|
||||||
return Array.from(roomStore.roomsData.values())
|
return Array.from(roomStore.roomsData.values())
|
||||||
.filter(room => room.co2) // Only include rooms with CO2 data
|
.filter((room) => room.co2) // Only include rooms with CO2 data
|
||||||
.sort((a, b) =>
|
.sort(
|
||||||
(b.co2?.current || 0) - (a.co2?.current || 0) // Sort by CO2 level descending
|
(a, b) => (b.co2?.current || 0) - (a.co2?.current || 0), // Sort by CO2 level descending
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -93,23 +114,27 @@ const overallStatus = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const roomsWithGoodAir = computed(() => {
|
const roomsWithGoodAir = computed(() => {
|
||||||
return roomsList.value.filter(room => room.co2?.status === 'good').length
|
return roomsList.value.filter((room) => room.co2?.status === 'good').length
|
||||||
})
|
})
|
||||||
|
|
||||||
const roomsNeedingAttention = computed(() => {
|
const roomsNeedingAttention = computed(() => {
|
||||||
return roomsList.value.filter(room => room.co2?.status && ['poor', 'critical'].includes(room.co2.status)).length
|
return roomsList.value.filter(
|
||||||
|
(room) => room.co2?.status && ['poor', 'critical'].includes(room.co2.status),
|
||||||
|
).length
|
||||||
})
|
})
|
||||||
|
|
||||||
const recommendations = computed(() => {
|
const recommendations = computed(() => {
|
||||||
const recs = []
|
const recs = []
|
||||||
const criticalRooms = roomsList.value.filter(room => room.co2?.status === 'critical')
|
const criticalRooms = roomsList.value.filter((room) => room.co2?.status === 'critical')
|
||||||
const poorRooms = roomsList.value.filter(room => room.co2?.status === 'poor')
|
const poorRooms = roomsList.value.filter((room) => room.co2?.status === 'poor')
|
||||||
|
|
||||||
if (criticalRooms.length > 0) {
|
if (criticalRooms.length > 0) {
|
||||||
recs.push(`Immediate ventilation needed in ${criticalRooms[0].room}`)
|
recs.push(`Immediate ventilation needed in ${criticalRooms[0].room}`)
|
||||||
}
|
}
|
||||||
if (poorRooms.length > 0) {
|
if (poorRooms.length > 0) {
|
||||||
recs.push(`Increase air circulation in ${poorRooms.length} room${poorRooms.length > 1 ? 's' : ''}`)
|
recs.push(
|
||||||
|
`Increase air circulation in ${poorRooms.length} room${poorRooms.length > 1 ? 's' : ''}`,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (overallCO2.value > 800) {
|
if (overallCO2.value > 800) {
|
||||||
recs.push('Consider adjusting HVAC settings building-wide')
|
recs.push('Consider adjusting HVAC settings building-wide')
|
||||||
@@ -120,61 +145,91 @@ const recommendations = computed(() => {
|
|||||||
|
|
||||||
const getOverallStatus = () => {
|
const getOverallStatus = () => {
|
||||||
switch (overallStatus.value) {
|
switch (overallStatus.value) {
|
||||||
case 'good': return 'Excellent Air Quality'
|
case 'good':
|
||||||
case 'moderate': return 'Moderate Air Quality'
|
return 'Excellent Air Quality'
|
||||||
case 'poor': return 'Poor Air Quality'
|
case 'moderate':
|
||||||
case 'critical': return 'Critical - Action Required'
|
return 'Moderate Air Quality'
|
||||||
default: return 'Unknown Status'
|
case 'poor':
|
||||||
|
return 'Poor Air Quality'
|
||||||
|
case 'critical':
|
||||||
|
return 'Critical - Action Required'
|
||||||
|
default:
|
||||||
|
return 'Unknown Status'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getOverallStatusBg = () => {
|
const getOverallStatusBg = () => {
|
||||||
switch (overallStatus.value) {
|
switch (overallStatus.value) {
|
||||||
case 'good': return 'bg-green-50 border border-green-200'
|
case 'good':
|
||||||
case 'moderate': return 'bg-yellow-50 border border-yellow-200'
|
return 'bg-green-50 border border-green-200'
|
||||||
case 'poor': return 'bg-orange-50 border border-orange-200'
|
case 'moderate':
|
||||||
case 'critical': return 'bg-red-50 border border-red-200'
|
return 'bg-yellow-50 border border-yellow-200'
|
||||||
default: return 'bg-gray-50 border border-gray-200'
|
case 'poor':
|
||||||
|
return 'bg-orange-50 border border-orange-200'
|
||||||
|
case 'critical':
|
||||||
|
return 'bg-red-50 border border-red-200'
|
||||||
|
default:
|
||||||
|
return 'bg-gray-50 border border-gray-200'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getOverallStatusText = () => {
|
const getOverallStatusText = () => {
|
||||||
switch (overallStatus.value) {
|
switch (overallStatus.value) {
|
||||||
case 'good': return 'text-green-700'
|
case 'good':
|
||||||
case 'moderate': return 'text-yellow-700'
|
return 'text-green-700'
|
||||||
case 'poor': return 'text-orange-700'
|
case 'moderate':
|
||||||
case 'critical': return 'text-red-700'
|
return 'text-yellow-700'
|
||||||
default: return 'text-gray-700'
|
case 'poor':
|
||||||
|
return 'text-orange-700'
|
||||||
|
case 'critical':
|
||||||
|
return 'text-red-700'
|
||||||
|
default:
|
||||||
|
return 'text-gray-700'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getOverallStatusIconBg = () => {
|
const getOverallStatusIconBg = () => {
|
||||||
switch (overallStatus.value) {
|
switch (overallStatus.value) {
|
||||||
case 'good': return 'bg-green-100'
|
case 'good':
|
||||||
case 'moderate': return 'bg-yellow-100'
|
return 'bg-green-100'
|
||||||
case 'poor': return 'bg-orange-100'
|
case 'moderate':
|
||||||
case 'critical': return 'bg-red-100'
|
return 'bg-yellow-100'
|
||||||
default: return 'bg-gray-100'
|
case 'poor':
|
||||||
|
return 'bg-orange-100'
|
||||||
|
case 'critical':
|
||||||
|
return 'bg-red-100'
|
||||||
|
default:
|
||||||
|
return 'bg-gray-100'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCO2StatusColor = (status: string) => {
|
const getCO2StatusColor = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'good': return 'bg-green-500'
|
case 'good':
|
||||||
case 'moderate': return 'bg-yellow-500'
|
return 'bg-green-500'
|
||||||
case 'poor': return 'bg-orange-500'
|
case 'moderate':
|
||||||
case 'critical': return 'bg-red-500'
|
return 'bg-yellow-500'
|
||||||
default: return 'bg-gray-500'
|
case 'poor':
|
||||||
|
return 'bg-orange-500'
|
||||||
|
case 'critical':
|
||||||
|
return 'bg-red-500'
|
||||||
|
default:
|
||||||
|
return 'bg-gray-500'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCO2TextColor = (status: string) => {
|
const getCO2TextColor = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'good': return 'text-green-600'
|
case 'good':
|
||||||
case 'moderate': return 'text-yellow-600'
|
return 'text-green-600'
|
||||||
case 'poor': return 'text-orange-600'
|
case 'moderate':
|
||||||
case 'critical': return 'text-red-600'
|
return 'text-yellow-600'
|
||||||
default: return 'text-gray-600'
|
case 'poor':
|
||||||
|
return 'text-orange-600'
|
||||||
|
case 'critical':
|
||||||
|
return 'text-red-600'
|
||||||
|
default:
|
||||||
|
return 'text-gray-600'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
class="w-2 h-2 rounded-full transition-all duration-300"
|
class="w-2 h-2 rounded-full transition-all duration-300"
|
||||||
:class="[
|
:class="[
|
||||||
getSensorStatusColor(sensor.status),
|
getSensorStatusColor(sensor.status),
|
||||||
isRecentlyUpdated ? 'animate-pulse shadow-lg shadow-green-400/50' : ''
|
isRecentlyUpdated ? 'animate-pulse shadow-lg shadow-green-400/50' : '',
|
||||||
]"
|
]"
|
||||||
></div>
|
></div>
|
||||||
<span class="text-xs text-gray-500 capitalize">{{ sensor.status }}</span>
|
<span class="text-xs text-gray-500 capitalize">{{ sensor.status }}</span>
|
||||||
@@ -32,7 +32,9 @@
|
|||||||
<label class="block text-sm font-medium text-gray-700 mb-2">Room Assignment</label>
|
<label class="block text-sm font-medium text-gray-700 mb-2">Room Assignment</label>
|
||||||
<select
|
<select
|
||||||
:value="sensor.room"
|
:value="sensor.room"
|
||||||
@change="$emit('updateRoom', sensor.sensor_id, ($event.target as HTMLSelectElement).value)"
|
@change="
|
||||||
|
$emit('updateRoom', sensor.sensor_id, ($event.target as HTMLSelectElement).value)
|
||||||
|
"
|
||||||
class="w-full px-3 py-2 border border-gray-200 rounded-lg bg-white text-sm"
|
class="w-full px-3 py-2 border border-gray-200 rounded-lg bg-white text-sm"
|
||||||
>
|
>
|
||||||
<option value="">Unassigned</option>
|
<option value="">Unassigned</option>
|
||||||
@@ -74,8 +76,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<div class="text-sm font-medium text-gray-700 mb-2">Current Values</div>
|
<div class="text-sm font-medium text-gray-700 mb-2">Current Values</div>
|
||||||
<div class="grid grid-cols-2 gap-2 text-xs">
|
<div class="grid grid-cols-2 gap-2 text-xs">
|
||||||
<div v-for="metric in sensorValues" :key="metric.type"
|
<div v-for="metric in sensorValues" :key="metric.type" class="bg-gray-50 rounded p-2">
|
||||||
class="bg-gray-50 rounded p-2">
|
|
||||||
<div class="text-gray-600 mb-1">{{ metric.label }}</div>
|
<div class="text-gray-600 mb-1">{{ metric.label }}</div>
|
||||||
<div class="font-medium text-gray-900">
|
<div class="font-medium text-gray-900">
|
||||||
{{ metric.value }} <span class="text-gray-500">{{ metric.unit }}</span>
|
{{ metric.value }} <span class="text-gray-500">{{ metric.unit }}</span>
|
||||||
@@ -126,7 +127,7 @@
|
|||||||
v-for="i in 4"
|
v-for="i in 4"
|
||||||
:key="i"
|
:key="i"
|
||||||
class="w-1 h-2 bg-gray-200 rounded-sm"
|
class="w-1 h-2 bg-gray-200 rounded-sm"
|
||||||
:class="{ 'bg-green-500': (sensor.metadata.signalStrength / 25) >= i }"
|
:class="{ 'bg-green-500': sensor.metadata.signalStrength / 25 >= i }"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -155,7 +156,9 @@
|
|||||||
<!-- No Actions State -->
|
<!-- No Actions State -->
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div class="text-sm font-medium text-gray-700 mb-2">Device Actions</div>
|
<div class="text-sm font-medium text-gray-700 mb-2">Device Actions</div>
|
||||||
<div class="text-xs text-gray-500 text-center py-3 bg-gray-50 rounded border-2 border-dashed border-gray-200">
|
<div
|
||||||
|
class="text-xs text-gray-500 text-center py-3 bg-gray-50 rounded border-2 border-dashed border-gray-200"
|
||||||
|
>
|
||||||
This device is monitor-only and has no available actions
|
This device is monitor-only and has no available actions
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -186,7 +189,10 @@ const getSensorValues = (sensor: SensorDevice) => {
|
|||||||
|
|
||||||
// Get real-time sensor reading from store
|
// Get real-time sensor reading from store
|
||||||
const latestReading = sensorStore.latestReadings.get(sensor.sensor_id)
|
const latestReading = sensorStore.latestReadings.get(sensor.sensor_id)
|
||||||
console.log(`[Detailed] Getting values for sensor ${sensor.sensor_id}, found reading:`, latestReading)
|
console.log(
|
||||||
|
`[Detailed] Getting values for sensor ${sensor.sensor_id}, found reading:`,
|
||||||
|
latestReading,
|
||||||
|
)
|
||||||
console.log('[Detailed] Available readings:', Array.from(sensorStore.latestReadings.keys()))
|
console.log('[Detailed] Available readings:', Array.from(sensorStore.latestReadings.keys()))
|
||||||
console.log(`[Detailed] Sensor capabilities:`, sensor.capabilities?.monitoring)
|
console.log(`[Detailed] Sensor capabilities:`, sensor.capabilities?.monitoring)
|
||||||
|
|
||||||
@@ -197,7 +203,7 @@ const getSensorValues = (sensor: SensorDevice) => {
|
|||||||
type: 'energy',
|
type: 'energy',
|
||||||
label: 'Energy Consumption',
|
label: 'Energy Consumption',
|
||||||
value: energyValue,
|
value: energyValue,
|
||||||
unit: latestReading?.energy?.unit || 'kWh'
|
unit: latestReading?.energy?.unit || 'kWh',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,19 +214,19 @@ const getSensorValues = (sensor: SensorDevice) => {
|
|||||||
type: 'co2',
|
type: 'co2',
|
||||||
label: 'CO2 Level',
|
label: 'CO2 Level',
|
||||||
value: co2Value,
|
value: co2Value,
|
||||||
unit: latestReading?.co2?.unit || 'ppm'
|
unit: latestReading?.co2?.unit || 'ppm',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only show temperature if the sensor monitors temperature
|
// Only show temperature if the sensor monitors temperature
|
||||||
if (sensor.capabilities?.monitoring?.includes('temperature')) {
|
if (sensor.capabilities?.monitoring?.includes('temperature')) {
|
||||||
const tempValue = latestReading?.temperature?.value?.toFixed(1) ||
|
const tempValue =
|
||||||
(Math.random() * 8 + 18).toFixed(1)
|
latestReading?.temperature?.value?.toFixed(1) || (Math.random() * 8 + 18).toFixed(1)
|
||||||
values.push({
|
values.push({
|
||||||
type: 'temperature',
|
type: 'temperature',
|
||||||
label: 'Temperature',
|
label: 'Temperature',
|
||||||
value: tempValue,
|
value: tempValue,
|
||||||
unit: latestReading?.temperature?.unit || '°C'
|
unit: latestReading?.temperature?.unit || '°C',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,7 +236,7 @@ const getSensorValues = (sensor: SensorDevice) => {
|
|||||||
type: 'humidity',
|
type: 'humidity',
|
||||||
label: 'Humidity',
|
label: 'Humidity',
|
||||||
value: Math.floor(Math.random() * 40 + 30),
|
value: Math.floor(Math.random() * 40 + 30),
|
||||||
unit: '%'
|
unit: '%',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,7 +246,7 @@ const getSensorValues = (sensor: SensorDevice) => {
|
|||||||
type: 'motion',
|
type: 'motion',
|
||||||
label: 'Motion Status',
|
label: 'Motion Status',
|
||||||
value: Math.random() > 0.7 ? 'Detected' : 'Clear',
|
value: Math.random() > 0.7 ? 'Detected' : 'Clear',
|
||||||
unit: ''
|
unit: '',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,13 +258,13 @@ const getSensorValues = (sensor: SensorDevice) => {
|
|||||||
type: 'brightness',
|
type: 'brightness',
|
||||||
label: 'Brightness Level',
|
label: 'Brightness Level',
|
||||||
value: Math.floor(Math.random() * 100),
|
value: Math.floor(Math.random() * 100),
|
||||||
unit: '%'
|
unit: '%',
|
||||||
})
|
})
|
||||||
values.push({
|
values.push({
|
||||||
type: 'power',
|
type: 'power',
|
||||||
label: 'Power Draw',
|
label: 'Power Draw',
|
||||||
value: Math.floor(Math.random() * 50 + 5),
|
value: Math.floor(Math.random() * 50 + 5),
|
||||||
unit: 'W'
|
unit: 'W',
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 'hvac':
|
case 'hvac':
|
||||||
@@ -266,13 +272,13 @@ const getSensorValues = (sensor: SensorDevice) => {
|
|||||||
type: 'setpoint',
|
type: 'setpoint',
|
||||||
label: 'Target Temperature',
|
label: 'Target Temperature',
|
||||||
value: (Math.random() * 6 + 18).toFixed(1),
|
value: (Math.random() * 6 + 18).toFixed(1),
|
||||||
unit: '°C'
|
unit: '°C',
|
||||||
})
|
})
|
||||||
values.push({
|
values.push({
|
||||||
type: 'mode',
|
type: 'mode',
|
||||||
label: 'Operating Mode',
|
label: 'Operating Mode',
|
||||||
value: ['Heat', 'Cool', 'Auto', 'Fan'][Math.floor(Math.random() * 4)],
|
value: ['Heat', 'Cool', 'Auto', 'Fan'][Math.floor(Math.random() * 4)],
|
||||||
unit: ''
|
unit: '',
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
case 'security':
|
case 'security':
|
||||||
@@ -280,13 +286,13 @@ const getSensorValues = (sensor: SensorDevice) => {
|
|||||||
type: 'status',
|
type: 'status',
|
||||||
label: 'Security Status',
|
label: 'Security Status',
|
||||||
value: Math.random() > 0.8 ? 'Alert' : 'Normal',
|
value: Math.random() > 0.8 ? 'Alert' : 'Normal',
|
||||||
unit: ''
|
unit: '',
|
||||||
})
|
})
|
||||||
values.push({
|
values.push({
|
||||||
type: 'armed',
|
type: 'armed',
|
||||||
label: 'System Armed',
|
label: 'System Armed',
|
||||||
value: Math.random() > 0.5 ? 'Yes' : 'No',
|
value: Math.random() > 0.5 ? 'Yes' : 'No',
|
||||||
unit: ''
|
unit: '',
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
@@ -295,7 +301,7 @@ const getSensorValues = (sensor: SensorDevice) => {
|
|||||||
type: 'status',
|
type: 'status',
|
||||||
label: 'Device Status',
|
label: 'Device Status',
|
||||||
value: sensor.status === 'online' ? 'Active' : 'Inactive',
|
value: sensor.status === 'online' ? 'Active' : 'Inactive',
|
||||||
unit: ''
|
unit: '',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -305,7 +311,7 @@ const getSensorValues = (sensor: SensorDevice) => {
|
|||||||
type: 'uptime',
|
type: 'uptime',
|
||||||
label: 'Uptime',
|
label: 'Uptime',
|
||||||
value: Math.floor(Math.random() * 30 + 1),
|
value: Math.floor(Math.random() * 30 + 1),
|
||||||
unit: 'days'
|
unit: 'days',
|
||||||
})
|
})
|
||||||
|
|
||||||
return values
|
return values
|
||||||
@@ -345,7 +351,7 @@ const getSensorTypeIcon = (type: string) => {
|
|||||||
humidity: '💧',
|
humidity: '💧',
|
||||||
hvac: '❄️',
|
hvac: '❄️',
|
||||||
lighting: '💡',
|
lighting: '💡',
|
||||||
security: '🔒'
|
security: '🔒',
|
||||||
}
|
}
|
||||||
return icons[type as keyof typeof icons] || '📱'
|
return icons[type as keyof typeof icons] || '📱'
|
||||||
}
|
}
|
||||||
@@ -358,17 +364,21 @@ const getSensorTypeStyle = (type: string) => {
|
|||||||
humidity: { bg: 'bg-blue-100', text: 'text-blue-700' },
|
humidity: { bg: 'bg-blue-100', text: 'text-blue-700' },
|
||||||
hvac: { bg: 'bg-cyan-100', text: 'text-cyan-700' },
|
hvac: { bg: 'bg-cyan-100', text: 'text-cyan-700' },
|
||||||
lighting: { bg: 'bg-amber-100', text: 'text-amber-700' },
|
lighting: { bg: 'bg-amber-100', text: 'text-amber-700' },
|
||||||
security: { bg: 'bg-purple-100', text: 'text-purple-700' }
|
security: { bg: 'bg-purple-100', text: 'text-purple-700' },
|
||||||
}
|
}
|
||||||
return styles[type as keyof typeof styles] || { bg: 'bg-gray-100', text: 'text-gray-700' }
|
return styles[type as keyof typeof styles] || { bg: 'bg-gray-100', text: 'text-gray-700' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSensorStatusColor = (status: string) => {
|
const getSensorStatusColor = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'online': return 'bg-green-500'
|
case 'online':
|
||||||
case 'offline': return 'bg-gray-400'
|
return 'bg-green-500'
|
||||||
case 'error': return 'bg-red-500'
|
case 'offline':
|
||||||
default: return 'bg-gray-400'
|
return 'bg-gray-400'
|
||||||
|
case 'error':
|
||||||
|
return 'bg-red-500'
|
||||||
|
default:
|
||||||
|
return 'bg-gray-400'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,14 +13,13 @@
|
|||||||
<h3 class="font-medium text-gray-900">{{ room.room }}</h3>
|
<h3 class="font-medium text-gray-900">{{ room.room }}</h3>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<!-- CO2 Status Indicator -->
|
<!-- CO2 Status Indicator -->
|
||||||
<div
|
<div class="w-3 h-3 rounded-full" :class="getCO2StatusColor(room.co2!.status)"></div>
|
||||||
class="w-3 h-3 rounded-full"
|
|
||||||
:class="getCO2StatusColor(room.co2!.status)"
|
|
||||||
></div>
|
|
||||||
<!-- Occupancy Indicator -->
|
<!-- Occupancy Indicator -->
|
||||||
<div class="flex items-center gap-1 text-xs text-gray-500">
|
<div class="flex items-center gap-1 text-xs text-gray-500">
|
||||||
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 24 24">
|
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 24 24">
|
||||||
<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>
|
<path
|
||||||
|
d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="capitalize">{{ room.occupancyEstimate }}</span>
|
<span class="capitalize">{{ room.occupancyEstimate }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -32,15 +31,21 @@
|
|||||||
<!-- Energy -->
|
<!-- Energy -->
|
||||||
<div class="bg-blue-50 rounded p-2">
|
<div class="bg-blue-50 rounded p-2">
|
||||||
<div class="text-blue-600 font-medium">Energy</div>
|
<div class="text-blue-600 font-medium">Energy</div>
|
||||||
<div class="text-blue-900">{{ room.energy!.current.toFixed(2) }} {{ room.energy!.unit }}</div>
|
<div class="text-blue-900">
|
||||||
|
{{ room.energy!.current.toFixed(2) }} {{ room.energy!.unit }}
|
||||||
|
</div>
|
||||||
<div class="text-blue-600 text-xs">Total: {{ room.energy!.total.toFixed(2) }}</div>
|
<div class="text-blue-600 text-xs">Total: {{ room.energy!.total.toFixed(2) }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- CO2 -->
|
<!-- CO2 -->
|
||||||
<div class="rounded p-2" :class="getCO2BackgroundColor(room.co2!.status)">
|
<div class="rounded p-2" :class="getCO2BackgroundColor(room.co2!.status)">
|
||||||
<div class="font-medium" :class="getCO2TextColor(room.co2!.status)">CO2</div>
|
<div class="font-medium" :class="getCO2TextColor(room.co2!.status)">CO2</div>
|
||||||
<div :class="getCO2TextColor(room.co2!.status)">{{ Math.round(room.co2!.current) }} {{ room.co2!.unit }}</div>
|
<div :class="getCO2TextColor(room.co2!.status)">
|
||||||
<div class="text-xs" :class="getCO2TextColor(room.co2!.status)">{{ room.co2!.status.toUpperCase() }}</div>
|
{{ Math.round(room.co2!.current) }} {{ room.co2!.unit }}
|
||||||
|
</div>
|
||||||
|
<div class="text-xs" :class="getCO2TextColor(room.co2!.status)">
|
||||||
|
{{ room.co2!.status.toUpperCase() }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -53,7 +58,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Summary Stats -->
|
<!-- Summary Stats -->
|
||||||
<div v-if="roomsList.length > 0" class="mt-4 pt-4 border-t border-gray-100 grid grid-cols-3 gap-4 text-center text-xs">
|
<div
|
||||||
|
v-if="roomsList.length > 0"
|
||||||
|
class="mt-4 pt-4 border-t border-gray-100 grid grid-cols-3 gap-4 text-center text-xs"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-medium text-gray-900">{{ roomsList.length }}</div>
|
<div class="font-medium text-gray-900">{{ roomsList.length }}</div>
|
||||||
<div class="text-gray-500">Rooms</div>
|
<div class="text-gray-500">Rooms</div>
|
||||||
@@ -78,7 +86,7 @@ const roomStore = useRoomStore()
|
|||||||
|
|
||||||
const roomsList = computed(() => {
|
const roomsList = computed(() => {
|
||||||
return Array.from(roomStore.roomsData.values())
|
return Array.from(roomStore.roomsData.values())
|
||||||
.filter(room => room.energy && room.co2) // Only show rooms with both metrics
|
.filter((room) => room.energy && room.co2) // Only show rooms with both metrics
|
||||||
.sort((a, b) => a.room.localeCompare(b.room))
|
.sort((a, b) => a.room.localeCompare(b.room))
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -94,31 +102,46 @@ const averageCO2 = computed(() => {
|
|||||||
|
|
||||||
const getCO2StatusColor = (status: string) => {
|
const getCO2StatusColor = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'good': return 'bg-green-500'
|
case 'good':
|
||||||
case 'moderate': return 'bg-yellow-500'
|
return 'bg-green-500'
|
||||||
case 'poor': return 'bg-orange-500'
|
case 'moderate':
|
||||||
case 'critical': return 'bg-red-500'
|
return 'bg-yellow-500'
|
||||||
default: return 'bg-gray-500'
|
case 'poor':
|
||||||
|
return 'bg-orange-500'
|
||||||
|
case 'critical':
|
||||||
|
return 'bg-red-500'
|
||||||
|
default:
|
||||||
|
return 'bg-gray-500'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCO2BackgroundColor = (status: string) => {
|
const getCO2BackgroundColor = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'good': return 'bg-green-50'
|
case 'good':
|
||||||
case 'moderate': return 'bg-yellow-50'
|
return 'bg-green-50'
|
||||||
case 'poor': return 'bg-orange-50'
|
case 'moderate':
|
||||||
case 'critical': return 'bg-red-50'
|
return 'bg-yellow-50'
|
||||||
default: return 'bg-gray-50'
|
case 'poor':
|
||||||
|
return 'bg-orange-50'
|
||||||
|
case 'critical':
|
||||||
|
return 'bg-red-50'
|
||||||
|
default:
|
||||||
|
return 'bg-gray-50'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getCO2TextColor = (status: string) => {
|
const getCO2TextColor = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'good': return 'text-green-700'
|
case 'good':
|
||||||
case 'moderate': return 'text-yellow-700'
|
return 'text-green-700'
|
||||||
case 'poor': return 'text-orange-700'
|
case 'moderate':
|
||||||
case 'critical': return 'text-red-700'
|
return 'text-yellow-700'
|
||||||
default: return 'text-gray-700'
|
case 'poor':
|
||||||
|
return 'text-orange-700'
|
||||||
|
case 'critical':
|
||||||
|
return 'text-red-700'
|
||||||
|
default:
|
||||||
|
return 'text-gray-700'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,9 +9,7 @@
|
|||||||
Token expires in: {{ formatTimeUntilExpiry() }}
|
Token expires in: {{ formatTimeUntilExpiry() }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="authStore.error" class="auth-status__error">
|
<div v-if="authStore.error" class="auth-status__error">Auth Error: {{ authStore.error }}</div>
|
||||||
Auth Error: {{ authStore.error }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
v-if="!authStore.isAuthenticated"
|
v-if="!authStore.isAuthenticated"
|
||||||
@@ -33,13 +31,13 @@ const authStore = useAuthStore()
|
|||||||
const authStatusClass = computed(() => ({
|
const authStatusClass = computed(() => ({
|
||||||
'auth-status--authenticated': authStore.isAuthenticated,
|
'auth-status--authenticated': authStore.isAuthenticated,
|
||||||
'auth-status--error': !authStore.isAuthenticated || authStore.error,
|
'auth-status--error': !authStore.isAuthenticated || authStore.error,
|
||||||
'auth-status--loading': authStore.isLoading
|
'auth-status--loading': authStore.isLoading,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const statusDotClass = computed(() => ({
|
const statusDotClass = computed(() => ({
|
||||||
'auth-status__dot--green': authStore.isAuthenticated && !authStore.error,
|
'auth-status__dot--green': authStore.isAuthenticated && !authStore.error,
|
||||||
'auth-status__dot--red': !authStore.isAuthenticated || authStore.error,
|
'auth-status__dot--red': !authStore.isAuthenticated || authStore.error,
|
||||||
'auth-status__dot--yellow': authStore.isLoading
|
'auth-status__dot--yellow': authStore.isLoading,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const statusText = computed(() => {
|
const statusText = computed(() => {
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
<div class="absolute inset-0 bg-black/50" @click="$emit('close')"></div>
|
<div class="absolute inset-0 bg-black/50" @click="$emit('close')"></div>
|
||||||
|
|
||||||
<!-- Modal -->
|
<!-- Modal -->
|
||||||
<div class="relative bg-white rounded-2xl shadow-xl max-w-md w-full mx-4 max-h-[90vh] overflow-y-auto">
|
<div
|
||||||
|
class="relative bg-white rounded-2xl shadow-xl max-w-md w-full mx-4 max-h-[90vh] overflow-y-auto"
|
||||||
|
>
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="p-6 border-b border-gray-100">
|
<div class="p-6 border-b border-gray-100">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
|
|||||||
@@ -10,7 +10,12 @@
|
|||||||
class="text-gray-400 hover:text-gray-600 transition-colors"
|
class="text-gray-400 hover:text-gray-600 transition-colors"
|
||||||
>
|
>
|
||||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M6 18L18 6M6 6l12 12"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -53,16 +58,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-3">
|
<div class="grid grid-cols-1 gap-3">
|
||||||
<div
|
<div v-for="room in roomsWithStats" :key="room.name" class="bg-gray-50 rounded-lg p-4">
|
||||||
v-for="room in roomsWithStats"
|
|
||||||
:key="room.name"
|
|
||||||
class="bg-gray-50 rounded-lg p-4"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="flex items-center gap-3 mb-2">
|
<div class="flex items-center gap-3 mb-2">
|
||||||
<h4 class="font-medium text-gray-900">{{ room.name }}</h4>
|
<h4 class="font-medium text-gray-900">{{ room.name }}</h4>
|
||||||
<span class="text-xs px-2 py-1 bg-blue-100 text-blue-700 rounded-full font-medium">
|
<span
|
||||||
|
class="text-xs px-2 py-1 bg-blue-100 text-blue-700 rounded-full font-medium"
|
||||||
|
>
|
||||||
{{ room.sensorCount }} sensors
|
{{ room.sensorCount }} sensors
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -78,14 +81,21 @@
|
|||||||
>
|
>
|
||||||
{{ type }}
|
{{ type }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="room.sensorTypes.length === 0" class="text-xs text-gray-500">None</span>
|
<span v-if="room.sensorTypes.length === 0" class="text-xs text-gray-500"
|
||||||
|
>None</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<span class="text-gray-600">Energy:</span>
|
<span class="text-gray-600">Energy:</span>
|
||||||
<div class="font-medium" :class="room.hasMetrics ? 'text-gray-900' : 'text-gray-400'">
|
<div
|
||||||
{{ room.hasMetrics ? room.energyConsumption.toFixed(2) + ' kWh' : 'No data' }}
|
class="font-medium"
|
||||||
|
:class="room.hasMetrics ? 'text-gray-900' : 'text-gray-400'"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
room.hasMetrics ? room.energyConsumption.toFixed(2) + ' kWh' : 'No data'
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -143,14 +153,19 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Delete Confirmation Modal -->
|
<!-- Delete Confirmation Modal -->
|
||||||
<div v-if="roomToDelete" class="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-60">
|
<div
|
||||||
|
v-if="roomToDelete"
|
||||||
|
class="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-60"
|
||||||
|
>
|
||||||
<div class="bg-white rounded-xl max-w-md w-full p-6">
|
<div class="bg-white rounded-xl max-w-md w-full p-6">
|
||||||
<h3 class="text-lg font-semibold text-gray-900 mb-2">Delete Room</h3>
|
<h3 class="text-lg font-semibold text-gray-900 mb-2">Delete Room</h3>
|
||||||
<p class="text-gray-600 mb-4">
|
<p class="text-gray-600 mb-4">
|
||||||
Are you sure you want to delete <strong>"{{ roomToDelete }}"</strong>?
|
Are you sure you want to delete <strong>"{{ roomToDelete }}"</strong>?
|
||||||
{{ getRoomStats(roomToDelete).sensorCount > 0
|
{{
|
||||||
? `This will unassign ${getRoomStats(roomToDelete).sensorCount} sensor(s).`
|
getRoomStats(roomToDelete).sensorCount > 0
|
||||||
: 'This action cannot be undone.' }}
|
? `This will unassign ${getRoomStats(roomToDelete).sensorCount} sensor(s).`
|
||||||
|
: 'This action cannot be undone.'
|
||||||
|
}}
|
||||||
</p>
|
</p>
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
type HealthCheck,
|
type HealthCheck,
|
||||||
type SystemStatus,
|
type SystemStatus,
|
||||||
type DataQuery,
|
type DataQuery,
|
||||||
type DataResponse
|
type DataResponse,
|
||||||
} from '@/services'
|
} from '@/services'
|
||||||
|
|
||||||
interface ApiState {
|
interface ApiState {
|
||||||
@@ -33,13 +33,13 @@ export function useApi() {
|
|||||||
// Global API state
|
// Global API state
|
||||||
const globalState = reactive<ApiState>({
|
const globalState = reactive<ApiState>({
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null
|
error: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
// Helper to handle API calls with state management
|
// Helper to handle API calls with state management
|
||||||
async function handleApiCall<T>(
|
async function handleApiCall<T>(
|
||||||
apiCall: () => Promise<T>,
|
apiCall: () => Promise<T>,
|
||||||
localState?: { loading: boolean; error: string | null }
|
localState?: { loading: boolean; error: string | null },
|
||||||
): Promise<T | null> {
|
): Promise<T | null> {
|
||||||
const state = localState || globalState
|
const state = localState || globalState
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ export function useApi() {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
globalState,
|
globalState,
|
||||||
handleApiCall
|
handleApiCall,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ export function useApi() {
|
|||||||
export function useSensorsApi() {
|
export function useSensorsApi() {
|
||||||
const state = reactive<ApiState>({
|
const state = reactive<ApiState>({
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null
|
error: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
const sensors = ref<SensorDevice[]>([])
|
const sensors = ref<SensorDevice[]>([])
|
||||||
@@ -83,10 +83,7 @@ export function useSensorsApi() {
|
|||||||
sensor_type?: SensorType
|
sensor_type?: SensorType
|
||||||
status?: SensorStatus
|
status?: SensorStatus
|
||||||
}) => {
|
}) => {
|
||||||
const result = await handleApiCall(
|
const result = await handleApiCall(() => sensorsApi.getSensors(params), state)
|
||||||
() => sensorsApi.getSensors(params),
|
|
||||||
state
|
|
||||||
)
|
|
||||||
if (result && result.sensors) {
|
if (result && result.sensors) {
|
||||||
sensors.value = result.sensors
|
sensors.value = result.sensors
|
||||||
}
|
}
|
||||||
@@ -94,10 +91,7 @@ export function useSensorsApi() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fetchSensor = async (sensorId: string) => {
|
const fetchSensor = async (sensorId: string) => {
|
||||||
const result = await handleApiCall(
|
const result = await handleApiCall(() => sensorsApi.getSensor(sensorId), state)
|
||||||
() => sensorsApi.getSensor(sensorId),
|
|
||||||
state
|
|
||||||
)
|
|
||||||
if (result) {
|
if (result) {
|
||||||
currentSensor.value = result
|
currentSensor.value = result
|
||||||
}
|
}
|
||||||
@@ -111,12 +105,9 @@ export function useSensorsApi() {
|
|||||||
end_time?: number
|
end_time?: number
|
||||||
limit?: number
|
limit?: number
|
||||||
offset?: number
|
offset?: number
|
||||||
}
|
},
|
||||||
) => {
|
) => {
|
||||||
const result = await handleApiCall(
|
const result = await handleApiCall(() => sensorsApi.getSensorData(sensorId, params), state)
|
||||||
() => sensorsApi.getSensorData(sensorId, params),
|
|
||||||
state
|
|
||||||
)
|
|
||||||
if (result) {
|
if (result) {
|
||||||
sensorData.value = result
|
sensorData.value = result
|
||||||
}
|
}
|
||||||
@@ -124,27 +115,15 @@ export function useSensorsApi() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const queryData = async (query: DataQuery) => {
|
const queryData = async (query: DataQuery) => {
|
||||||
return handleApiCall(
|
return handleApiCall(() => sensorsApi.queryData(query), state)
|
||||||
() => sensorsApi.queryData(query),
|
|
||||||
state
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateSensorMetadata = async (
|
const updateSensorMetadata = async (sensorId: string, metadata: SensorMetadata) => {
|
||||||
sensorId: string,
|
return handleApiCall(() => sensorsApi.updateSensorMetadata(sensorId, metadata), state)
|
||||||
metadata: SensorMetadata
|
|
||||||
) => {
|
|
||||||
return handleApiCall(
|
|
||||||
() => sensorsApi.updateSensorMetadata(sensorId, metadata),
|
|
||||||
state
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteSensor = async (sensorId: string) => {
|
const deleteSensor = async (sensorId: string) => {
|
||||||
return handleApiCall(
|
return handleApiCall(() => sensorsApi.deleteSensor(sensorId), state)
|
||||||
() => sensorsApi.deleteSensor(sensorId),
|
|
||||||
state
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const exportData = async (params: {
|
const exportData = async (params: {
|
||||||
@@ -153,10 +132,7 @@ export function useSensorsApi() {
|
|||||||
sensor_ids?: string
|
sensor_ids?: string
|
||||||
format?: 'json' | 'csv'
|
format?: 'json' | 'csv'
|
||||||
}) => {
|
}) => {
|
||||||
return handleApiCall(
|
return handleApiCall(() => sensorsApi.exportData(params), state)
|
||||||
() => sensorsApi.exportData(params),
|
|
||||||
state
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -170,7 +146,7 @@ export function useSensorsApi() {
|
|||||||
queryData,
|
queryData,
|
||||||
updateSensorMetadata,
|
updateSensorMetadata,
|
||||||
deleteSensor,
|
deleteSensor,
|
||||||
exportData
|
exportData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,7 +154,7 @@ export function useSensorsApi() {
|
|||||||
export function useRoomsApi() {
|
export function useRoomsApi() {
|
||||||
const state = reactive<ApiState>({
|
const state = reactive<ApiState>({
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null
|
error: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
const rooms = ref<RoomInfo[]>([])
|
const rooms = ref<RoomInfo[]>([])
|
||||||
@@ -187,10 +163,7 @@ export function useRoomsApi() {
|
|||||||
const { handleApiCall } = useApi()
|
const { handleApiCall } = useApi()
|
||||||
|
|
||||||
const fetchRooms = async () => {
|
const fetchRooms = async () => {
|
||||||
const result = await handleApiCall(
|
const result = await handleApiCall(() => roomsApi.getRooms(), state)
|
||||||
() => roomsApi.getRooms(),
|
|
||||||
state
|
|
||||||
)
|
|
||||||
if (result) {
|
if (result) {
|
||||||
rooms.value = result
|
rooms.value = result
|
||||||
}
|
}
|
||||||
@@ -203,12 +176,9 @@ export function useRoomsApi() {
|
|||||||
start_time?: number
|
start_time?: number
|
||||||
end_time?: number
|
end_time?: number
|
||||||
limit?: number
|
limit?: number
|
||||||
}
|
},
|
||||||
) => {
|
) => {
|
||||||
const result = await handleApiCall(
|
const result = await handleApiCall(() => roomsApi.getRoomData(roomName, params), state)
|
||||||
() => roomsApi.getRoomData(roomName, params),
|
|
||||||
state
|
|
||||||
)
|
|
||||||
if (result) {
|
if (result) {
|
||||||
currentRoomData.value = result
|
currentRoomData.value = result
|
||||||
}
|
}
|
||||||
@@ -220,7 +190,7 @@ export function useRoomsApi() {
|
|||||||
rooms,
|
rooms,
|
||||||
currentRoomData,
|
currentRoomData,
|
||||||
fetchRooms,
|
fetchRooms,
|
||||||
fetchRoomData
|
fetchRoomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,7 +198,7 @@ export function useRoomsApi() {
|
|||||||
export function useAnalyticsApi() {
|
export function useAnalyticsApi() {
|
||||||
const state = reactive<ApiState>({
|
const state = reactive<ApiState>({
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null
|
error: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
const summary = ref<AnalyticsSummary | null>(null)
|
const summary = ref<AnalyticsSummary | null>(null)
|
||||||
@@ -239,10 +209,7 @@ export function useAnalyticsApi() {
|
|||||||
const { handleApiCall } = useApi()
|
const { handleApiCall } = useApi()
|
||||||
|
|
||||||
const fetchAnalyticsSummary = async (hours: number = 24) => {
|
const fetchAnalyticsSummary = async (hours: number = 24) => {
|
||||||
const result = await handleApiCall(
|
const result = await handleApiCall(() => analyticsApi.getAnalyticsSummary(hours), state)
|
||||||
() => analyticsApi.getAnalyticsSummary(hours),
|
|
||||||
state
|
|
||||||
)
|
|
||||||
if (result) {
|
if (result) {
|
||||||
summary.value = result
|
summary.value = result
|
||||||
}
|
}
|
||||||
@@ -250,10 +217,7 @@ export function useAnalyticsApi() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fetchEnergyTrends = async (hours: number = 168) => {
|
const fetchEnergyTrends = async (hours: number = 168) => {
|
||||||
const result = await handleApiCall(
|
const result = await handleApiCall(() => analyticsApi.getEnergyTrends(hours), state)
|
||||||
() => analyticsApi.getEnergyTrends(hours),
|
|
||||||
state
|
|
||||||
)
|
|
||||||
if (result) {
|
if (result) {
|
||||||
trends.value = result
|
trends.value = result
|
||||||
}
|
}
|
||||||
@@ -261,10 +225,7 @@ export function useAnalyticsApi() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fetchRoomComparison = async (hours: number = 24) => {
|
const fetchRoomComparison = async (hours: number = 24) => {
|
||||||
const result = await handleApiCall(
|
const result = await handleApiCall(() => analyticsApi.getRoomComparison(hours), state)
|
||||||
() => analyticsApi.getRoomComparison(hours),
|
|
||||||
state
|
|
||||||
)
|
|
||||||
if (result) {
|
if (result) {
|
||||||
roomComparison.value = result
|
roomComparison.value = result
|
||||||
}
|
}
|
||||||
@@ -277,10 +238,7 @@ export function useAnalyticsApi() {
|
|||||||
hours?: number
|
hours?: number
|
||||||
limit?: number
|
limit?: number
|
||||||
}) => {
|
}) => {
|
||||||
const result = await handleApiCall(
|
const result = await handleApiCall(() => analyticsApi.getEvents(params), state)
|
||||||
() => analyticsApi.getEvents(params),
|
|
||||||
state
|
|
||||||
)
|
|
||||||
if (result) {
|
if (result) {
|
||||||
events.value = result.events
|
events.value = result.events
|
||||||
}
|
}
|
||||||
@@ -296,7 +254,7 @@ export function useAnalyticsApi() {
|
|||||||
fetchAnalyticsSummary,
|
fetchAnalyticsSummary,
|
||||||
fetchEnergyTrends,
|
fetchEnergyTrends,
|
||||||
fetchRoomComparison,
|
fetchRoomComparison,
|
||||||
fetchEvents
|
fetchEvents,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,7 +262,7 @@ export function useAnalyticsApi() {
|
|||||||
export function useHealthApi() {
|
export function useHealthApi() {
|
||||||
const state = reactive<ApiState>({
|
const state = reactive<ApiState>({
|
||||||
loading: false,
|
loading: false,
|
||||||
error: null
|
error: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
const health = ref<HealthCheck | null>(null)
|
const health = ref<HealthCheck | null>(null)
|
||||||
@@ -313,10 +271,7 @@ export function useHealthApi() {
|
|||||||
const { handleApiCall } = useApi()
|
const { handleApiCall } = useApi()
|
||||||
|
|
||||||
const fetchHealth = async () => {
|
const fetchHealth = async () => {
|
||||||
const result = await handleApiCall(
|
const result = await handleApiCall(() => healthApi.getHealth(), state)
|
||||||
() => healthApi.getHealth(),
|
|
||||||
state
|
|
||||||
)
|
|
||||||
if (result) {
|
if (result) {
|
||||||
health.value = result
|
health.value = result
|
||||||
}
|
}
|
||||||
@@ -324,10 +279,7 @@ export function useHealthApi() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fetchStatus = async () => {
|
const fetchStatus = async () => {
|
||||||
const result = await handleApiCall(
|
const result = await handleApiCall(() => healthApi.getStatus(), state)
|
||||||
() => healthApi.getStatus(),
|
|
||||||
state
|
|
||||||
)
|
|
||||||
if (result) {
|
if (result) {
|
||||||
status.value = result
|
status.value = result
|
||||||
}
|
}
|
||||||
@@ -339,6 +291,6 @@ export function useHealthApi() {
|
|||||||
health,
|
health,
|
||||||
status,
|
status,
|
||||||
fetchHealth,
|
fetchHealth,
|
||||||
fetchStatus
|
fetchStatus,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -346,7 +346,10 @@ class ApiClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async get<T>(endpoint: string, params?: Record<string, string | number | boolean | string[]>): Promise<T> {
|
async get<T>(
|
||||||
|
endpoint: string,
|
||||||
|
params?: Record<string, string | number | boolean | string[]>,
|
||||||
|
): Promise<T> {
|
||||||
const url = new URL(`${this.baseUrl}${endpoint}`)
|
const url = new URL(`${this.baseUrl}${endpoint}`)
|
||||||
|
|
||||||
if (params) {
|
if (params) {
|
||||||
|
|||||||
@@ -25,7 +25,10 @@ export const authApi = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
async saveToken(token: string): Promise<{ token: string; datetime: string; active: boolean }> {
|
async saveToken(token: string): Promise<{ token: string; datetime: string; active: boolean }> {
|
||||||
return apiClient.post<{ token: string; datetime: string; active: boolean }>('/api/v1/tokens/save', { token })
|
return apiClient.post<{ token: string; datetime: string; active: boolean }>(
|
||||||
|
'/api/v1/tokens/save',
|
||||||
|
{ token },
|
||||||
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
async validateToken(token: string): Promise<TokenValidation> {
|
async validateToken(token: string): Promise<TokenValidation> {
|
||||||
|
|||||||
@@ -26,5 +26,5 @@ export type {
|
|||||||
RoomComparison,
|
RoomComparison,
|
||||||
SystemEvent,
|
SystemEvent,
|
||||||
HealthCheck,
|
HealthCheck,
|
||||||
SystemStatus
|
SystemStatus,
|
||||||
} from './api'
|
} from './api'
|
||||||
@@ -131,11 +131,7 @@ export const useAnalyticsStore = defineStore('analytics', () => {
|
|||||||
|
|
||||||
// Initialize data from APIs
|
// Initialize data from APIs
|
||||||
async function initializeAnalyticsFromApi() {
|
async function initializeAnalyticsFromApi() {
|
||||||
await Promise.allSettled([
|
await Promise.allSettled([fetchAnalyticsSummary(), fetchSystemStatus(), fetchHealthStatus()])
|
||||||
fetchAnalyticsSummary(),
|
|
||||||
fetchSystemStatus(),
|
|
||||||
fetchHealthStatus(),
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ export const useAuthStore = defineStore('auth', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Initialize on store creation (async)
|
// Initialize on store creation (async)
|
||||||
loadTokenFromStorage().catch(error => {
|
loadTokenFromStorage().catch((error) => {
|
||||||
console.warn('Failed to load token from storage:', error)
|
console.warn('Failed to load token from storage:', error)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -316,7 +316,9 @@ export const useRoomStore = defineStore('room', () => {
|
|||||||
apiRooms.value = roomsArray
|
apiRooms.value = roomsArray
|
||||||
|
|
||||||
// Update available rooms from API data
|
// Update available rooms from API data
|
||||||
const roomNames = roomsArray.map((room: any) => room.name || room.room).filter((name: string) => name)
|
const roomNames = roomsArray
|
||||||
|
.map((room: any) => room.name || room.room)
|
||||||
|
.filter((name: string) => name)
|
||||||
if (roomNames.length > 0) {
|
if (roomNames.length > 0) {
|
||||||
availableRooms.value = [...new Set([...availableRooms.value, ...roomNames])].sort()
|
availableRooms.value = [...new Set([...availableRooms.value, ...roomNames])].sort()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ export const useSensorStore = defineStore('sensor', () => {
|
|||||||
// Aggregated CO2 metrics
|
// Aggregated CO2 metrics
|
||||||
const averageCO2Level = computed<number>(() => {
|
const averageCO2Level = computed<number>(() => {
|
||||||
const readings = Array.from(latestReadings.values())
|
const readings = Array.from(latestReadings.values())
|
||||||
const co2Readings = readings.filter(r => r.co2?.value !== undefined)
|
const co2Readings = readings.filter((r) => r.co2?.value !== undefined)
|
||||||
|
|
||||||
if (co2Readings.length === 0) return 0
|
if (co2Readings.length === 0) return 0
|
||||||
|
|
||||||
@@ -49,8 +49,8 @@ export const useSensorStore = defineStore('sensor', () => {
|
|||||||
const maxCO2Level = computed<number>(() => {
|
const maxCO2Level = computed<number>(() => {
|
||||||
const readings = Array.from(latestReadings.values())
|
const readings = Array.from(latestReadings.values())
|
||||||
const co2Values = readings
|
const co2Values = readings
|
||||||
.filter(r => r.co2?.value !== undefined)
|
.filter((r) => r.co2?.value !== undefined)
|
||||||
.map(r => r.co2?.value || 0)
|
.map((r) => r.co2?.value || 0)
|
||||||
|
|
||||||
return co2Values.length > 0 ? Math.max(...co2Values) : 0
|
return co2Values.length > 0 ? Math.max(...co2Values) : 0
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -117,7 +117,10 @@ export const useWebSocketStore = defineStore('websocket', () => {
|
|||||||
|
|
||||||
function processIncomingData(data: WebSocketReading): void {
|
function processIncomingData(data: WebSocketReading): void {
|
||||||
// Skip non-data messages
|
// Skip non-data messages
|
||||||
if ('type' in data && (data as any).type === 'connection_established' || (data as any).type === 'proxy_info') {
|
if (
|
||||||
|
('type' in data && (data as any).type === 'connection_established') ||
|
||||||
|
(data as any).type === 'proxy_info'
|
||||||
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -220,11 +220,7 @@
|
|||||||
No rooms found from API
|
No rooms found from API
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="space-y-3">
|
<div v-else class="space-y-3">
|
||||||
<div
|
<div v-for="room in apiRooms" :key="room.room" class="p-3 bg-gray-50 rounded-lg">
|
||||||
v-for="room in apiRooms"
|
|
||||||
:key="room.room"
|
|
||||||
class="p-3 bg-gray-50 rounded-lg"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between mb-2">
|
<div class="flex items-center justify-between mb-2">
|
||||||
<p class="font-medium text-gray-900">{{ room.room }}</p>
|
<p class="font-medium text-gray-900">{{ room.room }}</p>
|
||||||
<span class="text-sm text-gray-500">{{ room.sensor_count }} sensors</span>
|
<span class="text-sm text-gray-500">{{ room.sensor_count }} sensors</span>
|
||||||
|
|||||||
@@ -257,7 +257,11 @@ interface ActionParameters {
|
|||||||
[key: string]: unknown
|
[key: string]: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleActionExecute = async (sensorId: string, actionId: string, parameters: ActionParameters) => {
|
const handleActionExecute = async (
|
||||||
|
sensorId: string,
|
||||||
|
actionId: string,
|
||||||
|
parameters: ActionParameters,
|
||||||
|
) => {
|
||||||
isExecutingAction.value = true
|
isExecutingAction.value = true
|
||||||
try {
|
try {
|
||||||
await sensorStore.executeSensorAction(sensorId, actionId)
|
await sensorStore.executeSensorAction(sensorId, actionId)
|
||||||
|
|||||||
@@ -28,9 +28,11 @@
|
|||||||
<button
|
<button
|
||||||
@click="activeSection = section.id"
|
@click="activeSection = section.id"
|
||||||
class="w-full flex items-center gap-3 px-3 py-2 rounded-lg text-left transition-colors"
|
class="w-full flex items-center gap-3 px-3 py-2 rounded-lg text-left transition-colors"
|
||||||
:class="activeSection === section.id
|
:class="
|
||||||
? 'bg-blue-100 text-blue-700'
|
activeSection === section.id
|
||||||
: 'text-gray-700 hover:bg-gray-100'"
|
? 'bg-blue-100 text-blue-700'
|
||||||
|
: 'text-gray-700 hover:bg-gray-100'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<span class="text-lg">{{ section.icon }}</span>
|
<span class="text-lg">{{ section.icon }}</span>
|
||||||
<div>
|
<div>
|
||||||
@@ -46,7 +48,10 @@
|
|||||||
<!-- Settings Content -->
|
<!-- Settings Content -->
|
||||||
<div class="lg:col-span-2 space-y-6">
|
<div class="lg:col-span-2 space-y-6">
|
||||||
<!-- Appearance Settings -->
|
<!-- Appearance Settings -->
|
||||||
<div v-if="activeSection === 'appearance'" class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
<div
|
||||||
|
v-if="activeSection === 'appearance'"
|
||||||
|
class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden"
|
||||||
|
>
|
||||||
<div class="p-6 border-b border-gray-100">
|
<div class="p-6 border-b border-gray-100">
|
||||||
<h3 class="text-lg font-semibold text-gray-900">Appearance</h3>
|
<h3 class="text-lg font-semibold text-gray-900">Appearance</h3>
|
||||||
<p class="text-gray-600 text-sm mt-1">Customize the look and feel of your dashboard</p>
|
<p class="text-gray-600 text-sm mt-1">Customize the look and feel of your dashboard</p>
|
||||||
@@ -61,9 +66,11 @@
|
|||||||
:key="theme.value"
|
:key="theme.value"
|
||||||
@click="settingsStore.updateSetting('theme', theme.value)"
|
@click="settingsStore.updateSetting('theme', theme.value)"
|
||||||
class="relative p-4 border-2 rounded-lg cursor-pointer transition-all hover:border-blue-300"
|
class="relative p-4 border-2 rounded-lg cursor-pointer transition-all hover:border-blue-300"
|
||||||
:class="settingsStore.settings.theme === theme.value
|
:class="
|
||||||
? 'border-blue-500 bg-blue-50'
|
settingsStore.settings.theme === theme.value
|
||||||
: 'border-gray-200'"
|
? 'border-blue-500 bg-blue-50'
|
||||||
|
: 'border-gray-200'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
@@ -72,7 +79,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div v-if="settingsStore.settings.theme === theme.value" class="text-blue-600">
|
<div v-if="settingsStore.settings.theme === theme.value" class="text-blue-600">
|
||||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -89,9 +100,11 @@
|
|||||||
:key="mode.value"
|
:key="mode.value"
|
||||||
@click="settingsStore.updateSetting('ui.navigationMode', mode.value)"
|
@click="settingsStore.updateSetting('ui.navigationMode', mode.value)"
|
||||||
class="relative p-4 border-2 rounded-lg cursor-pointer transition-all hover:border-blue-300"
|
class="relative p-4 border-2 rounded-lg cursor-pointer transition-all hover:border-blue-300"
|
||||||
:class="settingsStore.settings.ui.navigationMode === mode.value
|
:class="
|
||||||
? 'border-blue-500 bg-blue-50'
|
settingsStore.settings.ui.navigationMode === mode.value
|
||||||
: 'border-gray-200'"
|
? 'border-blue-500 bg-blue-50'
|
||||||
|
: 'border-gray-200'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<div class="flex items-start justify-between">
|
<div class="flex items-start justify-between">
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
@@ -101,9 +114,16 @@
|
|||||||
<div class="text-sm text-gray-600 mt-1">{{ mode.description }}</div>
|
<div class="text-sm text-gray-600 mt-1">{{ mode.description }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="settingsStore.settings.ui.navigationMode === mode.value" class="text-blue-600 mt-1">
|
<div
|
||||||
|
v-if="settingsStore.settings.ui.navigationMode === mode.value"
|
||||||
|
class="text-blue-600 mt-1"
|
||||||
|
>
|
||||||
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -119,12 +139,21 @@
|
|||||||
<div class="text-sm text-gray-600">Reduce spacing and padding</div>
|
<div class="text-sm text-gray-600">Reduce spacing and padding</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@click="settingsStore.updateSetting('ui.compactMode', !settingsStore.settings.ui.compactMode)"
|
@click="
|
||||||
|
settingsStore.updateSetting(
|
||||||
|
'ui.compactMode',
|
||||||
|
!settingsStore.settings.ui.compactMode,
|
||||||
|
)
|
||||||
|
"
|
||||||
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
|
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
|
||||||
:class="settingsStore.settings.ui.compactMode ? 'bg-blue-600' : 'bg-gray-200'"
|
:class="settingsStore.settings.ui.compactMode ? 'bg-blue-600' : 'bg-gray-200'"
|
||||||
>
|
>
|
||||||
<span class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
|
<span
|
||||||
:class="settingsStore.settings.ui.compactMode ? 'translate-x-6' : 'translate-x-1'"></span>
|
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
|
||||||
|
:class="
|
||||||
|
settingsStore.settings.ui.compactMode ? 'translate-x-6' : 'translate-x-1'
|
||||||
|
"
|
||||||
|
></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -134,12 +163,21 @@
|
|||||||
<div class="text-sm text-gray-600">Enable smooth transitions</div>
|
<div class="text-sm text-gray-600">Enable smooth transitions</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@click="settingsStore.updateSetting('ui.showAnimations', !settingsStore.settings.ui.showAnimations)"
|
@click="
|
||||||
|
settingsStore.updateSetting(
|
||||||
|
'ui.showAnimations',
|
||||||
|
!settingsStore.settings.ui.showAnimations,
|
||||||
|
)
|
||||||
|
"
|
||||||
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
|
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
|
||||||
:class="settingsStore.settings.ui.showAnimations ? 'bg-blue-600' : 'bg-gray-200'"
|
:class="settingsStore.settings.ui.showAnimations ? 'bg-blue-600' : 'bg-gray-200'"
|
||||||
>
|
>
|
||||||
<span class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
|
<span
|
||||||
:class="settingsStore.settings.ui.showAnimations ? 'translate-x-6' : 'translate-x-1'"></span>
|
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
|
||||||
|
:class="
|
||||||
|
settingsStore.settings.ui.showAnimations ? 'translate-x-6' : 'translate-x-1'
|
||||||
|
"
|
||||||
|
></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -147,7 +185,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Data & Sync Settings -->
|
<!-- Data & Sync Settings -->
|
||||||
<div v-if="activeSection === 'data'" class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
<div
|
||||||
|
v-if="activeSection === 'data'"
|
||||||
|
class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden"
|
||||||
|
>
|
||||||
<div class="p-6 border-b border-gray-100">
|
<div class="p-6 border-b border-gray-100">
|
||||||
<h3 class="text-lg font-semibold text-gray-900">Data & Synchronization</h3>
|
<h3 class="text-lg font-semibold text-gray-900">Data & Synchronization</h3>
|
||||||
<p class="text-gray-600 text-sm mt-1">Configure data refresh and connection settings</p>
|
<p class="text-gray-600 text-sm mt-1">Configure data refresh and connection settings</p>
|
||||||
@@ -160,12 +201,19 @@
|
|||||||
<div class="text-sm text-gray-600">Automatically refresh data periodically</div>
|
<div class="text-sm text-gray-600">Automatically refresh data periodically</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@click="settingsStore.updateSetting('ui.autoRefresh', !settingsStore.settings.ui.autoRefresh)"
|
@click="
|
||||||
|
settingsStore.updateSetting(
|
||||||
|
'ui.autoRefresh',
|
||||||
|
!settingsStore.settings.ui.autoRefresh,
|
||||||
|
)
|
||||||
|
"
|
||||||
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
|
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
|
||||||
:class="settingsStore.settings.ui.autoRefresh ? 'bg-blue-600' : 'bg-gray-200'"
|
:class="settingsStore.settings.ui.autoRefresh ? 'bg-blue-600' : 'bg-gray-200'"
|
||||||
>
|
>
|
||||||
<span class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
|
<span
|
||||||
:class="settingsStore.settings.ui.autoRefresh ? 'translate-x-6' : 'translate-x-1'"></span>
|
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
|
||||||
|
:class="settingsStore.settings.ui.autoRefresh ? 'translate-x-6' : 'translate-x-1'"
|
||||||
|
></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -178,10 +226,17 @@
|
|||||||
min="1"
|
min="1"
|
||||||
max="60"
|
max="60"
|
||||||
:value="settingsStore.settings.ui.refreshInterval"
|
:value="settingsStore.settings.ui.refreshInterval"
|
||||||
@input="settingsStore.updateSetting('ui.refreshInterval', parseInt(($event.target as HTMLInputElement).value))"
|
@input="
|
||||||
|
settingsStore.updateSetting(
|
||||||
|
'ui.refreshInterval',
|
||||||
|
parseInt(($event.target as HTMLInputElement).value),
|
||||||
|
)
|
||||||
|
"
|
||||||
class="flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"
|
class="flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer"
|
||||||
/>
|
/>
|
||||||
<div class="bg-gray-100 px-3 py-1 rounded text-sm font-medium min-w-[80px] text-center">
|
<div
|
||||||
|
class="bg-gray-100 px-3 py-1 rounded text-sm font-medium min-w-[80px] text-center"
|
||||||
|
>
|
||||||
{{ settingsStore.settings.ui.refreshInterval }}s
|
{{ settingsStore.settings.ui.refreshInterval }}s
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -196,43 +251,62 @@
|
|||||||
type="text"
|
type="text"
|
||||||
placeholder="ws://localhost:8000/ws"
|
placeholder="ws://localhost:8000/ws"
|
||||||
class="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
class="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
:class="{ 'border-red-300 focus:ring-red-500 focus:border-red-500': websocketUrlError }"
|
:class="{
|
||||||
|
'border-red-300 focus:ring-red-500 focus:border-red-500': websocketUrlError,
|
||||||
|
}"
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
@click="updateWebSocketUrl"
|
@click="updateWebSocketUrl"
|
||||||
:disabled="!websocketUrlInput || websocketUrlInput === settingsStore.settings.websocketUrl"
|
:disabled="
|
||||||
|
!websocketUrlInput || websocketUrlInput === settingsStore.settings.websocketUrl
|
||||||
|
"
|
||||||
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-300 text-white rounded-lg text-sm font-medium transition-colors"
|
class="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-300 text-white rounded-lg text-sm font-medium transition-colors"
|
||||||
>
|
>
|
||||||
Update
|
Update
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="websocketUrlError" class="text-red-600 text-sm mt-1">{{ websocketUrlError }}</p>
|
<p v-if="websocketUrlError" class="text-red-600 text-sm mt-1">
|
||||||
<p v-else class="text-gray-500 text-sm mt-1">Current: {{ settingsStore.settings.websocketUrl }}</p>
|
{{ websocketUrlError }}
|
||||||
|
</p>
|
||||||
|
<p v-else class="text-gray-500 text-sm mt-1">
|
||||||
|
Current: {{ settingsStore.settings.websocketUrl }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Auto Connect -->
|
<!-- Auto Connect -->
|
||||||
<div class="flex items-center justify-between p-4 border border-gray-200 rounded-lg">
|
<div class="flex items-center justify-between p-4 border border-gray-200 rounded-lg">
|
||||||
<div>
|
<div>
|
||||||
<div class="font-medium text-gray-900">Auto Connect</div>
|
<div class="font-medium text-gray-900">Auto Connect</div>
|
||||||
<div class="text-sm text-gray-600">Automatically connect to WebSocket on app start</div>
|
<div class="text-sm text-gray-600">
|
||||||
|
Automatically connect to WebSocket on app start
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@click="settingsStore.updateSetting('autoConnect', !settingsStore.settings.autoConnect)"
|
@click="
|
||||||
|
settingsStore.updateSetting('autoConnect', !settingsStore.settings.autoConnect)
|
||||||
|
"
|
||||||
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
|
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
|
||||||
:class="settingsStore.settings.autoConnect ? 'bg-blue-600' : 'bg-gray-200'"
|
:class="settingsStore.settings.autoConnect ? 'bg-blue-600' : 'bg-gray-200'"
|
||||||
>
|
>
|
||||||
<span class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
|
<span
|
||||||
:class="settingsStore.settings.autoConnect ? 'translate-x-6' : 'translate-x-1'"></span>
|
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
|
||||||
|
:class="settingsStore.settings.autoConnect ? 'translate-x-6' : 'translate-x-1'"
|
||||||
|
></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Notifications Settings -->
|
<!-- Notifications Settings -->
|
||||||
<div v-if="activeSection === 'notifications'" class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
<div
|
||||||
|
v-if="activeSection === 'notifications'"
|
||||||
|
class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden"
|
||||||
|
>
|
||||||
<div class="p-6 border-b border-gray-100">
|
<div class="p-6 border-b border-gray-100">
|
||||||
<h3 class="text-lg font-semibold text-gray-900">Notifications</h3>
|
<h3 class="text-lg font-semibold text-gray-900">Notifications</h3>
|
||||||
<p class="text-gray-600 text-sm mt-1">Configure how you receive alerts and notifications</p>
|
<p class="text-gray-600 text-sm mt-1">
|
||||||
|
Configure how you receive alerts and notifications
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="p-6 space-y-6">
|
<div class="p-6 space-y-6">
|
||||||
<!-- Enable Notifications -->
|
<!-- Enable Notifications -->
|
||||||
@@ -242,34 +316,62 @@
|
|||||||
<div class="text-sm text-gray-600">Receive system alerts and updates</div>
|
<div class="text-sm text-gray-600">Receive system alerts and updates</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@click="settingsStore.updateSetting('notifications.enabled', !settingsStore.settings.notifications.enabled)"
|
@click="
|
||||||
|
settingsStore.updateSetting(
|
||||||
|
'notifications.enabled',
|
||||||
|
!settingsStore.settings.notifications.enabled,
|
||||||
|
)
|
||||||
|
"
|
||||||
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
|
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
|
||||||
:class="settingsStore.settings.notifications.enabled ? 'bg-blue-600' : 'bg-gray-200'"
|
:class="
|
||||||
|
settingsStore.settings.notifications.enabled ? 'bg-blue-600' : 'bg-gray-200'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<span class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
|
<span
|
||||||
:class="settingsStore.settings.notifications.enabled ? 'translate-x-6' : 'translate-x-1'"></span>
|
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
|
||||||
|
:class="
|
||||||
|
settingsStore.settings.notifications.enabled ? 'translate-x-6' : 'translate-x-1'
|
||||||
|
"
|
||||||
|
></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="settingsStore.settings.notifications.enabled" class="space-y-4">
|
<div v-if="settingsStore.settings.notifications.enabled" class="space-y-4">
|
||||||
<!-- Notification Types -->
|
<!-- Notification Types -->
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||||
<div class="flex items-center justify-between p-4 border border-gray-200 rounded-lg">
|
<div
|
||||||
|
class="flex items-center justify-between p-4 border border-gray-200 rounded-lg"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-medium text-gray-900">Sound Alerts</div>
|
<div class="font-medium text-gray-900">Sound Alerts</div>
|
||||||
<div class="text-sm text-gray-600">Play sound for notifications</div>
|
<div class="text-sm text-gray-600">Play sound for notifications</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@click="settingsStore.updateSetting('notifications.sound', !settingsStore.settings.notifications.sound)"
|
@click="
|
||||||
|
settingsStore.updateSetting(
|
||||||
|
'notifications.sound',
|
||||||
|
!settingsStore.settings.notifications.sound,
|
||||||
|
)
|
||||||
|
"
|
||||||
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
|
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
|
||||||
:class="settingsStore.settings.notifications.sound ? 'bg-blue-600' : 'bg-gray-200'"
|
:class="
|
||||||
|
settingsStore.settings.notifications.sound ? 'bg-blue-600' : 'bg-gray-200'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<span class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
|
<span
|
||||||
:class="settingsStore.settings.notifications.sound ? 'translate-x-6' : 'translate-x-1'"></span>
|
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
|
||||||
|
:class="
|
||||||
|
settingsStore.settings.notifications.sound
|
||||||
|
? 'translate-x-6'
|
||||||
|
: 'translate-x-1'
|
||||||
|
"
|
||||||
|
></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-between p-4 border border-gray-200 rounded-lg">
|
<div
|
||||||
|
class="flex items-center justify-between p-4 border border-gray-200 rounded-lg"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<div class="font-medium text-gray-900">Desktop Notifications</div>
|
<div class="font-medium text-gray-900">Desktop Notifications</div>
|
||||||
<div class="text-sm text-gray-600">Show browser notifications</div>
|
<div class="text-sm text-gray-600">Show browser notifications</div>
|
||||||
@@ -277,10 +379,18 @@
|
|||||||
<button
|
<button
|
||||||
@click="toggleDesktopNotifications"
|
@click="toggleDesktopNotifications"
|
||||||
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
|
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
|
||||||
:class="settingsStore.settings.notifications.desktop ? 'bg-blue-600' : 'bg-gray-200'"
|
:class="
|
||||||
|
settingsStore.settings.notifications.desktop ? 'bg-blue-600' : 'bg-gray-200'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<span class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
|
<span
|
||||||
:class="settingsStore.settings.notifications.desktop ? 'translate-x-6' : 'translate-x-1'"></span>
|
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
|
||||||
|
:class="
|
||||||
|
settingsStore.settings.notifications.desktop
|
||||||
|
? 'translate-x-6'
|
||||||
|
: 'translate-x-1'
|
||||||
|
"
|
||||||
|
></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -292,12 +402,27 @@
|
|||||||
<div class="text-sm text-gray-600">Only show high-priority notifications</div>
|
<div class="text-sm text-gray-600">Only show high-priority notifications</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@click="settingsStore.updateSetting('notifications.criticalOnly', !settingsStore.settings.notifications.criticalOnly)"
|
@click="
|
||||||
|
settingsStore.updateSetting(
|
||||||
|
'notifications.criticalOnly',
|
||||||
|
!settingsStore.settings.notifications.criticalOnly,
|
||||||
|
)
|
||||||
|
"
|
||||||
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
|
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
|
||||||
:class="settingsStore.settings.notifications.criticalOnly ? 'bg-blue-600' : 'bg-gray-200'"
|
:class="
|
||||||
|
settingsStore.settings.notifications.criticalOnly
|
||||||
|
? 'bg-blue-600'
|
||||||
|
: 'bg-gray-200'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
<span class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
|
<span
|
||||||
:class="settingsStore.settings.notifications.criticalOnly ? 'translate-x-6' : 'translate-x-1'"></span>
|
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
|
||||||
|
:class="
|
||||||
|
settingsStore.settings.notifications.criticalOnly
|
||||||
|
? 'translate-x-6'
|
||||||
|
: 'translate-x-1'
|
||||||
|
"
|
||||||
|
></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -305,7 +430,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Advanced Settings -->
|
<!-- Advanced Settings -->
|
||||||
<div v-if="activeSection === 'advanced'" class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
|
<div
|
||||||
|
v-if="activeSection === 'advanced'"
|
||||||
|
class="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden"
|
||||||
|
>
|
||||||
<div class="p-6 border-b border-gray-100">
|
<div class="p-6 border-b border-gray-100">
|
||||||
<h3 class="text-lg font-semibold text-gray-900">Advanced</h3>
|
<h3 class="text-lg font-semibold text-gray-900">Advanced</h3>
|
||||||
<p class="text-gray-600 text-sm mt-1">Developer options and advanced configuration</p>
|
<p class="text-gray-600 text-sm mt-1">Developer options and advanced configuration</p>
|
||||||
@@ -318,12 +446,19 @@
|
|||||||
<div class="text-sm text-gray-600">Enable debug logs and development features</div>
|
<div class="text-sm text-gray-600">Enable debug logs and development features</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@click="settingsStore.updateSetting('developerMode', !settingsStore.settings.developerMode)"
|
@click="
|
||||||
|
settingsStore.updateSetting(
|
||||||
|
'developerMode',
|
||||||
|
!settingsStore.settings.developerMode,
|
||||||
|
)
|
||||||
|
"
|
||||||
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
|
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
|
||||||
:class="settingsStore.settings.developerMode ? 'bg-blue-600' : 'bg-gray-200'"
|
:class="settingsStore.settings.developerMode ? 'bg-blue-600' : 'bg-gray-200'"
|
||||||
>
|
>
|
||||||
<span class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
|
<span
|
||||||
:class="settingsStore.settings.developerMode ? 'translate-x-6' : 'translate-x-1'"></span>
|
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
|
||||||
|
:class="settingsStore.settings.developerMode ? 'translate-x-6' : 'translate-x-1'"
|
||||||
|
></span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -357,7 +492,9 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="importError" class="text-red-600 text-sm mt-1">{{ importError }}</p>
|
<p v-if="importError" class="text-red-600 text-sm mt-1">{{ importError }}</p>
|
||||||
<p v-if="importSuccess" class="text-green-600 text-sm mt-1">Settings imported successfully!</p>
|
<p v-if="importSuccess" class="text-green-600 text-sm mt-1">
|
||||||
|
Settings imported successfully!
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -366,11 +503,15 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Reset Confirmation Dialog -->
|
<!-- Reset Confirmation Dialog -->
|
||||||
<div v-if="showResetDialog" class="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50">
|
<div
|
||||||
|
v-if="showResetDialog"
|
||||||
|
class="fixed inset-0 bg-black/50 flex items-center justify-center p-4 z-50"
|
||||||
|
>
|
||||||
<div class="bg-white rounded-xl max-w-md w-full p-6">
|
<div class="bg-white rounded-xl max-w-md w-full p-6">
|
||||||
<h3 class="text-lg font-semibold text-gray-900 mb-2">Reset All Settings</h3>
|
<h3 class="text-lg font-semibold text-gray-900 mb-2">Reset All Settings</h3>
|
||||||
<p class="text-gray-600 mb-4">
|
<p class="text-gray-600 mb-4">
|
||||||
Are you sure you want to reset all settings to their default values? This action cannot be undone.
|
Are you sure you want to reset all settings to their default values? This action cannot be
|
||||||
|
undone.
|
||||||
</p>
|
</p>
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
<button
|
<button
|
||||||
@@ -412,26 +553,26 @@ const settingSections = [
|
|||||||
id: 'appearance',
|
id: 'appearance',
|
||||||
name: 'Appearance',
|
name: 'Appearance',
|
||||||
description: 'Theme & UI',
|
description: 'Theme & UI',
|
||||||
icon: '🎨'
|
icon: '🎨',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'data',
|
id: 'data',
|
||||||
name: 'Data & Sync',
|
name: 'Data & Sync',
|
||||||
description: 'Connection & refresh',
|
description: 'Connection & refresh',
|
||||||
icon: '🔄'
|
icon: '🔄',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'notifications',
|
id: 'notifications',
|
||||||
name: 'Notifications',
|
name: 'Notifications',
|
||||||
description: 'Alerts & sounds',
|
description: 'Alerts & sounds',
|
||||||
icon: '🔔'
|
icon: '🔔',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'advanced',
|
id: 'advanced',
|
||||||
name: 'Advanced',
|
name: 'Advanced',
|
||||||
description: 'Developer options',
|
description: 'Developer options',
|
||||||
icon: '⚙️'
|
icon: '⚙️',
|
||||||
}
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
// Methods
|
// Methods
|
||||||
@@ -439,7 +580,7 @@ const formatTime = (date: Date) => {
|
|||||||
return new Intl.DateTimeFormat('en-US', {
|
return new Intl.DateTimeFormat('en-US', {
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
second: '2-digit'
|
second: '2-digit',
|
||||||
}).format(date)
|
}).format(date)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user