Warnings and Counters fully functionnal, made the navbar sticky, and improved UX to navigate through tabs

This commit is contained in:
LAHAY Damien 2025-07-04 11:20:00 +02:00
parent 8e006526b3
commit 3f6d55d36b
3 changed files with 339 additions and 16 deletions

View File

@ -6,6 +6,10 @@
<!-- <meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'"> -->
<title>Machine Description Form</title>
<style>
html {
scroll-behavior: smooth; /* Enables smooth scrolling */
scroll-padding-top: 80px; /* Prevents section titles from hiding under the sticky nav */
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background-color: #f4f4f9;
@ -15,7 +19,7 @@
padding: 20px;
}
.container {
max-width: 800px;
max-width: 1000px;
margin: auto;
background: #fff;
padding: 25px;
@ -91,9 +95,29 @@
margin-top: 20px;
transition: background-color 0.3s;
}
input[type="submit"]:hover {
background-color: #2649B2;
}
.next-tab-btn {
background-color: #4A74F3;
color: white;
padding: 12px 20px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 18px;
font-weight: bold;
width: 100%;
margin-top: 20px;
transition: background-color 0.3s;
}
.next-tab-btn:hover {
background-color: #2649B2;
}
.checkbox-container {
display: grid;
grid-template-columns: auto 1fr; /* Checkbox column, then text takes remaining space */
@ -120,6 +144,12 @@
.tab-nav {
border-bottom: 2px solid #D4D9F0;
display: flex;
position: sticky;
top: 0;
background-color: white;
padding: 10px 0;
border-bottom: 2px solid #D4D9F0;
z-index: 1000;
}
.tab-nav button {
background: none;
@ -205,6 +235,10 @@
<nav class="tab-nav">
<button class="tab-btn active" data-tab="machine" id="machine-tab-btn">Machine</button>
<button class="tab-btn" data-tab="alarms" id="alarms-tab-btn">Alarms</button>
<button class="tab-btn" data-tab="warnings" id="warnings-tab-btn">Warnings</button>
<button class="tab-btn" data-tab="counters" id="counters-tab-btn">Counters</button>
<button class="tab-btn" data-tab="setpoints" id="setpoints-tab-btn">Setpoints</button>
<button class="tab-btn" data-tab="realtime-variables" id="realtime-tab-btn">Real Time Variables</button>
</nav>
<form id="machine-form">
@ -365,7 +399,9 @@
</div>
</div>
</fieldset>
<button type="button" class="next-tab-btn">Next</button>
</div>
<div id="alarms-tab" class="tab-content">
<fieldset>
<legend>Alarms Exposed by the Machine</legend>
@ -378,22 +414,159 @@
</table>
<button type="button" class="add-row-btn" id="add-alarm-btn">Add Alarm</button>
</fieldset>
<button type="button" class="next-tab-btn">Next</button>
</div>
<div id="warnings-tab" class="tab-content">
<fieldset>
<legend>Warnings Exposed by the Machine</legend>
<p>Define all warnings for the machine. If the message is empty, the warning will not be generated.</p>
<table class="dynamic-table">
<thead>
<tr><th class="col-id">ID</th><th class="col-name">Name</th><th>Message</th><th class="col-actions">Actions</th></tr>
</thead>
<tbody id="warnings-table-body"></tbody>
</table>
<button type="button" class="add-row-btn" id="add-warning-btn">Add Warning</button>
</fieldset>
<button type="button" class="next-tab-btn">Next</button>
</div>
<div id="counters-tab" class="tab-content">
<fieldset>
<legend>Counters</legend>
<p>
Define all counters for the machine. If the description is empty, the counter will not be generated.
<br>
</p>
<p>
<strong>Note:</strong> The first three counters are predefined by the PackML standard
and are automatically included in the generated nodeset.
<br>PackML counters described by default contain only the first entry of each array.
</p>
<p>
You can add more counters as needed. Additional defective unit counter should be provided per type of defect (bad open flap, underfill, not printed, bad pump orientation, ...etc.) and station (head 1, nozzleX,....etc.).
</p>
<table class="dynamic-table">
<thead>
<tr><th class="col-id">ID</th><th class="col-name">Name</th><th>Type</th><th class="col-description">Description</th><th class="col-actions">Actions</th></tr>
</thead>
<tbody>
<tr>
<td><input type="text" value="-" disabled></td>
<td><input type="text" value="ProdProcessedCount" disabled></td>
<td><input type="text" value="ProductionCounter" disabled></td>
<td><textarea rows="2" cols="30" disabled>Total products processed (PackML standard)</textarea></td>
<td><span style="color: #999;">Predefined</span></td>
</tr>
<tr>
<td><input type="text" value="-" disabled></td>
<td><input type="text" value="ProdDefectiveCount" disabled></td>
<td><input type="text" value="DefectiveCounter" disabled></td>
<td><textarea rows="2" cols="30" disabled>Total defective products (PackML standard)</textarea></td>
<td><span style="color: #999;">Predefined</span></td>
</tr>
<tr>
<td><input type="text" value="-" disabled></td>
<td><input type="text" value="CycleCount" disabled></td>
<td><input type="text" value="CycleCounter" disabled></td>
<td><textarea rows="2" cols="30" disabled>Total machine cycles (PackML standard)</textarea></td>
<td><span style="color: #999;">Predefined</span></td>
</tr>
</tbody>
<tbody id="counters-table-body"></tbody>
</table>
<button type="button" class="add-row-btn" id="add-counter-btn">Add Counter</button>
</fieldset>
<button type="button" class="next-tab-btn">Next</button>
</div>
<div id="setpoints-tab" class="tab-content">
<fieldset>
<legend>Setpoints</legend>
<p>Define all setpoints for the machine. If the description is empty, the setpoint will not be generated.</p>
<table class="dynamic-table">
<thead>
<tr><th class="col-id">ID</th><th class="col-name">Name</th><th>Description</th><th class="col-actions">Actions</th></tr>
</thead>
<tbody id="setpoints-table-body"></tbody>
</table>
<button type="button" class="add-row-btn" id="add-setpoint-btn">Add Setpoint</button>
</fieldset>
<button type="button" class="next-tab-btn">Next</button>
</div>
<div id="realtime-tab" class="tab-content">
<fieldset>
<legend>Real Time Variables</legend>
<p>Define all real time variables for the machine. If the description is empty, the variable will not be generated.</p>
<table class="dynamic-table">
<thead>
<tr><th class="col-id">ID</th><th class="col-name">Name</th><th>Description</th><th class="col-actions">Actions</th></tr>
</thead>
<tbody id="realtime-table-body"></tbody>
</table>
<button type="button" class="add-row-btn" id="add-realtime-btn">Add Real Time Variable</button>
</fieldset>
<input type="submit" value="Validate and Submit Description">
</div>
<input type="submit" value="Validate and Submit Description">
</form>
</div>
<template id="alarm-row-template">
<tr>
<td><input type="number" name="alarm_id" min="1" placeholder="e.g., 1"></td>
<td><input type="text" name="alarm_name" placeholder="e.g., ALARM_1"></td>
<td><textarea name="alarm_message" rows="1" cols="50" placeholder="Message displayed on dashboard"></textarea></td>
<td><input type="text" name="alarm_name" placeholder="e.g., ALARM_001"></td>
<td><textarea name="alarm_message" rows="1" cols="50" placeholder="Message displayed to the operator"></textarea></td>
<td class="col-actions"><button type="button" class="remove-row-btn">Remove</button></td>
</tr>
</template>
<template id="warning-row-template">
<tr>
<td><input type="number" name="warning_id" min="1" placeholder="e.g., 1"></td>
<td><input type="text" name="warning_name" placeholder="e.g., WARNING_001"></td>
<td><textarea name="warning_message" rows="1" cols="50" placeholder="Message displayed to the operator"></textarea></td>
<td class="col-actions"><button type="button" class="remove-row-btn">Remove</button></td>
</tr>
</template>
<template id="realtime-row-template">
<tr>
<td><input type="number" name="realtime_id" min="1" placeholder="e.g., 1"></td>
<td><input type="text" name="realtime_name" placeholder="e.g., REALTIME_001"></td>
<td><textarea name="realtime_message" rows="1" cols="50" placeholder="Message displayed to the operator"></textarea></td>
<td class="col-actions"><button type="button" class="remove-row-btn">Remove</button></td>
</tr>
</template>
<template id="counter-row-template">
<tr>
<td><input type="number" name="counter_id" min="1" placeholder="e.g., 1"></td>
<td><input type="text" name="counter_name" placeholder="e.g., COUNTER_001"></td>
<td><select name="counter_type">
<option value="productionCounter">Production Counter</option>
<option value="defectiveCounter">Defective Counter</option>
<option value="cycleCounter">Cycle Counter</option>
<option value="otherCounter">Other</option>
</select></td>
<td><textarea name="counter_description" rows="1" cols="30" placeholder="Description of the counter"></textarea></td>
<td class="col-actions"><button type="button" class="remove-row-btn">Remove</button></td>
</tr>
</template>
<template id="setpoint-row-template">
<tr>
<td><input type="number" name="setpoint_id" min="1" placeholder="e.g., 1"></td>
<td><input type="text" name="setpoint_name" placeholder="e.g., SETPOINT_001"></td>
<td><textarea name="setpoint_message" rows="1" cols="30" placeholder="Message displayed to the operator"></textarea></td>
<td class="col-actions"><button type="button" class="remove-row-btn">Remove</button></td>
</tr>
</template>
<script src="renderer.js"></script>
</body>

51
main.js
View File

@ -53,18 +53,37 @@ app.whenReady().then(() => {
yamlLines.push(`${sp2}version: "${data.version || 'Unset'}"`);
// 2. Build the 'alarms' section from the alarms data
if (data.alarms && data.alarms.length > 0) {
if (data.alarms) {
yamlLines.push(''); // Blank line for separation
yamlLines.push(`${sp2}alarms:`);
yamlLines.push(`${sp4}packing: OBJECTS`); // As per PowerShell script logic
yamlLines.push(`${sp4}behavior: ${data.alarmBehavior || 'STATIC'}`);
yamlLines.push(`${sp4}count: ${data.alarms.length}`);
yamlLines.push(`${sp4}alarms: `); // Note the trailing space
yamlLines.push(`${sp4}count: ${data.alarmCount}`);
if (data.alarms.length > 0) {
yamlLines.push(`${sp4}alarms: `); // Note the trailing space
data.alarms.forEach(alarm => {
const id = alarm.id || 0;
const name = alarm.name || `Alarm_${id}`;
const message = alarm.message.replace(/:/g, ';'); // Sanitize message as in PowerShell
data.alarms.forEach(alarm => {
const id = alarm.id || 0;
const name = alarm.name || `Alarm_${id}`;
const message = alarm.message.replace(/:/g, ';'); // Sanitize message as in PowerShell
yamlLines.push(`${sp6}- ID: ${id}`);
yamlLines.push(`${sp8}name: ${name}`);
yamlLines.push(`${sp8}message: ${message}`);
});
}
}
//3. Build the 'warning' section from the warnings data
if (data.warnings && data.warnings.length > 0) {
yamlLines.push(''); // Blank line for separation
yamlLines.push(`${sp2}warnings:`);
yamlLines.push(`${sp4}count: ${data.warnings.length}`);
yamlLines.push(`${sp4}warnings: `); // Note the trailing space
data.warnings.forEach(warning => {
const id = warning.id || 0;
const name = warning.name || `Warning_${id}`;
const message = warning.message.replace(/:/g, ';'); // Sanitize message as in PowerShell
yamlLines.push(`${sp6}- ID: ${id}`);
yamlLines.push(`${sp8}name: ${name}`);
@ -72,6 +91,24 @@ app.whenReady().then(() => {
});
}
//Build the 'counters' section from the counters data
if (data.counters && data.counters.length > 0) {
yamlLines.push(''); // Blank line for separation
yamlLines.push(`${sp2}counters:`);
data.counters.forEach(counter => {
const id = counter.id || 0;
const name = counter.name || `Counter_${id}`;
const type = counter.type || '';
const description = counter.description || '';
yamlLines.push(`${sp4}- name: ${name}`);
yamlLines.push(`${sp6}ID: ${id}`);
yamlLines.push(`${sp6}type: ${type}`);
yamlLines.push(`${sp6}description: ${description}`);
});
}
// Build and Append the 'optionals' section if needed
if (data.stopReason === 'Yes') {
yamlLines.push(''); // Add a blank line for spacing

View File

@ -53,6 +53,58 @@ document.addEventListener('DOMContentLoaded', () => {
alarmsTableBody.appendChild(clone);
});
const addWarningBtn = document.getElementById('add-warning-btn');
const warningsTableBody = document.getElementById('warnings-table-body');
const warningTemplate = document.getElementById('warning-row-template');
addWarningBtn.addEventListener('click', () => {
const currentIdInputs = warningsTableBody.querySelectorAll('[name="warning_id"]');
let maxId = 0;
currentIdInputs.forEach(input => {
const currentId = parseInt(input.value, 10);
if (!isNaN(currentId) && currentId > maxId) {
maxId = currentId;
}
});
const nextWarningId = maxId + 1;
const paddedWarningId = nextWarningId.toString().padStart(3, '0');
const clone = warningTemplate.content.cloneNode(true);
clone.querySelector('[name="warning_id"]').value = nextWarningId;
clone.querySelector('[name="warning_name"]').value = `WARNING_${paddedWarningId}`;
const removeBtn = clone.querySelector('.remove-row-btn');
removeBtn.addEventListener('click', (e) => {
e.target.closest('tr').remove();
});
warningsTableBody.appendChild(clone);
});
//dynamic row for the counter tab
const addCounterBtn = document.getElementById('add-counter-btn');
const countersTableBody = document.getElementById('counters-table-body');
const counterTemplate = document.getElementById('counter-row-template');
addCounterBtn.addEventListener('click', () => {
const currentIdInputs = countersTableBody.querySelectorAll('[name="counter_id"]');
let maxId = 0;
currentIdInputs.forEach(input => {
const currentId = parseInt(input.value, 10);
if (!isNaN(currentId) && currentId > maxId) {
maxId = currentId;
}
});
const nextCounterId = maxId + 1;
const paddedCounterId = nextCounterId.toString().padStart(3, '0');
const clone = counterTemplate.content.cloneNode(true);
clone.querySelector('[name="counter_id"]').value = nextCounterId;
clone.querySelector('[name="counter_name"]').value = `COUNTER_${paddedCounterId}`;
const removeBtn = clone.querySelector('.remove-row-btn');
removeBtn.addEventListener('click', (e) => {
e.target.closest('tr').remove();
});
countersTableBody.appendChild(clone);
});
// --- 3. FORM SUBMISSION LOGIC ---
const form = document.getElementById('machine-form');
@ -86,8 +138,37 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
// D. Collect all warning rows from the table
const warningRows = warningsTableBody.querySelectorAll('tr');
const warningsData = [];
warningRows.forEach(row => {
const warning = {
id: row.querySelector('[name="warning_id"]').value,
name: row.querySelector('[name="warning_name"]').value,
message: row.querySelector('[name="warning_message"]').value,
};
if (warning.message) {
warningsData.push(warning);
}
});
// E. Collect all counter rows from the table
const counterRows = countersTableBody.querySelectorAll('tr');
const countersData = [];
counterRows.forEach(row => {
const counter = {
id: row.querySelector('[name="counter_id"]').value,
name: row.querySelector('[name="counter_name"]').value,
type: row.querySelector('[name="counter_type"]').value,
description: row.querySelector('[name="counter_description"]').value,
};
if (counter.type) {
countersData.push(counter);
}
});
// C. Combine all data into a single object
const finalData = { ...mainData, alarms: alarmsData };
const finalData = { ...mainData, alarms: alarmsData, warnings: warningsData, counters: countersData };
console.log('Sending all collected data to main process:', finalData);
@ -106,4 +187,36 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
}
// --- 4. TAB NAVIGATION BUTTONS ---
const tabOrder = [
'machine-tab',
'alarms-tab',
'warnings-tab',
'counters-tab',
'setpoints-tab',
'realtime-tab'
];
// Add event listeners to all "Next" buttons
document.querySelectorAll('.next-tab-btn').forEach((btn, idx) => {
btn.addEventListener('click', () => {
// Move to the next tab
const nextTabId = tabOrder[idx + 1];
if (nextTabId) {
// Switch tab button active state
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
document.querySelector(`[data-tab="${nextTabId.replace('-tab','')}"]`).classList.add('active');
// Switch tab content active state
document.querySelectorAll('.tab-content').forEach(tc => tc.classList.remove('active'));
document.getElementById(nextTabId).classList.add('active');
// Show submit button only on last tab
if (nextTabId === 'realtime-tab') {
document.getElementById('submit-container').style.display = '';
} else {
document.getElementById('submit-container').style.display = 'none';
}
}
});
});
});