Genesis
This commit is contained in:
346
src/client/RuleEditor.js
Normal file
346
src/client/RuleEditor.js
Normal file
@@ -0,0 +1,346 @@
|
||||
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
|
||||
} from '@mui/material';
|
||||
|
||||
const DAYS = [
|
||||
{ key: 'mon', label: 'Mon' },
|
||||
{ key: 'tue', label: 'Tue' },
|
||||
{ key: 'wed', label: 'Wed' },
|
||||
{ key: 'thu', label: 'Thu' },
|
||||
{ key: 'fri', label: 'Fri' },
|
||||
{ key: 'sat', label: 'Sat' },
|
||||
{ key: 'sun', label: 'Sun' }
|
||||
];
|
||||
|
||||
const SENSORS = ['Temperature', 'Humidity', 'CO2', 'VPD', 'Light Level'];
|
||||
const OPERATORS = [
|
||||
{ value: '>', label: 'Greater than (>)' },
|
||||
{ value: '<', label: 'Less than (<)' },
|
||||
{ value: '>=', label: 'Greater or equal (≥)' },
|
||||
{ value: '<=', label: 'Less or equal (≤)' },
|
||||
{ value: '==', label: 'Equal to (=)' }
|
||||
];
|
||||
|
||||
const OUTPUTS = [
|
||||
'Workshop Light',
|
||||
'Exhaust Fan',
|
||||
'Heater',
|
||||
'Humidifier',
|
||||
'All Outlets',
|
||||
'Grow Light',
|
||||
'Circulation Fan'
|
||||
];
|
||||
|
||||
export default function RuleEditor({ open, rule, onSave, onClose }) {
|
||||
const [name, setName] = useState('');
|
||||
const [triggerType, setTriggerType] = useState('time');
|
||||
|
||||
// Time trigger state
|
||||
const [time, setTime] = useState('08:00');
|
||||
const [days, setDays] = useState(['mon', 'tue', 'wed', 'thu', 'fri']);
|
||||
|
||||
// Sensor trigger state
|
||||
const [sensor, setSensor] = useState('Temperature');
|
||||
const [operator, setOperator] = useState('>');
|
||||
const [sensorValue, setSensorValue] = useState(25);
|
||||
|
||||
// Action state
|
||||
const [actionType, setActionType] = useState('toggle');
|
||||
const [target, setTarget] = useState('Workshop Light');
|
||||
const [toggleState, setToggleState] = useState(true);
|
||||
const [duration, setDuration] = useState(15);
|
||||
|
||||
// Reset form when rule changes
|
||||
useEffect(() => {
|
||||
if (rule) {
|
||||
setName(rule.name);
|
||||
setTriggerType(rule.trigger.type);
|
||||
|
||||
if (rule.trigger.type === 'time') {
|
||||
setTime(rule.trigger.time);
|
||||
setDays(rule.trigger.days || []);
|
||||
} else {
|
||||
setSensor(rule.trigger.sensor);
|
||||
setOperator(rule.trigger.operator);
|
||||
setSensorValue(rule.trigger.value);
|
||||
}
|
||||
|
||||
setActionType(rule.action.type);
|
||||
setTarget(rule.action.target);
|
||||
if (rule.action.type === 'toggle') {
|
||||
setToggleState(rule.action.state);
|
||||
} else {
|
||||
setDuration(rule.action.duration);
|
||||
}
|
||||
} else {
|
||||
// Reset to defaults for new rule
|
||||
setName('');
|
||||
setTriggerType('time');
|
||||
setTime('08:00');
|
||||
setDays(['mon', 'tue', 'wed', 'thu', 'fri']);
|
||||
setSensor('Temperature');
|
||||
setOperator('>');
|
||||
setSensorValue(25);
|
||||
setActionType('toggle');
|
||||
setTarget('Workshop Light');
|
||||
setToggleState(true);
|
||||
setDuration(15);
|
||||
}
|
||||
}, [rule, open]);
|
||||
|
||||
const handleDaysChange = (event, newDays) => {
|
||||
if (newDays.length > 0) {
|
||||
setDays(newDays);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
const ruleData = {
|
||||
name,
|
||||
trigger: triggerType === 'time'
|
||||
? { type: 'time', time, days }
|
||||
: { type: 'sensor', sensor, operator, value: sensorValue },
|
||||
action: actionType === 'toggle'
|
||||
? { type: 'toggle', target, state: toggleState }
|
||||
: { type: 'keepOn', target, duration }
|
||||
};
|
||||
onSave(ruleData);
|
||||
};
|
||||
|
||||
const isValid = name.trim().length > 0 &&
|
||||
(triggerType !== 'time' || days.length > 0);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
maxWidth="sm"
|
||||
fullWidth
|
||||
PaperProps={{
|
||||
sx: {
|
||||
background: 'linear-gradient(145deg, #3c3836 0%, #282828 100%)',
|
||||
border: '1px solid #504945'
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTitle>
|
||||
{rule ? '✏️ Edit Rule' : '➕ Create New Rule'}
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent dividers>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3, pt: 1 }}>
|
||||
{/* Rule Name */}
|
||||
<TextField
|
||||
label="Rule Name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
fullWidth
|
||||
placeholder="e.g., Morning Lights"
|
||||
/>
|
||||
|
||||
{/* Trigger Section */}
|
||||
<Box>
|
||||
<Typography variant="subtitle2" color="text.secondary" gutterBottom>
|
||||
TRIGGER (When to activate)
|
||||
</Typography>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
|
||||
<FormControl fullWidth sx={{ mb: 2 }}>
|
||||
<InputLabel>Trigger Type</InputLabel>
|
||||
<Select
|
||||
value={triggerType}
|
||||
label="Trigger Type"
|
||||
onChange={(e) => setTriggerType(e.target.value)}
|
||||
>
|
||||
<MenuItem value="time">⏰ Time-based</MenuItem>
|
||||
<MenuItem value="sensor">📊 Sensor Value</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
{triggerType === 'time' && (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<TextField
|
||||
label="Time"
|
||||
type="time"
|
||||
value={time}
|
||||
onChange={(e) => setTime(e.target.value)}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
fullWidth
|
||||
/>
|
||||
<Box>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
|
||||
Days of Week
|
||||
</Typography>
|
||||
<ToggleButtonGroup
|
||||
value={days}
|
||||
onChange={handleDaysChange}
|
||||
size="small"
|
||||
sx={{ flexWrap: 'wrap' }}
|
||||
>
|
||||
{DAYS.map(day => (
|
||||
<ToggleButton
|
||||
key={day.key}
|
||||
value={day.key}
|
||||
sx={{
|
||||
'&.Mui-selected': {
|
||||
bgcolor: '#8ec07c',
|
||||
color: '#282828',
|
||||
'&:hover': { bgcolor: '#98c98a' }
|
||||
}
|
||||
}}
|
||||
>
|
||||
{day.label}
|
||||
</ToggleButton>
|
||||
))}
|
||||
</ToggleButtonGroup>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{triggerType === 'sensor' && (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel>Sensor</InputLabel>
|
||||
<Select
|
||||
value={sensor}
|
||||
label="Sensor"
|
||||
onChange={(e) => setSensor(e.target.value)}
|
||||
>
|
||||
{SENSORS.map(s => (
|
||||
<MenuItem key={s} value={s}>{s}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Box sx={{ display: 'flex', gap: 2 }}>
|
||||
<FormControl sx={{ minWidth: 180 }}>
|
||||
<InputLabel>Condition</InputLabel>
|
||||
<Select
|
||||
value={operator}
|
||||
label="Condition"
|
||||
onChange={(e) => setOperator(e.target.value)}
|
||||
>
|
||||
{OPERATORS.map(op => (
|
||||
<MenuItem key={op.value} value={op.value}>{op.label}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<TextField
|
||||
label="Value"
|
||||
type="number"
|
||||
value={sensorValue}
|
||||
onChange={(e) => setSensorValue(Number(e.target.value))}
|
||||
fullWidth
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</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)}
|
||||
>
|
||||
<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)}
|
||||
>
|
||||
{OUTPUTS.map(o => (
|
||||
<MenuItem key={o} value={o}>{o}</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
{actionType === 'toggle' && (
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={toggleState}
|
||||
onChange={(e) => setToggleState(e.target.checked)}
|
||||
color="primary"
|
||||
/>
|
||||
}
|
||||
label={toggleState ? 'Turn ON' : 'Turn OFF'}
|
||||
/>
|
||||
)}
|
||||
|
||||
{actionType === 'keepOn' && (
|
||||
<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"
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
|
||||
<DialogActions sx={{ px: 3, py: 2 }}>
|
||||
<Button onClick={onClose} color="inherit">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
variant="contained"
|
||||
disabled={!isValid}
|
||||
sx={{
|
||||
background: 'linear-gradient(45deg, #8ec07c 30%, #b8bb26 90%)',
|
||||
'&:hover': {
|
||||
background: 'linear-gradient(45deg, #98c98a 30%, #c5c836 90%)',
|
||||
}
|
||||
}}
|
||||
>
|
||||
{rule ? 'Save Changes' : 'Create Rule'}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user