updated / print module / keypairing options
This commit is contained in:
@@ -101,4 +101,90 @@ def get_unprinted_orders_data(limit=100):
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error retrieving unprinted orders: {e}")
|
||||
return []
|
||||
|
||||
def get_printed_orders_data(limit=100):
|
||||
"""
|
||||
Retrieve printed orders from the database for display
|
||||
Returns list of order dictionaries where printed_labels = 1
|
||||
"""
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
# Check if printed_labels column exists
|
||||
cursor.execute("SHOW COLUMNS FROM order_for_labels LIKE 'printed_labels'")
|
||||
column_exists = cursor.fetchone()
|
||||
|
||||
if column_exists:
|
||||
# Get orders where printed_labels = 1
|
||||
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
|
||||
WHERE printed_labels = 1
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT %s
|
||||
""", (limit,))
|
||||
else:
|
||||
# Fallback: get all orders if no printed_labels column
|
||||
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
|
||||
FROM order_for_labels
|
||||
ORDER BY created_at DESC
|
||||
LIMIT %s
|
||||
""", (limit,))
|
||||
|
||||
orders = []
|
||||
for row in cursor.fetchall():
|
||||
if column_exists:
|
||||
orders.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 '-'
|
||||
})
|
||||
else:
|
||||
orders.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],
|
||||
# Add default values for missing columns
|
||||
'data_livrare': '-',
|
||||
'dimensiune': '-',
|
||||
'printed_labels': 0
|
||||
})
|
||||
|
||||
conn.close()
|
||||
return orders
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error retrieving printed orders: {e}")
|
||||
return []
|
||||
@@ -19,7 +19,7 @@ from app.settings import (
|
||||
delete_user_handler,
|
||||
save_external_db_handler
|
||||
)
|
||||
from .print_module import get_unprinted_orders_data
|
||||
from .print_module import get_unprinted_orders_data, get_printed_orders_data
|
||||
from .access_control import (
|
||||
requires_role, superadmin_only, admin_plus, manager_plus,
|
||||
requires_quality_module, requires_warehouse_module, requires_labels_module,
|
||||
@@ -1933,10 +1933,11 @@ def print_module():
|
||||
|
||||
@bp.route('/print_lost_labels')
|
||||
def print_lost_labels():
|
||||
"""Print lost labels module"""
|
||||
"""Print lost labels module - shows orders with printed labels for reprinting"""
|
||||
try:
|
||||
# Get orders data for lost labels (could be different logic than print_module)
|
||||
orders_data = get_unprinted_orders_data(limit=100)
|
||||
# Get orders that have already been printed (printed_labels = 1)
|
||||
# Limited to 50 most recent orders for performance
|
||||
orders_data = get_printed_orders_data(limit=50)
|
||||
return render_template('print_lost_labels.html', orders=orders_data)
|
||||
except Exception as e:
|
||||
print(f"Error loading print lost labels data: {e}")
|
||||
@@ -1995,16 +1996,19 @@ import secrets
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
@bp.route('/generate_pairing_key', methods=['POST'])
|
||||
@superadmin_only
|
||||
def generate_pairing_key():
|
||||
"""Generate a secure pairing key for a printer and store it."""
|
||||
printer_name = request.form.get('printer_name', '').strip()
|
||||
validity_days = int(request.form.get('validity_days', 90)) # Default to 90 days
|
||||
|
||||
if not printer_name:
|
||||
flash('Printer name is required.', 'danger')
|
||||
return redirect(url_for('main.download_extension'))
|
||||
|
||||
# Generate a secure random key
|
||||
pairing_key = secrets.token_urlsafe(32)
|
||||
warranty_until = (datetime.utcnow() + timedelta(days=365)).strftime('%Y-%m-%d')
|
||||
warranty_until = (datetime.utcnow() + timedelta(days=validity_days)).strftime('%Y-%m-%d')
|
||||
|
||||
# Load existing keys
|
||||
keys_path = os.path.join(current_app.instance_path, 'pairing_keys.json')
|
||||
@@ -2021,13 +2025,16 @@ def generate_pairing_key():
|
||||
keys.append({
|
||||
'printer_name': printer_name,
|
||||
'pairing_key': pairing_key,
|
||||
'warranty_until': warranty_until
|
||||
'warranty_until': warranty_until,
|
||||
'validity_days': validity_days
|
||||
})
|
||||
|
||||
# Save updated keys
|
||||
with open(keys_path, 'w') as f:
|
||||
json.dump(keys, f, indent=2)
|
||||
|
||||
flash(f'Pairing key generated successfully for "{printer_name}" (valid for {validity_days} days).', 'success')
|
||||
|
||||
# Pass new key and all keys to template
|
||||
return render_template('download_extension.html',
|
||||
pairing_key=pairing_key,
|
||||
@@ -2035,6 +2042,42 @@ def generate_pairing_key():
|
||||
warranty_until=warranty_until,
|
||||
pairing_keys=keys)
|
||||
|
||||
@bp.route('/delete_pairing_key', methods=['POST'])
|
||||
@superadmin_only
|
||||
def delete_pairing_key():
|
||||
"""Delete a pairing key."""
|
||||
pairing_key = request.form.get('pairing_key', '').strip()
|
||||
|
||||
if not pairing_key:
|
||||
flash('Pairing key is required.', 'danger')
|
||||
return redirect(url_for('main.download_extension'))
|
||||
|
||||
# Load existing keys
|
||||
keys_path = os.path.join(current_app.instance_path, 'pairing_keys.json')
|
||||
try:
|
||||
if os.path.exists(keys_path):
|
||||
with open(keys_path, 'r') as f:
|
||||
keys = json.load(f)
|
||||
else:
|
||||
keys = []
|
||||
except Exception as e:
|
||||
flash('Error loading pairing keys.', 'danger')
|
||||
return redirect(url_for('main.download_extension'))
|
||||
|
||||
# Find and remove the key
|
||||
original_count = len(keys)
|
||||
keys = [k for k in keys if k['pairing_key'] != pairing_key]
|
||||
|
||||
if len(keys) < original_count:
|
||||
# Save updated keys
|
||||
with open(keys_path, 'w') as f:
|
||||
json.dump(keys, f, indent=2)
|
||||
flash('Pairing key deleted successfully.', 'success')
|
||||
else:
|
||||
flash('Pairing key not found.', 'warning')
|
||||
|
||||
return redirect(url_for('main.download_extension'))
|
||||
|
||||
@bp.route('/download_extension')
|
||||
@superadmin_only
|
||||
def download_extension():
|
||||
|
||||
851
py_app/app/templates/download_extension.html
Executable file → Normal file
851
py_app/app/templates/download_extension.html
Executable file → Normal file
@@ -1,576 +1,327 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<div style="max-width: 600px; margin: 40px auto; padding: 32px; background: #fff; border-radius: 12px; box-shadow: 0 2px 12px #0002;">
|
||||
<h2>QZ Tray Pairing Key Management</h2>
|
||||
<form id="pairing-form" method="POST" action="/generate_pairing_key" style="margin-bottom: 32px;">
|
||||
<label for="printer_name">Printer Name:</label>
|
||||
<input type="text" id="printer_name" name="printer_name" required style="margin: 0 8px 0 8px;">
|
||||
<button type="submit">Generate Pairing Key</button>
|
||||
|
||||
<style>
|
||||
/* QZ Pairing Key Management Card */
|
||||
.qz-pairing-card {
|
||||
max-width: 800px;
|
||||
margin: 40px auto;
|
||||
padding: 32px;
|
||||
background: var(--card-bg, #fff);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
|
||||
color: var(--text-color, #333);
|
||||
}
|
||||
|
||||
.qz-pairing-card h2 {
|
||||
color: var(--text-color, #333);
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.qz-pairing-card h3 {
|
||||
color: var(--text-color, #333);
|
||||
margin-top: 32px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.qz-form-group {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.qz-form-group label {
|
||||
display: inline-block;
|
||||
font-weight: 600;
|
||||
color: var(--label-color, #555);
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.qz-form-group input[type="text"] {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--input-border, #ddd);
|
||||
border-radius: 6px;
|
||||
background: var(--input-bg, #fff);
|
||||
color: var(--text-color, #333);
|
||||
font-size: 1em;
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
.qz-form-group input[type="text"]:focus {
|
||||
outline: none;
|
||||
border-color: #2196f3;
|
||||
box-shadow: 0 0 0 3px rgba(33, 150, 243, 0.1);
|
||||
}
|
||||
|
||||
.qz-form-group select {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid var(--input-border, #ddd);
|
||||
border-radius: 6px;
|
||||
background: var(--input-bg, #fff);
|
||||
color: var(--text-color, #333);
|
||||
font-size: 1em;
|
||||
min-width: 150px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.qz-form-group select:focus {
|
||||
outline: none;
|
||||
border-color: #2196f3;
|
||||
box-shadow: 0 0 0 3px rgba(33, 150, 243, 0.1);
|
||||
}
|
||||
|
||||
.qz-form-group button {
|
||||
padding: 8px 20px;
|
||||
background: #4caf50;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.qz-form-group button:hover {
|
||||
background: #388e3c;
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
padding: 4px 12px;
|
||||
background: #dc3545;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 0.875em;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.btn-delete:hover {
|
||||
background: #c82333;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-end;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.qz-result {
|
||||
margin-top: 20px;
|
||||
padding: 16px;
|
||||
background: var(--result-bg, #e8f5e9);
|
||||
border-left: 4px solid #4caf50;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.qz-result strong {
|
||||
color: var(--result-label, #2e7d32);
|
||||
}
|
||||
|
||||
.qz-result span {
|
||||
font-family: 'Courier New', monospace;
|
||||
background: var(--code-bg, #c8e6c9);
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
color: var(--code-text, #1b5e20);
|
||||
}
|
||||
|
||||
.qz-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.qz-table thead tr {
|
||||
background: var(--table-header-bg, #f0f0f0);
|
||||
}
|
||||
|
||||
.qz-table th {
|
||||
padding: 12px;
|
||||
border: 1px solid var(--table-border, #ccc);
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
color: var(--text-color, #333);
|
||||
}
|
||||
|
||||
.qz-table td {
|
||||
padding: 12px;
|
||||
border: 1px solid var(--table-border, #ccc);
|
||||
color: var(--text-color, #333);
|
||||
}
|
||||
|
||||
.qz-table tbody tr:hover {
|
||||
background: var(--table-hover, #f5f5f5);
|
||||
}
|
||||
|
||||
.qz-table-code {
|
||||
font-family: 'Courier New', monospace;
|
||||
background: var(--code-bg, #f5f5f5);
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
color: var(--code-text, #333);
|
||||
}
|
||||
|
||||
/* Dark Mode Support */
|
||||
body.dark-mode .qz-pairing-card {
|
||||
background: #2d2d2d;
|
||||
color: #e0e0e0;
|
||||
--card-bg: #2d2d2d;
|
||||
--text-color: #e0e0e0;
|
||||
}
|
||||
|
||||
body.dark-mode .qz-form-group label {
|
||||
color: #bbb;
|
||||
--label-color: #bbb;
|
||||
}
|
||||
|
||||
body.dark-mode .qz-form-group input[type="text"],
|
||||
body.dark-mode .qz-form-group select {
|
||||
background: #3a3a3a;
|
||||
border-color: #555;
|
||||
color: #e0e0e0;
|
||||
--input-bg: #3a3a3a;
|
||||
--input-border: #555;
|
||||
}
|
||||
|
||||
body.dark-mode .qz-result {
|
||||
background: #1b5e20;
|
||||
--result-bg: #1b5e20;
|
||||
}
|
||||
|
||||
body.dark-mode .qz-result strong {
|
||||
color: #a5d6a7;
|
||||
--result-label: #a5d6a7;
|
||||
}
|
||||
|
||||
body.dark-mode .qz-result span {
|
||||
background: #2e7d32;
|
||||
color: #c8e6c9;
|
||||
--code-bg: #2e7d32;
|
||||
--code-text: #c8e6c9;
|
||||
}
|
||||
|
||||
body.dark-mode .qz-table thead tr {
|
||||
background: #3a3a3a;
|
||||
--table-header-bg: #3a3a3a;
|
||||
}
|
||||
|
||||
body.dark-mode .qz-table th,
|
||||
body.dark-mode .qz-table td {
|
||||
border-color: #555;
|
||||
color: #e0e0e0;
|
||||
--table-border: #555;
|
||||
}
|
||||
|
||||
body.dark-mode .qz-table tbody tr:hover {
|
||||
background: #3a3a3a;
|
||||
--table-hover: #3a3a3a;
|
||||
}
|
||||
|
||||
body.dark-mode .qz-table-code {
|
||||
background: #3a3a3a;
|
||||
color: #90caf9;
|
||||
--code-bg: #3a3a3a;
|
||||
--code-text: #90caf9;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="qz-pairing-card">
|
||||
<h2>🔐 QZ Tray Pairing Key Management</h2>
|
||||
|
||||
<form id="pairing-form" method="POST" action="/generate_pairing_key" class="qz-form-group">
|
||||
<div class="form-row">
|
||||
<div class="form-field">
|
||||
<label for="printer_name">Printer Name:</label>
|
||||
<input type="text" id="printer_name" name="printer_name" required placeholder="Enter printer name">
|
||||
</div>
|
||||
|
||||
<div class="form-field">
|
||||
<label for="validity_days">Validity Period:</label>
|
||||
<select id="validity_days" name="validity_days">
|
||||
<option value="30">30 Days</option>
|
||||
<option value="60">60 Days</option>
|
||||
<option value="90" selected>90 Days (Default)</option>
|
||||
<option value="180">180 Days (6 Months)</option>
|
||||
<option value="365">365 Days (1 Year)</option>
|
||||
<option value="730">730 Days (2 Years)</option>
|
||||
<option value="1825">1825 Days (5 Years)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button type="submit">🔑 Generate Pairing Key</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div id="pairing-result">
|
||||
{% if pairing_key %}
|
||||
<div style="margin-bottom: 16px;">
|
||||
<strong>Pairing Key:</strong> <span style="font-family: monospace;">{{ pairing_key }}</span><br>
|
||||
<strong>Printer Name:</strong> {{ printer_name }}<br>
|
||||
<strong>Valid Until:</strong> {{ warranty_until }}
|
||||
<div class="qz-result">
|
||||
<div style="margin-bottom: 8px;">
|
||||
<strong>🔑 Pairing Key:</strong> <span>{{ pairing_key }}</span>
|
||||
</div>
|
||||
<div style="margin-bottom: 8px;">
|
||||
<strong>🖨️ Printer Name:</strong> {{ printer_name }}
|
||||
</div>
|
||||
<div>
|
||||
<strong>⏰ Valid Until:</strong> {{ warranty_until }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h3>Active Pairing Keys</h3>
|
||||
<table style="width:100%; border-collapse:collapse;">
|
||||
|
||||
<h3>📋 Active Pairing Keys</h3>
|
||||
<table class="qz-table">
|
||||
<thead>
|
||||
<tr style="background:#f0f0f0;">
|
||||
<th style="padding:8px; border:1px solid #ccc;">Printer Name</th>
|
||||
<th style="padding:8px; border:1px solid #ccc;">Pairing Key</th>
|
||||
<th style="padding:8px; border:1px solid #ccc;">Valid Until</th>
|
||||
<tr>
|
||||
<th>🖨️ Printer Name</th>
|
||||
<th>🔑 Pairing Key</th>
|
||||
<th>⏰ Valid Until</th>
|
||||
<th>🛠️ Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for key in pairing_keys %}
|
||||
<tr>
|
||||
<td style="padding:8px; border:1px solid #ccc;">{{ key.printer_name }}</td>
|
||||
<td style="padding:8px; border:1px solid #ccc; font-family:monospace;">{{ key.pairing_key }}</td>
|
||||
<td style="padding:8px; border:1px solid #ccc;">{{ key.warranty_until }}</td>
|
||||
<td>{{ key.printer_name }}</td>
|
||||
<td><span class="qz-table-code">{{ key.pairing_key }}</span></td>
|
||||
<td>{{ key.warranty_until }}</td>
|
||||
<td>
|
||||
<button class="btn-delete" onclick="deletePairingKey('{{ key.pairing_key }}', '{{ key.printer_name }}')">
|
||||
🗑️ Delete
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<script>
|
||||
// Optionally add AJAX for key generation if you want dynamic updates
|
||||
</script>
|
||||
<ul>
|
||||
<li>⚡ <strong>Silent Printing</strong> - No user interaction needed</li>
|
||||
<li><EFBFBD>️ <strong>Direct Printer Access</strong> - System-level printing</li>
|
||||
<li>🏢 <strong>Enterprise Ready</strong> - Service auto-recovery</li>
|
||||
<li><EFBFBD> <strong>Advanced Features</strong> - Multiple print methods</li>
|
||||
<li>🛡️ <strong>Self-Contained</strong> - Zero external dependencies</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Download Cards Row -->
|
||||
<div class="row justify-content-center">
|
||||
<!-- Chrome Extension Card -->
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card h-100 border-success">
|
||||
<div class="card-header bg-success text-white text-center">
|
||||
<h4 class="mb-0">🌐 Chrome Extension</h4>
|
||||
<small>Browser-based printing solution</small>
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column">
|
||||
<div class="alert alert-success">
|
||||
<strong>📋 RECOMMENDED:</strong> Easy setup, works everywhere!
|
||||
</div>
|
||||
|
||||
<h5>🎯 Key Features:</h5>
|
||||
<ul>
|
||||
<li>🖨️ Opens PDF in hidden browser tab and triggers print dialog</li>
|
||||
<li>🔍 Automatic extension detection on web page</li>
|
||||
<li>📊 Print status feedback and error handling</li>
|
||||
<li>🔄 Graceful fallback to PDF download</li>
|
||||
<li>⚙️ Works with any printer Chrome can access</li>
|
||||
<li>🌍 Cross-platform (Windows, Mac, Linux)</li>
|
||||
</ul>
|
||||
|
||||
<h5>🚀 Quick Install (3 steps):</h5>
|
||||
<ol>
|
||||
<li>Download and extract the extension files</li>
|
||||
<li>Open Chrome → <code>chrome://extensions/</code></li>
|
||||
<li>Enable <strong>"Developer mode"</strong> → Click <strong>"Load unpacked"</strong> → Select folder</li>
|
||||
</ol>
|
||||
|
||||
<div class="text-center mt-auto">
|
||||
<button class="btn btn-success btn-lg mb-3" id="download-extension-btn">
|
||||
📥 Download Chrome Extension
|
||||
</button>
|
||||
<br>
|
||||
<small class="text-muted">Extension package (~10KB)</small>
|
||||
|
||||
<hr>
|
||||
<div class="text-center">
|
||||
<small class="text-muted">Individual files for inspection:</small><br>
|
||||
<a href="{{ url_for('main.extension_files', filename='manifest.json') }}" target="_blank" class="btn btn-sm btn-outline-secondary">manifest.json</a>
|
||||
<a href="{{ url_for('main.extension_files', filename='background.js') }}" target="_blank" class="btn btn-sm btn-outline-secondary">background.js</a>
|
||||
<a href="{{ url_for('main.extension_files', filename='content.js') }}" target="_blank" class="btn btn-sm btn-outline-secondary">content.js</a>
|
||||
<a href="{{ url_for('main.extension_files', filename='popup.html') }}" target="_blank" class="btn btn-sm btn-outline-secondary">popup.html</a>
|
||||
<a href="{{ url_for('main.extension_files', filename='popup.js') }}" target="_blank" class="btn btn-sm btn-outline-secondary">popup.js</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Windows Service Card -->
|
||||
<div class="col-md-6 mb-4">
|
||||
<div class="card h-100 border-primary">
|
||||
<div class="card-header bg-primary text-white text-center">
|
||||
<h4 class="mb-0">🔧 Windows Print Service</h4>
|
||||
<small>Enterprise-grade silent printing with Error 1053 fixes</small>
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column">
|
||||
<div class="alert alert-primary">
|
||||
<strong>🏢 ENTERPRISE:</strong> Silent printing with no user interaction!
|
||||
</div>
|
||||
|
||||
<div class="alert alert-success" style="background-color: #d1f2eb; border-color: #a3e4d7; color: #0e6b49;">
|
||||
<strong>🆕 NEW: Error 1053 FIXED!</strong><br>
|
||||
<small>✅ Multiple installation methods with automatic fallback<br>
|
||||
✅ Enhanced Windows Service Communication<br>
|
||||
✅ Comprehensive diagnostic and troubleshooting tools</small>
|
||||
</div>
|
||||
|
||||
<h5>🎯 Key Features:</h5>
|
||||
<ul>
|
||||
<li>⚡ Silent printing - no print dialogs</li>
|
||||
<li>🖨️ Direct system printer access</li>
|
||||
<li>🔄 Multiple print methods (Adobe, SumatraPDF, PowerShell)</li>
|
||||
<li>🛡️ Windows service with auto-recovery</li>
|
||||
<li>📦 Self-contained - zero dependencies</li>
|
||||
<li>🏢 Perfect for production environments</li>
|
||||
<li><strong style="color: #dc3545;">🔧 Error 1053 fixes included</strong></li>
|
||||
</ul>
|
||||
|
||||
<h5>🚀 Quick Install (3 steps):</h5>
|
||||
<ol>
|
||||
<li>Download and extract the enhanced service package</li>
|
||||
<li>Run <code>install_service_ENHANCED.bat</code> as Administrator</li>
|
||||
<li>Install Chrome extension (included in package)</li>
|
||||
</ol>
|
||||
|
||||
<div class="text-center mt-auto">
|
||||
<div class="btn-group-vertical mb-3" role="group">
|
||||
<button class="btn btn-primary btn-lg" id="download-service-btn">
|
||||
📥 Download Enhanced Windows Service
|
||||
</button>
|
||||
<small class="text-muted mb-2">🆕 Includes Error 1053 fixes & multiple installation methods</small>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-info">
|
||||
<small>
|
||||
<strong>🆕 Enhanced Package (~11MB):</strong> Embedded Python 3.11.9 + Error 1053 fixes<br>
|
||||
<strong>✅ Features:</strong> 4 installation methods, diagnostic tools, zero dependencies
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="alert alert-warning">
|
||||
<small>⚠️ <strong>Windows Only:</strong> Requires Administrator privileges for service installation</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Documentation Section -->
|
||||
<div class="row justify-content-center mb-4">
|
||||
<div class="col-md-10">
|
||||
<div class="card border-warning">
|
||||
<div class="card-header bg-warning text-dark">
|
||||
<h4 class="mb-0">📚 Documentation & Support</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header bg-success text-white text-center">
|
||||
<h6 class="mb-0">⚡ Quick Setup Guide</h6>
|
||||
</div>
|
||||
<div class="card-body text-center">
|
||||
<p class="card-text">2-minute installation guide with visual steps and troubleshooting tips.</p>
|
||||
<a href="/static/documentation/QUICK_SETUP.md" target="_blank" class="btn btn-success">
|
||||
📖 Quick Setup
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header bg-primary text-white text-center">
|
||||
<h6 class="mb-0">📋 Complete Guide</h6>
|
||||
</div>
|
||||
<div class="card-body text-center">
|
||||
<p class="card-text">Comprehensive installation documentation with troubleshooting and API reference.</p>
|
||||
<a href="/static/documentation/INSTALLATION_GUIDE.md" target="_blank" class="btn btn-primary">
|
||||
📚 Full Documentation
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<div class="card h-100">
|
||||
<div class="card-header bg-info text-white text-center">
|
||||
<h6 class="mb-0">🛠️ Technical Reference</h6>
|
||||
</div>
|
||||
<div class="card-body text-center">
|
||||
<p class="card-text">Developer documentation with API specs and customization examples.</p>
|
||||
<a href="/static/documentation/README.md" target="_blank" class="btn btn-info">
|
||||
🔧 Developer Docs
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Requirements -->
|
||||
<div class="row justify-content-center mb-4">
|
||||
<div class="col-md-10">
|
||||
<div class="card border-secondary">
|
||||
<div class="card-header bg-secondary text-white">
|
||||
<h4 class="mb-0">⚙️ System Requirements & Information</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<h5>💻 Requirements:</h5>
|
||||
<ul>
|
||||
<li><strong>Browser:</strong> Google Chrome (any recent version)</li>
|
||||
<li><strong>OS:</strong> Windows, Mac, or Linux</li>
|
||||
<li><strong>Privileges:</strong> None required (standard user)</li>
|
||||
<li><strong>Internet:</strong> Not required (works offline)</li>
|
||||
<li><strong>Installation:</strong> Just load extension in Chrome</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<h5>🔍 How It Works:</h5>
|
||||
<div class="alert alert-light">
|
||||
<ol class="mb-0">
|
||||
<li>🌐 Web page detects Chrome extension</li>
|
||||
<li>🖨️ <strong>Extension Available</strong> → Green "Print Labels (Extension)" button</li>
|
||||
<li>📄 <strong>Extension Unavailable</strong> → Blue "Generate PDF" button</li>
|
||||
<li>🔄 Extension opens PDF in hidden tab → triggers print dialog</li>
|
||||
<li>✅ User selects printer and confirms → automatic cleanup</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="row justify-content-center mb-5">
|
||||
<div class="col-md-10 text-center">
|
||||
<div class="card border-dark">
|
||||
<div class="card-body">
|
||||
<h5>🚀 Ready to Test?</h5>
|
||||
<p class="text-muted">Installation takes ~2 minutes • Zero maintenance required</p>
|
||||
|
||||
<!-- Test Extension Button -->
|
||||
<div class="alert alert-info">
|
||||
<h6>🧪 Test the Extension</h6>
|
||||
<p class="mb-2">After installing the extension, click below to test if the print module detects it correctly:</p>
|
||||
<button class="btn btn-info mb-2" id="test-extension-btn">
|
||||
🔍 Test Extension Detection
|
||||
</button>
|
||||
<div id="test-results" class="mt-2" style="display: none;"></div>
|
||||
</div>
|
||||
|
||||
<div class="btn-group-vertical btn-group-lg" role="group">
|
||||
<a href="{{ url_for('main.print_module') }}" class="btn btn-success btn-lg">
|
||||
🖨️ Go to Print Module
|
||||
</a>
|
||||
<button class="btn btn-secondary" onclick="window.close()">
|
||||
↩️ Close Window
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Chrome Extension Download Handler
|
||||
document.getElementById('download-extension-btn').addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Show loading state
|
||||
const originalText = this.innerHTML;
|
||||
this.innerHTML = '⏳ Preparing Chrome Extension Package...';
|
||||
this.disabled = true;
|
||||
|
||||
// Create the extension package
|
||||
fetch('/create_extension_package', {method: 'POST'})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// Start download
|
||||
window.location.href = data.download_url;
|
||||
|
||||
// Show success message
|
||||
setTimeout(() => {
|
||||
this.innerHTML = '✅ Download Started!';
|
||||
}, 500);
|
||||
|
||||
// Reset button
|
||||
setTimeout(() => {
|
||||
this.innerHTML = originalText;
|
||||
this.disabled = false;
|
||||
}, 3000);
|
||||
} else {
|
||||
alert('Error creating extension package: ' + data.error);
|
||||
this.innerHTML = originalText;
|
||||
this.disabled = false;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('Error: ' + error.message);
|
||||
this.innerHTML = originalText;
|
||||
this.disabled = false;
|
||||
});
|
||||
});
|
||||
|
||||
// Windows Service Download Handler
|
||||
document.getElementById('download-service-btn').addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Show loading state
|
||||
const originalText = this.innerHTML;
|
||||
this.innerHTML = '⏳ Preparing Enhanced Service Package...';
|
||||
this.disabled = true;
|
||||
|
||||
// Create the service package
|
||||
fetch('/create_service_package', {method: 'POST'})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// Start download
|
||||
window.location.href = data.download_url;
|
||||
|
||||
// Show enhanced success message with features
|
||||
const features = data.features ? data.features.join('\n• ') : 'Error 1053 fixes and enhanced installation';
|
||||
setTimeout(() => {
|
||||
this.innerHTML = '✅ Enhanced Package Downloaded!';
|
||||
|
||||
// Show feature alert
|
||||
if (data.package_type) {
|
||||
alert(`✅ ${data.package_type} Downloaded!\n\n🆕 Features included:\n• ${features}\n\n📦 Size: ${(data.zip_size / 1024 / 1024).toFixed(1)} MB\n🔧 Installation methods: ${data.installation_methods || 'Multiple'}\n\n📋 Next: Extract and run install_service_ENHANCED.bat as Administrator`);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
// Reset button
|
||||
setTimeout(() => {
|
||||
this.innerHTML = originalText;
|
||||
this.disabled = false;
|
||||
}, 5000);
|
||||
} else {
|
||||
alert('Error creating service package: ' + data.error);
|
||||
this.innerHTML = originalText;
|
||||
this.disabled = false;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('Error: ' + error.message);
|
||||
this.innerHTML = originalText;
|
||||
this.disabled = false;
|
||||
});
|
||||
});
|
||||
|
||||
// Zero Dependencies Service Download Handler
|
||||
document.getElementById('download-zero-deps-btn').addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Show loading state
|
||||
const originalText = this.innerHTML;
|
||||
this.innerHTML = '⏳ Creating Zero-Dependency Package (may take 1-2 minutes)...';
|
||||
this.disabled = true;
|
||||
|
||||
// Show progress info
|
||||
const progressInfo = document.createElement('div');
|
||||
progressInfo.className = 'alert alert-info mt-2';
|
||||
progressInfo.innerHTML = '📥 Downloading Python embedded distribution and creating complete package...';
|
||||
this.parentElement.appendChild(progressInfo);
|
||||
|
||||
// Create the zero-dependency service package
|
||||
fetch('/create_zero_dependency_service_package', {method: 'POST'})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
// Start download
|
||||
window.location.href = data.download_url;
|
||||
|
||||
// Show success message
|
||||
setTimeout(() => {
|
||||
this.innerHTML = `✅ Download Started! (${data.estimated_size_mb}MB)`;
|
||||
progressInfo.innerHTML = `🎉 Complete package created with ${data.files_included} files including Python ${data.python_version}!`;
|
||||
progressInfo.className = 'alert alert-success mt-2';
|
||||
}, 500);
|
||||
|
||||
// Reset button
|
||||
setTimeout(() => {
|
||||
this.innerHTML = originalText;
|
||||
this.disabled = false;
|
||||
progressInfo.remove();
|
||||
}, 5000);
|
||||
} else {
|
||||
alert('Error creating zero-dependency package: ' + data.error);
|
||||
this.innerHTML = originalText;
|
||||
this.disabled = false;
|
||||
progressInfo.remove();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('Error: ' + error.message);
|
||||
this.innerHTML = originalText;
|
||||
this.disabled = false;
|
||||
progressInfo.remove();
|
||||
});
|
||||
});
|
||||
|
||||
// Extension Test Functionality
|
||||
document.getElementById('test-extension-btn').addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const testResults = document.getElementById('test-results');
|
||||
const originalText = this.innerHTML;
|
||||
|
||||
this.innerHTML = '🔍 Testing...';
|
||||
this.disabled = true;
|
||||
testResults.style.display = 'block';
|
||||
testResults.innerHTML = '<div class="spinner-border spinner-border-sm" role="status"></div> Checking Chrome extension...';
|
||||
|
||||
// Test extension detection (same logic as print module)
|
||||
testExtensionConnection()
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
testResults.innerHTML = `
|
||||
<div class="alert alert-success">
|
||||
<strong>✅ Extension Test Successful!</strong><br>
|
||||
Extension ID: ${result.extensionId || 'Detected'}<br>
|
||||
Version: ${result.version || 'Unknown'}<br>
|
||||
Status: Ready for printing<br>
|
||||
<small class="text-muted">The print module will show the green "Print Labels (Extension)" button</small>
|
||||
</div>
|
||||
`;
|
||||
} else {
|
||||
testResults.innerHTML = `
|
||||
<div class="alert alert-warning">
|
||||
<strong>❌ Extension Not Detected</strong><br>
|
||||
Reason: ${result.error}<br>
|
||||
<small class="text-muted">
|
||||
Make sure you've:<br>
|
||||
1. Downloaded and extracted the extension<br>
|
||||
2. Loaded it in Chrome at chrome://extensions/<br>
|
||||
3. Enabled "Developer mode" first<br>
|
||||
4. Refreshed this page after installation
|
||||
</small>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
testResults.innerHTML = `
|
||||
<div class="alert alert-danger">
|
||||
<strong>❌ Test Failed</strong><br>
|
||||
Error: ${error.message}<br>
|
||||
<small class="text-muted">Chrome extension communication failed</small>
|
||||
</div>
|
||||
`;
|
||||
})
|
||||
.finally(() => {
|
||||
this.innerHTML = originalText;
|
||||
this.disabled = false;
|
||||
});
|
||||
});
|
||||
|
||||
// Test extension connection function
|
||||
async function testExtensionConnection() {
|
||||
return new Promise((resolve) => {
|
||||
// Try to get extension ID from injected DOM element or use fallback
|
||||
const extensionElement = document.getElementById('chrome-extension-id');
|
||||
const extensionId = extensionElement ?
|
||||
extensionElement.getAttribute('data-extension-id') :
|
||||
'cifcoidplhgclhcnlcgdkjbaoempjmdl'; // Fallback
|
||||
function deletePairingKey(pairingKey, printerName) {
|
||||
if (confirm(`Are you sure you want to delete the pairing key for "${printerName}"?\n\nKey: ${pairingKey}\n\nThis action cannot be undone.`)) {
|
||||
// Create a form and submit it
|
||||
const form = document.createElement('form');
|
||||
form.method = 'POST';
|
||||
form.action = '/delete_pairing_key';
|
||||
|
||||
if (!window.chrome || !window.chrome.runtime) {
|
||||
resolve({ success: false, error: 'Chrome runtime not available' });
|
||||
return;
|
||||
}
|
||||
const keyInput = document.createElement('input');
|
||||
keyInput.type = 'hidden';
|
||||
keyInput.name = 'pairing_key';
|
||||
keyInput.value = pairingKey;
|
||||
|
||||
try {
|
||||
chrome.runtime.sendMessage(extensionId, { action: 'ping' }, function(response) {
|
||||
if (chrome.runtime.lastError) {
|
||||
resolve({
|
||||
success: false,
|
||||
error: chrome.runtime.lastError.message,
|
||||
extensionId: extensionId
|
||||
});
|
||||
} else if (response && response.success) {
|
||||
resolve({
|
||||
success: true,
|
||||
extensionId: extensionId,
|
||||
version: response.extension_version,
|
||||
message: response.message
|
||||
});
|
||||
} else {
|
||||
resolve({
|
||||
success: false,
|
||||
error: 'Extension ping failed - no response',
|
||||
extensionId: extensionId
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
resolve({
|
||||
success: false,
|
||||
error: 'Exception: ' + error.message,
|
||||
extensionId: extensionId
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Show installation tips
|
||||
function showInstallationTips() {
|
||||
const tips = [
|
||||
'💡 Tip: Chrome Extension is recommended for most users - cross-platform and easy!',
|
||||
'💡 Tip: Windows Service is perfect for enterprise environments requiring silent printing',
|
||||
'💡 Tip: Both solutions work with the same web interface and automatically detected',
|
||||
'💡 Tip: The system gracefully falls back to PDF downloads if neither is available'
|
||||
];
|
||||
|
||||
let tipIndex = 0;
|
||||
const tipContainer = document.createElement('div');
|
||||
tipContainer.className = 'alert alert-info position-fixed';
|
||||
tipContainer.style.cssText = 'bottom: 20px; right: 20px; max-width: 300px; z-index: 1000;';
|
||||
|
||||
function showNextTip() {
|
||||
if (tipIndex < tips.length) {
|
||||
tipContainer.innerHTML = `
|
||||
<button type="button" class="btn-close float-end" onclick="this.parentElement.remove()"></button>
|
||||
${tips[tipIndex]}
|
||||
`;
|
||||
|
||||
if (!document.body.contains(tipContainer)) {
|
||||
document.body.appendChild(tipContainer);
|
||||
}
|
||||
|
||||
tipIndex++;
|
||||
setTimeout(showNextTip, 8000); // Show next tip after 8 seconds
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
if (document.body.contains(tipContainer)) {
|
||||
tipContainer.remove();
|
||||
}
|
||||
}, 5000);
|
||||
}
|
||||
form.appendChild(keyInput);
|
||||
document.body.appendChild(form);
|
||||
form.submit();
|
||||
}
|
||||
|
||||
// Start showing tips after 3 seconds
|
||||
setTimeout(showNextTip, 3000);
|
||||
}
|
||||
|
||||
// Initialize tips when page loads
|
||||
document.addEventListener('DOMContentLoaded', showInstallationTips);
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<p>Access the print module to print labels.</p>
|
||||
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
||||
<a href="{{ url_for('main.print_module') }}" class="btn">Launch Printing Module</a>
|
||||
<a href="{{ url_for('main.print_module') }}" class="btn">Launch lost labels printing module</a>
|
||||
<a href="{{ url_for('main.print_lost_labels') }}" class="btn">Launch lost labels printing module</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -33,7 +33,10 @@
|
||||
<label for="client-select" style="font-size: 11px; font-weight: 600; display: block; margin-bottom: 4px;">Select Printer/Client:</label>
|
||||
<select id="client-select" class="form-control form-control-sm" style="width: 85%; margin: 0 auto; font-size: 11px;"></select>
|
||||
</div>
|
||||
<!-- Manage Keys Button - Only visible for superadmin -->
|
||||
{% if session.role == 'superadmin' %}
|
||||
<a href="{{ url_for('main.download_extension') }}" class="btn btn-info btn-sm" target="_blank" style="font-size: 11px; padding: 4px 12px;">🔑 Manage Keys</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<!-- Label Preview Section -->
|
||||
<div id="label-preview" style="padding: 10px; position: relative; background: #fafafa; width: 301px; height: 434.7px;">
|
||||
@@ -194,6 +197,77 @@ let selectedOrderData = null;
|
||||
let qzTray = null;
|
||||
let availablePrinters = [];
|
||||
|
||||
// Function to display the last N orders in the table
|
||||
function displayRecentOrders(limit = 20) {
|
||||
const tbody = document.getElementById('unprinted-orders-table');
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (allOrders.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="16" style="text-align:center; color:#6c757d;">No printed orders found.</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the last N orders (they are already sorted by updated_at DESC from backend)
|
||||
const recentOrders = allOrders.slice(0, limit);
|
||||
|
||||
recentOrders.forEach((order, idx) => {
|
||||
const tr = document.createElement('tr');
|
||||
|
||||
// Format data_livrare as DD/MM/YYYY if possible
|
||||
let dataLivrareFormatted = '-';
|
||||
if (order.data_livrare) {
|
||||
const d = new Date(order.data_livrare);
|
||||
if (!isNaN(d)) {
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const year = d.getFullYear();
|
||||
dataLivrareFormatted = `${day}/${month}/${year}`;
|
||||
} else {
|
||||
dataLivrareFormatted = order.data_livrare;
|
||||
}
|
||||
}
|
||||
|
||||
tr.innerHTML = `
|
||||
<td>${order.id}</td>
|
||||
<td><strong>${order.comanda_productie}</strong></td>
|
||||
<td>${order.cod_articol || '-'}</td>
|
||||
<td>${order.descr_com_prod}</td>
|
||||
<td style="text-align: right; font-weight: 600;">${order.cantitate}</td>
|
||||
<td style="text-align: center;">${dataLivrareFormatted}</td>
|
||||
<td style="text-align: center;">${order.dimensiune || '-'}</td>
|
||||
<td>${order.com_achiz_client || '-'}</td>
|
||||
<td style="text-align: right;">${order.nr_linie_com_client || '-'}</td>
|
||||
<td>${order.customer_name || '-'}</td>
|
||||
<td>${order.customer_article_number || '-'}</td>
|
||||
<td>${order.open_for_order || '-'}</td>
|
||||
<td style="text-align: right;">${order.line_number || '-'}</td>
|
||||
<td style="text-align: center;">${order.printed_labels == 1 ? '<span style="color: #28a745; font-weight: bold;">✓ Yes</span>' : '<span style="color: #dc3545;">✗ No</span>'}</td>
|
||||
<td style="font-size: 11px; color: #6c757d;">${order.created_at || '-'}</td>
|
||||
<td>1</td>
|
||||
`;
|
||||
|
||||
tr.addEventListener('click', function() {
|
||||
document.querySelectorAll('.print-module-table tbody tr').forEach(row => {
|
||||
row.classList.remove('selected');
|
||||
const cells = row.querySelectorAll('td');
|
||||
cells.forEach(cell => {
|
||||
cell.style.backgroundColor = '';
|
||||
cell.style.color = '';
|
||||
});
|
||||
});
|
||||
this.classList.add('selected');
|
||||
const cells = this.querySelectorAll('td');
|
||||
cells.forEach(cell => {
|
||||
cell.style.backgroundColor = '#007bff';
|
||||
cell.style.color = 'white';
|
||||
});
|
||||
updatePreviewCard(order);
|
||||
});
|
||||
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
function searchOrder() {
|
||||
const searchValue = document.getElementById('search-input').value.trim().toLowerCase();
|
||||
if (!searchValue) {
|
||||
@@ -345,6 +419,9 @@ async function loadQZTrayPrinters() {
|
||||
|
||||
// Print Button Handler
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Display last 20 printed orders on page load
|
||||
displayRecentOrders(20);
|
||||
|
||||
setTimeout(initializeQZTray, 1000);
|
||||
document.getElementById('print-label-btn').addEventListener('click', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -24,8 +24,10 @@
|
||||
<label for="client-select" style="font-size: 11px; font-weight: 600; display: block; margin-bottom: 4px;">Select Printer/Client:</label>
|
||||
<select id="client-select" class="form-control form-control-sm" style="width: 85%; margin: 0 auto; font-size: 11px;"></select>
|
||||
</div>
|
||||
<!-- Manage Keys Button -->
|
||||
<!-- Manage Keys Button - Only visible for superadmin -->
|
||||
{% if session.role == 'superadmin' %}
|
||||
<a href="{{ url_for('main.download_extension') }}" class="btn btn-info btn-sm" target="_blank" style="font-size: 11px; padding: 4px 12px;">🔑 Manage Keys</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Label Preview Section -->
|
||||
|
||||
Reference in New Issue
Block a user