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

138
app.py
View File

@@ -1089,6 +1089,144 @@ def handle_search_parts(data):
import traceback
emit('library_error', {'error': f'{str(e)}\n\nTraceback:\n{traceback.format_exc()}'})
@socketio.on('get_missing_ipns')
def handle_get_missing_ipns():
try:
# Load config to get spreadsheet path
config_path = os.path.join(os.path.dirname(__file__), 'config.json')
with open(config_path, 'r') as f:
config = json.load(f)
spreadsheet_path = config.get('parts_spreadsheet_path', '')
if not spreadsheet_path or not os.path.exists(spreadsheet_path):
emit('library_error', {'error': 'Parts spreadsheet not found'})
return
# Get all IPNs from database
conn = get_db_connection()
if not conn:
emit('library_error', {'error': 'Could not connect to database'})
return
cursor = conn.cursor()
cursor.execute("SELECT ipn FROM parts")
db_ipns = set(row.ipn for row in cursor.fetchall() if row.ipn)
cursor.close()
conn.close()
# Read spreadsheet
import openpyxl
wb = openpyxl.load_workbook(spreadsheet_path, read_only=True, data_only=True)
ws = wb.active
# Find header row and column indices
header_row = None
for row in ws.iter_rows(min_row=1, max_row=10, values_only=True):
if row and 'GLE P/N' in row:
header_row = row
break
if not header_row:
emit('library_error', {'error': 'Could not find header row in spreadsheet'})
return
ipn_col = header_row.index('GLE P/N')
desc_col = header_row.index('Description') if 'Description' in header_row else None
class_col = header_row.index('Class') if 'Class' in header_row else None
mfg_col = header_row.index('Mfg.1') if 'Mfg.1' in header_row else None
mpn_col = header_row.index('Mfg.1 P/N') if 'Mfg.1 P/N' in header_row else None
# Collect missing IPNs
missing_parts = []
for row in ws.iter_rows(min_row=2, values_only=True):
if not row or not row[ipn_col]:
continue
ipn = str(row[ipn_col]).strip()
if not ipn or ipn in db_ipns:
continue
# Get manufacturer and MPN
manufacturer = str(row[mfg_col]).strip() if mfg_col and row[mfg_col] else ''
mpn = str(row[mpn_col]).strip() if mpn_col and row[mpn_col] else ''
# Skip section headers - rows with IPN and description but no manufacturer or MPN
if not manufacturer and not mpn:
continue
# Get and normalize class field
class_value = str(row[class_col]).strip() if class_col and row[class_col] else ''
if class_value:
# Replace spaces and special characters with underscore, collapse multiple underscores
import re
class_value = re.sub(r'[^a-zA-Z0-9]+', '_', class_value.upper())
class_value = re.sub(r'_+', '_', class_value).strip('_')
part = {
'ipn': ipn,
'description': str(row[desc_col]).strip() if desc_col and row[desc_col] else '',
'class': class_value,
'manufacturer': manufacturer,
'mpn': mpn
}
missing_parts.append(part)
wb.close()
# Sort by IPN
missing_parts.sort(key=lambda x: x['ipn'])
emit('missing_ipns_result', {'parts': missing_parts, 'count': len(missing_parts)})
except Exception as e:
import traceback
emit('library_error', {'error': f'{str(e)}\n\nTraceback:\n{traceback.format_exc()}'})
@socketio.on('create_part')
def handle_create_part(data):
try:
ipn = data.get('ipn', '').strip()
manufacturer = data.get('manufacturer', '').strip()
mpn = data.get('mpn', '').strip()
description = data.get('description', '').strip()
class_value = data.get('class', '').strip()
datasheet = data.get('datasheet', '').strip()
if not ipn:
emit('library_error', {'error': 'IPN is required'})
return
conn = get_db_connection()
if not conn:
emit('library_error', {'error': 'Could not connect to database'})
return
cursor = conn.cursor()
# Check if part already exists
cursor.execute("SELECT ipn FROM parts WHERE ipn = ?", (ipn,))
if cursor.fetchone():
emit('library_error', {'error': f'Part {ipn} already exists in database'})
cursor.close()
conn.close()
return
# Insert new part
cursor.execute("""
INSERT INTO parts (ipn, manufacturer, mpn, description, class, datasheet)
VALUES (?, ?, ?, ?, ?, ?)
""", (ipn, manufacturer, mpn, description, class_value, datasheet))
conn.commit()
cursor.close()
conn.close()
emit('part_created', {'message': f'Successfully created part {ipn}'})
except Exception as e:
import traceback
emit('library_error', {'error': f'{str(e)}\n\nTraceback:\n{traceback.format_exc()}'})
# ---------------------------------------------------------------------------
# BOM Generation
# ---------------------------------------------------------------------------