This commit is contained in:
rafaeldpsilva
2025-12-20 00:17:21 +00:00
parent 4b4338fb91
commit c3364cc422
31 changed files with 818 additions and 425 deletions

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;
}
} }

View File

@@ -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;
}

View File

@@ -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>

View File

@@ -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'
} }
} }

View File

@@ -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'
} }
} }

View File

@@ -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(() => {

View File

@@ -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">

View File

@@ -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

View File

@@ -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,
} }
} }

View File

@@ -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) {

View File

@@ -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> {

View File

@@ -26,5 +26,5 @@ export type {
RoomComparison, RoomComparison,
SystemEvent, SystemEvent,
HealthCheck, HealthCheck,
SystemStatus SystemStatus,
} from './api' } from './api'

View File

@@ -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 {

View File

@@ -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)
}) })

View File

@@ -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()
} }

View File

@@ -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
}) })

View File

@@ -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
} }

View File

@@ -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>

View File

@@ -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)

View File

@@ -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)
} }