feat: Major system improvements and production deployment

 New Features:
- Added view_orders route with proper table display
- Implemented CSV upload with preview workflow and date parsing
- Added production WSGI server configuration with Gunicorn
- Created comprehensive production management scripts

🔧 Bug Fixes:
- Fixed upload_data route column mapping for actual CSV structure
- Resolved print module database queries and template rendering
- Fixed view orders navigation routing (was pointing to JSON API)
- Corrected barcode display width constraints in print module
- Added proper date format parsing for MySQL compatibility

🎨 UI/UX Improvements:
- Updated view_orders template with theme-compliant styling
- Hidden barcode text in print module preview for cleaner display
- Enhanced CSV upload with two-step preview-then-save workflow
- Improved error handling and debugging throughout upload process

🚀 Production Infrastructure:
- Added Gunicorn WSGI server with proper configuration
- Created systemd service for production deployment
- Implemented production management scripts (start/stop/status)
- Added comprehensive logging setup
- Updated requirements.txt with production dependencies

📊 Database & Data:
- Enhanced order_for_labels table compatibility
- Fixed column mappings for real CSV data structure
- Added proper date parsing and validation
- Improved error handling with detailed debugging

🔧 Technical Debt:
- Reorganized database setup documentation
- Added proper error handling throughout upload workflow
- Enhanced debugging capabilities for troubleshooting
- Improved code organization and documentation
This commit is contained in:
Quality System Admin
2025-10-11 23:31:32 +03:00
parent af62fa478f
commit d264bcdca9
20 changed files with 1295 additions and 193 deletions

View File

@@ -34,9 +34,9 @@ def get_unprinted_orders_data(limit=100):
# Use printed_labels column
cursor.execute("""
SELECT id, comanda_productie, cod_articol, descr_com_prod, cantitate,
data_livrare, dimensiune, com_achiz_client, nr_linie_com_client, customer_name,
com_achiz_client, nr_linie_com_client, customer_name,
customer_article_number, open_for_order, line_number,
printed_labels, created_at, updated_at
created_at, updated_at, printed_labels, data_livrare, dimensiune
FROM order_for_labels
WHERE printed_labels != 1
ORDER BY created_at DESC
@@ -46,7 +46,7 @@ def get_unprinted_orders_data(limit=100):
# Fallback: get all orders if no printed_labels column
cursor.execute("""
SELECT id, comanda_productie, cod_articol, descr_com_prod, cantitate,
data_livrare, dimensiune, com_achiz_client, nr_linie_com_client, customer_name,
com_achiz_client, nr_linie_com_client, customer_name,
customer_article_number, open_for_order, line_number,
created_at, updated_at
FROM order_for_labels
@@ -63,17 +63,17 @@ def get_unprinted_orders_data(limit=100):
'cod_articol': row[2],
'descr_com_prod': row[3],
'cantitate': row[4],
'data_livrare': row[5],
'dimensiune': row[6],
'com_achiz_client': row[7],
'nr_linie_com_client': row[8],
'customer_name': row[9],
'customer_article_number': row[10],
'open_for_order': row[11],
'line_number': row[12],
'com_achiz_client': row[5],
'nr_linie_com_client': row[6],
'customer_name': row[7],
'customer_article_number': row[8],
'open_for_order': row[9],
'line_number': row[10],
'created_at': row[11],
'updated_at': row[12],
'printed_labels': row[13],
'created_at': row[14],
'updated_at': row[15]
'data_livrare': row[14] or '-',
'dimensiune': row[15] or '-'
})
else:
orders.append({
@@ -82,17 +82,18 @@ def get_unprinted_orders_data(limit=100):
'cod_articol': row[2],
'descr_com_prod': row[3],
'cantitate': row[4],
'data_livrare': row[5],
'dimensiune': row[6],
'com_achiz_client': row[7],
'nr_linie_com_client': row[8],
'customer_name': row[9],
'customer_article_number': row[10],
'open_for_order': row[11],
'line_number': row[12],
'printed_labels': 0, # Default to not printed
'created_at': row[13],
'updated_at': row[14]
'com_achiz_client': row[5],
'nr_linie_com_client': row[6],
'customer_name': row[7],
'customer_article_number': row[8],
'open_for_order': row[9],
'line_number': row[10],
'created_at': row[11],
'updated_at': row[12],
# Add default values for missing columns
'data_livrare': '-',
'dimensiune': '-',
'printed_labels': 0
})
conn.close()

View File

@@ -242,27 +242,42 @@ def scan():
conn = get_db_connection()
cursor = conn.cursor()
# Check if the CP_full_code already exists
cursor.execute("SELECT Id FROM scan1_orders WHERE CP_full_code = ?", (cp_code,))
existing_entry = cursor.fetchone()
if existing_entry:
# Update the existing entry
update_query = """
UPDATE scan1_orders
SET operator_code = ?, OC1_code = ?, OC2_code = ?, quality_code = ?, date = ?, time = ?
WHERE CP_full_code = ?
"""
cursor.execute(update_query, (operator_code, oc1_code, oc2_code, defect_code, date, time, cp_code))
flash('Existing entry updated successfully.')
# Always insert a new entry - each scan is a separate record
insert_query = """
INSERT INTO scan1_orders (operator_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time)
VALUES (%s, %s, %s, %s, %s, %s, %s)
"""
cursor.execute(insert_query, (operator_code, cp_code, oc1_code, oc2_code, defect_code, date, time))
# Get the CP_base_code (first 10 characters of CP_full_code)
cp_base_code = cp_code[:10]
# Count approved quantities (quality_code = 0) for this CP_base_code
cursor.execute("""
SELECT COUNT(*) FROM scan1_orders
WHERE CP_base_code = %s AND quality_code = 0
""", (cp_base_code,))
approved_count = cursor.fetchone()[0]
# Count rejected quantities (quality_code != 0) for this CP_base_code
cursor.execute("""
SELECT COUNT(*) FROM scan1_orders
WHERE CP_base_code = %s AND quality_code != 0
""", (cp_base_code,))
rejected_count = cursor.fetchone()[0]
# Update all records with the same CP_base_code with new quantities
cursor.execute("""
UPDATE scan1_orders
SET approved_quantity = %s, rejected_quantity = %s
WHERE CP_base_code = %s
""", (approved_count, rejected_count, cp_base_code))
# Flash appropriate message
if int(defect_code) == 0:
flash(f'✅ APPROVED scan recorded for {cp_code}. Total approved: {approved_count}')
else:
# Insert a new entry
insert_query = """
INSERT INTO scan1_orders (operator_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time)
VALUES (?, ?, ?, ?, ?, ?, ?)
"""
cursor.execute(insert_query, (operator_code, cp_code, oc1_code, oc2_code, defect_code, date, time))
flash('New entry inserted successfully.')
flash(f'❌ REJECTED scan recorded for {cp_code} (defect: {defect_code}). Total rejected: {rejected_count}')
# Commit the transaction
conn.commit()
@@ -278,7 +293,7 @@ def scan():
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("""
SELECT Id, operator_code, CP_base_code, OC1_code, OC2_code, quality_code, date, time, approved_quantity, rejected_quantity
SELECT Id, operator_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time, approved_quantity, rejected_quantity
FROM scan1_orders
ORDER BY Id DESC
LIMIT 15
@@ -321,27 +336,42 @@ def fg_scan():
conn = get_db_connection()
cursor = conn.cursor()
# Check if the CP_full_code already exists in scanfg_orders
cursor.execute("SELECT Id FROM scanfg_orders WHERE CP_full_code = ?", (cp_code,))
existing_entry = cursor.fetchone()
if existing_entry:
# Update the existing entry
update_query = """
UPDATE scanfg_orders
SET operator_code = ?, OC1_code = ?, OC2_code = ?, quality_code = ?, date = ?, time = ?
WHERE CP_full_code = ?
"""
cursor.execute(update_query, (operator_code, oc1_code, oc2_code, defect_code, date, time, cp_code))
flash('Existing entry updated successfully.')
# Always insert a new entry - each scan is a separate record
insert_query = """
INSERT INTO scanfg_orders (operator_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time)
VALUES (%s, %s, %s, %s, %s, %s, %s)
"""
cursor.execute(insert_query, (operator_code, cp_code, oc1_code, oc2_code, defect_code, date, time))
# Get the CP_base_code (first 10 characters of CP_full_code)
cp_base_code = cp_code[:10]
# Count approved quantities (quality_code = 0) for this CP_base_code
cursor.execute("""
SELECT COUNT(*) FROM scanfg_orders
WHERE CP_base_code = %s AND quality_code = 0
""", (cp_base_code,))
approved_count = cursor.fetchone()[0]
# Count rejected quantities (quality_code != 0) for this CP_base_code
cursor.execute("""
SELECT COUNT(*) FROM scanfg_orders
WHERE CP_base_code = %s AND quality_code != 0
""", (cp_base_code,))
rejected_count = cursor.fetchone()[0]
# Update all records with the same CP_base_code with new quantities
cursor.execute("""
UPDATE scanfg_orders
SET approved_quantity = %s, rejected_quantity = %s
WHERE CP_base_code = %s
""", (approved_count, rejected_count, cp_base_code))
# Flash appropriate message
if int(defect_code) == 0:
flash(f'✅ APPROVED scan recorded for {cp_code}. Total approved: {approved_count}')
else:
# Insert a new entry
insert_query = """
INSERT INTO scanfg_orders (operator_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time)
VALUES (?, ?, ?, ?, ?, ?, ?)
"""
cursor.execute(insert_query, (operator_code, cp_code, oc1_code, oc2_code, defect_code, date, time))
flash('New entry inserted successfully.')
flash(f'❌ REJECTED scan recorded for {cp_code} (defect: {defect_code}). Total rejected: {rejected_count}')
# Commit the transaction
conn.commit()
@@ -357,7 +387,7 @@ def fg_scan():
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("""
SELECT Id, operator_code, CP_base_code, OC1_code, OC2_code, quality_code, date, time, approved_quantity, rejected_quantity
SELECT Id, operator_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time, approved_quantity, rejected_quantity
FROM scanfg_orders
ORDER BY Id DESC
LIMIT 15
@@ -1036,13 +1066,273 @@ def etichete():
return redirect(url_for('main.dashboard'))
return render_template('main_page_etichete.html')
@bp.route('/upload_data')
@bp.route('/upload_data', methods=['GET', 'POST'])
def upload_data():
if request.method == 'POST':
action = request.form.get('action', 'preview')
if action == 'preview':
# Handle file upload and show preview
if 'file' not in request.files:
flash('No file selected', 'error')
return redirect(request.url)
file = request.files['file']
if file.filename == '':
flash('No file selected', 'error')
return redirect(request.url)
if file and file.filename.lower().endswith('.csv'):
try:
# Read CSV file
import csv
import io
# Read the file content
stream = io.StringIO(file.stream.read().decode("UTF8"), newline=None)
csv_input = csv.DictReader(stream)
# Convert to list for preview
preview_data = []
headers = []
for i, row in enumerate(csv_input):
if i == 0:
headers = list(row.keys())
if i < 10: # Show only first 10 rows for preview
preview_data.append(row)
else:
break
# Store the full file content in session for later processing
file.stream.seek(0) # Reset file pointer
session['csv_content'] = file.stream.read().decode("UTF8")
session['csv_filename'] = file.filename
return render_template('upload_orders.html',
preview_data=preview_data,
headers=headers,
show_preview=True,
filename=file.filename)
except Exception as e:
flash(f'Error reading CSV file: {str(e)}', 'error')
return redirect(request.url)
else:
flash('Please upload a CSV file', 'error')
return redirect(request.url)
elif action == 'save':
# Save the data to database
if 'csv_content' not in session:
flash('No data to save. Please upload a file first.', 'error')
return redirect(request.url)
try:
import csv
import io
print(f"DEBUG: Starting CSV upload processing...")
# Read the CSV content from session
stream = io.StringIO(session['csv_content'], newline=None)
csv_input = csv.DictReader(stream)
# Connect to database
conn = get_db_connection()
cursor = conn.cursor()
inserted_count = 0
error_count = 0
errors = []
print(f"DEBUG: Connected to database, processing rows...")
# Process each row
for index, row in enumerate(csv_input):
try:
print(f"DEBUG: Processing row {index + 1}: {row}")
# Extract data from CSV row with proper column mapping
comanda_productie = str(row.get('comanda_productie', row.get('Comanda Productie', row.get('Order Number', '')))).strip()
cod_articol = str(row.get('cod_articol', row.get('Cod Articol', row.get('Article Code', '')))).strip()
descr_com_prod = str(row.get('descr_com_prod', row.get('Descr. Com. Prod', row.get('Descr Com Prod', row.get('Description', ''))))).strip()
cantitate = int(float(row.get('cantitate', row.get('Cantitate', row.get('Quantity', 0)))))
com_achiz_client = str(row.get('com_achiz_client', row.get('Com.Achiz.Client', row.get('Com Achiz Client', '')))).strip()
nr_linie_com_client = row.get('nr_linie_com_client', row.get('Nr. Linie com. Client', row.get('Nr Linie Com Client', '')))
customer_name = str(row.get('customer_name', row.get('Customer Name', ''))).strip()
customer_article_number = str(row.get('customer_article_number', row.get('Customer Article Number', ''))).strip()
open_for_order = str(row.get('open_for_order', row.get('Open for order', row.get('Open For Order', '')))).strip()
line_number = row.get('line_number', row.get('Line ', row.get('Line Number', '')))
data_livrare = str(row.get('data_livrare', row.get('DataLivrare', row.get('Data Livrare', '')))).strip()
dimensiune = str(row.get('dimensiune', row.get('Dimensiune', ''))).strip()
print(f"DEBUG: Extracted data - comanda_productie: {comanda_productie}, descr_com_prod: {descr_com_prod}, cantitate: {cantitate}")
# Convert empty strings to None for integer fields
nr_linie_com_client = int(nr_linie_com_client) if nr_linie_com_client and str(nr_linie_com_client).strip() else None
line_number = int(line_number) if line_number and str(line_number).strip() else None
# Convert empty string to None for date field
if data_livrare:
try:
# Parse date from various formats (9/23/2023, 23/9/2023, 2023-09-23, etc.)
from datetime import datetime
# Try different date formats
date_formats = ['%m/%d/%Y', '%d/%m/%Y', '%Y-%m-%d', '%m-%d-%Y', '%d-%m-%Y']
parsed_date = None
for fmt in date_formats:
try:
parsed_date = datetime.strptime(data_livrare, fmt)
break
except ValueError:
continue
if parsed_date:
data_livrare = parsed_date.strftime('%Y-%m-%d') # MySQL date format
print(f"DEBUG: Parsed date: {data_livrare}")
else:
print(f"DEBUG: Could not parse date: {data_livrare}, setting to None")
data_livrare = None
except Exception as date_error:
print(f"DEBUG: Date parsing error: {date_error}")
data_livrare = None
else:
data_livrare = None
dimensiune = dimensiune if dimensiune else None
print(f"DEBUG: Final data before insert - nr_linie: {nr_linie_com_client}, line_number: {line_number}, data_livrare: {data_livrare}")
if comanda_productie and descr_com_prod and cantitate > 0:
# Insert into order_for_labels table with correct columns
print(f"DEBUG: Inserting order: {comanda_productie}")
try:
cursor.execute("""
INSERT INTO order_for_labels (
comanda_productie, cod_articol, descr_com_prod, cantitate,
com_achiz_client, nr_linie_com_client, customer_name,
customer_article_number, open_for_order, line_number,
data_livrare, dimensiune, printed_labels
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, 0)
""", (comanda_productie, cod_articol, descr_com_prod, cantitate,
com_achiz_client, nr_linie_com_client, customer_name,
customer_article_number, open_for_order, line_number,
data_livrare, dimensiune))
inserted_count += 1
print(f"DEBUG: Successfully inserted order: {comanda_productie}")
except Exception as insert_error:
print(f"DEBUG: Database insert error for {comanda_productie}: {insert_error}")
errors.append(f"Row {index + 1}: Database error - {str(insert_error)}")
error_count += 1
else:
missing_fields = []
if not comanda_productie:
missing_fields.append("comanda_productie")
if not descr_com_prod:
missing_fields.append("descr_com_prod")
if cantitate <= 0:
missing_fields.append("cantitate (must be > 0)")
errors.append(f"Row {index + 1}: Missing required fields: {', '.join(missing_fields)}")
error_count += 1
except ValueError as e:
errors.append(f"Row {index + 1}: Invalid quantity value")
error_count += 1
except Exception as e:
errors.append(f"Row {index + 1}: {str(e)}")
error_count += 1
continue
# Commit the transaction
conn.commit()
conn.close()
print(f"DEBUG: Committed {inserted_count} records to database")
# Clear session data
session.pop('csv_content', None)
session.pop('csv_filename', None)
# Show results
if error_count > 0:
flash(f'Upload completed: {inserted_count} orders saved, {error_count} errors', 'warning')
for error in errors[:5]: # Show only first 5 errors
flash(error, 'error')
if len(errors) > 5:
flash(f'... and {len(errors) - 5} more errors', 'error')
else:
flash(f'Successfully uploaded {inserted_count} orders for labels', 'success')
except Exception as e:
flash(f'Error processing data: {str(e)}', 'error')
return redirect(url_for('main.upload_data'))
# GET request - show the upload form
return render_template('upload_orders.html')
@bp.route('/upload_orders')
def upload_orders():
"""Redirect to upload_data for compatibility"""
return redirect(url_for('main.upload_data'))
@bp.route('/print_module')
def print_module():
return render_template('print_module.html')
try:
# Get unprinted orders data
orders_data = get_unprinted_orders_data(limit=100)
return render_template('print_module.html', orders=orders_data)
except Exception as e:
print(f"Error loading print module data: {e}")
flash(f"Error loading orders: {e}", 'error')
return render_template('print_module.html', orders=[])
@bp.route('/view_orders')
def view_orders():
"""View all orders in a table format"""
try:
# Get all orders data (not just unprinted)
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("""
SELECT id, comanda_productie, cod_articol, descr_com_prod, cantitate,
com_achiz_client, nr_linie_com_client, customer_name,
customer_article_number, open_for_order, line_number,
created_at, updated_at, printed_labels, data_livrare, dimensiune
FROM order_for_labels
ORDER BY created_at DESC
LIMIT 500
""")
orders_data = []
for row in cursor.fetchall():
orders_data.append({
'id': row[0],
'comanda_productie': row[1],
'cod_articol': row[2],
'descr_com_prod': row[3],
'cantitate': row[4],
'com_achiz_client': row[5],
'nr_linie_com_client': row[6],
'customer_name': row[7],
'customer_article_number': row[8],
'open_for_order': row[9],
'line_number': row[10],
'created_at': row[11],
'updated_at': row[12],
'printed_labels': row[13],
'data_livrare': row[14] or '-',
'dimensiune': row[15] or '-'
})
conn.close()
return render_template('view_orders.html', orders=orders_data)
except Exception as e:
print(f"Error loading view orders data: {e}")
flash(f"Error loading orders: {e}", 'error')
return render_template('view_orders.html', orders=[])
import secrets

View File

@@ -23,6 +23,80 @@ document.addEventListener('DOMContentLoaded', function() {
const defectCodeInput = document.getElementById('defect_code');
const form = document.getElementById('fg-scan-form');
// Restore saved operator code from localStorage (only Quality Operator Code)
const savedOperatorCode = localStorage.getItem('fg_scan_operator_code');
if (savedOperatorCode) {
operatorCodeInput.value = savedOperatorCode;
}
// Check if we need to clear fields after a successful submission
const shouldClearAfterSubmit = localStorage.getItem('fg_scan_clear_after_submit');
if (shouldClearAfterSubmit === 'true') {
// Clear the flag
localStorage.removeItem('fg_scan_clear_after_submit');
localStorage.removeItem('fg_scan_last_cp');
localStorage.removeItem('fg_scan_last_defect');
// Clear CP code, OC1, OC2, and defect code for next scan
cpCodeInput.value = '';
oc1CodeInput.value = '';
oc2CodeInput.value = '';
defectCodeInput.value = '';
// Show success indicator
setTimeout(function() {
// Focus on CP code field for next scan
cpCodeInput.focus();
// Add visual feedback
const successIndicator = document.createElement('div');
successIndicator.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #4CAF50;
color: white;
padding: 10px 20px;
border-radius: 5px;
z-index: 1000;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
font-weight: bold;
`;
successIndicator.textContent = '✅ Scan recorded! Ready for next scan';
document.body.appendChild(successIndicator);
// Remove success indicator after 3 seconds
setTimeout(function() {
if (successIndicator.parentNode) {
successIndicator.parentNode.removeChild(successIndicator);
}
}, 3000);
}, 100);
}
// Focus on the first empty required field (only if not clearing after submit)
if (shouldClearAfterSubmit !== 'true') {
if (!operatorCodeInput.value) {
operatorCodeInput.focus();
} else if (!oc1CodeInput.value) {
oc1CodeInput.focus();
} else if (!oc2CodeInput.value) {
oc2CodeInput.focus();
} else if (!cpCodeInput.value) {
cpCodeInput.focus();
} else {
defectCodeInput.focus();
}
}
// Save operator codes to localStorage when they change (only Quality Operator Code)
operatorCodeInput.addEventListener('input', function() {
if (this.value.startsWith('OP') && this.value.length >= 3) {
localStorage.setItem('fg_scan_operator_code', this.value);
}
});
// Create error message element for operator code
const operatorErrorMessage = document.createElement('div');
operatorErrorMessage.className = 'error-message';
@@ -338,6 +412,11 @@ document.addEventListener('DOMContentLoaded', function() {
const seconds = String(now.getSeconds()).padStart(2, '0');
timeInput.value = `${hours}:${minutes}:${seconds}`;
// Save current CP code and defect code to localStorage for clearing after reload
localStorage.setItem('fg_scan_clear_after_submit', 'true');
localStorage.setItem('fg_scan_last_cp', cpCodeInput.value);
localStorage.setItem('fg_scan_last_defect', defectCodeInput.value);
// Submit the form
form.submit();
}
@@ -399,6 +478,27 @@ document.addEventListener('DOMContentLoaded', function() {
}
}
});
// Add functionality for clear saved codes button
const clearSavedBtn = document.getElementById('clear-saved-btn');
clearSavedBtn.addEventListener('click', function() {
if (confirm('Clear saved Quality Operator code? You will need to re-enter it.')) {
// Clear localStorage (only Quality Operator Code)
localStorage.removeItem('fg_scan_operator_code');
localStorage.removeItem('fg_scan_clear_after_submit');
localStorage.removeItem('fg_scan_last_cp');
localStorage.removeItem('fg_scan_last_defect');
// Clear Quality Operator Code field only
operatorCodeInput.value = '';
// Focus on operator code field
operatorCodeInput.focus();
// Show confirmation
alert('✅ Saved Quality Operator code cleared! Please re-enter your operator code.');
}
});
});
</script>
{% endblock %}
@@ -430,6 +530,7 @@ document.addEventListener('DOMContentLoaded', function() {
<input type="text" id="time" name="time" value="{{ now().strftime('%H:%M:%S') }}" readonly>
<button type="submit" class="btn">Submit</button>
<button type="button" class="btn" id="clear-saved-btn" style="background-color: #ff6b6b; margin-left: 10px;">Clear Quality Operator</button>
</form>
</div>

View File

@@ -15,7 +15,7 @@
<p>Upload new orders or view existing orders and manage label data for printing.</p>
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<a href="{{ url_for('main.upload_data') }}" class="btn">Upload Orders</a>
<a href="{{ url_for('main.get_unprinted_orders') }}" class="btn">View Orders</a>
<a href="{{ url_for('main.view_orders') }}" class="btn">View Orders</a>
</div>
</div>

View File

@@ -10,8 +10,9 @@
/* Enhanced table styling */
.card.scan-table-card table.print-module-table.scan-table thead th {
border-bottom: 2px solid #dee2e6 !important;
background-color: #f8f9fa !important;
border-bottom: 2px solid var(--app-border-color, #dee2e6) !important;
background-color: var(--app-table-header-bg, #2a3441) !important;
color: var(--app-text-color, #ffffff) !important;
padding: 0.25rem 0.4rem !important;
text-align: left !important;
font-weight: 600 !important;
@@ -22,13 +23,21 @@
.card.scan-table-card table.print-module-table.scan-table {
width: 100% !important;
border-collapse: collapse !important;
background-color: var(--app-card-bg, #2a3441) !important;
}
.card.scan-table-card table.print-module-table.scan-table tbody tr:hover td {
background-color: #f8f9fa !important;
background-color: var(--app-hover-bg, #3a4451) !important;
cursor: pointer !important;
}
.card.scan-table-card table.print-module-table.scan-table tbody td {
background-color: var(--app-card-bg, #2a3441) !important;
color: var(--app-text-color, #ffffff) !important;
border: 1px solid var(--app-border-color, #495057) !important;
padding: 0.25rem 0.4rem !important;
}
.card.scan-table-card table.print-module-table.scan-table tbody tr.selected td {
background-color: #007bff !important;
color: white !important;
@@ -140,13 +149,13 @@
</div>
</div>
<!-- Barcode Frame - positioned 10px below rectangle, centered, 90% of label width -->
<div id="barcode-frame" style="position: absolute; top: 395px; left: 50%; transform: translateX(-50%); width: 90%; max-width: 270px; height: 50px; background: white; display: flex; flex-direction: column; align-items: center; justify-content: center;">
<!-- Barcode Frame - positioned 10px below rectangle, centered, constrained to label width -->
<div id="barcode-frame" style="position: absolute; top: 395px; left: 50%; transform: translateX(-50%); width: 220px; max-width: 220px; height: 50px; background: white; display: flex; flex-direction: column; align-items: center; justify-content: center; overflow: hidden;">
<!-- Code 128 Barcode representation -->
<svg id="barcode-display" style="width: 100%; height: 40px;"></svg>
<svg id="barcode-display" style="width: 100%; height: 40px; max-width: 220px;"></svg>
<!-- Barcode text below the bars -->
<div id="barcode-text" style="font-size: 8px; font-family: 'Courier New', monospace; margin-top: 2px; text-align: center; font-weight: bold;">
<!-- Barcode text below the bars (hidden in preview) -->
<div id="barcode-text" style="font-size: 8px; font-family: 'Courier New', monospace; margin-top: 2px; text-align: center; font-weight: bold; display: none;">
<!-- Barcode text will be populated here -->
</div>
</div>
@@ -156,8 +165,8 @@
<!-- Vertical Code 128 Barcode representation -->
<svg id="vertical-barcode-display" style="width: 100%; height: 35px;"></svg>
<!-- Vertical barcode text -->
<div id="vertical-barcode-text" style="position: absolute; bottom: -15px; font-size: 7px; font-family: 'Courier New', monospace; text-align: center; font-weight: bold; width: 100%;">
<!-- Vertical barcode text (hidden in preview) -->
<div id="vertical-barcode-text" style="position: absolute; bottom: -15px; font-size: 7px; font-family: 'Courier New', monospace; text-align: center; font-weight: bold; width: 100%; display: none;">
<!-- Vertical barcode text will be populated here -->
</div>
</div>
@@ -475,10 +484,12 @@ function updateLabelPreview(order) {
JsBarcode("#barcode-display", horizontalBarcodeData, {
format: "CODE128",
width: 2,
width: 1.2,
height: 40,
displayValue: false,
margin: 2
margin: 0,
fontSize: 0,
textMargin: 0
});
console.log('✅ Horizontal barcode generated successfully');
} catch (e) {

View File

@@ -22,6 +22,74 @@ document.addEventListener('DOMContentLoaded', function() {
const defectCodeInput = document.getElementById('defect_code');
const form = document.querySelector('.form-centered');
// Load saved operator codes from localStorage (only Quality Operator Code)
function loadSavedCodes() {
const savedOperatorCode = localStorage.getItem('scan_operator_code');
if (savedOperatorCode) {
operatorCodeInput.value = savedOperatorCode;
}
}
// Save operator codes to localStorage (only Quality Operator Code)
function saveCodes() {
if (operatorCodeInput.value.startsWith('OP')) {
localStorage.setItem('scan_operator_code', operatorCodeInput.value);
}
}
// Clear saved codes from localStorage (only Quality Operator Code)
function clearSavedCodes() {
localStorage.removeItem('scan_operator_code');
operatorCodeInput.value = '';
showSuccessMessage('Quality Operator code cleared!');
operatorCodeInput.focus();
}
// Show success message
function showSuccessMessage(message) {
const successDiv = document.createElement('div');
successDiv.className = 'success-message';
successDiv.textContent = message;
successDiv.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #4CAF50;
color: white;
padding: 15px 20px;
border-radius: 4px;
z-index: 1000;
font-weight: bold;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
`;
document.body.appendChild(successDiv);
setTimeout(() => {
if (document.body.contains(successDiv)) {
document.body.removeChild(successDiv);
}
}, 3000);
}
// Load saved codes on page load
loadSavedCodes();
// Focus on the first empty field
setTimeout(() => {
if (!operatorCodeInput.value) {
operatorCodeInput.focus();
} else if (!cpCodeInput.value) {
cpCodeInput.focus();
} else if (!oc1CodeInput.value) {
oc1CodeInput.focus();
} else if (!oc2CodeInput.value) {
oc2CodeInput.focus();
} else {
defectCodeInput.focus();
}
}, 100);
// Create error message element for operator code
const operatorErrorMessage = document.createElement('div');
operatorErrorMessage.className = 'error-message';
@@ -333,8 +401,21 @@ document.addEventListener('DOMContentLoaded', function() {
const seconds = String(now.getSeconds()).padStart(2, '0');
timeInput.value = `${hours}:${minutes}:${seconds}`;
// Save operator codes before submitting
saveCodes();
// Submit the form
form.submit();
// Clear CP, OC1, OC2, and defect code fields after successful submission
setTimeout(() => {
cpCodeInput.value = '';
oc1CodeInput.value = '';
oc2CodeInput.value = '';
defectCodeInput.value = '';
showSuccessMessage('Scan submitted successfully!');
cpCodeInput.focus();
}, 100);
}
});
@@ -425,6 +506,7 @@ document.addEventListener('DOMContentLoaded', function() {
<input type="text" id="time" name="time" value="{{ now().strftime('%H:%M:%S') }}" readonly>
<button type="submit" class="btn">Submit</button>
<button type="button" class="btn btn-secondary" onclick="clearSavedCodes()" style="margin-top: 10px; background-color: #6c757d;">Clear Quality Operator</button>
</form>
</div>

View File

@@ -82,15 +82,35 @@ table.view-orders-table.scan-table tbody tr:hover td {
<h3>Upload Order Data for Labels</h3>
{% endif %}
<form method="POST" enctype="multipart/form-data" class="form-centered" id="csv-upload-form">
<label for="csv_file">Choose CSV file:</label>
{% if leftover_description %}
<button type="submit" class="btn btn-danger" name="clear_table" value="1">Clear Table</button>
{% elif not orders %}
<input type="file" name="csv_file" accept=".csv" required><br>
<button type="submit" class="btn">Upload & Review</button>
{% if show_preview %}
<!-- Show preview controls -->
<input type="hidden" name="action" value="save">
<label style="font-weight: bold;">Preview of: {{ filename }}</label><br>
<p style="color: #666; font-size: 14px; margin: 10px 0;">
Showing first 10 rows. Review the data below and click "Save to Database" to confirm.
</p>
<button type="submit" class="btn" style="background-color: #28a745;">Save to Database</button>
<a href="{{ url_for('main.upload_data') }}" class="btn" style="background-color: #6c757d; margin-left: 10px;">Cancel</a>
{% else %}
<label style="font-weight: bold;">Selected file: {{ session['csv_filename'] if session['csv_filename'] else 'Unknown' }}</label><br>
<button type="button" class="btn" onclick="showPopupAndSubmit()">Upload to Database</button>
<!-- Show file upload -->
<input type="hidden" name="action" value="preview">
<label for="file">Choose CSV file:</label>
<input type="file" name="file" accept=".csv" required><br>
<button type="submit" class="btn">Upload & Preview</button>
<!-- CSV Format Information -->
<div style="margin-top: 20px; padding: 15px; background-color: var(--app-card-bg, #2a3441); border-radius: 5px; border-left: 4px solid var(--app-accent-color, #007bff); color: var(--app-text-color, #ffffff);">
<h5 style="margin-top: 0; color: var(--app-accent-color, #007bff);">Expected CSV Format</h5>
<p style="margin-bottom: 10px; color: var(--app-text-color, #ffffff);">Your CSV file should contain columns such as:</p>
<ul style="margin-bottom: 10px; color: var(--app-text-color, #ffffff);">
<li><strong>order_number</strong> - The order/production number</li>
<li><strong>quantity</strong> - Number of items</li>
<li><strong>warehouse_location</strong> - Storage location</li>
</ul>
<p style="color: var(--app-secondary-text, #b8c5d1); font-size: 14px; margin-bottom: 0;">
Column names are case-insensitive and can have variations like "Order Number", "Quantity", "Location", etc.
</p>
</div>
{% endif %}
</form>
@@ -124,108 +144,45 @@ table.view-orders-table.scan-table tbody tr:hover td {
</div>
<!-- Preview Table Card (expandable height, scrollable) -->
<div class="card scan-table-card{% if leftover_description %} leftover-table-card{% endif %}" style="margin-bottom: 24px; max-height: 480px; overflow-y: auto;">
{% if leftover_description %}
<h3>Left over orders</h3>
<div class="card scan-table-card" style="margin-bottom: 24px; max-height: 480px; overflow-y: auto;">
{% if show_preview %}
<h3>CSV Data Preview - {{ filename }}</h3>
<table class="scan-table">
<thead>
<tr>
{% for header in headers %}
<th>{{ header }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% if preview_data %}
{% for row in preview_data %}
<tr>
{% for header in headers %}
<td>{{ row.get(header, '') }}</td>
{% endfor %}
</tr>
{% endfor %}
{% else %}
<tr><td colspan="{{ headers|length }}" style="text-align:center;">No data to preview</td></tr>
{% endif %}
</tbody>
</table>
{% else %}
<h3>Preview Table</h3>
<h3>CSV Data Preview</h3>
<table class="scan-table">
<thead>
<tr>
<th>Upload a CSV file to see preview</th>
</tr>
</thead>
<tbody>
<tr><td style="text-align:center; padding: 40px;">No CSV file uploaded yet. Use the form above to upload and preview your data.</td></tr>
</tbody>
</table>
{% endif %}
<table class="scan-table view-orders-table{% if leftover_description %} leftover-table{% endif %}">
<thead>
<tr>
<th>ID</th>
<th>Comanda<br>Productie</th>
<th>Cod<br>Articol</th>
<th>Descr. Com.<br>Prod</th>
<th>Cantitate</th>
<th>Data<br>Livrare</th>
<th>Dimensiune</th>
<th>Com.Achiz.<br>Client</th>
<th>Nr.<br>Linie</th>
<th>Customer<br>Name</th>
<th>Customer<br>Art. Nr.</th>
<th>Open<br>Order</th>
<th>Line</th>
<th>Printed</th>
<th>Created</th>
</tr>
</thead>
<tbody>
{% if orders %}
{% for order in orders %}
{% if order and (order.get('comanda_productie', '') or order.get('descr_com_prod', '')) %}
<tr>
<td>{{ order.get('id', '') }}</td>
<td><strong>{{ order.get('comanda_productie', '') }}</strong></td>
<td>{{ order.get('cod_articol', '-') }}</td>
<td>{{ order.get('descr_com_prod', '') }}</td>
<td style="text-align: right; font-weight: 600;">{{ order.get('cantitate', '') }}</td>
<td style="text-align: center;">{{ order.get('data_livrare', '') }}</td>
<td style="text-align: center;">{{ order.get('dimensiune', '-') }}</td>
<td>{{ order.get('com_achiz_client', '-') }}</td>
<td style="text-align: right;">{{ order.get('nr_linie_com_client', '-') }}</td>
<td>{{ order.get('customer_name', '-') }}</td>
<td>{{ order.get('customer_article_number', '-') }}</td>
<td>{{ order.get('open_for_order', '-') }}</td>
<td style="text-align: right;">{{ order.get('line_number', '-') }}</td>
<td style="text-align: center;">
{% if order.get('printed_labels', 0) == 1 %}
<span style="color: #28a745; font-weight: bold;">✓ Yes</span>
{% else %}
<span style="color: #dc3545;">✗ No</span>
{% endif %}
</td>
<td style="font-size: 11px; color: #6c757d;">{{ order.get('created_at', '-') }}</td>
</tr>
{% if order.error_message %}
<tr>
<td colspan="15" style="color: #dc3545; font-size: 12px; background: #fff3f3;">
<strong>Error:</strong> {{ order.error_message }}
</td>
</tr>
{% endif %}
{% endif %}
{% endfor %}
{% else %}
<tr><td colspan="15" style="text-align:center;">No CSV file uploaded yet.</td></tr>
{% endif %}
</tbody>
</table>
</div>
{% if validation_errors or validation_warnings %}
{% if not leftover_description %}
<div class="card" style="margin-bottom: 24px;">
<h4>Validation Results</h4>
{% if validation_errors %}
<div style="color: #dc3545; margin-bottom: 16px;">
<strong>Errors found:</strong>
<ul>
{% for error in validation_errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if validation_warnings %}
<div style="color: #ffc107;">
<strong>Warnings:</strong>
<ul>
{% for warning in validation_warnings %}
<li>{{ warning }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
{% if report %}
<div class="card" style="margin-bottom: 24px;">
<h4>Import Report</h4>
<p>{{ report }}</p>
</div>
{% endif %}
{% endif %}
{% endif %}
</div>
<style>

View File

@@ -25,13 +25,14 @@ table.view-orders-table.scan-table thead th {
line-height: 1.3 !important;
padding: 6px 3px !important;
font-size: 11px !important;
background-color: #e9ecef !important;
background-color: var(--header-bg-color) !important;
color: var(--header-text-color) !important;
font-weight: bold !important;
text-transform: none !important;
letter-spacing: 0 !important;
overflow: visible !important;
box-sizing: border-box !important;
border: 1px solid #ddd !important;
border: 1px solid var(--border-color) !important;
text-overflow: clip !important;
position: relative !important;
}
@@ -41,7 +42,9 @@ table.view-orders-table.scan-table tbody td {
padding: 4px 2px !important;
font-size: 10px !important;
text-align: center !important;
border: 1px solid #ddd !important;
border: 1px solid var(--border-color) !important;
background-color: var(--card-bg-color) !important;
color: var(--text-color) !important;
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
@@ -68,7 +71,7 @@ table.view-orders-table.scan-table tbody td {
/* HOVER EFFECTS */
table.view-orders-table.scan-table tbody tr:hover td {
background-color: #f8f9fa !important;
background-color: var(--hover-color) !important;
}
/* COLUMN WIDTH SPECIFICATIONS */