Compare commits
5 Commits
docker_upd
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a3e26d86d | ||
|
|
1e5572a5e9 | ||
|
|
c7f5203aa7 | ||
|
|
f8209e0e0a | ||
|
|
5905c693e6 |
@@ -14,7 +14,7 @@ def create_app():
|
|||||||
from app.routes import bp as main_bp, warehouse_bp
|
from app.routes import bp as main_bp, warehouse_bp
|
||||||
from app.daily_mirror import daily_mirror_bp
|
from app.daily_mirror import daily_mirror_bp
|
||||||
app.register_blueprint(main_bp, url_prefix='/')
|
app.register_blueprint(main_bp, url_prefix='/')
|
||||||
app.register_blueprint(warehouse_bp)
|
app.register_blueprint(warehouse_bp, url_prefix='/warehouse')
|
||||||
app.register_blueprint(daily_mirror_bp)
|
app.register_blueprint(daily_mirror_bp)
|
||||||
|
|
||||||
# Add 'now' function to Jinja2 globals
|
# Add 'now' function to Jinja2 globals
|
||||||
|
|||||||
@@ -184,6 +184,108 @@ def create_warehouse_locations_table():
|
|||||||
print_error(f"Failed to create warehouse_locations table: {e}")
|
print_error(f"Failed to create warehouse_locations table: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def create_boxes_crates_table():
|
||||||
|
"""Create boxes_crates table for tracking boxes containing scanned orders"""
|
||||||
|
print_step(5, "Creating Boxes/Crates Table")
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = mariadb.connect(**DB_CONFIG)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
boxes_query = """
|
||||||
|
CREATE TABLE IF NOT EXISTS boxes_crates (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
box_number VARCHAR(8) NOT NULL UNIQUE,
|
||||||
|
status ENUM('open', 'closed') DEFAULT 'open',
|
||||||
|
location_id BIGINT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
created_by VARCHAR(100),
|
||||||
|
FOREIGN KEY (location_id) REFERENCES warehouse_locations(id) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
cursor.execute(boxes_query)
|
||||||
|
print_success("Table 'boxes_crates' created successfully")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print_error(f"Failed to create boxes_crates table: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def create_box_contents_table():
|
||||||
|
"""Create box_contents table for tracking CP codes in boxes"""
|
||||||
|
print_step(6, "Creating Box Contents Tracking Table")
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = mariadb.connect(**DB_CONFIG)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
box_contents_query = """
|
||||||
|
CREATE TABLE IF NOT EXISTS box_contents (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
box_id BIGINT NOT NULL,
|
||||||
|
cp_code VARCHAR(15) NOT NULL,
|
||||||
|
scan_id BIGINT,
|
||||||
|
scanned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
scanned_by VARCHAR(100),
|
||||||
|
FOREIGN KEY (box_id) REFERENCES boxes_crates(id) ON DELETE CASCADE,
|
||||||
|
INDEX idx_box_id (box_id),
|
||||||
|
INDEX idx_cp_code (cp_code)
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
cursor.execute(box_contents_query)
|
||||||
|
print_success("Table 'box_contents' created successfully")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print_error(f"Failed to create box_contents table: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def create_location_contents_table():
|
||||||
|
"""Create location_contents table for tracking boxes in locations"""
|
||||||
|
print_step(7, "Creating Location Contents Tracking Table")
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = mariadb.connect(**DB_CONFIG)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
location_contents_query = """
|
||||||
|
CREATE TABLE IF NOT EXISTS location_contents (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
location_id BIGINT NOT NULL,
|
||||||
|
box_id BIGINT NOT NULL,
|
||||||
|
placed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
placed_by VARCHAR(100),
|
||||||
|
removed_at TIMESTAMP NULL,
|
||||||
|
removed_by VARCHAR(100),
|
||||||
|
status ENUM('active', 'removed') DEFAULT 'active',
|
||||||
|
FOREIGN KEY (location_id) REFERENCES warehouse_locations(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (box_id) REFERENCES boxes_crates(id) ON DELETE CASCADE,
|
||||||
|
INDEX idx_location_id (location_id),
|
||||||
|
INDEX idx_box_id (box_id),
|
||||||
|
INDEX idx_status (status)
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
cursor.execute(location_contents_query)
|
||||||
|
print_success("Table 'location_contents' created successfully")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
return True
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print_error(f"Failed to create location_contents table: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
def create_permissions_tables():
|
def create_permissions_tables():
|
||||||
"""Create permission management tables"""
|
"""Create permission management tables"""
|
||||||
print_step(5, "Creating Permission Management Tables")
|
print_step(5, "Creating Permission Management Tables")
|
||||||
@@ -705,6 +807,9 @@ def main():
|
|||||||
create_scan_tables,
|
create_scan_tables,
|
||||||
create_order_for_labels_table,
|
create_order_for_labels_table,
|
||||||
create_warehouse_locations_table,
|
create_warehouse_locations_table,
|
||||||
|
create_boxes_crates_table,
|
||||||
|
create_box_contents_table,
|
||||||
|
create_location_contents_table,
|
||||||
create_permissions_tables,
|
create_permissions_tables,
|
||||||
create_users_table_mariadb,
|
create_users_table_mariadb,
|
||||||
# create_sqlite_tables, # Disabled - using MariaDB only
|
# create_sqlite_tables, # Disabled - using MariaDB only
|
||||||
|
|||||||
@@ -217,6 +217,74 @@ def get_db_connection():
|
|||||||
database=settings['database_name']
|
database=settings['database_name']
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def ensure_scanfg_orders_table():
|
||||||
|
"""Ensure scanfg_orders table exists with proper structure and trigger"""
|
||||||
|
try:
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Check if table exists
|
||||||
|
cursor.execute("SHOW TABLES LIKE 'scanfg_orders'")
|
||||||
|
if cursor.fetchone():
|
||||||
|
conn.close()
|
||||||
|
return # Table already exists
|
||||||
|
|
||||||
|
print("Creating scanfg_orders table...")
|
||||||
|
|
||||||
|
# Create table
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS scanfg_orders (
|
||||||
|
Id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
operator_code VARCHAR(50),
|
||||||
|
CP_base_code VARCHAR(10),
|
||||||
|
CP_full_code VARCHAR(15),
|
||||||
|
OC1_code VARCHAR(50),
|
||||||
|
OC2_code VARCHAR(50),
|
||||||
|
quality_code INT,
|
||||||
|
date DATE,
|
||||||
|
time TIME,
|
||||||
|
approved_quantity INT DEFAULT 0,
|
||||||
|
rejected_quantity INT DEFAULT 0,
|
||||||
|
INDEX idx_cp_base (CP_base_code),
|
||||||
|
INDEX idx_date (date),
|
||||||
|
INDEX idx_quality (quality_code)
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# Create trigger
|
||||||
|
cursor.execute("DROP TRIGGER IF EXISTS set_quantities_fg")
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TRIGGER set_quantities_fg
|
||||||
|
BEFORE INSERT ON scanfg_orders
|
||||||
|
FOR EACH ROW
|
||||||
|
BEGIN
|
||||||
|
SET @cp_base = SUBSTRING(NEW.CP_full_code, 1, 10);
|
||||||
|
SET @approved = (SELECT COUNT(*) FROM scanfg_orders
|
||||||
|
WHERE SUBSTRING(CP_full_code, 1, 10) = @cp_base
|
||||||
|
AND quality_code = 0);
|
||||||
|
SET @rejected = (SELECT COUNT(*) FROM scanfg_orders
|
||||||
|
WHERE SUBSTRING(CP_full_code, 1, 10) = @cp_base
|
||||||
|
AND quality_code != 0);
|
||||||
|
|
||||||
|
IF NEW.quality_code = 0 THEN
|
||||||
|
SET NEW.approved_quantity = @approved + 1;
|
||||||
|
SET NEW.rejected_quantity = @rejected;
|
||||||
|
ELSE
|
||||||
|
SET NEW.approved_quantity = @approved;
|
||||||
|
SET NEW.rejected_quantity = @rejected + 1;
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
SET NEW.CP_base_code = @cp_base;
|
||||||
|
END
|
||||||
|
""")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
print("✅ scanfg_orders table and trigger created successfully")
|
||||||
|
|
||||||
|
except mariadb.Error as e:
|
||||||
|
print(f"Error creating scanfg_orders table: {e}")
|
||||||
|
|
||||||
@bp.route('/dashboard')
|
@bp.route('/dashboard')
|
||||||
def dashboard():
|
def dashboard():
|
||||||
print("Session user:", session.get('user'), session.get('role'))
|
print("Session user:", session.get('user'), session.get('role'))
|
||||||
@@ -589,6 +657,8 @@ def logout():
|
|||||||
@bp.route('/fg_scan', methods=['GET', 'POST'])
|
@bp.route('/fg_scan', methods=['GET', 'POST'])
|
||||||
@requires_quality_module
|
@requires_quality_module
|
||||||
def fg_scan():
|
def fg_scan():
|
||||||
|
# Ensure scanfg_orders table exists
|
||||||
|
ensure_scanfg_orders_table()
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
# Handle form submission
|
# Handle form submission
|
||||||
@@ -637,6 +707,14 @@ def fg_scan():
|
|||||||
except mariadb.Error as e:
|
except mariadb.Error as e:
|
||||||
print(f"Error saving finish goods scan data: {e}")
|
print(f"Error saving finish goods scan data: {e}")
|
||||||
flash(f"Error saving scan data: {e}")
|
flash(f"Error saving scan data: {e}")
|
||||||
|
|
||||||
|
# Check if this is an AJAX request (for scan-to-boxes feature)
|
||||||
|
if request.headers.get('X-Requested-With') == 'XMLHttpRequest' or request.accept_mimetypes.best == 'application/json':
|
||||||
|
# For AJAX requests, return JSON response without redirect
|
||||||
|
return jsonify({'success': True, 'message': 'Scan recorded successfully'})
|
||||||
|
|
||||||
|
# For normal form submissions, redirect to prevent form resubmission (POST-Redirect-GET pattern)
|
||||||
|
return redirect(url_for('main.fg_scan'))
|
||||||
|
|
||||||
# Fetch the latest scan data for display from scanfg_orders
|
# Fetch the latest scan data for display from scanfg_orders
|
||||||
scan_data = []
|
scan_data = []
|
||||||
@@ -1336,7 +1414,7 @@ def get_fg_report_data():
|
|||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT Id, operator_code, CP_base_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time, approved_quantity, rejected_quantity
|
SELECT Id, operator_code, CP_base_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time, approved_quantity, rejected_quantity
|
||||||
FROM scanfg_orders
|
FROM scanfg_orders
|
||||||
WHERE date = ?
|
WHERE date = %s
|
||||||
ORDER BY date DESC, time DESC
|
ORDER BY date DESC, time DESC
|
||||||
""", (today,))
|
""", (today,))
|
||||||
rows = cursor.fetchall()
|
rows = cursor.fetchall()
|
||||||
@@ -1351,7 +1429,7 @@ def get_fg_report_data():
|
|||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT Id, operator_code, CP_base_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time, approved_quantity, rejected_quantity
|
SELECT Id, operator_code, CP_base_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time, approved_quantity, rejected_quantity
|
||||||
FROM scanfg_orders
|
FROM scanfg_orders
|
||||||
WHERE date >= ?
|
WHERE date >= %s
|
||||||
ORDER BY date DESC, time DESC
|
ORDER BY date DESC, time DESC
|
||||||
""", (start_date,))
|
""", (start_date,))
|
||||||
rows = cursor.fetchall()
|
rows = cursor.fetchall()
|
||||||
@@ -1365,7 +1443,7 @@ def get_fg_report_data():
|
|||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT Id, operator_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time, approved_quantity, rejected_quantity
|
SELECT Id, operator_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time, approved_quantity, rejected_quantity
|
||||||
FROM scanfg_orders
|
FROM scanfg_orders
|
||||||
WHERE date = ? AND quality_code != 0
|
WHERE date = %s AND quality_code != 0
|
||||||
ORDER BY date DESC, time DESC
|
ORDER BY date DESC, time DESC
|
||||||
""", (today,))
|
""", (today,))
|
||||||
rows = cursor.fetchall()
|
rows = cursor.fetchall()
|
||||||
@@ -1380,7 +1458,7 @@ def get_fg_report_data():
|
|||||||
cursor.execute("""
|
cursor.execute("""
|
||||||
SELECT Id, operator_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time, approved_quantity, rejected_quantity
|
SELECT Id, operator_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time, approved_quantity, rejected_quantity
|
||||||
FROM scanfg_orders
|
FROM scanfg_orders
|
||||||
WHERE date >= ? AND quality_code != 0
|
WHERE date >= %s AND quality_code != 0
|
||||||
ORDER BY date DESC, time DESC
|
ORDER BY date DESC, time DESC
|
||||||
""", (start_date,))
|
""", (start_date,))
|
||||||
rows = cursor.fetchall()
|
rows = cursor.fetchall()
|
||||||
@@ -3878,6 +3956,7 @@ def mark_printed():
|
|||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
@warehouse_bp.route('/create_locations', methods=['GET', 'POST'])
|
@warehouse_bp.route('/create_locations', methods=['GET', 'POST'])
|
||||||
|
@requires_warehouse_module
|
||||||
def create_locations():
|
def create_locations():
|
||||||
from app.warehouse import create_locations_handler
|
from app.warehouse import create_locations_handler
|
||||||
return create_locations_handler()
|
return create_locations_handler()
|
||||||
@@ -3939,6 +4018,27 @@ def delete_location():
|
|||||||
return jsonify({'success': False, 'error': str(e)})
|
return jsonify({'success': False, 'error': str(e)})
|
||||||
|
|
||||||
|
|
||||||
|
@warehouse_bp.route('/manage_boxes', methods=['GET', 'POST'])
|
||||||
|
@requires_warehouse_module
|
||||||
|
def manage_boxes():
|
||||||
|
from app.warehouse import manage_boxes_handler
|
||||||
|
return manage_boxes_handler()
|
||||||
|
|
||||||
|
|
||||||
|
@warehouse_bp.route('/assign_cp_to_box', methods=['POST'])
|
||||||
|
@requires_warehouse_module
|
||||||
|
def assign_cp_to_box():
|
||||||
|
from app.warehouse import assign_cp_to_box_handler
|
||||||
|
return assign_cp_to_box_handler()
|
||||||
|
|
||||||
|
|
||||||
|
@warehouse_bp.route('/inventory', methods=['GET'])
|
||||||
|
@requires_warehouse_module
|
||||||
|
def warehouse_inventory():
|
||||||
|
from app.warehouse import view_warehouse_inventory_handler
|
||||||
|
return view_warehouse_inventory_handler()
|
||||||
|
|
||||||
|
|
||||||
# Daily Mirror Route Redirects for Backward Compatibility
|
# Daily Mirror Route Redirects for Backward Compatibility
|
||||||
@bp.route('/daily_mirror_main')
|
@bp.route('/daily_mirror_main')
|
||||||
def daily_mirror_main_route():
|
def daily_mirror_main_route():
|
||||||
@@ -3976,19 +4076,18 @@ def api_daily_mirror_history_data():
|
|||||||
# Help/Documentation Routes
|
# Help/Documentation Routes
|
||||||
@bp.route('/help')
|
@bp.route('/help')
|
||||||
@bp.route('/help/<page>')
|
@bp.route('/help/<page>')
|
||||||
def help(page='index'):
|
def help(page='dashboard'):
|
||||||
"""Display help documentation from Markdown files"""
|
"""Display help documentation from Markdown files"""
|
||||||
import markdown
|
import markdown
|
||||||
import os
|
import os
|
||||||
|
|
||||||
# Map page names to markdown files
|
# Map page names to markdown files
|
||||||
doc_files = {
|
doc_files = {
|
||||||
'index': 'index.md',
|
|
||||||
'dashboard': 'dashboard.md',
|
'dashboard': 'dashboard.md',
|
||||||
'print_module': 'print_module.md',
|
'print_module': 'print_module.md',
|
||||||
'upload_data': 'upload_data.md',
|
'print_lost_labels': 'print_lost_labels.md',
|
||||||
'view_orders': 'view_orders.md',
|
'daily_mirror': 'daily_mirror.md',
|
||||||
'print_lost_labels': 'print_lost_labels.md'
|
'etichete': 'etichete.md'
|
||||||
}
|
}
|
||||||
|
|
||||||
# Get the markdown file path
|
# Get the markdown file path
|
||||||
@@ -3996,7 +4095,7 @@ def help(page='index'):
|
|||||||
return render_template('docs/help_viewer.html',
|
return render_template('docs/help_viewer.html',
|
||||||
error=f"Documentația pentru '{page}' nu a fost găsită.")
|
error=f"Documentația pentru '{page}' nu a fost găsită.")
|
||||||
|
|
||||||
doc_path = os.path.join(current_app.static_folder, 'docs', doc_files[page])
|
doc_path = os.path.join(current_app.root_path, 'static', 'docs', doc_files[page])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Read and convert markdown to HTML
|
# Read and convert markdown to HTML
|
||||||
|
|||||||
297
py_app/app/static/docs/daily_mirror.md
Normal file
297
py_app/app/static/docs/daily_mirror.md
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
# Daily Mirror - Ghid de Utilizare
|
||||||
|
|
||||||
|
## Prezentare Generală
|
||||||
|
|
||||||
|
Daily Mirror este modulul de raportare și analiză a producției care oferă o vedere de ansamblu completă asupra activităților zilnice, performanței operatorilor și indicatorilor cheie de producție.
|
||||||
|
|
||||||
|
## Cum să Accesați Daily Mirror
|
||||||
|
|
||||||
|
1. Din dashboard, accesați **Daily Mirror** din meniul lateral
|
||||||
|
2. Sau folosiți butonul de acces rapid "Daily Mirror Dashboard"
|
||||||
|
|
||||||
|
**Notă**: Trebuie să aveți permisiuni pentru modulul Daily Mirror pentru a-l accesa.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Secțiuni Principale
|
||||||
|
|
||||||
|
### 1. Dashboard Principal (Daily Mirror Main)
|
||||||
|
|
||||||
|
Ecranul principal oferă o vedere de ansamblu zilnică cu:
|
||||||
|
|
||||||
|
#### Indicatori Cheie (KPIs)
|
||||||
|
|
||||||
|
- **📦 Comenzi Totale**: Numărul total de comenzi procesate în ziua curentă
|
||||||
|
- **🚀 Producție Lansată**: Cantitatea de produse lansate în producție
|
||||||
|
- **✅ Producție Finalizată**: Cantitatea de produse finalizate
|
||||||
|
- **📤 Comenzi Livrate**: Numărul de comenzi livrate către clienți
|
||||||
|
- **👥 Operatori Activi**: Numărul de operatori care au lucrat în ziua respectivă
|
||||||
|
|
||||||
|
#### Grafice Interactive
|
||||||
|
|
||||||
|
**Grafic Producție pe Oră**
|
||||||
|
- Vizualizează volumul de producție pe parcursul zilei
|
||||||
|
- Identifică orele de vârf de producție
|
||||||
|
- Monitorizează tendințele de performanță
|
||||||
|
|
||||||
|
**Grafic Eficiență Producție**
|
||||||
|
- Rata de lansare în producție
|
||||||
|
- Rata de finalizare
|
||||||
|
- Rata de livrare
|
||||||
|
- Comparație cu obiectivele stabilite
|
||||||
|
|
||||||
|
**Grafic Performanță Operatori**
|
||||||
|
- Top operatori după cantitate procesată
|
||||||
|
- Analiza productivității individuale
|
||||||
|
- Identificarea operatorilor cu performanță ridicată
|
||||||
|
|
||||||
|
### 2. Istoric Producție (Daily Mirror History)
|
||||||
|
|
||||||
|
Această secțiune permite analiza datelor istorice:
|
||||||
|
|
||||||
|
#### Filtrare Date
|
||||||
|
|
||||||
|
1. **Selectați Perioada**:
|
||||||
|
- Utilizați selectorii de dată pentru început și sfârșit
|
||||||
|
- Opțiuni rapide: Astăzi, Ultima săptămână, Luna curentă
|
||||||
|
|
||||||
|
2. **Filtre Avansate**:
|
||||||
|
- Filtrare după operator
|
||||||
|
- Filtrare după comandă
|
||||||
|
- Filtrare după client
|
||||||
|
- Filtrare după status
|
||||||
|
|
||||||
|
#### Vizualizare Tendințe
|
||||||
|
|
||||||
|
- **Grafic Trend Producție**: Vezi evoluția producției în timp
|
||||||
|
- **Comparație Perioade**: Compară performanța între diferite perioade
|
||||||
|
- **Analiza Sezonieră**: Identifică fluctuații sezoniere
|
||||||
|
|
||||||
|
#### Export Date
|
||||||
|
|
||||||
|
- **Export Excel**: Descarcă rapoarte complete în format Excel
|
||||||
|
- **Export PDF**: Generează rapoarte printabile
|
||||||
|
- **Export CSV**: Exportă date brute pentru analiză
|
||||||
|
|
||||||
|
### 3. Construire Bază de Date (Build Database)
|
||||||
|
|
||||||
|
Funcție administrativă pentru gestionarea datelor:
|
||||||
|
|
||||||
|
#### Când să Utilizați
|
||||||
|
|
||||||
|
- Dacă datele nu se afișează corect
|
||||||
|
- După o resetare de sistem
|
||||||
|
- Pentru rebuild complet al datelor de raportare
|
||||||
|
|
||||||
|
#### Cum să Utilizați
|
||||||
|
|
||||||
|
1. Accesați secțiunea "Build Database"
|
||||||
|
2. Selectați tipul de rebuild:
|
||||||
|
- **Rebuild Complet**: Reconstruiește toate datele
|
||||||
|
- **Rebuild Incremental**: Actualizează doar datele noi
|
||||||
|
3. Confirmați acțiunea
|
||||||
|
4. Așteptați finalizarea procesului (poate dura câteva minute)
|
||||||
|
|
||||||
|
**⚠️ ATENȚIE**: Această operațiune poate dura timp îndelungat pentru volume mari de date.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Interpretarea Datelor
|
||||||
|
|
||||||
|
### Indicatori de Performanță
|
||||||
|
|
||||||
|
#### Rata de Lansare
|
||||||
|
```
|
||||||
|
Rata de Lansare = (Producție Lansată / Comenzi Totale) × 100%
|
||||||
|
```
|
||||||
|
- **Verde (>90%)**: Performanță excelentă
|
||||||
|
- **Galben (70-90%)**: Performanță acceptabilă
|
||||||
|
- **Roșu (<70%)**: Necesită atenție
|
||||||
|
|
||||||
|
#### Rata de Finalizare
|
||||||
|
```
|
||||||
|
Rata de Finalizare = (Producție Finalizată / Producție Lansată) × 100%
|
||||||
|
```
|
||||||
|
- **Verde (>85%)**: Eficiență ridicată
|
||||||
|
- **Galben (60-85%)**: Necesită îmbunătățiri
|
||||||
|
- **Roșu (<60%)**: Probleme semnificative
|
||||||
|
|
||||||
|
#### Rata de Livrare
|
||||||
|
```
|
||||||
|
Rata de Livrare = (Comenzi Livrate / Producție Finalizată) × 100%
|
||||||
|
```
|
||||||
|
- **Verde (>95%)**: Livrări la timp
|
||||||
|
- **Galben (80-95%)**: Întârzieri minore
|
||||||
|
- **Roșu (<80%)**: Probleme de livrare
|
||||||
|
|
||||||
|
### Cod Culori pentru Grafice
|
||||||
|
|
||||||
|
- 🟦 **Albastru**: Comenzi procesate
|
||||||
|
- 🟩 **Verde**: Producție finalizată
|
||||||
|
- 🟨 **Galben**: Producție în curs
|
||||||
|
- 🟥 **Roșu**: Comenzi cu probleme
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Funcții Avansate
|
||||||
|
|
||||||
|
### 1. Alerte Automate
|
||||||
|
|
||||||
|
Sistemul generează alerte automate pentru:
|
||||||
|
|
||||||
|
- ⚠️ **Scăderea producției**: Când producția scade sub pragul normal
|
||||||
|
- ⚠️ **Întârzieri**: Când comenzile depășesc termenul de livrare
|
||||||
|
- ⚠️ **Calitate**: Când rata de reject crește
|
||||||
|
- ⚠️ **Operatori**: Când performanța scade semnificativ
|
||||||
|
|
||||||
|
### 2. Comparație cu Obiectivele
|
||||||
|
|
||||||
|
Setați obiective zilnice/lunare și monitorizați:
|
||||||
|
|
||||||
|
- Progresul față de obiectiv în timp real
|
||||||
|
- Estimări pentru îndeplinirea obiectivelor
|
||||||
|
- Sugestii pentru îmbunătățire
|
||||||
|
|
||||||
|
### 3. Rapoarte Programate
|
||||||
|
|
||||||
|
Configurați rapoarte automate:
|
||||||
|
|
||||||
|
1. Accesați Settings > Daily Mirror Settings
|
||||||
|
2. Selectați "Scheduled Reports"
|
||||||
|
3. Configurați:
|
||||||
|
- Frecvența (zilnic, săptămânal, lunar)
|
||||||
|
- Destinatarii email
|
||||||
|
- Tipul de raport
|
||||||
|
- Format (PDF/Excel)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### Pentru Operatori
|
||||||
|
|
||||||
|
✅ **Actualizați statusul comenzilor prompt**
|
||||||
|
- Marcați comenzile ca "În producție" imediat ce începeți lucrul
|
||||||
|
- Actualizați progresul regulat
|
||||||
|
- Marcați finalizarea imediat după terminare
|
||||||
|
|
||||||
|
✅ **Verificați datele introduse**
|
||||||
|
- Asigurați-vă că cantitățile sunt corecte
|
||||||
|
- Verificați codurile de produs
|
||||||
|
- Confirmați datele clientului
|
||||||
|
|
||||||
|
### Pentru Manageri
|
||||||
|
|
||||||
|
✅ **Monitorizați dashboard-ul zilnic**
|
||||||
|
- Verificați indicatorii cheie dimineața
|
||||||
|
- Monitorizați progresul pe parcursul zilei
|
||||||
|
- Analizați rezultatele la sfârșitul zilei
|
||||||
|
|
||||||
|
✅ **Analizați tendințele săptămânal**
|
||||||
|
- Identificați pattern-uri de performanță
|
||||||
|
- Ajustați planificarea producției
|
||||||
|
- Oferiți feedback operatorilor
|
||||||
|
|
||||||
|
✅ **Generați rapoarte lunar**
|
||||||
|
- Raport complet de performanță
|
||||||
|
- Comparație cu lunile anterioare
|
||||||
|
- Planificare pentru luna următoare
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rezolvare Probleme
|
||||||
|
|
||||||
|
### Problema: Date Lipsă pe Dashboard
|
||||||
|
|
||||||
|
**Soluții**:
|
||||||
|
1. ✓ Verificați conexiunea la internet
|
||||||
|
2. ✓ Reîncărcați pagina (F5)
|
||||||
|
3. ✓ Verificați dacă datele au fost introduse corect
|
||||||
|
4. ✓ Rulați "Build Database" pentru a regenera datele
|
||||||
|
5. ✓ Contactați administratorul dacă problema persistă
|
||||||
|
|
||||||
|
### Problema: Graficele Nu Se Încarcă
|
||||||
|
|
||||||
|
**Soluții**:
|
||||||
|
1. ✓ Curățați cache-ul browserului (CTRL+SHIFT+DEL)
|
||||||
|
2. ✓ Verificați conexiunea la internet
|
||||||
|
3. ✓ Încercați un alt browser
|
||||||
|
4. ✓ Dezactivați extensiile de browser temporar
|
||||||
|
5. ✓ Verificați firewall-ul companiei
|
||||||
|
|
||||||
|
### Problema: Export-ul Nu Funcționează
|
||||||
|
|
||||||
|
**Soluții**:
|
||||||
|
1. ✓ Verificați permisiunile de descărcare în browser
|
||||||
|
2. ✓ Asigurați-vă că nu blocați pop-up-uri
|
||||||
|
3. ✓ Verificați spațiul disponibil pe disc
|
||||||
|
4. ✓ Încercați alt format de export (Excel → PDF)
|
||||||
|
|
||||||
|
### Problema: Cifre Incorecte în Rapoarte
|
||||||
|
|
||||||
|
**Soluții**:
|
||||||
|
1. ✓ Verificați perioada selectată
|
||||||
|
2. ✓ Confirmați filtrele aplicate
|
||||||
|
3. ✓ Rulați "Build Database" pentru refresh
|
||||||
|
4. ✓ Verificați datele sursă în modulul de comenzi
|
||||||
|
5. ✓ Contactați administratorul pentru verificare
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tips & Tricks
|
||||||
|
|
||||||
|
💡 **Sfat**: Folosiți filtrele rapide pentru a accesa rapid date specifice (Astăzi, Săptămâna asta, Luna asta).
|
||||||
|
|
||||||
|
💡 **Sfat**: Exportați rapoartele zilnice la sfârșitul fiecărei zile pentru evidență.
|
||||||
|
|
||||||
|
💡 **Sfat**: Setați alerte personalizate pentru indicatorii care vă interesează cel mai mult.
|
||||||
|
|
||||||
|
💡 **Sfat**: Folosiți graficele interactive - click pe elemente pentru detalii suplimentare.
|
||||||
|
|
||||||
|
💡 **Sfat**: Adăugați Daily Mirror la favorite în browser pentru acces rapid.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Întrebări Frecvente
|
||||||
|
|
||||||
|
**Î: Cât de des se actualizează datele?**
|
||||||
|
R: Datele se actualizează în timp real. Dashboard-ul se reîmprospătează automat la fiecare 30 de secunde.
|
||||||
|
|
||||||
|
**Î: Pot vedea date de acum o lună?**
|
||||||
|
R: Da, folosiți secțiunea "Daily Mirror History" și selectați perioada dorită.
|
||||||
|
|
||||||
|
**Î: Cum export datele pentru mai multe luni?**
|
||||||
|
R: Selectați perioada dorită în History și folosiți butonul "Export Excel" pentru export complet.
|
||||||
|
|
||||||
|
**Î: Pot compara performanța între operatori?**
|
||||||
|
R: Da, graficul "Performanță Operatori" afișează comparație directă. Pentru analiză detaliată, exportați datele în Excel.
|
||||||
|
|
||||||
|
**Î: Ce fac dacă datele nu corespund cu realitatea?**
|
||||||
|
R: Rulați "Build Database" pentru a regenera datele. Dacă problema persistă, contactați administratorul.
|
||||||
|
|
||||||
|
**Î: Pot seta alerte personalizate?**
|
||||||
|
R: Da, accesați Settings > Daily Mirror > Alerts și configurați alertele dorite.
|
||||||
|
|
||||||
|
**Î: Cum printez un raport?**
|
||||||
|
R: Exportați raportul ca PDF și apoi printați folosind opțiunile standard ale browserului (CTRL+P).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Suport Tehnic
|
||||||
|
|
||||||
|
Pentru asistență suplimentară sau raportarea problemelor:
|
||||||
|
|
||||||
|
- 📧 Email: support@quality-app.com
|
||||||
|
- 📞 Telefon: Contact administratorul de sistem
|
||||||
|
- 💬 Chat: Folosiți funcția de chat din aplicație (dacă este disponibilă)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Actualizări și Noutăți
|
||||||
|
|
||||||
|
**Versiunea 2.0 (Decembrie 2025)**
|
||||||
|
- ✨ Dashboard îmbunătățit cu grafice interactive
|
||||||
|
- ✨ Export în multiple formate (Excel, PDF, CSV)
|
||||||
|
- ✨ Alerte automate configurabile
|
||||||
|
- ✨ Analiză avansată a tendințelor
|
||||||
|
- ✨ Suport pentru mod întunecat (dark mode)
|
||||||
331
py_app/app/static/docs/etichete.md
Normal file
331
py_app/app/static/docs/etichete.md
Normal file
@@ -0,0 +1,331 @@
|
|||||||
|
# Modul Etichete - Ghid de Utilizare
|
||||||
|
|
||||||
|
## Prezentare Generală
|
||||||
|
|
||||||
|
Modulul Etichete este centrul de comandă pentru gestionarea completă a etichetelor de producție. Acesta oferă acces rapid la toate funcționalitățile necesare pentru procesarea comenzilor și generarea etichetelor.
|
||||||
|
|
||||||
|
## Cum să Accesați Modulul Etichete
|
||||||
|
|
||||||
|
1. Din dashboard principal, accesați **Etichete** din meniul lateral
|
||||||
|
2. Sau folosiți butonul de acces rapid "Labels Module"
|
||||||
|
|
||||||
|
**Notă**: Trebuie să aveți permisiuni pentru modulul de etichete pentru a-l accesa.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Secțiuni Principale
|
||||||
|
|
||||||
|
Modulul Etichete este organizat în carduri funcționale care oferă acces rapid la diferite operațiuni:
|
||||||
|
|
||||||
|
### 1. View Orders (Vizualizare Comenzi)
|
||||||
|
|
||||||
|
Această secțiune vă permite să gestionați comenzile și datele pentru etichete.
|
||||||
|
|
||||||
|
#### Upload Orders (Încărcare Comenzi)
|
||||||
|
|
||||||
|
**Descriere**: Încărcați comenzi noi în sistem din fișiere Excel sau CSV.
|
||||||
|
|
||||||
|
**Cum să utilizați**:
|
||||||
|
|
||||||
|
1. Click pe butonul **"Upload Orders"**
|
||||||
|
2. Selectați fișierul cu comenzile (format: `.xlsx`, `.xls`, sau `.csv`)
|
||||||
|
3. Verificați preview-ul datelor
|
||||||
|
4. Confirmați încărcarea
|
||||||
|
|
||||||
|
**Câmpuri Necesare în Fișier**:
|
||||||
|
- `comanda_productie` - Număr comandă de producție
|
||||||
|
- `cod_articol` - Cod articol produs
|
||||||
|
- `descr_com_prod` - Descriere comandă producție
|
||||||
|
- `cantitate` - Cantitate comandată
|
||||||
|
- `data_livrare` - Data livrare
|
||||||
|
- `customer_name` - Nume client
|
||||||
|
- `customer_article_number` - Număr articol client
|
||||||
|
|
||||||
|
**Formate Acceptate**:
|
||||||
|
- 📊 **Excel**: `.xlsx`, `.xls`
|
||||||
|
- 📄 **CSV**: `.csv` (UTF-8)
|
||||||
|
|
||||||
|
**Tips**:
|
||||||
|
- 💡 Verificați întotdeauna preview-ul înainte de confirmare
|
||||||
|
- 💡 Asigurați-vă că toate coloanele necesare sunt prezente
|
||||||
|
- 💡 Datele duplicate vor fi ignorate
|
||||||
|
|
||||||
|
#### View Orders (Vizualizare Comenzi)
|
||||||
|
|
||||||
|
**Descriere**: Vizualizați și gestionați comenzile existente în sistem.
|
||||||
|
|
||||||
|
**Funcționalități**:
|
||||||
|
- 📋 Lista completă a tuturor comenzilor
|
||||||
|
- 🔍 Căutare și filtrare avansată
|
||||||
|
- ✏️ Editare comenzi existente
|
||||||
|
- 🗑️ Ștergere comenzi
|
||||||
|
- 📊 Vizualizare status producție
|
||||||
|
|
||||||
|
**Filtre Disponibile**:
|
||||||
|
- Filtrare după număr comandă
|
||||||
|
- Filtrare după client
|
||||||
|
- Filtrare după data livrare
|
||||||
|
- Filtrare după status (activ/inactiv)
|
||||||
|
|
||||||
|
**Acțiuni Disponibile**:
|
||||||
|
- ✏️ **Edit**: Modificați detaliile comenzii
|
||||||
|
- 🖨️ **Print**: Generați etichete pentru comandă
|
||||||
|
- 🗑️ **Delete**: Ștergeți comanda din sistem
|
||||||
|
- 👁️ **View**: Vedeți detalii complete
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. Printing Module (Modul de Printare)
|
||||||
|
|
||||||
|
Centrul pentru toate operațiunile de printare a etichetelor.
|
||||||
|
|
||||||
|
#### Launch Printing Module
|
||||||
|
|
||||||
|
**Descriere**: Deschide modulul principal de printare pentru generarea etichetelor standard.
|
||||||
|
|
||||||
|
**Când să utilizați**:
|
||||||
|
- Pentru printarea etichetelor pentru comenzi noi
|
||||||
|
- Pentru reprintarea etichetelor existente
|
||||||
|
- Pentru generarea etichetelor în lot
|
||||||
|
|
||||||
|
**Pași de utilizare**:
|
||||||
|
|
||||||
|
1. **Click pe "Launch Printing Module"**
|
||||||
|
2. **Selectați comanda** din lista disponibilă
|
||||||
|
3. **Configurați opțiunile de printare**:
|
||||||
|
- Selectați produsele
|
||||||
|
- Specificați cantitățile
|
||||||
|
- Alegeți formatul etichetelor
|
||||||
|
4. **Generați etichetele**
|
||||||
|
5. **Printați** automat sau descărcați PDF
|
||||||
|
|
||||||
|
**Opțiuni de Printare**:
|
||||||
|
- 🖨️ **Printare Automată**: Trimite direct la imprimantă
|
||||||
|
- 💾 **Download PDF**: Salvează ca PDF pentru printare ulterioară
|
||||||
|
- 📄 **Preview**: Vizualizează înainte de printare
|
||||||
|
|
||||||
|
#### Launch Lost Labels Printing Module
|
||||||
|
|
||||||
|
**Descriere**: Modul special pentru reprintarea etichetelor pierdute sau deteriorate.
|
||||||
|
|
||||||
|
**Când să utilizați**:
|
||||||
|
- Etichete pierdute în producție
|
||||||
|
- Etichete deteriorate sau ilizibile
|
||||||
|
- Etichete printate incorect
|
||||||
|
|
||||||
|
**Caracteristici Speciale**:
|
||||||
|
- 🔍 Căutare rapidă după număr comandă
|
||||||
|
- 🎯 Selectare precisă a etichetelor necesare
|
||||||
|
- 📝 Tracking pentru etichete reprinate
|
||||||
|
- ⚠️ Alertă pentru printări multiple
|
||||||
|
|
||||||
|
**Pași de utilizare**:
|
||||||
|
|
||||||
|
1. **Click pe "Launch lost labels printing module"**
|
||||||
|
2. **Introduceți numărul comenzii**
|
||||||
|
3. **Selectați produsele** pentru care aveți nevoie de etichete
|
||||||
|
4. **Specificați motivul** reprintării (opțional)
|
||||||
|
5. **Generați și printați** etichetele
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Fluxul de Lucru Recomandat
|
||||||
|
|
||||||
|
### Pentru Comenzi Noi
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Upload Orders → 2. View Orders (verificare) → 3. Printing Module → 4. Printare
|
||||||
|
```
|
||||||
|
|
||||||
|
**Detaliat**:
|
||||||
|
|
||||||
|
1. **Încărcați comenzile** folosind "Upload Orders"
|
||||||
|
- Pregătiți fișierul cu comenzile noi
|
||||||
|
- Încărcați și verificați preview-ul
|
||||||
|
- Confirmați încărcarea
|
||||||
|
|
||||||
|
2. **Verificați comenzile** în "View Orders"
|
||||||
|
- Asigurați-vă că toate datele sunt corecte
|
||||||
|
- Editați dacă este necesar
|
||||||
|
- Verificați cantitățile
|
||||||
|
|
||||||
|
3. **Generați etichetele** în "Printing Module"
|
||||||
|
- Selectați comenzile pentru printare
|
||||||
|
- Configurați opțiunile
|
||||||
|
- Generați etichetele
|
||||||
|
|
||||||
|
4. **Printați etichetele**
|
||||||
|
- Printare automată sau download PDF
|
||||||
|
- Verificați calitatea printării
|
||||||
|
- Atașați etichetele la produse
|
||||||
|
|
||||||
|
### Pentru Etichete Pierdute
|
||||||
|
|
||||||
|
```
|
||||||
|
1. Lost Labels Module → 2. Căutare Comandă → 3. Selectare → 4. Reprintare
|
||||||
|
```
|
||||||
|
|
||||||
|
**Detaliat**:
|
||||||
|
|
||||||
|
1. **Accesați modulul** de etichete pierdute
|
||||||
|
2. **Căutați comanda** specifică
|
||||||
|
3. **Selectați produsele** necesare
|
||||||
|
4. **Reprintați** etichetele
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Funcționalități Avansate
|
||||||
|
|
||||||
|
### 1. Procesare în Lot (Batch Processing)
|
||||||
|
|
||||||
|
Pentru comenzi multiple:
|
||||||
|
|
||||||
|
1. Accesați "View Orders"
|
||||||
|
2. Selectați multiple comenzi (checkbox-uri)
|
||||||
|
3. Click pe "Batch Actions"
|
||||||
|
4. Alegeți acțiunea dorită:
|
||||||
|
- 🖨️ Print All
|
||||||
|
- ✏️ Bulk Edit
|
||||||
|
- 🗑️ Delete Selected
|
||||||
|
|
||||||
|
### 2. Export și Raportare
|
||||||
|
|
||||||
|
Exportați date pentru analiză:
|
||||||
|
|
||||||
|
- **Export Excel**: Date complete comenzi
|
||||||
|
- **Export CSV**: Pentru procesare externă
|
||||||
|
- **Print Reports**: Rapoarte printabile
|
||||||
|
- **Statistics**: Statistici de utilizare
|
||||||
|
|
||||||
|
|
||||||
|
## Integrări și Automatizări
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### QZ Tray
|
||||||
|
|
||||||
|
QZ Tray este necesar pentru comunicarea cu imprimanta:
|
||||||
|
|
||||||
|
1. Descărcați și instalați QZ Tray
|
||||||
|
2. Verificați că rulează (iconiță în system tray)
|
||||||
|
3. Extensia Chrome se va conecta automat
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rezolvare Probleme
|
||||||
|
|
||||||
|
### Problema: Nu pot încărca fișierul cu comenzi
|
||||||
|
|
||||||
|
**Soluții**:
|
||||||
|
1. ✓ Verificați formatul fișierului (Excel sau CSV)
|
||||||
|
2. ✓ Asigurați-vă că toate coloanele necesare există
|
||||||
|
3. ✓ Verificați că fișierul nu este corupt
|
||||||
|
4. ✓ Încercați să salvați fișierul din nou
|
||||||
|
5. ✓ Verificați dimensiunea fișierului (max 10MB)
|
||||||
|
|
||||||
|
### Problema: Comenzile nu apar în listă
|
||||||
|
|
||||||
|
**Soluții**:
|
||||||
|
1. ✓ Reîmprospătați pagina (F5)
|
||||||
|
2. ✓ Verificați filtrele aplicate
|
||||||
|
3. ✓ Asigurați-vă că încărcarea s-a finalizat cu succes
|
||||||
|
4. ✓ Verificați conexiunea la baza de date
|
||||||
|
5. ✓ Contactați administratorul
|
||||||
|
|
||||||
|
### Problema: Nu pot printa etichete
|
||||||
|
|
||||||
|
**Soluții**:
|
||||||
|
1. ✓ Verificați că imprimanta este pornită
|
||||||
|
2. ✓ Verificați instalarea extensiei Chrome
|
||||||
|
3. ✓ Verificați că QZ Tray rulează
|
||||||
|
4. ✓ Testați cu download PDF mai întâi
|
||||||
|
5. ✓ Restartați browserul
|
||||||
|
|
||||||
|
### Problema: Etichete printate incorect
|
||||||
|
|
||||||
|
**Soluții**:
|
||||||
|
1. ✓ Verificați setările imprimantei
|
||||||
|
2. ✓ Verificați formatul etichetelor selectat
|
||||||
|
3. ✓ Testați cu o singură etichetă mai întâi
|
||||||
|
4. ✓ Verificați template-ul utilizat
|
||||||
|
5. ✓ Recalibrați imprimanta
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tips & Tricks
|
||||||
|
|
||||||
|
💡 **Sfat**: Folosiți preview-ul înainte de fiecare încărcare pentru a evita erorile.
|
||||||
|
|
||||||
|
💡 **Sfat**: Folosiți căutarea rapidă (CTRL+F) în lista de comenzi pentru a găsi rapid comenzile.
|
||||||
|
|
||||||
|
💡 **Sfat**: Exportați lista de comenzi săptămânal pentru evidență.
|
||||||
|
|
||||||
|
💡 **Sfat**: Verificați zilnic comenzile cu status "pending" pentru a evita întârzierile.
|
||||||
|
|
||||||
|
💡 **Sfat**: Folosiți modulul de etichete pierdute pentru tracking-ul reprintărilor.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scurtături de Tastatură
|
||||||
|
|
||||||
|
Accelerați lucrul cu aceste scurtături:
|
||||||
|
|
||||||
|
- **CTRL + U**: Deschide Upload Orders
|
||||||
|
- **CTRL + V**: Deschide View Orders
|
||||||
|
- **CTRL + P**: Deschide Printing Module
|
||||||
|
- **CTRL + L**: Deschide Lost Labels Module
|
||||||
|
- **CTRL + F**: Căutare în lista de comenzi
|
||||||
|
- **ESC**: Închide dialog-uri/ferestre
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Întrebări Frecvente
|
||||||
|
|
||||||
|
**Î: Ce format trebuie să aibă fișierul cu comenzi?**
|
||||||
|
R: Acceptăm Excel (.xlsx, .xls) și CSV (.csv). Fișierul trebuie să conțină coloanele standard: comanda_productie, cod_articol, cantitate, etc.
|
||||||
|
|
||||||
|
**Î: Pot edita o comandă după încărcare?**
|
||||||
|
R: Da, accesați "View Orders", găsiți comanda și click pe butonul "Edit".
|
||||||
|
|
||||||
|
**Î: Cât timp rămân comenzile în sistem?**
|
||||||
|
R: Comenzile rămân permanent până sunt șterse manual sau arhivate de administrator.
|
||||||
|
|
||||||
|
**Î: Pot printa etichete pentru multiple comenzi simultan?**
|
||||||
|
R: Da, folosiți funcția "Batch Print" din View Orders.
|
||||||
|
|
||||||
|
**Î: Ce fac dacă o etichetă s-a deteriorat?**
|
||||||
|
R: Folosiți "Lost Labels Printing Module" pentru reprintare rapidă.
|
||||||
|
|
||||||
|
**Î: Pot personaliza template-urile de etichete?**
|
||||||
|
R: Momentan nu, dar funcționalitatea este în dezvoltare. Contactați administratorul pentru cerințe speciale.
|
||||||
|
|
||||||
|
**Î: Cum verific câte etichete am printat pentru o comandă?**
|
||||||
|
R: În "View Orders", click pe comandă pentru a vedea istoricul complet de printare.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Suport și Asistență
|
||||||
|
|
||||||
|
Pentru ajutor suplimentar:
|
||||||
|
|
||||||
|
- 📚 Consultați documentația detaliată a fiecărui modul
|
||||||
|
- 💬 Contactați administratorul de sistem
|
||||||
|
- 📧 Email: support@quality-app.com
|
||||||
|
- 📞 Telefon: Verificați în Settings pentru numărul de contact
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Actualizări Recente
|
||||||
|
|
||||||
|
**Decembrie 2025**
|
||||||
|
- ✨ Interfață îmbunătățită pentru modul etichete
|
||||||
|
- ✨ Suport pentru formate multiple de fișiere
|
||||||
|
- ✨ Funcție de reprintare etichete pierdute
|
||||||
|
- ✨ Integrare îmbunătățită cu QZ Tray
|
||||||
|
- ✨ Validare automată date la încărcare
|
||||||
|
|
||||||
|
**Viitor**
|
||||||
|
- 🔜 Gestionare template-uri personalizate
|
||||||
|
- 🔜 Printare QR codes
|
||||||
|
- 🔜 Integrare cu sistem WMS
|
||||||
|
- 🔜 Aplicație mobilă pentru scanare
|
||||||
@@ -50,6 +50,9 @@
|
|||||||
{% if request.endpoint in ['main.etichete', 'main.upload_data', 'main.view_orders', 'main.print_module', 'main.print_lost_labels'] %}
|
{% if request.endpoint in ['main.etichete', 'main.upload_data', 'main.view_orders', 'main.print_module', 'main.print_lost_labels'] %}
|
||||||
<a href="{{ url_for('main.etichete') }}" class="btn btn-success btn-sm ms-2"> <i class="fas fa-tags"></i> Labels Module</a>
|
<a href="{{ url_for('main.etichete') }}" class="btn btn-success btn-sm ms-2"> <i class="fas fa-tags"></i> Labels Module</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if request.endpoint.startswith('warehouse.') %}
|
||||||
|
<a href="{{ url_for('main.warehouse') }}" class="btn btn-warning btn-sm ms-2"> <i class="fas fa-warehouse"></i> Warehouse Main</a>
|
||||||
|
{% endif %}
|
||||||
<a href="{{ url_for('main.dashboard') }}" class="btn go-to-dashboard-btn ms-2">Go to Dashboard</a>
|
<a href="{{ url_for('main.dashboard') }}" class="btn go-to-dashboard-btn ms-2">Go to Dashboard</a>
|
||||||
{% if 'user' in session %}
|
{% if 'user' in session %}
|
||||||
<span class="user-info ms-2">You are logged in as {{ session['user'] }}</span>
|
<span class="user-info ms-2">You are logged in as {{ session['user'] }}</span>
|
||||||
|
|||||||
@@ -331,6 +331,13 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<!-- Floating Help Button -->
|
||||||
|
<div class="floating-help-btn">
|
||||||
|
<a href="{{ url_for('main.help', page='daily_mirror') }}" target="_blank" title="Ajutor - Daily Mirror">
|
||||||
|
📖
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<!-- Dashboard Header -->
|
<!-- Dashboard Header -->
|
||||||
<div class="dashboard-header">
|
<div class="dashboard-header">
|
||||||
|
|||||||
@@ -169,10 +169,10 @@
|
|||||||
<div class="help-navigation">
|
<div class="help-navigation">
|
||||||
<strong>Documentație disponibilă:</strong>
|
<strong>Documentație disponibilă:</strong>
|
||||||
<a href="{{ url_for('main.help', page='dashboard') }}">Dashboard</a>
|
<a href="{{ url_for('main.help', page='dashboard') }}">Dashboard</a>
|
||||||
|
<a href="{{ url_for('main.help', page='etichete') }}">Modul Etichete</a>
|
||||||
<a href="{{ url_for('main.help', page='print_module') }}">Print Module</a>
|
<a href="{{ url_for('main.help', page='print_module') }}">Print Module</a>
|
||||||
<a href="{{ url_for('main.help', page='upload_data') }}">Upload Data</a>
|
|
||||||
<a href="{{ url_for('main.help', page='view_orders') }}">View Orders</a>
|
|
||||||
<a href="{{ url_for('main.help', page='print_lost_labels') }}">Print Lost Labels</a>
|
<a href="{{ url_for('main.help', page='print_lost_labels') }}">Print Lost Labels</a>
|
||||||
|
<a href="{{ url_for('main.help', page='daily_mirror') }}">Daily Mirror</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="help-content">
|
<div class="help-content">
|
||||||
|
|||||||
@@ -15,7 +15,128 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<script>
|
<script>
|
||||||
|
// Global variables for scan-to-boxes feature - must be accessible by all scripts
|
||||||
|
let scanToBoxesEnabled = false;
|
||||||
|
let currentCpCode = null;
|
||||||
|
|
||||||
|
// Define scan-to-boxes functions at global scope
|
||||||
|
async function submitScanWithBoxAssignment() {
|
||||||
|
const form = document.getElementById('fg-scan-form');
|
||||||
|
const formData = new FormData(form);
|
||||||
|
|
||||||
|
console.log('=== submitScanWithBoxAssignment called ===');
|
||||||
|
console.log('Form data entries:');
|
||||||
|
for (let [key, value] of formData.entries()) {
|
||||||
|
console.log(` ${key}: ${value}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(window.location.href, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
},
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Response status:', response.status);
|
||||||
|
console.log('Response ok:', response.ok);
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
currentCpCode = formData.get('cp_code');
|
||||||
|
const defectCode = formData.get('defect_code') || '000';
|
||||||
|
|
||||||
|
console.log('CP Code:', currentCpCode);
|
||||||
|
console.log('Defect Code:', defectCode);
|
||||||
|
|
||||||
|
showNotification('✅ Scan recorded successfully!', 'success');
|
||||||
|
|
||||||
|
// Only show box modal if quality code is 000
|
||||||
|
if (defectCode === '000' || defectCode === '0') {
|
||||||
|
console.log('Should show box modal');
|
||||||
|
showBoxModal(currentCpCode);
|
||||||
|
} else {
|
||||||
|
console.log('Defect code not 000, reloading page');
|
||||||
|
setTimeout(() => window.location.reload(), 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear form fields (except operator code)
|
||||||
|
document.getElementById('cp_code').value = '';
|
||||||
|
document.getElementById('oc1_code').value = '';
|
||||||
|
document.getElementById('oc2_code').value = '';
|
||||||
|
document.getElementById('defect_code').value = '';
|
||||||
|
} else {
|
||||||
|
console.error('Response not OK');
|
||||||
|
showNotification('❌ Scan submission failed', 'error');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in submitScanWithBoxAssignment:', error);
|
||||||
|
showNotification('❌ Error: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showBoxModal(cpCode) {
|
||||||
|
document.getElementById('modal-cp-code').textContent = cpCode;
|
||||||
|
document.getElementById('box-assignment-modal').style.display = 'block';
|
||||||
|
document.getElementById('scan-box-input').value = '';
|
||||||
|
document.getElementById('scan-box-input').focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeBoxModal() {
|
||||||
|
document.getElementById('box-assignment-modal').style.display = 'none';
|
||||||
|
window.location.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function assignCpToBox(boxNumber) {
|
||||||
|
const response = await fetch('/warehouse/assign_cp_to_box', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
box_number: boxNumber,
|
||||||
|
cp_code: currentCpCode
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json();
|
||||||
|
throw new Error(error.error || 'Failed to assign CP to box');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNotification(message, type = 'info') {
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background: ${type === 'success' ? '#4CAF50' : type === 'error' ? '#f44336' : type === 'warning' ? '#ff9800' : '#2196F3'};
|
||||||
|
color: white;
|
||||||
|
padding: 15px 20px;
|
||||||
|
border-radius: 5px;
|
||||||
|
z-index: 10001;
|
||||||
|
box-shadow: 0 4px 10px rgba(0,0,0,0.3);
|
||||||
|
font-weight: bold;
|
||||||
|
`;
|
||||||
|
notification.textContent = message;
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if (notification.parentNode) {
|
||||||
|
notification.parentNode.removeChild(notification);
|
||||||
|
}
|
||||||
|
}, 4000);
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Load toggle state FIRST
|
||||||
|
const savedState = localStorage.getItem('scan_to_boxes_enabled');
|
||||||
|
if (savedState === 'true') {
|
||||||
|
scanToBoxesEnabled = true;
|
||||||
|
}
|
||||||
|
console.log('Initial scanToBoxesEnabled:', scanToBoxesEnabled);
|
||||||
|
|
||||||
const operatorCodeInput = document.getElementById('operator_code');
|
const operatorCodeInput = document.getElementById('operator_code');
|
||||||
const cpCodeInput = document.getElementById('cp_code');
|
const cpCodeInput = document.getElementById('cp_code');
|
||||||
const oc1CodeInput = document.getElementById('oc1_code');
|
const oc1CodeInput = document.getElementById('oc1_code');
|
||||||
@@ -23,6 +144,17 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const defectCodeInput = document.getElementById('defect_code');
|
const defectCodeInput = document.getElementById('defect_code');
|
||||||
const form = document.getElementById('fg-scan-form');
|
const form = document.getElementById('fg-scan-form');
|
||||||
|
|
||||||
|
// Set up toggle checkbox
|
||||||
|
const toggleElement = document.getElementById('scan-to-boxes-toggle');
|
||||||
|
if (toggleElement) {
|
||||||
|
toggleElement.checked = scanToBoxesEnabled;
|
||||||
|
toggleElement.addEventListener('change', function() {
|
||||||
|
scanToBoxesEnabled = this.checked;
|
||||||
|
localStorage.setItem('scan_to_boxes_enabled', this.checked);
|
||||||
|
console.log('Toggle changed - Scan to boxes:', scanToBoxesEnabled);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Restore saved operator code from localStorage (only Quality Operator Code)
|
// Restore saved operator code from localStorage (only Quality Operator Code)
|
||||||
const savedOperatorCode = localStorage.getItem('fg_scan_operator_code');
|
const savedOperatorCode = localStorage.getItem('fg_scan_operator_code');
|
||||||
|
|
||||||
@@ -417,13 +549,20 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
localStorage.setItem('fg_scan_last_cp', cpCodeInput.value);
|
localStorage.setItem('fg_scan_last_cp', cpCodeInput.value);
|
||||||
localStorage.setItem('fg_scan_last_defect', defectCodeInput.value);
|
localStorage.setItem('fg_scan_last_defect', defectCodeInput.value);
|
||||||
|
|
||||||
// Submit the form
|
// Check if scan-to-boxes is enabled and defect code is 000
|
||||||
form.submit();
|
if (scanToBoxesEnabled && this.value === '000') {
|
||||||
|
console.log('Auto-submit: Scan-to-boxes enabled, calling submitScanWithBoxAssignment');
|
||||||
|
submitScanWithBoxAssignment();
|
||||||
|
} else {
|
||||||
|
console.log('Auto-submit: Normal form submission');
|
||||||
|
// Submit the form normally
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Validate form on submit
|
// Validate form on submit
|
||||||
form.addEventListener('submit', function(e) {
|
form.addEventListener('submit', async function(e) {
|
||||||
let hasError = false;
|
let hasError = false;
|
||||||
|
|
||||||
if (!operatorCodeInput.value.startsWith('OP')) {
|
if (!operatorCodeInput.value.startsWith('OP')) {
|
||||||
@@ -477,6 +616,15 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
hasError = true;
|
hasError = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If validation passed and scan-to-boxes is enabled, intercept submission
|
||||||
|
if (!hasError && scanToBoxesEnabled) {
|
||||||
|
console.log('Validation passed, intercepting for scan-to-boxes');
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
await submitScanWithBoxAssignment();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add functionality for clear saved codes button
|
// Add functionality for clear saved codes button
|
||||||
@@ -501,6 +649,65 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Functions for scan-to-boxes feature
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Quick box creation button
|
||||||
|
document.getElementById('quick-box-create-btn').addEventListener('click', async function() {
|
||||||
|
try {
|
||||||
|
const createResponse = await fetch('/warehouse/manage_boxes', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
},
|
||||||
|
body: 'action=add_box'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!createResponse.ok) throw new Error('Failed to create box');
|
||||||
|
|
||||||
|
const result = await createResponse.json();
|
||||||
|
|
||||||
|
if (result.success && result.box_number) {
|
||||||
|
await assignCpToBox(result.box_number);
|
||||||
|
showNotification(`✅ Box ${result.box_number} created and CP assigned!`, 'success');
|
||||||
|
closeBoxModal();
|
||||||
|
} else {
|
||||||
|
throw new Error(result.error || 'Failed to create box');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showNotification('❌ Error creating box: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Assign to scanned box button
|
||||||
|
document.getElementById('assign-to-box-btn').addEventListener('click', async function() {
|
||||||
|
const boxNumber = document.getElementById('scan-box-input').value.trim();
|
||||||
|
if (!boxNumber) {
|
||||||
|
showNotification('⚠️ Please scan or enter a box number', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await assignCpToBox(boxNumber);
|
||||||
|
showNotification(`✅ CP ${currentCpCode} assigned to box ${boxNumber}`, 'success');
|
||||||
|
setTimeout(() => closeBoxModal(), 1000);
|
||||||
|
} catch (error) {
|
||||||
|
showNotification('❌ Error: ' + error.message, 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Close modal when clicking outside
|
||||||
|
window.onclick = function(event) {
|
||||||
|
const modal = document.getElementById('box-assignment-modal');
|
||||||
|
if (event.target == modal) {
|
||||||
|
closeBoxModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="scan-container">
|
<div class="scan-container">
|
||||||
@@ -531,6 +738,17 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
<button type="submit" class="btn">Submit</button>
|
<button type="submit" class="btn">Submit</button>
|
||||||
<button type="button" class="btn" id="clear-saved-btn" style="background-color: #ff6b6b; margin-left: 10px;">Clear Quality Operator</button>
|
<button type="button" class="btn" id="clear-saved-btn" style="background-color: #ff6b6b; margin-left: 10px;">Clear Quality Operator</button>
|
||||||
|
|
||||||
|
<!-- Enable/Disable Scan to Boxes Toggle -->
|
||||||
|
<div style="margin-top: 20px; padding-top: 15px; border-top: 1px solid #ddd;">
|
||||||
|
<label style="display: flex; align-items: center; justify-content: center; gap: 10px; cursor: pointer;">
|
||||||
|
<span style="font-weight: bold;">Enable Scan to Boxes:</span>
|
||||||
|
<input type="checkbox" id="scan-to-boxes-toggle" style="width: 20px; height: 20px; cursor: pointer;">
|
||||||
|
</label>
|
||||||
|
<p style="font-size: 0.85em; color: #666; text-align: center; margin-top: 8px;">
|
||||||
|
When enabled, good quality scans (000) will prompt for box assignment
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -573,4 +791,118 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Box Assignment Popup Modal -->
|
||||||
|
<div id="box-assignment-modal" class="box-modal" style="display: none;">
|
||||||
|
<div class="box-modal-content">
|
||||||
|
<div class="box-modal-header">
|
||||||
|
<h3>Assign to Box</h3>
|
||||||
|
<span class="box-modal-close" onclick="closeBoxModal()">×</span>
|
||||||
|
</div>
|
||||||
|
<div class="box-modal-body">
|
||||||
|
<p>CP Code: <strong id="modal-cp-code"></strong></p>
|
||||||
|
|
||||||
|
<!-- Quick Box Creation -->
|
||||||
|
<div style="margin: 20px 0; padding: 15px; background: #f0f8ff; border-radius: 5px;">
|
||||||
|
<button type="button" id="quick-box-create-btn" class="btn" style="width: 100%; background: #28a745; color: white;">
|
||||||
|
📦 Quick Box Label Creation
|
||||||
|
</button>
|
||||||
|
<p style="font-size: 0.85em; color: #666; margin-top: 8px; text-align: center;">
|
||||||
|
Creates new box and prints label immediately
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="text-align: center; margin: 15px 0; color: #999;">— OR —</div>
|
||||||
|
|
||||||
|
<!-- Scan Existing Box -->
|
||||||
|
<div style="margin: 20px 0;">
|
||||||
|
<label style="font-weight: bold;">Scan Box Number:</label>
|
||||||
|
<input type="text" id="scan-box-input" placeholder="Scan or enter box number" style="width: 100%; padding: 8px; font-size: 1em; margin-top: 5px;">
|
||||||
|
<p style="font-size: 0.85em; color: #666; margin-top: 5px;">
|
||||||
|
Scan an existing box label to assign this CP code to that box
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="box-modal-buttons" style="margin-top: 20px;">
|
||||||
|
<button type="button" class="btn" onclick="closeBoxModal()" style="background: #6c757d;">Skip</button>
|
||||||
|
<button type="button" id="assign-to-box-btn" class="btn" style="background: #007bff;">Assign to Box</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.box-modal {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 10000;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-modal-content {
|
||||||
|
background-color: #fefefe;
|
||||||
|
margin: 10% auto;
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid #888;
|
||||||
|
width: 500px;
|
||||||
|
max-width: 90%;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-modal-header {
|
||||||
|
padding: 15px 20px;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-modal-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-modal-close {
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-modal-close:hover {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-modal-body {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-modal-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .box-modal-content {
|
||||||
|
background-color: #1e293b;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .box-modal-body {
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode #scan-box-input {
|
||||||
|
background: #0f172a;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -3,6 +3,13 @@
|
|||||||
{% block title %}Modul Etichete{% endblock %}
|
{% block title %}Modul Etichete{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<!-- Floating Help Button -->
|
||||||
|
<div class="floating-help-btn">
|
||||||
|
<a href="{{ url_for('main.help', page='etichete') }}" target="_blank" title="Ajutor - Modul Etichete">
|
||||||
|
📖
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="etichete-container">
|
<div class="etichete-container">
|
||||||
<h1>Modul Etichete</h1>
|
<h1>Modul Etichete</h1>
|
||||||
<p>Aceasta este pagina pentru gestionarea etichetelor.</p>
|
<p>Aceasta este pagina pentru gestionarea etichetelor.</p>
|
||||||
|
|||||||
@@ -23,7 +23,21 @@
|
|||||||
<a href="{{ url_for('warehouse.create_locations') }}" class="btn">Go to Locations</a>
|
<a href="{{ url_for('warehouse.create_locations') }}" class="btn">Go to Locations</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Card 3: Warehouse Reports -->
|
<!-- Card 3: Manage Boxes/Crates -->
|
||||||
|
<div class="dashboard-card">
|
||||||
|
<h3>Manage Boxes/Crates</h3>
|
||||||
|
<p>Track and manage boxes and crates in the warehouse.</p>
|
||||||
|
<a href="{{ url_for('warehouse.manage_boxes') }}" class="btn">Go to Boxes</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Card 4: View Inventory -->
|
||||||
|
<div class="dashboard-card">
|
||||||
|
<h3>View Products/Boxes/Locations</h3>
|
||||||
|
<p>Search and view products, boxes, and their warehouse locations.</p>
|
||||||
|
<a href="{{ url_for('warehouse.warehouse_inventory') }}" class="btn">View Inventory</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Card 5: Warehouse Reports -->
|
||||||
<div class="dashboard-card">
|
<div class="dashboard-card">
|
||||||
<h3>Warehouse Reports</h3>
|
<h3>Warehouse Reports</h3>
|
||||||
<p>View and export warehouse activity and inventory reports.</p>
|
<p>View and export warehouse activity and inventory reports.</p>
|
||||||
|
|||||||
605
py_app/app/templates/manage_boxes.html
Normal file
605
py_app/app/templates/manage_boxes.html
Normal file
@@ -0,0 +1,605 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Manage Boxes{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/warehouse.css') }}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<style>
|
||||||
|
.warehouse-page-container .warehouse-container-1 {
|
||||||
|
flex: 0.5; /* Reduced from 1 to 0.5 */
|
||||||
|
}
|
||||||
|
.warehouse-page-container .warehouse-container-2 {
|
||||||
|
flex: 2.5; /* Increased from 2 to 2.5 */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="warehouse-page-container">
|
||||||
|
<!-- Container 1: Add Box (0.5 part width) -->
|
||||||
|
<div class="warehouse-container-1">
|
||||||
|
<h3>Add New Box</h3>
|
||||||
|
{% with messages = get_flashed_messages(with_categories=true) %}
|
||||||
|
{% if messages %}
|
||||||
|
{% for category, message in messages %}
|
||||||
|
<div class="form-message {{ 'alert-success' if category == 'success' else 'alert-danger' if category == 'error' else 'alert-info' }}">
|
||||||
|
{{ message }}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endwith %}
|
||||||
|
<form method="POST" class="form-centered">
|
||||||
|
<input type="hidden" name="action" value="add_box">
|
||||||
|
<div style="font-size: 0.9em; color: #666; margin: 8px 0;">
|
||||||
|
Box number will be auto-generated (8 digits)<br>
|
||||||
|
Location can be assigned later after filling the box.
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn">Create Box</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Container 2: Boxes Table (2 parts width) -->
|
||||||
|
<div class="warehouse-container-2">
|
||||||
|
<h3>All Boxes</h3>
|
||||||
|
<div class="warehouse-table-scroll">
|
||||||
|
<table class="scan-table" id="boxes-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Box Number</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Location</th>
|
||||||
|
<th>Created At</th>
|
||||||
|
<th>Created By</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for box in boxes %}
|
||||||
|
<tr class="box-row selectable-row" data-box-id="{{ box[0] }}" data-box-number="{{ box[1] }}">
|
||||||
|
<td>{{ box[0] }}</td>
|
||||||
|
<td><strong>{{ box[1] }}</strong></td>
|
||||||
|
<td>
|
||||||
|
<span class="status-badge status-{{ box[2] }}">{{ box[2]|upper }}</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ box[3] or 'Not assigned' }}</td>
|
||||||
|
<td>{{ box[4].strftime('%Y-%m-%d %H:%M') if box[4] else '' }}</td>
|
||||||
|
<td>{{ box[6] or '' }}</td>
|
||||||
|
<td class="action-cell">
|
||||||
|
<div class="action-buttons">
|
||||||
|
<!-- Status toggle -->
|
||||||
|
<form method="POST" style="display: inline;">
|
||||||
|
<input type="hidden" name="action" value="update_status">
|
||||||
|
<input type="hidden" name="box_id" value="{{ box[0] }}">
|
||||||
|
<input type="hidden" name="new_status" value="{{ 'closed' if box[2] == 'open' else 'open' }}">
|
||||||
|
<button type="submit" class="btn-small btn-info" title="Toggle status">
|
||||||
|
{{ '🔒 Close' if box[2] == 'open' else '🔓 Open' }}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<!-- Location change -->
|
||||||
|
<button type="button" class="btn-small btn-warning" onclick="openLocationModal({{ box[0] }}, '{{ box[1] }}', {{ box[7] or 'null' }});" title="Change location">
|
||||||
|
📍 Location
|
||||||
|
</button>
|
||||||
|
<!-- Make label button -->
|
||||||
|
<button type="button" class="btn-small btn-success" onclick="selectAndShowLabel('{{ box[0] }}', '{{ box[1] }}', this);" title="Select for label preview">
|
||||||
|
📋 Make Label
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Container 3: Actions and Print (1 part width) -->
|
||||||
|
<div class="warehouse-container-3">
|
||||||
|
<!-- Delete and Stats Section -->
|
||||||
|
<div style="padding: 12px; border: 1px solid #dee2e6; border-radius: 5px; margin-bottom: 16px;">
|
||||||
|
<h4 style="font-size: 1em; margin-bottom: 8px;">Box Statistics</h4>
|
||||||
|
<div style="font-size: 0.85em; display: flex; gap: 12px; margin-bottom: 12px;">
|
||||||
|
<span><strong>Total:</strong> {{ boxes|length }}</span>
|
||||||
|
<span><strong>Open:</strong> {{ boxes|selectattr('2', 'equalto', 'open')|list|length }}</span>
|
||||||
|
<span><strong>Closed:</strong> {{ boxes|selectattr('2', 'equalto', 'closed')|list|length }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if session['role'] in ['administrator', 'management'] %}
|
||||||
|
<div style="border-top: 1px solid #dee2e6; padding-top: 8px;">
|
||||||
|
<label style="font-weight:bold; font-size: 0.9em;">Delete Boxes</label>
|
||||||
|
<form method="POST" style="display:flex; gap:6px; align-items:center; margin-top: 6px;" onsubmit="return confirmDeleteBoxes();">
|
||||||
|
<input type="hidden" name="action" value="delete_boxes">
|
||||||
|
<input type="text" name="delete_ids" placeholder="e.g. 5,7,12" style="width:120px; font-size: 0.85em; padding: 4px;" required>
|
||||||
|
<button type="submit" class="btn" style="padding:4px 12px; font-size: 0.85em;">Delete</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Print Box Label Section -->
|
||||||
|
<div style="padding: 12px; border: 1px solid #dee2e6; border-radius: 5px;">
|
||||||
|
<h3 style="font-size: 1.1em; margin-bottom: 12px;">Print Box Label</h3>
|
||||||
|
<div id="box-selection-hint" style="margin-bottom: 10px; font-size: 11px; color: #666; text-align: center;">
|
||||||
|
Click on a box row in the table to select it for printing
|
||||||
|
</div>
|
||||||
|
<!-- Label Preview -->
|
||||||
|
<div id="box-label-preview" style="display: flex; align-items: center; justify-content: center; min-height: 120px;">
|
||||||
|
<div style="width: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center;">
|
||||||
|
<div class="box-text">
|
||||||
|
<div id="box-number-display" style="font-size: 18px; font-weight: bold;">Select a box</div>
|
||||||
|
</div>
|
||||||
|
<canvas id="box-barcode" style="display: none; margin-top: 10px;"></canvas>
|
||||||
|
<div id="box-barcode-placeholder" style="color: #999; font-style: italic; text-align: center;">No box selected</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Print Controls -->
|
||||||
|
<div style="text-align: center; margin-top: 15px;">
|
||||||
|
<div style="margin-bottom: 10px;">
|
||||||
|
<label for="printer-select" style="font-size: 11px; font-weight: 600;">Select Printer:</label>
|
||||||
|
<select id="printer-select" class="form-control form-control-sm" style="width: 200px; margin: 5px auto; font-size: 11px;">
|
||||||
|
<option value="">Loading printers...</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<button id="print-box-btn" class="btn btn-success" style="font-size: 12px; padding: 6px 20px;" disabled>
|
||||||
|
🖨️ Print Box Label
|
||||||
|
</button>
|
||||||
|
<button onclick="testQZConnection()" class="btn btn-info" style="font-size: 11px; padding: 4px 12px; margin-left: 8px;">
|
||||||
|
🔧 Test QZ Tray
|
||||||
|
</button>
|
||||||
|
<div id="print-status" style="margin-top: 8px; font-size: 11px;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Change Location Modal -->
|
||||||
|
<div id="location-modal" class="modal" style="display: none;">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>Change Box Location</h3>
|
||||||
|
<span class="close" onclick="closeLocationModal()">×</span>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>Change location for box: <strong id="modal-box-number"></strong></p>
|
||||||
|
<form method="POST" id="location-form">
|
||||||
|
<input type="hidden" name="action" value="update_location">
|
||||||
|
<input type="hidden" name="box_id" id="modal-box-id">
|
||||||
|
<label>New Location:</label>
|
||||||
|
<select name="new_location_id" id="modal-location-select" required>
|
||||||
|
<option value="">Select a location...</option>
|
||||||
|
{% for loc in locations %}
|
||||||
|
<option value="{{ loc[0] }}">{{ loc[1] }} - {{ loc[3] or 'No description' }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select><br>
|
||||||
|
<div class="modal-buttons">
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="closeLocationModal()">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-primary">Update Location</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.status-badge {
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.status-open {
|
||||||
|
background-color: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
.status-closed {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.btn-small {
|
||||||
|
padding: 3px 8px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.btn-info {
|
||||||
|
background-color: #17a2b8;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.btn-warning {
|
||||||
|
background-color: #ffc107;
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.btn-success {
|
||||||
|
background-color: #28a745;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.btn-info:hover {
|
||||||
|
background-color: #138496;
|
||||||
|
}
|
||||||
|
.btn-warning:hover {
|
||||||
|
background-color: #e0a800;
|
||||||
|
}
|
||||||
|
.btn-success:hover {
|
||||||
|
background-color: #218838;
|
||||||
|
}
|
||||||
|
.box-row {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
.box-row:hover {
|
||||||
|
background-color: #e2e8f0;
|
||||||
|
}
|
||||||
|
.box-row.selected {
|
||||||
|
background-color: #ffb300 !important;
|
||||||
|
color: #222 !important;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.selectable-row {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
.selectable-row:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
}
|
||||||
|
.selectable-row.selected {
|
||||||
|
background-color: #e3f2fd !important;
|
||||||
|
}
|
||||||
|
.modal {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1000;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
.modal-content {
|
||||||
|
background-color: #fefefe;
|
||||||
|
margin: 10% auto;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #888;
|
||||||
|
width: 400px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
.close {
|
||||||
|
color: #aaa;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.close:hover {
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
.modal-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
.form-message {
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.alert-success {
|
||||||
|
background-color: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
}
|
||||||
|
.alert-error, .alert-danger {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
.alert-info {
|
||||||
|
background-color: #d1ecf1;
|
||||||
|
color: #0c5460;
|
||||||
|
border: 1px solid #bee5eb;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function confirmDeleteBoxes() {
|
||||||
|
return confirm('Do you really want to delete the selected boxes?');
|
||||||
|
}
|
||||||
|
|
||||||
|
function openLocationModal(boxId, boxNumber, currentLocationId) {
|
||||||
|
document.getElementById('modal-box-id').value = boxId;
|
||||||
|
document.getElementById('modal-box-number').textContent = boxNumber;
|
||||||
|
|
||||||
|
const locationSelect = document.getElementById('modal-location-select');
|
||||||
|
if (currentLocationId) {
|
||||||
|
locationSelect.value = currentLocationId;
|
||||||
|
} else {
|
||||||
|
locationSelect.value = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('location-modal').style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeLocationModal() {
|
||||||
|
document.getElementById('location-modal').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close modal when clicking outside of it
|
||||||
|
window.onclick = function(event) {
|
||||||
|
const modal = document.getElementById('location-modal');
|
||||||
|
if (event.target == modal) {
|
||||||
|
closeLocationModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Include required JS libraries for printing -->
|
||||||
|
<script src="{{ url_for('static', filename='JsBarcode.all.min.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='qz-tray.js') }}"></script>
|
||||||
|
<script src="{{ url_for('static', filename='html2canvas.min.js') }}"></script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Box printing functionality
|
||||||
|
let selectedBoxNumber = null;
|
||||||
|
let selectedBoxId = null;
|
||||||
|
|
||||||
|
// Notification system
|
||||||
|
function showNotification(message, type = 'info') {
|
||||||
|
const existingNotifications = document.querySelectorAll('.notification');
|
||||||
|
existingNotifications.forEach(n => n.remove());
|
||||||
|
|
||||||
|
const notification = document.createElement('div');
|
||||||
|
notification.className = `notification alert alert-${type === 'error' ? 'danger' : type === 'success' ? 'success' : type === 'warning' ? 'warning' : 'info'}`;
|
||||||
|
notification.style.cssText = `
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
z-index: 9999;
|
||||||
|
max-width: 450px;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const formattedMessage = message.replace(/\n/g, '<br>');
|
||||||
|
|
||||||
|
notification.innerHTML = `
|
||||||
|
<div style="display: flex; align-items: flex-start; justify-content: space-between;">
|
||||||
|
<span style="flex: 1; padding-right: 10px; white-space: pre-wrap; font-family: monospace; font-size: 12px;">${formattedMessage}</span>
|
||||||
|
<button type="button" onclick="this.parentElement.parentElement.remove()" style="background: none; border: none; font-size: 20px; cursor: pointer; flex-shrink: 0;">×</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(notification);
|
||||||
|
|
||||||
|
const timeout = type === 'error' ? 15000 : 5000;
|
||||||
|
setTimeout(() => {
|
||||||
|
if (notification.parentNode) {
|
||||||
|
notification.remove();
|
||||||
|
}
|
||||||
|
}, timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle box row selection
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
console.log('=== Initializing box selection ===');
|
||||||
|
|
||||||
|
const boxRows = document.querySelectorAll('.box-row');
|
||||||
|
const printButton = document.getElementById('print-box-btn');
|
||||||
|
|
||||||
|
console.log('Found box rows:', boxRows.length);
|
||||||
|
console.log('Box rows array:', Array.from(boxRows));
|
||||||
|
|
||||||
|
if (boxRows.length === 0) {
|
||||||
|
console.error('ERROR: No box rows found! Check if .box-row class exists on tr elements');
|
||||||
|
}
|
||||||
|
|
||||||
|
boxRows.forEach((row, index) => {
|
||||||
|
console.log(`Attaching click handler to row ${index}:`, row);
|
||||||
|
|
||||||
|
row.addEventListener('click', function(e) {
|
||||||
|
console.log('=== ROW CLICKED ===');
|
||||||
|
console.log('Event target:', e.target);
|
||||||
|
console.log('Current target:', e.currentTarget);
|
||||||
|
|
||||||
|
// Don't trigger if clicking on buttons or forms in action cell
|
||||||
|
if (e.target.closest('.action-cell')) {
|
||||||
|
console.log('Clicked in action cell, ignoring');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Processing row selection...');
|
||||||
|
|
||||||
|
// Remove selection from other rows
|
||||||
|
boxRows.forEach(r => r.classList.remove('selected'));
|
||||||
|
|
||||||
|
// Select this row
|
||||||
|
this.classList.add('selected');
|
||||||
|
|
||||||
|
// Get box data from data attributes
|
||||||
|
selectedBoxId = this.dataset.boxId;
|
||||||
|
selectedBoxNumber = this.dataset.boxNumber;
|
||||||
|
|
||||||
|
console.log('Selected box ID:', selectedBoxId);
|
||||||
|
console.log('Selected box number:', selectedBoxNumber);
|
||||||
|
|
||||||
|
// Update preview
|
||||||
|
updateBoxPreview();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('=== Box selection initialized ===');
|
||||||
|
|
||||||
|
// Initialize QZ Tray
|
||||||
|
initQZTray();
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateBoxPreview() {
|
||||||
|
if (selectedBoxNumber) {
|
||||||
|
document.getElementById('box-number-display').textContent = selectedBoxNumber;
|
||||||
|
document.getElementById('box-barcode-placeholder').style.display = 'none';
|
||||||
|
|
||||||
|
const canvas = document.getElementById('box-barcode');
|
||||||
|
canvas.style.display = 'block';
|
||||||
|
|
||||||
|
// Generate barcode
|
||||||
|
JsBarcode(canvas, selectedBoxNumber, {
|
||||||
|
format: "CODE128",
|
||||||
|
width: 2,
|
||||||
|
height: 50,
|
||||||
|
displayValue: true,
|
||||||
|
fontSize: 14
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('print-box-btn').disabled = false;
|
||||||
|
} else {
|
||||||
|
document.getElementById('box-number-display').textContent = 'Select a box';
|
||||||
|
document.getElementById('box-barcode-placeholder').style.display = 'block';
|
||||||
|
document.getElementById('box-barcode').style.display = 'none';
|
||||||
|
document.getElementById('print-box-btn').disabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QZ Tray functionality
|
||||||
|
async function initQZTray() {
|
||||||
|
try {
|
||||||
|
if (!window.qz) {
|
||||||
|
console.error('QZ Tray library not loaded');
|
||||||
|
document.getElementById('print-status').innerHTML = '<span style="color: red;">QZ Tray library not loaded</span>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!window.qz.websocket.isActive()) {
|
||||||
|
await window.qz.websocket.connect();
|
||||||
|
}
|
||||||
|
await loadPrinters();
|
||||||
|
} catch (e) {
|
||||||
|
console.error('QZ Tray initialization failed:', e);
|
||||||
|
document.getElementById('print-status').innerHTML = '<span style="color: red;">QZ Tray not connected</span>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPrinters() {
|
||||||
|
try {
|
||||||
|
const printers = await window.qz.printers.find();
|
||||||
|
const printerSelect = document.getElementById('printer-select');
|
||||||
|
printerSelect.innerHTML = '';
|
||||||
|
|
||||||
|
if (printers.length === 0) {
|
||||||
|
printerSelect.innerHTML = '<option value="">No printers found</option>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
printers.forEach(printer => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = printer;
|
||||||
|
option.textContent = printer;
|
||||||
|
printerSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Select first printer by default
|
||||||
|
if (printers.length > 0) {
|
||||||
|
printerSelect.value = printers[0];
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Error loading printers:', e);
|
||||||
|
showNotification('Error loading printers: ' + e.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function testQZConnection() {
|
||||||
|
try {
|
||||||
|
if (!window.qz.websocket.isActive()) {
|
||||||
|
await window.qz.websocket.connect();
|
||||||
|
}
|
||||||
|
showNotification('QZ Tray connected successfully!', 'success');
|
||||||
|
await loadPrinters();
|
||||||
|
} catch (e) {
|
||||||
|
showNotification('QZ Tray connection failed: ' + e.message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to select box and show label preview from action button
|
||||||
|
function selectAndShowLabel(boxId, boxNumber, buttonElement) {
|
||||||
|
console.log('Make Label button clicked for box:', boxId, boxNumber);
|
||||||
|
|
||||||
|
// Find the row that contains this button
|
||||||
|
const row = buttonElement.closest('tr.box-row');
|
||||||
|
|
||||||
|
if (!row) {
|
||||||
|
console.error('Could not find parent row');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove selection from all rows
|
||||||
|
document.querySelectorAll('.box-row').forEach(r => {
|
||||||
|
r.classList.remove('selected');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Select this row
|
||||||
|
row.classList.add('selected');
|
||||||
|
|
||||||
|
// Update selected box data
|
||||||
|
selectedBoxId = boxId;
|
||||||
|
selectedBoxNumber = boxNumber;
|
||||||
|
|
||||||
|
console.log('Box selected for label:', selectedBoxId, selectedBoxNumber);
|
||||||
|
|
||||||
|
// Update preview
|
||||||
|
updateBoxPreview();
|
||||||
|
|
||||||
|
// Scroll to print preview section
|
||||||
|
document.getElementById('box-label-preview').scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('print-box-btn').addEventListener('click', async function() {
|
||||||
|
if (!selectedBoxNumber) {
|
||||||
|
showNotification('Please select a box from the table', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const printerSelect = document.getElementById('printer-select');
|
||||||
|
const selectedPrinter = printerSelect.value;
|
||||||
|
|
||||||
|
if (!selectedPrinter) {
|
||||||
|
showNotification('Please select a printer', 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
document.getElementById('print-status').innerHTML = '<span style="color: blue;">Printing...</span>';
|
||||||
|
|
||||||
|
// Create ZPL code for box label
|
||||||
|
const zpl = `^XA
|
||||||
|
^FO50,50^A0N,40,40^FDBox: ${selectedBoxNumber}^FS
|
||||||
|
^FO50,120^BY2,3,80^BCN,80,Y,N,N^FD${selectedBoxNumber}^FS
|
||||||
|
^XZ`;
|
||||||
|
|
||||||
|
const config = window.qz.configs.create(selectedPrinter);
|
||||||
|
await window.qz.print(config, [zpl]);
|
||||||
|
|
||||||
|
showNotification('Box label printed successfully!', 'success');
|
||||||
|
document.getElementById('print-status').innerHTML = '<span style="color: green;">✓ Printed successfully</span>';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
document.getElementById('print-status').innerHTML = '';
|
||||||
|
}, 3000);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Print error:', e);
|
||||||
|
showNotification('Print failed: ' + e.message, 'error');
|
||||||
|
document.getElementById('print-status').innerHTML = '<span style="color: red;">Print failed</span>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@@ -367,7 +367,7 @@
|
|||||||
|
|
||||||
<!-- Full Database Restore Section (Superadmin Only) -->
|
<!-- Full Database Restore Section (Superadmin Only) -->
|
||||||
{% if session.role == 'superadmin' %}
|
{% if session.role == 'superadmin' %}
|
||||||
<div style="margin-top: 16px; padding: 16px; background: var(--warning-bg, rgba(255, 87, 34, 0.1)); border: 1px solid #ff5722; border-radius: 8px;">
|
<div style="grid-column: 1 / -1; margin-top: 16px; padding: 16px; background: var(--warning-bg, rgba(255, 87, 34, 0.1)); border: 1px solid #ff5722; border-radius: 8px;">
|
||||||
<h4 style="margin: 0 0 12px 0; color: var(--text-primary, #333); display: flex; align-items: center; gap: 8px;">
|
<h4 style="margin: 0 0 12px 0; color: var(--text-primary, #333); display: flex; align-items: center; gap: 8px;">
|
||||||
<span>🔄 Full Database Restore</span>
|
<span>🔄 Full Database Restore</span>
|
||||||
<span style="background: #ff5722; color: white; font-size: 0.65em; padding: 3px 8px; border-radius: 4px; font-weight: 600;">SUPERADMIN</span>
|
<span style="background: #ff5722; color: white; font-size: 0.65em; padding: 3px 8px; border-radius: 4px; font-weight: 600;">SUPERADMIN</span>
|
||||||
@@ -400,7 +400,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<!-- Info -->
|
<!-- Info -->
|
||||||
<div style="margin-top: 12px; padding: 10px; background: var(--info-bg, rgba(76, 175, 80, 0.1)); border-left: 4px solid #4caf50; border-radius: 4px; font-size: 0.85em;">
|
<div style="grid-column: 1 / -1; margin-top: 12px; padding: 10px; background: var(--info-bg, rgba(76, 175, 80, 0.1)); border-left: 4px solid #4caf50; border-radius: 4px; font-size: 0.85em;">
|
||||||
<strong>💾 Location:</strong> <code style="background: var(--code-bg, rgba(0,0,0,0.05)); padding: 2px 6px; border-radius: 3px;">/srv/quality_app/backups</code>
|
<strong>💾 Location:</strong> <code style="background: var(--code-bg, rgba(0,0,0,0.05)); padding: 2px 6px; border-radius: 3px;">/srv/quality_app/backups</code>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
348
py_app/app/templates/warehouse_inventory.html
Normal file
348
py_app/app/templates/warehouse_inventory.html
Normal file
@@ -0,0 +1,348 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Warehouse Inventory{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/warehouse.css') }}">
|
||||||
|
<style>
|
||||||
|
.inventory-container {
|
||||||
|
width: 100%;
|
||||||
|
padding: 20px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-container h2 {
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-table-container h3 {
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .inventory-container h2 {
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .inventory-table-container h3 {
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-panel {
|
||||||
|
background: #f8fafc;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-panel h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 15px;
|
||||||
|
align-items: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-field label {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
color: #334155;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-field input {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.95em;
|
||||||
|
background: white;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode styles */
|
||||||
|
body.dark-mode .search-panel {
|
||||||
|
background: #1e293b;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .search-panel h3 {
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .search-field label {
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .search-field input {
|
||||||
|
background: #0f172a;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .search-field input::placeholder {
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-table-container {
|
||||||
|
background: #f8fafc;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
overflow-x: auto;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .inventory-table-container {
|
||||||
|
background: #1e293b;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-table th,
|
||||||
|
.inventory-table td {
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
padding: 10px;
|
||||||
|
text-align: left;
|
||||||
|
color: #1e293b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-table th {
|
||||||
|
background-color: #e2e8f0;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #334155;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inventory-table tbody tr:hover {
|
||||||
|
background-color: #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .inventory-table th,
|
||||||
|
body.dark-mode .inventory-table td {
|
||||||
|
border: 1px solid #334155;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .inventory-table th {
|
||||||
|
background-color: #0f172a;
|
||||||
|
color: #cbd5e1;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .inventory-table tbody tr:hover {
|
||||||
|
background-color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 3px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.85em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-open {
|
||||||
|
background-color: #d4edda;
|
||||||
|
color: #155724;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-closed {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
color: #721c24;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-active {
|
||||||
|
background-color: #d1ecf1;
|
||||||
|
color: #0c5460;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-data {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
color: #64748b;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .no-data {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card {
|
||||||
|
background: white;
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
border-left: 4px solid #007bff;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 150px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card h4 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.85em;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card .stat-value {
|
||||||
|
font-size: 1.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #1e293b;
|
||||||
|
margin-top: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .stat-card {
|
||||||
|
background: #0f172a;
|
||||||
|
border: 1px solid #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .stat-card h4 {
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .stat-card .stat-value {
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="inventory-container">
|
||||||
|
<h2>Warehouse Inventory - Products/Boxes/Locations</h2>
|
||||||
|
|
||||||
|
<!-- Search Panel -->
|
||||||
|
<div class="search-panel">
|
||||||
|
<h3>🔍 Search Inventory</h3>
|
||||||
|
<form method="GET" class="search-form">
|
||||||
|
<div class="search-field">
|
||||||
|
<label for="search_cp">CP Code:</label>
|
||||||
|
<input type="text" id="search_cp" name="search_cp" value="{{ search_cp }}" placeholder="Search by CP code...">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="search-field">
|
||||||
|
<label for="search_box">Box Number:</label>
|
||||||
|
<input type="text" id="search_box" name="search_box" value="{{ search_box }}" placeholder="Search by box number...">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="search-field">
|
||||||
|
<label for="search_location">Location:</label>
|
||||||
|
<input type="text" id="search_location" name="search_location" value="{{ search_location }}" placeholder="Search by location...">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="search-buttons">
|
||||||
|
<button type="submit" class="btn" style="background: #007bff;">Search</button>
|
||||||
|
<a href="{{ url_for('warehouse.warehouse_inventory') }}" class="btn" style="background: #6c757d;">Clear</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Summary Statistics -->
|
||||||
|
{% if inventory_data %}
|
||||||
|
<div class="summary-stats">
|
||||||
|
<div class="stat-card">
|
||||||
|
<h4>Total Records</h4>
|
||||||
|
<div class="stat-value">{{ inventory_data|length }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card" style="border-left-color: #28a745;">
|
||||||
|
<h4>Unique CP Codes</h4>
|
||||||
|
<div class="stat-value">{{ inventory_data|map(attribute=0)|unique|list|length }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card" style="border-left-color: #ffc107;">
|
||||||
|
<h4>Unique Boxes</h4>
|
||||||
|
<div class="stat-value">{{ inventory_data|map(attribute=1)|unique|list|length }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-card" style="border-left-color: #17a2b8;">
|
||||||
|
<h4>Locations Used</h4>
|
||||||
|
<div class="stat-value">{{ inventory_data|map(attribute=2)|select|unique|list|length }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Inventory Table -->
|
||||||
|
<div class="inventory-table-container">
|
||||||
|
<h3>Inventory Details</h3>
|
||||||
|
|
||||||
|
{% if inventory_data %}
|
||||||
|
<table class="inventory-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>CP Code</th>
|
||||||
|
<th>Box Number</th>
|
||||||
|
<th>Location</th>
|
||||||
|
<th>Scanned At</th>
|
||||||
|
<th>Scanned By</th>
|
||||||
|
<th>Placed At Location</th>
|
||||||
|
<th>Placed By</th>
|
||||||
|
<th>Box Status</th>
|
||||||
|
<th>Location Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for row in inventory_data %}
|
||||||
|
<tr>
|
||||||
|
<td><strong>{{ row[0] }}</strong></td>
|
||||||
|
<td>{{ row[1] }}</td>
|
||||||
|
<td>{{ row[2] or '<span style="color: #999;">Not assigned</span>'|safe }}</td>
|
||||||
|
<td>{{ row[3].strftime('%Y-%m-%d %H:%M') if row[3] else '' }}</td>
|
||||||
|
<td>{{ row[4] or '' }}</td>
|
||||||
|
<td>{{ row[5].strftime('%Y-%m-%d %H:%M') if row[5] else '' }}</td>
|
||||||
|
<td>{{ row[6] or '' }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="status-badge status-{{ row[7] }}">
|
||||||
|
{{ row[7]|upper if row[7] else 'N/A' }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if row[8] %}
|
||||||
|
<span class="status-badge status-{{ row[8] }}">
|
||||||
|
{{ row[8]|upper }}
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span style="color: #999;">-</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% else %}
|
||||||
|
<div class="no-data">
|
||||||
|
<p>📦 No inventory data found</p>
|
||||||
|
{% if search_cp or search_box or search_location %}
|
||||||
|
<p>Try adjusting your search criteria</p>
|
||||||
|
{% else %}
|
||||||
|
<p>Start scanning products to boxes to populate inventory</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -44,6 +44,88 @@ def ensure_warehouse_locations_table():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error ensuring warehouse_locations table: {e}")
|
print(f"Error ensuring warehouse_locations table: {e}")
|
||||||
|
|
||||||
|
def ensure_boxes_crates_table():
|
||||||
|
try:
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SHOW TABLES LIKE 'boxes_crates'")
|
||||||
|
result = cursor.fetchone()
|
||||||
|
if not result:
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS boxes_crates (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
box_number VARCHAR(8) NOT NULL UNIQUE,
|
||||||
|
status ENUM('open', 'closed') DEFAULT 'open',
|
||||||
|
location_id BIGINT,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
created_by VARCHAR(100),
|
||||||
|
FOREIGN KEY (location_id) REFERENCES warehouse_locations(id) ON DELETE SET NULL
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error ensuring boxes_crates table: {e}")
|
||||||
|
|
||||||
|
def ensure_box_contents_table():
|
||||||
|
"""Ensure box_contents table exists for tracking CP codes in boxes"""
|
||||||
|
try:
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SHOW TABLES LIKE 'box_contents'")
|
||||||
|
result = cursor.fetchone()
|
||||||
|
if not result:
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS box_contents (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
box_id BIGINT NOT NULL,
|
||||||
|
cp_code VARCHAR(15) NOT NULL,
|
||||||
|
scan_id BIGINT,
|
||||||
|
scanned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
scanned_by VARCHAR(100),
|
||||||
|
FOREIGN KEY (box_id) REFERENCES boxes_crates(id) ON DELETE CASCADE,
|
||||||
|
INDEX idx_box_id (box_id),
|
||||||
|
INDEX idx_cp_code (cp_code)
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
conn.commit()
|
||||||
|
print("box_contents table created successfully")
|
||||||
|
conn.close()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error ensuring box_contents table: {e}")
|
||||||
|
|
||||||
|
def ensure_location_contents_table():
|
||||||
|
"""Ensure location_contents table exists for tracking boxes in locations"""
|
||||||
|
try:
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SHOW TABLES LIKE 'location_contents'")
|
||||||
|
result = cursor.fetchone()
|
||||||
|
if not result:
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE IF NOT EXISTS location_contents (
|
||||||
|
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
location_id BIGINT NOT NULL,
|
||||||
|
box_id BIGINT NOT NULL,
|
||||||
|
placed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
placed_by VARCHAR(100),
|
||||||
|
removed_at TIMESTAMP NULL,
|
||||||
|
removed_by VARCHAR(100),
|
||||||
|
status ENUM('active', 'removed') DEFAULT 'active',
|
||||||
|
FOREIGN KEY (location_id) REFERENCES warehouse_locations(id) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (box_id) REFERENCES boxes_crates(id) ON DELETE CASCADE,
|
||||||
|
INDEX idx_location_id (location_id),
|
||||||
|
INDEX idx_box_id (box_id),
|
||||||
|
INDEX idx_status (status)
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
conn.commit()
|
||||||
|
print("location_contents table created successfully")
|
||||||
|
conn.close()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error ensuring location_contents table: {e}")
|
||||||
|
|
||||||
# Add warehouse-specific functions below
|
# Add warehouse-specific functions below
|
||||||
def add_location(location_code, size, description):
|
def add_location(location_code, size, description):
|
||||||
conn = get_db_connection()
|
conn = get_db_connection()
|
||||||
@@ -99,24 +181,34 @@ def delete_locations_by_ids(ids_str):
|
|||||||
return f"Deleted {deleted} location(s)."
|
return f"Deleted {deleted} location(s)."
|
||||||
|
|
||||||
def create_locations_handler():
|
def create_locations_handler():
|
||||||
if request.method == "POST":
|
try:
|
||||||
if request.form.get("delete_locations"):
|
# Ensure table exists
|
||||||
ids_str = request.form.get("delete_ids", "")
|
ensure_warehouse_locations_table()
|
||||||
message = delete_locations_by_ids(ids_str)
|
|
||||||
session['flash_message'] = message
|
if request.method == "POST":
|
||||||
else:
|
if request.form.get("delete_locations"):
|
||||||
location_code = request.form.get("location_code")
|
ids_str = request.form.get("delete_ids", "")
|
||||||
size = request.form.get("size")
|
message = delete_locations_by_ids(ids_str)
|
||||||
description = request.form.get("description")
|
session['flash_message'] = message
|
||||||
message = add_location(location_code, size, description)
|
else:
|
||||||
session['flash_message'] = message
|
location_code = request.form.get("location_code")
|
||||||
# Redirect to prevent form resubmission on page reload
|
size = request.form.get("size")
|
||||||
return redirect(url_for('warehouse.create_locations'))
|
description = request.form.get("description")
|
||||||
|
message = add_location(location_code, size, description)
|
||||||
# Get flash message from session if any
|
session['flash_message'] = message
|
||||||
message = session.pop('flash_message', None)
|
# Redirect to prevent form resubmission on page reload
|
||||||
locations = get_locations()
|
return redirect(url_for('warehouse.create_locations'))
|
||||||
return render_template("create_locations.html", locations=locations, message=message)
|
|
||||||
|
# Get flash message from session if any
|
||||||
|
message = session.pop('flash_message', None)
|
||||||
|
locations = get_locations()
|
||||||
|
return render_template("create_locations.html", locations=locations, message=message)
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
error_trace = traceback.format_exc()
|
||||||
|
print(f"Error in create_locations_handler: {e}")
|
||||||
|
print(error_trace)
|
||||||
|
return f"<h1>Error loading warehouse locations</h1><pre>{error_trace}</pre>", 500
|
||||||
|
|
||||||
def import_locations_csv_handler():
|
def import_locations_csv_handler():
|
||||||
report = None
|
report = None
|
||||||
@@ -258,9 +350,169 @@ def update_location(location_id, location_code, size, description):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error updating location: {e}")
|
print(f"Error updating location: {e}")
|
||||||
return {"success": False, "error": str(e)}
|
return {"success": False, "error": str(e)}
|
||||||
print(f"Error updating location: {e}")
|
|
||||||
|
# ============================================================================
|
||||||
|
# Boxes/Crates Functions
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def generate_box_number():
|
||||||
|
"""Generate next box number with 8 digits (00000001, 00000002, etc.)"""
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("SELECT MAX(CAST(box_number AS UNSIGNED)) FROM boxes_crates")
|
||||||
|
result = cursor.fetchone()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
if result and result[0]:
|
||||||
|
next_number = int(result[0]) + 1
|
||||||
|
else:
|
||||||
|
next_number = 1
|
||||||
|
|
||||||
|
return str(next_number).zfill(8)
|
||||||
|
|
||||||
|
def add_box(location_id=None, created_by=None):
|
||||||
|
"""Add a new box/crate"""
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
box_number = generate_box_number()
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor.execute(
|
||||||
|
"INSERT INTO boxes_crates (box_number, status, location_id, created_by) VALUES (%s, %s, %s, %s)",
|
||||||
|
(box_number, 'open', location_id if location_id else None, created_by)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return f"Box {box_number} created successfully"
|
||||||
|
except Exception as e:
|
||||||
|
conn.close()
|
||||||
|
return f"Error creating box: {e}"
|
||||||
|
|
||||||
|
def get_boxes():
|
||||||
|
"""Get all boxes with location information"""
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT
|
||||||
|
b.id,
|
||||||
|
b.box_number,
|
||||||
|
b.status,
|
||||||
|
l.location_code,
|
||||||
|
b.created_at,
|
||||||
|
b.updated_at,
|
||||||
|
b.created_by,
|
||||||
|
b.location_id
|
||||||
|
FROM boxes_crates b
|
||||||
|
LEFT JOIN warehouse_locations l ON b.location_id = l.id
|
||||||
|
ORDER BY b.id DESC
|
||||||
|
""")
|
||||||
|
boxes = cursor.fetchall()
|
||||||
|
conn.close()
|
||||||
|
return boxes
|
||||||
|
|
||||||
|
def update_box_status(box_id, status):
|
||||||
|
"""Update box status (open/closed)"""
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
try:
|
||||||
|
cursor.execute(
|
||||||
|
"UPDATE boxes_crates SET status = %s WHERE id = %s",
|
||||||
|
(status, box_id)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return {"success": True, "message": f"Box status updated to {status}"}
|
||||||
|
except Exception as e:
|
||||||
|
conn.close()
|
||||||
return {"success": False, "error": str(e)}
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
|
def update_box_location(box_id, location_id):
|
||||||
|
"""Update box location"""
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
try:
|
||||||
|
cursor.execute(
|
||||||
|
"UPDATE boxes_crates SET location_id = %s WHERE id = %s",
|
||||||
|
(location_id if location_id else None, box_id)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return {"success": True, "message": "Box location updated"}
|
||||||
|
except Exception as e:
|
||||||
|
conn.close()
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
|
def delete_boxes_by_ids(ids_str):
|
||||||
|
"""Delete boxes by comma-separated IDs"""
|
||||||
|
ids = [id.strip() for id in ids_str.split(',') if id.strip().isdigit()]
|
||||||
|
if not ids:
|
||||||
|
return "No valid IDs provided."
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
deleted = 0
|
||||||
|
for id in ids:
|
||||||
|
cursor.execute("DELETE FROM boxes_crates WHERE id = %s", (id,))
|
||||||
|
if cursor.rowcount:
|
||||||
|
deleted += 1
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return f"Deleted {deleted} box(es)."
|
||||||
|
|
||||||
|
def manage_boxes_handler():
|
||||||
|
"""Handler for boxes/crates management page"""
|
||||||
|
try:
|
||||||
|
# Ensure table exists
|
||||||
|
ensure_boxes_crates_table()
|
||||||
|
ensure_warehouse_locations_table()
|
||||||
|
|
||||||
|
if request.method == "POST":
|
||||||
|
action = request.form.get("action")
|
||||||
|
|
||||||
|
if action == "delete_boxes":
|
||||||
|
ids_str = request.form.get("delete_ids", "")
|
||||||
|
message = delete_boxes_by_ids(ids_str)
|
||||||
|
session['flash_message'] = message
|
||||||
|
elif action == "add_box":
|
||||||
|
created_by = session.get('user', 'Unknown')
|
||||||
|
message = add_box(None, created_by) # Create box without location
|
||||||
|
|
||||||
|
# Check if this is an AJAX request
|
||||||
|
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
|
||||||
|
# Extract box number from message (format: "Box 12345678 created successfully")
|
||||||
|
import re
|
||||||
|
match = re.search(r'Box (\d{8})', message)
|
||||||
|
if match:
|
||||||
|
return jsonify({'success': True, 'box_number': match.group(1), 'message': message})
|
||||||
|
else:
|
||||||
|
return jsonify({'success': False, 'error': message})
|
||||||
|
|
||||||
|
session['flash_message'] = message
|
||||||
|
elif action == "update_status":
|
||||||
|
box_id = request.form.get("box_id")
|
||||||
|
new_status = request.form.get("new_status")
|
||||||
|
message = update_box_status(box_id, new_status)
|
||||||
|
session['flash_message'] = message
|
||||||
|
elif action == "update_location":
|
||||||
|
box_id = request.form.get("box_id")
|
||||||
|
new_location_id = request.form.get("new_location_id")
|
||||||
|
message = update_box_location(box_id, new_location_id)
|
||||||
|
session['flash_message'] = message
|
||||||
|
|
||||||
|
return redirect(url_for('warehouse.manage_boxes'))
|
||||||
|
|
||||||
|
# Get flash message from session if any
|
||||||
|
message = session.pop('flash_message', None)
|
||||||
|
boxes = get_boxes()
|
||||||
|
locations = get_locations()
|
||||||
|
return render_template("manage_boxes.html", boxes=boxes, locations=locations, message=message)
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
error_trace = traceback.format_exc()
|
||||||
|
print(f"Error in manage_boxes_handler: {e}")
|
||||||
|
print(error_trace)
|
||||||
|
return f"<h1>Error loading boxes management</h1><pre>{error_trace}</pre>", 500
|
||||||
|
|
||||||
def delete_location_by_id(location_id):
|
def delete_location_by_id(location_id):
|
||||||
"""Delete a warehouse location by ID"""
|
"""Delete a warehouse location by ID"""
|
||||||
try:
|
try:
|
||||||
@@ -284,3 +536,127 @@ def delete_location_by_id(location_id):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error deleting location: {e}")
|
print(f"Error deleting location: {e}")
|
||||||
return {"success": False, "error": str(e)}
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
|
def assign_cp_to_box_handler():
|
||||||
|
"""Handle assigning CP code to a box"""
|
||||||
|
from flask import request, jsonify, session
|
||||||
|
import json
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Ensure box_contents table exists
|
||||||
|
ensure_box_contents_table()
|
||||||
|
|
||||||
|
data = json.loads(request.data)
|
||||||
|
box_number = data.get('box_number')
|
||||||
|
cp_code = data.get('cp_code')
|
||||||
|
scanned_by = session.get('user', 'Unknown')
|
||||||
|
|
||||||
|
if not box_number or not cp_code:
|
||||||
|
return jsonify({'success': False, 'error': 'Missing box_number or cp_code'}), 400
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Find the box by number
|
||||||
|
cursor.execute("SELECT id FROM boxes_crates WHERE box_number = %s", (box_number,))
|
||||||
|
box = cursor.fetchone()
|
||||||
|
|
||||||
|
if not box:
|
||||||
|
conn.close()
|
||||||
|
return jsonify({'success': False, 'error': f'Box {box_number} not found'}), 404
|
||||||
|
|
||||||
|
box_id = box[0]
|
||||||
|
|
||||||
|
# Insert into box_contents
|
||||||
|
cursor.execute("""
|
||||||
|
INSERT INTO box_contents (box_id, cp_code, scanned_by)
|
||||||
|
VALUES (%s, %s, %s)
|
||||||
|
""", (box_id, cp_code, scanned_by))
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'message': f'CP {cp_code} assigned to box {box_number}'
|
||||||
|
}), 200
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
print(f"Error in assign_cp_to_box_handler: {e}")
|
||||||
|
print(traceback.format_exc())
|
||||||
|
return jsonify({'success': False, 'error': str(e)}), 500
|
||||||
|
|
||||||
|
def view_warehouse_inventory_handler():
|
||||||
|
"""Handle warehouse inventory view - shows CP codes, boxes, and locations"""
|
||||||
|
from flask import render_template, request
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Ensure tables exist
|
||||||
|
ensure_box_contents_table()
|
||||||
|
ensure_location_contents_table()
|
||||||
|
|
||||||
|
# Get search parameters
|
||||||
|
search_cp = request.args.get('search_cp', '').strip()
|
||||||
|
search_box = request.args.get('search_box', '').strip()
|
||||||
|
search_location = request.args.get('search_location', '').strip()
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Build query with joins and filters
|
||||||
|
query = """
|
||||||
|
SELECT
|
||||||
|
bc.cp_code,
|
||||||
|
b.box_number,
|
||||||
|
wl.location_code,
|
||||||
|
bc.scanned_at,
|
||||||
|
bc.scanned_by,
|
||||||
|
lc.placed_at,
|
||||||
|
lc.placed_by,
|
||||||
|
b.status as box_status,
|
||||||
|
lc.status as location_status
|
||||||
|
FROM box_contents bc
|
||||||
|
INNER JOIN boxes_crates b ON bc.box_id = b.id
|
||||||
|
LEFT JOIN location_contents lc ON b.id = lc.box_id AND lc.status = 'active'
|
||||||
|
LEFT JOIN warehouse_locations wl ON lc.location_id = wl.id
|
||||||
|
WHERE 1=1
|
||||||
|
"""
|
||||||
|
|
||||||
|
params = []
|
||||||
|
|
||||||
|
if search_cp:
|
||||||
|
query += " AND bc.cp_code LIKE %s"
|
||||||
|
params.append(f"%{search_cp}%")
|
||||||
|
|
||||||
|
if search_box:
|
||||||
|
query += " AND b.box_number LIKE %s"
|
||||||
|
params.append(f"%{search_box}%")
|
||||||
|
|
||||||
|
if search_location:
|
||||||
|
query += " AND wl.location_code LIKE %s"
|
||||||
|
params.append(f"%{search_location}%")
|
||||||
|
|
||||||
|
query += " ORDER BY bc.scanned_at DESC"
|
||||||
|
|
||||||
|
cursor.execute(query, params)
|
||||||
|
inventory_data = cursor.fetchall()
|
||||||
|
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
'warehouse_inventory.html',
|
||||||
|
inventory_data=inventory_data,
|
||||||
|
search_cp=search_cp,
|
||||||
|
search_box=search_box,
|
||||||
|
search_location=search_location
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
import traceback
|
||||||
|
error_trace = traceback.format_exc()
|
||||||
|
print(f"Error in view_warehouse_inventory_handler: {e}")
|
||||||
|
print(error_trace)
|
||||||
|
return f"<h1>Error loading warehouse inventory</h1><pre>{error_trace}</pre>", 500
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,4 +8,5 @@ reportlab
|
|||||||
requests
|
requests
|
||||||
pandas
|
pandas
|
||||||
openpyxl
|
openpyxl
|
||||||
APScheduler
|
APScheduler
|
||||||
|
Markdown
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
/* Theme variables for popup and cards */
|
|
||||||
:root {
|
|
||||||
--app-overlay-bg: rgba(30,41,59,0.85);
|
|
||||||
--app-card-bg: #f8fafc;
|
|
||||||
--app-card-text: #1e293b;
|
|
||||||
--app-input-bg: #e2e8f0;
|
|
||||||
--app-input-text: #1e293b;
|
|
||||||
--app-label-text: #334155;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.light-mode {
|
|
||||||
background: #f8fafc;
|
|
||||||
color: #1e293b;
|
|
||||||
}
|
|
||||||
body.dark-mode {
|
|
||||||
background: #1e293b;
|
|
||||||
color: #f8fafc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.popup {
|
|
||||||
background: var(--app-overlay-bg) !important;
|
|
||||||
}
|
|
||||||
.popup-content {
|
|
||||||
background: var(--app-card-bg) !important;
|
|
||||||
color: var(--app-card-text) !important;
|
|
||||||
}
|
|
||||||
.popup-content label,
|
|
||||||
#user-popup-title {
|
|
||||||
color: var(--app-label-text) !important;
|
|
||||||
}
|
|
||||||
.popup-content input,
|
|
||||||
.popup-content select {
|
|
||||||
background: var(--app-input-bg) !important;
|
|
||||||
color: var(--app-input-text) !important;
|
|
||||||
border: 1px solid #cbd5e1;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 8px;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
width: 90%;
|
|
||||||
font-size: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card {
|
|
||||||
background: var(--app-card-bg);
|
|
||||||
color: var(--app-card-text);
|
|
||||||
box-shadow: 0 2px 8px #333;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
background: #1e293b;
|
|
||||||
color: #f8fafc;
|
|
||||||
border: none;
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 8px 16px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.btn.cancel-btn {
|
|
||||||
background: #e11d48;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Label preview card title theme styles */
|
|
||||||
.label-view-title {
|
|
||||||
color: var(--app-card-text) !important;
|
|
||||||
transition: color 0.2s;
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user