import React, { useState, useEffect } from 'react'; import { Dialog, DialogTitle, DialogContent, DialogActions, TextField, Button, Box, FormControl, InputLabel, Select, MenuItem, ToggleButton, ToggleButtonGroup, Typography, Divider, Slider, Switch, FormControlLabel, CircularProgress, IconButton, Paper, Chip } from '@mui/material'; import { useI18n } from './I18nContext'; const DAYS_KEYS = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']; const OPERATORS = [ { value: '>', label: '>' }, { value: '<', label: '<' }, { value: '>=', label: '≥' }, { value: '<=', label: '≤' }, { value: '==', label: '=' } ]; // Single sensor condition component function SensorCondition({ condition, sensors, onChange, onRemove, disabled }) { const selectedSensor = sensors.find(s => s.id === condition.sensor); const isStateSensor = selectedSensor?.type === 'output-state'; // When sensor changes, reset operator to appropriate default const handleSensorChange = (newSensorId) => { const newSensor = sensors.find(s => s.id === newSensorId); const newIsState = newSensor?.type === 'output-state'; onChange({ ...condition, sensor: newSensorId, operator: newIsState ? '==' : '>', value: newIsState ? 1 : (condition.value ?? 25) }); }; return ( Sensor {isStateSensor ? ( // State sensor: is on / is off ) : ( // Value sensor: numeric comparison <> onChange({ ...condition, value: Number(e.target.value) })} sx={{ width: 80 }} disabled={disabled} /> )} {onRemove && ( )} ); } export default function RuleEditor({ open, rule, onSave, onClose, sensors = [], outputs = [], colorTags: availableColorTags = [], saving }) { const { t } = useI18n(); const [name, setName] = useState(''); const [selectedTags, setSelectedTags] = useState([]); // array of tag ids // Scheduled time state (trigger at specific time) const [useScheduledTime, setUseScheduledTime] = useState(false); const [scheduledTime, setScheduledTime] = useState('08:00'); const [scheduledDays, setScheduledDays] = useState(['mon', 'tue', 'wed', 'thu', 'fri']); // Time range state (active during window) const [useTimeRange, setUseTimeRange] = useState(false); const [timeStart, setTimeStart] = useState('08:00'); const [timeEnd, setTimeEnd] = useState('18:00'); const [timeRangeDays, setTimeRangeDays] = useState(['mon', 'tue', 'wed', 'thu', 'fri']); // Sensor conditions state const [useSensors, setUseSensors] = useState(false); const [sensorConditions, setSensorConditions] = useState([{ sensor: '', operator: '>', value: 25 }]); const [sensorLogic, setSensorLogic] = useState('and'); // 'and' or 'or' // Action state const [actionType, setActionType] = useState('toggle'); const [target, setTarget] = useState(''); const [toggleState, setToggleState] = useState(true); const [outputLevel, setOutputLevel] = useState(5); // 1-10 for port outputs const [duration, setDuration] = useState(15); // Check if target is a binary (on/off) output or level (1-10) output const selectedOutput = outputs.find(o => o.id === target); const isBinaryOutput = selectedOutput?.type === 'plug' || selectedOutput?.type === 'virtual'; // Reset form when rule changes or dialog opens useEffect(() => { if (rule) { setName(rule.name); // colorTags can be array or single value for backwards compat const tags = rule.colorTags || (rule.colorTag ? [rule.colorTag] : []); setSelectedTags(Array.isArray(tags) ? tags : []); // Parse trigger const trigger = rule.trigger || {}; // Scheduled time setUseScheduledTime(!!trigger.scheduledTime); if (trigger.scheduledTime) { setScheduledTime(trigger.scheduledTime.time || '08:00'); setScheduledDays(trigger.scheduledTime.days || []); } // Time range setUseTimeRange(!!trigger.timeRange); if (trigger.timeRange) { setTimeStart(trigger.timeRange.start || '08:00'); setTimeEnd(trigger.timeRange.end || '18:00'); setTimeRangeDays(trigger.timeRange.days || []); } setUseSensors(!!trigger.sensors && trigger.sensors.length > 0); if (trigger.sensors && trigger.sensors.length > 0) { setSensorConditions(trigger.sensors); setSensorLogic(trigger.sensorLogic || 'and'); } // Parse action setActionType(rule.action?.type || 'toggle'); setTarget(rule.action?.target || ''); if (rule.action?.type === 'toggle') { setToggleState(rule.action?.state ?? true); setOutputLevel(rule.action?.level ?? 5); } else { setDuration(rule.action?.duration || 15); } } else { // Reset to defaults setName(''); setSelectedTags([]); setUseScheduledTime(true); setScheduledTime('08:00'); setScheduledDays(['mon', 'tue', 'wed', 'thu', 'fri']); setUseTimeRange(false); setTimeStart('08:00'); setTimeEnd('18:00'); setTimeRangeDays(['mon', 'tue', 'wed', 'thu', 'fri']); setUseSensors(false); setSensorConditions([{ sensor: sensors[0]?.id || '', operator: '>', value: 25 }]); setSensorLogic('and'); setActionType('toggle'); setTarget(outputs[0]?.id || ''); setToggleState(true); setOutputLevel(5); setDuration(15); } }, [rule, open, sensors, outputs]); // Set default sensor/output when lists load useEffect(() => { if (sensorConditions[0]?.sensor === '' && sensors.length > 0) { setSensorConditions([{ ...sensorConditions[0], sensor: sensors[0].id }]); } if (!target && outputs.length > 0) setTarget(outputs[0].id); }, [sensors, outputs, sensorConditions, target]); const handleScheduledDaysChange = (event, newDays) => { if (newDays.length > 0) setScheduledDays(newDays); }; const handleTimeRangeDaysChange = (event, newDays) => { if (newDays.length > 0) setTimeRangeDays(newDays); }; const addSensorCondition = () => { setSensorConditions([...sensorConditions, { sensor: sensors[0]?.id || '', operator: '>', value: 25 }]); }; const updateSensorCondition = (index, newCondition) => { const updated = [...sensorConditions]; updated[index] = newCondition; setSensorConditions(updated); }; const removeSensorCondition = (index) => { if (sensorConditions.length > 1) { setSensorConditions(sensorConditions.filter((_, i) => i !== index)); } }; const handleSave = () => { const selectedOutput = outputs.find(o => o.id === target); // Build trigger object const trigger = {}; if (useScheduledTime) { trigger.scheduledTime = { time: scheduledTime, days: scheduledDays }; } if (useTimeRange) { trigger.timeRange = { start: timeStart, end: timeEnd, days: timeRangeDays }; } if (useSensors && sensorConditions.length > 0) { trigger.sensors = sensorConditions.map(c => ({ ...c, sensorLabel: sensors.find(s => s.id === c.sensor)?.label })); trigger.sensorLogic = sensorLogic; } const isBinaryTarget = selectedOutput?.type === 'plug' || selectedOutput?.type === 'virtual'; // Build action object based on output type let action; if (actionType === 'toggle') { action = { type: 'toggle', target, targetLabel: selectedOutput?.label, ...(isBinaryTarget ? { state: toggleState } : { level: outputLevel }) }; } else { action = { type: 'keepOn', target, targetLabel: selectedOutput?.label, duration, ...(isBinaryTarget ? {} : { level: outputLevel }) }; } const ruleData = { name, trigger, action, colorTags: selectedTags }; onSave(ruleData); }; const isValid = name.trim().length > 0 && (useScheduledTime || useTimeRange || useSensors) && (!useScheduledTime || scheduledDays.length > 0) && (!useTimeRange || timeRangeDays.length > 0) && (!useSensors || sensorConditions.every(c => c.sensor)) && target; return ( {rule ? t('ruleEditor.editTitle') : t('ruleEditor.createTitle')} {/* Rule Name */} setName(e.target.value)} fullWidth placeholder={t('ruleEditor.ruleNamePlaceholder')} disabled={saving} /> {/* Color Tag Picker (multi-select) */} Tags: setSelectedTags([])} sx={{ width: 24, height: 24, borderRadius: '50%', bgcolor: '#504945', cursor: 'pointer', border: selectedTags.length === 0 ? '3px solid #ebdbb2' : '2px solid #504945', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '0.7rem', '&:hover': { opacity: 0.8 } }} > ✕ {availableColorTags.map(tag => ( { if (selectedTags.includes(tag.id)) { setSelectedTags(selectedTags.filter(t => t !== tag.id)); } else { setSelectedTags([...selectedTags, tag.id]); } }} sx={{ width: 24, height: 24, borderRadius: '50%', bgcolor: tag.color, cursor: 'pointer', border: selectedTags.includes(tag.id) ? '3px solid #ebdbb2' : '2px solid transparent', display: 'flex', alignItems: 'center', justifyContent: 'center', '&:hover': { opacity: 0.8 } }} > {selectedTags.includes(tag.id) && } ))} {/* TRIGGERS SECTION */} {t('ruleEditor.triggersSection')} {/* Scheduled Time Trigger (fires at exact time) */} setUseScheduledTime(e.target.checked)} disabled={saving} /> } label={{t('ruleEditor.scheduledTime')}} /> {useScheduledTime && ( setScheduledTime(e.target.value)} InputLabelProps={{ shrink: true }} size="small" sx={{ width: 150 }} disabled={saving} /> {t('ruleEditor.days')} {DAYS_KEYS.map(key => ( {t(`days.${key}`)} ))} )} {/* Time Range Trigger (active within window) */} setUseTimeRange(e.target.checked)} disabled={saving} /> } label={⏰ Time Range (active during window)} /> {useTimeRange && ( setTimeStart(e.target.value)} InputLabelProps={{ shrink: true }} size="small" disabled={saving} /> to setTimeEnd(e.target.value)} InputLabelProps={{ shrink: true }} size="small" disabled={saving} /> {t('ruleEditor.days')} {DAYS_KEYS.map(key => ( {t(`days.${key}`)} ))} )} {/* Sensor Conditions Trigger */} setUseSensors(e.target.checked)} disabled={saving || sensors.length === 0} /> } label={ 📊 Sensor Conditions {sensors.length === 0 && '(no sensors available)'} } /> {useSensors && ( {sensorConditions.length > 1 && ( Combine conditions with: v && setSensorLogic(v)} size="small" disabled={saving} > AND OR )} {sensorConditions.map((cond, i) => ( {i > 0 && ( )} updateSensorCondition(i, newCond)} onRemove={sensorConditions.length > 1 ? () => removeSensorCondition(i) : null} disabled={saving} /> ))} )} {/* ACTION SECTION */} ACTION (What to do) Action Type Target Output {/* Toggle action controls */} {actionType === 'toggle' && ( {isBinaryOutput ? ( // Binary output: On/Off switch setToggleState(e.target.checked)} color="primary" disabled={saving} /> } label={toggleState ? 'Turn ON' : 'Turn OFF'} /> ) : ( // Level output: 1-10 slider Set Level: {outputLevel} setOutputLevel(val)} min={1} max={10} step={1} marks={[ { value: 1, label: '1' }, { value: 5, label: '5' }, { value: 10, label: '10' } ]} valueLabelDisplay="auto" disabled={saving} /> )} )} {/* Keep On action controls */} {actionType === 'keepOn' && ( {!isBinaryOutput && ( // Level for port outputs Set Level: {outputLevel} setOutputLevel(val)} min={1} max={10} step={1} marks={[ { value: 1, label: '1' }, { value: 5, label: '5' }, { value: 10, label: '10' } ]} valueLabelDisplay="auto" disabled={saving} /> )} Duration: {duration} minutes setDuration(val)} min={1} max={120} marks={[ { value: 1, label: '1m' }, { value: 30, label: '30m' }, { value: 60, label: '1h' }, { value: 120, label: '2h' } ]} valueLabelDisplay="auto" disabled={saving} /> )} ); }