diff --git a/files/Articole livrate_returnate.xlsx b/files/Articole livrate_returnate.xlsx new file mode 100644 index 0000000..1572e8d Binary files /dev/null and b/files/Articole livrate_returnate.xlsx differ diff --git a/files/Comenzi Productie.xlsx b/files/Comenzi Productie.xlsx new file mode 100644 index 0000000..ebf6390 Binary files /dev/null and b/files/Comenzi Productie.xlsx differ diff --git a/files/Open .Orders WIZ New.xlsb b/files/Open .Orders WIZ New.xlsb new file mode 100644 index 0000000..766c994 Binary files /dev/null and b/files/Open .Orders WIZ New.xlsb differ diff --git a/files/Vizual. Artic. Comenzi Deschise.xlsx b/files/Vizual. Artic. Comenzi Deschise.xlsx new file mode 100644 index 0000000..2222f29 Binary files /dev/null and b/files/Vizual. Artic. Comenzi Deschise.xlsx differ diff --git a/logs/access.log b/logs/access.log index 32aa04f..3330e6b 100644 --- a/logs/access.log +++ b/logs/access.log @@ -447,3 +447,20 @@ 192.168.0.132 - - [22/Oct/2025:21:02:46 +0300] "GET /static/fg_quality.js HTTP/1.1" 200 0 "https://quality.moto-adv.com/fg_quality" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 2489 192.168.0.132 - - [22/Oct/2025:21:02:59 +0300] "GET /generate_fg_report?report=6&date=2025-10-16 HTTP/1.1" 200 2422 "https://quality.moto-adv.com/fg_quality" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 35769 192.168.0.132 - - [22/Oct/2025:21:03:18 +0300] "GET /quality HTTP/1.1" 200 8860 "https://quality.moto-adv.com/reports" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 10411 +192.168.0.132 - - [23/Oct/2025:00:18:40 +0300] "GET / HTTP/1.1" 200 1627 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36" 59084 +192.168.0.132 - - [23/Oct/2025:00:18:41 +0300] "POST / HTTP/1.1" 200 1627 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36" 5974 +192.168.0.132 - - [24/Oct/2025:19:45:55 +0300] "GET /.well-known/change-password HTTP/1.1" 404 207 "-" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36" 3174 +192.168.0.132 - - [24/Oct/2025:19:45:55 +0300] "GET /.well-known/resource-that-should-not-exist-whose-status-code-should-not-be-200 HTTP/1.1" 404 207 "-" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36" 1381 +192.168.0.132 - - [24/Oct/2025:19:45:55 +0300] "GET / HTTP/1.1" 200 1627 "-" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36" 8205 +192.168.0.132 - - [24/Oct/2025:19:45:55 +0300] "GET /static/style.css HTTP/1.1" 200 0 "https://quality.moto-adv.com/" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36" 2287 +192.168.0.132 - - [24/Oct/2025:19:45:55 +0300] "GET /static/css/login.css HTTP/1.1" 200 0 "https://quality.moto-adv.com/" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36" 2370 +192.168.0.132 - - [24/Oct/2025:19:45:55 +0300] "GET /static/css/base.css HTTP/1.1" 200 0 "https://quality.moto-adv.com/" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36" 2078 +192.168.0.132 - - [24/Oct/2025:19:45:55 +0300] "GET /static/logo_login.jpg HTTP/1.1" 200 0 "https://quality.moto-adv.com/" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36" 4452 +192.168.0.132 - - [24/Oct/2025:19:45:55 +0300] "GET /static/script.js HTTP/1.1" 200 0 "https://quality.moto-adv.com/" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36" 1950 +192.168.0.132 - - [24/Oct/2025:19:45:56 +0300] "GET /favicon.ico HTTP/1.1" 404 207 "https://quality.moto-adv.com/" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36" 1364 +192.168.0.132 - - [24/Oct/2025:19:45:58 +0300] "GET / HTTP/1.1" 200 1627 "https://www.google.com/" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.52 Mobile Safari/537.36" 45825 +192.168.0.132 - - [24/Oct/2025:19:45:59 +0300] "GET /static/css/login.css HTTP/1.1" 200 0 "https://quality.moto-adv.com/" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.52 Mobile Safari/537.36" 2111 +192.168.0.132 - - [24/Oct/2025:19:45:59 +0300] "GET /static/style.css HTTP/1.1" 200 0 "https://quality.moto-adv.com/" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.52 Mobile Safari/537.36" 2136 +192.168.0.132 - - [24/Oct/2025:19:45:59 +0300] "GET /static/logo_login.jpg HTTP/1.1" 200 0 "https://quality.moto-adv.com/" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.52 Mobile Safari/537.36" 4653 +192.168.0.132 - - [24/Oct/2025:19:45:59 +0300] "GET /static/script.js HTTP/1.1" 200 0 "https://quality.moto-adv.com/" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.52 Mobile Safari/537.36" 1971 +192.168.0.132 - - [24/Oct/2025:19:45:59 +0300] "GET /static/css/base.css HTTP/1.1" 200 0 "https://quality.moto-adv.com/" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.52 Mobile Safari/537.36" 3201 diff --git a/py_app/app/__init__.py b/py_app/app/__init__.py index 8d5e585..ef843d6 100755 --- a/py_app/app/__init__.py +++ b/py_app/app/__init__.py @@ -13,8 +13,10 @@ def create_app(): db.init_app(app) from app.routes import bp as main_bp, warehouse_bp + from app.daily_mirror import daily_mirror_bp app.register_blueprint(main_bp, url_prefix='/') app.register_blueprint(warehouse_bp) + app.register_blueprint(daily_mirror_bp) # Add 'now' function to Jinja2 globals app.jinja_env.globals['now'] = datetime.now diff --git a/py_app/app/access_control.py b/py_app/app/access_control.py index 806a70c..6c7cba4 100644 --- a/py_app/app/access_control.py +++ b/py_app/app/access_control.py @@ -77,6 +77,10 @@ def requires_labels_module(f): """Decorator for labels module access""" return requires_role(required_modules=['labels'])(f) +def requires_daily_mirror_module(f): + """Decorator for daily mirror module access""" + return requires_role(required_modules=['daily_mirror'])(f) + def quality_manager_plus(f): """Decorator for quality module manager+ access""" return requires_role(min_role_level=70, required_modules=['quality'])(f) @@ -87,4 +91,8 @@ def warehouse_manager_plus(f): def labels_manager_plus(f): """Decorator for labels module manager+ access""" - return requires_role(min_role_level=70, required_modules=['labels'])(f) \ No newline at end of file + return requires_role(min_role_level=70, required_modules=['labels'])(f) + +def daily_mirror_manager_plus(f): + """Decorator for daily mirror module manager+ access""" + return requires_role(min_role_level=70, required_modules=['daily_mirror'])(f) \ No newline at end of file diff --git a/py_app/app/daily_mirror.py b/py_app/app/daily_mirror.py new file mode 100644 index 0000000..c74bf98 --- /dev/null +++ b/py_app/app/daily_mirror.py @@ -0,0 +1,1016 @@ +""" +Daily Mirror Module - Business Intelligence and Production Reporting +Quality Recticel Application + +This module provides comprehensive daily production reporting and analytics, +including order tracking, quality control metrics, and historical analysis. +""" + +from flask import Blueprint, request, jsonify, render_template, flash, redirect, url_for, session, current_app +from datetime import datetime, timedelta, date +import json +import pandas as pd +import os +from werkzeug.utils import secure_filename +from app.print_module import get_db_connection +from app.daily_mirror_db_setup import DailyMirrorDatabase + +# Create Blueprint for Daily Mirror routes +daily_mirror_bp = Blueprint('daily_mirror', __name__, url_prefix='/daily_mirror') + + +def check_daily_mirror_access(): + """Helper function to check if user has access to Daily Mirror functionality""" + # Check if user is logged in + if 'user' not in session: + flash('Please log in to access this page.') + return redirect(url_for('main.login')) + + # Check if user has admin+ access + user_role = session.get('role', '') + if user_role not in ['superadmin', 'admin']: + flash('Access denied: Admin privileges required for Daily Mirror.') + return redirect(url_for('main.dashboard')) + + return None # Access granted + + +def check_daily_mirror_api_access(): + """Helper function to check API access for Daily Mirror""" + # Check if user is logged in and has admin+ access + if 'user' not in session: + return jsonify({'error': 'Authentication required'}), 401 + + user_role = session.get('role', '') + if user_role not in ['superadmin', 'admin']: + return jsonify({'error': 'Admin privileges required'}), 403 + + return None # Access granted + + +class DailyMirrorManager: + """Main class for managing Daily Mirror functionality""" + + def __init__(self): + self.module_name = "daily_mirror" + self.module_display_name = "Daily Mirror" + self.module_description = "Business Intelligence and Production Reporting" + + def get_daily_production_data(self, report_date): + """Get comprehensive daily production data for a specific date""" + try: + conn = get_db_connection() + cursor = conn.cursor() + + # Initialize report data structure + report_data = { + 'date': report_date, + 'orders_quantity': 0, + 'production_launched': 0, + 'production_finished': 0, + 'orders_delivered': 0, + 'operators': { + 'active_operators': 0, + 'operator_performance': [] + }, + 'production_efficiency': { + 'launch_rate': 0, + 'completion_rate': 0, + 'delivery_rate': 0 + } + } + + # Get orders data from order_for_labels table + cursor.execute(""" + SELECT COUNT(*) as total_orders, + SUM(cantitate) as total_quantity, + SUM(printed_labels) as total_printed, + COUNT(DISTINCT customer_name) as unique_customers + FROM order_for_labels + WHERE DATE(created_at) = ? + """, (report_date,)) + + orders_row = cursor.fetchone() + if orders_row: + report_data['orders_quantity'] = orders_row[1] or 0 + report_data['production_launched'] = orders_row[0] or 0 + + # Get production data from dm_production_orders if available + cursor.execute(""" + SELECT COUNT(*) as total_production, + SUM(CASE WHEN production_status = 'FINISHED' THEN 1 ELSE 0 END) as finished_production, + SUM(CASE WHEN end_of_quilting IS NOT NULL THEN 1 ELSE 0 END) as quilting_done, + SUM(CASE WHEN end_of_sewing IS NOT NULL THEN 1 ELSE 0 END) as sewing_done, + COUNT(DISTINCT customer_code) as unique_customers + FROM dm_production_orders + WHERE DATE(data_planificare) = ? + """, (report_date,)) + + production_row = cursor.fetchone() + if production_row: + report_data['production_launched'] = max(report_data['production_launched'], production_row[0] or 0) + report_data['production_finished'] = production_row[1] or 0 + report_data['orders_delivered'] = production_row[3] or 0 # Use sewing_done as delivery proxy + + # Get operator count + cursor.execute(""" + SELECT COUNT(DISTINCT CASE + WHEN t1_operator_name IS NOT NULL THEN t1_operator_name + WHEN t2_operator_name IS NOT NULL THEN t2_operator_name + WHEN t3_operator_name IS NOT NULL THEN t3_operator_name + END) as active_operators + FROM dm_production_orders + WHERE DATE(data_planificare) = ? + """, (report_date,)) + + operator_row = cursor.fetchone() + if operator_row: + report_data['operators']['active_operators'] = operator_row[0] or 0 + + # Calculate efficiency metrics + if report_data['production_launched'] > 0: + report_data['production_efficiency'] = { + 'launch_rate': 100, # All launched orders are 100% launched + 'completion_rate': (report_data['production_finished'] / report_data['production_launched']) * 100, + 'delivery_rate': (report_data['orders_delivered'] / report_data['production_launched']) * 100 + } + + cursor.close() + conn.close() + + return report_data + + except Exception as e: + print(f"Error getting daily production data: {e}") + return None + + def get_historical_data(self, start_date, end_date): + """Get historical production data for date range""" + try: + conn = get_db_connection() + cursor = conn.cursor() + + # Get daily aggregated data for the date range + cursor.execute(""" + SELECT DATE(created_at) as report_date, + COUNT(*) as orders_count, + SUM(cantitate) as total_quantity, + SUM(printed_labels) as total_printed, + COUNT(DISTINCT customer_name) as unique_customers + FROM order_for_labels + WHERE DATE(created_at) BETWEEN %s AND %s + GROUP BY DATE(created_at) + ORDER BY report_date DESC + """, (start_date, end_date)) + + orders_data = {} + for row in cursor.fetchall(): + date_str = str(row[0]) + orders_data[date_str] = { + 'orders_count': row[1] or 0, + 'orders_quantity': row[2] or 0, + 'production_launched': row[3] or 0, + 'unique_customers': row[4] or 0 + } + + # Get production data from dm_production_orders if available + cursor.execute(""" + SELECT DATE(data_planificare) as production_date, + COUNT(*) as total_production, + SUM(CASE WHEN production_status = 'FINISHED' THEN 1 ELSE 0 END) as finished_production, + SUM(CASE WHEN end_of_sewing IS NOT NULL THEN 1 ELSE 0 END) as sewing_done, + COUNT(DISTINCT customer_code) as unique_customers + FROM dm_production_orders + WHERE DATE(data_planificare) BETWEEN %s AND %s + GROUP BY DATE(data_planificare) + ORDER BY production_date DESC + """, (start_date, end_date)) + + production_data = {} + for row in cursor.fetchall(): + date_str = str(row[0]) + production_data[date_str] = { + 'production_launched': row[1] or 0, + 'production_finished': row[2] or 0, + 'orders_delivered': row[3] or 0, # Use sewing_done as delivery proxy + 'unique_customers': row[4] or 0 + } + + conn.close() + + # Combine data by date + all_dates = set(orders_data.keys()) | set(production_data.keys()) + + history_data = [] + for date_str in sorted(all_dates, reverse=True): + orders_info = orders_data.get(date_str, { + 'orders_count': 0, 'orders_quantity': 0, + 'production_launched': 0, 'unique_customers': 0 + }) + production_info = production_data.get(date_str, { + 'production_launched': 0, 'production_finished': 0, + 'orders_delivered': 0, 'unique_customers': 0 + }) + + day_data = { + 'date': date_str, + 'orders_quantity': orders_info['orders_quantity'], + 'production_launched': max(orders_info['production_launched'], production_info['production_launched']), + 'production_finished': production_info['production_finished'], + 'orders_delivered': production_info['orders_delivered'], + 'unique_customers': max(orders_info['unique_customers'], production_info['unique_customers']) + } + + history_data.append(day_data) + + return history_data + + except Exception as e: + print(f"Error getting historical data: {e}") + return [] + + def generate_trend_analysis(self, history_data): + """Generate trend analysis from historical data""" + try: + if not history_data or len(history_data) < 2: + return None + + # Calculate moving averages and trends + trends = { + 'orders_quantity': [], + 'production_efficiency': [], + 'daily_performance': [] + } + + for day in history_data: + trends['orders_quantity'].append({ + 'date': day['date'], + 'value': day['orders_quantity'] + }) + + # Calculate efficiency rates + orders_qty = day['orders_quantity'] + if orders_qty > 0: + launch_rate = round((day['production_launched'] / orders_qty * 100), 1) + completion_rate = round((day['production_finished'] / orders_qty * 100), 1) + delivery_rate = round((day['orders_delivered'] / orders_qty * 100), 1) + else: + launch_rate = completion_rate = delivery_rate = 0 + + trends['production_efficiency'].append({ + 'date': day['date'], + 'launch_rate': launch_rate, + 'completion_rate': completion_rate, + 'delivery_rate': delivery_rate + }) + + trends['daily_performance'].append({ + 'date': day['date'], + 'orders_quantity': day['orders_quantity'], + 'production_launched': day['production_launched'], + 'production_finished': day['production_finished'], + 'orders_delivered': day['orders_delivered'] + }) + + return trends + + except Exception as e: + print(f"Error generating trend analysis: {e}") + return None + + +# Initialize the Daily Mirror manager +daily_mirror_manager = DailyMirrorManager() + + +# Route handler functions +@daily_mirror_bp.route('/main') +def daily_mirror_main_route(): + """Main Daily Mirror page - central hub for all Daily Mirror functionalities""" + access_check = check_daily_mirror_access() + if access_check: + return access_check + + try: + from datetime import datetime + + # Get current date for default values + today = datetime.now().strftime('%Y-%m-%d') + + # Get quick stats for dashboard display + quick_stats = daily_mirror_manager.get_daily_production_data(today) + + return render_template('daily_mirror_main.html', + today=today, + quick_stats=quick_stats, + module_info={ + 'name': daily_mirror_manager.module_name, + 'display_name': daily_mirror_manager.module_display_name, + 'description': daily_mirror_manager.module_description + }) + + except Exception as e: + print(f"Error loading Daily Mirror main page: {e}") + flash('Error loading Daily Mirror main page.', 'error') + return redirect(url_for('main.dashboard')) + + +@daily_mirror_bp.route('/') +def daily_mirror_route(): + """Daily Mirror report generation page""" + access_check = check_daily_mirror_access() + if access_check: + return access_check + + try: + from datetime import datetime + + # Get current date for default values + today = datetime.now().strftime('%Y-%m-%d') + + return render_template('daily_mirror.html', today=today) + + except Exception as e: + print(f"Error loading Daily Mirror report page: {e}") + flash('Error loading Daily Mirror report page.', 'error') + return redirect(url_for('daily_mirror.daily_mirror_main_route')) + + +@daily_mirror_bp.route('/history') +def daily_mirror_history_route(): + """Daily Mirror history and trend analysis page""" + access_check = check_daily_mirror_access() + if access_check: + return access_check + + try: + from datetime import datetime, timedelta + + # Get last 30 days of data for history view + end_date = datetime.now() + start_date = end_date - timedelta(days=30) + + return render_template('daily_mirror_history.html', + start_date=start_date.strftime('%Y-%m-%d'), + end_date=end_date.strftime('%Y-%m-%d')) + + except Exception as e: + print(f"Error loading Daily Mirror history page: {e}") + flash('Error loading Daily Mirror history page.', 'error') + return redirect(url_for('daily_mirror.daily_mirror_main_route')) + + +@daily_mirror_bp.route('/build_database', methods=['GET', 'POST']) +def daily_mirror_build_database(): + """Daily Mirror - Build Database: Upload Excel files to populate database tables""" + access_check = check_daily_mirror_access() + if access_check: + return access_check + + if request.method == 'POST': + is_ajax = request.headers.get('X-Requested-With') == 'XMLHttpRequest' + try: + # Check if file was uploaded + if 'excel_file' not in request.files: + if is_ajax: + return jsonify({'error': 'No file selected.'}), 400 + flash('No file selected.', 'error') + return redirect(request.url) + file = request.files['excel_file'] + if file.filename == '': + if is_ajax: + return jsonify({'error': 'No file selected.'}), 400 + flash('No file selected.', 'error') + return redirect(request.url) + if not file.filename.lower().endswith(('.xlsx', '.xls')): + if is_ajax: + return jsonify({'error': 'Please upload an Excel file (.xlsx or .xls).'}), 400 + flash('Please upload an Excel file (.xlsx or .xls).', 'error') + return redirect(request.url) + target_table = request.form.get('target_table', '') + if not target_table: + if is_ajax: + return jsonify({'error': 'Please select a target table.'}), 400 + flash('Please select a target table.', 'error') + return redirect(request.url) + filename = secure_filename(file.filename) + temp_path = os.path.join('/tmp', f'upload_{filename}') + file.save(temp_path) + try: + success_count = 0 + error_count = 0 + created_rows = 0 + updated_rows = 0 + try: + df = pd.read_excel(temp_path) + except Exception as excel_error: + if is_ajax: + return jsonify({'error': f'Error reading Excel file: {str(excel_error)}'}), 400 + flash(f'Error reading Excel file: {str(excel_error)}', 'error') + return redirect(request.url) + dm_db = DailyMirrorDatabase() + if not dm_db.connect(): + if is_ajax: + return jsonify({'error': 'Database connection failed.'}), 500 + flash('Database connection failed.', 'error') + return redirect(request.url) + try: + result = None + if target_table == 'production_data': + result = dm_db.import_production_data(temp_path) + elif target_table == 'orders_data': + result = dm_db.import_orders_data(temp_path) + elif target_table == 'delivery_data': + result = dm_db.import_delivery_data(temp_path) + else: + if is_ajax: + return jsonify({'error': f'Unknown target table: {target_table}'}), 400 + flash(f'Unknown target table: {target_table}', 'error') + return redirect(request.url) + if result: + success_count = result.get('success_count', 0) + error_count = result.get('error_count', 0) + total_rows = result.get('total_rows', 0) + created_rows = result.get('created_count', 0) + updated_rows = result.get('updated_count', 0) + + if is_ajax: + return jsonify({ + 'total_rows': total_rows, + 'created_rows': created_rows, + 'updated_rows': updated_rows, + 'error_count': error_count + }) + else: + if is_ajax: + return jsonify({'error': 'Import failed.'}), 500 + flash('Import failed.', 'error') + return redirect(request.url) + finally: + dm_db.disconnect() + finally: + if os.path.exists(temp_path): + os.remove(temp_path) + except Exception as e: + if is_ajax: + return jsonify({'error': f'Error processing file: {str(e)}'}), 500 + flash(f'Error processing file: {str(e)}', 'error') + + # For GET request, show the upload form + try: + # Get available tables for the dropdown + + # Get list of tables (customized for our database schema) + available_tables = [ + { + 'name': 'production_data', + 'display': 'Production Data (Comenzi Productie)', + 'description': 'Production orders with timeline, quality stages, and machine data' + }, + { + 'name': 'orders_data', + 'display': 'Orders Data (Vizual. Artic. Comenzi Deschise)', + 'description': 'Open orders with customer, article, and delivery information' + }, + { + 'name': 'delivery_data', + 'display': 'Delivery Data (Articole livrate)', + 'description': 'Shipped and delivered orders with dates and quantities' + } + ] + + return render_template('daily_mirror_build_database.html', + available_tables=available_tables) + + except Exception as e: + print(f"Error loading Build Database page: {e}") + flash('Error loading Build Database page.', 'error') + return redirect(url_for('daily_mirror.daily_mirror_main_route')) + + +@daily_mirror_bp.route('/api/data', methods=['GET']) +def api_daily_mirror_data(): + """API endpoint to get daily production data for reports""" + access_check = check_daily_mirror_api_access() + if access_check: + return access_check + + try: + # Get date parameter or use today + report_date = request.args.get('date', datetime.now().strftime('%Y-%m-%d')) + + print(f"DEBUG: Getting daily mirror data for date: {report_date}") + + # Use the manager to get data + report_data = daily_mirror_manager.get_daily_production_data(report_date) + + if report_data is None: + return jsonify({'error': 'Failed to retrieve daily production data'}), 500 + + print(f"DEBUG: Daily mirror data retrieved successfully for {report_date}") + return jsonify(report_data) + + except Exception as e: + print(f"Error getting daily mirror data: {e}") + return jsonify({'error': str(e)}), 500 + + +@daily_mirror_bp.route('/api/history_data', methods=['GET']) +def api_daily_mirror_history_data(): + """API endpoint to get historical daily production data""" + access_check = check_daily_mirror_api_access() + if access_check: + return access_check + + try: + from datetime import datetime, timedelta + + # Get date range parameters + start_date = request.args.get('start_date') + end_date = request.args.get('end_date') + + if not start_date or not end_date: + # Default to last 30 days + end_date_obj = datetime.now() + start_date_obj = end_date_obj - timedelta(days=30) + start_date = start_date_obj.strftime('%Y-%m-%d') + end_date = end_date_obj.strftime('%Y-%m-%d') + + print(f"DEBUG: Getting daily mirror history from {start_date} to {end_date}") + + # Use the manager to get historical data + history_result = daily_mirror_manager.get_historical_data(start_date, end_date) + + if history_result is None: + return jsonify({'error': 'Failed to retrieve historical data'}), 500 + + # Generate trend analysis + trends = daily_mirror_manager.generate_trend_analysis(history_result['history']) + if trends: + history_result['trends'] = trends + + print(f"DEBUG: Retrieved {history_result['total_days']} days of history data") + return jsonify(history_result) + + except Exception as e: + print(f"Error getting daily mirror history data: {e}") + return jsonify({'error': str(e)}), 500 + + +# ============================================= +# TUNE DATABASE ROUTES +# ============================================= + +@daily_mirror_bp.route('/tune/production') +def tune_production_data(): + """Tune Production Orders Data - Edit and update production records""" + access_check = check_daily_mirror_access() + if access_check: + return access_check + + return render_template('daily_mirror_tune_production.html') + + +@daily_mirror_bp.route('/tune/orders') +def tune_orders_data(): + """Tune Customer Orders Data - Edit and update order records""" + access_check = check_daily_mirror_access() + if access_check: + return access_check + + return render_template('daily_mirror_tune_orders.html') + +@daily_mirror_bp.route('/api/tune/orders_data', methods=['GET']) +def api_get_orders_data(): + """API endpoint to get orders data for editing""" + access_check = check_daily_mirror_access() + if access_check: + return access_check + + try: + db = DailyMirrorDatabase() + db.connect() + cursor = db.connection.cursor() + + # Get pagination parameters + page = int(request.args.get('page', 1)) + per_page = int(request.args.get('per_page', 50)) + search = request.args.get('search', '').strip() + status_filter = request.args.get('status', '').strip() + customer_filter = request.args.get('customer', '').strip() + + # Build WHERE clause for filters + where_conditions = [] + params = [] + + if search: + where_conditions.append(""" + (order_id LIKE ? OR customer_name LIKE ? OR article_code LIKE ? OR + article_description LIKE ? OR client_order LIKE ?) + """) + search_param = f'%{search}%' + params.extend([search_param] * 5) + + if status_filter: + where_conditions.append("order_status = ?") + params.append(status_filter) + + if customer_filter: + where_conditions.append("customer_code = ?") + params.append(customer_filter) + + where_clause = "" + if where_conditions: + where_clause = "WHERE " + " AND ".join(where_conditions) + + # Get total count + count_query = f"SELECT COUNT(*) FROM dm_orders {where_clause}" + cursor.execute(count_query, params) + total_records = cursor.fetchone()[0] + + # Calculate offset + offset = (page - 1) * per_page + + # Get paginated data + data_query = f""" + SELECT id, order_id, customer_code, customer_name, client_order, + article_code, article_description, quantity_requested, + delivery_date, order_status, priority, product_group, order_date + FROM dm_orders {where_clause} + ORDER BY order_date DESC, order_id + LIMIT ? OFFSET ? + """ + cursor.execute(data_query, params + [per_page, offset]) + records = cursor.fetchall() + + # Format data for JSON response + data = [] + for record in records: + data.append({ + 'id': record[0], + 'order_id': record[1], + 'customer_code': record[2], + 'customer_name': record[3], + 'client_order': record[4], + 'article_code': record[5], + 'article_description': record[6], + 'quantity_requested': record[7], + 'delivery_date': record[8].strftime('%Y-%m-%d') if record[8] else '', + 'order_status': record[9], + 'priority': record[10], + 'product_group': record[11], + 'order_date': record[12].strftime('%Y-%m-%d') if record[12] else '' + }) + + # Get unique customers for filter dropdown + cursor.execute("SELECT DISTINCT customer_code, customer_name FROM dm_orders ORDER BY customer_name") + customers = [{'code': row[0], 'name': row[1]} for row in cursor.fetchall()] + + # Get unique statuses for filter dropdown + cursor.execute("SELECT DISTINCT order_status FROM dm_orders WHERE order_status IS NOT NULL ORDER BY order_status") + statuses = [row[0] for row in cursor.fetchall()] + + return jsonify({ + 'success': True, + 'data': data, + 'total_records': total_records, + 'page': page, + 'per_page': per_page, + 'total_pages': (total_records + per_page - 1) // per_page, + 'customers': customers, + 'statuses': statuses + }) + + except Exception as e: + current_app.logger.error(f"Error getting orders data: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + +@daily_mirror_bp.route('/api/tune/orders_data/', methods=['PUT']) +def api_update_orders_data(record_id): + """API endpoint to update orders record""" + access_check = check_daily_mirror_access() + if access_check: + return access_check + + try: + data = request.get_json() + + db = DailyMirrorDatabase() + db.connect() + cursor = db.connection.cursor() + + # Update the record + update_query = """ + UPDATE dm_orders SET + customer_code = ?, customer_name = ?, client_order = ?, + article_code = ?, article_description = ?, quantity_requested = ?, + delivery_date = ?, order_status = ?, priority = ?, product_group = ?, + order_date = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ? + """ + + cursor.execute(update_query, ( + data['customer_code'], data['customer_name'], data['client_order'], + data['article_code'], data['article_description'], data['quantity_requested'], + data['delivery_date'] if data['delivery_date'] else None, + data['order_status'], data['priority'], data['product_group'], + data['order_date'] if data['order_date'] else None, + record_id + )) + + db.connection.commit() + + return jsonify({'success': True, 'message': 'Order updated successfully'}) + + except Exception as e: + current_app.logger.error(f"Error updating orders record: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + + +@daily_mirror_bp.route('/tune/delivery') +def tune_delivery_data(): + """Tune Delivery Records Data - Edit and update delivery information""" + access_check = check_daily_mirror_access() + if access_check: + return access_check + + return render_template('daily_mirror_tune_delivery.html') + +@daily_mirror_bp.route('/api/tune/delivery_data', methods=['GET']) +def api_get_delivery_data(): + """API endpoint to get delivery data for editing""" + access_check = check_daily_mirror_access() + if access_check: + return access_check + + try: + db = DailyMirrorDatabase() + db.connect() + cursor = db.connection.cursor() + + # Get pagination parameters + page = int(request.args.get('page', 1)) + per_page = int(request.args.get('per_page', 50)) + search = request.args.get('search', '').strip() + status_filter = request.args.get('status', '').strip() + customer_filter = request.args.get('customer', '').strip() + + # Build WHERE clause for filters + where_conditions = [] + params = [] + + if search: + where_conditions.append(""" + (shipment_id LIKE ? OR customer_name LIKE ? OR article_code LIKE ? OR + article_description LIKE ? OR order_id LIKE ?) + """) + search_param = f'%{search}%' + params.extend([search_param] * 5) + + if status_filter: + where_conditions.append("delivery_status = ?") + params.append(status_filter) + + if customer_filter: + where_conditions.append("customer_code = ?") + params.append(customer_filter) + + where_clause = "" + if where_conditions: + where_clause = "WHERE " + " AND ".join(where_conditions) + + # Get total count + count_query = f"SELECT COUNT(*) FROM dm_deliveries {where_clause}" + cursor.execute(count_query, params) + total_records = cursor.fetchone()[0] + + # Calculate offset + offset = (page - 1) * per_page + + # Get paginated data + data_query = f""" + SELECT id, shipment_id, order_id, customer_code, customer_name, + article_code, article_description, quantity_delivered, + shipment_date, delivery_date, delivery_status, total_value + FROM dm_deliveries {where_clause} + ORDER BY shipment_date DESC, shipment_id + LIMIT ? OFFSET ? + """ + cursor.execute(data_query, params + [per_page, offset]) + records = cursor.fetchall() + + # Format data for JSON response + data = [] + for record in records: + data.append({ + 'id': record[0], + 'shipment_id': record[1], + 'order_id': record[2], + 'customer_code': record[3], + 'customer_name': record[4], + 'article_code': record[5], + 'article_description': record[6], + 'quantity_delivered': record[7], + 'shipment_date': record[8].strftime('%Y-%m-%d') if record[8] else '', + 'delivery_date': record[9].strftime('%Y-%m-%d') if record[9] else '', + 'delivery_status': record[10], + 'total_value': float(record[11]) if record[11] else 0.0 + }) + + # Get unique customers for filter dropdown + cursor.execute("SELECT DISTINCT customer_code, customer_name FROM dm_deliveries ORDER BY customer_name") + customers = [{'code': row[0], 'name': row[1]} for row in cursor.fetchall()] + + # Get unique statuses for filter dropdown + cursor.execute("SELECT DISTINCT delivery_status FROM dm_deliveries WHERE delivery_status IS NOT NULL ORDER BY delivery_status") + statuses = [row[0] for row in cursor.fetchall()] + + return jsonify({ + 'success': True, + 'data': data, + 'total_records': total_records, + 'page': page, + 'per_page': per_page, + 'total_pages': (total_records + per_page - 1) // per_page, + 'customers': customers, + 'statuses': statuses + }) + + except Exception as e: + current_app.logger.error(f"Error getting delivery data: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + +@daily_mirror_bp.route('/api/tune/delivery_data/', methods=['PUT']) +def api_update_delivery_data(record_id): + """API endpoint to update delivery record""" + access_check = check_daily_mirror_access() + if access_check: + return access_check + + try: + data = request.get_json() + + db = DailyMirrorDatabase() + db.connect() + cursor = db.connection.cursor() + + # Update the record + update_query = """ + UPDATE dm_deliveries SET + customer_code = ?, customer_name = ?, order_id = ?, + article_code = ?, article_description = ?, quantity_delivered = ?, + shipment_date = ?, delivery_date = ?, delivery_status = ?, + total_value = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ? + """ + + cursor.execute(update_query, ( + data['customer_code'], data['customer_name'], data['order_id'], + data['article_code'], data['article_description'], data['quantity_delivered'], + data['shipment_date'] if data['shipment_date'] else None, + data['delivery_date'] if data['delivery_date'] else None, + data['delivery_status'], data['total_value'], + record_id + )) + + db.connection.commit() + + return jsonify({'success': True, 'message': 'Delivery record updated successfully'}) + + except Exception as e: + current_app.logger.error(f"Error updating delivery record: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + + +@daily_mirror_bp.route('/api/tune/production_data', methods=['GET']) +def api_get_production_data(): + """API endpoint to get production data for editing""" + access_check = check_daily_mirror_api_access() + if access_check: + return access_check + + try: + # Get pagination parameters + page = int(request.args.get('page', 1)) + per_page = int(request.args.get('per_page', 50)) + search = request.args.get('search', '') + filter_status = request.args.get('status', '') + filter_customer = request.args.get('customer', '') + + dm_db = DailyMirrorDatabase() + dm_db.connect() + cursor = dm_db.connection.cursor() + + # Build the query with filters + where_conditions = [] + params = [] + + if search: + where_conditions.append("(production_order LIKE %s OR customer_code LIKE %s OR article_code LIKE %s)") + params.extend([f"%{search}%", f"%{search}%", f"%{search}%"]) + + if filter_status: + where_conditions.append("production_status = %s") + params.append(filter_status) + + if filter_customer: + where_conditions.append("customer_code = %s") + params.append(filter_customer) + + where_clause = "WHERE " + " AND ".join(where_conditions) if where_conditions else "" + + # Get total count + count_query = f"SELECT COUNT(*) FROM dm_production_orders {where_clause}" + cursor.execute(count_query, params) + total_records = cursor.fetchone()[0] + + # Get paginated data + offset = (page - 1) * per_page + data_query = f""" + SELECT id, production_order, customer_code, customer_name, client_order, + article_code, article_description, quantity_requested, delivery_date, + production_status, end_of_quilting, end_of_sewing, machine_code, + data_planificare + FROM dm_production_orders {where_clause} + ORDER BY data_planificare DESC, production_order + LIMIT %s OFFSET %s + """ + cursor.execute(data_query, params + [per_page, offset]) + + records = [] + for row in cursor.fetchall(): + records.append({ + 'id': row[0], + 'production_order': row[1], + 'customer_code': row[2], + 'customer_name': row[3], + 'client_order': row[4], + 'article_code': row[5], + 'article_description': row[6], + 'quantity_requested': row[7], + 'delivery_date': str(row[8]) if row[8] else None, + 'production_status': row[9], + 'end_of_quilting': str(row[10]) if row[10] else None, + 'end_of_sewing': str(row[11]) if row[11] else None, + 'machine_code': row[12], + 'data_planificare': str(row[13]) if row[13] else None + }) + + dm_db.disconnect() + + return jsonify({ + 'records': records, + 'total': total_records, + 'page': page, + 'per_page': per_page, + 'total_pages': (total_records + per_page - 1) // per_page + }) + + except Exception as e: + print(f"Error getting production data: {e}") + return jsonify({'error': str(e)}), 500 + + +@daily_mirror_bp.route('/api/tune/production_data/', methods=['PUT']) +def api_update_production_data(record_id): + """API endpoint to update production record""" + access_check = check_daily_mirror_api_access() + if access_check: + return access_check + + try: + data = request.get_json() + dm_db = DailyMirrorDatabase() + dm_db.connect() + cursor = dm_db.connection.cursor() + + update_query = """ + UPDATE dm_production_orders SET + customer_code = %s, customer_name = %s, client_order = %s, + article_code = %s, article_description = %s, quantity_requested = %s, + delivery_date = %s, production_status = %s, machine_code = %s, + updated_at = CURRENT_TIMESTAMP + WHERE id = %s + """ + + cursor.execute(update_query, ( + data.get('customer_code'), + data.get('customer_name'), + data.get('client_order'), + data.get('article_code'), + data.get('article_description'), + data.get('quantity_requested'), + data.get('delivery_date') if data.get('delivery_date') else None, + data.get('production_status'), + data.get('machine_code'), + record_id + )) + + dm_db.connection.commit() + dm_db.disconnect() + + return jsonify({'success': True, 'message': 'Production record updated successfully'}) + + except Exception as e: + print(f"Error updating production data: {e}") + return jsonify({'error': str(e)}), 500 \ No newline at end of file diff --git a/py_app/app/daily_mirror_database_schema.sql b/py_app/app/daily_mirror_database_schema.sql new file mode 100644 index 0000000..99db030 --- /dev/null +++ b/py_app/app/daily_mirror_database_schema.sql @@ -0,0 +1,320 @@ +-- Daily Mirror Database Schema +-- Quality Recticel Production Tracking System +-- Created: October 24, 2025 + +-- ============================================= +-- ORDERS DATA TABLES +-- ============================================= + +-- Main Orders Table (from Vizual. Artic. Comenzi Deschise) +CREATE TABLE IF NOT EXISTS dm_orders ( + id INT AUTO_INCREMENT PRIMARY KEY, + order_id VARCHAR(50) UNIQUE NOT NULL, + customer_code VARCHAR(50), + customer_name VARCHAR(255), + client_order VARCHAR(100), + article_code VARCHAR(50), + article_description TEXT, + quantity_requested INT, + delivery_date DATE, + order_status VARCHAR(50), + priority VARCHAR(20), + product_group VARCHAR(100), + order_date DATE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + INDEX idx_order_id (order_id), + INDEX idx_customer (customer_code), + INDEX idx_article (article_code), + INDEX idx_delivery_date (delivery_date), + INDEX idx_order_date (order_date), + INDEX idx_status (order_status) +); + +-- ============================================= +-- PRODUCTION DATA TABLES +-- ============================================= + +-- Production Orders Table (from Comenzi Productie) +CREATE TABLE IF NOT EXISTS dm_production_orders ( + id INT AUTO_INCREMENT PRIMARY KEY, + production_order VARCHAR(50) UNIQUE NOT NULL, + order_id VARCHAR(50), + customer_code VARCHAR(50), + customer_name VARCHAR(255), + client_order VARCHAR(100), + article_code VARCHAR(50), + article_description TEXT, + quantity_requested INT, + delivery_date DATE, + production_status VARCHAR(50), + + -- Production Timeline + end_of_quilting DATETIME, + end_of_sewing DATETIME, + data_deschiderii DATE, + data_planificare DATE, + + -- Quality Control Stages + t1_status DECIMAL(3,1), + t1_registration_date DATETIME, + t1_operator_name VARCHAR(100), + t2_status DECIMAL(3,1), + t2_registration_date DATETIME, + t2_operator_name VARCHAR(100), + t3_status DECIMAL(3,1), + t3_registration_date DATETIME, + t3_operator_name VARCHAR(100), + + -- Machine and Production Details + machine_code VARCHAR(50), + machine_type VARCHAR(50), + machine_number VARCHAR(20), + classification VARCHAR(100), + design_number INT, + needle_position INT, + total_norm_time DECIMAL(8,2), + model_lb2 VARCHAR(255), + + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + INDEX idx_production_order (production_order), + INDEX idx_order_id (order_id), + INDEX idx_customer (customer_code), + INDEX idx_article (article_code), + INDEX idx_delivery_date (delivery_date), + INDEX idx_status (production_status), + INDEX idx_machine (machine_code), + INDEX idx_quilting_date (end_of_quilting), + INDEX idx_sewing_date (end_of_sewing) +); + +-- ============================================= +-- DELIVERY DATA TABLES +-- ============================================= + +-- Delivery/Shipment Table (from Articole livrate) +CREATE TABLE IF NOT EXISTS dm_deliveries ( + id INT AUTO_INCREMENT PRIMARY KEY, + shipment_id VARCHAR(50) UNIQUE NOT NULL, + order_id VARCHAR(50), + production_order VARCHAR(50), + customer_code VARCHAR(50), + customer_name VARCHAR(255), + article_code VARCHAR(50), + article_description TEXT, + quantity_delivered INT, + quantity_returned INT DEFAULT 0, + + -- Delivery Timeline + shipment_date DATE, + delivery_date DATE, + return_date DATE, + + -- Delivery Status + delivery_status VARCHAR(50), -- 'shipped', 'delivered', 'returned', 'partial' + shipping_method VARCHAR(100), + tracking_number VARCHAR(100), + shipping_address TEXT, + delivery_notes TEXT, + + -- Financial + unit_price DECIMAL(10,2), + total_value DECIMAL(12,2), + currency VARCHAR(3) DEFAULT 'RON', + + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + INDEX idx_shipment_id (shipment_id), + INDEX idx_order_id (order_id), + INDEX idx_production_order (production_order), + INDEX idx_customer (customer_code), + INDEX idx_article (article_code), + INDEX idx_shipment_date (shipment_date), + INDEX idx_delivery_date (delivery_date), + INDEX idx_status (delivery_status) +); + +-- ============================================= +-- DAILY MIRROR AGGREGATION TABLES +-- ============================================= + +-- Daily Summary Table (for fast reporting) +CREATE TABLE IF NOT EXISTS dm_daily_summary ( + id INT AUTO_INCREMENT PRIMARY KEY, + report_date DATE UNIQUE NOT NULL, + + -- Orders Metrics + orders_received INT DEFAULT 0, + orders_quantity INT DEFAULT 0, + orders_value DECIMAL(15,2) DEFAULT 0, + unique_customers INT DEFAULT 0, + + -- Production Metrics + production_launched INT DEFAULT 0, + production_finished INT DEFAULT 0, + production_in_progress INT DEFAULT 0, + quilting_completed INT DEFAULT 0, + sewing_completed INT DEFAULT 0, + + -- Delivery Metrics + orders_shipped INT DEFAULT 0, + orders_delivered INT DEFAULT 0, + orders_returned INT DEFAULT 0, + delivery_value DECIMAL(15,2) DEFAULT 0, + + -- Efficiency Metrics + on_time_deliveries INT DEFAULT 0, + late_deliveries INT DEFAULT 0, + active_operators INT DEFAULT 0, + + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + INDEX idx_report_date (report_date) +); + + +-- ============================================= +-- CONFIGURATION AND LOOKUP TABLES +-- ============================================= + +-- Customer Master +CREATE TABLE IF NOT EXISTS dm_customers ( + id INT AUTO_INCREMENT PRIMARY KEY, + customer_code VARCHAR(50) UNIQUE NOT NULL, + customer_name VARCHAR(255) NOT NULL, + customer_group VARCHAR(100), + country VARCHAR(50), + currency VARCHAR(3) DEFAULT 'RON', + payment_terms VARCHAR(100), + credit_limit DECIMAL(15,2), + active BOOLEAN DEFAULT TRUE, + + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + INDEX idx_customer_code (customer_code), + INDEX idx_customer_name (customer_name), + INDEX idx_customer_group (customer_group) +); + +-- Article Master +CREATE TABLE IF NOT EXISTS dm_articles ( + id INT AUTO_INCREMENT PRIMARY KEY, + article_code VARCHAR(50) UNIQUE NOT NULL, + article_description TEXT NOT NULL, + product_group VARCHAR(100), + classification VARCHAR(100), + unit_of_measure VARCHAR(20), + standard_price DECIMAL(10,2), + standard_time DECIMAL(8,2), + active BOOLEAN DEFAULT TRUE, + + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + INDEX idx_article_code (article_code), + INDEX idx_product_group (product_group), + INDEX idx_classification (classification) +); + +-- Machine Master +CREATE TABLE IF NOT EXISTS dm_machines ( + id INT AUTO_INCREMENT PRIMARY KEY, + machine_code VARCHAR(50) UNIQUE NOT NULL, + machine_name VARCHAR(255), + machine_type VARCHAR(50), + machine_number VARCHAR(20), + department VARCHAR(100), + capacity_per_hour DECIMAL(8,2), + active BOOLEAN DEFAULT TRUE, + + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + + INDEX idx_machine_code (machine_code), + INDEX idx_machine_type (machine_type), + INDEX idx_department (department) +); + +-- ============================================= +-- DATA IMPORT TRACKING +-- ============================================= + +-- Track file uploads and data imports +CREATE TABLE IF NOT EXISTS dm_import_log ( + id INT AUTO_INCREMENT PRIMARY KEY, + file_name VARCHAR(255) NOT NULL, + file_type VARCHAR(50) NOT NULL, -- 'orders', 'production', 'delivery' + upload_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + uploaded_by VARCHAR(100), + records_processed INT DEFAULT 0, + records_successful INT DEFAULT 0, + records_failed INT DEFAULT 0, + status VARCHAR(50) DEFAULT 'processing', -- 'processing', 'completed', 'failed' + error_message TEXT, + processing_time DECIMAL(8,2), -- seconds + + INDEX idx_upload_date (upload_date), + INDEX idx_file_type (file_type), + INDEX idx_status (status), + INDEX idx_uploaded_by (uploaded_by) +); + +-- ============================================= +-- VIEWS FOR DAILY MIRROR REPORTING +-- ============================================= + +-- View: Current Production Status +CREATE OR REPLACE VIEW v_daily_production_status AS +SELECT + DATE(p.data_planificare) as production_date, + COUNT(*) as total_orders, + SUM(p.quantity_requested) as total_quantity, + SUM(CASE WHEN p.production_status = 'Inchis' THEN 1 ELSE 0 END) as completed_orders, + SUM(CASE WHEN p.production_status != 'Inchis' THEN 1 ELSE 0 END) as pending_orders, + SUM(CASE WHEN p.end_of_quilting IS NOT NULL THEN 1 ELSE 0 END) as quilting_done, + SUM(CASE WHEN p.end_of_sewing IS NOT NULL THEN 1 ELSE 0 END) as sewing_done, + COUNT(DISTINCT p.customer_code) as unique_customers, + COUNT(DISTINCT p.machine_code) as machines_used +FROM dm_production_orders p +WHERE p.data_planificare >= CURDATE() - INTERVAL 30 DAY +GROUP BY DATE(p.data_planificare) +ORDER BY production_date DESC; + +-- View: Quality Performance Summary +CREATE OR REPLACE VIEW v_daily_quality_summary AS +SELECT + DATE(p.t1_registration_date) as scan_date, + COUNT(*) as total_t1_scans, + SUM(CASE WHEN p.t1_status = 0 THEN 1 ELSE 0 END) as t1_approved, + ROUND(SUM(CASE WHEN p.t1_status = 0 THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) as t1_approval_rate, + COUNT(CASE WHEN p.t2_registration_date IS NOT NULL THEN 1 END) as total_t2_scans, + SUM(CASE WHEN p.t2_status = 0 THEN 1 ELSE 0 END) as t2_approved, + ROUND(SUM(CASE WHEN p.t2_status = 0 THEN 1 ELSE 0 END) / COUNT(CASE WHEN p.t2_registration_date IS NOT NULL THEN 1 END) * 100, 2) as t2_approval_rate, + COUNT(DISTINCT p.t1_operator_name) as active_operators +FROM dm_production_orders p +WHERE p.t1_registration_date >= CURDATE() - INTERVAL 30 DAY +GROUP BY DATE(p.t1_registration_date) +ORDER BY scan_date DESC; + +-- View: Delivery Performance +CREATE OR REPLACE VIEW v_daily_delivery_summary AS +SELECT + d.delivery_date, + COUNT(*) as total_deliveries, + SUM(d.quantity_delivered) as total_quantity_delivered, + SUM(d.total_value) as total_delivery_value, + SUM(CASE WHEN d.delivery_date <= o.delivery_date THEN 1 ELSE 0 END) as on_time_deliveries, + SUM(CASE WHEN d.delivery_date > o.delivery_date THEN 1 ELSE 0 END) as late_deliveries, + COUNT(DISTINCT d.customer_code) as unique_customers +FROM dm_deliveries d +LEFT JOIN dm_orders o ON d.order_id = o.order_id +WHERE d.delivery_date >= CURDATE() - INTERVAL 30 DAY + AND d.delivery_status = 'delivered' +GROUP BY d.delivery_date +ORDER BY d.delivery_date DESC; \ No newline at end of file diff --git a/py_app/app/daily_mirror_db_setup.py b/py_app/app/daily_mirror_db_setup.py new file mode 100644 index 0000000..f888c52 --- /dev/null +++ b/py_app/app/daily_mirror_db_setup.py @@ -0,0 +1,744 @@ +""" +Daily Mirror Database Setup and Management +Quality Recticel Application + +This script creates the database schema and provides utilities for +data import and Daily Mirror reporting functionality. +""" + +import mariadb +import pandas as pd +import os +from datetime import datetime, timedelta +import logging + +# Setup logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +class DailyMirrorDatabase: + def __init__(self, host='localhost', user='trasabilitate', password='Initial01!', database='trasabilitate'): + self.host = host + self.user = user + self.password = password + self.database = database + self.connection = None + + def connect(self): + """Establish database connection""" + try: + self.connection = mariadb.connect( + host=self.host, + user=self.user, + password=self.password, + database=self.database + ) + logger.info("Database connection established") + return True + except Exception as e: + logger.error(f"Database connection failed: {e}") + return False + + def disconnect(self): + """Close database connection""" + if self.connection: + self.connection.close() + logger.info("Database connection closed") + + def create_database_schema(self): + """Create the Daily Mirror database schema""" + try: + cursor = self.connection.cursor() + + # Read and execute the schema file + schema_file = os.path.join(os.path.dirname(__file__), 'daily_mirror_database_schema.sql') + + if not os.path.exists(schema_file): + logger.error(f"Schema file not found: {schema_file}") + return False + + with open(schema_file, 'r') as file: + schema_sql = file.read() + + # Split by statements and execute each one + statements = [] + current_statement = "" + + for line in schema_sql.split('\n'): + line = line.strip() + if line and not line.startswith('--'): + current_statement += line + " " + if line.endswith(';'): + statements.append(current_statement.strip()) + current_statement = "" + + # Add any remaining statement + if current_statement.strip(): + statements.append(current_statement.strip()) + + for statement in statements: + if statement and any(statement.upper().startswith(cmd) for cmd in ['CREATE', 'ALTER', 'DROP', 'INSERT']): + try: + cursor.execute(statement) + logger.info(f"Executed: {statement[:80]}...") + except Exception as e: + if "already exists" not in str(e).lower(): + logger.warning(f"Error executing statement: {e}") + + self.connection.commit() + logger.info("Database schema created successfully") + return True + + except Exception as e: + logger.error(f"Error creating database schema: {e}") + return False + + def import_production_data(self, file_path): + """Import production data from Excel file (Comenzi Productie format)""" + try: + # The correct data is in the first sheet (DataSheet) + df = None + sheet_used = None + + # Get available sheets + excel_file = pd.ExcelFile(file_path) + logger.info(f"Available sheets: {excel_file.sheet_names}") + + # Try DataSheet first (where the actual production data is), then fallback options + sheet_attempts = [ + ('DataSheet', 'openpyxl'), + ('DataSheet', 'xlrd'), + (0, 'openpyxl'), + (0, 'xlrd'), + ('Sheet1', 'openpyxl'), # fallback to Sheet1 if DataSheet fails + (1, 'openpyxl') + ] + + for sheet_name, engine in sheet_attempts: + try: + logger.info(f"Trying to read sheet '{sheet_name}' with engine '{engine}'") + df = pd.read_excel(file_path, sheet_name=sheet_name, engine=engine, header=0) + sheet_used = f"{sheet_name} (engine: {engine})" + logger.info(f"Successfully read from sheet: {sheet_used}") + break + except Exception as e: + logger.warning(f"Failed to read sheet {sheet_name} with {engine}: {e}") + continue + + # If all engines fail on DataSheet, try a different approach + if df is None: + try: + logger.info("Trying alternative method: reading without specifying engine") + df = pd.read_excel(file_path, sheet_name='DataSheet') + sheet_used = "DataSheet (default engine)" + logger.info("Successfully read with default engine") + except Exception as e: + logger.error(f"Failed with default engine: {e}") + raise Exception("Could not read the DataSheet from the Excel file. The file may be corrupted.") + + logger.info(f"Loaded production data from {sheet_used}: {len(df)} rows, {len(df.columns)} columns") + logger.info(f"Available columns: {list(df.columns)}") + + cursor = self.connection.cursor() + success_count = 0 + created_count = 0 + updated_count = 0 + error_count = 0 + + # Prepare insert statement + insert_sql = """ + INSERT INTO dm_production_orders ( + production_order, customer_code, client_order, article_code, + article_description, quantity_requested, delivery_date, production_status, + end_of_quilting, end_of_sewing, t1_status, t1_registration_date, t1_operator_name, + t2_status, t2_registration_date, t2_operator_name, t3_status, t3_registration_date, + t3_operator_name, machine_code, machine_type, classification, total_norm_time, + data_deschiderii, model_lb2, data_planificare, machine_number, design_number, needle_position + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + customer_code = VALUES(customer_code), + client_order = VALUES(client_order), + article_code = VALUES(article_code), + article_description = VALUES(article_description), + quantity_requested = VALUES(quantity_requested), + delivery_date = VALUES(delivery_date), + production_status = VALUES(production_status), + updated_at = CURRENT_TIMESTAMP + """ + + for index, row in df.iterrows(): + try: + # Prepare data tuple + data = ( + row.get('Comanda Productie', ''), + row.get('Customer', ''), + row.get('Comanda client', ''), + row.get('Cod Articol', ''), + row.get('Descriere', ''), + row.get('Cantitate ceruta', 0), + self._parse_date(row.get('Delivery date')), + row.get('Status', ''), + self._parse_datetime(row.get('End of Quilting')), + self._parse_datetime(row.get('End of sewing')), + row.get('T1', 0), + self._parse_datetime(row.get('Data inregistrare T1')), + row.get('Numele Complet T1', ''), + row.get('T2', 0), + self._parse_datetime(row.get('Data inregistrare T2')), + row.get('Numele Complet T2', ''), + row.get('T3', 0), + self._parse_datetime(row.get('Data inregistrare T3')), + row.get('Numele Complet T3', ''), + row.get('Masina Cusut ', ''), + row.get('Tip Masina', ''), + row.get('Clasificare', ''), + row.get('Timp normat total', 0), + self._parse_date(row.get('Data Deschiderii')), + row.get('Model Lb2', ''), + self._parse_date(row.get('Data Planific.')), + row.get('Numar masina', ''), + row.get('Design nr', 0), + row.get('Needle position', 0) + ) + + cursor.execute(insert_sql, data) + + # Check if row was inserted (created) or updated + # In MySQL with ON DUPLICATE KEY UPDATE: + # - rowcount = 1 means INSERT (new row created) + # - rowcount = 2 means UPDATE (existing row updated) + # - rowcount = 0 means no change + if cursor.rowcount == 1: + created_count += 1 + elif cursor.rowcount == 2: + updated_count += 1 + + success_count += 1 + + except Exception as row_error: + logger.warning(f"Error processing row {index}: {row_error}") + error_count += 1 + continue + + self.connection.commit() + logger.info(f"Production data import completed: {success_count} successful, {error_count} failed") + + return { + 'success_count': success_count, + 'created_count': created_count, + 'updated_count': updated_count, + 'error_count': error_count, + 'total_rows': len(df) + } + + except Exception as e: + logger.error(f"Error importing production data: {e}") + return None + + def import_orders_data(self, file_path): + """Import orders data from Excel file with enhanced error handling""" + try: + # Ensure we have a database connection + if not self.connection: + self.connect() + if not self.connection: + return { + 'success_count': 0, + 'error_count': 1, + 'total_rows': 0, + 'error_message': 'Could not establish database connection.' + } + + logger.info(f"Attempting to import orders data from: {file_path}") + + # Check if file exists + if not os.path.exists(file_path): + logger.error(f"Orders file not found: {file_path}") + return { + 'success_count': 0, + 'error_count': 1, + 'total_rows': 0, + 'error_message': f'Orders file not found: {file_path}' + } + + # Try to get sheet names first + try: + excel_file = pd.ExcelFile(file_path) + sheet_names = excel_file.sheet_names + logger.info(f"Available sheets in orders file: {sheet_names}") + except Exception as e: + logger.warning(f"Could not get sheet names: {e}") + sheet_names = ['DataSheet', 'Sheet1'] + + # Try multiple approaches to read the Excel file + df = None + sheet_used = None + approaches = [ + ('openpyxl', 0, 'read_only'), + ('openpyxl', 0, 'normal'), + ('openpyxl', 1, 'normal'), + ('xlrd', 0, 'normal') if file_path.endswith('.xls') else None, + ('default', 0, 'normal') + ] + + for approach in approaches: + if approach is None: + continue + + engine, sheet_name, mode = approach + try: + logger.info(f"Trying to read orders with engine: {engine}, sheet: {sheet_name}, mode: {mode}") + + if engine == 'default': + df = pd.read_excel(file_path, sheet_name=sheet_name, header=0) + elif mode == 'read_only': + df = pd.read_excel(file_path, sheet_name=sheet_name, engine=engine, header=0) + else: + df = pd.read_excel(file_path, sheet_name=sheet_name, engine=engine, header=0) + + sheet_used = f"{engine} (sheet: {sheet_name}, mode: {mode})" + logger.info(f"Successfully read orders data with: {sheet_used}") + break + + except Exception as e: + logger.warning(f"Failed with {engine}, sheet {sheet_name}, mode {mode}: {e}") + continue + + if df is None: + logger.error("Could not read the orders file with any method") + return { + 'success_count': 0, + 'error_count': 1, + 'total_rows': 0, + 'error_message': 'Could not read the orders Excel file. The file may have formatting issues or be corrupted.' + } + + logger.info(f"Loaded orders data from {sheet_used}: {len(df)} rows, {len(df.columns)} columns") + logger.info(f"Available columns: {list(df.columns)[:10]}...") + + cursor = self.connection.cursor() + success_count = 0 + created_count = 0 + updated_count = 0 + error_count = 0 + + # Prepare insert statement for orders + insert_sql = """ + INSERT INTO dm_orders ( + order_id, customer_code, customer_name, client_order, + article_code, article_description, quantity_requested, delivery_date, + order_status, product_group, order_date + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + customer_code = VALUES(customer_code), + customer_name = VALUES(customer_name), + client_order = VALUES(client_order), + article_code = VALUES(article_code), + article_description = VALUES(article_description), + quantity_requested = VALUES(quantity_requested), + delivery_date = VALUES(delivery_date), + order_status = VALUES(order_status), + product_group = VALUES(product_group), + order_date = VALUES(order_date), + updated_at = CURRENT_TIMESTAMP + """ + + # Process each row with the actual column mapping and better null handling + for index, row in df.iterrows(): + try: + # Helper function to safely get values and handle NaN + def safe_get(row, column, default=''): + value = row.get(column, default) + if pd.isna(value) or value == 'nan': + return default + return str(value).strip() if isinstance(value, str) else value + + def safe_get_int(row, column, default=0): + value = row.get(column, default) + if pd.isna(value) or value == 'nan': + return default + try: + return int(float(value)) if value != '' else default + except (ValueError, TypeError): + return default + + # Map columns based on the actual Vizual. Artic. Comenzi Deschise format + data = ( + safe_get(row, 'Comanda', f'ORD_{index:06d}'), # Order ID + safe_get(row, 'Cod. Client'), # Customer Code + safe_get(row, 'Customer Name'), # Customer Name + safe_get(row, 'Com. Achiz. Client'), # Client Order + safe_get(row, 'Cod Articol'), # Article Code + safe_get(row, 'Part Description', safe_get(row, 'Descr. Articol')), # Article Description + safe_get_int(row, 'Cantitate'), # Quantity + self._parse_date(row.get('Data livrare')), # Delivery Date + safe_get(row, 'Statut Comanda', 'PENDING'), # Order Status + safe_get(row, 'Model'), # Product Group + self._parse_date(row.get('Data Comenzii')) # Order Date + ) + + cursor.execute(insert_sql, data) + + # Track created vs updated + if cursor.rowcount == 1: + created_count += 1 + elif cursor.rowcount == 2: + updated_count += 1 + + success_count += 1 + + except Exception as row_error: + logger.warning(f"Error processing row {index}: {row_error}") + error_count += 1 + continue + + self.connection.commit() + logger.info(f"Orders import completed: {success_count} successful, {error_count} errors") + + return { + 'success_count': success_count, + 'created_count': created_count, + 'updated_count': updated_count, + 'error_count': error_count, + 'total_rows': len(df), + 'error_message': None if error_count == 0 else f'{error_count} rows failed to import' + } + + except Exception as e: + logger.error(f"Error importing orders data: {e}") + return { + 'success_count': 0, + 'error_count': 1, + 'total_rows': 0, + 'error_message': str(e) + } + + def import_delivery_data(self, file_path): + """Import delivery data from Excel file with enhanced error handling""" + try: + # Ensure we have a database connection + if not self.connection: + self.connect() + if not self.connection: + return { + 'success_count': 0, + 'error_count': 1, + 'total_rows': 0, + 'error_message': 'Could not establish database connection.' + } + + logger.info(f"Attempting to import delivery data from: {file_path}") + + # Check if file exists + if not os.path.exists(file_path): + logger.error(f"Delivery file not found: {file_path}") + return { + 'success_count': 0, + 'error_count': 1, + 'total_rows': 0, + 'error_message': f'Delivery file not found: {file_path}' + } + + # Try to get sheet names first + try: + excel_file = pd.ExcelFile(file_path) + sheet_names = excel_file.sheet_names + logger.info(f"Available sheets in delivery file: {sheet_names}") + except Exception as e: + logger.warning(f"Could not get sheet names: {e}") + sheet_names = ['DataSheet', 'Sheet1'] + + # Try multiple approaches to read the Excel file + df = None + sheet_used = None + approaches = [ + ('openpyxl', 0, 'read_only'), + ('openpyxl', 0, 'normal'), + ('openpyxl', 1, 'normal'), + ('xlrd', 0, 'normal') if file_path.endswith('.xls') else None, + ('default', 0, 'normal') + ] + + for approach in approaches: + if approach is None: + continue + + engine, sheet_name, mode = approach + try: + logger.info(f"Trying to read delivery data with engine: {engine}, sheet: {sheet_name}, mode: {mode}") + + if engine == 'default': + df = pd.read_excel(file_path, sheet_name=sheet_name, header=0) + elif mode == 'read_only': + df = pd.read_excel(file_path, sheet_name=sheet_name, engine=engine, header=0) + else: + df = pd.read_excel(file_path, sheet_name=sheet_name, engine=engine, header=0) + + sheet_used = f"{engine} (sheet: {sheet_name}, mode: {mode})" + logger.info(f"Successfully read delivery data with: {sheet_used}") + break + + except Exception as e: + logger.warning(f"Failed with {engine}, sheet {sheet_name}, mode {mode}: {e}") + continue + + if df is None: + logger.error("Could not read the delivery file with any method") + return { + 'success_count': 0, + 'error_count': 1, + 'total_rows': 0, + 'error_message': 'Could not read the delivery Excel file. The file may have formatting issues or be corrupted.' + } + + logger.info(f"Loaded delivery data from {sheet_used}: {len(df)} rows, {len(df.columns)} columns") + logger.info(f"Available columns: {list(df.columns)[:10]}...") + + cursor = self.connection.cursor() + success_count = 0 + created_count = 0 + updated_count = 0 + error_count = 0 + + # Prepare insert statement for deliveries + insert_sql = """ + INSERT INTO dm_deliveries ( + shipment_id, order_id, customer_code, customer_name, + article_code, article_description, quantity_delivered, + shipment_date, delivery_date, delivery_status, total_value + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + customer_code = VALUES(customer_code), + customer_name = VALUES(customer_name), + article_code = VALUES(article_code), + article_description = VALUES(article_description), + quantity_delivered = VALUES(quantity_delivered), + shipment_date = VALUES(shipment_date), + delivery_date = VALUES(delivery_date), + delivery_status = VALUES(delivery_status), + total_value = VALUES(total_value), + updated_at = CURRENT_TIMESTAMP + """ + + # Process each row with the actual column mapping and better null handling + for index, row in df.iterrows(): + try: + # Helper function to safely get values and handle NaN + def safe_get(row, column, default=''): + value = row.get(column, default) + if pd.isna(value) or value == 'nan': + return default + return str(value).strip() if isinstance(value, str) else value + + def safe_get_float(row, column, default=0.0): + value = row.get(column, default) + if pd.isna(value) or value == 'nan': + return default + try: + return float(value) if value != '' else default + except (ValueError, TypeError): + return default + + def safe_get_int(row, column, default=0): + value = row.get(column, default) + if pd.isna(value) or value == 'nan': + return default + try: + return int(float(value)) if value != '' else default + except (ValueError, TypeError): + return default + + # Map columns based on the actual Articole livrate_returnate format + data = ( + safe_get(row, 'Document Number', f'SH_{index:06d}'), # Shipment ID + safe_get(row, 'Comanda'), # Order ID + safe_get(row, 'Cod. Client'), # Customer Code + safe_get(row, 'Nume client'), # Customer Name + safe_get(row, 'Cod Articol'), # Article Code + safe_get(row, 'Part Description'), # Article Description + safe_get_int(row, 'Cantitate'), # Quantity Delivered + self._parse_date(row.get('Data')), # Shipment Date + self._parse_date(row.get('Data')), # Delivery Date (same as shipment for now) + safe_get(row, 'Stare', 'DELIVERED'), # Delivery Status + safe_get_float(row, 'Total Price') # Total Value + ) + + cursor.execute(insert_sql, data) + + # Track created vs updated + if cursor.rowcount == 1: + created_count += 1 + elif cursor.rowcount == 2: + updated_count += 1 + + success_count += 1 + + except Exception as row_error: + logger.warning(f"Error processing delivery row {index}: {row_error}") + error_count += 1 + continue + + self.connection.commit() + logger.info(f"Delivery import completed: {success_count} successful, {error_count} errors") + + return { + 'success_count': success_count, + 'created_count': created_count, + 'updated_count': updated_count, + 'error_count': error_count, + 'total_rows': len(df), + 'error_message': None if error_count == 0 else f'{error_count} rows failed to import' + } + + except Exception as e: + logger.error(f"Error importing delivery data: {e}") + return { + 'success_count': 0, + 'error_count': 1, + 'total_rows': 0, + 'error_message': str(e) + } + + def generate_daily_summary(self, report_date=None): + """Generate daily summary for Daily Mirror reporting""" + if not report_date: + report_date = datetime.now().date() + + try: + cursor = self.connection.cursor() + + # Check if summary already exists for this date + cursor.execute("SELECT id FROM dm_daily_summary WHERE report_date = ?", (report_date,)) + existing = cursor.fetchone() + + # Get production metrics + cursor.execute(""" + SELECT + COUNT(*) as total_orders, + SUM(quantity_requested) as total_quantity, + SUM(CASE WHEN production_status = 'Inchis' THEN 1 ELSE 0 END) as completed_orders, + SUM(CASE WHEN end_of_quilting IS NOT NULL THEN 1 ELSE 0 END) as quilting_done, + SUM(CASE WHEN end_of_sewing IS NOT NULL THEN 1 ELSE 0 END) as sewing_done, + COUNT(DISTINCT customer_code) as unique_customers + FROM dm_production_orders + WHERE DATE(data_planificare) = ? + """, (report_date,)) + + production_metrics = cursor.fetchone() + + # Get active operators count + cursor.execute(""" + SELECT COUNT(DISTINCT CASE + WHEN t1_operator_name IS NOT NULL THEN t1_operator_name + WHEN t2_operator_name IS NOT NULL THEN t2_operator_name + WHEN t3_operator_name IS NOT NULL THEN t3_operator_name + END) as active_operators + FROM dm_production_orders + WHERE DATE(data_planificare) = ? + """, (report_date,)) + + operator_metrics = cursor.fetchone() + active_operators = operator_metrics[0] or 0 + + if existing: + # Update existing summary + update_sql = """ + UPDATE dm_daily_summary SET + orders_quantity = ?, production_launched = ?, production_finished = ?, + quilting_completed = ?, sewing_completed = ?, unique_customers = ?, + active_operators = ?, updated_at = CURRENT_TIMESTAMP + WHERE report_date = ? + """ + cursor.execute(update_sql, ( + production_metrics[1] or 0, production_metrics[0] or 0, production_metrics[2] or 0, + production_metrics[3] or 0, production_metrics[4] or 0, production_metrics[5] or 0, + active_operators, report_date + )) + else: + # Insert new summary + insert_sql = """ + INSERT INTO dm_daily_summary ( + report_date, orders_quantity, production_launched, production_finished, + quilting_completed, sewing_completed, unique_customers, active_operators + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) + """ + cursor.execute(insert_sql, ( + report_date, production_metrics[1] or 0, production_metrics[0] or 0, production_metrics[2] or 0, + production_metrics[3] or 0, production_metrics[4] or 0, production_metrics[5] or 0, + active_operators + )) + + self.connection.commit() + logger.info(f"Daily summary generated for {report_date}") + return True + + except Exception as e: + logger.error(f"Error generating daily summary: {e}") + return False + + def clear_production_orders(self): + """Delete all rows from the Daily Mirror production orders table""" + try: + cursor = self.connection.cursor() + cursor.execute("DELETE FROM dm_production_orders") + self.connection.commit() + logger.info("All production orders deleted from dm_production_orders table.") + return True + except Exception as e: + logger.error(f"Error deleting production orders: {e}") + return False + + def _parse_date(self, date_value): + """Parse date with better null handling""" + if pd.isna(date_value) or date_value == 'nan' or date_value is None or date_value == '': + return None + + try: + if isinstance(date_value, str): + # Handle various date formats + for fmt in ['%Y-%m-%d', '%d/%m/%Y', '%m/%d/%Y', '%d.%m.%Y']: + try: + return datetime.strptime(date_value, fmt).date() + except ValueError: + continue + + elif hasattr(date_value, 'date'): + return date_value.date() + elif isinstance(date_value, datetime): + return date_value.date() + + return None # If all parsing attempts fail + + except Exception as e: + logger.warning(f"Error parsing date {date_value}: {e}") + return None + + def _parse_datetime(self, datetime_value): + """Parse datetime value from Excel""" + if pd.isna(datetime_value): + return None + if isinstance(datetime_value, str) and datetime_value == '00:00:00': + return None + return datetime_value + +def setup_daily_mirror_database(): + """Setup the Daily Mirror database schema""" + db = DailyMirrorDatabase() + + if not db.connect(): + return False + + try: + success = db.create_database_schema() + if success: + print("✅ Daily Mirror database schema created successfully!") + + # Generate sample daily summary for today + db.generate_daily_summary() + + return success + finally: + db.disconnect() + +if __name__ == "__main__": + setup_daily_mirror_database() \ No newline at end of file diff --git a/py_app/app/permissions_simple.py b/py_app/app/permissions_simple.py index 9664b2a..a12f65c 100644 --- a/py_app/app/permissions_simple.py +++ b/py_app/app/permissions_simple.py @@ -23,6 +23,13 @@ MODULES = { 'scan_pages': ['move_orders'], 'management_pages': ['create_locations', 'warehouse_reports', 'inventory_management'], 'worker_access': ['move_orders_only'] # Workers can move orders but not create locations + }, + 'daily_mirror': { + 'name': 'Daily Mirror', + 'scan_pages': [], # No scanning, purely reporting/analytics + 'management_pages': ['daily_mirror_main', 'daily_mirror_report', 'daily_mirror_history', 'daily_mirror_analytics'], + 'worker_access': ['view_only'], # Workers can view daily reports but cannot generate or export + 'description': 'Business Intelligence and Production Reporting Module' } } @@ -96,7 +103,14 @@ PAGE_ACCESS = { 'labels': {'min_level': 50, 'modules': ['labels']}, 'label_scan': {'min_level': 50, 'modules': ['labels']}, 'label_creation': {'min_level': 70, 'modules': ['labels']}, # Manager+ only - 'label_reports': {'min_level': 70, 'modules': ['labels']} # Manager+ only + 'label_reports': {'min_level': 70, 'modules': ['labels']}, # Manager+ only + + # Daily Mirror module pages + 'daily_mirror_main': {'min_level': 70, 'modules': ['daily_mirror']}, # Manager+ only + 'daily_mirror_report': {'min_level': 70, 'modules': ['daily_mirror']}, # Manager+ only + 'daily_mirror_history': {'min_level': 70, 'modules': ['daily_mirror']}, # Manager+ only + 'daily_mirror_analytics': {'min_level': 90, 'modules': ['daily_mirror']}, # Admin+ only for advanced analytics + 'daily_mirror': {'min_level': 70, 'modules': ['daily_mirror']} # Legacy route support } def check_access(user_role, user_modules, page): diff --git a/py_app/app/routes.py b/py_app/app/routes.py index f048527..3187d48 100755 --- a/py_app/app/routes.py +++ b/py_app/app/routes.py @@ -3469,6 +3469,40 @@ def delete_location(): return jsonify({'success': False, 'error': str(e)}) +# Daily Mirror Route Redirects for Backward Compatibility +@bp.route('/daily_mirror_main') +def daily_mirror_main_route(): + """Redirect to new Daily Mirror main route""" + return redirect(url_for('daily_mirror.daily_mirror_main_route')) + +@bp.route('/daily_mirror') +def daily_mirror_route(): + """Redirect to new Daily Mirror route""" + return redirect(url_for('daily_mirror.daily_mirror_route')) + +@bp.route('/daily_mirror_history') +def daily_mirror_history_route(): + """Redirect to new Daily Mirror history route""" + return redirect(url_for('daily_mirror.daily_mirror_history_route')) + +@bp.route('/daily_mirror_build_database', methods=['GET', 'POST']) +def daily_mirror_build_database(): + """Redirect to new Daily Mirror build database route""" + if request.method == 'POST': + # For POST requests, we need to forward the data + return redirect(url_for('daily_mirror.daily_mirror_build_database'), code=307) + return redirect(url_for('daily_mirror.daily_mirror_build_database')) + +@bp.route('/api/daily_mirror_data', methods=['GET']) +def api_daily_mirror_data(): + """Redirect to new Daily Mirror API data route""" + return redirect(url_for('daily_mirror.api_daily_mirror_data') + '?' + request.query_string.decode()) + +@bp.route('/api/daily_mirror_history_data', methods=['GET']) +def api_daily_mirror_history_data(): + """Redirect to new Daily Mirror API history data route""" + return redirect(url_for('daily_mirror.api_daily_mirror_history_data') + '?' + request.query_string.decode()) + # NOTE for frontend/extension developers: # To print labels, call the Chrome extension and pass the PDF URL: # /generate_labels_pdf/ diff --git a/py_app/app/static/css/daily_mirror_tune.css b/py_app/app/static/css/daily_mirror_tune.css new file mode 100644 index 0000000..e7485a8 --- /dev/null +++ b/py_app/app/static/css/daily_mirror_tune.css @@ -0,0 +1,241 @@ +/* Daily Mirror Tune Pages - Modal Styles */ +/* Fixes for editable modals across tune/production, tune/orders, and tune/delivery pages */ + +/* Force Bootstrap modal to have proper z-index */ +#editModal.modal { + z-index: 9999 !important; +} + +#editModal .modal-backdrop { + z-index: 9998 !important; +} + +/* Ensure modal dialog is interactive */ +#editModal .modal-dialog { + pointer-events: auto !important; + z-index: 10000 !important; +} + +#editModal .modal-content { + pointer-events: auto !important; +} + +/* Make all inputs in the modal fully interactive */ +#editModal .form-control:not([readonly]), +#editModal .form-select:not([readonly]), +#editModal input:not([readonly]):not([type="hidden"]), +#editModal select:not([readonly]), +#editModal textarea:not([readonly]) { + pointer-events: auto !important; + user-select: text !important; + cursor: text !important; + background-color: #ffffff !important; + color: #000000 !important; + opacity: 1 !important; + -webkit-user-select: text !important; + -moz-user-select: text !important; + -ms-user-select: text !important; +} + +#editModal .form-control:focus:not([readonly]), +#editModal input:focus:not([readonly]), +#editModal select:focus:not([readonly]), +#editModal textarea:focus:not([readonly]) { + background-color: #ffffff !important; + color: #000000 !important; + border-color: #007bff !important; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25) !important; + outline: none !important; +} + +/* Dark mode specific overrides for modal inputs */ +body.dark-mode #editModal .form-control:not([readonly]), +body.dark-mode #editModal input:not([readonly]):not([type="hidden"]), +body.dark-mode #editModal select:not([readonly]), +body.dark-mode #editModal textarea:not([readonly]) { + background-color: #ffffff !important; + color: #000000 !important; + opacity: 1 !important; +} + +body.dark-mode #editModal .form-control:focus:not([readonly]), +body.dark-mode #editModal input:focus:not([readonly]), +body.dark-mode #editModal select:focus:not([readonly]), +body.dark-mode #editModal textarea:focus:not([readonly]) { + background-color: #ffffff !important; + color: #000000 !important; + border-color: #007bff !important; +} + +/* Readonly fields should still look readonly */ +#editModal .form-control[readonly], +#editModal input[readonly] { + background-color: #e9ecef !important; + cursor: not-allowed !important; +} + +body.dark-mode #editModal .form-control[readonly], +body.dark-mode #editModal input[readonly] { + background-color: #6c757d !important; + color: #e2e8f0 !important; +} + +/* Dark mode styles for cards and tables */ +body.dark-mode .card { + background-color: #2d3748; + color: #e2e8f0; + border: 1px solid #4a5568; +} + +body.dark-mode .card-header { + background-color: #4a5568; + border-bottom: 1px solid #6b7280; + color: #e2e8f0; +} + +body.dark-mode .form-control { + background-color: #4a5568; + border-color: #6b7280; + color: #e2e8f0; +} + +body.dark-mode .form-control:focus { + background-color: #4a5568; + border-color: #007bff; + color: #e2e8f0; + box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); +} + +body.dark-mode .table { + color: #e2e8f0; +} + +body.dark-mode .table-striped tbody tr:nth-of-type(odd) { + background-color: rgba(255, 255, 255, 0.05); +} + +body.dark-mode .table-hover tbody tr:hover { + background-color: rgba(255, 255, 255, 0.1); +} + +body.dark-mode .modal-content { + background-color: #2d3748; + color: #e2e8f0; +} + +body.dark-mode .modal-header { + border-bottom: 1px solid #4a5568; +} + +body.dark-mode .modal-footer { + border-top: 1px solid #4a5568; +} + +body.dark-mode .btn-secondary { + background-color: #4a5568; + border-color: #6b7280; +} + +body.dark-mode .btn-secondary:hover { + background-color: #6b7280; +} + +body.dark-mode .btn-close { + filter: invert(1); +} + +/* Table and button styling */ +.table td { + vertical-align: middle; +} + +.btn-action { + padding: 0.25rem 0.5rem; + margin: 0.1rem; +} + +/* Editable field highlighting */ +.editable { + background-color: #fff3cd; + border: 1px dashed #ffc107; +} + +body.dark-mode .editable { + background-color: #2d2d00; + border: 1px dashed #ffc107; +} + +/* Compact table styling */ +.table-sm th, +.table-sm td { + padding: 0.5rem; + font-size: 0.9rem; +} + +/* Action button styling */ +.btn-sm { + padding: 0.25rem 0.5rem; + font-size: 0.875rem; +} + +/* Pagination styling */ +.pagination { + margin-bottom: 0; +} + +.page-link { + cursor: pointer; +} + +body.dark-mode .pagination .page-link { + background-color: #4a5568; + border: 1px solid #6b7280; + color: #e2e8f0; +} + +body.dark-mode .pagination .page-link:hover { + background-color: #374151; + border-color: #6b7280; + color: #e2e8f0; +} + +body.dark-mode .pagination .page-item.active .page-link { + background-color: #3b82f6; + border-color: #3b82f6; + color: #ffffff; +} + +/* Additional dark mode styles */ +body.dark-mode .container-fluid { + color: #e2e8f0; +} + +body.dark-mode .text-muted { + color: #a0aec0 !important; +} + +body.dark-mode .table-dark th { + background-color: #1a202c; + color: #e2e8f0; + border-color: #4a5568; +} + +body.dark-mode .table-striped > tbody > tr:nth-of-type(odd) > td { + background-color: #374151; +} + +body.dark-mode .table-hover > tbody > tr:hover > td { + background-color: #4a5568; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .table-responsive { + font-size: 0.875rem; + } + + .btn-sm { + font-size: 0.75rem; + padding: 0.375rem 0.5rem; + } +} diff --git a/py_app/app/templates/base.html b/py_app/app/templates/base.html index 414c53d..7588f43 100755 --- a/py_app/app/templates/base.html +++ b/py_app/app/templates/base.html @@ -39,18 +39,15 @@ {% endif %}
- - {% if request.endpoint in ['main.upload_data', 'main.upload_orders', 'main.print_module', 'main.label_templates', 'main.create_template', 'main.print_lost_labels', 'main.view_orders'] %} - Main Page Etichete - {% endif %} - {% if request.endpoint in ['main.quality', 'main.fg_quality'] %} - Main Page Reports - {% endif %} - Go to Dashboard - {% if 'user' in session %} - - Logout - {% endif %} + + {% if request.endpoint.startswith('daily_mirror') %} + Daily Mirror Main + {% endif %} + Go to Dashboard + {% if 'user' in session %} + + Logout + {% endif %}
diff --git a/py_app/app/templates/daily_mirror.html b/py_app/app/templates/daily_mirror.html new file mode 100644 index 0000000..aa1bd7d --- /dev/null +++ b/py_app/app/templates/daily_mirror.html @@ -0,0 +1,447 @@ +{% extends "base.html" %} + +{% block title %}Daily Mirror - Quality Recticel{% endblock %} + +{% block content %} +
+ +
+
+
+
+

📈 Daily Mirror

+

Generate comprehensive daily production reports

+
+ +
+
+
+ + +
+
+
+
+
+ Select Report Date +
+
+
+
+
+ + +
+
+ +
+
+ +
+
+
+
+
+
+ + + + + + + + + +
+ + + + +{% endblock %} \ No newline at end of file diff --git a/py_app/app/templates/daily_mirror_build_database.html b/py_app/app/templates/daily_mirror_build_database.html new file mode 100644 index 0000000..e524b6e --- /dev/null +++ b/py_app/app/templates/daily_mirror_build_database.html @@ -0,0 +1,719 @@ +{% extends "base.html" %} + +{% block title %}Build Database - Daily Mirror{% endblock %} + +{% block content %} +
+ +
+
+
+
+

🔨 Build Database

+

Upload Excel files to populate Daily Mirror database tables

+
+
+
+
+ +
+
+ +
+
+
+ Upload Excel File +
+
+
+
+ +
+ + + + Select a table to see its description. + +
+ + +
+ + + + Accepted formats: .xlsx, .xls (Maximum file size: 10MB) + +
+ + +
+ +
+
+
+
+
+ +
+ +
+
+
+ Excel File Format Instructions +
+
+
+
+ +
+

+ +

+
+
+

Expected columns for Production Data:

+
+
+
    +
  • Production Order ID Unique identifier
  • +
  • Customer Code Customer code
  • +
  • Customer Name Customer name
  • +
  • Article Code Article code
  • +
+
+
+
    +
  • Article Description Description
  • +
  • Quantity To produce
  • +
  • Production Date Date
  • +
  • Status Production status
  • +
+
+
+
+
+
+ + +
+

+ +

+
+
+

Expected columns for Orders Data:

+
+
+
    +
  • Order ID Unique identifier
  • +
  • Customer Code Customer code
  • +
  • Customer Name Customer name
  • +
  • Article Code Article code
  • +
+
+
+
    +
  • Article Description Description
  • +
  • Quantity Ordered Ordered
  • +
  • Order Date Date
  • +
  • Status Order status
  • +
+
+
+
+
+
+ + +
+

+ +

+
+
+

Expected columns for Delivery Data:

+
+
+
    +
  • Shipment ID Unique shipment identifier
  • +
  • Order ID Related order
  • +
  • Customer Customer info
  • +
  • Article Code/description
  • +
+
+
+
    +
  • Quantity Delivered Delivered quantity
  • +
  • Delivery Date Date
  • +
  • Status Delivery status
  • +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + +{% endblock %} diff --git a/py_app/app/templates/daily_mirror_history.html b/py_app/app/templates/daily_mirror_history.html new file mode 100644 index 0000000..b5a0518 --- /dev/null +++ b/py_app/app/templates/daily_mirror_history.html @@ -0,0 +1,449 @@ +{% extends "base.html" %} + +{% block title %}Daily Mirror History - Quality Recticel{% endblock %} + +{% block content %} +
+ +
+
+
+
+

📋 Daily Mirror History

+

Analyze historical daily production reports and trends

+
+ +
+
+
+ + +
+
+
+
+
+ Select Date Range +
+
+
+
+
+ + +
+
+ + +
+
+ +
+
+
+ + +
+
+
+
+
+
+
+ + + + + + + + + + + + + + + +
+ + + + +{% endblock %} \ No newline at end of file diff --git a/py_app/app/templates/daily_mirror_main.html b/py_app/app/templates/daily_mirror_main.html new file mode 100644 index 0000000..8f3c39c --- /dev/null +++ b/py_app/app/templates/daily_mirror_main.html @@ -0,0 +1,262 @@ +{% extends "base.html" %} + +{% block title %}Daily Mirror - Quality Recticel{% endblock %} + +{% block content %} +
+ +
+
+
+
+

📊 Daily Mirror

+

Business Intelligence and Production Reporting

+
+
+ +
+
+
+
+ + +
+ +
+
+
+
+
+ +
+
+
Build Database
+

+ Upload Excel files to create and populate tables. +

+ +
+
+
+ + +
+
+
+
+
+ +
+
+
Tune Database
+

+ Edit and update records after import. +

+ +
+
+
+ + +
+
+
+
+
+ +
+
+
Daily Mirror
+

+ Generate daily production reports. +

+ +
+
+
+ + +
+
+
+
+
+ +
+
+
Daily Mirror History
+

+ View historical production reports. +

+ +
+
+
+
+
+ + + + +{% endblock %} \ No newline at end of file diff --git a/py_app/app/templates/daily_mirror_tune_delivery.html b/py_app/app/templates/daily_mirror_tune_delivery.html new file mode 100644 index 0000000..42b5934 --- /dev/null +++ b/py_app/app/templates/daily_mirror_tune_delivery.html @@ -0,0 +1,503 @@ +{% extends "base.html" %} + +{% block title %}Tune Delivery Data - Daily Mirror{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +
+ +
+
+
+
+

🚚 Tune Delivery Data

+

Edit and update delivery records information

+
+
+ +
+
+
+
+ + +
+
+
+
+
+ Filters and Search +
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+
+ Delivery Records Data +
+
+ + +
+
+
+
+ + + + + + + + + + + + + + + + + + + +
Shipment IDCustomerOrder IDArticle CodeDescriptionQuantityShipment DateDelivery DateStatusTotal ValueActions
+
+ + + + + + +
+ + + +
+
+
+
+ + + + + +{% endblock %} \ No newline at end of file diff --git a/py_app/app/templates/daily_mirror_tune_orders.html b/py_app/app/templates/daily_mirror_tune_orders.html new file mode 100644 index 0000000..b969bdc --- /dev/null +++ b/py_app/app/templates/daily_mirror_tune_orders.html @@ -0,0 +1,522 @@ +{% extends "base.html" %} + +{% block title %}Tune Orders Data - Daily Mirror{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +
+ +
+
+
+
+

🛒 Tune Orders Data

+

Edit and update customer orders information

+
+
+ +
+
+
+
+ + +
+
+
+
+
+ Filters and Search +
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+
+ Customer Orders Data +
+
+ + +
+
+
+
+ + + + + + + + + + + + + + + + + + + + +
Order IDCustomerClient OrderArticle CodeDescriptionQuantityDelivery DateStatusPriorityProduct GroupOrder DateActions
+
+ + + + + + +
+ + + +
+
+
+
+ + + + + +{% endblock %} \ No newline at end of file diff --git a/py_app/app/templates/daily_mirror_tune_production.html b/py_app/app/templates/daily_mirror_tune_production.html new file mode 100644 index 0000000..66c40ab --- /dev/null +++ b/py_app/app/templates/daily_mirror_tune_production.html @@ -0,0 +1,516 @@ +{% extends "base.html" %} + +{% block title %}Tune Production Data - Daily Mirror{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +
+ +
+
+
+
+

🏭 Tune Production Data

+

Edit and update production orders information

+
+
+ +
+
+
+
+ + +
+
+
+
+
+ Filters and Search +
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+
+
+
+
+ + +
+
+
+
+
+ Production Orders Data +
+
+ + +
+
+
+
+ + + + + + + + + + + + + + + + + + + +
Production OrderCustomerClient OrderArticle CodeDescriptionQuantityDelivery DateStatusMachinePlanning DateActions
+
+ + + + + + +
+ + + +
+
+
+
+ + + + + +{% endblock %} \ No newline at end of file diff --git a/py_app/app/templates/dashboard.html b/py_app/app/templates/dashboard.html index fb402e8..9daed5f 100755 --- a/py_app/app/templates/dashboard.html +++ b/py_app/app/templates/dashboard.html @@ -34,5 +34,17 @@ Access Settings Page +
+

📊 Daily Mirror

+

Business Intelligence and Production Reporting - Generate comprehensive daily reports including order quantities, production status, and delivery tracking.

+ +
+ Tracks: Orders quantity • Production launched • Production finished • Orders delivered +
+
+ + {% endblock %} \ No newline at end of file diff --git a/py_app/app/templates/user_management_simple.html b/py_app/app/templates/user_management_simple.html index 8c5aa78..5e2f8a5 100644 --- a/py_app/app/templates/user_management_simple.html +++ b/py_app/app/templates/user_management_simple.html @@ -408,6 +408,10 @@ +
+ + +
@@ -454,6 +458,10 @@ +
+ + +
@@ -621,6 +629,10 @@ +
+ + +