Add create new part functionality to library browser

- Add UI to create parts from missing IPNs in spreadsheet
- "+ Add New Part" button switches to create part view
- Auto-populate fields from spreadsheet (description, class, manufacturer, MPN)
- Normalize class field (uppercase, replace special chars with underscores)
- Add optional datasheet URL field
- Exclude section header rows (no manufacturer/MPN)
- Cancel button returns to browser view
- Align logo banner and h1 header to left

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
brentperteet
2026-02-23 09:30:19 -06:00
parent 55235f222b
commit bcb2c70e93
2 changed files with 340 additions and 10 deletions

View File

@@ -18,6 +18,7 @@
color: #333;
border-bottom: 3px solid #007bff;
padding-bottom: 10px;
text-align: left;
}
.arg-container {
background: white;
@@ -265,7 +266,7 @@
</style>
</head>
<body>
<img src="{{ url_for('static', filename='logo_banner.png') }}" alt="Banner" style="width: 100%; max-width: 800px; display: block; margin: 0 auto 20px auto;">
<img src="{{ url_for('static', filename='logo_banner.png') }}" alt="Banner" style="width: 100%; max-width: 800px; display: block; margin: 0 0 20px 0;">
<div id="status" class="status disconnected">Disconnected</div>
@@ -375,17 +376,62 @@
<div class="container">
<h2>Parts Library Browser</h2>
<div style="margin: 20px 0;">
<input type="text" id="librarySearchInput" placeholder="Search by IPN, MPN, Manufacturer, or Description..."
style="width: 70%; padding: 10px; border: 1px solid #dee2e6; border-radius: 5px; font-size: 14px;">
<button id="librarySearchBtn" class="btn" style="margin-left: 10px;">Search</button>
<button id="libraryClearBtn" class="btn secondary" style="margin-left: 5px;">Clear</button>
<!-- Browser View -->
<div id="libraryBrowserView">
<div style="margin: 20px 0;">
<input type="text" id="librarySearchInput" placeholder="Search by IPN, MPN, Manufacturer, or Description..."
style="width: 70%; padding: 10px; border: 1px solid #dee2e6; border-radius: 5px; font-size: 14px;">
<button id="librarySearchBtn" class="btn" style="margin-left: 10px;">Search</button>
<button id="libraryClearBtn" class="btn secondary" style="margin-left: 5px;">Clear</button>
<button id="showCreatePartBtn" class="btn" style="margin-left: 10px; background-color: #28a745;">+ Add New Part</button>
</div>
<div id="libraryMessage" class="message"></div>
<div id="libraryResults" style="margin-top: 20px;">
<p style="color: #6c757d;">Enter a search query or click Search to browse all parts</p>
</div>
</div>
<div id="libraryMessage" class="message"></div>
<div id="libraryResults" style="margin-top: 20px;">
<p style="color: #6c757d;">Enter a search query or click Search to browse all parts</p>
<!-- Create Part View -->
<div id="libraryCreateView" style="display: none;">
<div style="margin: 20px 0; padding: 20px; background: #f8f9fa; border-radius: 8px;">
<h3>Create New Part</h3>
<div id="createPartFormLoading" style="padding: 20px; text-align: center;">
<p>Loading missing IPNs from spreadsheet...</p>
</div>
<div id="createPartForm" style="display: none;">
<div style="margin-bottom: 15px;">
<label style="display: block; font-weight: bold; margin-bottom: 5px;">Select IPN:</label>
<select id="ipnDropdown" style="width: 100%; padding: 8px; border: 1px solid #dee2e6; border-radius: 5px; font-size: 14px;">
<option value="">-- Select an IPN --</option>
</select>
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; font-weight: bold; margin-bottom: 5px;">Description:</label>
<input type="text" id="descriptionInput" readonly style="width: 100%; padding: 8px; border: 1px solid #dee2e6; border-radius: 5px; font-size: 14px; background-color: #e9ecef;">
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; font-weight: bold; margin-bottom: 5px;">Class:</label>
<input type="text" id="classInput" readonly style="width: 100%; padding: 8px; border: 1px solid #dee2e6; border-radius: 5px; font-size: 14px; background-color: #e9ecef;">
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; font-weight: bold; margin-bottom: 5px;">Manufacturer:</label>
<input type="text" id="manufacturerInput" readonly style="width: 100%; padding: 8px; border: 1px solid #dee2e6; border-radius: 5px; font-size: 14px; background-color: #e9ecef;">
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; font-weight: bold; margin-bottom: 5px;">MPN:</label>
<input type="text" id="mpnInput" readonly style="width: 100%; padding: 8px; border: 1px solid #dee2e6; border-radius: 5px; font-size: 14px; background-color: #e9ecef;">
</div>
<div style="margin-bottom: 15px;">
<label style="display: block; font-weight: bold; margin-bottom: 5px;">Datasheet URL (optional):</label>
<input type="text" id="datasheetInput" placeholder="https://..." style="width: 100%; padding: 8px; border: 1px solid #dee2e6; border-radius: 5px; font-size: 14px;">
</div>
<button id="createPartBtn" class="btn" disabled>Create Part</button>
<button id="cancelCreatePartBtn" class="btn secondary" style="margin-left: 10px;">Cancel</button>
</div>
<div id="createPartMessage" class="message"></div>
</div>
</div>
</div>
</div><!-- End libraryTab -->
@@ -926,6 +972,152 @@
showLibraryMessage('Error: ' + data.error, 'error');
});
// ========================================
// Create New Part
// ========================================
const libraryBrowserView = document.getElementById('libraryBrowserView');
const libraryCreateView = document.getElementById('libraryCreateView');
const showCreatePartBtn = document.getElementById('showCreatePartBtn');
const cancelCreatePartBtn = document.getElementById('cancelCreatePartBtn');
const createPartFormLoading = document.getElementById('createPartFormLoading');
const createPartForm = document.getElementById('createPartForm');
const createPartMessage = document.getElementById('createPartMessage');
const ipnDropdown = document.getElementById('ipnDropdown');
const descriptionInput = document.getElementById('descriptionInput');
const classInput = document.getElementById('classInput');
const manufacturerInput = document.getElementById('manufacturerInput');
const mpnInput = document.getElementById('mpnInput');
const datasheetInput = document.getElementById('datasheetInput');
const createPartBtn = document.getElementById('createPartBtn');
let missingPartsData = [];
function showCreatePartMessage(text, type) {
createPartMessage.textContent = text;
createPartMessage.className = 'message ' + type;
createPartMessage.style.display = 'block';
}
// Show create part view
showCreatePartBtn.addEventListener('click', () => {
libraryBrowserView.style.display = 'none';
libraryCreateView.style.display = 'block';
createPartFormLoading.style.display = 'block';
createPartForm.style.display = 'none';
createPartMessage.style.display = 'none';
socket.emit('get_missing_ipns');
});
// Cancel and return to browser
cancelCreatePartBtn.addEventListener('click', () => {
libraryCreateView.style.display = 'none';
libraryBrowserView.style.display = 'block';
// Clear form
ipnDropdown.innerHTML = '<option value="">-- Select an IPN --</option>';
descriptionInput.value = '';
classInput.value = '';
manufacturerInput.value = '';
mpnInput.value = '';
datasheetInput.value = '';
createPartBtn.disabled = true;
missingPartsData = [];
});
socket.on('missing_ipns_result', (data) => {
createPartFormLoading.style.display = 'none';
if (data.count === 0) {
showCreatePartMessage('No missing IPNs found. All parts from spreadsheet are in database!', 'success');
createPartForm.style.display = 'none';
return;
}
missingPartsData = data.parts;
// Populate dropdown
ipnDropdown.innerHTML = '<option value="">-- Select an IPN --</option>';
for (const part of data.parts) {
const option = document.createElement('option');
option.value = part.ipn;
option.textContent = `${part.ipn} - ${part.description}`;
ipnDropdown.appendChild(option);
}
createPartForm.style.display = 'block';
showCreatePartMessage(`Found ${data.count} missing IPN(s). Select one to create in database.`, 'success');
});
ipnDropdown.addEventListener('change', () => {
const selectedIpn = ipnDropdown.value;
if (!selectedIpn) {
descriptionInput.value = '';
classInput.value = '';
manufacturerInput.value = '';
mpnInput.value = '';
createPartBtn.disabled = true;
return;
}
const part = missingPartsData.find(p => p.ipn === selectedIpn);
if (part) {
descriptionInput.value = part.description;
classInput.value = part.class;
manufacturerInput.value = part.manufacturer;
mpnInput.value = part.mpn;
createPartBtn.disabled = false;
}
});
createPartBtn.addEventListener('click', () => {
const selectedIpn = ipnDropdown.value;
if (!selectedIpn) return;
const part = missingPartsData.find(p => p.ipn === selectedIpn);
if (!part) return;
createPartBtn.disabled = true;
showCreatePartMessage('Creating part in database...', 'info');
socket.emit('create_part', {
ipn: part.ipn,
description: part.description,
class: part.class,
manufacturer: part.manufacturer,
mpn: part.mpn,
datasheet: datasheetInput.value.trim()
});
});
socket.on('part_created', (data) => {
showCreatePartMessage(data.message, 'success');
createPartBtn.disabled = false;
// Remove created part from dropdown
const selectedIpn = ipnDropdown.value;
missingPartsData = missingPartsData.filter(p => p.ipn !== selectedIpn);
ipnDropdown.innerHTML = '<option value="">-- Select an IPN --</option>';
for (const part of missingPartsData) {
const option = document.createElement('option');
option.value = part.ipn;
option.textContent = `${part.ipn} - ${part.description}`;
ipnDropdown.appendChild(option);
}
descriptionInput.value = '';
classInput.value = '';
manufacturerInput.value = '';
mpnInput.value = '';
datasheetInput.value = '';
createPartBtn.disabled = true;
if (missingPartsData.length === 0) {
createPartForm.style.display = 'none';
showCreatePartMessage('All missing parts have been created!', 'success');
}
});
// ========================================
// Variant Manager
// ========================================