mirror of
http://10.0.2.1:3031/sauer/bfa-dryer-design.git
synced 2026-06-30 14:26:42 +10:00
User-friendly automation editor with dropdowns and visual UI
- Replace text prompt with proper popup form - Dropdown to select control (grouped: Temperatures, Motors, Outputs) - Action dropdown: Turn ON / Turn OFF / Set speed to / Set temp to - Number input with units (°C or %) only shows when action is "set" - Add/remove rule buttons - Save/Cancel buttons - Non-technical customer can use without typing any code Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c6e3b0c0eb
commit
4017617c37
@ -168,6 +168,22 @@ body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(
|
|||||||
.popup .sp-btn:active{background:var(--border)}
|
.popup .sp-btn:active{background:var(--border)}
|
||||||
.popup .sp-val{font-size:32px;min-width:80px;text-align:center}
|
.popup .sp-val{font-size:32px;min-width:80px;text-align:center}
|
||||||
.popup .close-btn{width:160px;height:44px;border:none;border-radius:8px;background:var(--blue);color:var(--text);font-size:16px;font-weight:600;cursor:pointer}
|
.popup .close-btn{width:160px;height:44px;border:none;border-radius:8px;background:var(--blue);color:var(--text);font-size:16px;font-weight:600;cursor:pointer}
|
||||||
|
|
||||||
|
/* Automation editor */
|
||||||
|
.auto-editor{width:700px;max-width:95vw;max-height:80vh;overflow-y:auto}
|
||||||
|
.auto-editor h3{font-size:20px;margin-bottom:12px}
|
||||||
|
.auto-rule{display:flex;gap:8px;align-items:center;padding:10px;background:var(--bg);border-radius:8px;margin-bottom:8px}
|
||||||
|
.auto-rule select,.auto-rule input{padding:8px 10px;border:1px solid var(--border);border-radius:6px;background:var(--card);color:var(--text);font-size:14px}
|
||||||
|
.auto-rule select{min-width:120px}
|
||||||
|
.auto-rule input[type=number]{width:80px}
|
||||||
|
.auto-rule .rule-del{width:32px;height:32px;border:none;border-radius:6px;background:rgba(255,23,68,0.2);color:var(--red);font-size:16px;cursor:pointer;flex-shrink:0}
|
||||||
|
.auto-rule .rule-del:hover{background:var(--red);color:#fff}
|
||||||
|
.auto-add-rule{width:100%;padding:12px;border:2px dashed var(--dim);border-radius:8px;background:none;color:var(--text2);font-size:14px;cursor:pointer;margin:8px 0}
|
||||||
|
.auto-add-rule:hover{border-color:var(--blue);color:var(--blue)}
|
||||||
|
.auto-btns{display:flex;gap:12px;justify-content:center;margin-top:16px}
|
||||||
|
.auto-btns button{padding:10px 24px;border:none;border-radius:8px;font-size:16px;font-weight:600;cursor:pointer}
|
||||||
|
.auto-btns .save-btn{background:var(--green);color:#000}
|
||||||
|
.auto-btns .cancel-btn{background:var(--card);color:var(--text);border:1px solid var(--border)}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -499,27 +515,170 @@ function createCardEl(c) {
|
|||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let autoEditCard = null;
|
||||||
|
|
||||||
function editAutomation(c) {
|
function editAutomation(c) {
|
||||||
const rulesStr = (c.rules || []).map(r => `${r.type}:${r.target}:${r.action}:${r.value||''}`).join('\n');
|
autoEditCard = c;
|
||||||
const help = `Edit automation rules (one per line):
|
// Gather all available controls from the layout
|
||||||
Format: type:target:action:value
|
const allControls = [];
|
||||||
|
layout.pages.forEach(p => p.cards.forEach(card => {
|
||||||
|
if (card.id !== c.id) allControls.push(card);
|
||||||
|
}));
|
||||||
|
|
||||||
Types: temp, motor, output
|
const popup = document.getElementById('popup');
|
||||||
Actions: set, on, off
|
popup.style.borderColor = 'var(--blue)';
|
||||||
Examples:
|
|
||||||
temp:Heat Input:set:130
|
|
||||||
motor:Hot Fan:set:65
|
|
||||||
motor:Conveyor:on
|
|
||||||
output:Brush:on
|
|
||||||
output:Mill:off
|
|
||||||
|
|
||||||
Current rules:`;
|
let rulesHtml = '';
|
||||||
const input = prompt(help, rulesStr);
|
(c.rules || []).forEach((r, i) => {
|
||||||
if (input === null) return;
|
rulesHtml += buildRuleRow(i, r, allControls);
|
||||||
c.rules = input.split('\n').filter(l => l.trim()).map(l => {
|
|
||||||
const [type, target, action, value] = l.split(':');
|
|
||||||
return {type: type||'output', target: target||'', label: target||'', action: action||'on', value: value ? parseInt(value) : undefined};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
popup.innerHTML = `
|
||||||
|
<div class="auto-editor">
|
||||||
|
<h3>Edit Automation: ${c.label}</h3>
|
||||||
|
<p style="color:var(--text2);font-size:13px;margin-bottom:16px">
|
||||||
|
When this automation is activated, all these actions happen at once.
|
||||||
|
Add what should turn on, set speeds, or adjust temperatures.
|
||||||
|
</p>
|
||||||
|
<div id="auto-rules">${rulesHtml}</div>
|
||||||
|
<button class="auto-add-rule" onclick="addAutoRule()">+ Add another action</button>
|
||||||
|
<div class="auto-btns">
|
||||||
|
<button class="cancel-btn" onclick="closePopup()">Cancel</button>
|
||||||
|
<button class="save-btn" onclick="saveAutoRules()">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
document.getElementById('popup-overlay').classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRuleRow(idx, rule, controls) {
|
||||||
|
if (!controls) {
|
||||||
|
controls = [];
|
||||||
|
layout.pages.forEach(p => p.cards.forEach(c => { if(autoEditCard && c.id !== autoEditCard.id) controls.push(c); }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group controls by type
|
||||||
|
const temps = controls.filter(c => c.type === 'temp');
|
||||||
|
const motors = controls.filter(c => c.type === 'motor');
|
||||||
|
const outputs = controls.filter(c => c.type === 'output' || c.type === 'burner');
|
||||||
|
|
||||||
|
// What to control dropdown
|
||||||
|
let targetOpts = '<option value="">-- Select --</option>';
|
||||||
|
if (temps.length) {
|
||||||
|
targetOpts += '<optgroup label="Temperatures">';
|
||||||
|
temps.forEach(c => targetOpts += `<option value="${c.id}" data-type="temp" ${rule.target===c.id?'selected':''}>${c.label}</option>`);
|
||||||
|
targetOpts += '</optgroup>';
|
||||||
|
}
|
||||||
|
if (motors.length) {
|
||||||
|
targetOpts += '<optgroup label="Motors / Speeds">';
|
||||||
|
motors.forEach(c => targetOpts += `<option value="${c.id}" data-type="motor" ${rule.target===c.id?'selected':''}>${c.label}</option>`);
|
||||||
|
targetOpts += '</optgroup>';
|
||||||
|
}
|
||||||
|
if (outputs.length) {
|
||||||
|
targetOpts += '<optgroup label="Outputs / Switches">';
|
||||||
|
outputs.forEach(c => targetOpts += `<option value="${c.id}" data-type="output" ${rule.target===c.id?'selected':''}>${c.label}</option>`);
|
||||||
|
targetOpts += '</optgroup>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Action depends on type
|
||||||
|
const isTemp = rule.type === 'temp';
|
||||||
|
const isMotor = rule.type === 'motor';
|
||||||
|
const isOutput = rule.type === 'output' || rule.type === 'burner';
|
||||||
|
|
||||||
|
let actionOpts = '';
|
||||||
|
if (isTemp) {
|
||||||
|
actionOpts = `<option value="set" ${rule.action==='set'?'selected':''}>Set to</option>`;
|
||||||
|
} else if (isMotor) {
|
||||||
|
actionOpts = `
|
||||||
|
<option value="on" ${rule.action==='on'?'selected':''}>Turn ON</option>
|
||||||
|
<option value="off" ${rule.action==='off'?'selected':''}>Turn OFF</option>
|
||||||
|
<option value="set" ${rule.action==='set'?'selected':''}>Set speed to</option>`;
|
||||||
|
} else {
|
||||||
|
actionOpts = `
|
||||||
|
<option value="on" ${rule.action==='on'?'selected':''}>Turn ON</option>
|
||||||
|
<option value="off" ${rule.action==='off'?'selected':''}>Turn OFF</option>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const showValue = rule.action === 'set';
|
||||||
|
const valueUnit = isTemp ? '°C' : '%';
|
||||||
|
const valueMax = isTemp ? 200 : 100;
|
||||||
|
const valueStep = isTemp ? 1 : 5;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="auto-rule" data-idx="${idx}">
|
||||||
|
<select onchange="autoRuleTargetChanged(this, ${idx})">${targetOpts}</select>
|
||||||
|
<select class="rule-action" data-idx="${idx}" onchange="autoRuleActionChanged(${idx})">${actionOpts}</select>
|
||||||
|
<span class="rule-value-wrap" style="display:${showValue?'flex':'none'};align-items:center;gap:6px">
|
||||||
|
<input type="number" class="rule-value" data-idx="${idx}" value="${rule.value||0}" min="0" max="${valueMax}" step="${valueStep}">
|
||||||
|
<span style="color:var(--text2);font-size:13px">${valueUnit}</span>
|
||||||
|
</span>
|
||||||
|
<button class="rule-del" onclick="removeAutoRule(${idx})">✕</button>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function autoRuleTargetChanged(sel, idx) {
|
||||||
|
const opt = sel.options[sel.selectedIndex];
|
||||||
|
const type = opt.dataset.type || 'output';
|
||||||
|
// Store temporarily so we can rebuild the action dropdown
|
||||||
|
const rules = collectAutoRules();
|
||||||
|
if (rules[idx]) {
|
||||||
|
rules[idx].target = sel.value;
|
||||||
|
rules[idx].type = type;
|
||||||
|
rules[idx].label = opt.textContent;
|
||||||
|
// Reset action based on type
|
||||||
|
if (type === 'temp') rules[idx].action = 'set';
|
||||||
|
else rules[idx].action = 'on';
|
||||||
|
}
|
||||||
|
autoEditCard.rules = rules;
|
||||||
|
editAutomation(autoEditCard); // re-render the whole popup
|
||||||
|
}
|
||||||
|
|
||||||
|
function autoRuleActionChanged(idx) {
|
||||||
|
const actionSel = document.querySelector(`.rule-action[data-idx="${idx}"]`);
|
||||||
|
const valueWrap = document.querySelectorAll('.rule-value-wrap')[idx];
|
||||||
|
if (actionSel && valueWrap) {
|
||||||
|
valueWrap.style.display = actionSel.value === 'set' ? 'flex' : 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addAutoRule() {
|
||||||
|
if (!autoEditCard) return;
|
||||||
|
const rules = collectAutoRules();
|
||||||
|
rules.push({type:'output', target:'', label:'', action:'on', value:0});
|
||||||
|
autoEditCard.rules = rules;
|
||||||
|
editAutomation(autoEditCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeAutoRule(idx) {
|
||||||
|
if (!autoEditCard) return;
|
||||||
|
const rules = collectAutoRules();
|
||||||
|
rules.splice(idx, 1);
|
||||||
|
autoEditCard.rules = rules;
|
||||||
|
editAutomation(autoEditCard);
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectAutoRules() {
|
||||||
|
const rows = document.querySelectorAll('.auto-rule');
|
||||||
|
const rules = [];
|
||||||
|
rows.forEach(row => {
|
||||||
|
const targetSel = row.querySelector('select');
|
||||||
|
const actionSel = row.querySelector('.rule-action');
|
||||||
|
const valueInput = row.querySelector('.rule-value');
|
||||||
|
const opt = targetSel.options[targetSel.selectedIndex];
|
||||||
|
rules.push({
|
||||||
|
type: opt?.dataset?.type || 'output',
|
||||||
|
target: targetSel.value,
|
||||||
|
label: opt?.textContent || '',
|
||||||
|
action: actionSel?.value || 'on',
|
||||||
|
value: actionSel?.value === 'set' ? parseInt(valueInput?.value || 0) : undefined
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return rules;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveAutoRules() {
|
||||||
|
if (!autoEditCard) return;
|
||||||
|
autoEditCard.rules = collectAutoRules().filter(r => r.target); // remove empty rows
|
||||||
|
closePopup();
|
||||||
renderCards();
|
renderCards();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user