mirror of
http://10.0.2.1:3031/sauer/bfa-dryer-design.git
synced 2026-06-30 11:36:42 +10:00
Restore all V1 features: automation, inline +, card editor, popups, burner-fan link
This commit is contained in:
parent
9a80739b2d
commit
b76cb7be9e
@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<title>BFA Banana Dryer — HMI Design Tool</title>
|
<title>BFA Banana Dryer — HMI Design Tool</title>
|
||||||
<script src="static/sortable.min.js?v=2"></script>
|
<script src="static/sortable.min.js?v=3"></script>
|
||||||
<style>
|
<style>
|
||||||
*{margin:0;padding:0;box-sizing:border-box;user-select:none;-webkit-tap-highlight-color:transparent}
|
*{margin:0;padding:0;box-sizing:border-box;user-select:none;-webkit-tap-highlight-color:transparent}
|
||||||
:root{--bg:#0a0e17;--card:#131a2b;--border:#1e2a45;--text:#e8ecf4;--text2:#7a8baa;--dim:#4a5670;--blue:#2d7ff9;--green:#00c853;--amber:#ffab00;--red:#ff1744;--thot:#ff4444;--twarm:#ff8844;--tcool:#44aaff;--track:#1a2240}
|
:root{--bg:#0a0e17;--card:#131a2b;--border:#1e2a45;--text:#e8ecf4;--text2:#7a8baa;--dim:#4a5670;--blue:#2d7ff9;--green:#00c853;--amber:#ffab00;--red:#ff1744;--thot:#ff4444;--twarm:#ff8844;--tcool:#44aaff;--track:#1a2240}
|
||||||
@ -52,6 +52,8 @@ body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(
|
|||||||
.hmi-card.type-motor,.hmi-card.type-output,.hmi-card.type-burner{background:var(--red)}
|
.hmi-card.type-motor,.hmi-card.type-output,.hmi-card.type-burner{background:var(--red)}
|
||||||
.hmi-card.on.type-motor,.hmi-card.on.type-output{background:var(--green)}
|
.hmi-card.on.type-motor,.hmi-card.on.type-output{background:var(--green)}
|
||||||
.hmi-card.on.type-burner{background:var(--amber)}
|
.hmi-card.on.type-burner{background:var(--amber)}
|
||||||
|
.hmi-card.type-automation{background:var(--red);border:none}
|
||||||
|
.hmi-card.on.type-automation{background:var(--green)}
|
||||||
|
|
||||||
.hmi-card .card-label{font-size:18px;color:var(--text2);text-align:center}
|
.hmi-card .card-label{font-size:18px;color:var(--text2);text-align:center}
|
||||||
.hmi-card .card-value{font-size:28px;font-weight:700}
|
.hmi-card .card-value{font-size:28px;font-weight:700}
|
||||||
@ -72,10 +74,17 @@ body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(
|
|||||||
.sortable-ghost{opacity:0.4;border:2px dashed var(--blue) !important}
|
.sortable-ghost{opacity:0.4;border:2px dashed var(--blue) !important}
|
||||||
|
|
||||||
/* Add card palette */
|
/* Add card palette */
|
||||||
.add-palette{display:none;padding:8px;gap:8px;border-top:1px solid var(--border);flex-shrink:0}
|
/* Add card placeholder */
|
||||||
.edit-mode .add-palette{display:flex}
|
.add-placeholder{width:calc(50% - 4px);height:calc(33.3% - 6px);border:2px dashed var(--dim);border-radius:10px;display:none;align-items:center;justify-content:center;cursor:pointer;transition:all 0.15s;position:relative}
|
||||||
.add-palette button{flex:1;padding:10px;border:2px dashed var(--dim);border-radius:8px;background:none;color:var(--text2);font-size:13px;cursor:pointer}
|
.edit-mode .add-placeholder{display:flex}
|
||||||
.add-palette button:hover{border-color:var(--blue);color:var(--blue)}
|
.add-placeholder:hover{border-color:var(--blue);background:rgba(45,127,249,0.05)}
|
||||||
|
.add-placeholder .plus{font-size:36px;color:var(--dim)}
|
||||||
|
.add-placeholder:hover .plus{color:var(--blue)}
|
||||||
|
/* Add menu dropdown */
|
||||||
|
.add-menu{display:none;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);background:var(--card);border:1px solid var(--border);border-radius:10px;padding:6px;z-index:20;min-width:160px;box-shadow:0 8px 24px rgba(0,0,0,0.5)}
|
||||||
|
.add-menu.open{display:flex;flex-direction:column;gap:2px}
|
||||||
|
.add-menu button{padding:10px 16px;border:none;border-radius:6px;background:none;color:var(--text);font-size:14px;text-align:left;cursor:pointer}
|
||||||
|
.add-menu button:hover{background:var(--border)}
|
||||||
|
|
||||||
/* Comment badge on cards */
|
/* Comment badge on cards */
|
||||||
.hmi-card .card-comment-badge{position:absolute;bottom:4px;right:4px;width:20px;height:20px;border-radius:50%;background:var(--amber);color:#000;font-size:10px;font-weight:700;display:none;align-items:center;justify-content:center}
|
.hmi-card .card-comment-badge{position:absolute;bottom:4px;right:4px;width:20px;height:20px;border-radius:50%;background:var(--amber);color:#000;font-size:10px;font-weight:700;display:none;align-items:center;justify-content:center}
|
||||||
@ -149,9 +158,9 @@ body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(
|
|||||||
.inline-edit{background:transparent;border:none;border-bottom:2px solid var(--blue);color:var(--text);font-size:inherit;font-family:inherit;text-align:center;outline:none;width:100%}
|
.inline-edit{background:transparent;border:none;border-bottom:2px solid var(--blue);color:var(--text);font-size:inherit;font-family:inherit;text-align:center;outline:none;width:100%}
|
||||||
|
|
||||||
/* Popup */
|
/* Popup */
|
||||||
#popup-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.5);display:none;align-items:center;justify-content:center;z-index:50}
|
#popup-overlay{position:fixed;inset:0;background:rgba(0,0,0,0.85);display:none;align-items:center;justify-content:center;z-index:500}
|
||||||
#popup-overlay.active{display:flex}
|
#popup-overlay.active{display:flex}
|
||||||
.popup{background:var(--card);border:3px solid var(--blue);border-radius:16px;padding:24px;width:500px;display:flex;flex-direction:column;align-items:center;gap:16px}
|
.popup{z-index:501;background:#0d1220;border:none;border-radius:16px;padding:24px;width:500px;display:flex;flex-direction:column;align-items:center;gap:16px;box-shadow:0 16px 48px rgba(0,0,0,0.8)}
|
||||||
.popup .title{font-size:24px;color:var(--text2)}
|
.popup .title{font-size:24px;color:var(--text2)}
|
||||||
.popup .live{font-size:32px;font-weight:700}
|
.popup .live{font-size:32px;font-weight:700}
|
||||||
.popup .sp-row{display:flex;align-items:center;gap:20px}
|
.popup .sp-row{display:flex;align-items:center;gap:20px}
|
||||||
@ -159,6 +168,32 @@ 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;background:#0d1220;border-radius:16px;padding:24px}
|
||||||
|
.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)}
|
||||||
|
|
||||||
|
/* Card properties editor */
|
||||||
|
.props-editor{width:420px;max-width:95vw}
|
||||||
|
.props-editor h3{font-size:20px;margin-bottom:16px}
|
||||||
|
.prop-row{display:flex;align-items:center;gap:12px;margin-bottom:12px}
|
||||||
|
.prop-row label{width:100px;font-size:13px;color:var(--text2);flex-shrink:0;text-align:right}
|
||||||
|
.prop-row input,.prop-row select{flex:1;padding:10px;border:1px solid var(--border);border-radius:6px;background:var(--card);color:var(--text);font-size:14px}
|
||||||
|
.prop-row input[type=color]{width:50px;height:38px;padding:2px;cursor:pointer;flex:0}
|
||||||
|
.prop-row input[type=range]{flex:1}
|
||||||
|
.prop-preview{display:inline-block;width:24px;height:24px;border-radius:4px;border:1px solid var(--border);vertical-align:middle;margin-left:8px}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@ -193,12 +228,7 @@ body{font-family:'Segoe UI',system-ui,sans-serif;background:var(--bg);color:var(
|
|||||||
</div>
|
</div>
|
||||||
<div class="hmi-tabs" id="hmi-tabs"></div>
|
<div class="hmi-tabs" id="hmi-tabs"></div>
|
||||||
<div class="card-grid edit-mode" id="card-grid"></div>
|
<div class="card-grid edit-mode" id="card-grid"></div>
|
||||||
<div class="add-palette" id="add-palette">
|
<!-- add palette removed — inline + buttons in grid instead -->
|
||||||
<button onclick="addCard('temp')">+ Temperature</button>
|
|
||||||
<button onclick="addCard('motor')">+ Motor</button>
|
|
||||||
<button onclick="addCard('output')">+ Output</button>
|
|
||||||
<button onclick="addCard('burner')">+ Burner</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- RIGHT: MACHINE SIM -->
|
<!-- RIGHT: MACHINE SIM -->
|
||||||
@ -326,7 +356,7 @@ function setMode(m) {
|
|||||||
document.getElementById('card-grid').classList.toggle('edit-mode', m === 'edit');
|
document.getElementById('card-grid').classList.toggle('edit-mode', m === 'edit');
|
||||||
document.getElementById('preview-tag').style.display = m === 'preview' ? 'inline' : 'none';
|
document.getElementById('preview-tag').style.display = m === 'preview' ? 'inline' : 'none';
|
||||||
document.querySelectorAll('.edit-overlay').forEach(e => e.style.display = m === 'edit' ? 'flex' : 'none');
|
document.querySelectorAll('.edit-overlay').forEach(e => e.style.display = m === 'edit' ? 'flex' : 'none');
|
||||||
document.getElementById('add-palette').style.display = m === 'edit' ? 'flex' : 'none';
|
// add-palette removed — inline + buttons handle this now
|
||||||
if (m === 'edit') initSortable();
|
if (m === 'edit') initSortable();
|
||||||
else if (sortableInstance) { sortableInstance.destroy(); sortableInstance = null; }
|
else if (sortableInstance) { sortableInstance.destroy(); sortableInstance = null; }
|
||||||
}
|
}
|
||||||
@ -398,7 +428,35 @@ function renderCards() {
|
|||||||
const el = createCardEl(c);
|
const el = createCardEl(c);
|
||||||
grid.appendChild(el);
|
grid.appendChild(el);
|
||||||
});
|
});
|
||||||
if (mode === 'edit') initSortable();
|
// In edit mode, fill remaining slots with + placeholders (up to 6 slots per page)
|
||||||
|
if (mode === 'edit') {
|
||||||
|
const halfCards = page.cards.filter(c => c.width !== 'full').length;
|
||||||
|
const fullCards = page.cards.filter(c => c.width === 'full').length;
|
||||||
|
const slotsUsed = halfCards + fullCards * 2;
|
||||||
|
const emptySlots = Math.max(1, 6 - slotsUsed); // always show at least 1
|
||||||
|
for (let i = 0; i < emptySlots; i++) {
|
||||||
|
const ph = document.createElement('div');
|
||||||
|
ph.className = 'add-placeholder';
|
||||||
|
ph.innerHTML = `<span class="plus">+</span>
|
||||||
|
<div class="add-menu" id="addmenu-${i}">
|
||||||
|
<button onclick="addCard('temp')">Temperature</button>
|
||||||
|
<button onclick="addCard('motor')">Motor / Speed</button>
|
||||||
|
<button onclick="addCard('output')">Output / Switch</button>
|
||||||
|
<button onclick="addCard('burner')">Burner</button>
|
||||||
|
<button onclick="addCard('automation')">Automation</button>
|
||||||
|
</div>`;
|
||||||
|
ph.addEventListener('click', (e) => {
|
||||||
|
if (e.target.tagName === 'BUTTON') return;
|
||||||
|
// Toggle the menu
|
||||||
|
const menu = ph.querySelector('.add-menu');
|
||||||
|
document.querySelectorAll('.add-menu.open').forEach(m => { if(m!==menu) m.classList.remove('open'); });
|
||||||
|
menu.classList.toggle('open');
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
grid.appendChild(ph);
|
||||||
|
}
|
||||||
|
initSortable();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createCardEl(c) {
|
function createCardEl(c) {
|
||||||
@ -413,6 +471,8 @@ function createCardEl(c) {
|
|||||||
|
|
||||||
// Edit buttons
|
// Edit buttons
|
||||||
el.innerHTML += `<div class="edit-overlay" style="position:absolute;top:4px;right:4px;gap:4px">
|
el.innerHTML += `<div class="edit-overlay" style="position:absolute;top:4px;right:4px;gap:4px">
|
||||||
|
<button class="edit-btn" onclick="event.stopPropagation();editCardProps('${c.id}')" title="Edit" style="background:var(--blue);color:#fff">✎</button>
|
||||||
|
<button class="edit-btn" onclick="event.stopPropagation();toggleCardSize('${c.id}')" title="Resize">${c.width==='full'?'◧':'◻'}</button>
|
||||||
<button class="edit-btn comment-btn" onclick="event.stopPropagation();openComments('${c.id}','${c.label}')">💬</button>
|
<button class="edit-btn comment-btn" onclick="event.stopPropagation();openComments('${c.id}','${c.label}')">💬</button>
|
||||||
<button class="edit-btn" onclick="event.stopPropagation();deleteCard('${c.id}')">✕</button>
|
<button class="edit-btn" onclick="event.stopPropagation();deleteCard('${c.id}')">✕</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
@ -421,32 +481,219 @@ function createCardEl(c) {
|
|||||||
const commentCount = (layout.comments || []).filter(x => x.target === c.id).length;
|
const commentCount = (layout.comments || []).filter(x => x.target === c.id).length;
|
||||||
el.innerHTML += `<div class="card-comment-badge ${commentCount > 0 ? 'has-comments' : ''}">${commentCount}</div>`;
|
el.innerHTML += `<div class="card-comment-badge ${commentCount > 0 ? 'has-comments' : ''}">${commentCount}</div>`;
|
||||||
|
|
||||||
|
const cfs = c.fontSize || 18;
|
||||||
if (c.type === 'temp') {
|
if (c.type === 'temp') {
|
||||||
const color = c.color || '#ff8844';
|
const color = c.color || '#ff8844';
|
||||||
el.innerHTML += `<div class="card-label" onclick="if(mode==='edit'){event.stopPropagation();renameCard('${c.id}')}">${c.label}</div>`;
|
el.innerHTML += `<div class="card-label" style="font-size:${cfs}px" onclick="if(mode==='edit'){event.stopPropagation();editCardProps('${c.id}')}">${c.label}</div>`;
|
||||||
el.innerHTML += `<div class="card-value" style="color:${color}">${Math.round(s.value)} °C</div>`;
|
el.innerHTML += `<div class="card-value" style="color:${color}">${Math.round(s.value)} °C</div>`;
|
||||||
el.innerHTML += `<div class="card-bar"><div class="card-bar-fill" style="width:${Math.min(100,s.value/2)}%;background:${color}"></div></div>`;
|
el.innerHTML += `<div class="card-bar"><div class="card-bar-fill" style="width:${Math.min(100,s.value/2)}%;background:${color}"></div></div>`;
|
||||||
el.innerHTML += `<div class="card-sub">SP: ${c.sp_default || 70} °C</div>`;
|
el.innerHTML += `<div class="card-sub">SP: ${c.sp_default || 70} °C</div>`;
|
||||||
el.onclick = () => { if (mode === 'preview') openTempPopup(c); };
|
el.onclick = () => { if (mode === 'preview') openTempPopup(c); };
|
||||||
} else if (c.type === 'motor') {
|
} else if (c.type === 'motor') {
|
||||||
el.innerHTML += `<div style="display:flex;gap:10px;align-items:center">
|
el.innerHTML += `<div style="display:flex;gap:10px;align-items:center">
|
||||||
<span class="card-label" onclick="if(mode==='edit'){event.stopPropagation();renameCard('${c.id}')}">${c.label}</span>
|
<span class="card-label" style="font-size:${cfs}px" onclick="if(mode==='edit'){event.stopPropagation();editCardProps('${c.id}')}">${c.label}</span>
|
||||||
<span style="font-size:18px;color:var(--text2)">${c.sp_default || 50}%</span>
|
<span style="font-size:18px;color:var(--text2)">${c.sp_default || 50}%</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
el.innerHTML += `<div class="card-value">${s.on ? 'ON' : 'OFF'}</div>`;
|
el.innerHTML += `<div class="card-value">${s.on ? 'ON' : 'OFF'}</div>`;
|
||||||
el.onclick = () => { if (mode === 'preview') toggleSim(c.id); };
|
el.onclick = () => { if (mode === 'preview') toggleSim(c.id); };
|
||||||
} else if (c.type === 'burner') {
|
} else if (c.type === 'burner') {
|
||||||
el.innerHTML += `<div class="card-label" onclick="if(mode==='edit'){event.stopPropagation();renameCard('${c.id}')}">${c.label}</div>`;
|
el.innerHTML += `<div class="card-label" style="font-size:${cfs}px" onclick="if(mode==='edit'){event.stopPropagation();editCardProps('${c.id}')}">${c.label}</div>`;
|
||||||
el.innerHTML += `<div class="card-value">${s.on ? 'ON' : 'OFF'}</div>`;
|
el.innerHTML += `<div class="card-value">${s.on ? 'ON' : 'OFF'}</div>`;
|
||||||
el.onclick = () => { if (mode === 'preview') toggleSim(c.id); };
|
el.onclick = () => { if (mode === 'preview') toggleSim(c.id); };
|
||||||
|
} else if (c.type === 'automation') {
|
||||||
|
// Automation card — combines outputs, speeds, temp setpoints
|
||||||
|
el.innerHTML += `<div class="card-label" onclick="if(mode==='edit'){event.stopPropagation();editCardProps('${c.id}')}" style="font-size:${Math.max(cfs,20)}px;font-weight:700">${c.label}</div>`;
|
||||||
|
const rules = c.rules || [];
|
||||||
|
if (rules.length === 0) {
|
||||||
|
el.innerHTML += `<div class="card-sub" style="text-align:center">No rules configured<br>Click to edit in edit mode</div>`;
|
||||||
|
} else {
|
||||||
|
let rulesHtml = '<div style="width:100%;font-size:12px;color:var(--text2);text-align:left;padding:0 8px;overflow:hidden">';
|
||||||
|
rules.forEach(r => {
|
||||||
|
const icon = r.type === 'temp' ? '🌡' : r.type === 'motor' ? '⚙' : r.type === 'output' ? '⚡' : '●';
|
||||||
|
rulesHtml += `<div style="padding:3px 0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${icon} ${r.label || r.target}: ${r.action || ''} ${r.value !== undefined ? r.value : ''}</div>`;
|
||||||
|
});
|
||||||
|
rulesHtml += '</div>';
|
||||||
|
el.innerHTML += rulesHtml;
|
||||||
|
}
|
||||||
|
el.onclick = () => {
|
||||||
|
if (mode === 'edit') { event.stopPropagation(); editAutomation(c); }
|
||||||
|
else if (mode === 'preview') toggleSim(c.id);
|
||||||
|
};
|
||||||
} else { // output
|
} else { // output
|
||||||
el.innerHTML += `<div class="card-label" onclick="if(mode==='edit'){event.stopPropagation();renameCard('${c.id}')}" style="text-align:center">${c.label}</div>`;
|
el.innerHTML += `<div class="card-label" onclick="if(mode==='edit'){event.stopPropagation();editCardProps('${c.id}')}" style="text-align:center;font-size:${cfs}px">${c.label}</div>`;
|
||||||
el.innerHTML += `<div class="card-value">${s.on ? 'ON' : 'OFF'}</div>`;
|
el.innerHTML += `<div class="card-value">${s.on ? 'ON' : 'OFF'}</div>`;
|
||||||
el.onclick = () => { if (mode === 'preview') toggleSim(c.id); };
|
el.onclick = () => { if (mode === 'preview') toggleSim(c.id); };
|
||||||
}
|
}
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let autoEditCard = null;
|
||||||
|
|
||||||
|
function editAutomation(c) {
|
||||||
|
autoEditCard = c;
|
||||||
|
// Gather all available controls from the layout
|
||||||
|
const allControls = [];
|
||||||
|
layout.pages.forEach(p => p.cards.forEach(card => {
|
||||||
|
if (card.id !== c.id) allControls.push(card);
|
||||||
|
}));
|
||||||
|
|
||||||
|
const popup = document.getElementById('popup');
|
||||||
|
popup.style.borderColor = 'var(--border)';
|
||||||
|
|
||||||
|
let rulesHtml = '';
|
||||||
|
(c.rules || []).forEach((r, i) => {
|
||||||
|
rulesHtml += buildRuleRow(i, r, allControls);
|
||||||
|
});
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════
|
||||||
// DRAG & DROP
|
// DRAG & DROP
|
||||||
// ══════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════
|
||||||
@ -469,14 +716,17 @@ function initSortable() {
|
|||||||
// CARD ACTIONS
|
// CARD ACTIONS
|
||||||
// ══════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════
|
||||||
function addCard(type) {
|
function addCard(type) {
|
||||||
|
// Close any open add menus
|
||||||
|
document.querySelectorAll('.add-menu.open').forEach(m => m.classList.remove('open'));
|
||||||
const id = type + '_' + Date.now();
|
const id = type + '_' + Date.now();
|
||||||
const defaults = {
|
const defaults = {
|
||||||
temp: {label: 'New Temp', color: '#ff8844', sp_default: 70},
|
temp: {label: 'New Temp', color: '#ff8844', sp_default: 70},
|
||||||
motor: {label: 'New Motor', sp_default: 50},
|
motor: {label: 'New Motor', sp_default: 50},
|
||||||
output: {label: 'New Output'},
|
output: {label: 'New Output'},
|
||||||
burner: {label: 'Burner'}
|
burner: {label: 'Burner'},
|
||||||
|
automation: {label: 'New Automation', width: 'full', rules: []}
|
||||||
};
|
};
|
||||||
const card = {id, type, width: 'half', ...defaults[type]};
|
const card = {id, type, width: defaults[type]?.width || 'half', ...defaults[type]};
|
||||||
layout.pages[currentPage].cards.push(card);
|
layout.pages[currentPage].cards.push(card);
|
||||||
simState[id] = {on: false, value: card.sp_default || 0};
|
simState[id] = {on: false, value: card.sp_default || 0};
|
||||||
renderCards();
|
renderCards();
|
||||||
@ -490,12 +740,97 @@ function deleteCard(id) {
|
|||||||
renderCards();
|
renderCards();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renameCard(id) {
|
function editCardProps(id) {
|
||||||
|
const card = findCard(id);
|
||||||
|
if (!card) return;
|
||||||
|
|
||||||
|
const p = document.getElementById('popup');
|
||||||
|
p.style.borderColor = 'transparent';
|
||||||
|
|
||||||
|
let typeOpts = ['temp','motor','output','burner','automation'].map(t =>
|
||||||
|
`<option value="${t}" ${card.type===t?'selected':''}>${t.charAt(0).toUpperCase()+t.slice(1)}</option>`
|
||||||
|
).join('');
|
||||||
|
|
||||||
|
let extraFields = '';
|
||||||
|
if (card.type === 'temp') {
|
||||||
|
extraFields = `
|
||||||
|
<div class="prop-row"><label>Setpoint</label><input type="number" id="prop-sp" value="${card.sp_default||70}" min="0" max="200"> <span style="color:var(--text2)">°C</span></div>`;
|
||||||
|
} else if (card.type === 'motor') {
|
||||||
|
extraFields = `
|
||||||
|
<div class="prop-row"><label>Speed SP</label><input type="number" id="prop-sp" value="${card.sp_default||50}" min="0" max="100" step="5"> <span style="color:var(--text2)">%</span></div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
p.innerHTML = `
|
||||||
|
<div class="props-editor">
|
||||||
|
<h3>Edit Card</h3>
|
||||||
|
<div class="prop-row">
|
||||||
|
<label>Name</label>
|
||||||
|
<input type="text" id="prop-name" value="${card.label}">
|
||||||
|
</div>
|
||||||
|
<div class="prop-row">
|
||||||
|
<label>Type</label>
|
||||||
|
<select id="prop-type">${typeOpts}</select>
|
||||||
|
</div>
|
||||||
|
<div class="prop-row">
|
||||||
|
<label>Size</label>
|
||||||
|
<select id="prop-width">
|
||||||
|
<option value="half" ${card.width!=='full'?'selected':''}>Half width</option>
|
||||||
|
<option value="full" ${card.width==='full'?'selected':''}>Full width</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="prop-row">
|
||||||
|
<label>Color</label>
|
||||||
|
<input type="color" id="prop-color" value="${card.color||'#ff8844'}">
|
||||||
|
<span class="prop-preview" id="prop-color-preview" style="background:${card.color||'#ff8844'}"></span>
|
||||||
|
<button onclick="document.getElementById('prop-color').value='';document.getElementById('prop-color-preview').style.background='var(--card)'" style="padding:6px 10px;border:1px solid var(--border);border-radius:4px;background:var(--card);color:var(--text2);font-size:11px;cursor:pointer">Reset</button>
|
||||||
|
</div>
|
||||||
|
<div class="prop-row">
|
||||||
|
<label>Font Size</label>
|
||||||
|
<input type="range" id="prop-fontsize" min="12" max="36" value="${card.fontSize||18}">
|
||||||
|
<span id="prop-fontsize-val" style="color:var(--text2);min-width:30px">${card.fontSize||18}px</span>
|
||||||
|
</div>
|
||||||
|
${extraFields}
|
||||||
|
<div class="auto-btns">
|
||||||
|
<button class="cancel-btn" onclick="closePopup()">Cancel</button>
|
||||||
|
<button class="save-btn" onclick="saveCardProps('${id}')">Save</button>
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
// Live preview for color picker
|
||||||
|
document.getElementById('prop-color').addEventListener('input', e => {
|
||||||
|
document.getElementById('prop-color-preview').style.background = e.target.value;
|
||||||
|
});
|
||||||
|
document.getElementById('prop-fontsize').addEventListener('input', e => {
|
||||||
|
document.getElementById('prop-fontsize-val').textContent = e.target.value + 'px';
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('popup-overlay').classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveCardProps(id) {
|
||||||
|
const card = findCard(id);
|
||||||
|
if (!card) return;
|
||||||
|
|
||||||
|
card.label = document.getElementById('prop-name').value || card.label;
|
||||||
|
card.type = document.getElementById('prop-type').value;
|
||||||
|
card.width = document.getElementById('prop-width').value;
|
||||||
|
const color = document.getElementById('prop-color').value;
|
||||||
|
card.color = color || null;
|
||||||
|
card.fontSize = parseInt(document.getElementById('prop-fontsize').value) || 18;
|
||||||
|
|
||||||
|
const spEl = document.getElementById('prop-sp');
|
||||||
|
if (spEl) card.sp_default = parseInt(spEl.value) || 0;
|
||||||
|
|
||||||
|
closePopup();
|
||||||
|
renderCards();
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleCardSize(id) {
|
||||||
const page = layout.pages[currentPage];
|
const page = layout.pages[currentPage];
|
||||||
const card = page.cards.find(c => c.id === id);
|
const card = page.cards.find(c => c.id === id);
|
||||||
if (!card) return;
|
if (!card) return;
|
||||||
const name = prompt('New name:', card.label);
|
card.width = card.width === 'full' ? 'half' : 'full';
|
||||||
if (name) { card.label = name; renderCards(); }
|
renderCards();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════
|
||||||
@ -504,6 +839,34 @@ function renameCard(id) {
|
|||||||
function toggleSim(id) {
|
function toggleSim(id) {
|
||||||
if (!simState[id]) simState[id] = {on: false, value: 0};
|
if (!simState[id]) simState[id] = {on: false, value: 0};
|
||||||
simState[id].on = !simState[id].on;
|
simState[id].on = !simState[id].on;
|
||||||
|
|
||||||
|
// Burner turns on Hot Fan automatically
|
||||||
|
const card = findCard(id);
|
||||||
|
if (card && card.type === 'burner') {
|
||||||
|
const fanCard = findCardByLabel('Hot Fan');
|
||||||
|
if (fanCard) {
|
||||||
|
if (!simState[fanCard.id]) simState[fanCard.id] = {on: false, value: fanCard.sp_default || 50};
|
||||||
|
simState[fanCard.id].on = simState[id].on;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is an automation card, activate/deactivate all its rules
|
||||||
|
if (card && card.type === 'automation' && card.rules) {
|
||||||
|
card.rules.forEach(r => {
|
||||||
|
if (!r.target) return;
|
||||||
|
if (!simState[r.target]) simState[r.target] = {on: false, value: 0};
|
||||||
|
if (simState[id].on) {
|
||||||
|
// Activating: apply all rules
|
||||||
|
if (r.action === 'on') simState[r.target].on = true;
|
||||||
|
else if (r.action === 'off') simState[r.target].on = false;
|
||||||
|
else if (r.action === 'set') { simState[r.target].on = true; simState[r.target].value = r.value || 0; }
|
||||||
|
} else {
|
||||||
|
// Deactivating: turn everything off
|
||||||
|
if (r.action !== 'off') simState[r.target].on = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
renderCards();
|
renderCards();
|
||||||
updateSchematicAnimations();
|
updateSchematicAnimations();
|
||||||
}
|
}
|
||||||
@ -540,6 +903,10 @@ function findCard(id) {
|
|||||||
for (const p of layout.pages) { const c = p.cards.find(x => x.id === id); if (c) return c; }
|
for (const p of layout.pages) { const c = p.cards.find(x => x.id === id); if (c) return c; }
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
function findCardByLabel(label) {
|
||||||
|
for (const p of layout.pages) { const c = p.cards.find(x => x.label === label); if (c) return c; }
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// ══════════════════════════════════════════════════════
|
// ══════════════════════════════════════════════════════
|
||||||
// COMMENTS
|
// COMMENTS
|
||||||
@ -654,7 +1021,10 @@ function hideCtx() {
|
|||||||
document.getElementById('eq-context').style.display = 'none';
|
document.getElementById('eq-context').style.display = 'none';
|
||||||
ctxEquip = null;
|
ctxEquip = null;
|
||||||
}
|
}
|
||||||
document.addEventListener('click', e => { if (!e.target.closest('#eq-context')) hideCtx(); });
|
document.addEventListener('click', e => {
|
||||||
|
if (!e.target.closest('#eq-context')) hideCtx();
|
||||||
|
if (!e.target.closest('.add-placeholder')) document.querySelectorAll('.add-menu.open').forEach(m => m.classList.remove('open'));
|
||||||
|
});
|
||||||
|
|
||||||
function ctxAction(action) {
|
function ctxAction(action) {
|
||||||
if (!ctxEquip) return;
|
if (!ctxEquip) return;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user