Add a summary of real-time metrics per room, including energy, CO2, sensor count, and occupancy. Sensor cards now display live readings from the store instead of mock data. Refactor card logic for reactivity and update navigation colors for clarity.
229 lines
7.2 KiB
Vue
229 lines
7.2 KiB
Vue
<template>
|
|
<div class="fixed inset-0 z-50 flex items-center justify-center">
|
|
<!-- Backdrop -->
|
|
<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"
|
|
>
|
|
<!-- Header -->
|
|
<div class="p-6 border-b border-gray-100">
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h3 class="text-lg font-medium text-gray-900">{{ action.name }}</h3>
|
|
<p class="text-sm text-gray-600">{{ sensor.name }} • {{ sensor.room }}</p>
|
|
</div>
|
|
<button
|
|
@click="$emit('close')"
|
|
class="p-2 hover:bg-gray-100 rounded-full transition-colors"
|
|
>
|
|
<svg class="w-5 h-5 text-gray-400" fill="currentColor" viewBox="0 0 24 24">
|
|
<path
|
|
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Content -->
|
|
<div class="p-6">
|
|
<div class="space-y-4">
|
|
<!-- Range Input (for numeric parameters) -->
|
|
<div v-if="action.type === 'adjust' && hasNumericRange">
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
{{ action.name }}
|
|
</label>
|
|
<div class="space-y-3">
|
|
<input
|
|
v-model.number="numericValue"
|
|
type="range"
|
|
:min="action.parameters.min"
|
|
:max="action.parameters.max"
|
|
:step="action.parameters.step"
|
|
class="w-full h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer slider"
|
|
/>
|
|
<div class="flex justify-between text-sm text-gray-600">
|
|
<span>{{ action.parameters.min }}</span>
|
|
<span class="font-medium">{{ numericValue }}{{ getUnit() }}</span>
|
|
<span>{{ action.parameters.max }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Option Selection -->
|
|
<div v-if="action.type === 'adjust' && action.parameters?.options">
|
|
<label class="block text-sm font-medium text-gray-700 mb-2">
|
|
{{ action.name }}
|
|
</label>
|
|
<div class="grid grid-cols-2 gap-2">
|
|
<button
|
|
v-for="option in action.parameters.options"
|
|
:key="option"
|
|
@click="selectedOption = option"
|
|
class="px-3 py-2 border rounded-lg text-sm font-medium transition-colors"
|
|
:class="
|
|
selectedOption === option
|
|
? 'bg-blue-500 text-white border-blue-500'
|
|
: 'bg-white text-gray-700 border-gray-200 hover:bg-gray-50'
|
|
"
|
|
>
|
|
{{ option }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Toggle Action -->
|
|
<div v-if="action.type === 'toggle'">
|
|
<div class="flex items-center justify-between">
|
|
<span class="text-sm font-medium text-gray-700">{{ action.name }}</span>
|
|
<button
|
|
@click="toggleValue = !toggleValue"
|
|
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
|
|
:class="toggleValue ? 'bg-blue-600' : 'bg-gray-200'"
|
|
>
|
|
<span
|
|
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
|
|
:class="toggleValue ? 'translate-x-6' : 'translate-x-1'"
|
|
/>
|
|
</button>
|
|
</div>
|
|
<p class="text-sm text-gray-500 mt-1">
|
|
Current state: {{ toggleValue ? 'ON' : 'OFF' }}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Trigger Action -->
|
|
<div v-if="action.type === 'trigger'">
|
|
<div class="bg-gray-50 rounded-lg p-4">
|
|
<div class="flex items-center gap-3">
|
|
<span class="text-2xl">{{ action.icon }}</span>
|
|
<div>
|
|
<div class="font-medium text-gray-900">{{ action.name }}</div>
|
|
<div class="text-sm text-gray-600">Click execute to perform this action</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Footer -->
|
|
<div class="px-6 py-4 bg-gray-50 rounded-b-2xl flex gap-3">
|
|
<button
|
|
@click="$emit('close')"
|
|
class="flex-1 px-4 py-2 border border-gray-200 rounded-lg text-gray-700 font-medium hover:bg-gray-100 transition-colors"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
@click="executeAction"
|
|
:disabled="isExecuting"
|
|
class="flex-1 px-4 py-2 bg-blue-600 text-white rounded-lg font-medium hover:bg-blue-700 disabled:bg-blue-400 disabled:cursor-not-allowed transition-colors"
|
|
>
|
|
{{ isExecuting ? 'Executing...' : 'Execute' }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, watch } from 'vue'
|
|
|
|
const props = defineProps<{
|
|
sensor: any
|
|
action: any
|
|
}>()
|
|
|
|
const emit = defineEmits<{
|
|
execute: [sensorId: string, actionId: string, parameters: any]
|
|
close: []
|
|
}>()
|
|
|
|
const isExecuting = ref(false)
|
|
const numericValue = ref(0)
|
|
const selectedOption = ref('')
|
|
const toggleValue = ref(false)
|
|
|
|
// Initialize default values
|
|
watch(
|
|
() => props.action,
|
|
(action) => {
|
|
if (action) {
|
|
if (action.parameters?.min !== undefined) {
|
|
numericValue.value = action.parameters.min
|
|
}
|
|
if (action.parameters?.options?.length > 0) {
|
|
selectedOption.value = action.parameters.options[0]
|
|
}
|
|
toggleValue.value = false
|
|
}
|
|
},
|
|
{ immediate: true },
|
|
)
|
|
|
|
const hasNumericRange = computed(() => {
|
|
return (
|
|
props.action.parameters?.min !== undefined &&
|
|
props.action.parameters?.max !== undefined &&
|
|
!props.action.parameters?.options
|
|
)
|
|
})
|
|
|
|
const getUnit = () => {
|
|
if (props.action.id === 'temp_adjust') return '°C'
|
|
if (props.action.id === 'brightness') return '%'
|
|
if (props.action.id === 'fan_speed') return ''
|
|
return ''
|
|
}
|
|
|
|
const executeAction = async () => {
|
|
isExecuting.value = true
|
|
|
|
const parameters: any = {}
|
|
|
|
if (props.action.type === 'adjust') {
|
|
if (hasNumericRange.value) {
|
|
parameters.value = numericValue.value
|
|
} else if (props.action.parameters?.options) {
|
|
parameters.value = selectedOption.value
|
|
}
|
|
} else if (props.action.type === 'toggle') {
|
|
parameters.enabled = toggleValue.value
|
|
}
|
|
|
|
try {
|
|
emit('execute', props.sensor.id, props.action.id, parameters)
|
|
} catch (error) {
|
|
console.error('Failed to execute action:', error)
|
|
} finally {
|
|
isExecuting.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
/* Custom slider styling */
|
|
.slider::-webkit-slider-thumb {
|
|
appearance: none;
|
|
height: 20px;
|
|
width: 20px;
|
|
border-radius: 50%;
|
|
background: #3b82f6;
|
|
cursor: pointer;
|
|
box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.2);
|
|
}
|
|
|
|
.slider::-moz-range-thumb {
|
|
height: 20px;
|
|
width: 20px;
|
|
border-radius: 50%;
|
|
background: #3b82f6;
|
|
cursor: pointer;
|
|
border: none;
|
|
box-shadow: 0 0 2px 0 rgba(0, 0, 0, 0.2);
|
|
}
|
|
</style>
|