Files
sa4cps-frontend/src/components/modals/ActionModal.vue
rafaeldpsilva 55a2d6d097 Show real-time room metrics and improve sensor cards
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.
2025-09-03 16:34:04 +01:00

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>