434 lines
14 KiB
Python
434 lines
14 KiB
Python
#!/usr/bin/env python3
|
|
import json
|
|
|
|
# Read flows.json
|
|
with open('/home/mdares/.node-red/flows.json', 'r') as f:
|
|
flows = json.load(f)
|
|
|
|
# Find Home template
|
|
home_template_node = None
|
|
for node in flows:
|
|
if node.get('id') == '1821c4842945ecd8':
|
|
home_template_node = node
|
|
break
|
|
|
|
if not home_template_node:
|
|
print("❌ ERROR: Could not find Home template")
|
|
exit(1)
|
|
|
|
template_code = home_template_node.get('format', '')
|
|
|
|
# ============================================================================
|
|
# PART 1: Add message handler for stop-prompt
|
|
# ============================================================================
|
|
|
|
# Find where to insert the handler - after scrapPrompt handler
|
|
insert_point = template_code.find("if (msg.topic === 'scrapPrompt')")
|
|
|
|
if insert_point < 0:
|
|
print("❌ ERROR: Could not find scrapPrompt handler")
|
|
exit(1)
|
|
|
|
# Find the end of the scrapPrompt handler block (find the next "if (msg" after it)
|
|
next_handler_start = template_code.find("\n if (msg", insert_point + 100)
|
|
if next_handler_start < 0:
|
|
# Try to find end of watch block
|
|
next_handler_start = template_code.find("\n });", insert_point + 100)
|
|
|
|
stop_prompt_handler = '''
|
|
// Stop Reason Prompt Handler
|
|
if (!scope.stopPrompt) {
|
|
scope.stopPrompt = {
|
|
show: false,
|
|
selectedCategory: null,
|
|
selectedReason: null,
|
|
notes: ''
|
|
};
|
|
}
|
|
|
|
if (msg._mode === 'stop-prompt') {
|
|
scope.stopPrompt.show = true;
|
|
scope.stopPrompt.selectedCategory = null;
|
|
scope.stopPrompt.selectedReason = null;
|
|
scope.stopPrompt.notes = '';
|
|
scope.$applyAsync();
|
|
return;
|
|
}
|
|
'''
|
|
|
|
# Insert the handler
|
|
template_code = template_code[:next_handler_start] + stop_prompt_handler + template_code[next_handler_start:]
|
|
|
|
print("✅ Added stop prompt message handler")
|
|
|
|
# ============================================================================
|
|
# PART 2: Add stop prompt modal HTML
|
|
# ============================================================================
|
|
|
|
# Find where scrap modal is defined (should be near the end, before </div> closing tags)
|
|
# Look for closing div tags at the end
|
|
scrap_modal_end = template_code.rfind('</div>\n</div>\n\n<script')
|
|
|
|
if scrap_modal_end < 0:
|
|
print("❌ ERROR: Could not find insertion point for modal HTML")
|
|
exit(1)
|
|
|
|
stop_modal_html = '''
|
|
<!-- Stop Reason Modal -->
|
|
<div class="modal" ng-show="stopPrompt.show" ng-click="stopPrompt.show = false">
|
|
<div class="modal-overlay"></div>
|
|
<div class="modal-card modal-card-stop" ng-click="$event.stopPropagation()">
|
|
<div class="modal-header-stop">
|
|
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
|
<span style="font-size: 1.5rem;">⚠️</span>
|
|
<span>Production Stopped</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-body">
|
|
<p class="stop-question">Why are you stopping production?</p>
|
|
|
|
<!-- Planned Stops Section -->
|
|
<div class="stop-category-section">
|
|
<div class="stop-category-header planned-header">
|
|
<span>📋 Planned Stops</span>
|
|
<span class="stop-category-note">(Won't affect availability)</span>
|
|
</div>
|
|
<div class="stop-reasons-grid">
|
|
<button class="stop-reason-option planned"
|
|
ng-class="{selected: stopPrompt.selectedCategory === 'planned' && stopPrompt.selectedReason === 'Lunch break'}"
|
|
ng-click="selectStopReason('planned', 'Lunch break')">
|
|
<span class="reason-icon">🍽️</span>
|
|
<span class="reason-text">Lunch break</span>
|
|
</button>
|
|
<button class="stop-reason-option planned"
|
|
ng-class="{selected: stopPrompt.selectedCategory === 'planned' && stopPrompt.selectedReason === 'Scheduled break'}"
|
|
ng-click="selectStopReason('planned', 'Scheduled break')">
|
|
<span class="reason-icon">☕</span>
|
|
<span class="reason-text">Scheduled break</span>
|
|
</button>
|
|
<button class="stop-reason-option planned"
|
|
ng-class="{selected: stopPrompt.selectedCategory === 'planned' && stopPrompt.selectedReason === 'Shift change'}"
|
|
ng-click="selectStopReason('planned', 'Shift change')">
|
|
<span class="reason-icon">🔄</span>
|
|
<span class="reason-text">Shift change</span>
|
|
</button>
|
|
<button class="stop-reason-option planned"
|
|
ng-class="{selected: stopPrompt.selectedCategory === 'planned' && stopPrompt.selectedReason === 'Planned maintenance'}"
|
|
ng-click="selectStopReason('planned', 'Planned maintenance')">
|
|
<span class="reason-icon">🔧</span>
|
|
<span class="reason-text">Planned maintenance</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Unplanned Stops Section -->
|
|
<div class="stop-category-section">
|
|
<div class="stop-category-header unplanned-header">
|
|
<span>🚨 Unplanned Stops</span>
|
|
<span class="stop-category-note">(Will affect availability)</span>
|
|
</div>
|
|
<div class="stop-reasons-grid">
|
|
<button class="stop-reason-option unplanned"
|
|
ng-class="{selected: stopPrompt.selectedCategory === 'unplanned' && stopPrompt.selectedReason === 'Machine malfunction'}"
|
|
ng-click="selectStopReason('unplanned', 'Machine malfunction')">
|
|
<span class="reason-icon">⚙️</span>
|
|
<span class="reason-text">Machine malfunction</span>
|
|
</button>
|
|
<button class="stop-reason-option unplanned"
|
|
ng-class="{selected: stopPrompt.selectedCategory === 'unplanned' && stopPrompt.selectedReason === 'Material shortage'}"
|
|
ng-click="selectStopReason('unplanned', 'Material shortage')">
|
|
<span class="reason-icon">📦</span>
|
|
<span class="reason-text">Material shortage</span>
|
|
</button>
|
|
<button class="stop-reason-option unplanned"
|
|
ng-class="{selected: stopPrompt.selectedCategory === 'unplanned' && stopPrompt.selectedReason === 'Quality issue'}"
|
|
ng-click="selectStopReason('unplanned', 'Quality issue')">
|
|
<span class="reason-icon">❌</span>
|
|
<span class="reason-text">Quality issue</span>
|
|
</button>
|
|
<button class="stop-reason-option unplanned"
|
|
ng-class="{selected: stopPrompt.selectedCategory === 'unplanned' && stopPrompt.selectedReason === 'Operator error'}"
|
|
ng-click="selectStopReason('unplanned', 'Operator error')">
|
|
<span class="reason-icon">👤</span>
|
|
<span class="reason-text">Operator error</span>
|
|
</button>
|
|
<button class="stop-reason-option unplanned"
|
|
ng-class="{selected: stopPrompt.selectedCategory === 'unplanned' && stopPrompt.selectedReason === 'Other'}"
|
|
ng-click="selectStopReason('unplanned', 'Other')">
|
|
<span class="reason-icon">❓</span>
|
|
<span class="reason-text">Other</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Notes Section -->
|
|
<div class="stop-notes-section">
|
|
<label class="stop-notes-label">Additional notes (optional):</label>
|
|
<textarea class="stop-notes-input"
|
|
ng-model="stopPrompt.notes"
|
|
placeholder="Enter any additional details about the stop..."></textarea>
|
|
</div>
|
|
|
|
<!-- Action Buttons -->
|
|
<div class="modal-actions">
|
|
<button class="btn-cancel" ng-click="stopPrompt.show = false">Cancel</button>
|
|
<button class="btn-primary"
|
|
ng-disabled="!stopPrompt.selectedReason"
|
|
ng-click="submitStopReason()">
|
|
Submit Stop Reason
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
'''
|
|
|
|
# Insert the modal HTML
|
|
template_code = template_code[:scrap_modal_end] + stop_modal_html + template_code[scrap_modal_end:]
|
|
|
|
print("✅ Added stop prompt modal HTML")
|
|
|
|
# ============================================================================
|
|
# PART 3: Add CSS for stop modal (matching scrap modal theme)
|
|
# ============================================================================
|
|
|
|
# Find where CSS is defined (look for <style> tag)
|
|
css_end = template_code.find('</style>')
|
|
|
|
if css_end < 0:
|
|
print("❌ ERROR: Could not find CSS section")
|
|
exit(1)
|
|
|
|
stop_modal_css = '''
|
|
/* Stop Reason Modal Styling */
|
|
.modal-card-stop {
|
|
width: min(90vw, 36rem);
|
|
max-height: 85vh;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.modal-header-stop {
|
|
background: linear-gradient(135deg, #d32f2f 0%, #f44336 100%);
|
|
color: white;
|
|
padding: 1rem;
|
|
font-size: var(--fs-section-title);
|
|
font-weight: 600;
|
|
border-radius: 0.5rem 0.5rem 0 0;
|
|
}
|
|
|
|
.stop-question {
|
|
font-size: var(--fs-label-lg);
|
|
text-align: center;
|
|
margin-bottom: 1rem;
|
|
color: var(--text);
|
|
font-weight: 500;
|
|
}
|
|
|
|
.stop-category-section {
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.stop-category-header {
|
|
padding: 0.5rem 0.75rem;
|
|
border-radius: 0.25rem;
|
|
margin-bottom: 0.5rem;
|
|
font-weight: 600;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
}
|
|
|
|
.stop-category-header.planned-header {
|
|
background: #e8f5e9;
|
|
color: #2e7d32;
|
|
}
|
|
|
|
.stop-category-header.unplanned-header {
|
|
background: #ffebee;
|
|
color: #c62828;
|
|
}
|
|
|
|
.stop-category-note {
|
|
font-size: 0.75rem;
|
|
font-weight: 400;
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.stop-reasons-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
|
|
gap: 0.5rem;
|
|
}
|
|
|
|
.stop-reason-option {
|
|
padding: 0.75rem;
|
|
border: 2px solid var(--border);
|
|
background: white;
|
|
border-radius: 0.5rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
text-align: center;
|
|
}
|
|
|
|
.stop-reason-option:hover {
|
|
border-color: var(--accent);
|
|
background: var(--surface);
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
}
|
|
|
|
.stop-reason-option.selected {
|
|
border-color: var(--accent);
|
|
background: var(--accent);
|
|
color: white;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.stop-reason-option.planned {
|
|
border-left: 4px solid #4caf50;
|
|
}
|
|
|
|
.stop-reason-option.unplanned {
|
|
border-left: 4px solid #f44336;
|
|
}
|
|
|
|
.stop-reason-option.planned.selected {
|
|
border-color: #4caf50;
|
|
background: #4caf50;
|
|
}
|
|
|
|
.stop-reason-option.unplanned.selected {
|
|
border-color: #f44336;
|
|
background: #f44336;
|
|
}
|
|
|
|
.reason-icon {
|
|
font-size: 1.5rem;
|
|
}
|
|
|
|
.reason-text {
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
.stop-notes-section {
|
|
margin-bottom: 1rem;
|
|
}
|
|
|
|
.stop-notes-label {
|
|
display: block;
|
|
margin-bottom: 0.5rem;
|
|
font-weight: 500;
|
|
color: var(--text);
|
|
}
|
|
|
|
.stop-notes-input {
|
|
width: 100%;
|
|
padding: 0.75rem;
|
|
border: 1px solid var(--border);
|
|
border-radius: 0.375rem;
|
|
font-family: inherit;
|
|
font-size: var(--fs-body);
|
|
resize: vertical;
|
|
min-height: 4rem;
|
|
background: white;
|
|
}
|
|
|
|
.stop-notes-input:focus {
|
|
outline: none;
|
|
border-color: var(--accent);
|
|
box-shadow: 0 0 0 3px rgba(33, 150, 243, 0.1);
|
|
}
|
|
|
|
'''
|
|
|
|
template_code = template_code[:css_end] + stop_modal_css + template_code[css_end:]
|
|
|
|
print("✅ Added stop prompt CSS")
|
|
|
|
# ============================================================================
|
|
# PART 4: Add JavaScript functions for stop modal
|
|
# ============================================================================
|
|
|
|
# Find where JavaScript functions are (after <script> tag)
|
|
script_start = template_code.find('<script>')
|
|
if script_start < 0:
|
|
print("❌ ERROR: Could not find script section")
|
|
exit(1)
|
|
|
|
# Find a good insertion point - after scope definition
|
|
scope_start = template_code.find('(function(scope) {', script_start)
|
|
if scope_start < 0:
|
|
print("❌ ERROR: Could not find scope function")
|
|
exit(1)
|
|
|
|
insertion_point = scope_start + len('(function(scope) {') + 1
|
|
|
|
stop_modal_js = '''
|
|
// ========================================================================
|
|
// Stop Reason Modal Functions
|
|
// ========================================================================
|
|
|
|
scope.selectStopReason = function(category, reason) {
|
|
scope.stopPrompt.selectedCategory = category;
|
|
scope.stopPrompt.selectedReason = reason;
|
|
};
|
|
|
|
scope.submitStopReason = function() {
|
|
if (!scope.stopPrompt.selectedCategory || !scope.stopPrompt.selectedReason) {
|
|
return;
|
|
}
|
|
|
|
// Send stop reason to Node-RED
|
|
scope.send({
|
|
action: 'stop-reason',
|
|
payload: {
|
|
category: scope.stopPrompt.selectedCategory,
|
|
reason: scope.stopPrompt.selectedReason,
|
|
notes: scope.stopPrompt.notes || ''
|
|
}
|
|
});
|
|
|
|
// Close the modal
|
|
scope.stopPrompt.show = false;
|
|
};
|
|
|
|
'''
|
|
|
|
template_code = template_code[:insertion_point] + stop_modal_js + template_code[insertion_point:]
|
|
|
|
print("✅ Added stop prompt JavaScript functions")
|
|
|
|
# Update the node
|
|
home_template_node['format'] = template_code
|
|
|
|
# Write updated flows
|
|
with open('/home/mdares/.node-red/flows.json', 'w') as f:
|
|
json.dump(flows, f, indent=4)
|
|
|
|
print("\n" + "="*60)
|
|
print("✅ STOP PROMPT ADDED TO HOME TEMPLATE")
|
|
print("="*60)
|
|
print("\n📋 What was added:")
|
|
print(" 1. Message handler for _mode='stop-prompt'")
|
|
print(" 2. Stop reason modal HTML (matches scrap modal style)")
|
|
print(" 3. CSS styling for stop modal")
|
|
print(" 4. JavaScript functions: selectStopReason, submitStopReason")
|
|
print("\n✨ Features:")
|
|
print(" - Planned stops: Lunch, Break, Shift change, Maintenance")
|
|
print(" - Unplanned stops: Malfunction, Material, Quality, Operator, Other")
|
|
print(" - Optional notes field")
|
|
print(" - Visual categorization (green=planned, red=unplanned)")
|
|
print(" - Sends action='stop-reason' back to Work Order buttons")
|
|
print("\n🧪 To test:")
|
|
print(" 1. Restart Node-RED")
|
|
print(" 2. Start a work order")
|
|
print(" 3. Click START to begin production")
|
|
print(" 4. Click STOP - modal should appear immediately")
|
|
print(" 5. Select a reason and submit")
|
|
print(" 6. Verify tracking is disabled and reason is logged")
|