Refactor dashboard layout and update sensor table columns
- Make metric cards and charts more compact and consistent - Change SensorConsumptionTable columns: show Room and Value, remove Current/Total/Average - Update headings and layout for AnalyticsView and HomeView - Improve responsiveness and spacing for cards and sections
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="bg-white rounded-2xl shadow-sm flex flex-col justify-between h-full w-full p-4">
|
<div class="bg-white rounded-2xl shadow-sm flex flex-col justify-between aspect-square p-4">
|
||||||
<h6 class="text-sm font-bold text-gray-500">{{ title }}</h6>
|
<h6 class="text-sm font-bold text-gray-500">{{ title }}</h6>
|
||||||
<div class="flex-grow flex items-center justify-start">
|
<div class="flex-grow flex items-center justify-start">
|
||||||
<p class="text-gray-900 font-bold text-2xl">
|
<p class="text-gray-900 font-bold text-2xl">
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="bg-white rounded-2xl shadow-sm flex flex-col h-full min-h-[300px]">
|
<div class="bg-white rounded-2xl shadow-sm flex flex-col p-4">
|
||||||
<div class="p-4 h-full">
|
<h6 class="text-sm font-bold text-gray-500 mb-2">{{ title }}</h6>
|
||||||
<h6 class="text-sm font-bold text-gray-500 mb-2">{{ title }}</h6>
|
<div class="w-full h-[400px]">
|
||||||
<v-chart class="h-64 w-full" :option="option" autoresize />
|
<v-chart class="w-full h-full" :option="option" autoresize />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="bg-white rounded-2xl shadow-sm p-4">
|
<div class="bg-white rounded-2xl shadow-sm p-4">
|
||||||
<h6 class="text-sm font-bold text-gray-500 mb-4">Sensor Consumption</h6>
|
<h6 class="text-sm font-bold text-gray-500 mb-4">Sensor Readings</h6>
|
||||||
|
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<table class="min-w-full">
|
<table class="min-w-full">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -10,13 +9,10 @@
|
|||||||
Sensor ID
|
Sensor ID
|
||||||
</th>
|
</th>
|
||||||
<th class="text-right text-xs font-medium text-gray-500 uppercase tracking-wider py-3">
|
<th class="text-right text-xs font-medium text-gray-500 uppercase tracking-wider py-3">
|
||||||
Current
|
Room
|
||||||
</th>
|
</th>
|
||||||
<th class="text-right text-xs font-medium text-gray-500 uppercase tracking-wider py-3">
|
<th class="text-right text-xs font-medium text-gray-500 uppercase tracking-wider py-3">
|
||||||
Total
|
Value
|
||||||
</th>
|
|
||||||
<th class="text-right text-xs font-medium text-gray-500 uppercase tracking-wider py-3">
|
|
||||||
Average
|
|
||||||
</th>
|
</th>
|
||||||
<th class="text-right text-xs font-medium text-gray-500 uppercase tracking-wider py-3">
|
<th class="text-right text-xs font-medium text-gray-500 uppercase tracking-wider py-3">
|
||||||
Last Updated
|
Last Updated
|
||||||
@@ -55,9 +51,6 @@
|
|||||||
sensor.humidity?.unit
|
sensor.humidity?.unit
|
||||||
}}
|
}}
|
||||||
</td>
|
</td>
|
||||||
<td class="py-3 text-sm text-gray-600 text-right">
|
|
||||||
{{ sensor.room }}
|
|
||||||
</td>
|
|
||||||
<td class="py-3 text-sm text-gray-500 text-right">
|
<td class="py-3 text-sm text-gray-500 text-right">
|
||||||
{{ formatTime(sensor.timestamp) }}
|
{{ formatTime(sensor.timestamp) }}
|
||||||
</td>
|
</td>
|
||||||
@@ -66,7 +59,6 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Connection Status Indicator -->
|
|
||||||
<div class="mt-4 flex items-center justify-between text-xs text-gray-500">
|
<div class="mt-4 flex items-center justify-between text-xs text-gray-500">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,262 +1,21 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-gray-50">
|
<div class="space-y-6">
|
||||||
<div class="px-4 py-6 mx-auto max-w-7xl sm:px-6 lg:px-8">
|
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div class="mb-8">
|
<div>
|
||||||
<h1 class="text-3xl font-bold text-gray-900">API Dashboard</h1>
|
<h1 class="text-2xl font-bold text-gray-900">Analytics</h1>
|
||||||
<p class="text-gray-600 mt-2">Real-time data from backend APIs</p>
|
<p class="text-gray-600">Manage sensors, assign rooms, and control device actions</p>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<!-- API Status Section -->
|
<div class="mb-8">
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
<h1 class="text-3xl font-bold text-gray-900">API Dashboard</h1>
|
||||||
<div class="bg-white rounded-lg shadow p-6">
|
<p class="text-gray-600 mt-2">Real-time data from backend APIs</p>
|
||||||
<div class="flex items-center">
|
</div>
|
||||||
<div class="p-2 bg-blue-100 rounded-lg">
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||||
<svg
|
|
||||||
class="w-6 h-6 text-blue-600"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="ml-4">
|
|
||||||
<p class="text-sm font-medium text-gray-600">System Status</p>
|
|
||||||
<p
|
|
||||||
class="text-lg font-semibold"
|
|
||||||
:class="healthStatus?.status === 'healthy' ? 'text-green-600' : 'text-red-600'"
|
|
||||||
>
|
|
||||||
{{ healthStatus?.status || 'Unknown' }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-white rounded-lg shadow p-6">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div class="p-2 bg-green-100 rounded-lg">
|
|
||||||
<svg
|
|
||||||
class="w-6 h-6 text-green-600"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M13 10V3L4 14h7v7l9-11h-7z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="ml-4">
|
|
||||||
<p class="text-sm font-medium text-gray-600">Total Sensors</p>
|
|
||||||
<p class="text-lg font-semibold text-gray-900">{{ sensorStore.totalSensors }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-white rounded-lg shadow p-6">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div class="p-2 bg-yellow-100 rounded-lg">
|
|
||||||
<svg
|
|
||||||
class="w-6 h-6 text-yellow-600"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="ml-4">
|
|
||||||
<p class="text-sm font-medium text-gray-600">Active Sensors</p>
|
|
||||||
<p class="text-lg font-semibold text-gray-900">{{ sensorStore.activeSensors }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="bg-white rounded-lg shadow p-6">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div class="p-2 bg-purple-100 rounded-lg">
|
|
||||||
<svg
|
|
||||||
class="w-6 h-6 text-purple-600"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="ml-4">
|
|
||||||
<p class="text-sm font-medium text-gray-600">Total Readings</p>
|
|
||||||
<p class="text-lg font-semibold text-gray-900">
|
|
||||||
{{ formatNumber(sensorStore.totalReadings) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Loading States -->
|
|
||||||
<div v-if="isLoading" class="bg-white rounded-lg shadow p-6 mb-8">
|
|
||||||
<div class="flex items-center justify-center">
|
|
||||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
|
||||||
<p class="ml-3 text-gray-600">Loading API data...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Error States -->
|
|
||||||
<div v-if="apiError" class="bg-red-50 border border-red-200 rounded-lg p-4 mb-8">
|
|
||||||
<div class="flex">
|
|
||||||
<div class="flex-shrink-0">
|
|
||||||
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="ml-3">
|
|
||||||
<h3 class="text-sm font-medium text-red-800">API Error</h3>
|
|
||||||
<p class="mt-1 text-sm text-red-700">{{ apiError }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Energy Metrics Summary -->
|
|
||||||
<div class="bg-white rounded-lg shadow p-6 mb-8">
|
|
||||||
<h2 class="text-xl font-semibold text-gray-900 mb-4">Energy Metrics</h2>
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium text-gray-600 mb-2">Current Energy Consumption</p>
|
|
||||||
<p class="text-2xl font-bold text-blue-600">
|
|
||||||
{{ energyStore.currentEnergyValue.toFixed(2) }} kWh
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium text-gray-600 mb-2">Average Energy Usage</p>
|
|
||||||
<p class="text-2xl font-bold text-green-600">
|
|
||||||
{{ energyStore.averageEnergyUsage.toFixed(2) }} kWh
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p class="text-sm font-medium text-gray-600 mb-2">Total Consumption</p>
|
|
||||||
<p class="text-2xl font-bold text-purple-600">
|
|
||||||
{{ energyStore.currentConsumption.toFixed(2) }} kWh
|
|
||||||
</p>
|
|
||||||
<p class="text-xs text-gray-500 mt-1">Cumulative</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Sensors Section -->
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
|
||||||
<div class="bg-white rounded-lg shadow">
|
|
||||||
<div class="px-6 py-4 border-b border-gray-200">
|
|
||||||
<h2 class="text-lg font-semibold text-gray-900">API Sensors</h2>
|
|
||||||
</div>
|
|
||||||
<div class="p-6">
|
|
||||||
<div v-if="apiSensors.length === 0" class="text-center text-gray-500 py-8">
|
|
||||||
No sensors found from API
|
|
||||||
</div>
|
|
||||||
<div v-else class="space-y-3">
|
|
||||||
<div
|
|
||||||
v-for="sensor in apiSensors"
|
|
||||||
:key="sensor.sensor_id"
|
|
||||||
class="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<p class="font-medium text-gray-900">{{ sensor.sensor_id }}</p>
|
|
||||||
<p class="text-sm text-gray-500">{{ sensor.room || 'No room assigned' }}</p>
|
|
||||||
<p class="text-xs text-gray-400">
|
|
||||||
{{ sensor.sensor_type }} • {{ sensor.total_readings }} readings
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<span
|
|
||||||
:class="[
|
|
||||||
'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium',
|
|
||||||
sensor.status === 'online'
|
|
||||||
? 'bg-green-100 text-green-800'
|
|
||||||
: sensor.status === 'offline'
|
|
||||||
? 'bg-red-100 text-red-800'
|
|
||||||
: 'bg-yellow-100 text-yellow-800',
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
{{ sensor.status }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Rooms Section -->
|
|
||||||
<div class="bg-white rounded-lg shadow">
|
|
||||||
<div class="px-6 py-4 border-b border-gray-200">
|
|
||||||
<h2 class="text-lg font-semibold text-gray-900">API Rooms</h2>
|
|
||||||
</div>
|
|
||||||
<div class="p-6">
|
|
||||||
<div v-if="apiRooms.length === 0" class="text-center text-gray-500 py-8">
|
|
||||||
No rooms found from API
|
|
||||||
</div>
|
|
||||||
<div v-else class="space-y-3">
|
|
||||||
<div
|
|
||||||
v-for="room in apiRooms"
|
|
||||||
:key="room.name || room.room"
|
|
||||||
class="p-3 bg-gray-50 rounded-lg"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between mb-2">
|
|
||||||
<p class="font-medium text-gray-900">{{ room.name || room.room }}</p>
|
|
||||||
<span class="text-sm text-gray-500">{{ room.sensor_count }} sensors</span>
|
|
||||||
</div>
|
|
||||||
<div class="text-xs text-gray-600 space-y-1">
|
|
||||||
<p v-if="room.sensor_types">Types: {{ room.sensor_types.join(', ') }}</p>
|
|
||||||
<div v-if="room.latest_metrics">
|
|
||||||
<span v-if="room.latest_metrics.energy" class="mr-4">
|
|
||||||
Energy: {{ room.latest_metrics.energy.current }}
|
|
||||||
{{ room.latest_metrics.energy.unit }}
|
|
||||||
</span>
|
|
||||||
<span v-if="room.latest_metrics.co2">
|
|
||||||
CO2: {{ room.latest_metrics.co2.current }} {{ room.latest_metrics.co2.unit }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p v-else class="text-gray-400 italic">No metrics available</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
|
||||||
<div class="bg-white rounded-lg shadow p-6">
|
<div class="bg-white rounded-lg shadow p-6">
|
||||||
<h2 class="text-lg font-semibold text-gray-900 mb-4">API Actions</h2>
|
<div class="flex items-center">
|
||||||
<div class="flex flex-wrap gap-3">
|
<div class="p-2 bg-blue-100 rounded-lg">
|
||||||
<button
|
|
||||||
@click="refreshAllData"
|
|
||||||
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
|
||||||
:disabled="isLoading"
|
|
||||||
>
|
|
||||||
<svg
|
<svg
|
||||||
v-if="!isLoading"
|
class="w-6 h-6 text-blue-600"
|
||||||
class="-ml-1 mr-2 h-4 w-4"
|
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@@ -265,41 +24,284 @@
|
|||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
></path>
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
<div
|
</div>
|
||||||
v-else
|
<div class="ml-4">
|
||||||
class="animate-spin -ml-1 mr-2 h-4 w-4 border-2 border-white border-t-transparent rounded-full"
|
<p class="text-sm font-medium text-gray-600">System Status</p>
|
||||||
></div>
|
<p
|
||||||
Refresh All Data
|
class="text-lg font-semibold"
|
||||||
</button>
|
:class="healthStatus?.status === 'healthy' ? 'text-green-600' : 'text-red-600'"
|
||||||
|
>
|
||||||
<button
|
{{ healthStatus?.status || 'Unknown' }}
|
||||||
@click="fetchSensorsOnly"
|
</p>
|
||||||
class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
</div>
|
||||||
:disabled="isLoading"
|
|
||||||
>
|
|
||||||
Fetch Sensors
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
@click="fetchRoomsOnly"
|
|
||||||
class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
|
||||||
:disabled="isLoading"
|
|
||||||
>
|
|
||||||
Fetch Rooms
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
@click="fetchAnalyticsOnly"
|
|
||||||
class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
|
||||||
:disabled="isLoading"
|
|
||||||
>
|
|
||||||
Fetch Analytics
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-lg shadow p-6">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="p-2 bg-green-100 rounded-lg">
|
||||||
|
<svg
|
||||||
|
class="w-6 h-6 text-green-600"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M13 10V3L4 14h7v7l9-11h-7z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-4">
|
||||||
|
<p class="text-sm font-medium text-gray-600">Total Sensors</p>
|
||||||
|
<p class="text-lg font-semibold text-gray-900">{{ sensorStore.totalSensors }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-lg shadow p-6">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="p-2 bg-yellow-100 rounded-lg">
|
||||||
|
<svg
|
||||||
|
class="w-6 h-6 text-yellow-600"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-4">
|
||||||
|
<p class="text-sm font-medium text-gray-600">Active Sensors</p>
|
||||||
|
<p class="text-lg font-semibold text-gray-900">{{ sensorStore.activeSensors }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-white rounded-lg shadow p-6">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="p-2 bg-purple-100 rounded-lg">
|
||||||
|
<svg
|
||||||
|
class="w-6 h-6 text-purple-600"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-4">
|
||||||
|
<p class="text-sm font-medium text-gray-600">Total Readings</p>
|
||||||
|
<p class="text-lg font-semibold text-gray-900">
|
||||||
|
{{ formatNumber(sensorStore.totalReadings) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading States -->
|
||||||
|
<div v-if="isLoading" class="bg-white rounded-lg shadow p-6 mb-8">
|
||||||
|
<div class="flex items-center justify-center">
|
||||||
|
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||||
|
<p class="ml-3 text-gray-600">Loading API data...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error States -->
|
||||||
|
<div v-if="apiError" class="bg-red-50 border border-red-200 rounded-lg p-4 mb-8">
|
||||||
|
<div class="flex">
|
||||||
|
<div class="flex-shrink-0">
|
||||||
|
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ml-3">
|
||||||
|
<h3 class="text-sm font-medium text-red-800">API Error</h3>
|
||||||
|
<p class="mt-1 text-sm text-red-700">{{ apiError }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Energy Metrics Summary -->
|
||||||
|
<div class="bg-white rounded-lg shadow p-6 mb-8">
|
||||||
|
<h2 class="text-xl font-semibold text-gray-900 mb-4">Energy Metrics</h2>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-gray-600 mb-2">Current Energy Consumption</p>
|
||||||
|
<p class="text-2xl font-bold text-blue-600">
|
||||||
|
{{ energyStore.currentEnergyValue.toFixed(2) }} kWh
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-gray-600 mb-2">Average Energy Usage</p>
|
||||||
|
<p class="text-2xl font-bold text-green-600">
|
||||||
|
{{ energyStore.averageEnergyUsage.toFixed(2) }} kWh
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="text-sm font-medium text-gray-600 mb-2">Total Consumption</p>
|
||||||
|
<p class="text-2xl font-bold text-purple-600">
|
||||||
|
{{ energyStore.currentConsumption.toFixed(2) }} kWh
|
||||||
|
</p>
|
||||||
|
<p class="text-xs text-gray-500 mt-1">Cumulative</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sensors Section -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-8">
|
||||||
|
<div class="bg-white rounded-lg shadow">
|
||||||
|
<div class="px-6 py-4 border-b border-gray-200">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-900">API Sensors</h2>
|
||||||
|
</div>
|
||||||
|
<div class="p-6">
|
||||||
|
<div v-if="apiSensors.length === 0" class="text-center text-gray-500 py-8">
|
||||||
|
No sensors found from API
|
||||||
|
</div>
|
||||||
|
<div v-else class="space-y-3">
|
||||||
|
<div
|
||||||
|
v-for="sensor in apiSensors"
|
||||||
|
:key="sensor.sensor_id"
|
||||||
|
class="flex items-center justify-between p-3 bg-gray-50 rounded-lg"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<p class="font-medium text-gray-900">{{ sensor.sensor_id }}</p>
|
||||||
|
<p class="text-sm text-gray-500">{{ sensor.room || 'No room assigned' }}</p>
|
||||||
|
<p class="text-xs text-gray-400">
|
||||||
|
{{ sensor.sensor_type }} • {{ sensor.total_readings }} readings
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
'inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium',
|
||||||
|
sensor.status === 'online'
|
||||||
|
? 'bg-green-100 text-green-800'
|
||||||
|
: sensor.status === 'offline'
|
||||||
|
? 'bg-red-100 text-red-800'
|
||||||
|
: 'bg-yellow-100 text-yellow-800',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ sensor.status }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rooms Section -->
|
||||||
|
<div class="bg-white rounded-lg shadow">
|
||||||
|
<div class="px-6 py-4 border-b border-gray-200">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-900">API Rooms</h2>
|
||||||
|
</div>
|
||||||
|
<div class="p-6">
|
||||||
|
<div v-if="apiRooms.length === 0" class="text-center text-gray-500 py-8">
|
||||||
|
No rooms found from API
|
||||||
|
</div>
|
||||||
|
<div v-else class="space-y-3">
|
||||||
|
<div
|
||||||
|
v-for="room in apiRooms"
|
||||||
|
:key="room.name || room.room"
|
||||||
|
class="p-3 bg-gray-50 rounded-lg"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<p class="font-medium text-gray-900">{{ room.name || room.room }}</p>
|
||||||
|
<span class="text-sm text-gray-500">{{ room.sensor_count }} sensors</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-600 space-y-1">
|
||||||
|
<p v-if="room.sensor_types">Types: {{ room.sensor_types.join(', ') }}</p>
|
||||||
|
<div v-if="room.latest_metrics">
|
||||||
|
<span v-if="room.latest_metrics.energy" class="mr-4">
|
||||||
|
Energy: {{ room.latest_metrics.energy.current }}
|
||||||
|
{{ room.latest_metrics.energy.unit }}
|
||||||
|
</span>
|
||||||
|
<span v-if="room.latest_metrics.co2">
|
||||||
|
CO2: {{ room.latest_metrics.co2.current }} {{ room.latest_metrics.co2.unit }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p v-else class="text-gray-400 italic">No metrics available</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="bg-white rounded-lg shadow p-6">
|
||||||
|
<h2 class="text-lg font-semibold text-gray-900 mb-4">API Actions</h2>
|
||||||
|
<div class="flex flex-wrap gap-3">
|
||||||
|
<button
|
||||||
|
@click="refreshAllData"
|
||||||
|
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||||
|
:disabled="isLoading"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
v-if="!isLoading"
|
||||||
|
class="-ml-1 mr-2 h-4 w-4"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="animate-spin -ml-1 mr-2 h-4 w-4 border-2 border-white border-t-transparent rounded-full"
|
||||||
|
></div>
|
||||||
|
Refresh All Data
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="fetchSensorsOnly"
|
||||||
|
class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||||
|
:disabled="isLoading"
|
||||||
|
>
|
||||||
|
Fetch Sensors
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="fetchRoomsOnly"
|
||||||
|
class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||||
|
:disabled="isLoading"
|
||||||
|
>
|
||||||
|
Fetch Rooms
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="fetchAnalyticsOnly"
|
||||||
|
class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||||
|
:disabled="isLoading"
|
||||||
|
>
|
||||||
|
Fetch Analytics
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,29 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="space-y-6">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 pb-20">
|
||||||
<!-- Filter Controls Row -->
|
<div class="space-y-6">
|
||||||
<!--div class="flex flex-col sm:flex-row gap-4 mb-6">
|
<div class="grid grid-cols-2 sm:grid-cols-3 gap-4">
|
||||||
<select class="px-4 py-2 border border-gray-200 rounded-lg bg-white">
|
|
||||||
<option>Timeframe: All-time</option>
|
|
||||||
</select>
|
|
||||||
<select class="px-4 py-2 border border-gray-200 rounded-lg bg-white">
|
|
||||||
<option>People: All</option>
|
|
||||||
</select>
|
|
||||||
<select class="px-4 py-2 border border-gray-200 rounded-lg bg-white">
|
|
||||||
<option>Topic: All</option>
|
|
||||||
</select>
|
|
||||||
</div-->
|
|
||||||
|
|
||||||
<!-- Top Metric Cards Row -->
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 min-h-96">
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-x-4 gap-y-3">
|
|
||||||
<MetricCard title="Current Energy" :content="currentEnergyValue" details="kWh" />
|
<MetricCard title="Current Energy" :content="currentEnergyValue" details="kWh" />
|
||||||
|
<MetricCard title="Average Usage" :content="averageEnergyUsage" details="kWh" />
|
||||||
<MetricCard
|
<MetricCard
|
||||||
title="Connection Status"
|
title="Connection Status"
|
||||||
:content="websocketStore.isConnected ? 'Connected' : 'Disconnected'"
|
:content="websocketStore.isConnected ? 'Connected' : 'Disconnected'"
|
||||||
/>
|
/>
|
||||||
<MetricCard title="Average Usage" :content="averageEnergyUsage" details="kWh" />
|
|
||||||
<MetricCard title="Average CO2" :content="averageCO2" details="ppm" />
|
|
||||||
<MetricCard title="Max CO2" :content="maxCO2" details="ppm" />
|
|
||||||
<GraphMetricCard
|
<GraphMetricCard
|
||||||
title="Real-time Energy"
|
title="Real-time Energy"
|
||||||
:content="currentEnergyValue"
|
:content="currentEnergyValue"
|
||||||
@@ -31,19 +15,16 @@
|
|||||||
:trend-data="energyStore.energyHistory.slice(-8)"
|
:trend-data="energyStore.energyHistory.slice(-8)"
|
||||||
trend-direction="neutral"
|
trend-direction="neutral"
|
||||||
/>
|
/>
|
||||||
|
<GraphMetricCard title="Average CO2" :content="averageCO2" details="ppm" />
|
||||||
|
<GraphMetricCard title="Max CO2" :content="maxCO2" details="ppm" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<SensorConsumptionTable />
|
||||||
<RealtimeEnergyChartCard title="Month" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Charts and Knowledge Cards Row -->
|
<div class="space-y-6">
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
<RealtimeEnergyChartCard title="Month" />
|
||||||
<SensorConsumptionTable />
|
<RoomMetricsCard />
|
||||||
<div class="grid grid-cols-1 gap-4">
|
<AirQualityCard />
|
||||||
<RoomMetricsCard />
|
|
||||||
<AirQualityCard />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -66,7 +47,6 @@ const sensorStore = useSensorStore()
|
|||||||
const websocketStore = useWebSocketStore()
|
const websocketStore = useWebSocketStore()
|
||||||
const settingsStore = useSettingsStore()
|
const settingsStore = useSettingsStore()
|
||||||
|
|
||||||
// Use energy store for aggregated values across all sensors
|
|
||||||
const currentEnergyValue = computed(() => {
|
const currentEnergyValue = computed(() => {
|
||||||
return energyStore.currentEnergyValue.toFixed(2)
|
return energyStore.currentEnergyValue.toFixed(2)
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user