732 lines
34 KiB
JavaScript
732 lines
34 KiB
JavaScript
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 (
|
||
<Paper sx={{ p: 1.5, display: 'flex', alignItems: 'center', gap: 1, bgcolor: 'background.default' }}>
|
||
<FormControl size="small" sx={{ minWidth: 180 }}>
|
||
<InputLabel>Sensor</InputLabel>
|
||
<Select
|
||
value={condition.sensor || ''}
|
||
label="Sensor"
|
||
onChange={(e) => handleSensorChange(e.target.value)}
|
||
disabled={disabled}
|
||
>
|
||
{sensors.map(s => (
|
||
<MenuItem key={s.id} value={s.id}>{s.label}</MenuItem>
|
||
))}
|
||
</Select>
|
||
</FormControl>
|
||
|
||
{isStateSensor ? (
|
||
// State sensor: is on / is off
|
||
<FormControl size="small" sx={{ minWidth: 100 }}>
|
||
<Select
|
||
value={condition.value === 1 ? 'on' : 'off'}
|
||
onChange={(e) => onChange({ ...condition, operator: '==', value: e.target.value === 'on' ? 1 : 0 })}
|
||
disabled={disabled}
|
||
>
|
||
<MenuItem value="on">is ON</MenuItem>
|
||
<MenuItem value="off">is OFF</MenuItem>
|
||
</Select>
|
||
</FormControl>
|
||
) : (
|
||
// Value sensor: numeric comparison
|
||
<>
|
||
<FormControl size="small" sx={{ minWidth: 70 }}>
|
||
<Select
|
||
value={condition.operator || '>'}
|
||
onChange={(e) => onChange({ ...condition, operator: e.target.value })}
|
||
disabled={disabled}
|
||
>
|
||
{OPERATORS.map(op => (
|
||
<MenuItem key={op.value} value={op.value}>{op.label}</MenuItem>
|
||
))}
|
||
</Select>
|
||
</FormControl>
|
||
<TextField
|
||
size="small"
|
||
type="number"
|
||
value={condition.value ?? ''}
|
||
onChange={(e) => onChange({ ...condition, value: Number(e.target.value) })}
|
||
sx={{ width: 80 }}
|
||
disabled={disabled}
|
||
/>
|
||
</>
|
||
)}
|
||
|
||
{onRemove && (
|
||
<IconButton size="small" onClick={onRemove} disabled={disabled}>
|
||
❌
|
||
</IconButton>
|
||
)}
|
||
</Paper>
|
||
);
|
||
}
|
||
|
||
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 (
|
||
<Dialog
|
||
open={open}
|
||
onClose={onClose}
|
||
maxWidth="md"
|
||
fullWidth
|
||
PaperProps={{
|
||
sx: {
|
||
background: 'linear-gradient(145deg, #3c3836 0%, #282828 100%)',
|
||
border: '1px solid #504945'
|
||
}
|
||
}}
|
||
>
|
||
<DialogTitle>
|
||
{rule ? t('ruleEditor.editTitle') : t('ruleEditor.createTitle')}
|
||
</DialogTitle>
|
||
|
||
<DialogContent dividers>
|
||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3, pt: 1 }}>
|
||
{/* Rule Name */}
|
||
<TextField
|
||
label={t('ruleEditor.ruleName')}
|
||
value={name}
|
||
onChange={(e) => setName(e.target.value)}
|
||
fullWidth
|
||
placeholder={t('ruleEditor.ruleNamePlaceholder')}
|
||
disabled={saving}
|
||
/>
|
||
|
||
{/* Color Tag Picker (multi-select) */}
|
||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, flexWrap: 'wrap' }}>
|
||
<Typography variant="body2" color="text.secondary">Tags:</Typography>
|
||
<Box
|
||
onClick={() => 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 }
|
||
}}
|
||
>
|
||
✕
|
||
</Box>
|
||
{availableColorTags.map(tag => (
|
||
<Box
|
||
key={tag.id}
|
||
onClick={() => {
|
||
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) && <span style={{ fontSize: '0.7rem' }}>✓</span>}
|
||
</Box>
|
||
))}
|
||
</Box>
|
||
|
||
{/* TRIGGERS SECTION */}
|
||
<Box>
|
||
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
|
||
{t('ruleEditor.triggersSection')}
|
||
</Typography>
|
||
<Divider sx={{ mb: 2 }} />
|
||
|
||
{/* Scheduled Time Trigger (fires at exact time) */}
|
||
<Paper sx={{ p: 2, mb: 2, bgcolor: useScheduledTime ? 'action.selected' : 'background.default' }}>
|
||
<FormControlLabel
|
||
control={
|
||
<Switch
|
||
checked={useScheduledTime}
|
||
onChange={(e) => setUseScheduledTime(e.target.checked)}
|
||
disabled={saving}
|
||
/>
|
||
}
|
||
label={<Typography fontWeight={600}>{t('ruleEditor.scheduledTime')}</Typography>}
|
||
/>
|
||
|
||
{useScheduledTime && (
|
||
<Box sx={{ mt: 2, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||
<TextField
|
||
label={t('ruleEditor.triggerAt')}
|
||
type="time"
|
||
value={scheduledTime}
|
||
onChange={(e) => setScheduledTime(e.target.value)}
|
||
InputLabelProps={{ shrink: true }}
|
||
size="small"
|
||
sx={{ width: 150 }}
|
||
disabled={saving}
|
||
/>
|
||
<Box>
|
||
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
||
{t('ruleEditor.days')}
|
||
</Typography>
|
||
<ToggleButtonGroup
|
||
value={scheduledDays}
|
||
onChange={handleScheduledDaysChange}
|
||
size="small"
|
||
disabled={saving}
|
||
>
|
||
{DAYS_KEYS.map(key => (
|
||
<ToggleButton
|
||
key={key}
|
||
value={key}
|
||
sx={{
|
||
'&.Mui-selected': {
|
||
bgcolor: '#d3869b',
|
||
color: '#282828',
|
||
'&:hover': { bgcolor: '#e396a5' }
|
||
}
|
||
}}
|
||
>
|
||
{t(`days.${key}`)}
|
||
</ToggleButton>
|
||
))}
|
||
</ToggleButtonGroup>
|
||
</Box>
|
||
</Box>
|
||
)}
|
||
</Paper>
|
||
|
||
{/* Time Range Trigger (active within window) */}
|
||
<Paper sx={{ p: 2, mb: 2, bgcolor: useTimeRange ? 'action.selected' : 'background.default' }}>
|
||
<FormControlLabel
|
||
control={
|
||
<Switch
|
||
checked={useTimeRange}
|
||
onChange={(e) => setUseTimeRange(e.target.checked)}
|
||
disabled={saving}
|
||
/>
|
||
}
|
||
label={<Typography fontWeight={600}>⏰ Time Range (active during window)</Typography>}
|
||
/>
|
||
|
||
{useTimeRange && (
|
||
<Box sx={{ mt: 2, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
|
||
<TextField
|
||
label="From"
|
||
type="time"
|
||
value={timeStart}
|
||
onChange={(e) => setTimeStart(e.target.value)}
|
||
InputLabelProps={{ shrink: true }}
|
||
size="small"
|
||
disabled={saving}
|
||
/>
|
||
<Typography>to</Typography>
|
||
<TextField
|
||
label="Until"
|
||
type="time"
|
||
value={timeEnd}
|
||
onChange={(e) => setTimeEnd(e.target.value)}
|
||
InputLabelProps={{ shrink: true }}
|
||
size="small"
|
||
disabled={saving}
|
||
/>
|
||
</Box>
|
||
<Box>
|
||
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
||
{t('ruleEditor.days')}
|
||
</Typography>
|
||
<ToggleButtonGroup
|
||
value={timeRangeDays}
|
||
onChange={handleTimeRangeDaysChange}
|
||
size="small"
|
||
disabled={saving}
|
||
>
|
||
{DAYS_KEYS.map(key => (
|
||
<ToggleButton
|
||
key={key}
|
||
value={key}
|
||
sx={{
|
||
'&.Mui-selected': {
|
||
bgcolor: '#8ec07c',
|
||
color: '#282828',
|
||
'&:hover': { bgcolor: '#98c98a' }
|
||
}
|
||
}}
|
||
>
|
||
{t(`days.${key}`)}
|
||
</ToggleButton>
|
||
))}
|
||
</ToggleButtonGroup>
|
||
</Box>
|
||
</Box>
|
||
)}
|
||
</Paper>
|
||
|
||
{/* Sensor Conditions Trigger */}
|
||
<Paper sx={{ p: 2, bgcolor: useSensors ? 'action.selected' : 'background.default' }}>
|
||
<FormControlLabel
|
||
control={
|
||
<Switch
|
||
checked={useSensors}
|
||
onChange={(e) => setUseSensors(e.target.checked)}
|
||
disabled={saving || sensors.length === 0}
|
||
/>
|
||
}
|
||
label={
|
||
<Typography fontWeight={600}>
|
||
📊 Sensor Conditions {sensors.length === 0 && '(no sensors available)'}
|
||
</Typography>
|
||
}
|
||
/>
|
||
|
||
{useSensors && (
|
||
<Box sx={{ mt: 2, display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||
{sensorConditions.length > 1 && (
|
||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||
<Typography variant="body2">Combine conditions with:</Typography>
|
||
<ToggleButtonGroup
|
||
value={sensorLogic}
|
||
exclusive
|
||
onChange={(e, v) => v && setSensorLogic(v)}
|
||
size="small"
|
||
disabled={saving}
|
||
>
|
||
<ToggleButton value="and" sx={{ '&.Mui-selected': { bgcolor: '#8ec07c', color: '#282828' } }}>
|
||
AND
|
||
</ToggleButton>
|
||
<ToggleButton value="or" sx={{ '&.Mui-selected': { bgcolor: '#fabd2f', color: '#282828' } }}>
|
||
OR
|
||
</ToggleButton>
|
||
</ToggleButtonGroup>
|
||
</Box>
|
||
)}
|
||
|
||
{sensorConditions.map((cond, i) => (
|
||
<Box key={i} sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||
{i > 0 && (
|
||
<Chip
|
||
label={sensorLogic.toUpperCase()}
|
||
size="small"
|
||
sx={{
|
||
bgcolor: sensorLogic === 'and' ? '#8ec07c' : '#fabd2f',
|
||
color: '#282828',
|
||
fontWeight: 600,
|
||
minWidth: 45
|
||
}}
|
||
/>
|
||
)}
|
||
<SensorCondition
|
||
condition={cond}
|
||
sensors={sensors}
|
||
onChange={(newCond) => updateSensorCondition(i, newCond)}
|
||
onRemove={sensorConditions.length > 1 ? () => removeSensorCondition(i) : null}
|
||
disabled={saving}
|
||
/>
|
||
</Box>
|
||
))}
|
||
|
||
<Button
|
||
size="small"
|
||
onClick={addSensorCondition}
|
||
disabled={saving}
|
||
sx={{ alignSelf: 'flex-start' }}
|
||
>
|
||
+ Add Condition
|
||
</Button>
|
||
</Box>
|
||
)}
|
||
</Paper>
|
||
</Box>
|
||
|
||
{/* ACTION SECTION */}
|
||
<Box>
|
||
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
|
||
ACTION (What to do)
|
||
</Typography>
|
||
<Divider sx={{ mb: 2 }} />
|
||
|
||
<FormControl fullWidth sx={{ mb: 2 }}>
|
||
<InputLabel>Action Type</InputLabel>
|
||
<Select
|
||
value={actionType}
|
||
label="Action Type"
|
||
onChange={(e) => setActionType(e.target.value)}
|
||
disabled={saving}
|
||
>
|
||
<MenuItem value="toggle">🔛 Toggle On/Off</MenuItem>
|
||
<MenuItem value="keepOn">⏱️ Keep On for X Minutes</MenuItem>
|
||
</Select>
|
||
</FormControl>
|
||
|
||
<FormControl fullWidth sx={{ mb: 2 }}>
|
||
<InputLabel>Target Output</InputLabel>
|
||
<Select
|
||
value={target}
|
||
label="Target Output"
|
||
onChange={(e) => setTarget(e.target.value)}
|
||
disabled={saving}
|
||
>
|
||
{outputs.map(o => (
|
||
<MenuItem key={o.id} value={o.id}>{o.label}</MenuItem>
|
||
))}
|
||
</Select>
|
||
</FormControl>
|
||
|
||
{/* Toggle action controls */}
|
||
{actionType === 'toggle' && (
|
||
<Box>
|
||
{isBinaryOutput ? (
|
||
// Binary output: On/Off switch
|
||
<FormControlLabel
|
||
control={
|
||
<Switch
|
||
checked={toggleState}
|
||
onChange={(e) => setToggleState(e.target.checked)}
|
||
color="primary"
|
||
disabled={saving}
|
||
/>
|
||
}
|
||
label={toggleState ? 'Turn ON' : 'Turn OFF'}
|
||
/>
|
||
) : (
|
||
// Level output: 1-10 slider
|
||
<Box>
|
||
<Typography variant="body2" color="text.secondary">
|
||
Set Level: {outputLevel}
|
||
</Typography>
|
||
<Slider
|
||
value={outputLevel}
|
||
onChange={(e, val) => 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}
|
||
/>
|
||
</Box>
|
||
)}
|
||
</Box>
|
||
)}
|
||
|
||
{/* Keep On action controls */}
|
||
{actionType === 'keepOn' && (
|
||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||
{!isBinaryOutput && (
|
||
// Level for port outputs
|
||
<Box>
|
||
<Typography variant="body2" color="text.secondary">
|
||
Set Level: {outputLevel}
|
||
</Typography>
|
||
<Slider
|
||
value={outputLevel}
|
||
onChange={(e, val) => 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}
|
||
/>
|
||
</Box>
|
||
)}
|
||
<Box>
|
||
<Typography variant="body2" color="text.secondary">
|
||
Duration: {duration} minutes
|
||
</Typography>
|
||
<Slider
|
||
value={duration}
|
||
onChange={(e, val) => 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}
|
||
/>
|
||
</Box>
|
||
</Box>
|
||
)}
|
||
</Box>
|
||
</Box>
|
||
</DialogContent>
|
||
|
||
<DialogActions sx={{ px: 3, py: 2 }}>
|
||
<Button onClick={onClose} color="inherit" disabled={saving}>
|
||
{t('ruleEditor.cancel')}
|
||
</Button>
|
||
<Button
|
||
onClick={handleSave}
|
||
variant="contained"
|
||
disabled={!isValid || saving}
|
||
sx={{
|
||
background: 'linear-gradient(45deg, #8ec07c 30%, #b8bb26 90%)',
|
||
'&:hover': {
|
||
background: 'linear-gradient(45deg, #98c98a 30%, #c5c836 90%)',
|
||
}
|
||
}}
|
||
>
|
||
{saving ? (
|
||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||
<CircularProgress size={16} color="inherit" />
|
||
{t('ruleEditor.saving')}
|
||
</Box>
|
||
) : (
|
||
rule ? t('ruleEditor.saveChanges') : t('ruleEditor.createRule')
|
||
)}
|
||
</Button>
|
||
</DialogActions>
|
||
</Dialog >
|
||
);
|
||
}
|