format
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div class="bg-white rounded-2xl shadow-sm p-4">
|
||||
<h6 class="text-sm font-bold text-gray-500 mb-4">Air Quality Status</h6>
|
||||
|
||||
|
||||
<!-- Overall Status -->
|
||||
<div class="mb-4 p-3 rounded-lg" :class="getOverallStatusBg()">
|
||||
<div class="flex items-center justify-between">
|
||||
@@ -13,11 +13,28 @@
|
||||
Building Average: {{ overallCO2.toFixed(0) }} ppm
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-12 h-12 rounded-full flex items-center justify-center" :class="getOverallStatusIconBg()">
|
||||
<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/>
|
||||
<div
|
||||
class="w-12 h-12 rounded-full flex items-center justify-center"
|
||||
:class="getOverallStatusIconBg()"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -28,8 +45,12 @@
|
||||
<div v-if="roomsList.length === 0" class="text-center text-gray-500 py-4">
|
||||
No air quality data available
|
||||
</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="w-3 h-3 rounded-full"
|
||||
@@ -76,9 +97,9 @@ const roomStore = useRoomStore()
|
||||
|
||||
const roomsList = computed(() => {
|
||||
return Array.from(roomStore.roomsData.values())
|
||||
.filter(room => room.co2) // Only include rooms with CO2 data
|
||||
.sort((a, b) =>
|
||||
(b.co2?.current || 0) - (a.co2?.current || 0) // Sort by CO2 level descending
|
||||
.filter((room) => room.co2) // Only include rooms with CO2 data
|
||||
.sort(
|
||||
(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(() => {
|
||||
return roomsList.value.filter(room => room.co2?.status === 'good').length
|
||||
return roomsList.value.filter((room) => room.co2?.status === 'good').length
|
||||
})
|
||||
|
||||
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 recs = []
|
||||
const criticalRooms = roomsList.value.filter(room => room.co2?.status === 'critical')
|
||||
const poorRooms = roomsList.value.filter(room => room.co2?.status === 'poor')
|
||||
const criticalRooms = roomsList.value.filter((room) => room.co2?.status === 'critical')
|
||||
const poorRooms = roomsList.value.filter((room) => room.co2?.status === 'poor')
|
||||
|
||||
if (criticalRooms.length > 0) {
|
||||
recs.push(`Immediate ventilation needed in ${criticalRooms[0].room}`)
|
||||
}
|
||||
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) {
|
||||
recs.push('Consider adjusting HVAC settings building-wide')
|
||||
@@ -120,61 +145,91 @@ const recommendations = computed(() => {
|
||||
|
||||
const getOverallStatus = () => {
|
||||
switch (overallStatus.value) {
|
||||
case 'good': return 'Excellent Air Quality'
|
||||
case 'moderate': return 'Moderate Air Quality'
|
||||
case 'poor': return 'Poor Air Quality'
|
||||
case 'critical': return 'Critical - Action Required'
|
||||
default: return 'Unknown Status'
|
||||
case 'good':
|
||||
return 'Excellent Air Quality'
|
||||
case 'moderate':
|
||||
return 'Moderate Air Quality'
|
||||
case 'poor':
|
||||
return 'Poor Air Quality'
|
||||
case 'critical':
|
||||
return 'Critical - Action Required'
|
||||
default:
|
||||
return 'Unknown Status'
|
||||
}
|
||||
}
|
||||
|
||||
const getOverallStatusBg = () => {
|
||||
switch (overallStatus.value) {
|
||||
case 'good': return 'bg-green-50 border border-green-200'
|
||||
case 'moderate': return 'bg-yellow-50 border border-yellow-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'
|
||||
case 'good':
|
||||
return 'bg-green-50 border border-green-200'
|
||||
case 'moderate':
|
||||
return 'bg-yellow-50 border border-yellow-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 = () => {
|
||||
switch (overallStatus.value) {
|
||||
case 'good': return 'text-green-700'
|
||||
case 'moderate': return 'text-yellow-700'
|
||||
case 'poor': return 'text-orange-700'
|
||||
case 'critical': return 'text-red-700'
|
||||
default: return 'text-gray-700'
|
||||
case 'good':
|
||||
return 'text-green-700'
|
||||
case 'moderate':
|
||||
return 'text-yellow-700'
|
||||
case 'poor':
|
||||
return 'text-orange-700'
|
||||
case 'critical':
|
||||
return 'text-red-700'
|
||||
default:
|
||||
return 'text-gray-700'
|
||||
}
|
||||
}
|
||||
|
||||
const getOverallStatusIconBg = () => {
|
||||
switch (overallStatus.value) {
|
||||
case 'good': return 'bg-green-100'
|
||||
case 'moderate': return 'bg-yellow-100'
|
||||
case 'poor': return 'bg-orange-100'
|
||||
case 'critical': return 'bg-red-100'
|
||||
default: return 'bg-gray-100'
|
||||
case 'good':
|
||||
return 'bg-green-100'
|
||||
case 'moderate':
|
||||
return 'bg-yellow-100'
|
||||
case 'poor':
|
||||
return 'bg-orange-100'
|
||||
case 'critical':
|
||||
return 'bg-red-100'
|
||||
default:
|
||||
return 'bg-gray-100'
|
||||
}
|
||||
}
|
||||
|
||||
const getCO2StatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'good': return 'bg-green-500'
|
||||
case 'moderate': return 'bg-yellow-500'
|
||||
case 'poor': return 'bg-orange-500'
|
||||
case 'critical': return 'bg-red-500'
|
||||
default: return 'bg-gray-500'
|
||||
case 'good':
|
||||
return 'bg-green-500'
|
||||
case 'moderate':
|
||||
return 'bg-yellow-500'
|
||||
case 'poor':
|
||||
return 'bg-orange-500'
|
||||
case 'critical':
|
||||
return 'bg-red-500'
|
||||
default:
|
||||
return 'bg-gray-500'
|
||||
}
|
||||
}
|
||||
|
||||
const getCO2TextColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'good': return 'text-green-600'
|
||||
case 'moderate': return 'text-yellow-600'
|
||||
case 'poor': return 'text-orange-600'
|
||||
case 'critical': return 'text-red-600'
|
||||
default: return 'text-gray-600'
|
||||
case 'good':
|
||||
return 'text-green-600'
|
||||
case 'moderate':
|
||||
return 'text-yellow-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="[
|
||||
getSensorStatusColor(sensor.status),
|
||||
isRecentlyUpdated ? 'animate-pulse shadow-lg shadow-green-400/50' : ''
|
||||
isRecentlyUpdated ? 'animate-pulse shadow-lg shadow-green-400/50' : '',
|
||||
]"
|
||||
></div>
|
||||
<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>
|
||||
<select
|
||||
: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"
|
||||
>
|
||||
<option value="">Unassigned</option>
|
||||
@@ -46,7 +48,7 @@
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">Tags</label>
|
||||
<div class="flex flex-wrap gap-1 mb-2">
|
||||
<span
|
||||
<span
|
||||
v-for="tag in sensor.tags || getDefaultTags(sensor)"
|
||||
:key="tag"
|
||||
class="px-2 py-1 bg-gray-100 text-gray-700 rounded-full text-xs font-medium"
|
||||
@@ -60,7 +62,7 @@
|
||||
<div>
|
||||
<div class="text-sm font-medium text-gray-700 mb-2">Monitoring Capabilities</div>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<span
|
||||
<span
|
||||
v-for="capability in sensor.capabilities.monitoring"
|
||||
:key="capability"
|
||||
class="px-2 py-1 bg-blue-100 text-blue-700 rounded text-xs font-medium"
|
||||
@@ -74,8 +76,7 @@
|
||||
<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 v-for="metric in sensorValues" :key="metric.type"
|
||||
class="bg-gray-50 rounded p-2">
|
||||
<div v-for="metric in sensorValues" :key="metric.type" class="bg-gray-50 rounded p-2">
|
||||
<div class="text-gray-600 mb-1">{{ metric.label }}</div>
|
||||
<div class="font-medium text-gray-900">
|
||||
{{ metric.value }} <span class="text-gray-500">{{ metric.unit }}</span>
|
||||
@@ -109,7 +110,7 @@
|
||||
<div class="flex items-center gap-1">
|
||||
<span>{{ sensor.metadata.battery }}%</span>
|
||||
<div class="w-3 h-1 bg-gray-200 rounded-full">
|
||||
<div
|
||||
<div
|
||||
class="h-full rounded-full transition-all"
|
||||
:class="getBatteryColor(sensor.metadata.battery)"
|
||||
:style="{ width: sensor.metadata.battery + '%' }"
|
||||
@@ -122,11 +123,11 @@
|
||||
<div class="flex items-center gap-1">
|
||||
<span>{{ sensor.metadata.signalStrength }}%</span>
|
||||
<div class="flex gap-0.5">
|
||||
<div
|
||||
v-for="i in 4"
|
||||
<div
|
||||
v-for="i in 4"
|
||||
:key="i"
|
||||
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>
|
||||
@@ -155,7 +156,9 @@
|
||||
<!-- No Actions State -->
|
||||
<div v-else>
|
||||
<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
|
||||
</div>
|
||||
</div>
|
||||
@@ -186,7 +189,10 @@ const getSensorValues = (sensor: SensorDevice) => {
|
||||
|
||||
// Get real-time sensor reading from store
|
||||
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] Sensor capabilities:`, sensor.capabilities?.monitoring)
|
||||
|
||||
@@ -197,7 +203,7 @@ const getSensorValues = (sensor: SensorDevice) => {
|
||||
type: 'energy',
|
||||
label: 'Energy Consumption',
|
||||
value: energyValue,
|
||||
unit: latestReading?.energy?.unit || 'kWh'
|
||||
unit: latestReading?.energy?.unit || 'kWh',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -208,19 +214,19 @@ const getSensorValues = (sensor: SensorDevice) => {
|
||||
type: 'co2',
|
||||
label: 'CO2 Level',
|
||||
value: co2Value,
|
||||
unit: latestReading?.co2?.unit || 'ppm'
|
||||
unit: latestReading?.co2?.unit || 'ppm',
|
||||
})
|
||||
}
|
||||
|
||||
// Only show temperature if the sensor monitors temperature
|
||||
if (sensor.capabilities?.monitoring?.includes('temperature')) {
|
||||
const tempValue = latestReading?.temperature?.value?.toFixed(1) ||
|
||||
(Math.random() * 8 + 18).toFixed(1)
|
||||
const tempValue =
|
||||
latestReading?.temperature?.value?.toFixed(1) || (Math.random() * 8 + 18).toFixed(1)
|
||||
values.push({
|
||||
type: 'temperature',
|
||||
label: 'Temperature',
|
||||
value: tempValue,
|
||||
unit: latestReading?.temperature?.unit || '°C'
|
||||
unit: latestReading?.temperature?.unit || '°C',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -230,7 +236,7 @@ const getSensorValues = (sensor: SensorDevice) => {
|
||||
type: 'humidity',
|
||||
label: 'Humidity',
|
||||
value: Math.floor(Math.random() * 40 + 30),
|
||||
unit: '%'
|
||||
unit: '%',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -240,7 +246,7 @@ const getSensorValues = (sensor: SensorDevice) => {
|
||||
type: 'motion',
|
||||
label: 'Motion Status',
|
||||
value: Math.random() > 0.7 ? 'Detected' : 'Clear',
|
||||
unit: ''
|
||||
unit: '',
|
||||
})
|
||||
}
|
||||
|
||||
@@ -252,13 +258,13 @@ const getSensorValues = (sensor: SensorDevice) => {
|
||||
type: 'brightness',
|
||||
label: 'Brightness Level',
|
||||
value: Math.floor(Math.random() * 100),
|
||||
unit: '%'
|
||||
unit: '%',
|
||||
})
|
||||
values.push({
|
||||
type: 'power',
|
||||
label: 'Power Draw',
|
||||
value: Math.floor(Math.random() * 50 + 5),
|
||||
unit: 'W'
|
||||
unit: 'W',
|
||||
})
|
||||
break
|
||||
case 'hvac':
|
||||
@@ -266,13 +272,13 @@ const getSensorValues = (sensor: SensorDevice) => {
|
||||
type: 'setpoint',
|
||||
label: 'Target Temperature',
|
||||
value: (Math.random() * 6 + 18).toFixed(1),
|
||||
unit: '°C'
|
||||
unit: '°C',
|
||||
})
|
||||
values.push({
|
||||
type: 'mode',
|
||||
label: 'Operating Mode',
|
||||
value: ['Heat', 'Cool', 'Auto', 'Fan'][Math.floor(Math.random() * 4)],
|
||||
unit: ''
|
||||
unit: '',
|
||||
})
|
||||
break
|
||||
case 'security':
|
||||
@@ -280,13 +286,13 @@ const getSensorValues = (sensor: SensorDevice) => {
|
||||
type: 'status',
|
||||
label: 'Security Status',
|
||||
value: Math.random() > 0.8 ? 'Alert' : 'Normal',
|
||||
unit: ''
|
||||
unit: '',
|
||||
})
|
||||
values.push({
|
||||
type: 'armed',
|
||||
label: 'System Armed',
|
||||
value: Math.random() > 0.5 ? 'Yes' : 'No',
|
||||
unit: ''
|
||||
unit: '',
|
||||
})
|
||||
break
|
||||
default:
|
||||
@@ -295,7 +301,7 @@ const getSensorValues = (sensor: SensorDevice) => {
|
||||
type: 'status',
|
||||
label: 'Device Status',
|
||||
value: sensor.status === 'online' ? 'Active' : 'Inactive',
|
||||
unit: ''
|
||||
unit: '',
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -305,7 +311,7 @@ const getSensorValues = (sensor: SensorDevice) => {
|
||||
type: 'uptime',
|
||||
label: 'Uptime',
|
||||
value: Math.floor(Math.random() * 30 + 1),
|
||||
unit: 'days'
|
||||
unit: 'days',
|
||||
})
|
||||
|
||||
return values
|
||||
@@ -345,7 +351,7 @@ const getSensorTypeIcon = (type: string) => {
|
||||
humidity: '💧',
|
||||
hvac: '❄️',
|
||||
lighting: '💡',
|
||||
security: '🔒'
|
||||
security: '🔒',
|
||||
}
|
||||
return icons[type as keyof typeof icons] || '📱'
|
||||
}
|
||||
@@ -358,17 +364,21 @@ const getSensorTypeStyle = (type: string) => {
|
||||
humidity: { bg: 'bg-blue-100', text: 'text-blue-700' },
|
||||
hvac: { bg: 'bg-cyan-100', text: 'text-cyan-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' }
|
||||
}
|
||||
|
||||
const getSensorStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'online': return 'bg-green-500'
|
||||
case 'offline': return 'bg-gray-400'
|
||||
case 'error': return 'bg-red-500'
|
||||
default: return 'bg-gray-400'
|
||||
case 'online':
|
||||
return 'bg-green-500'
|
||||
case 'offline':
|
||||
return 'bg-gray-400'
|
||||
case 'error':
|
||||
return 'bg-red-500'
|
||||
default:
|
||||
return 'bg-gray-400'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,7 +393,7 @@ const formatTime = (timestamp: number) => {
|
||||
const now = new Date()
|
||||
const diffMs = now.getTime() - date.getTime()
|
||||
const diffSecs = Math.floor(diffMs / 1000)
|
||||
|
||||
|
||||
if (diffSecs < 60) {
|
||||
return `${diffSecs}s ago`
|
||||
} else if (diffSecs < 3600) {
|
||||
@@ -394,4 +404,4 @@ const formatTime = (timestamp: number) => {
|
||||
return date.toLocaleDateString()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -1,26 +1,25 @@
|
||||
<template>
|
||||
<div class="bg-white rounded-2xl shadow-sm p-4">
|
||||
<h6 class="text-sm font-bold text-gray-500 mb-4">Room Overview</h6>
|
||||
|
||||
|
||||
<div class="space-y-4">
|
||||
<div v-if="roomsList.length === 0" class="text-center text-gray-500 py-8">
|
||||
No room data available. Connect sensors to see room metrics.
|
||||
</div>
|
||||
|
||||
|
||||
<div v-for="room in roomsList" :key="room.room" class="border border-gray-100 rounded-lg p-3">
|
||||
<!-- Room Header -->
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="font-medium text-gray-900">{{ room.room }}</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- CO2 Status Indicator -->
|
||||
<div
|
||||
class="w-3 h-3 rounded-full"
|
||||
:class="getCO2StatusColor(room.co2!.status)"
|
||||
></div>
|
||||
<div class="w-3 h-3 rounded-full" :class="getCO2StatusColor(room.co2!.status)"></div>
|
||||
<!-- Occupancy Indicator -->
|
||||
<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">
|
||||
<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>
|
||||
<span class="capitalize">{{ room.occupancyEstimate }}</span>
|
||||
</div>
|
||||
@@ -32,15 +31,21 @@
|
||||
<!-- Energy -->
|
||||
<div class="bg-blue-50 rounded p-2">
|
||||
<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>
|
||||
|
||||
<!-- CO2 -->
|
||||
<div class="rounded p-2" :class="getCO2BackgroundColor(room.co2!.status)">
|
||||
<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="text-xs" :class="getCO2TextColor(room.co2!.status)">{{ room.co2!.status.toUpperCase() }}</div>
|
||||
<div :class="getCO2TextColor(room.co2!.status)">
|
||||
{{ 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>
|
||||
|
||||
@@ -53,7 +58,10 @@
|
||||
</div>
|
||||
|
||||
<!-- 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 class="font-medium text-gray-900">{{ roomsList.length }}</div>
|
||||
<div class="text-gray-500">Rooms</div>
|
||||
@@ -78,7 +86,7 @@ const roomStore = useRoomStore()
|
||||
|
||||
const roomsList = computed(() => {
|
||||
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))
|
||||
})
|
||||
|
||||
@@ -94,31 +102,46 @@ const averageCO2 = computed(() => {
|
||||
|
||||
const getCO2StatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'good': return 'bg-green-500'
|
||||
case 'moderate': return 'bg-yellow-500'
|
||||
case 'poor': return 'bg-orange-500'
|
||||
case 'critical': return 'bg-red-500'
|
||||
default: return 'bg-gray-500'
|
||||
case 'good':
|
||||
return 'bg-green-500'
|
||||
case 'moderate':
|
||||
return 'bg-yellow-500'
|
||||
case 'poor':
|
||||
return 'bg-orange-500'
|
||||
case 'critical':
|
||||
return 'bg-red-500'
|
||||
default:
|
||||
return 'bg-gray-500'
|
||||
}
|
||||
}
|
||||
|
||||
const getCO2BackgroundColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'good': return 'bg-green-50'
|
||||
case 'moderate': return 'bg-yellow-50'
|
||||
case 'poor': return 'bg-orange-50'
|
||||
case 'critical': return 'bg-red-50'
|
||||
default: return 'bg-gray-50'
|
||||
case 'good':
|
||||
return 'bg-green-50'
|
||||
case 'moderate':
|
||||
return 'bg-yellow-50'
|
||||
case 'poor':
|
||||
return 'bg-orange-50'
|
||||
case 'critical':
|
||||
return 'bg-red-50'
|
||||
default:
|
||||
return 'bg-gray-50'
|
||||
}
|
||||
}
|
||||
|
||||
const getCO2TextColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'good': return 'text-green-700'
|
||||
case 'moderate': return 'text-yellow-700'
|
||||
case 'poor': return 'text-orange-700'
|
||||
case 'critical': return 'text-red-700'
|
||||
default: return 'text-gray-700'
|
||||
case 'good':
|
||||
return 'text-green-700'
|
||||
case 'moderate':
|
||||
return 'text-yellow-700'
|
||||
case 'poor':
|
||||
return 'text-orange-700'
|
||||
case 'critical':
|
||||
return 'text-red-700'
|
||||
default:
|
||||
return 'text-gray-700'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,7 +150,7 @@ const formatTime = (timestamp: number) => {
|
||||
const now = new Date()
|
||||
const diffMs = now.getTime() - date.getTime()
|
||||
const diffSecs = Math.floor(diffMs / 1000)
|
||||
|
||||
|
||||
if (diffSecs < 60) {
|
||||
return `${diffSecs}s ago`
|
||||
} else if (diffSecs < 3600) {
|
||||
@@ -136,4 +159,4 @@ const formatTime = (timestamp: number) => {
|
||||
return date.toLocaleTimeString()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -9,9 +9,7 @@
|
||||
Token expires in: {{ formatTimeUntilExpiry() }}
|
||||
</div>
|
||||
|
||||
<div v-if="authStore.error" class="auth-status__error">
|
||||
Auth Error: {{ authStore.error }}
|
||||
</div>
|
||||
<div v-if="authStore.error" class="auth-status__error">Auth Error: {{ authStore.error }}</div>
|
||||
|
||||
<button
|
||||
v-if="!authStore.isAuthenticated"
|
||||
@@ -33,13 +31,13 @@ const authStore = useAuthStore()
|
||||
const authStatusClass = computed(() => ({
|
||||
'auth-status--authenticated': authStore.isAuthenticated,
|
||||
'auth-status--error': !authStore.isAuthenticated || authStore.error,
|
||||
'auth-status--loading': authStore.isLoading
|
||||
'auth-status--loading': authStore.isLoading,
|
||||
}))
|
||||
|
||||
const statusDotClass = computed(() => ({
|
||||
'auth-status__dot--green': 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(() => {
|
||||
@@ -116,4 +114,4 @@ async function handleReauth() {
|
||||
@apply bg-blue-500 text-white px-2 py-1 rounded text-xs hover:bg-blue-600 disabled:opacity-50;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
<div class="absolute inset-0 bg-black/50" @click="$emit('close')"></div>
|
||||
|
||||
<!-- 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 -->
|
||||
<div class="p-6 border-b border-gray-100">
|
||||
<div class="flex items-center justify-between">
|
||||
|
||||
@@ -5,12 +5,17 @@
|
||||
<div class="p-6 border-b border-gray-200">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-xl font-semibold text-gray-900">Room Management</h2>
|
||||
<button
|
||||
<button
|
||||
@click="$emit('close')"
|
||||
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">
|
||||
<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>
|
||||
</button>
|
||||
</div>
|
||||
@@ -53,49 +58,54 @@
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-3">
|
||||
<div
|
||||
v-for="room in roomsWithStats"
|
||||
:key="room.name"
|
||||
class="bg-gray-50 rounded-lg p-4"
|
||||
>
|
||||
<div 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-1">
|
||||
<div class="flex items-center gap-3 mb-2">
|
||||
<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
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="grid grid-cols-2 lg:grid-cols-4 gap-4 text-sm">
|
||||
<div>
|
||||
<span class="text-gray-600">Types:</span>
|
||||
<div class="flex flex-wrap gap-1 mt-1">
|
||||
<span
|
||||
v-for="type in room.sensorTypes"
|
||||
<span
|
||||
v-for="type in room.sensorTypes"
|
||||
:key="type"
|
||||
class="text-xs px-2 py-0.5 bg-gray-200 text-gray-700 rounded"
|
||||
>
|
||||
{{ type }}
|
||||
</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>
|
||||
<span class="text-gray-600">Energy:</span>
|
||||
<div class="font-medium" :class="room.hasMetrics ? 'text-gray-900' : 'text-gray-400'">
|
||||
{{ room.hasMetrics ? room.energyConsumption.toFixed(2) + ' kWh' : 'No data' }}
|
||||
<div
|
||||
class="font-medium"
|
||||
:class="room.hasMetrics ? 'text-gray-900' : 'text-gray-400'"
|
||||
>
|
||||
{{
|
||||
room.hasMetrics ? room.energyConsumption.toFixed(2) + ' kWh' : 'No data'
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<span class="text-gray-600">CO2:</span>
|
||||
<div class="font-medium" :class="getCO2Color(room.co2Level)">
|
||||
{{ room.hasMetrics ? Math.round(room.co2Level) + ' ppm' : 'No data' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<span class="text-gray-600">Last Update:</span>
|
||||
<div class="text-xs text-gray-500">
|
||||
@@ -104,7 +114,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="ml-4">
|
||||
<button
|
||||
@click="confirmDeleteRoom(room.name)"
|
||||
@@ -118,7 +128,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Empty state -->
|
||||
<div v-if="roomsWithStats.length === 0" class="text-center py-8">
|
||||
<div class="text-gray-400 text-4xl mb-2">🏢</div>
|
||||
@@ -132,7 +142,7 @@
|
||||
<!-- Footer -->
|
||||
<div class="p-6 border-t border-gray-200 bg-gray-50">
|
||||
<div class="flex justify-end gap-3">
|
||||
<button
|
||||
<button
|
||||
@click="$emit('close')"
|
||||
class="px-4 py-2 border border-gray-300 text-gray-700 rounded-lg text-sm font-medium hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
@@ -143,23 +153,28 @@
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<h3 class="text-lg font-semibold text-gray-900 mb-2">Delete Room</h3>
|
||||
<p class="text-gray-600 mb-4">
|
||||
Are you sure you want to delete <strong>"{{ roomToDelete }}"</strong>?
|
||||
{{ getRoomStats(roomToDelete).sensorCount > 0
|
||||
? `This will unassign ${getRoomStats(roomToDelete).sensorCount} sensor(s).`
|
||||
: 'This action cannot be undone.' }}
|
||||
Are you sure you want to delete <strong>"{{ roomToDelete }}"</strong>?
|
||||
{{
|
||||
getRoomStats(roomToDelete).sensorCount > 0
|
||||
? `This will unassign ${getRoomStats(roomToDelete).sensorCount} sensor(s).`
|
||||
: 'This action cannot be undone.'
|
||||
}}
|
||||
</p>
|
||||
<div class="flex gap-3">
|
||||
<button
|
||||
<button
|
||||
@click="roomToDelete = null"
|
||||
class="flex-1 px-4 py-2 border border-gray-300 text-gray-700 rounded-lg text-sm font-medium hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
<button
|
||||
@click="deleteRoom"
|
||||
class="flex-1 px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-lg text-sm font-medium transition-colors"
|
||||
>
|
||||
@@ -274,7 +289,7 @@ const formatTime = (timestamp: number) => {
|
||||
const now = new Date()
|
||||
const diffMs = now.getTime() - date.getTime()
|
||||
const diffSecs = Math.floor(diffMs / 1000)
|
||||
|
||||
|
||||
if (diffSecs < 60) {
|
||||
return `${diffSecs}s ago`
|
||||
} else if (diffSecs < 3600) {
|
||||
@@ -296,4 +311,4 @@ const clearError = () => {
|
||||
// Watch for changes in newRoomName to clear errors
|
||||
import { watch } from 'vue'
|
||||
watch(newRoomName, clearError)
|
||||
</script>
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user