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:
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
159
app/templates/modules/settings/logs_explorer.html
Normal file
159
app/templates/modules/settings/logs_explorer.html
Normal 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 %}
|
||||
142
app/templates/modules/settings/search_logs.html
Normal file
142
app/templates/modules/settings/search_logs.html
Normal 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 %}
|
||||
@@ -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">
|
||||
|
||||
87
app/templates/modules/settings/view_log.html
Normal file
87
app/templates/modules/settings/view_log.html
Normal 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 %}
|
||||
679
app/templates/modules/warehouse/boxes.html
Normal file
679
app/templates/modules/warehouse/boxes.html
Normal 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 %}
|
||||
151
app/templates/modules/warehouse/index.html
Normal file
151
app/templates/modules/warehouse/index.html
Normal 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 %}
|
||||
67
app/templates/modules/warehouse/inventory.html
Normal file
67
app/templates/modules/warehouse/inventory.html
Normal 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 %}
|
||||
848
app/templates/modules/warehouse/locations.html
Normal file
848
app/templates/modules/warehouse/locations.html
Normal 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 %}
|
||||
68
app/templates/modules/warehouse/reports.html
Normal file
68
app/templates/modules/warehouse/reports.html
Normal 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 %}
|
||||
43
app/templates/modules/warehouse/set_boxes_locations.html
Normal file
43
app/templates/modules/warehouse/set_boxes_locations.html
Normal 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 %}
|
||||
147
app/templates/modules/warehouse/test_barcode.html
Normal file
147
app/templates/modules/warehouse/test_barcode.html
Normal 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 %}
|
||||
Reference in New Issue
Block a user