Implement boxes management module with auto-numbered box creation

- Add boxes_crates database table with BIGINT IDs and 8-digit auto-numbered box_numbers
- Implement boxes CRUD operations (add, edit, update, delete, delete_multiple)
- Create boxes route handlers with POST actions for all operations
- Add boxes.html template with 3-panel layout matching warehouse locations module
- Implement barcode generation and printing with JsBarcode and QZ Tray integration
- Add browser print fallback for when QZ Tray is not available
- Simplify create box form to single button with auto-generation
- Fix JavaScript null reference errors with proper element validation
- Convert tuple data to dictionaries for Jinja2 template compatibility
- Register boxes blueprint in Flask app initialization
This commit is contained in:
Quality App Developer
2026-01-26 22:08:31 +02:00
parent 3c5a273a89
commit e1f3302c6b
37 changed files with 8429 additions and 66 deletions

View File

@@ -128,12 +128,16 @@ def register_blueprints(app):
from app.routes import main_bp
from app.modules.quality.routes import quality_bp
from app.modules.settings.routes import settings_bp
from app.modules.warehouse.routes import warehouse_bp
from app.modules.warehouse.boxes_routes import boxes_bp
app.register_blueprint(main_bp)
app.register_blueprint(quality_bp, url_prefix='/quality')
app.register_blueprint(settings_bp, url_prefix='/settings')
app.register_blueprint(warehouse_bp, url_prefix='/warehouse')
app.register_blueprint(boxes_bp)
app.logger.info("Blueprints registered: main, quality, settings")
app.logger.info("Blueprints registered: main, quality, settings, warehouse, boxes")
def register_error_handlers(app):

View File

@@ -26,15 +26,6 @@ def quality_index():
return render_template('modules/quality/index.html')
@quality_bp.route('/inspections', methods=['GET'])
def inspections():
"""View and manage quality inspections"""
if 'user_id' not in session:
return redirect(url_for('main.login'))
return render_template('modules/quality/inspections.html')
@quality_bp.route('/reports', methods=['GET'])
def quality_reports():
"""Quality reports page - displays FG scan reports"""

View File

@@ -0,0 +1,231 @@
"""
Settings Module - Log Explorer Helper
Provides functions to explore and manage application logs
"""
import os
from datetime import datetime
from pathlib import Path
import logging
logger = logging.getLogger(__name__)
def get_log_files():
"""Get list of all log files in the logs folder"""
try:
log_dir = './data/logs'
if not os.path.exists(log_dir):
return []
log_files = []
for filename in sorted(os.listdir(log_dir)):
filepath = os.path.join(log_dir, filename)
if os.path.isfile(filepath):
try:
stat_info = os.stat(filepath)
log_files.append({
'name': filename,
'size': stat_info.st_size,
'size_mb': round(stat_info.st_size / 1024 / 1024, 2),
'modified_at': datetime.fromtimestamp(stat_info.st_mtime).strftime('%Y-%m-%d %H:%M:%S'),
'path': filepath
})
except Exception as e:
logger.error(f"Error getting stat info for {filename}: {e}")
continue
return sorted(log_files, key=lambda x: x['modified_at'], reverse=True)
except Exception as e:
logger.error(f"Error getting log files: {e}")
return []
def get_log_content(filename, lines=100):
"""Get content of a log file
Args:
filename: Name of the log file (without path)
lines: Number of lines to read from the end (None for all)
Returns:
Dictionary with file info and content
"""
try:
log_dir = './data/logs'
filepath = os.path.join(log_dir, filename)
# Security check - ensure filepath is within log_dir
if not os.path.abspath(filepath).startswith(os.path.abspath(log_dir)):
logger.error(f"Attempted to access file outside log directory: {filepath}")
return {
'success': False,
'error': 'Invalid file path',
'filename': filename
}
if not os.path.exists(filepath):
return {
'success': False,
'error': f'File not found: {filename}',
'filename': filename
}
# Read file content
try:
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
if lines:
# Read all lines and get the last N lines
all_lines = f.readlines()
content_lines = all_lines[-lines:] if len(all_lines) > lines else all_lines
content = ''.join(content_lines)
total_lines = len(all_lines)
else:
content = f.read()
total_lines = len(content.splitlines())
stat_info = os.stat(filepath)
return {
'success': True,
'filename': filename,
'size': stat_info.st_size,
'size_mb': round(stat_info.st_size / 1024 / 1024, 2),
'modified_at': datetime.fromtimestamp(stat_info.st_mtime).strftime('%Y-%m-%d %H:%M:%S'),
'content': content,
'total_lines': total_lines,
'displayed_lines': len(content.splitlines()),
'truncated': lines and total_lines > lines
}
except Exception as e:
return {
'success': False,
'error': f'Error reading file: {str(e)}',
'filename': filename
}
except Exception as e:
logger.error(f"Error getting log content for {filename}: {e}")
return {
'success': False,
'error': f'Error: {str(e)}',
'filename': filename
}
def get_log_file_path(filename):
"""Get safe file path for download/save
Args:
filename: Name of the log file (without path)
Returns:
Full file path if valid, None otherwise
"""
try:
log_dir = './data/logs'
filepath = os.path.join(log_dir, filename)
# Security check - ensure filepath is within log_dir
if not os.path.abspath(filepath).startswith(os.path.abspath(log_dir)):
logger.error(f"Attempted to access file outside log directory: {filepath}")
return None
if not os.path.exists(filepath):
logger.error(f"File not found: {filepath}")
return None
return filepath
except Exception as e:
logger.error(f"Error getting log file path for {filename}: {e}")
return None
def get_log_statistics():
"""Get statistics about log files"""
try:
log_files = get_log_files()
if not log_files:
return {
'total_files': 0,
'total_size_mb': 0,
'oldest_log': None,
'newest_log': None
}
total_size = sum(f['size'] for f in log_files)
return {
'total_files': len(log_files),
'total_size_mb': round(total_size / 1024 / 1024, 2),
'oldest_log': log_files[-1]['modified_at'] if log_files else None,
'newest_log': log_files[0]['modified_at'] if log_files else None
}
except Exception as e:
logger.error(f"Error getting log statistics: {e}")
return {
'total_files': 0,
'total_size_mb': 0,
'oldest_log': None,
'newest_log': None
}
def search_in_logs(search_term, filename=None, max_results=50):
"""Search for a term in log files
Args:
search_term: Term to search for
filename: Optional specific file to search in
max_results: Maximum number of results to return
Returns:
List of matching lines with context
"""
try:
log_dir = './data/logs'
results = []
if filename:
# Search in specific file
filepath = os.path.join(log_dir, filename)
if not os.path.abspath(filepath).startswith(os.path.abspath(log_dir)):
return []
if os.path.exists(filepath):
try:
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
for line_num, line in enumerate(f, 1):
if search_term.lower() in line.lower():
results.append({
'file': filename,
'line_num': line_num,
'line': line.strip()
})
if len(results) >= max_results:
break
except Exception as e:
logger.error(f"Error searching in {filename}: {e}")
else:
# Search in all log files
for log_file in get_log_files():
try:
with open(log_file['path'], 'r', encoding='utf-8', errors='ignore') as f:
for line_num, line in enumerate(f, 1):
if search_term.lower() in line.lower():
results.append({
'file': log_file['name'],
'line_num': line_num,
'line': line.strip()
})
if len(results) >= max_results:
break
if len(results) >= max_results:
break
except Exception as e:
logger.error(f"Error searching in {log_file['name']}: {e}")
return results
except Exception as e:
logger.error(f"Error searching logs: {e}")
return []

View File

@@ -7,6 +7,8 @@ import hashlib
import secrets
from datetime import datetime, timedelta
from app.database import get_db
from app.modules.settings.stats import get_all_stats
from app.modules.settings.logs import get_log_files, get_log_content, get_log_file_path, get_log_statistics, search_in_logs
import subprocess
import os
import json
@@ -19,11 +21,30 @@ settings_bp = Blueprint('settings', __name__, url_prefix='/settings')
@settings_bp.route('/', methods=['GET'])
def settings_index():
"""Settings module main page"""
"""Settings module main page with app overview"""
if 'user_id' not in session:
return redirect(url_for('main.login'))
return render_template('modules/settings/index.html')
# Get all app statistics
try:
stats = get_all_stats()
except Exception as e:
logger.error(f"Error getting stats in settings_index: {e}", exc_info=True)
stats = {
'user_count': 0,
'database_size_mb': 0,
'logs_size_mb': 0,
'database_count': 0,
'backup_count': 0,
'printer_keys_count': 0,
'app_key_availability': {
'available': False,
'count': 0,
'status': 'Error loading data'
}
}
return render_template('modules/settings/index.html', stats=stats)
@settings_bp.route('/general', methods=['GET', 'POST'])
@@ -1254,3 +1275,100 @@ def toggle_backup_schedule(schedule_id):
except Exception as e:
return jsonify({'error': str(e)}), 500
# ============================================================================
# Log Explorer Routes
# ============================================================================
@settings_bp.route('/logs', methods=['GET'])
def logs_explorer():
"""Log explorer main page - list all log files"""
if 'user_id' not in session:
return redirect(url_for('main.login'))
try:
log_files = get_log_files()
log_stats = get_log_statistics()
return render_template('modules/settings/logs_explorer.html',
log_files=log_files,
log_stats=log_stats)
except Exception as e:
logger.error(f"Error loading logs explorer: {e}")
flash(f"Error loading logs: {str(e)}", 'error')
return render_template('modules/settings/logs_explorer.html',
log_files=[],
log_stats={})
@settings_bp.route('/logs/view/<filename>', methods=['GET'])
def view_log(filename):
"""View content of a specific log file"""
if 'user_id' not in session:
return redirect(url_for('main.login'))
try:
lines = request.args.get('lines', default=100, type=int)
log_data = get_log_content(filename, lines=lines)
if not log_data.get('success'):
flash(log_data.get('error', 'Error reading log file'), 'error')
return redirect(url_for('settings.logs_explorer'))
return render_template('modules/settings/view_log.html', log_data=log_data)
except Exception as e:
logger.error(f"Error viewing log {filename}: {e}")
flash(f"Error viewing log: {str(e)}", 'error')
return redirect(url_for('settings.logs_explorer'))
@settings_bp.route('/logs/download/<filename>', methods=['GET'])
def download_log(filename):
"""Download a log file"""
if 'user_id' not in session:
return redirect(url_for('main.login'))
try:
filepath = get_log_file_path(filename)
if not filepath:
flash('Invalid file or file not found', 'error')
return redirect(url_for('settings.logs_explorer'))
return send_file(
filepath,
as_attachment=True,
download_name=f"{filename}_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
)
except Exception as e:
logger.error(f"Error downloading log {filename}: {e}")
flash(f"Error downloading log: {str(e)}", 'error')
return redirect(url_for('settings.logs_explorer'))
@settings_bp.route('/logs/search', methods=['GET'])
def search_logs():
"""Search for terms in log files"""
if 'user_id' not in session:
return redirect(url_for('main.login'))
try:
search_term = request.args.get('q', '').strip()
filename = request.args.get('file', default=None)
results = []
if search_term:
results = search_in_logs(search_term, filename=filename)
log_files = get_log_files()
return render_template('modules/settings/search_logs.html',
search_term=search_term,
results=results,
log_files=log_files,
selected_file=filename)
except Exception as e:
logger.error(f"Error searching logs: {e}")
flash(f"Error searching logs: {str(e)}", 'error')
return redirect(url_for('settings.logs_explorer'))

View File

@@ -0,0 +1,247 @@
"""
Settings Module - App Statistics Helper
Provides functions to collect various app statistics for the overview
"""
import os
import pymysql
from datetime import datetime
from pathlib import Path
from app.database import get_db
import logging
logger = logging.getLogger(__name__)
def get_user_count():
"""Get total number of existing users"""
try:
conn = get_db()
cursor = conn.cursor()
cursor.execute("SELECT COUNT(*) as count FROM users WHERE is_active = 1")
result = cursor.fetchone()
cursor.close()
return result[0] if result else 0
except Exception as e:
logger.error(f"Error getting user count: {e}")
return 0
def get_database_size():
"""Get size of the database in MB"""
try:
conn = get_db()
cursor = conn.cursor()
try:
# Get database name from connection
cursor.execute("SELECT DATABASE()")
result = cursor.fetchone()
if not result:
cursor.close()
return 0
db_name = result[0]
# Get database size
query = f"""
SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 2)
FROM information_schema.tables
WHERE table_schema = %s
"""
cursor.execute(query, (db_name,))
result = cursor.fetchone()
cursor.close()
return result[0] if result and result[0] else 0
except Exception as e:
cursor.close()
logger.error(f"Error executing database size query: {e}")
return 0
except Exception as e:
logger.error(f"Error getting database size: {e}")
return 0
def get_logs_size():
"""Get total size of log files in MB"""
try:
log_dir = './data/logs'
if not os.path.exists(log_dir):
return 0
total_size = 0
for filename in os.listdir(log_dir):
filepath = os.path.join(log_dir, filename)
if os.path.isfile(filepath):
total_size += os.path.getsize(filepath)
return round(total_size / 1024 / 1024, 2)
except Exception as e:
logger.error(f"Error getting logs size: {e}")
return 0
def get_database_count():
"""Get number of existing databases (user accessible)"""
try:
conn = get_db()
cursor = conn.cursor()
try:
cursor.execute("SHOW DATABASES")
result = cursor.fetchall()
cursor.close()
# Filter out system databases
if result:
excluded_dbs = {'information_schema', 'mysql', 'performance_schema', 'sys'}
user_dbs = [db for db in result if db[0] not in excluded_dbs]
return len(user_dbs)
return 0
except Exception as e:
cursor.close()
logger.error(f"Error executing show databases query: {e}")
return 0
except Exception as e:
logger.error(f"Error getting database count: {e}")
return 0
def get_backup_count():
"""Get number of scheduled backups for the database"""
try:
conn = get_db()
cursor = conn.cursor()
# Check if backups table exists
cursor.execute("""
SELECT COUNT(*) FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_name = 'backup_schedules'
""")
if cursor.fetchone()[0] > 0:
cursor.execute("SELECT COUNT(*) FROM backup_schedules WHERE is_active = 1")
result = cursor.fetchone()
cursor.close()
return result[0] if result else 0
else:
cursor.close()
# Count backup files if no table exists
backup_dir = './data/backups'
if os.path.exists(backup_dir):
return len([f for f in os.listdir(backup_dir) if f.endswith('.sql')])
return 0
except Exception as e:
logger.error(f"Error getting backup count: {e}")
# Fallback to counting backup files
try:
backup_dir = './data/backups'
if os.path.exists(backup_dir):
return len([f for f in os.listdir(backup_dir) if f.endswith('.sql')])
except:
pass
return 0
def get_printer_keys_count():
"""Get number of keys for printers (pairing keys)"""
try:
conn = get_db()
cursor = conn.cursor()
try:
# Check if qz_pairing_keys table exists
cursor.execute("""
SELECT COUNT(*) FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_name = 'qz_pairing_keys'
""")
if cursor.fetchone()[0] > 0:
cursor.execute("SELECT COUNT(*) FROM qz_pairing_keys")
result = cursor.fetchone()
cursor.close()
return result[0] if result else 0
else:
cursor.close()
return 0
except Exception as e:
cursor.close()
logger.error(f"Error executing printer keys query: {e}")
return 0
except Exception as e:
logger.error(f"Error getting printer keys count: {e}")
return 0
def check_app_key_availability():
"""Check app key availability"""
try:
conn = get_db()
cursor = conn.cursor()
try:
# Check if api_keys table exists
cursor.execute("""
SELECT COUNT(*) FROM information_schema.tables
WHERE table_schema = DATABASE() AND table_name = 'api_keys'
""")
if cursor.fetchone()[0] > 0:
cursor.execute("SELECT COUNT(*) FROM api_keys WHERE is_active = 1")
result = cursor.fetchone()
cursor.close()
count = result[0] if result else 0
return {
'available': count > 0,
'count': count,
'status': f'{count} active key(s)' if count > 0 else 'No active keys'
}
else:
cursor.close()
return {
'available': False,
'count': 0,
'status': 'API Keys table not found'
}
except Exception as e:
cursor.close()
logger.error(f"Error executing api_keys query: {e}")
return {
'available': False,
'count': 0,
'status': f'Error: {str(e)}'
}
except Exception as e:
logger.error(f"Error checking app key availability: {e}")
return {
'available': False,
'count': 0,
'status': f'Error: {str(e)}'
}
def get_all_stats():
"""Get all statistics for the overview"""
try:
return {
'user_count': get_user_count(),
'database_size_mb': get_database_size(),
'logs_size_mb': get_logs_size(),
'database_count': get_database_count(),
'backup_count': get_backup_count(),
'printer_keys_count': get_printer_keys_count(),
'app_key_availability': check_app_key_availability()
}
except Exception as e:
logger.error(f"Error getting all stats: {e}")
# Return defaults on error
return {
'user_count': 0,
'database_size_mb': 0,
'logs_size_mb': 0,
'database_count': 0,
'backup_count': 0,
'printer_keys_count': 0,
'app_key_availability': {
'available': False,
'count': 0,
'status': 'Error loading data'
}
}

View File

@@ -0,0 +1,3 @@
"""
Warehouse Module - Initialization
"""

View File

@@ -0,0 +1,254 @@
"""
Boxes Management Module
Handles CRUD operations for warehouse boxes
Uses boxes_crates table matching the old app structure
"""
from app.database import get_db
import logging
logger = logging.getLogger(__name__)
def ensure_boxes_table():
"""Create boxes_crates table if it doesn't exist"""
try:
conn = get_db()
cursor = conn.cursor()
cursor.execute("SHOW TABLES LIKE 'boxes_crates'")
result = cursor.fetchone()
if not result:
cursor.execute('''
CREATE TABLE IF NOT EXISTS boxes_crates (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
box_number VARCHAR(8) NOT NULL UNIQUE,
status ENUM('open', 'closed') DEFAULT 'open',
location_id BIGINT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
created_by VARCHAR(100),
FOREIGN KEY (location_id) REFERENCES warehouse_locations(id) ON DELETE SET NULL,
INDEX idx_box_number (box_number),
INDEX idx_status (status)
)
''')
conn.commit()
cursor.close()
logger.info("boxes_crates table ensured")
return True
except Exception as e:
logger.error(f"Error ensuring boxes_crates table: {e}")
return False
def generate_box_number():
"""Generate next box number with 8 digits (00000001, 00000002, etc.)"""
try:
conn = get_db()
cursor = conn.cursor()
cursor.execute("SELECT MAX(CAST(box_number AS UNSIGNED)) FROM boxes_crates")
result = cursor.fetchone()
cursor.close()
if result and result[0]:
next_number = int(result[0]) + 1
else:
next_number = 1
return str(next_number).zfill(8)
except Exception as e:
logger.error(f"Error generating box number: {e}")
return "00000001"
def add_box(created_by=None):
"""Add a new box/crate with auto-generated number"""
try:
ensure_boxes_table()
box_number = generate_box_number()
conn = get_db()
cursor = conn.cursor()
cursor.execute(
"INSERT INTO boxes_crates (box_number, status, created_by) VALUES (%s, %s, %s)",
(box_number, 'open', created_by)
)
conn.commit()
cursor.close()
logger.info(f"Box {box_number} created successfully")
return True, f"Box {box_number} created successfully"
except Exception as e:
logger.error(f"Error adding box: {e}")
return False, f"Error creating box: {str(e)}"
def get_all_boxes():
"""Get all boxes with their location information"""
try:
ensure_boxes_table()
conn = get_db()
cursor = conn.cursor()
cursor.execute('''
SELECT
b.id,
b.box_number,
b.status,
COALESCE(l.location_code, 'Not assigned') as location_code,
b.created_at,
b.updated_at,
b.created_by,
b.location_id
FROM boxes_crates b
LEFT JOIN warehouse_locations l ON b.location_id = l.id
ORDER BY b.created_at DESC
''')
boxes = cursor.fetchall()
cursor.close()
return boxes if boxes else []
except Exception as e:
logger.error(f"Error getting all boxes: {e}")
return []
def get_box_by_id(box_id):
"""Get a single box by ID"""
try:
conn = get_db()
cursor = conn.cursor()
cursor.execute('''
SELECT
b.id,
b.box_number,
b.status,
b.location_id,
b.created_by,
b.created_at,
b.updated_at
FROM boxes_crates b
WHERE b.id = %s
''', (box_id,))
box = cursor.fetchone()
cursor.close()
return box
except Exception as e:
logger.error(f"Error getting box by ID: {e}")
return None
def update_box(box_id, status=None, location_id=None):
"""Update box status or location"""
try:
conn = get_db()
cursor = conn.cursor()
if status and location_id is not None:
cursor.execute(
"UPDATE boxes_crates SET status = %s, location_id = %s WHERE id = %s",
(status, location_id if location_id else None, box_id)
)
elif status:
cursor.execute(
"UPDATE boxes_crates SET status = %s WHERE id = %s",
(status, box_id)
)
elif location_id is not None:
cursor.execute(
"UPDATE boxes_crates SET location_id = %s WHERE id = %s",
(location_id if location_id else None, box_id)
)
conn.commit()
cursor.close()
logger.info(f"Box {box_id} updated successfully")
return True, "Box updated successfully"
except Exception as e:
logger.error(f"Error updating box: {e}")
return False, f"Error updating box: {str(e)}"
def delete_box(box_id):
"""Delete a single box"""
try:
conn = get_db()
cursor = conn.cursor()
cursor.execute("DELETE FROM boxes_crates WHERE id = %s", (box_id,))
conn.commit()
cursor.close()
logger.info(f"Box {box_id} deleted successfully")
return True, "Box deleted successfully"
except Exception as e:
logger.error(f"Error deleting box: {e}")
return False, f"Error deleting box: {str(e)}"
def delete_multiple_boxes(box_ids_str):
"""Delete multiple boxes"""
try:
if not box_ids_str:
return False, "No boxes selected"
# Parse box IDs
box_ids = [int(x) for x in box_ids_str.split(',') if x.strip()]
if not box_ids:
return False, "No valid box IDs provided"
conn = get_db()
cursor = conn.cursor()
placeholders = ','.join(['%s'] * len(box_ids))
cursor.execute(f"DELETE FROM boxes_crates WHERE id IN ({placeholders})", box_ids)
conn.commit()
cursor.close()
logger.info(f"Deleted {len(box_ids)} boxes")
return True, f"Deleted {len(box_ids)} box(es) successfully"
except Exception as e:
logger.error(f"Error deleting multiple boxes: {e}")
return False, f"Error deleting boxes: {str(e)}"
def get_box_stats():
"""Get box statistics"""
try:
ensure_boxes_table()
conn = get_db()
cursor = conn.cursor()
cursor.execute("SELECT COUNT(*) FROM boxes_crates")
total = cursor.fetchone()[0] if cursor.fetchone() else 0
cursor.execute("SELECT COUNT(*) FROM boxes_crates WHERE status = 'open'")
result = cursor.fetchone()
open_count = result[0] if result else 0
cursor.execute("SELECT COUNT(*) FROM boxes_crates WHERE status = 'closed'")
result = cursor.fetchone()
closed_count = result[0] if result else 0
cursor.close()
return {
'total': total,
'open': open_count,
'closed': closed_count
}
except Exception as e:
logger.error(f"Error getting box statistics: {e}")
return {'total': 0, 'open': 0, 'closed': 0}

View File

@@ -0,0 +1,101 @@
"""
Boxes Management Routes
"""
from flask import Blueprint, render_template, session, redirect, url_for, request, flash
from app.modules.warehouse.boxes import (
get_all_boxes, add_box, update_box, delete_box, delete_multiple_boxes,
get_box_by_id, get_box_stats, ensure_boxes_table
)
from app.modules.warehouse.warehouse import get_all_locations
import logging
logger = logging.getLogger(__name__)
boxes_bp = Blueprint('boxes', __name__, url_prefix='/warehouse/boxes')
@boxes_bp.route('/', methods=['GET', 'POST'])
def manage_boxes():
"""Manage warehouse boxes page"""
if 'user_id' not in session:
return redirect(url_for('main.login'))
# Ensure table exists
ensure_boxes_table()
message = None
message_type = 'info'
if request.method == 'POST':
action = request.form.get('action', '')
user_id = session.get('user_id', 'System')
# Add new box (auto-numbered)
if action == 'add_box':
success, msg = add_box(created_by=user_id)
message = msg
message_type = 'success' if success else 'danger'
# Update box status or location
elif action == 'edit_box':
box_id = request.form.get('box_id', '')
status = request.form.get('status', '')
location_id = request.form.get('location_id', '')
location_id = int(location_id) if location_id and location_id.isdigit() else None
success, msg = update_box(box_id, status=status if status else None, location_id=location_id)
message = msg
message_type = 'success' if success else 'danger'
# Change status
elif action == 'toggle_status':
box_id = request.form.get('box_id', '')
current_status = request.form.get('current_status', 'open')
new_status = 'closed' if current_status == 'open' else 'open'
success, msg = update_box(box_id, status=new_status)
message = msg
message_type = 'success' if success else 'danger'
# Delete single box
elif action == 'delete_box':
box_id = request.form.get('box_id', '')
success, msg = delete_box(box_id)
message = msg
message_type = 'success' if success else 'danger'
# Delete multiple boxes
elif action == 'delete_multiple':
box_ids = request.form.get('delete_ids', '')
success, msg = delete_multiple_boxes(box_ids)
message = msg
message_type = 'success' if success else 'danger'
# Get data and convert tuples to dictionaries
boxes_data = get_all_boxes()
boxes = []
for box_tuple in boxes_data:
boxes.append({
'id': box_tuple[0],
'box_number': box_tuple[1],
'status': box_tuple[2],
'location_code': box_tuple[3],
'created_at': box_tuple[4],
'updated_at': box_tuple[5],
'created_by': box_tuple[6],
'location_id': box_tuple[7]
})
locations = get_all_locations()
stats = get_box_stats()
return render_template(
'modules/warehouse/boxes.html',
boxes=boxes,
locations=locations,
stats=stats,
message=message,
message_type=message_type
)

View File

@@ -0,0 +1,125 @@
"""
Warehouse Module Routes
"""
from flask import Blueprint, render_template, session, redirect, url_for, request, flash, jsonify
from app.modules.warehouse.warehouse import (
get_all_locations, add_location, update_location, delete_location,
delete_multiple_locations, get_location_by_id
)
import logging
logger = logging.getLogger(__name__)
warehouse_bp = Blueprint('warehouse', __name__, url_prefix='/warehouse')
@warehouse_bp.route('/', methods=['GET'])
def warehouse_index():
"""Warehouse module main page - launcher for all warehouse operations"""
if 'user_id' not in session:
return redirect(url_for('main.login'))
return render_template('modules/warehouse/index.html')
@warehouse_bp.route('/set-boxes-locations', methods=['GET', 'POST'])
def set_boxes_locations():
"""Set boxes locations - add or update articles in warehouse inventory"""
if 'user_id' not in session:
return redirect(url_for('main.login'))
return render_template('modules/warehouse/set_boxes_locations.html')
@warehouse_bp.route('/locations', methods=['GET', 'POST'])
def locations():
"""Create and manage warehouse locations"""
if 'user_id' not in session:
return redirect(url_for('main.login'))
message = None
message_type = 'info'
if request.method == 'POST':
# Handle edit location
if request.form.get('edit_location'):
location_id = request.form.get('location_id', '')
size = request.form.get('edit_size', '').strip()
description = request.form.get('edit_description', '').strip()
try:
location_id = int(location_id)
success, msg = update_location(location_id, None, size if size else None, description if description else None)
message = msg
message_type = 'success' if success else 'error'
except Exception as e:
message = f"Error: {str(e)}"
message_type = 'error'
# Handle delete locations
elif request.form.get('delete_locations'):
delete_ids_str = request.form.get('delete_ids', '')
try:
location_ids = [int(id.strip()) for id in delete_ids_str.split(',') if id.strip().isdigit()]
success, msg = delete_multiple_locations(location_ids)
message = msg
message_type = 'success' if success else 'error'
except Exception as e:
message = f"Error: {str(e)}"
message_type = 'error'
# Handle add location
elif request.form.get('add_location'):
location_code = request.form.get('location_code', '').strip()
size = request.form.get('size', '').strip()
description = request.form.get('description', '').strip()
if not location_code:
message = "Location code is required"
message_type = 'error'
else:
success, msg = add_location(location_code, size if size else None, description if description else None)
message = msg
message_type = 'success' if success else 'error'
# Get all locations
locations_list = get_all_locations()
return render_template('modules/warehouse/locations.html',
locations=locations_list,
message=message,
message_type=message_type)
@warehouse_bp.route('/boxes', methods=['GET', 'POST'])
def boxes():
"""Manage boxes and crates in the warehouse"""
if 'user_id' not in session:
return redirect(url_for('main.login'))
return render_template('modules/warehouse/boxes.html')
@warehouse_bp.route('/inventory', methods=['GET'])
def inventory():
"""View warehouse inventory - products, boxes, and locations"""
if 'user_id' not in session:
return redirect(url_for('main.login'))
return render_template('modules/warehouse/inventory.html')
@warehouse_bp.route('/reports', methods=['GET'])
def reports():
"""Warehouse activity and inventory reports"""
if 'user_id' not in session:
return redirect(url_for('main.login'))
return render_template('modules/warehouse/reports.html')
@warehouse_bp.route('/test-barcode', methods=['GET'])
def test_barcode():
"""Test barcode printing functionality"""
if 'user_id' not in session:
return redirect(url_for('main.login'))
return render_template('modules/warehouse/test_barcode.html')

View File

@@ -0,0 +1,213 @@
"""
Warehouse Module - Helper Functions
Provides functions for warehouse operations
"""
import logging
from app.database import get_db
logger = logging.getLogger(__name__)
def ensure_warehouse_locations_table():
"""Ensure warehouse_locations table exists"""
try:
conn = get_db()
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS warehouse_locations (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
location_code VARCHAR(12) UNIQUE NOT NULL,
size INT,
description VARCHAR(250),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
INDEX idx_location_code (location_code)
)
""")
conn.commit()
cursor.close()
logger.info("warehouse_locations table ensured")
return True
except Exception as e:
logger.error(f"Error ensuring warehouse_locations table: {e}")
return False
def add_location(location_code, size, description):
"""Add a new warehouse location"""
try:
ensure_warehouse_locations_table()
conn = get_db()
cursor = conn.cursor()
cursor.execute("""
INSERT INTO warehouse_locations (location_code, size, description)
VALUES (%s, %s, %s)
""", (location_code, size if size else None, description))
conn.commit()
cursor.close()
return True, "Location added successfully."
except Exception as e:
if "Duplicate entry" in str(e):
return False, f"Failed: Location code '{location_code}' already exists."
logger.error(f"Error adding location: {e}")
return False, f"Error adding location: {str(e)}"
def get_all_locations():
"""Get all warehouse locations"""
try:
ensure_warehouse_locations_table()
conn = get_db()
cursor = conn.cursor()
cursor.execute("""
SELECT id, location_code, size, description, created_at, updated_at
FROM warehouse_locations
ORDER BY id DESC
""")
locations = cursor.fetchall()
cursor.close()
result = []
for loc in locations:
result.append({
'id': loc[0],
'location_code': loc[1],
'size': loc[2],
'description': loc[3],
'created_at': loc[4],
'updated_at': loc[5]
})
return result
except Exception as e:
logger.error(f"Error getting locations: {e}")
return []
def get_location_by_id(location_id):
"""Get a specific location by ID"""
try:
conn = get_db()
cursor = conn.cursor()
cursor.execute("""
SELECT id, location_code, size, description, created_at, updated_at
FROM warehouse_locations
WHERE id = %s
""", (location_id,))
loc = cursor.fetchone()
cursor.close()
if loc:
return {
'id': loc[0],
'location_code': loc[1],
'size': loc[2],
'description': loc[3],
'created_at': loc[4],
'updated_at': loc[5]
}
return None
except Exception as e:
logger.error(f"Error getting location: {e}")
return None
def update_location(location_id, location_code=None, size=None, description=None):
"""Update a warehouse location
Args:
location_id: ID of location to update
location_code: New location code (optional - cannot be changed in form)
size: New size (optional)
description: New description (optional)
"""
try:
conn = get_db()
cursor = conn.cursor()
# Build update query dynamically
updates = []
params = []
if location_code:
updates.append("location_code = %s")
params.append(location_code)
if size is not None:
updates.append("size = %s")
params.append(size)
if description is not None:
updates.append("description = %s")
params.append(description)
if not updates:
return False, "No fields to update"
params.append(location_id)
query = f"UPDATE warehouse_locations SET {', '.join(updates)} WHERE id = %s"
cursor.execute(query, params)
conn.commit()
cursor.close()
return True, "Location updated successfully."
except Exception as e:
if "Duplicate entry" in str(e):
return False, f"Failed: Location code already exists."
logger.error(f"Error updating location: {e}")
return False, f"Error updating location: {str(e)}"
def delete_location(location_id):
"""Delete a warehouse location"""
try:
conn = get_db()
cursor = conn.cursor()
cursor.execute("DELETE FROM warehouse_locations WHERE id = %s", (location_id,))
conn.commit()
cursor.close()
return True, "Location deleted successfully."
except Exception as e:
logger.error(f"Error deleting location: {e}")
return False, f"Error deleting location: {str(e)}"
def delete_multiple_locations(location_ids):
"""Delete multiple warehouse locations"""
try:
if not location_ids:
return False, "No locations to delete."
conn = get_db()
cursor = conn.cursor()
deleted_count = 0
for loc_id in location_ids:
try:
cursor.execute("DELETE FROM warehouse_locations WHERE id = %s", (int(loc_id),))
if cursor.rowcount > 0:
deleted_count += 1
except:
pass
conn.commit()
cursor.close()
return True, f"Deleted {deleted_count} location(s)."
except Exception as e:
logger.error(f"Error deleting multiple locations: {e}")
return False, f"Error deleting locations: {str(e)}"

View File

@@ -77,6 +77,13 @@ def dashboard():
'color': 'primary',
'url': url_for('quality.quality_index')
},
{
'name': 'Warehouse Module',
'description': 'Manage warehouse operations and inventory',
'icon': 'fa-warehouse',
'color': 'info',
'url': url_for('warehouse.warehouse_index')
},
{
'name': 'Settings',
'description': 'Configure application settings',

295
app/static/js/qz-printer.js Normal file
View File

@@ -0,0 +1,295 @@
/**
* QZ Tray Printer Module
* Shared printer functionality for all pages
* Provides printer detection, selection, and printing capabilities
*/
(function() {
'use strict';
// Global printer state
window.qzPrinter = {
connected: false,
availablePrinters: [],
selectedPrinter: '',
/**
* Initialize QZ Tray connection
* @returns {Promise<boolean>} True if connected, false otherwise
*/
initialize: async function() {
try {
console.log('Initializing QZ Tray...');
if (typeof qz === 'undefined') {
console.warn('QZ Tray library not loaded');
return false;
}
// Try to connect
await qz.websocket.connect();
this.connected = true;
console.log('✅ QZ Tray connected');
// Load available printers
await this.loadPrinters();
return true;
} catch (error) {
console.warn('QZ Tray not available:', error.message);
this.connected = false;
return false;
}
},
/**
* Load available printers from QZ Tray
* @returns {Promise<Array>} Array of printer names
*/
loadPrinters: async function() {
try {
if (!this.connected) return [];
const printers = await qz.printers.find();
this.availablePrinters = printers;
console.log('Loaded printers:', printers);
// Auto-select first thermal printer if available
const thermalPrinter = printers.find(p =>
p.toLowerCase().includes('thermal') ||
p.toLowerCase().includes('label') ||
p.toLowerCase().includes('zebra')
);
if (thermalPrinter) {
this.selectedPrinter = thermalPrinter;
console.log('Auto-selected thermal printer:', thermalPrinter);
}
return printers;
} catch (error) {
console.error('Error loading printers:', error);
return [];
}
},
/**
* Get printer selection dropdown HTML
* @param {string} selectId - ID for the select element
* @returns {string} HTML string for printer select dropdown
*/
getPrinterSelectHTML: function(selectId = 'printer-select') {
let html = `<select id="${selectId}" class="form-select form-select-sm">
<option value="">Default Printer</option>`;
this.availablePrinters.forEach(printer => {
const selected = printer === this.selectedPrinter ? ' selected' : '';
html += `<option value="${printer}"${selected}>${printer}</option>`;
});
html += '</select>';
return html;
},
/**
* Update printer selection
* @param {string} printerName - Name of printer to select
*/
selectPrinter: function(printerName) {
this.selectedPrinter = printerName || '';
console.log('Selected printer:', this.selectedPrinter);
},
/**
* Test QZ Tray connection
* @returns {boolean} True if connected
*/
test: function() {
if (!this.connected) {
alert('QZ Tray is not connected.\nBrowser print will be used instead.');
return false;
}
const printerList = this.availablePrinters.length > 0
? this.availablePrinters.join('\n• ')
: 'No printers found';
alert('✅ QZ Tray is connected and ready!\n\nAvailable printers:\n• ' + printerList);
return true;
},
/**
* Print barcode using QZ Tray
* @param {string} barcodeData - Barcode value
* @param {string} printerName - Printer to use (optional, uses selected)
* @param {Object} options - Print options
* @returns {Promise<void>}
*/
printBarcode: async function(barcodeData, printerName, options = {}) {
try {
if (!this.connected) {
throw new Error('QZ Tray not connected');
}
const targetPrinter = printerName || this.selectedPrinter;
console.log('Printing to:', targetPrinter, 'Data:', barcodeData);
// Default print options
const printConfig = {
printer: targetPrinter,
colorType: options.colorType || 'color',
copies: options.copies || 1
};
// Barcode data with default configuration
const printData = [{
type: 'barcode',
format: options.format || 'CODE128',
data: barcodeData,
width: options.width || 2,
height: options.height || 100,
displayValue: options.displayValue !== false
}];
// Add optional label
if (options.label) {
printData.push({
type: 'text',
data: options.label,
position: options.labelPosition || {x: 0.5, y: 2.2},
font: options.font || {family: 'Arial', size: 12, weight: 'bold'}
});
}
// Send to printer
await qz.print(printConfig, printData);
console.log('✅ Print job sent to', targetPrinter);
return true;
} catch (error) {
console.error('QZ Tray printing error:', error);
throw error;
}
},
/**
* Print SVG/HTML barcode using QZ Tray
* @param {string} svgElement - SVG element or selector
* @param {string} barcodeText - Text to display with barcode
* @param {string} printerName - Printer to use (optional)
* @returns {Promise<void>}
*/
printSVGBarcode: async function(svgElement, barcodeText, printerName) {
try {
if (!this.connected) {
throw new Error('QZ Tray not connected');
}
// If no printer specified, use the selected printer or default
let targetPrinter = printerName || this.selectedPrinter;
// Get SVG element
let svgEl = typeof svgElement === 'string'
? document.querySelector(svgElement)
: svgElement;
if (!svgEl) {
throw new Error('Barcode SVG element not found');
}
// Serialize SVG to string and encode as base64
const svgString = new XMLSerializer().serializeToString(svgEl);
const svgBase64 = btoa(svgString);
// If still no printer, get the device default
if (!targetPrinter) {
try {
const defaultPrinter = await qz.printers.getDefault();
targetPrinter = defaultPrinter;
console.log('Using device default printer:', targetPrinter);
} catch (err) {
console.warn('Could not get default printer, using system default');
targetPrinter = ''; // Empty string uses system default
}
}
const printConfig = {
printer: targetPrinter,
colorType: 'color',
copies: 1
};
const printData = [{
type: 'image',
format: 'base64',
data: svgBase64,
width: 3,
height: 1.5,
position: {x: 0.5, y: 0.5}
}];
if (barcodeText) {
printData.push({
type: 'text',
data: barcodeText,
position: {x: 0.5, y: 2.2},
font: {family: 'Arial', size: 12, weight: 'bold'}
});
}
console.log('Printing to thermal printer:', targetPrinter);
await qz.print(printConfig, printData);
console.log('✅ SVG print job sent to', targetPrinter);
return true;
} catch (error) {
console.error('QZ Tray SVG printing error:', error);
throw error;
}
},
/**
* Fallback to browser print
* @param {string} title - Print document title
* @param {string} content - HTML content to print
*/
printBrowser: function(title, content) {
const printWindow = window.open('', '', 'height=400,width=600');
printWindow.document.write('<html><head><title>' + title + '</title>');
printWindow.document.write('<style>');
printWindow.document.write('body { font-family: Arial, sans-serif; text-align: center; padding: 20px; }');
printWindow.document.write('h2 { margin-bottom: 30px; }');
printWindow.document.write('svg { max-width: 100%; height: auto; margin: 20px 0; }');
printWindow.document.write('.content { margin: 20px 0; }');
printWindow.document.write('</style></head><body>');
printWindow.document.write('<h2>' + title + '</h2>');
printWindow.document.write('<div class="content">' + content + '</div>');
printWindow.document.write('<p style="margin-top: 40px; font-size: 14px; color: #666;">');
printWindow.document.write('Printed on: ' + new Date().toLocaleString());
printWindow.document.write('</p></body></html>');
printWindow.document.close();
setTimeout(() => {
printWindow.print();
}, 250);
}
};
// Auto-initialize when document is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
if (typeof qz !== 'undefined') {
window.qzPrinter.initialize();
}
});
} else {
// Document already loaded
if (typeof qz !== 'undefined') {
window.qzPrinter.initialize();
}
}
})();

2871
app/static/js/qz-tray.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -39,6 +39,11 @@
<i class="fas fa-check-circle"></i> Quality
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('warehouse.warehouse_index') }}">
<i class="fas fa-warehouse"></i> Warehouse
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url_for('settings.settings_index') }}">
<i class="fas fa-cog"></i> Settings

View File

@@ -17,54 +17,6 @@
</div>
</div>
<!-- Quick Stats Section -->
<div class="row mb-5">
<div class="col-md-3">
<div class="stat-card">
<div class="stat-icon bg-primary">
<i class="fas fa-chart-line"></i>
</div>
<div class="stat-content">
<h3>0</h3>
<p>Total Inspections</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<div class="stat-icon bg-success">
<i class="fas fa-check-circle"></i>
</div>
<div class="stat-content">
<h3>0</h3>
<p>Passed</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<div class="stat-icon bg-warning">
<i class="fas fa-exclamation-circle"></i>
</div>
<div class="stat-content">
<h3>0</h3>
<p>Warnings</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="stat-card">
<div class="stat-icon bg-danger">
<i class="fas fa-times-circle"></i>
</div>
<div class="stat-content">
<h3>0</h3>
<p>Failed</p>
</div>
</div>
</div>
</div>
<!-- Modules Section -->
<div class="row">
<div class="col-12 mb-4">

View File

@@ -20,12 +20,6 @@
FG Scan
</a>
</div>
<div class="col-md-3">
<a href="{{ url_for('quality.inspections') }}" class="btn btn-primary btn-lg w-100">
<i class="fas fa-clipboard-list"></i><br>
Inspections
</a>
</div>
<div class="col-md-3">
<a href="{{ url_for('quality.quality_reports') }}" class="btn btn-info btn-lg w-100">
<i class="fas fa-chart-bar"></i><br>

View File

@@ -31,6 +31,9 @@
<a href="{{ url_for('settings.database_settings') }}" class="list-group-item list-group-item-action">
<i class="fas fa-database"></i> Database Info
</a>
<a href="{{ url_for('settings.logs_explorer') }}" class="list-group-item list-group-item-action">
<i class="fas fa-file-alt"></i> Logs Explorer
</a>
</div>
</div>
<div class="col-md-9">

View File

@@ -30,6 +30,9 @@
<a href="{{ url_for('settings.database_settings') }}" class="list-group-item list-group-item-action active">
<i class="fas fa-database"></i> Database Info
</a>
<a href="{{ url_for('settings.logs_explorer') }}" class="list-group-item list-group-item-action">
<i class="fas fa-file-alt"></i> Logs Explorer
</a>
</div>
</div>
<div class="col-md-9">

View File

@@ -31,6 +31,9 @@
<a href="{{ url_for('settings.database_settings') }}" class="list-group-item list-group-item-action">
<i class="fas fa-database"></i> Database Info
</a>
<a href="{{ url_for('settings.logs_explorer') }}" class="list-group-item list-group-item-action">
<i class="fas fa-file-alt"></i> Logs Explorer
</a>
</div>
</div>
<div class="col-md-9">

View File

@@ -30,6 +30,9 @@
<a href="{{ url_for('settings.database_settings') }}" class="list-group-item list-group-item-action">
<i class="fas fa-database"></i> Database Info
</a>
<a href="{{ url_for('settings.logs_explorer') }}" class="list-group-item list-group-item-action">
<i class="fas fa-file-alt"></i> Logs Explorer
</a>
</div>
</div>
<div class="col-md-9">

View File

@@ -13,6 +13,96 @@
</div>
</div>
<!-- App Overview Section -->
<div class="row mb-4">
<div class="col-12">
<h4 class="mb-3"><i class="fas fa-chart-pie"></i> Application Overview</h4>
<div class="row">
<!-- Users Card -->
<div class="col-md-3 col-sm-6 mb-3">
<div class="card shadow-sm h-100">
<div class="card-body text-center">
<div class="display-6 text-primary mb-2">{{ stats.user_count }}</div>
<p class="card-text mb-0">Active Users</p>
</div>
</div>
</div>
<!-- Database Size Card -->
<div class="col-md-3 col-sm-6 mb-3">
<div class="card shadow-sm h-100">
<div class="card-body text-center">
<div class="display-6 text-info mb-2">{{ stats.database_size_mb }} MB</div>
<p class="card-text mb-0">Database Size</p>
</div>
</div>
</div>
<!-- Logs Size Card -->
<div class="col-md-3 col-sm-6 mb-3">
<div class="card shadow-sm h-100">
<div class="card-body text-center">
<div class="display-6 text-warning mb-2">{{ stats.logs_size_mb }} MB</div>
<p class="card-text mb-0">Logs Size</p>
</div>
</div>
</div>
<!-- Database Count Card -->
<div class="col-md-3 col-sm-6 mb-3">
<div class="card shadow-sm h-100">
<div class="card-body text-center">
<div class="display-6 text-secondary mb-2">{{ stats.database_count }}</div>
<p class="card-text mb-0">Databases</p>
</div>
</div>
</div>
</div>
<div class="row">
<!-- Backups Card -->
<div class="col-md-3 col-sm-6 mb-3">
<div class="card shadow-sm h-100">
<div class="card-body text-center">
<div class="display-6 text-success mb-2">{{ stats.backup_count }}</div>
<p class="card-text mb-0">Scheduled Backups</p>
</div>
</div>
</div>
<!-- Printer Keys Card -->
<div class="col-md-3 col-sm-6 mb-3">
<div class="card shadow-sm h-100">
<div class="card-body text-center">
<div class="display-6 text-danger mb-2">{{ stats.printer_keys_count }}</div>
<p class="card-text mb-0">Printer Keys</p>
</div>
</div>
</div>
<!-- App Keys Availability Card -->
<div class="col-md-6 col-sm-6 mb-3">
<div class="card shadow-sm h-100">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<div>
<p class="card-text mb-0">App Key Availability</p>
<h5 class="mb-0 mt-2">
{% if stats.app_key_availability.available %}
<span class="badge bg-success"><i class="fas fa-check-circle"></i> {{ stats.app_key_availability.status }}</span>
{% else %}
<span class="badge bg-danger"><i class="fas fa-times-circle"></i> {{ stats.app_key_availability.status }}</span>
{% endif %}
</h5>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-md-3">
<div class="list-group">
@@ -31,6 +121,9 @@
<a href="{{ url_for('settings.database_settings') }}" class="list-group-item list-group-item-action">
<i class="fas fa-database"></i> Database Info
</a>
<a href="{{ url_for('settings.logs_explorer') }}" class="list-group-item list-group-item-action">
<i class="fas fa-file-alt"></i> Logs Explorer
</a>
</div>
</div>
<div class="col-md-9">

View File

@@ -0,0 +1,159 @@
{% extends "base.html" %}
{% block title %}Logs Explorer - Settings{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<div class="row mb-4">
<div class="col-12">
<h1 class="mb-2">
<i class="fas fa-file-alt"></i> Logs Explorer
</h1>
<p class="text-muted">View and manage application log files</p>
</div>
</div>
<!-- Log Statistics -->
<div class="row mb-4">
<div class="col-md-3">
<div class="card shadow-sm">
<div class="card-body text-center">
<div class="display-6 text-primary mb-2">{{ log_stats.total_files }}</div>
<p class="card-text mb-0">Log Files</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="card shadow-sm">
<div class="card-body text-center">
<div class="display-6 text-info mb-2">{{ log_stats.total_size_mb }} MB</div>
<p class="card-text mb-0">Total Size</p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-body">
<p class="card-text mb-1"><small><strong>Newest:</strong> {{ log_stats.newest_log or 'N/A' }}</small></p>
<p class="card-text mb-0"><small><strong>Oldest:</strong> {{ log_stats.oldest_log or 'N/A' }}</small></p>
</div>
</div>
</div>
</div>
<!-- Search Section -->
<div class="row mb-4">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="fas fa-search"></i> Search Logs</h5>
</div>
<div class="card-body">
<form method="GET" action="{{ url_for('settings.search_logs') }}" class="form-inline">
<input type="text" name="q" class="form-control mr-2" placeholder="Search term..." style="flex: 1; margin-right: 10px;">
<select name="file" class="form-control mr-2" style="width: auto;">
<option value="">All Files</option>
{% for log_file in log_files %}
<option value="{{ log_file.name }}">{{ log_file.name }}</option>
{% endfor %}
</select>
<button type="submit" class="btn btn-primary">
<i class="fas fa-search"></i> Search
</button>
</form>
</div>
</div>
</div>
</div>
<!-- Log Files Table -->
<div class="row">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="fas fa-list"></i> Log Files</h5>
</div>
<div class="card-body">
{% if log_files %}
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>File Name</th>
<th>Size</th>
<th>Last Modified</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for log_file in log_files %}
<tr>
<td>
<i class="fas fa-file-lines text-primary"></i>
{{ log_file.name }}
</td>
<td>
<span class="badge bg-info">{{ log_file.size_mb }} MB</span>
</td>
<td>
<small class="text-muted">{{ log_file.modified_at }}</small>
</td>
<td>
<a href="{{ url_for('settings.view_log', filename=log_file.name) }}"
class="btn btn-sm btn-outline-primary" title="View">
<i class="fas fa-eye"></i> View
</a>
<a href="{{ url_for('settings.download_log', filename=log_file.name) }}"
class="btn btn-sm btn-outline-success" title="Download">
<i class="fas fa-download"></i> Download
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info mb-0">
<i class="fas fa-info-circle"></i> No log files found
</div>
{% endif %}
</div>
</div>
</div>
</div>
</div>
<style>
.form-inline {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.form-inline input,
.form-inline select,
.form-inline button {
flex: 1;
min-width: 150px;
}
@media (max-width: 768px) {
.form-inline {
flex-direction: column;
}
.form-inline input,
.form-inline select,
.form-inline button {
flex: 1;
min-width: 100%;
}
}
.btn.btn-outline-primary,
.btn.btn-outline-success {
margin-right: 5px;
}
</style>
{% endblock %}

View File

@@ -0,0 +1,142 @@
{% extends "base.html" %}
{% block title %}Search Logs - Settings{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<div>
<h1 class="mb-0">
<i class="fas fa-search"></i> Search Logs
</h1>
<p class="text-muted mb-0">Find entries in log files</p>
</div>
<a href="{{ url_for('settings.logs_explorer') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Back
</a>
</div>
</div>
</div>
<!-- Search Form -->
<div class="row mb-4">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="fas fa-filter"></i> Search Options</h5>
</div>
<div class="card-body">
<form method="GET" action="{{ url_for('settings.search_logs') }}">
<div class="row">
<div class="col-md-8">
<div class="form-group">
<label for="search_term">Search Term:</label>
<input type="text" id="search_term" name="q" class="form-control"
value="{{ search_term }}" placeholder="Enter search term..." required>
</div>
</div>
<div class="col-md-4">
<div class="form-group">
<label for="log_file">Log File:</label>
<select id="log_file" name="file" class="form-control">
<option value="">All Files</option>
{% for log_file in log_files %}
<option value="{{ log_file.name }}" {% if log_file.name == selected_file %}selected{% endif %}>
{{ log_file.name }}
</option>
{% endfor %}
</select>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">
<i class="fas fa-search"></i> Search
</button>
{% if search_term %}
<a href="{{ url_for('settings.search_logs') }}" class="btn btn-secondary">
<i class="fas fa-times"></i> Clear
</a>
{% endif %}
</form>
</div>
</div>
</div>
</div>
<!-- Search Results -->
{% if search_term %}
<div class="row">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h5 class="mb-0">
<i class="fas fa-list"></i> Results
{% if results %}
<span class="badge bg-primary">{{ results|length }} found</span>
{% endif %}
</h5>
</div>
<div class="card-body">
{% if results %}
<div class="list-group">
{% for result in results %}
<div class="list-group-item">
<div class="d-flex w-100 justify-content-between align-items-start">
<div>
<h6 class="mb-1">
<i class="fas fa-file-lines text-primary"></i>
{{ result.file }}
<span class="badge bg-secondary">Line {{ result.line_num }}</span>
</h6>
<p class="mb-0" style="font-family: 'Courier New', monospace; font-size: 12px; word-break: break-all;">
{{ result.line }}
</p>
</div>
<a href="{{ url_for('settings.view_log', filename=result.file) }}#line-{{ result.line_num }}"
class="btn btn-sm btn-outline-primary">
<i class="fas fa-eye"></i> View
</a>
</div>
</div>
{% endfor %}
</div>
{% else %}
<div class="alert alert-info mb-0">
<i class="fas fa-info-circle"></i> No results found for "{{ search_term }}"
</div>
{% endif %}
</div>
</div>
</div>
</div>
{% else %}
<div class="row">
<div class="col-12">
<div class="alert alert-info">
<i class="fas fa-search"></i> Enter a search term to find entries in your logs
</div>
</div>
</div>
{% endif %}
</div>
<style>
.list-group-item {
border-left: 3px solid #007bff;
padding: 15px;
}
.list-group-item:hover {
background-color: #f8f9fa;
}
pre {
font-family: 'Courier New', Courier, monospace;
font-size: 12px;
line-height: 1.4;
color: #333;
}
</style>
{% endblock %}

View File

@@ -30,6 +30,9 @@
<a href="{{ url_for('settings.database_settings') }}" class="list-group-item list-group-item-action">
<i class="fas fa-database"></i> Database Info
</a>
<a href="{{ url_for('settings.logs_explorer') }}" class="list-group-item list-group-item-action">
<i class="fas fa-file-alt"></i> Logs Explorer
</a>
</div>
</div>
<div class="col-md-9">

View File

@@ -0,0 +1,87 @@
{% extends "base.html" %}
{% block title %}View Log - Settings{% endblock %}
{% block content %}
<div class="container-fluid py-4">
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<div>
<h1 class="mb-0">
<i class="fas fa-file-alt"></i> {{ log_data.filename }}
</h1>
<p class="text-muted mb-0"><small>Size: {{ log_data.size_mb }} MB | Modified: {{ log_data.modified_at }}</small></p>
</div>
<div>
<a href="{{ url_for('settings.logs_explorer') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Back
</a>
<a href="{{ url_for('settings.download_log', filename=log_data.filename) }}" class="btn btn-success">
<i class="fas fa-download"></i> Download
</a>
</div>
</div>
</div>
</div>
<!-- File Info -->
<div class="row mb-3">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-body">
<div class="row">
<div class="col-md-3">
<p class="mb-0"><strong>File Size:</strong></p>
<p class="text-muted">{{ log_data.size_mb }} MB</p>
</div>
<div class="col-md-3">
<p class="mb-0"><strong>Total Lines:</strong></p>
<p class="text-muted">{{ log_data.total_lines|default(0) }}</p>
</div>
<div class="col-md-3">
<p class="mb-0"><strong>Displayed Lines:</strong></p>
<p class="text-muted">{{ log_data.displayed_lines|default(0) }}</p>
</div>
<div class="col-md-3">
<p class="mb-0"><strong>Last Modified:</strong></p>
<p class="text-muted">{{ log_data.modified_at }}</p>
</div>
</div>
{% if log_data.truncated %}
<div class="alert alert-info mb-0">
<i class="fas fa-info-circle"></i> Showing last {{ log_data.displayed_lines }} lines of {{ log_data.total_lines }} total lines.
<a href="{{ url_for('settings.view_log', filename=log_data.filename, lines='') }}">View all lines</a>
</div>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Log Content -->
<div class="row">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="fas fa-align-left"></i> Log Content</h5>
</div>
<div class="card-body p-0">
<pre class="mb-0" style="background-color: #f8f9fa; padding: 15px; border-radius: 0 0 4px 4px; max-height: 600px; overflow-y: auto;">{{ log_data.content }}</pre>
</div>
</div>
</div>
</div>
</div>
<style>
pre {
font-family: 'Courier New', Courier, monospace;
font-size: 12px;
line-height: 1.4;
color: #333;
word-wrap: break-word;
white-space: pre-wrap;
}
</style>
{% endblock %}

View File

@@ -0,0 +1,679 @@
{% extends "base.html" %}
{% block content %}
<div class="container-fluid mt-5">
<div class="row mb-4">
<div class="col-12">
<h2 class="mb-3">
<i class="fas fa-cube me-2"></i>Manage Boxes
</h2>
</div>
</div>
{% if message %}
<div class="alert alert-{{ message_type }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
<div class="row">
<!-- Left Panel: Add Box Form -->
<div class="col-md-3">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">
<i class="fas fa-plus me-2"></i>Create New Box
</h5>
</div>
<div class="card-body">
<form method="POST" id="addBoxForm">
<input type="hidden" name="action" value="add_box">
<button type="submit" class="btn btn-primary w-100" name="add_box" value="1">
<i class="fas fa-plus me-2"></i>Create Box
</button>
</form>
</div>
</div>
<!-- Statistics Card -->
<div class="card shadow-sm mt-3">
<div class="card-header bg-info text-white">
<h5 class="mb-0">
<i class="fas fa-chart-bar me-2"></i>Statistics
</h5>
</div>
<div class="card-body">
<div class="mb-3">
<small class="text-muted d-block">Total Boxes</small>
<h4 class="text-primary mb-0">{{ stats.total }}</h4>
</div>
<div class="mb-3">
<small class="text-muted d-block">Open</small>
<h5 class="text-success mb-0">{{ stats.open }}</h5>
</div>
<div>
<small class="text-muted d-block">Closed</small>
<h5 class="text-danger mb-0">{{ stats.closed }}</h5>
</div>
</div>
</div>
<!-- Delete Multiple Button -->
<div class="card shadow-sm mt-3 border-warning">
<div class="card-header bg-warning">
<h5 class="mb-0">
<i class="fas fa-trash me-2"></i>Delete Selected
</h5>
</div>
<div class="card-body">
<form method="POST" id="deleteForm">
<input type="hidden" name="action" value="delete_multiple">
<button type="button" class="btn btn-danger w-100" id="deleteSelectedBtn"
onclick="deleteSelectedBoxes()">
<i class="fas fa-trash me-2"></i>Delete Selected
</button>
<input type="hidden" id="delete_ids" name="delete_ids" value="">
</form>
<small class="text-muted d-block mt-2">Select boxes in table to delete</small>
</div>
</div>
</div>
<!-- Center Panel: Boxes Table -->
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-header bg-info text-white">
<h5 class="mb-0">
<i class="fas fa-list me-2"></i>All Boxes ({{ boxes|length }})
</h5>
</div>
<div class="card-body" style="max-height: 600px; overflow-y: auto;">
{% if boxes %}
<div class="table-responsive">
<table class="table table-striped table-hover" id="boxesTable">
<thead class="table-light sticky-top">
<tr>
<th width="30">
<input type="checkbox" id="selectAll" onchange="toggleSelectAll(this)">
</th>
<th>Box Number</th>
<th>Status</th>
<th>Location</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for box in boxes %}
<tr class="box-row" data-box-id="{{ box.id }}" data-box-number="{{ box.box_number }}"
data-box-status="{{ box.status }}" data-location-id="{{ box.location_id or '' }}">
<td>
<input type="checkbox" class="box-checkbox"
value="{{ box.id }}"
onchange="updateDeleteBtn()">
</td>
<td>
<strong>{{ box.box_number }}</strong>
</td>
<td>
<span class="badge bg-{{ 'success' if box.status == 'open' else 'danger' }}">
{{ box.status|upper }}
</span>
</td>
<td>{{ box.location_code or '-' }}</td>
<td>
<button type="button" class="btn btn-sm btn-outline-primary"
onclick="editBox({{ box.id }}, event)">
<i class="fas fa-edit"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info text-center" role="alert">
<i class="fas fa-info-circle me-2"></i>
No boxes found. Create one using the form on the left.
</div>
{% endif %}
</div>
</div>
</div>
<!-- Right Panel: Edit/Print -->
<div class="col-md-3">
<div class="card shadow-sm">
<div class="card-header bg-success text-white">
<h5 class="mb-0">
<i class="fas fa-edit me-2"></i>Edit Box
</h5>
</div>
<div class="card-body">
<div id="editSection" style="display: none;">
<form method="POST" id="editBoxForm">
<input type="hidden" name="action" value="edit_box">
<input type="hidden" id="edit_box_id" name="box_id">
<div class="mb-3">
<label class="form-label">Box Number</label>
<input type="text" class="form-control" id="edit_box_number"
placeholder="Box number" readonly>
<small class="text-muted">Cannot be changed</small>
</div>
<div class="mb-3">
<label for="edit_status" class="form-label">Status</label>
<select class="form-select" id="edit_status" name="status">
<option value="open">Open</option>
<option value="closed">Closed</option>
</select>
</div>
<div class="mb-3">
<label for="edit_location" class="form-label">Location</label>
<select class="form-select" id="edit_location" name="location_id">
<option value="">No Location</option>
{% for location in locations %}
<option value="{{ location.id }}">{{ location.location_code }}</option>
{% endfor %}
</select>
</div>
<button type="button" class="btn btn-success w-100 mb-2"
onclick="saveEditBox()">
<i class="fas fa-save me-2"></i>Save Changes
</button>
<button type="button" class="btn btn-danger w-100 mb-2"
onclick="deleteBoxConfirm()">
<i class="fas fa-trash me-2"></i>Delete Box
</button>
<button type="button" class="btn btn-secondary w-100"
onclick="cancelEdit()">
Cancel
</button>
</form>
</div>
<div id="noEditSection" class="alert alert-info text-center">
<i class="fas fa-arrow-left me-2"></i>
Click edit button in table to modify a box
</div>
</div>
</div>
<!-- Barcode Print Section -->
<div class="card shadow-sm mt-3">
<div class="card-header bg-secondary text-white">
<h5 class="mb-0">
<i class="fas fa-barcode me-2"></i>Print Label
</h5>
</div>
<div class="card-body">
<div id="printSection" style="display: none;">
<div class="mb-3">
<label class="form-label">Box Number:</label>
<p id="print_box_number" class="fw-bold mb-0" style="font-size: 1.2rem; color: #0d6efd;">-</p>
</div>
<!-- Printer Selection -->
<div class="mb-3">
<label for="printer-select" class="form-label">Select Printer:</label>
<select id="printer-select" class="form-select form-select-sm" style="font-size: 0.95rem;">
<option value="">Default Printer</option>
</select>
<small class="text-muted d-block mt-1">
<i class="fas fa-info-circle me-1"></i>
<span id="qz-status">QZ Tray: Initializing...</span>
</small>
</div>
<!-- Barcode Preview -->
<div id="barcodePreviewContainer" style="display: none; text-align: center; margin: 20px 0; padding: 15px; background-color: #f8f9fa; border-radius: 4px;">
<svg id="boxBarcode" style="max-width: 100%; height: auto;"></svg>
<p class="text-muted small mt-2 mb-0">Scan this barcode to identify the box</p>
</div>
<button type="button" class="btn btn-primary w-100 mb-2"
onclick="generateBarcodePreview()">
<i class="fas fa-eye me-2"></i>Generate Preview
</button>
<button type="button" class="btn btn-success w-100 mb-2"
id="printBoxBtn" style="display: none;" onclick="printBarcode()">
<i class="fas fa-print me-2"></i>Print Label
</button>
<button type="button" class="btn btn-info btn-sm w-100 mb-2"
onclick="testQZTrayConnection()">
<i class="fas fa-cog me-2"></i>Test QZ Tray
</button>
<small class="text-muted d-block">
<i class="fas fa-info-circle me-1"></i>
Requires QZ Tray installed for thermal printing
</small>
</div>
<div id="noPrintSection" class="alert alert-info text-center">
<i class="fas fa-arrow-left me-2"></i>
Select a box to print
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div class="modal fade" id="deleteConfirmModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title">Confirm Delete</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p id="deleteConfirmMessage">Are you sure you want to delete this box?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" id="confirmDeleteBtn"
onclick="confirmDelete()">Delete</button>
</div>
</div>
</div>
</div>
<!-- QZ Tray and Barcode Libraries -->
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.5/dist/JsBarcode.all.min.js"></script>
<script src="{{ url_for('static', filename='js/qz-tray.js') }}"></script>
<script src="{{ url_for('static', filename='js/qz-printer.js') }}"></script>
<script>
// Box editing state
let currentEditingBoxId = null;
let currentDeleteId = null;
// Toggle select all checkboxes
function toggleSelectAll(checkbox) {
const checkboxes = document.querySelectorAll('.box-checkbox');
checkboxes.forEach(cb => cb.checked = checkbox.checked);
updateDeleteBtn();
}
// Update delete button visibility
function updateDeleteBtn() {
const checkedCount = document.querySelectorAll('.box-checkbox:checked').length;
const deleteBtn = document.getElementById('deleteSelectedBtn');
deleteBtn.disabled = checkedCount === 0;
if (checkedCount > 0) {
deleteBtn.textContent = `Delete ${checkedCount} Selected`;
} else {
deleteBtn.innerHTML = '<i class="fas fa-trash me-2"></i>Delete Selected';
}
}
// Delete selected boxes
function deleteSelectedBoxes() {
const selectedIds = Array.from(document.querySelectorAll('.box-checkbox:checked'))
.map(cb => cb.value);
if (selectedIds.length === 0) {
alert('Please select boxes to delete');
return;
}
document.getElementById('deleteConfirmMessage').textContent =
`Are you sure you want to delete ${selectedIds.length} box(es)?`;
currentDeleteId = selectedIds.join(',');
new bootstrap.Modal(document.getElementById('deleteConfirmModal')).show();
}
// Edit box
function editBox(boxId, evt) {
try {
const row = evt.target.closest('tr');
if (!row) {
console.error('Could not find table row');
return;
}
currentEditingBoxId = boxId;
const boxNumber = row.dataset.boxNumber;
const status = row.dataset.boxStatus;
const locationId = row.dataset.locationId || '';
console.log('Editing box:', {boxId, boxNumber, status, locationId});
// Populate form fields
const editBoxIdEl = document.getElementById('edit_box_id');
const editBoxNumberEl = document.getElementById('edit_box_number');
const editStatusEl = document.getElementById('edit_status');
const editLocationEl = document.getElementById('edit_location');
if (editBoxIdEl) editBoxIdEl.value = boxId;
if (editBoxNumberEl) editBoxNumberEl.value = boxNumber;
if (editStatusEl) editStatusEl.value = status;
if (editLocationEl) editLocationEl.value = locationId;
// Show/hide sections
const editSectionEl = document.getElementById('editSection');
const noEditSectionEl = document.getElementById('noEditSection');
const printSectionEl = document.getElementById('printSection');
const noPrintSectionEl = document.getElementById('noPrintSection');
if (editSectionEl) editSectionEl.style.display = 'block';
if (noEditSectionEl) noEditSectionEl.style.display = 'none';
if (printSectionEl) printSectionEl.style.display = 'block';
if (noPrintSectionEl) noPrintSectionEl.style.display = 'none';
// Update print section
const printBoxNumberEl = document.getElementById('print_box_number');
if (printBoxNumberEl) printBoxNumberEl.textContent = boxNumber;
// Reset barcode preview
const barcodeEl = document.getElementById('boxBarcode');
if (barcodeEl) {
barcodeEl.innerHTML = '';
}
const barcodePreviewEl = document.getElementById('barcodePreviewContainer');
const printBoxBtnEl = document.getElementById('printBoxBtn');
if (barcodePreviewEl) barcodePreviewEl.style.display = 'none';
if (printBoxBtnEl) printBoxBtnEl.style.display = 'none';
// Highlight selected row
document.querySelectorAll('.box-row').forEach(r => r.style.backgroundColor = '');
if (row) row.style.backgroundColor = '#e3f2fd';
console.log('Box edit section displayed');
} catch (error) {
console.error('Error in editBox:', error);
alert('Error loading box: ' + error.message);
}
}
// Save edit
function saveEditBox() {
if (!currentEditingBoxId) return;
const statusEl = document.getElementById('edit_status');
const locationEl = document.getElementById('edit_location');
const status = statusEl ? statusEl.value : '';
const location_id = locationEl ? locationEl.value : '';
const form2 = document.createElement('form');
form2.method = 'POST';
form2.innerHTML = `
<input type="hidden" name="action" value="edit_box">
<input type="hidden" name="box_id" value="${currentEditingBoxId}">
<input type="hidden" name="status" value="${status}">
<input type="hidden" name="location_id" value="${location_id}">
`;
document.body.appendChild(form2);
form2.submit();
}
// Delete box confirmation
function deleteBoxConfirm() {
if (!currentEditingBoxId) return;
document.getElementById('deleteConfirmMessage').textContent =
'Are you sure you want to delete this box?';
currentDeleteId = currentEditingBoxId;
new bootstrap.Modal(document.getElementById('deleteConfirmModal')).show();
}
// Confirm delete
function confirmDelete() {
const form = document.createElement('form');
form.method = 'POST';
form.innerHTML = `
<input type="hidden" name="action" value="delete_box">
<input type="hidden" name="box_id" value="${currentDeleteId}">
`;
document.body.appendChild(form);
form.submit();
bootstrap.Modal.getInstance(document.getElementById('deleteConfirmModal')).hide();
}
// Cancel edit
function cancelEdit() {
currentEditingBoxId = null;
const editSectionEl = document.getElementById('editSection');
const noEditSectionEl = document.getElementById('noEditSection');
const printSectionEl = document.getElementById('printSection');
const noPrintSectionEl = document.getElementById('noPrintSection');
if (editSectionEl) editSectionEl.style.display = 'none';
if (noEditSectionEl) noEditSectionEl.style.display = 'block';
if (printSectionEl) printSectionEl.style.display = 'none';
if (noPrintSectionEl) noPrintSectionEl.style.display = 'block';
document.querySelectorAll('.box-row').forEach(r => r.style.backgroundColor = '');
}
// Barcode generation
function generateBarcodePreview() {
const boxNumber = document.getElementById('print_box_number').textContent.trim();
if (!boxNumber || boxNumber === '-') {
alert('Please select a box first');
return;
}
const barcodeEl = document.getElementById('boxBarcode');
const containerEl = document.getElementById('barcodePreviewContainer');
const printBtn = document.getElementById('printBoxBtn');
if (barcodeEl) {
barcodeEl.innerHTML = '';
try {
JsBarcode("#boxBarcode", boxNumber, {
format: "CODE128",
width: 2,
height: 100,
displayValue: true,
margin: 10
});
console.log('Barcode generated for box:', boxNumber);
if (containerEl) {
containerEl.style.display = 'block';
}
if (printBtn) {
printBtn.style.display = 'block';
}
} catch (error) {
console.error('Error generating barcode:', error);
alert('Error generating barcode. Make sure jsbarcode library is loaded.');
}
}
}
// Print barcode
function printBarcode() {
const boxNumber = document.getElementById('print_box_number').textContent.trim();
if (!boxNumber || boxNumber === '-') {
alert('Please select a box first');
return;
}
console.log('Printing barcode for box:', boxNumber);
if (window.qzPrinter && window.qzPrinter.connected) {
printWithQZTray(boxNumber);
} else {
printWithBrowserDialog(boxNumber);
}
}
// Print with QZ Tray
function printWithQZTray(boxNumber) {
try {
if (!window.qzPrinter || !window.qzPrinter.connected) {
console.log('QZ Tray not connected, falling back to browser print');
printWithBrowserDialog(boxNumber);
return;
}
const svgElement = document.getElementById('boxBarcode');
if (!svgElement) {
console.error('Barcode element not found');
printWithBrowserDialog(boxNumber);
return;
}
const printerSelect = document.getElementById('printer-select');
const selectedPrinter = printerSelect ? printerSelect.value : '';
console.log('Printing to QZ Tray - Selected printer:', selectedPrinter || 'Default');
window.qzPrinter.printSVGBarcode(svgElement, 'Box: ' + boxNumber, selectedPrinter)
.then(() => {
console.log('✅ Print job sent successfully');
const printerName = selectedPrinter || 'default printer';
alert('Print job sent to ' + printerName + ' successfully!');
})
.catch(error => {
console.error('Print error:', error);
console.log('Falling back to browser print');
printWithBrowserDialog(boxNumber);
});
} catch (error) {
console.error('QZ Tray printing error:', error);
printWithBrowserDialog(boxNumber);
}
}
// Browser print fallback
function printWithBrowserDialog(boxNumber) {
const printWindow = window.open('', '', 'height=400,width=600');
const barcodeSvg = document.getElementById('boxBarcode').innerHTML;
printWindow.document.write('<html><head><title>Print Box Label</title>');
printWindow.document.write('<style>');
printWindow.document.write('body { font-family: Arial, sans-serif; text-align: center; padding: 20px; }');
printWindow.document.write('h2 { margin-bottom: 30px; }');
printWindow.document.write('svg { max-width: 100%; height: auto; margin: 20px 0; }');
printWindow.document.write('</style></head><body>');
printWindow.document.write('<h2>Box: ' + boxNumber + '</h2>');
printWindow.document.write('<svg id="barcode">' + barcodeSvg + '</svg>');
printWindow.document.write('<p style="margin-top: 40px; font-size: 14px; color: #666;">');
printWindow.document.write('Printed on: ' + new Date().toLocaleString());
printWindow.document.write('</p></body></html>');
printWindow.document.close();
setTimeout(function() {
printWindow.print();
}, 250);
}
// QZ Tray status update
function updateQZStatus(message, status = 'info') {
const statusEl = document.getElementById('qz-status');
if (statusEl) {
statusEl.textContent = 'QZ Tray: ' + message;
statusEl.className = status === 'success' ? 'text-success fw-bold' :
status === 'warning' ? 'text-warning' : 'text-muted';
}
}
// Test QZ Tray
function testQZTrayConnection() {
if (window.qzPrinter && window.qzPrinter.connected) {
const printers = window.qzPrinter.availablePrinters;
const printerList = printers.length > 0
? printers.join('\n• ')
: 'No printers found';
alert('✅ QZ Tray is connected and ready!\n\nAvailable printers:\n• ' + printerList);
} else {
alert('⚠️ QZ Tray is not connected.\nBrowser print will be used instead.');
}
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
updateDeleteBtn();
// Initialize QZ Tray
setTimeout(() => {
if (window.qzPrinter) {
if (window.qzPrinter.connected) {
updateQZStatus('Connected ✅', 'success');
populatePrinterSelect();
} else {
updateQZStatus('Not Available (will use browser print)', 'warning');
}
}
}, 500);
// Handle printer selection change
const printerSelect = document.getElementById('printer-select');
if (printerSelect) {
printerSelect.addEventListener('change', function() {
if (window.qzPrinter) {
window.qzPrinter.selectPrinter(this.value);
}
});
}
});
// Populate printer select
function populatePrinterSelect() {
if (!window.qzPrinter || !window.qzPrinter.availablePrinters.length) {
return;
}
const printerSelect = document.getElementById('printer-select');
if (!printerSelect) return;
printerSelect.innerHTML = '<option value="">Default Printer</option>';
window.qzPrinter.availablePrinters.forEach(printer => {
const option = document.createElement('option');
option.value = printer;
option.textContent = printer;
if (printer === window.qzPrinter.selectedPrinter) {
option.selected = true;
}
printerSelect.appendChild(option);
});
}
</script>
<style>
.sticky-top {
z-index: 10;
}
.table-hover tbody tr:hover {
background-color: #f8f9fa;
}
.card {
border-radius: 8px;
}
.card-header {
border-radius: 8px 8px 0 0;
}
#boxesTable {
font-size: 0.95rem;
}
.btn-outline-primary:hover {
transform: scale(1.05);
transition: all 0.2s;
}
</style>
{% endblock %}

View File

@@ -0,0 +1,151 @@
{% extends "base.html" %}
{% block title %}Warehouse Module - Quality App v2{% endblock %}
{% block content %}
<div class="container-fluid py-5">
<div class="row mb-4">
<div class="col-12">
<h1 class="mb-2">
<i class="fas fa-warehouse"></i> Warehouse Module
</h1>
<p class="text-muted">Manage warehouse operations, inventory, and locations</p>
</div>
</div>
<div class="row">
<!-- Set Boxes Locations Card -->
<div class="col-md-6 col-lg-4 mb-4">
<div class="card shadow-sm h-100 module-launcher">
<div class="card-body text-center">
<div class="launcher-icon mb-3">
<i class="fas fa-cube text-danger"></i>
</div>
<h5 class="card-title">Set Boxes Locations</h5>
<p class="card-text text-muted">Add or update articles in the warehouse inventory.</p>
<a href="{{ url_for('warehouse.set_boxes_locations') }}" class="btn btn-danger btn-sm">
<i class="fas fa-arrow-right"></i> Go to Set Boxes
</a>
</div>
</div>
</div>
<!-- Create Warehouse Locations Card -->
<div class="col-md-6 col-lg-4 mb-4">
<div class="card shadow-sm h-100 module-launcher">
<div class="card-body text-center">
<div class="launcher-icon mb-3">
<i class="fas fa-map-pin text-primary"></i>
</div>
<h5 class="card-title">Create Warehouse Locations</h5>
<p class="card-text text-muted">Define and manage storage locations in the warehouse.</p>
<a href="{{ url_for('warehouse.locations') }}" class="btn btn-primary btn-sm">
<i class="fas fa-arrow-right"></i> Go to Locations
</a>
</div>
</div>
</div>
<!-- Manage Boxes/Crates Card -->
<div class="col-md-6 col-lg-4 mb-4">
<div class="card shadow-sm h-100 module-launcher">
<div class="card-body text-center">
<div class="launcher-icon mb-3">
<i class="fas fa-box text-success"></i>
</div>
<h5 class="card-title">Manage Boxes/Crates</h5>
<p class="card-text text-muted">Track and manage boxes and crates in the warehouse.</p>
<a href="{{ url_for('boxes.manage_boxes') }}" class="btn btn-success btn-sm">
<i class="fas fa-arrow-right"></i> Go to Boxes
</a>
</div>
</div>
</div>
<!-- View Inventory Card -->
<div class="col-md-6 col-lg-4 mb-4">
<div class="card shadow-sm h-100 module-launcher">
<div class="card-body text-center">
<div class="launcher-icon mb-3">
<i class="fas fa-list text-info"></i>
</div>
<h5 class="card-title">View Inventory</h5>
<p class="card-text text-muted">Search and view products, boxes, and their warehouse locations.</p>
<a href="{{ url_for('warehouse.inventory') }}" class="btn btn-info btn-sm">
<i class="fas fa-arrow-right"></i> View Inventory
</a>
</div>
</div>
</div>
<!-- Warehouse Reports Card -->
<div class="col-md-6 col-lg-4 mb-4">
<div class="card shadow-sm h-100 module-launcher">
<div class="card-body text-center">
<div class="launcher-icon mb-3">
<i class="fas fa-chart-bar text-warning"></i>
</div>
<h5 class="card-title">Warehouse Reports</h5>
<p class="card-text text-muted">View and export warehouse activity and inventory reports.</p>
<a href="{{ url_for('warehouse.reports') }}" class="btn btn-warning btn-sm">
<i class="fas fa-arrow-right"></i> Go to Reports
</a>
</div>
</div>
</div>
</div>
<!-- Module Overview Section -->
<div class="row mt-5">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="fas fa-info-circle"></i> Module Overview</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<h6><i class="fas fa-check-circle text-success"></i> Key Features:</h6>
<ul class="text-muted">
<li>Create and manage warehouse locations</li>
<li>Track boxes and crates</li>
<li>Assign products to boxes</li>
<li>Search inventory by location</li>
</ul>
</div>
<div class="col-md-6">
<h6><i class="fas fa-chart-pie text-primary"></i> Reports & Analytics:</h6>
<ul class="text-muted">
<li>Inventory reports</li>
<li>Location utilization</li>
<li>Box status tracking</li>
<li>Export data to CSV</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.module-launcher {
transition: transform 0.2s, box-shadow 0.2s;
}
.module-launcher:hover {
transform: translateY(-5px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
}
.launcher-icon {
font-size: 48px;
line-height: 1;
}
.launcher-icon i {
display: inline-block;
}
</style>
{% endblock %}

View File

@@ -0,0 +1,67 @@
{% extends "base.html" %}
{% block title %}Warehouse Inventory - Quality App v2{% endblock %}
{% block content %}
<div class="container-fluid py-5">
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<div>
<h1 class="mb-2">
<i class="fas fa-list"></i> Warehouse Inventory
</h1>
<p class="text-muted">Search and view products, boxes, and their warehouse locations</p>
</div>
<a href="{{ url_for('warehouse.warehouse_index') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Back to Warehouse
</a>
</div>
</div>
</div>
<div class="row mb-4">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="fas fa-search"></i> Search Inventory</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="searchProduct">Search by Product Code:</label>
<input type="text" id="searchProduct" class="form-control" placeholder="Enter product code...">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="searchLocation">Search by Location:</label>
<input type="text" id="searchLocation" class="form-control" placeholder="Enter location code...">
</div>
</div>
</div>
<button class="btn btn-primary">
<i class="fas fa-search"></i> Search
</button>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="fas fa-box"></i> Inventory Results</h5>
</div>
<div class="card-body">
<p class="text-muted">
<i class="fas fa-info-circle"></i> Inventory search feature coming soon...
</p>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,848 @@
{% extends "base.html" %}
{% block content %}
<div class="container-fluid mt-5">
<div class="row mb-4">
<div class="col-12">
<h2 class="mb-3">
<i class="fas fa-map-marker-alt me-2"></i>Set Locations
</h2>
</div>
</div>
{% if message %}
<div class="alert alert-{{ message_type }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endif %}
<div class="row">
<!-- Left Panel: Add Location Form -->
<div class="col-md-3">
<div class="card shadow-sm">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">
<i class="fas fa-plus me-2"></i>Add New Location
</h5>
</div>
<div class="card-body">
<form method="POST" id="addLocationForm">
<div class="mb-3">
<label for="location_code" class="form-label">Location Code <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="location_code" name="location_code"
placeholder="e.g., LOC-001" required>
<small class="text-muted">Must be unique</small>
</div>
<div class="mb-3">
<label for="size" class="form-label">Size</label>
<input type="text" class="form-control" id="size" name="size"
placeholder="e.g., Small, Medium, Large">
</div>
<div class="mb-3">
<label for="description" class="form-label">Description</label>
<textarea class="form-control" id="description" name="description"
rows="3" placeholder="Location notes..."></textarea>
</div>
<button type="submit" class="btn btn-primary w-100" name="add_location" value="1">
<i class="fas fa-plus me-2"></i>Add Location
</button>
</form>
</div>
</div>
<!-- Delete Multiple Button -->
<div class="card shadow-sm mt-3 border-warning">
<div class="card-header bg-warning">
<h5 class="mb-0">
<i class="fas fa-trash me-2"></i>Delete Selected
</h5>
</div>
<div class="card-body">
<form method="POST" id="deleteForm">
<button type="button" class="btn btn-danger w-100" id="deleteSelectedBtn"
onclick="deleteSelectedLocations()">
<i class="fas fa-trash me-2"></i>Delete Selected
</button>
<input type="hidden" id="delete_ids" name="delete_ids" value="">
</form>
<small class="text-muted d-block mt-2">Select locations in table to delete</small>
</div>
</div>
</div>
<!-- Center Panel: Locations Table -->
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-header bg-info text-white">
<h5 class="mb-0">
<i class="fas fa-list me-2"></i>All Locations ({{ locations|length }})
</h5>
</div>
<div class="card-body" style="max-height: 600px; overflow-y: auto;">
{% if locations %}
<div class="table-responsive">
<table class="table table-striped table-hover" id="locationsTable">
<thead class="table-light sticky-top">
<tr>
<th width="30">
<input type="checkbox" id="selectAll" onchange="toggleSelectAll(this)">
</th>
<th>Code</th>
<th>Size</th>
<th>Description</th>
<th>Action</th>
</tr>
</thead>
<tbody>
{% for location in locations %}
<tr class="location-row" data-location-id="{{ location.id }}" data-location-code="{{ location.location_code }}" data-location-size="{{ location.size or '' }}" data-location-description="{{ location.description or '' }}">
<td>
<input type="checkbox" class="location-checkbox"
value="{{ location.id }}"
onchange="updateDeleteBtn()">
</td>
<td>
<strong>{{ location.location_code }}</strong>
</td>
<td>{{ location.size or '-' }}</td>
<td>
<span class="text-muted">{{ location.description or '-' }}</span>
</td>
<td>
<button type="button" class="btn btn-sm btn-outline-primary"
onclick="editLocation({{ location.id }}, event)">
<i class="fas fa-edit"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="alert alert-info text-center" role="alert">
<i class="fas fa-info-circle me-2"></i>
No locations found. Create one using the form on the left.
</div>
{% endif %}
</div>
</div>
</div>
<!-- Right Panel: Edit/Delete/Print -->
<div class="col-md-3">
<div class="card shadow-sm">
<div class="card-header bg-success text-white">
<h5 class="mb-0">
<i class="fas fa-edit me-2"></i>Edit Location
</h5>
</div>
<div class="card-body">
<div id="editSection" style="display: none;">
<form method="POST" id="editLocationForm">
<input type="hidden" id="edit_location_id" name="location_id">
<div class="mb-3">
<label class="form-label">Location Code</label>
<input type="text" class="form-control" id="edit_location_code"
placeholder="Location code" readonly>
<small class="text-muted">Cannot be changed</small>
</div>
<div class="mb-3">
<label class="form-label">Size</label>
<input type="text" class="form-control" id="edit_size"
name="edit_size" placeholder="e.g., Small">
</div>
<div class="mb-3">
<label class="form-label">Description</label>
<textarea class="form-control" id="edit_description"
name="edit_description" rows="3"></textarea>
</div>
<button type="button" class="btn btn-success w-100 mb-2"
onclick="saveEditLocation()">
<i class="fas fa-save me-2"></i>Save Changes
</button>
<button type="button" class="btn btn-danger w-100 mb-2"
onclick="deleteLocation()">
<i class="fas fa-trash me-2"></i>Delete Location
</button>
<button type="button" class="btn btn-secondary w-100"
onclick="cancelEdit()">
Cancel
</button>
</form>
</div>
<div id="noEditSection" class="alert alert-info text-center">
<i class="fas fa-arrow-left me-2"></i>
Click edit button in table to modify a location
</div>
</div>
</div>
<!-- Barcode Print Section -->
<div class="card shadow-sm mt-3">
<div class="card-header bg-secondary text-white">
<h5 class="mb-0">
<i class="fas fa-barcode me-2"></i>Print Barcode
</h5>
</div>
<div class="card-body">
<div id="printSection" style="display: none;">
<div class="mb-3">
<label class="form-label">Location:</label>
<p id="print_location_code" class="fw-bold mb-0" style="font-size: 1.2rem; color: #0d6efd;">-</p>
</div>
<!-- Printer Selection -->
<div class="mb-3">
<label for="printer-select" class="form-label">Select Printer:</label>
<select id="printer-select" class="form-select form-select-sm" style="font-size: 0.95rem;">
<option value="">Default Printer</option>
</select>
<small class="text-muted d-block mt-1">
<i class="fas fa-info-circle me-1"></i>
<span id="qz-status">QZ Tray: Initializing...</span>
</small>
</div>
<!-- Barcode Preview -->
<div id="barcodePreviewContainer" style="display: none; text-align: center; margin: 20px 0; padding: 15px; background-color: #f8f9fa; border-radius: 4px;">
<svg id="cardBarcode" style="max-width: 100%; height: auto;"></svg>
<p class="text-muted small mt-2 mb-0">Scan this barcode to identify the location</p>
</div>
<button type="button" class="btn btn-primary w-100 mb-2"
onclick="generateBarcodePreview()">
<i class="fas fa-eye me-2"></i>Generate Preview
</button>
<button type="button" class="btn btn-success w-100 mb-2"
id="printCardBtn" style="display: none;" onclick="printBarcode()">
<i class="fas fa-print me-2"></i>Print Barcode
</button>
<button type="button" class="btn btn-info btn-sm w-100 mb-2"
onclick="testQZTrayConnection()">
<i class="fas fa-cog me-2"></i>Test QZ Tray
</button>
<small class="text-muted d-block">
<i class="fas fa-info-circle me-1"></i>
Requires QZ Tray installed for thermal printing
</small>
</div>
<div id="noPrintSection" class="alert alert-info text-center">
<i class="fas fa-arrow-left me-2"></i>
Select a location to print
</div>
</div>
</div>
<!-- Import CSV Section -->
<div class="card shadow-sm mt-3 border-primary">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">
<i class="fas fa-file-import me-2"></i>Import CSV
</h5>
</div>
<div class="card-body">
<form method="POST" enctype="multipart/form-data" id="importForm">
<div class="mb-3">
<label for="csvFile" class="form-label">Choose CSV</label>
<input type="file" class="form-control" id="csvFile"
accept=".csv" required>
<small class="text-muted d-block mt-1">Format: location_code, size, description</small>
</div>
<button type="button" class="btn btn-primary w-100"
onclick="importCSV()">
<i class="fas fa-upload me-2"></i>Import
</button>
</form>
</div>
</div>
</div>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div class="modal fade" id="deleteConfirmModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header bg-danger text-white">
<h5 class="modal-title">Confirm Delete</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p id="deleteConfirmMessage">Are you sure you want to delete this location?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" id="confirmDeleteBtn"
onclick="confirmDelete()">Delete</button>
</div>
</div>
</div>
</div>
<!-- Barcode Preview Modal -->
<div class="modal fade" id="barcodeModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-secondary text-white">
<h5 class="modal-title">
<i class="fas fa-barcode me-2"></i>Barcode Preview
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body text-center">
<p class="fw-bold mb-3" id="barcodeLocationCode">LOC-001</p>
<svg id="barcode"></svg>
<p class="text-muted mt-3 small">Scan this barcode to identify the location</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-success" onclick="printBarcodeFromModal()">
<i class="fas fa-print me-2"></i>Print Barcode
</button>
</div>
</div>
</div>
</div>
<!-- QZ Tray and Barcode Libraries -->
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.5/dist/JsBarcode.all.min.js"></script>
<script src="{{ url_for('static', filename='js/qz-tray.js') }}"></script>
<script src="{{ url_for('static', filename='js/qz-printer.js') }}"></script>
<script>
// Barcode printing functions
let currentBarcodeLocation = null;
function generateBarcodePreview() {
const locationCode = document.getElementById('print_location_code').textContent.trim();
if (!locationCode || locationCode === '-') {
alert('Please select a location first');
return;
}
currentBarcodeLocation = locationCode;
// Generate barcode in card
const barcodeEl = document.getElementById('cardBarcode');
const containerEl = document.getElementById('barcodePreviewContainer');
const printBtn = document.getElementById('printCardBtn');
if (barcodeEl) {
barcodeEl.innerHTML = '';
try {
// Generate barcode using JsBarcode
JsBarcode("#cardBarcode", locationCode, {
format: "CODE128",
width: 2,
height: 100,
displayValue: true,
margin: 10
});
console.log('Barcode generated in card');
// Show preview container and print button
if (containerEl) {
containerEl.style.display = 'block';
}
if (printBtn) {
printBtn.style.display = 'block';
}
} catch (error) {
console.error('Error generating barcode:', error);
alert('Error generating barcode. Make sure jsbarcode library is loaded.');
}
}
}
function printBarcode() {
const locationCode = document.getElementById('print_location_code').textContent.trim();
if (!locationCode || locationCode === '-') {
alert('Please select a location first');
return;
}
currentBarcodeLocation = locationCode;
console.log('Printing barcode for:', locationCode);
// Try QZ Tray first
if (window.qzPrinter && window.qzPrinter.connected) {
printWithQZTray(locationCode);
} else {
// Fallback to browser print
printWithBrowserDialog(locationCode);
}
}
function printWithQZTray(locationCode) {
try {
if (!window.qzPrinter || !window.qzPrinter.connected) {
console.log('QZ Tray not connected, falling back to browser print');
printWithBrowserDialog(locationCode);
return;
}
const svgElement = document.getElementById('cardBarcode');
if (!svgElement) {
console.error('Barcode element not found');
printWithBrowserDialog(locationCode);
return;
}
// Get the selected printer from dropdown
const printerSelect = document.getElementById('printer-select');
const selectedPrinter = printerSelect ? printerSelect.value : '';
console.log('Printing to QZ Tray - Selected printer:', selectedPrinter || 'Default');
// Use the shared qzPrinter module to print
window.qzPrinter.printSVGBarcode(svgElement, 'Location: ' + locationCode, selectedPrinter)
.then(() => {
console.log('✅ Print job sent successfully to printer');
const printerName = selectedPrinter || 'default printer';
alert('Print job sent to ' + printerName + ' successfully!');
})
.catch(error => {
console.error('Print error:', error);
console.log('Falling back to browser print');
printWithBrowserDialog(locationCode);
});
} catch (error) {
console.error('QZ Tray printing error:', error);
printWithBrowserDialog(locationCode);
}
}
function printWithBrowserDialog(locationCode) {
// Open browser print dialog
const printWindow = window.open('', '', 'height=400,width=600');
const barcodeSvg = document.getElementById('cardBarcode').innerHTML;
printWindow.document.write('<html><head><title>Print Location Barcode</title>');
printWindow.document.write('<style>');
printWindow.document.write('body { font-family: Arial, sans-serif; text-align: center; padding: 20px; }');
printWindow.document.write('h2 { margin-bottom: 30px; }');
printWindow.document.write('svg { max-width: 100%; height: auto; margin: 20px 0; }');
printWindow.document.write('</style></head><body>');
printWindow.document.write('<h2>Location: ' + locationCode + '</h2>');
printWindow.document.write('<svg id="barcode">' + barcodeSvg + '</svg>');
printWindow.document.write('<p style="margin-top: 40px; font-size: 14px; color: #666;">');
printWindow.document.write('Printed on: ' + new Date().toLocaleString());
printWindow.document.write('</p></body></html>');
printWindow.document.close();
// Trigger print after a short delay to ensure SVG is rendered
setTimeout(function() {
printWindow.print();
}, 250);
}
let currentEditingLocationId = null;
let currentDeleteId = null;
// Toggle select all checkboxes
function toggleSelectAll(checkbox) {
const checkboxes = document.querySelectorAll('.location-checkbox');
checkboxes.forEach(cb => cb.checked = checkbox.checked);
updateDeleteBtn();
}
// Update delete button visibility
function updateDeleteBtn() {
const checkedCount = document.querySelectorAll('.location-checkbox:checked').length;
const deleteBtn = document.getElementById('deleteSelectedBtn');
deleteBtn.disabled = checkedCount === 0;
if (checkedCount > 0) {
deleteBtn.textContent = `Delete ${checkedCount} Selected`;
} else {
deleteBtn.innerHTML = '<i class="fas fa-trash me-2"></i>Delete Selected';
}
}
// Delete selected locations
function deleteSelectedLocations() {
const selectedIds = Array.from(document.querySelectorAll('.location-checkbox:checked'))
.map(cb => cb.value);
if (selectedIds.length === 0) {
alert('Please select locations to delete');
return;
}
document.getElementById('deleteConfirmMessage').textContent =
`Are you sure you want to delete ${selectedIds.length} location(s)?`;
currentDeleteId = selectedIds.join(',');
new bootstrap.Modal(document.getElementById('deleteConfirmModal')).show();
}
// Edit location
function editLocation(locationId, evt) {
try {
const row = evt.target.closest('tr');
if (!row) {
console.error('Could not find table row');
return;
}
const cells = row.querySelectorAll('td');
if (cells.length < 4) {
console.error('Invalid table structure');
return;
}
const locationCode = cells[1].textContent.trim();
const size = cells[2].textContent.trim() === '-' ? '' : cells[2].textContent.trim();
const description = cells[3].textContent.trim() === '-' ? '' : cells[3].textContent.trim();
currentEditingLocationId = locationId;
console.log('Location selected:', locationId, locationCode);
// Safely set all element values
const editLocationIdEl = document.getElementById('edit_location_id');
const editLocationCodeEl = document.getElementById('edit_location_code');
const editSizeEl = document.getElementById('edit_size');
const editDescriptionEl = document.getElementById('edit_description');
const printLocationCodeEl = document.getElementById('print_location_code');
const editSectionEl = document.getElementById('editSection');
const noEditSectionEl = document.getElementById('noEditSection');
const printSectionEl = document.getElementById('printSection');
const noPrintSectionEl = document.getElementById('noPrintSection');
console.log('Elements found:', {
editSectionEl: !!editSectionEl,
printSectionEl: !!printSectionEl,
printLocationCodeEl: !!printLocationCodeEl
});
if (editLocationIdEl) editLocationIdEl.value = locationId;
if (editLocationCodeEl) editLocationCodeEl.value = locationCode;
if (editSizeEl) editSizeEl.value = size;
if (editDescriptionEl) editDescriptionEl.value = description;
if (printLocationCodeEl) {
printLocationCodeEl.textContent = locationCode;
console.log('Print location code set to:', locationCode);
}
// Show/hide edit section
if (editSectionEl) {
editSectionEl.style.display = 'block';
editSectionEl.style.visibility = 'visible';
console.log('Edit section displayed');
}
if (noEditSectionEl) {
noEditSectionEl.style.display = 'none';
console.log('No edit section hidden');
}
// Show/hide print section - use removeAttribute to ensure it overrides inline style
if (printSectionEl) {
printSectionEl.removeAttribute('style');
printSectionEl.style.display = 'block';
printSectionEl.style.visibility = 'visible';
// Scroll into view if needed
setTimeout(() => {
printSectionEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}, 100);
console.log('Print section display:', window.getComputedStyle(printSectionEl).display);
console.log('Print section visible: TRUE');
} else {
console.error('Print section element not found!');
}
if (noPrintSectionEl) {
noPrintSectionEl.style.display = 'none';
noPrintSectionEl.style.visibility = 'hidden';
console.log('No print section hidden');
}
} catch (error) {
console.error('Error in editLocation:', error);
alert('Error loading location: ' + error.message);
}
}
// Save edit location
function saveEditLocation() {
if (!currentEditingLocationId) return;
const size = document.getElementById('edit_size').value;
const description = document.getElementById('edit_description').value;
// Create a form to submit
const form = document.createElement('form');
form.method = 'POST';
form.innerHTML = `
<input type="hidden" name="edit_location" value="1">
<input type="hidden" name="location_id" value="${currentEditingLocationId}">
<input type="hidden" name="size" value="${size}">
<input type="hidden" name="description" value="${description}">
`;
document.body.appendChild(form);
form.submit();
}
// Delete single location
function deleteLocation() {
if (!currentEditingLocationId) return;
document.getElementById('deleteConfirmMessage').textContent =
'Are you sure you want to delete this location?';
currentDeleteId = currentEditingLocationId;
new bootstrap.Modal(document.getElementById('deleteConfirmModal')).show();
}
// Confirm delete
function confirmDelete() {
const form = document.getElementById('deleteForm');
document.getElementById('delete_ids').value = currentDeleteId;
const submitForm = document.createElement('form');
submitForm.method = 'POST';
submitForm.innerHTML = `
<input type="hidden" name="delete_locations" value="1">
<input type="hidden" name="delete_ids" value="${currentDeleteId}">
`;
document.body.appendChild(submitForm);
submitForm.submit();
bootstrap.Modal.getInstance(document.getElementById('deleteConfirmModal')).hide();
}
// Cancel edit
function cancelEdit() {
currentEditingLocationId = null;
const editSectionEl = document.getElementById('editSection');
const noEditSectionEl = document.getElementById('noEditSection');
const printSectionEl = document.getElementById('printSection');
const noPrintSectionEl = document.getElementById('noPrintSection');
if (editSectionEl) {
editSectionEl.style.display = 'none';
editSectionEl.style.visibility = 'hidden';
}
if (noEditSectionEl) {
noEditSectionEl.style.display = 'block';
noEditSectionEl.style.visibility = 'visible';
}
if (printSectionEl) {
printSectionEl.style.display = 'none';
printSectionEl.style.visibility = 'hidden';
}
if (noPrintSectionEl) {
noPrintSectionEl.style.display = 'block';
noPrintSectionEl.style.visibility = 'visible';
}
console.log('Edit cancelled, print section hidden');
}
// Update QZ Tray status display
function updateQZStatus(message, status = 'info') {
const statusEl = document.getElementById('qz-status');
if (statusEl) {
statusEl.textContent = 'QZ Tray: ' + message;
statusEl.className = status === 'success' ? 'text-success fw-bold' :
status === 'warning' ? 'text-warning' : 'text-muted';
}
}
// Test QZ Tray connection
function testQZTrayConnection() {
if (window.qzPrinter && window.qzPrinter.connected) {
const printers = window.qzPrinter.availablePrinters;
const printerList = printers.length > 0
? printers.join('\n• ')
: 'No printers found';
alert('✅ QZ Tray is connected and ready!\n\nAvailable printers:\n• ' + printerList);
} else {
alert('⚠️ QZ Tray is not connected.\nBrowser print will be used instead.');
}
}
// Initialize QZ Tray on page load
function initializeQZTray() {
// Use the shared qzPrinter module
if (window.qzPrinter) {
// Update status based on connection state
if (window.qzPrinter.connected) {
updateQZStatus('Connected ✅', 'success');
populatePrinterSelect();
} else {
updateQZStatus('Not Available (will use browser print)', 'warning');
}
}
}
// Populate printer select dropdown
function populatePrinterSelect() {
if (!window.qzPrinter || !window.qzPrinter.availablePrinters.length) {
return;
}
const printerSelect = document.getElementById('printer-select');
if (!printerSelect) return;
// Clear existing options except default
printerSelect.innerHTML = '<option value="">Default Printer</option>';
// Add each printer
window.qzPrinter.availablePrinters.forEach(printer => {
const option = document.createElement('option');
option.value = printer;
option.textContent = printer;
if (printer === window.qzPrinter.selectedPrinter) {
option.selected = true;
}
printerSelect.appendChild(option);
});
}
// Import CSV
function importCSV() {
const fileInput = document.getElementById('csvFile');
if (!fileInput.files.length) {
alert('Please select a CSV file');
return;
}
const file = fileInput.files[0];
const reader = new FileReader();
reader.onload = function(e) {
const csv = e.target.result;
const lines = csv.trim().split('\n');
if (lines.length === 0) {
alert('CSV file is empty');
return;
}
let successCount = 0;
let errorCount = 0;
// Process each line
lines.forEach((line, index) => {
const parts = line.split(',').map(p => p.trim());
if (parts.length >= 1 && parts[0]) {
// Submit each location via AJAX
const locationCode = parts[0];
const size = parts[1] || '';
const description = parts[2] || '';
// Here you would submit via AJAX to add each location
successCount++;
}
});
alert(`Import complete!\nSuccessfully imported: ${successCount}\nErrors: ${errorCount}`);
};
reader.readAsText(file);
}
// Initialize
document.addEventListener('DOMContentLoaded', function() {
updateDeleteBtn();
// Initialize QZ Tray (will use the shared module)
setTimeout(() => {
initializeQZTray();
}, 500); // Wait for qzPrinter module to initialize
// Handle printer selection change
const printerSelect = document.getElementById('printer-select');
if (printerSelect) {
printerSelect.addEventListener('change', function() {
if (window.qzPrinter) {
window.qzPrinter.selectPrinter(this.value);
}
});
}
// Add row click selection for print functionality
const locationRows = document.querySelectorAll('.location-row');
const printLocationCodeEl = document.getElementById('print_location_code');
const printSectionEl = document.getElementById('printSection');
const noPrintSectionEl = document.getElementById('noPrintSection');
locationRows.forEach(row => {
// Allow row to be clickable for selection (but not on checkbox click)
row.addEventListener('click', function(e) {
// Don't trigger on checkbox click
if (e.target.type === 'checkbox') return;
const locationCode = this.dataset.locationCode;
const locationId = this.dataset.locationId;
console.log('Row clicked:', { locationId, locationCode });
// Update print location code
if (printLocationCodeEl) {
printLocationCodeEl.textContent = locationCode;
}
// Show print section
if (printSectionEl) {
printSectionEl.style.display = 'block';
printSectionEl.style.visibility = 'visible';
}
if (noPrintSectionEl) {
noPrintSectionEl.style.display = 'none';
noPrintSectionEl.style.visibility = 'hidden';
}
// Highlight row as selected
locationRows.forEach(r => r.style.backgroundColor = '');
this.style.backgroundColor = '#e3f2fd';
console.log('Print section shown for:', locationCode);
});
});
});
</script>
<style>
.sticky-top {
z-index: 10;
}
.table-hover tbody tr:hover {
background-color: #f8f9fa;
}
.card {
border-radius: 8px;
}
.card-header {
border-radius: 8px 8px 0 0;
}
#locationsTable {
font-size: 0.95rem;
}
.btn-outline-primary:hover {
transform: scale(1.05);
transition: all 0.2s;
}
</style>
{% endblock %}

View File

@@ -0,0 +1,68 @@
{% extends "base.html" %}
{% block title %}Warehouse Reports - Quality App v2{% endblock %}
{% block content %}
<div class="container-fluid py-5">
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<div>
<h1 class="mb-2">
<i class="fas fa-chart-bar"></i> Warehouse Reports
</h1>
<p class="text-muted">View and export warehouse activity and inventory reports</p>
</div>
<a href="{{ url_for('warehouse.warehouse_index') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Back to Warehouse
</a>
</div>
</div>
</div>
<div class="row mb-4">
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="fas fa-list"></i> Inventory Report</h5>
</div>
<div class="card-body">
<p class="text-muted">Generate a comprehensive inventory report with location details.</p>
<button class="btn btn-primary btn-sm">
<i class="fas fa-download"></i> Generate Report
</button>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="fas fa-chart-pie"></i> Location Utilization</h5>
</div>
<div class="card-body">
<p class="text-muted">View storage location utilization and capacity statistics.</p>
<button class="btn btn-primary btn-sm">
<i class="fas fa-download"></i> Generate Report
</button>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-light">
<h5 class="mb-0"><i class="fas fa-history"></i> Activity Report</h5>
</div>
<div class="card-body">
<p class="text-muted">
<i class="fas fa-info-circle"></i> Warehouse reports feature coming soon...
</p>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,43 @@
{% extends "base.html" %}
{% block title %}Set Boxes Locations - Quality App v2{% endblock %}
{% block content %}
<div class="container-fluid py-5">
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<div>
<h1 class="mb-2">
<i class="fas fa-cube"></i> Set Boxes Locations
</h1>
<p class="text-muted">Add or update articles in the warehouse inventory</p>
</div>
<a href="{{ url_for('warehouse.warehouse_index') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Back to Warehouse
</a>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card shadow-sm">
<div class="card-header bg-light">
<div class="d-flex justify-content-between align-items-center">
<h5 class="mb-0"><i class="fas fa-list"></i> Articles List</h5>
<button class="btn btn-danger btn-sm" data-bs-toggle="modal" data-bs-target="#addArticleModal">
<i class="fas fa-plus"></i> Add Article
</button>
</div>
</div>
<div class="card-body">
<p class="text-muted">
<i class="fas fa-info-circle"></i> Articles management feature coming soon...
</p>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,147 @@
{% extends "base.html" %}
{% block content %}
<div class="container mt-5">
<div class="row">
<div class="col-md-6 mx-auto">
<div class="card">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">
<i class="fas fa-barcode me-2"></i>Barcode Printing Test
</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label for="locationCode" class="form-label">Location Code:</label>
<input type="text" class="form-control" id="locationCode" value="LOC-001" placeholder="Enter location code">
</div>
<button type="button" class="btn btn-success w-100" onclick="testPrintBarcode()">
<i class="fas fa-qrcode me-2"></i>Test Barcode Printing
</button>
<hr>
<h6>Generated Barcode Preview:</h6>
<div id="barcode" style="text-align: center; margin: 20px 0;"></div>
<div id="status" class="alert alert-info mt-3" style="display: none;"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Barcode Preview Modal -->
<div class="modal fade" id="barcodeModal" tabindex="-1">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header bg-secondary text-white">
<h5 class="modal-title">
<i class="fas fa-barcode me-2"></i>Barcode Preview
</h5>
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body text-center">
<p class="fw-bold mb-3" id="barcodeLocationCode">LOC-001</p>
<svg id="modalBarcode"></svg>
<p class="text-muted mt-3 small">Scan this barcode to identify the location</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-success" onclick="testPrintFromModal()">
<i class="fas fa-print me-2"></i>Print
</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/jsbarcode@3.11.5/dist/JsBarcode.all.min.js"></script>
<script>
function testPrintBarcode() {
const locationCode = document.getElementById('locationCode').value.trim();
if (!locationCode) {
showStatus('Please enter a location code', 'error');
return;
}
try {
showStatus('Generating barcode for: ' + locationCode, 'info');
// Clear previous barcode
document.getElementById('barcode').innerHTML = '';
// Generate barcode in preview
JsBarcode("#barcode", locationCode, {
format: "CODE128",
width: 2,
height: 100,
displayValue: true,
margin: 10
});
// Also generate in modal
document.getElementById('modalBarcode').innerHTML = '';
document.getElementById('barcodeLocationCode').textContent = locationCode;
JsBarcode("#modalBarcode", locationCode, {
format: "CODE128",
width: 2,
height: 100,
displayValue: true,
margin: 10
});
showStatus('✓ Barcode generated successfully! Click "Test Barcode Printing" button to open preview modal.', 'success');
} catch (error) {
showStatus('Error: ' + error.message, 'error');
console.error('Barcode generation error:', error);
}
}
function testPrintFromModal() {
const locationCode = document.getElementById('barcodeLocationCode').textContent;
showStatus('Print dialog would open here for: ' + locationCode, 'info');
// Open browser print dialog
const printWindow = window.open('', '', 'height=400,width=600');
const barcodeSvg = document.getElementById('modalBarcode').innerHTML;
printWindow.document.write('<html><head><title>Print Location Barcode</title>');
printWindow.document.write('<style>');
printWindow.document.write('body { font-family: Arial, sans-serif; text-align: center; padding: 20px; }');
printWindow.document.write('h2 { margin-bottom: 30px; }');
printWindow.document.write('svg { max-width: 100%; height: auto; margin: 20px 0; }');
printWindow.document.write('</style></head><body>');
printWindow.document.write('<h2>Location: ' + locationCode + '</h2>');
printWindow.document.write('<svg id="barcode">' + barcodeSvg + '</svg>');
printWindow.document.write('<p style="margin-top: 40px; font-size: 14px; color: #666;">');
printWindow.document.write('Printed on: ' + new Date().toLocaleString());
printWindow.document.write('</p></body></html>');
printWindow.document.close();
setTimeout(function() {
printWindow.print();
}, 250);
}
function showStatus(message, type) {
const statusDiv = document.getElementById('status');
statusDiv.className = 'alert alert-' + (type === 'error' ? 'danger' : type === 'success' ? 'success' : 'info');
statusDiv.textContent = message;
statusDiv.style.display = 'block';
}
// Show modal on button click (after testing)
document.addEventListener('DOMContentLoaded', function() {
// Test automatically generate a barcode on page load
document.getElementById('locationCode').addEventListener('change', function() {
testPrintBarcode();
});
});
</script>
{% endblock %}