This commit is contained in:
sebseb7
2025-12-21 01:46:12 +01:00
commit 2baa1af2e8
23 changed files with 9874 additions and 0 deletions

346
src/client/RuleEditor.js Normal file
View 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>
);
}