updated documentation folder

This commit is contained in:
Quality System Admin
2025-11-03 21:17:10 +02:00
parent 8d47e6e82d
commit 1ade0b5681
29 changed files with 6113 additions and 32 deletions

View File

@@ -1,16 +1,12 @@
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
db = SQLAlchemy()
def create_app():
app = Flask(__name__)
app.config['SECRET_KEY'] = 'your_secret_key'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db.init_app(app)
# Application uses direct MariaDB connections via external_server.conf
# No SQLAlchemy ORM needed - all database operations use raw SQL
from app.routes import bp as main_bp, warehouse_bp
from app.daily_mirror import daily_mirror_bp
@@ -21,7 +17,4 @@ def create_app():
# Add 'now' function to Jinja2 globals
app.jinja_env.globals['now'] = datetime.now
with app.app_context():
db.create_all() # Create database tables if they don't exist
return app

View File

@@ -41,8 +41,8 @@ def requires_role(min_role_level=None, required_modules=None, page=None):
# Module requirement checking
if required_modules:
if user_role in ['superadmin', 'admin']:
# Superadmin and admin have access to all modules
if user_role == 'superadmin':
# Superadmin has access to all modules
pass
else:
if not any(module in user_modules for module in required_modules):
@@ -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)
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)

View File

@@ -26,10 +26,15 @@ def check_daily_mirror_access():
flash('Please log in to access this page.')
return redirect(url_for('main.login'))
# Check if user has admin+ access
# Superadmin has access to everything
user_role = session.get('role', '')
if user_role not in ['superadmin', 'admin']:
flash('Access denied: Admin privileges required for Daily Mirror.')
if user_role == 'superadmin':
return None # Access granted
# Check if user has daily_mirror module access
user_modules = session.get('modules', [])
if 'daily_mirror' not in user_modules:
flash('Access denied: Daily Mirror module access required.')
return redirect(url_for('main.dashboard'))
return None # Access granted
@@ -37,13 +42,19 @@ def check_daily_mirror_access():
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
# Check if user is logged in
if 'user' not in session:
return jsonify({'error': 'Authentication required'}), 401
# Superadmin has access to everything
user_role = session.get('role', '')
if user_role not in ['superadmin', 'admin']:
return jsonify({'error': 'Admin privileges required'}), 403
if user_role == 'superadmin':
return None # Access granted
# Check if user has daily_mirror module access
user_modules = session.get('modules', [])
if 'daily_mirror' not in user_modules:
return jsonify({'error': 'Daily Mirror module access required'}), 403
return None # Access granted

View File

@@ -0,0 +1,431 @@
"""
Database Backup Management Module
Quality Recticel Application
This module provides functionality for backing up and restoring the MariaDB database,
including scheduled backups, manual backups, and backup file management.
"""
import os
import subprocess
import json
from datetime import datetime, timedelta
from pathlib import Path
import configparser
from flask import current_app
import mariadb
class DatabaseBackupManager:
"""Manages database backup operations"""
def __init__(self):
"""Initialize the backup manager with configuration from external_server.conf"""
self.config = self._load_database_config()
self.backup_path = self._get_backup_path()
self._ensure_backup_directory()
def _load_database_config(self):
"""Load database configuration from external_server.conf"""
try:
settings_file = os.path.join(current_app.instance_path, 'external_server.conf')
config = {}
if os.path.exists(settings_file):
with open(settings_file, 'r') as f:
for line in f:
if '=' in line:
key, value = line.strip().split('=', 1)
config[key] = value
return {
'host': config.get('server_domain', 'localhost'),
'port': config.get('port', '3306'),
'database': config.get('database_name', 'trasabilitate'),
'user': config.get('username', 'trasabilitate'),
'password': config.get('password', '')
}
except Exception as e:
print(f"Error loading database config: {e}")
return None
def _get_backup_path(self):
"""Get backup path from environment or use default"""
# Check environment variable (set in docker-compose)
backup_path = os.environ.get('BACKUP_PATH', '/srv/quality_app/backups')
# Check if custom path is set in config
try:
settings_file = os.path.join(current_app.instance_path, 'external_server.conf')
if os.path.exists(settings_file):
with open(settings_file, 'r') as f:
for line in f:
if line.startswith('backup_path='):
backup_path = line.strip().split('=', 1)[1]
break
except Exception as e:
print(f"Error reading backup path from config: {e}")
return backup_path
def _ensure_backup_directory(self):
"""Ensure backup directory exists"""
try:
Path(self.backup_path).mkdir(parents=True, exist_ok=True)
print(f"Backup directory ensured: {self.backup_path}")
except Exception as e:
print(f"Error creating backup directory: {e}")
def create_backup(self, backup_name=None):
"""
Create a complete backup of the database
Args:
backup_name (str, optional): Custom name for the backup file
Returns:
dict: Result with success status, message, and backup file path
"""
try:
if not self.config:
return {
'success': False,
'message': 'Database configuration not loaded'
}
# Generate backup filename
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
if backup_name:
filename = f"{backup_name}_{timestamp}.sql"
else:
filename = f"backup_{self.config['database']}_{timestamp}.sql"
backup_file = os.path.join(self.backup_path, filename)
# Build mysqldump command
cmd = [
'mysqldump',
f"--host={self.config['host']}",
f"--port={self.config['port']}",
f"--user={self.config['user']}",
f"--password={self.config['password']}",
'--single-transaction',
'--routines',
'--triggers',
'--events',
'--add-drop-database',
'--databases',
self.config['database']
]
# Execute mysqldump and save to file
with open(backup_file, 'w') as f:
result = subprocess.run(
cmd,
stdout=f,
stderr=subprocess.PIPE,
text=True
)
if result.returncode == 0:
# Get file size
file_size = os.path.getsize(backup_file)
file_size_mb = file_size / (1024 * 1024)
# Save backup metadata
self._save_backup_metadata(filename, file_size)
return {
'success': True,
'message': f'Backup created successfully',
'filename': filename,
'file_path': backup_file,
'size': f"{file_size_mb:.2f} MB",
'timestamp': timestamp
}
else:
error_msg = result.stderr
print(f"Backup error: {error_msg}")
return {
'success': False,
'message': f'Backup failed: {error_msg}'
}
except Exception as e:
print(f"Exception during backup: {e}")
return {
'success': False,
'message': f'Backup failed: {str(e)}'
}
def _save_backup_metadata(self, filename, file_size):
"""Save metadata about the backup"""
try:
metadata_file = os.path.join(self.backup_path, 'backups_metadata.json')
# Load existing metadata
metadata = []
if os.path.exists(metadata_file):
with open(metadata_file, 'r') as f:
metadata = json.load(f)
# Add new backup metadata
metadata.append({
'filename': filename,
'size': file_size,
'timestamp': datetime.now().isoformat(),
'database': self.config['database']
})
# Save updated metadata
with open(metadata_file, 'w') as f:
json.dump(metadata, f, indent=2)
except Exception as e:
print(f"Error saving backup metadata: {e}")
def list_backups(self):
"""
List all available backups
Returns:
list: List of backup information dictionaries
"""
try:
backups = []
# Get all .sql files in backup directory
if os.path.exists(self.backup_path):
for filename in os.listdir(self.backup_path):
if filename.endswith('.sql'):
file_path = os.path.join(self.backup_path, filename)
file_stat = os.stat(file_path)
backups.append({
'filename': filename,
'size': file_stat.st_size,
'size_mb': f"{file_stat.st_size / (1024 * 1024):.2f}",
'created': datetime.fromtimestamp(file_stat.st_ctime).strftime('%Y-%m-%d %H:%M:%S'),
'timestamp': file_stat.st_ctime
})
# Sort by timestamp (newest first)
backups.sort(key=lambda x: x['timestamp'], reverse=True)
return backups
except Exception as e:
print(f"Error listing backups: {e}")
return []
def delete_backup(self, filename):
"""
Delete a backup file
Args:
filename (str): Name of the backup file to delete
Returns:
dict: Result with success status and message
"""
try:
# Security: ensure filename doesn't contain path traversal
if '..' in filename or '/' in filename:
return {
'success': False,
'message': 'Invalid filename'
}
file_path = os.path.join(self.backup_path, filename)
if os.path.exists(file_path):
os.remove(file_path)
# Update metadata
self._remove_backup_metadata(filename)
return {
'success': True,
'message': f'Backup {filename} deleted successfully'
}
else:
return {
'success': False,
'message': 'Backup file not found'
}
except Exception as e:
print(f"Error deleting backup: {e}")
return {
'success': False,
'message': f'Failed to delete backup: {str(e)}'
}
def _remove_backup_metadata(self, filename):
"""Remove metadata entry for deleted backup"""
try:
metadata_file = os.path.join(self.backup_path, 'backups_metadata.json')
if os.path.exists(metadata_file):
with open(metadata_file, 'r') as f:
metadata = json.load(f)
# Filter out the deleted backup
metadata = [m for m in metadata if m['filename'] != filename]
with open(metadata_file, 'w') as f:
json.dump(metadata, f, indent=2)
except Exception as e:
print(f"Error removing backup metadata: {e}")
def restore_backup(self, filename):
"""
Restore database from a backup file
Args:
filename (str): Name of the backup file to restore
Returns:
dict: Result with success status and message
"""
try:
# Security: ensure filename doesn't contain path traversal
if '..' in filename or '/' in filename:
return {
'success': False,
'message': 'Invalid filename'
}
file_path = os.path.join(self.backup_path, filename)
if not os.path.exists(file_path):
return {
'success': False,
'message': 'Backup file not found'
}
# Build mysql restore command
cmd = [
'mysql',
f"--host={self.config['host']}",
f"--port={self.config['port']}",
f"--user={self.config['user']}",
f"--password={self.config['password']}"
]
# Execute mysql restore
with open(file_path, 'r') as f:
result = subprocess.run(
cmd,
stdin=f,
stderr=subprocess.PIPE,
text=True
)
if result.returncode == 0:
return {
'success': True,
'message': f'Database restored successfully from {filename}'
}
else:
error_msg = result.stderr
print(f"Restore error: {error_msg}")
return {
'success': False,
'message': f'Restore failed: {error_msg}'
}
except Exception as e:
print(f"Exception during restore: {e}")
return {
'success': False,
'message': f'Restore failed: {str(e)}'
}
def get_backup_schedule(self):
"""Get current backup schedule configuration"""
try:
schedule_file = os.path.join(self.backup_path, 'backup_schedule.json')
if os.path.exists(schedule_file):
with open(schedule_file, 'r') as f:
return json.load(f)
# Default schedule
return {
'enabled': False,
'time': '02:00', # 2 AM
'frequency': 'daily', # daily, weekly, monthly
'retention_days': 30 # Keep backups for 30 days
}
except Exception as e:
print(f"Error loading backup schedule: {e}")
return None
def save_backup_schedule(self, schedule):
"""
Save backup schedule configuration
Args:
schedule (dict): Schedule configuration
Returns:
dict: Result with success status and message
"""
try:
schedule_file = os.path.join(self.backup_path, 'backup_schedule.json')
with open(schedule_file, 'w') as f:
json.dump(schedule, f, indent=2)
return {
'success': True,
'message': 'Backup schedule saved successfully'
}
except Exception as e:
print(f"Error saving backup schedule: {e}")
return {
'success': False,
'message': f'Failed to save schedule: {str(e)}'
}
def cleanup_old_backups(self, retention_days=30):
"""
Delete backups older than retention_days
Args:
retention_days (int): Number of days to keep backups
Returns:
dict: Result with count of deleted backups
"""
try:
deleted_count = 0
cutoff_time = datetime.now() - timedelta(days=retention_days)
if os.path.exists(self.backup_path):
for filename in os.listdir(self.backup_path):
if filename.endswith('.sql'):
file_path = os.path.join(self.backup_path, filename)
file_time = datetime.fromtimestamp(os.path.getctime(file_path))
if file_time < cutoff_time:
os.remove(file_path)
self._remove_backup_metadata(filename)
deleted_count += 1
print(f"Deleted old backup: {filename}")
return {
'success': True,
'deleted_count': deleted_count,
'message': f'Cleaned up {deleted_count} old backup(s)'
}
except Exception as e:
print(f"Error cleaning up old backups: {e}")
return {
'success': False,
'message': f'Cleanup failed: {str(e)}'
}

View File

@@ -3,8 +3,6 @@ import os
import mariadb
from datetime import datetime, timedelta
from flask import Blueprint, render_template, redirect, url_for, request, flash, session, current_app, jsonify, send_from_directory
from .models import User
from . import db
from reportlab.lib.pagesizes import letter
from reportlab.pdfgen import canvas
import csv
@@ -94,9 +92,9 @@ def login():
except:
user_modules = []
# Superadmin and admin have access to all modules
if user['role'] in ['superadmin', 'admin']:
user_modules = ['quality', 'warehouse', 'labels']
# Superadmin has access to all modules
if user['role'] == 'superadmin':
user_modules = ['quality', 'warehouse', 'labels', 'daily_mirror']
session['modules'] = user_modules
print("Logged in as:", session.get('user'), session.get('role'), "modules:", user_modules)
@@ -3580,4 +3578,137 @@ def help(page='index'):
# "pdf_url": "https://your-linux-server/generate_labels_pdf/15",
# "printer_name": "default",
# "copies": 1
# }
# }
# ========================================
# DATABASE BACKUP MANAGEMENT ROUTES
# ========================================
@bp.route('/api/backup/create', methods=['POST'])
@admin_plus
def api_backup_create():
"""Create a new database backup"""
try:
from app.database_backup import DatabaseBackupManager
backup_manager = DatabaseBackupManager()
result = backup_manager.create_backup()
return jsonify(result)
except Exception as e:
return jsonify({
'success': False,
'message': f'Backup failed: {str(e)}'
}), 500
@bp.route('/api/backup/list', methods=['GET'])
@admin_plus
def api_backup_list():
"""List all available backups"""
try:
from app.database_backup import DatabaseBackupManager
backup_manager = DatabaseBackupManager()
backups = backup_manager.list_backups()
return jsonify({
'success': True,
'backups': backups
})
except Exception as e:
return jsonify({
'success': False,
'message': f'Failed to list backups: {str(e)}'
}), 500
@bp.route('/api/backup/download/<filename>', methods=['GET'])
@admin_plus
def api_backup_download(filename):
"""Download a backup file"""
try:
from app.database_backup import DatabaseBackupManager
from flask import send_file
import os
backup_manager = DatabaseBackupManager()
backup_path = backup_manager.backup_path
file_path = os.path.join(backup_path, filename)
# Security: ensure filename doesn't contain path traversal
if '..' in filename or '/' in filename:
return jsonify({
'success': False,
'message': 'Invalid filename'
}), 400
if os.path.exists(file_path):
return send_file(file_path, as_attachment=True, download_name=filename)
else:
return jsonify({
'success': False,
'message': 'Backup file not found'
}), 404
except Exception as e:
return jsonify({
'success': False,
'message': f'Download failed: {str(e)}'
}), 500
@bp.route('/api/backup/delete/<filename>', methods=['DELETE'])
@admin_plus
def api_backup_delete(filename):
"""Delete a backup file"""
try:
from app.database_backup import DatabaseBackupManager
backup_manager = DatabaseBackupManager()
result = backup_manager.delete_backup(filename)
return jsonify(result)
except Exception as e:
return jsonify({
'success': False,
'message': f'Delete failed: {str(e)}'
}), 500
@bp.route('/api/backup/schedule', methods=['GET', 'POST'])
@admin_plus
def api_backup_schedule():
"""Get or save backup schedule configuration"""
try:
from app.database_backup import DatabaseBackupManager
backup_manager = DatabaseBackupManager()
if request.method == 'POST':
schedule = request.json
result = backup_manager.save_backup_schedule(schedule)
return jsonify(result)
else:
schedule = backup_manager.get_backup_schedule()
return jsonify({
'success': True,
'schedule': schedule
})
except Exception as e:
return jsonify({
'success': False,
'message': f'Schedule operation failed: {str(e)}'
}), 500
@bp.route('/api/backup/restore/<filename>', methods=['POST'])
@superadmin_only
def api_backup_restore(filename):
"""Restore database from a backup file (superadmin only)"""
try:
from app.database_backup import DatabaseBackupManager
backup_manager = DatabaseBackupManager()
result = backup_manager.restore_backup(filename)
return jsonify(result)
except Exception as e:
return jsonify({
'success': False,
'message': f'Restore failed: {str(e)}'
}), 500

View File

@@ -1,6 +1,4 @@
from flask import render_template, request, session, redirect, url_for, flash, current_app, jsonify
from .models import User
from . import db
from .permissions import APP_PERMISSIONS, ROLE_HIERARCHY, ACTIONS, get_all_permissions, get_default_permissions_for_role
import mariadb
import os

View File

@@ -0,0 +1,447 @@
{% extends "base.html" %}
{% block title %}Daily Mirror - Quality Recticel{% endblock %}
{% block content %}
<div class="container-fluid">
<!-- Page Header -->
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<div>
<h1 class="h3 mb-0">📈 Daily Mirror</h1>
<p class="text-muted">Generate comprehensive daily production reports</p>
</div>
<div>
<a href="{{ url_for('daily_mirror.daily_mirror_history_route') }}" class="btn btn-outline-primary">
<i class="fas fa-history"></i> View History
</a>
<a href="{{ url_for('main.dashboard') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Back to Dashboard
</a>
</div>
</div>
</div>
</div>
<!-- Date Selection Card -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-calendar-alt"></i> Select Report Date
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4">
<label for="reportDate" class="form-label">Report Date:</label>
<input type="date" class="form-control" id="reportDate" value="{{ today }}">
</div>
<div class="col-md-4 d-flex align-items-end">
<button type="button" class="btn btn-primary" onclick="generateDailyReport()">
<i class="fas fa-chart-line"></i> Generate Report
</button>
</div>
<div class="col-md-4 d-flex align-items-end">
<button type="button" class="btn btn-success" onclick="setTodayDate()">
<i class="fas fa-calendar-day"></i> Today's Report
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Loading Indicator -->
<div id="loadingIndicator" class="row mb-4" style="display: none;">
<div class="col-12">
<div class="card">
<div class="card-body text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2 mb-0">Generating daily report...</p>
</div>
</div>
</div>
</div>
<!-- Daily Report Results -->
<div id="reportResults" style="display: none;">
<!-- Key Metrics Overview -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-tachometer-alt"></i> Daily Production Overview
<span id="reportDateDisplay" class="badge bg-primary ms-2"></span>
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3">
<div class="metric-card orders-quantity">
<div class="metric-icon">
<i class="fas fa-clipboard-list"></i>
</div>
<div class="metric-content">
<h3 id="ordersQuantity">-</h3>
<p>Orders Quantity</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="metric-card production-launched">
<div class="metric-icon">
<i class="fas fa-play-circle"></i>
</div>
<div class="metric-content">
<h3 id="productionLaunched">-</h3>
<p>Production Launched</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="metric-card production-finished">
<div class="metric-icon">
<i class="fas fa-check-circle"></i>
</div>
<div class="metric-content">
<h3 id="productionFinished">-</h3>
<p>Production Finished</p>
</div>
</div>
</div>
<div class="col-md-3">
<div class="metric-card orders-delivered">
<div class="metric-icon">
<i class="fas fa-truck"></i>
</div>
<div class="metric-content">
<h3 id="ordersDelivered">-</h3>
<p>Orders Delivered</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Quality Control Metrics -->
<div class="row mb-4">
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-search"></i> Quality Control Scans
</h5>
</div>
<div class="card-body">
<div class="quality-stats">
<div class="row">
<div class="col-4">
<div class="stat-item">
<h4 id="qualityTotalScans">-</h4>
<p>Total Scans</p>
</div>
</div>
<div class="col-4">
<div class="stat-item approved">
<h4 id="qualityApprovedScans">-</h4>
<p>Approved</p>
</div>
</div>
<div class="col-4">
<div class="stat-item rejected">
<h4 id="qualityRejectedScans">-</h4>
<p>Rejected</p>
</div>
</div>
</div>
<div class="mt-3">
<div class="progress">
<div id="qualityApprovalBar" class="progress-bar bg-success" role="progressbar" style="width: 0%"></div>
</div>
<p class="text-center mt-2 mb-0">
Approval Rate: <span id="qualityApprovalRate" class="fw-bold">0%</span>
</p>
</div>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-clipboard-check"></i> Finish Goods Quality
</h5>
</div>
<div class="card-body">
<div class="quality-stats">
<div class="row">
<div class="col-4">
<div class="stat-item">
<h4 id="fgQualityTotalScans">-</h4>
<p>Total Scans</p>
</div>
</div>
<div class="col-4">
<div class="stat-item approved">
<h4 id="fgQualityApprovedScans">-</h4>
<p>Approved</p>
</div>
</div>
<div class="col-4">
<div class="stat-item rejected">
<h4 id="fgQualityRejectedScans">-</h4>
<p>Rejected</p>
</div>
</div>
</div>
<div class="mt-3">
<div class="progress">
<div id="fgQualityApprovalBar" class="progress-bar bg-success" role="progressbar" style="width: 0%"></div>
</div>
<p class="text-center mt-2 mb-0">
Approval Rate: <span id="fgQualityApprovalRate" class="fw-bold">0%</span>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Export and Actions -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-download"></i> Export Options
</h5>
</div>
<div class="card-body">
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-success" onclick="exportReportPDF()">
<i class="fas fa-file-pdf"></i> Export PDF
</button>
<button type="button" class="btn btn-outline-primary" onclick="exportReportExcel()">
<i class="fas fa-file-excel"></i> Export Excel
</button>
<button type="button" class="btn btn-outline-info" onclick="printReport()">
<i class="fas fa-print"></i> Print Report
</button>
<button type="button" class="btn btn-outline-secondary" onclick="shareReport()">
<i class="fas fa-share"></i> Share Report
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Error Message -->
<div id="errorMessage" class="row mb-4" style="display: none;">
<div class="col-12">
<div class="alert alert-danger" role="alert">
<i class="fas fa-exclamation-triangle"></i>
<strong>Error:</strong> <span id="errorText"></span>
</div>
</div>
</div>
</div>
<style>
.metric-card {
display: flex;
align-items: center;
padding: 1.5rem;
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
border-radius: 10px;
margin-bottom: 1rem;
transition: transform 0.2s ease;
}
.metric-card:hover {
transform: translateY(-2px);
}
.metric-card.orders-quantity {
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
}
.metric-card.production-launched {
background: linear-gradient(135deg, #f3e5f5 0%, #ce93d8 100%);
}
.metric-card.production-finished {
background: linear-gradient(135deg, #e8f5e8 0%, #a5d6a7 100%);
}
.metric-card.orders-delivered {
background: linear-gradient(135deg, #fff3e0 0%, #ffcc02 100%);
}
.metric-icon {
font-size: 2.5rem;
margin-right: 1rem;
opacity: 0.8;
}
.metric-content h3 {
font-size: 2rem;
font-weight: bold;
margin: 0;
color: #2c3e50;
}
.metric-content p {
margin: 0;
color: #6c757d;
font-weight: 500;
}
.quality-stats .stat-item {
text-align: center;
padding: 1rem 0;
}
.quality-stats .stat-item h4 {
font-size: 1.5rem;
font-weight: bold;
margin: 0;
color: #2c3e50;
}
.quality-stats .stat-item.approved h4 {
color: #28a745;
}
.quality-stats .stat-item.rejected h4 {
color: #dc3545;
}
.quality-stats .stat-item p {
margin: 0;
color: #6c757d;
font-size: 0.9rem;
}
@media (max-width: 768px) {
.metric-card {
flex-direction: column;
text-align: center;
}
.metric-icon {
margin-right: 0;
margin-bottom: 0.5rem;
}
}
</style>
<script>
function setTodayDate() {
const today = new Date().toISOString().split('T')[0];
document.getElementById('reportDate').value = today;
generateDailyReport();
}
function generateDailyReport() {
const reportDate = document.getElementById('reportDate').value;
if (!reportDate) {
showError('Please select a report date');
return;
}
// Show loading indicator
document.getElementById('loadingIndicator').style.display = 'block';
document.getElementById('reportResults').style.display = 'none';
document.getElementById('errorMessage').style.display = 'none';
// Make API call to get daily data
fetch(`/daily_mirror/api/data?date=${reportDate}`)
.then(response => response.json())
.then(data => {
if (data.error) {
showError(data.error);
return;
}
// Update display with data
updateDailyReport(data);
// Hide loading and show results
document.getElementById('loadingIndicator').style.display = 'none';
document.getElementById('reportResults').style.display = 'block';
})
.catch(error => {
console.error('Error generating daily report:', error);
showError('Failed to generate daily report. Please try again.');
});
}
function updateDailyReport(data) {
// Update date display
document.getElementById('reportDateDisplay').textContent = data.date;
// Update key metrics
document.getElementById('ordersQuantity').textContent = data.orders_quantity.toLocaleString();
document.getElementById('productionLaunched').textContent = data.production_launched.toLocaleString();
document.getElementById('productionFinished').textContent = data.production_finished.toLocaleString();
document.getElementById('ordersDelivered').textContent = data.orders_delivered.toLocaleString();
// Update quality control data
document.getElementById('qualityTotalScans').textContent = data.quality_scans.total_scans.toLocaleString();
document.getElementById('qualityApprovedScans').textContent = data.quality_scans.approved_scans.toLocaleString();
document.getElementById('qualityRejectedScans').textContent = data.quality_scans.rejected_scans.toLocaleString();
document.getElementById('qualityApprovalRate').textContent = data.quality_scans.approval_rate + '%';
document.getElementById('qualityApprovalBar').style.width = data.quality_scans.approval_rate + '%';
// Update FG quality data
document.getElementById('fgQualityTotalScans').textContent = data.fg_quality_scans.total_scans.toLocaleString();
document.getElementById('fgQualityApprovedScans').textContent = data.fg_quality_scans.approved_scans.toLocaleString();
document.getElementById('fgQualityRejectedScans').textContent = data.fg_quality_scans.rejected_scans.toLocaleString();
document.getElementById('fgQualityApprovalRate').textContent = data.fg_quality_scans.approval_rate + '%';
document.getElementById('fgQualityApprovalBar').style.width = data.fg_quality_scans.approval_rate + '%';
}
function showError(message) {
document.getElementById('errorText').textContent = message;
document.getElementById('errorMessage').style.display = 'block';
document.getElementById('loadingIndicator').style.display = 'none';
document.getElementById('reportResults').style.display = 'none';
}
function exportReportPDF() {
alert('PDF export functionality will be implemented soon.');
}
function exportReportExcel() {
alert('Excel export functionality will be implemented soon.');
}
function printReport() {
window.print();
}
function shareReport() {
alert('Share functionality will be implemented soon.');
}
// Auto-generate today's report on page load
document.addEventListener('DOMContentLoaded', function() {
generateDailyReport();
});
</script>
{% endblock %}

View File

@@ -0,0 +1,760 @@
{% extends "base.html" %}
{% block title %}Build Database - Daily Mirror{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='daily_mirror_tune.css') }}">
{% endblock %}
{% block content %}
<div class="container-fluid">
<!-- Page Header -->
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<div>
<h1 class="h3 mb-0">🔨 Build Database</h1>
<p class="text-muted">Upload Excel files to populate Daily Mirror database tables</p>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-6 mb-4">
<!-- Card 1: Upload Excel File -->
<div class="card h-100">
<div class="card-header bg-primary text-white">
<h5 class="card-title mb-0">
<i class="fas fa-upload"></i> Upload Excel File
</h5>
</div>
<div class="card-body">
<form method="POST" enctype="multipart/form-data" id="uploadForm">
<!-- Table Selection -->
<div class="form-group mb-4">
<label for="target_table" class="form-label">
<strong>Select Target Table:</strong>
</label>
<select class="form-control" name="target_table" id="target_table" required>
<option value="">-- Choose a table --</option>
{% for table in available_tables %}
<option value="{{ table.name }}" data-description="{{ table.description }}">
{{ table.display }}
</option>
{% endfor %}
</select>
<small id="tableDescription" class="form-text text-muted mt-2">
Select a table to see its description.
</small>
</div>
<!-- File Upload -->
<div class="form-group mb-4">
<label for="excel_file" class="form-label">
<strong>Select Excel File:</strong>
</label>
<input type="file" class="form-control" name="excel_file" id="excel_file"
accept=".xlsx,.xls" required>
<small class="form-text text-muted">
Accepted formats: .xlsx, .xls (Maximum file size: 10MB)
</small>
</div>
<!-- Upload Button -->
<div class="text-center">
<button type="button" class="btn btn-primary btn-lg" id="uploadBtn">
<i class="fas fa-cloud-upload-alt"></i> Upload and Process File
</button>
</div>
</form>
</div>
</div>
</div>
<div class="col-lg-6 mb-4">
<!-- Card 2: Excel File Format Instructions -->
<div class="card h-100">
<div class="card-header bg-info text-white">
<h5 class="card-title mb-0">
<i class="fas fa-info-circle"></i> Excel File Format Instructions
</h5>
</div>
<div class="card-body">
<div class="accordion" id="formatAccordion">
<!-- Production Data Format -->
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#productionCollapse" aria-expanded="false">
🏭 Production Data Format
</button>
</h2>
<div id="productionCollapse" class="accordion-collapse collapse" data-bs-parent="#formatAccordion">
<div class="accordion-body">
<p><strong>Expected columns for Production Data:</strong></p>
<div class="row">
<div class="col-md-6">
<ul class="list-unstyled mb-0">
<li><code>Production Order ID</code> <span class="text-muted">Unique identifier</span></li>
<li><code>Customer Code</code> <span class="text-muted">Customer code</span></li>
<li><code>Customer Name</code> <span class="text-muted">Customer name</span></li>
<li><code>Article Code</code> <span class="text-muted">Article code</span></li>
</ul>
</div>
<div class="col-md-6">
<ul class="list-unstyled mb-0">
<li><code>Article Description</code> <span class="text-muted">Description</span></li>
<li><code>Quantity</code> <span class="text-muted">To produce</span></li>
<li><code>Production Date</code> <span class="text-muted">Date</span></li>
<li><code>Status</code> <span class="text-muted">Production status</span></li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- Orders Data Format -->
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#ordersCollapse" aria-expanded="false">
🛒 Orders Data Format
</button>
</h2>
<div id="ordersCollapse" class="accordion-collapse collapse" data-bs-parent="#formatAccordion">
<div class="accordion-body">
<p><strong>Expected columns for Orders Data:</strong></p>
<div class="row">
<div class="col-md-6">
<ul class="list-unstyled mb-0">
<li><code>Order ID</code> <span class="text-muted">Unique identifier</span></li>
<li><code>Customer Code</code> <span class="text-muted">Customer code</span></li>
<li><code>Customer Name</code> <span class="text-muted">Customer name</span></li>
<li><code>Article Code</code> <span class="text-muted">Article code</span></li>
</ul>
</div>
<div class="col-md-6">
<ul class="list-unstyled mb-0">
<li><code>Article Description</code> <span class="text-muted">Description</span></li>
<li><code>Quantity Ordered</code> <span class="text-muted">Ordered</span></li>
<li><code>Order Date</code> <span class="text-muted">Date</span></li>
<li><code>Status</code> <span class="text-muted">Order status</span></li>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- Delivery Data Format -->
<div class="accordion-item">
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#deliveryCollapse" aria-expanded="false">
🚚 Delivery Data Format (Articole livrate)
</button>
</h2>
<div id="deliveryCollapse" class="accordion-collapse collapse" data-bs-parent="#formatAccordion">
<div class="accordion-body">
<p><strong>Expected columns for Delivery Data:</strong></p>
<div class="row">
<div class="col-md-6">
<ul class="list-unstyled mb-0">
<li><code>Shipment ID</code> <span class="text-muted">Unique shipment identifier</span></li>
<li><code>Order ID</code> <span class="text-muted">Related order</span></li>
<li><code>Customer</code> <span class="text-muted">Customer info</span></li>
<li><code>Article</code> <span class="text-muted">Code/description</span></li>
</ul>
</div>
<div class="col-md-6">
<ul class="list-unstyled mb-0">
<li><code>Quantity Delivered</code> <span class="text-muted">Delivered quantity</span></li>
<li><code>Delivery Date</code> <span class="text-muted">Date</span></li>
<li><code>Status</code> <span class="text-muted">Delivery status</span></li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Upload Result Modal (Better Solution) -->
<div class="modal fade" id="uploadResultModal" tabindex="-1" aria-labelledby="uploadResultModalLabel" aria-hidden="true" data-bs-backdrop="true" data-bs-keyboard="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header" id="modalHeader">
<h5 class="modal-title" id="uploadResultModalLabel">
<i class="fas fa-check-circle"></i> Upload Result
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" id="modalCloseBtn"></button>
</div>
<div class="modal-body">
<div id="uploadResultContent" class="text-center py-3">
<!-- Result content will be inserted here -->
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" id="modalOkBtn">OK</button>
</div>
</div>
</div>
</div>
<style>
/* Additional modal fixes specific to this page */
#uploadResultModal {
z-index: 10000 !important;
pointer-events: auto !important;
}
#uploadResultModal .modal-dialog {
pointer-events: auto !important;
z-index: 10001 !important;
}
#uploadResultModal .modal-content {
pointer-events: auto !important;
background-color: white !important;
}
#uploadResultModal .modal-backdrop {
z-index: 9998 !important;
}
#uploadResultModal .btn-close,
#uploadResultModal .modal-footer button {
pointer-events: auto !important;
cursor: pointer !important;
opacity: 1 !important;
}
/* Ensure modal can be dismissed */
.modal-backdrop.show {
pointer-events: auto !important;
}
/* Dark mode modal fixes */
body.dark-mode #uploadResultModal .modal-content {
background-color: #2d3748 !important;
}
.accordion-button:not(.collapsed) {
background-color: #e7f3ff;
color: #0066cc;
}
.form-control:focus {
border-color: #007bff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
.btn-primary {
transition: all 0.3s ease;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 123, 255, 0.3);
}
/* Result stats styling */
.upload-stats {
display: flex;
justify-content: space-around;
flex-wrap: wrap;
margin-top: 20px;
}
.stat-box {
text-align: center;
padding: 15px;
min-width: 100px;
margin: 5px;
border-radius: 8px;
}
.stat-box.success {
background-color: #d4edda;
border: 1px solid #c3e6cb;
}
.stat-box.warning {
background-color: #fff3cd;
border: 1px solid #ffeeba;
}
.stat-box.error {
background-color: #f8d7da;
border: 1px solid #f5c6cb;
}
.stat-value {
font-size: 2rem;
font-weight: bold;
display: block;
}
.stat-label {
font-size: 0.9rem;
color: #666;
}
/* Reduce font size for Excel Format Instructions card rows */
.col-lg-6:nth-child(2) .card-body {
font-size: 0.95rem;
}
/* Make accordion button labels smaller */
.accordion-button {
font-size: 1rem;
padding-top: 0.4rem;
padding-bottom: 0.4rem;
}
/* Override h2 size in accordion headers */
.accordion-header {
font-size: 1rem;
}
.accordion-header h2 {
font-size: 1rem;
margin: 0;
}
/* Modal summary list styling */
#uploadResultContent ul {
list-style-type: none;
padding-left: 0;
margin: 10px auto;
max-width: 400px;
}
#uploadResultContent ul li {
padding: 5px 0;
font-size: 1rem;
}
#uploadResultContent ul li::before {
content: '✓ ';
color: #28a745;
font-weight: bold;
margin-right: 5px;
}
/* Make "Expected columns" text smaller in accordion bodies */
.accordion-body p {
font-size: 0.9rem;
}
.accordion-body strong {
font-size: 0.9rem;
}
/* Dark mode styles */
body.dark-mode .card {
background-color: #2d3748;
border-color: #4a5568;
color: #e2e8f0;
box-shadow: 0 4px 6px rgba(255, 255, 255, 0.1);
}
body.dark-mode .card-header {
background-color: #4a5568;
color: #e2e8f0;
border-bottom: 1px solid rgba(226, 232, 240, 0.2);
}
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 .accordion-button {
background-color: #374151;
color: #e2e8f0;
border-color: #6b7280;
}
body.dark-mode .accordion-button:not(.collapsed) {
background-color: #1e3a8a;
color: #bfdbfe;
}
body.dark-mode .accordion-body {
background-color: #374151;
color: #e2e8f0;
}
body.dark-mode code {
background-color: #374151;
color: #e2e8f0;
border: 1px solid #6b7280;
}
body.dark-mode .modal-content {
background-color: #2d3748;
color: #e2e8f0;
}
body.dark-mode .modal-header {
border-bottom-color: #4a5568;
}
body.dark-mode .modal-footer {
border-top-color: #4a5568;
}
body.dark-mode .stat-box.success {
background-color: #1e4620;
border-color: #2d5a2e;
color: #a3d9a5;
}
body.dark-mode .stat-box.warning {
background-color: #5a4a1e;
border-color: #6b5a2d;
color: #f4d88f;
}
body.dark-mode .stat-box.error {
background-color: #5a1e1e;
border-color: #6b2d2d;
color: #f8a3a8;
}
body.dark-mode .stat-label {
color: #a0aec0;
}
body.dark-mode .text-muted {
color: #a0aec0 !important;
}
</style>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Initialize theme on page load
const savedTheme = localStorage.getItem('theme');
const body = document.body;
if (savedTheme === 'dark') {
body.classList.add('dark-mode');
} else {
body.classList.remove('dark-mode');
}
const tableSelect = document.getElementById('target_table');
const tableDescription = document.getElementById('tableDescription');
const uploadBtn = document.getElementById('uploadBtn');
const uploadForm = document.getElementById('uploadForm');
const fileInput = document.getElementById('excel_file');
// Update table description when selection changes
tableSelect.addEventListener('change', function() {
const selectedOption = this.options[this.selectedIndex];
if (selectedOption.value) {
const description = selectedOption.getAttribute('data-description');
tableDescription.innerHTML = `<strong>Selected:</strong> ${description}`;
tableDescription.className = 'form-text text-info mt-2';
} else {
tableDescription.innerHTML = 'Select a table to see its description.';
tableDescription.className = 'form-text text-muted mt-2';
}
});
// File input change handler to show file info
fileInput.addEventListener('change', function(e) {
const file = e.target.files[0];
if (file) {
const fileName = file.name;
const fileSize = (file.size / 1024 / 1024).toFixed(2);
console.log(`Selected file: ${fileName} (${fileSize} MB)`);
}
});
// Upload button click handler (AJAX submission)
uploadBtn.addEventListener('click', function(e) {
e.preventDefault();
// Validate file selection
if (!fileInput.files.length) {
alert('Please select an Excel file to upload.');
return;
}
// Validate table selection
if (!tableSelect.value) {
alert('Please select a target table.');
return;
}
// Check file size (10MB limit)
const file = fileInput.files[0];
if (file.size > 10 * 1024 * 1024) {
alert('File size must be less than 10MB.');
return;
}
// Prepare form data
const formData = new FormData(uploadForm);
// Show loading state
uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Processing...';
uploadBtn.disabled = true;
// Submit via AJAX
fetch('/daily_mirror/build_database', {
method: 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest'
}
})
.then(response => {
if (!response.ok) {
return response.json().then(err => Promise.reject(err));
}
return response.json();
})
.then(result => {
// Reset button
uploadBtn.innerHTML = '<i class="fas fa-cloud-upload-alt"></i> Upload and Process File';
uploadBtn.disabled = false;
// Show result in modal
showUploadResult(result);
// Reset form
uploadForm.reset();
tableDescription.innerHTML = 'Select a table to see its description.';
tableDescription.className = 'form-text text-muted mt-2';
})
.catch(error => {
// Reset button
uploadBtn.innerHTML = '<i class="fas fa-cloud-upload-alt"></i> Upload and Process File';
uploadBtn.disabled = false;
// Show error in modal
showUploadError(error);
});
});
function showUploadResult(result) {
const modal = new bootstrap.Modal(document.getElementById('uploadResultModal'));
const modalHeader = document.getElementById('modalHeader');
const modalTitle = document.getElementById('uploadResultModalLabel');
const content = document.getElementById('uploadResultContent');
// Determine overall status
const hasErrors = result.error_count && result.error_count > 0;
const hasSuccess = result.created_rows > 0 || result.updated_rows > 0;
// Update modal header color
if (hasErrors && !hasSuccess) {
modalHeader.className = 'modal-header bg-danger text-white';
modalTitle.innerHTML = '<i class="fas fa-times-circle"></i> Upload Failed';
} else if (hasErrors && hasSuccess) {
modalHeader.className = 'modal-header bg-warning text-dark';
modalTitle.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Upload Completed with Warnings';
} else {
modalHeader.className = 'modal-header bg-success text-white';
modalTitle.innerHTML = '<i class="fas fa-check-circle"></i> Upload Successful';
}
// Build result content with stats
let html = '<div class="upload-stats">';
// Total rows processed from Excel
html += `
<div class="stat-box ${hasErrors ? 'warning' : 'success'}">
<span class="stat-value">${result.total_rows || 0}</span>
<span class="stat-label">Rows Processed</span>
</div>
`;
// Created rows (new in database)
if (result.created_rows > 0) {
html += `
<div class="stat-box success">
<span class="stat-value">${result.created_rows}</span>
<span class="stat-label">New Rows Created</span>
</div>
`;
}
// Updated rows (existing in database)
if (result.updated_rows > 0) {
html += `
<div class="stat-box success">
<span class="stat-value">${result.updated_rows}</span>
<span class="stat-label">Rows Updated</span>
</div>
`;
}
// Errors
if (hasErrors) {
html += `
<div class="stat-box error">
<span class="stat-value">${result.error_count}</span>
<span class="stat-label">Errors</span>
</div>
`;
}
html += '</div>';
// Add detailed summary message
const successCount = (result.created_rows || 0) + (result.updated_rows || 0);
if (successCount > 0) {
let msg = `<p class="mt-3 mb-0"><strong>Successfully processed ${result.total_rows} rows from Excel:</strong></p>`;
msg += '<ul class="text-start">';
if (result.created_rows > 0) {
msg += `<li>${result.created_rows} new ${result.created_rows === 1 ? 'record' : 'records'} created in database</li>`;
}
if (result.updated_rows > 0) {
msg += `<li>${result.updated_rows} existing ${result.updated_rows === 1 ? 'record' : 'records'} updated</li>`;
}
msg += '</ul>';
html += msg;
}
if (hasErrors) {
html += `<p class="text-danger mb-0"><strong>⚠️ ${result.error_count} ${result.error_count === 1 ? 'row' : 'rows'} could not be processed due to errors.</strong></p>`;
}
// Add auto-close countdown for successful uploads without errors
if (!hasErrors && successCount > 0) {
html += `<p class="text-muted mt-2 mb-0" id="autoCloseCountdown"><small>This window will close automatically in <span id="countdown">3</span> seconds...</small></p>`;
}
content.innerHTML = html;
modal.show();
// Get modal element
const modalElement = document.getElementById('uploadResultModal');
// Add explicit close handlers
const okBtn = document.getElementById('modalOkBtn');
const closeBtn = document.getElementById('modalCloseBtn');
if (okBtn) {
okBtn.onclick = function() {
modal.hide();
// Also trigger Bootstrap's native close
modalElement.classList.remove('show');
document.querySelector('.modal-backdrop')?.remove();
document.body.classList.remove('modal-open');
document.body.style.overflow = '';
document.body.style.paddingRight = '';
};
}
if (closeBtn) {
closeBtn.onclick = function() {
modal.hide();
// Also trigger Bootstrap's native close
modalElement.classList.remove('show');
document.querySelector('.modal-backdrop')?.remove();
document.body.classList.remove('modal-open');
document.body.style.overflow = '';
document.body.style.paddingRight = '';
};
}
// Auto-close after 3 seconds for successful uploads without errors
if (!hasErrors && successCount > 0) {
let countdown = 3;
const countdownInterval = setInterval(function() {
countdown--;
const countdownSpan = document.getElementById('countdown');
if (countdownSpan) {
countdownSpan.textContent = countdown;
}
if (countdown <= 0) {
clearInterval(countdownInterval);
}
}, 1000);
setTimeout(function() {
clearInterval(countdownInterval);
modal.hide();
modalElement.classList.remove('show');
document.querySelector('.modal-backdrop')?.remove();
document.body.classList.remove('modal-open');
document.body.style.overflow = '';
document.body.style.paddingRight = '';
}, 3000);
}
}
function showUploadError(error) {
const modal = new bootstrap.Modal(document.getElementById('uploadResultModal'));
const modalHeader = document.getElementById('modalHeader');
const modalTitle = document.getElementById('uploadResultModalLabel');
const content = document.getElementById('uploadResultContent');
// Update modal header
modalHeader.className = 'modal-header bg-danger text-white';
modalTitle.innerHTML = '<i class="fas fa-times-circle"></i> Upload Error';
// Show error message
const errorMsg = error.error || error.message || 'An unexpected error occurred during upload.';
content.innerHTML = `
<div class="alert alert-danger mb-0">
<i class="fas fa-exclamation-triangle"></i>
<strong>Error:</strong> ${errorMsg}
</div>
`;
modal.show();
// Get modal element
const modalElement = document.getElementById('uploadResultModal');
// Add explicit close handlers
const okBtn = document.getElementById('modalOkBtn');
const closeBtn = document.getElementById('modalCloseBtn');
if (okBtn) {
okBtn.onclick = function() {
modal.hide();
// Also trigger Bootstrap's native close
modalElement.classList.remove('show');
document.querySelector('.modal-backdrop')?.remove();
document.body.classList.remove('modal-open');
document.body.style.overflow = '';
document.body.style.paddingRight = '';
};
}
if (closeBtn) {
closeBtn.onclick = function() {
modal.hide();
// Also trigger Bootstrap's native close
modalElement.classList.remove('show');
document.querySelector('.modal-backdrop')?.remove();
document.body.classList.remove('modal-open');
document.body.style.overflow = '';
document.body.style.paddingRight = '';
};
}
}
});
</script>
{% endblock %}

View File

@@ -0,0 +1,449 @@
{% extends "base.html" %}
{% block title %}Daily Mirror History - Quality Recticel{% endblock %}
{% block content %}
<div class="container-fluid">
<!-- Page Header -->
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<div>
<h1 class="h3 mb-0">📋 Daily Mirror History</h1>
<p class="text-muted">Analyze historical daily production reports and trends</p>
</div>
<div>
<a href="{{ url_for('daily_mirror.daily_mirror_route') }}" class="btn btn-outline-success">
<i class="fas fa-chart-line"></i> Create New Report
</a>
<a href="{{ url_for('main.dashboard') }}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Back to Dashboard
</a>
</div>
</div>
</div>
</div>
<!-- Date Range Selection -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-calendar-week"></i> Select Date Range
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3">
<label for="startDate" class="form-label">Start Date:</label>
<input type="date" class="form-control" id="startDate" value="{{ start_date }}">
</div>
<div class="col-md-3">
<label for="endDate" class="form-label">End Date:</label>
<input type="date" class="form-control" id="endDate" value="{{ end_date }}">
</div>
<div class="col-md-3 d-flex align-items-end">
<button type="button" class="btn btn-primary" onclick="loadHistoryData()">
<i class="fas fa-search"></i> Load History
</button>
</div>
<div class="col-md-3 d-flex align-items-end">
<div class="btn-group" role="group">
<button type="button" class="btn btn-outline-secondary" onclick="setDateRange(7)">Last 7 days</button>
<button type="button" class="btn btn-outline-secondary" onclick="setDateRange(30)">Last 30 days</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Loading Indicator -->
<div id="loadingIndicator" class="row mb-4" style="display: none;">
<div class="col-12">
<div class="card">
<div class="card-body text-center">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2 mb-0">Loading historical data...</p>
</div>
</div>
</div>
</div>
<!-- Summary Statistics -->
<div id="summaryStats" style="display: none;">
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-chart-bar"></i> Period Summary
<span id="periodRange" class="badge bg-secondary ms-2"></span>
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3">
<div class="summary-metric">
<h4 id="totalOrdersQuantity">-</h4>
<p>Total Orders Quantity</p>
<small id="avgOrdersQuantity" class="text-muted">Avg: -</small>
</div>
</div>
<div class="col-md-3">
<div class="summary-metric">
<h4 id="totalProductionLaunched">-</h4>
<p>Total Production Launched</p>
<small id="avgProductionLaunched" class="text-muted">Avg: -</small>
</div>
</div>
<div class="col-md-3">
<div class="summary-metric">
<h4 id="totalProductionFinished">-</h4>
<p>Total Production Finished</p>
<small id="avgProductionFinished" class="text-muted">Avg: -</small>
</div>
</div>
<div class="col-md-3">
<div class="summary-metric">
<h4 id="totalOrdersDelivered">-</h4>
<p>Total Orders Delivered</p>
<small id="avgOrdersDelivered" class="text-muted">Avg: -</small>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Historical Data Table -->
<div id="historyTable" style="display: none;">
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">
<i class="fas fa-table"></i> Historical Daily Reports
</h5>
<div class="btn-group btn-group-sm" role="group">
<button type="button" class="btn btn-outline-primary" onclick="exportHistoryCSV()">
<i class="fas fa-file-csv"></i> CSV
</button>
<button type="button" class="btn btn-outline-success" onclick="exportHistoryExcel()">
<i class="fas fa-file-excel"></i> Excel
</button>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover" id="historyDataTable">
<thead class="table-dark">
<tr>
<th>Date</th>
<th>Orders Quantity</th>
<th>Production Launched</th>
<th>Production Finished</th>
<th>Orders Delivered</th>
<th>Quality Approval Rate</th>
<th>FG Quality Approval Rate</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="historyTableBody">
<!-- Data will be populated here -->
</tbody>
</table>
</div>
<!-- Pagination -->
<nav aria-label="History pagination" id="historyPagination" style="display: none;">
<ul class="pagination pagination-sm justify-content-center">
<!-- Pagination will be populated here -->
</ul>
</nav>
</div>
</div>
</div>
</div>
</div>
<!-- Chart Visualization -->
<div id="chartVisualization" style="display: none;">
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-chart-line"></i> Trend Analysis
</h5>
</div>
<div class="card-body">
<canvas id="trendChart" height="100"></canvas>
</div>
</div>
</div>
</div>
</div>
<!-- Error Message -->
<div id="errorMessage" class="row mb-4" style="display: none;">
<div class="col-12">
<div class="alert alert-danger" role="alert">
<i class="fas fa-exclamation-triangle"></i>
<strong>Error:</strong> <span id="errorText"></span>
</div>
</div>
</div>
</div>
<style>
.summary-metric {
text-align: center;
padding: 1rem;
background: #f8f9fa;
border-radius: 8px;
margin-bottom: 1rem;
}
.summary-metric h4 {
font-size: 1.8rem;
font-weight: bold;
color: #2c3e50;
margin: 0;
}
.summary-metric p {
margin: 0.5rem 0;
color: #6c757d;
font-weight: 500;
}
.table th {
white-space: nowrap;
}
.approval-rate {
font-weight: bold;
}
.approval-rate.high {
color: #28a745;
}
.approval-rate.medium {
color: #ffc107;
}
.approval-rate.low {
color: #dc3545;
}
@media (max-width: 768px) {
.table-responsive {
font-size: 0.9rem;
}
}
</style>
<script>
let historyData = [];
let currentPage = 1;
const itemsPerPage = 20;
function setDateRange(days) {
const endDate = new Date();
const startDate = new Date();
startDate.setDate(endDate.getDate() - days);
document.getElementById('endDate').value = endDate.toISOString().split('T')[0];
document.getElementById('startDate').value = startDate.toISOString().split('T')[0];
loadHistoryData();
}
function loadHistoryData() {
const startDate = document.getElementById('startDate').value;
const endDate = document.getElementById('endDate').value;
if (!startDate || !endDate) {
showError('Please select both start and end dates');
return;
}
if (new Date(startDate) > new Date(endDate)) {
showError('Start date cannot be after end date');
return;
}
// Show loading indicator
document.getElementById('loadingIndicator').style.display = 'block';
document.getElementById('summaryStats').style.display = 'none';
document.getElementById('historyTable').style.display = 'none';
document.getElementById('chartVisualization').style.display = 'none';
document.getElementById('errorMessage').style.display = 'none';
// Make API call to get historical data
fetch(`/daily_mirror/api/history_data?start_date=${startDate}&end_date=${endDate}`)
.then(response => response.json())
.then(data => {
if (data.error) {
showError(data.error);
return;
}
historyData = data.history;
// Update displays
updateSummaryStats(data);
updateHistoryTable();
updateTrendChart();
// Hide loading and show results
document.getElementById('loadingIndicator').style.display = 'none';
document.getElementById('summaryStats').style.display = 'block';
document.getElementById('historyTable').style.display = 'block';
document.getElementById('chartVisualization').style.display = 'block';
})
.catch(error => {
console.error('Error loading history data:', error);
showError('Failed to load historical data. Please try again.');
});
}
function updateSummaryStats(data) {
const history = data.history;
if (history.length === 0) {
document.getElementById('periodRange').textContent = 'No Data';
return;
}
document.getElementById('periodRange').textContent = `${data.start_date} to ${data.end_date}`;
// Calculate totals and averages
const totals = history.reduce((acc, day) => {
acc.ordersQuantity += day.orders_quantity;
acc.productionLaunched += day.production_launched;
acc.productionFinished += day.production_finished;
acc.ordersDelivered += day.orders_delivered;
return acc;
}, { ordersQuantity: 0, productionLaunched: 0, productionFinished: 0, ordersDelivered: 0 });
const avgDivisor = history.length;
document.getElementById('totalOrdersQuantity').textContent = totals.ordersQuantity.toLocaleString();
document.getElementById('avgOrdersQuantity').textContent = `Avg: ${Math.round(totals.ordersQuantity / avgDivisor).toLocaleString()}`;
document.getElementById('totalProductionLaunched').textContent = totals.productionLaunched.toLocaleString();
document.getElementById('avgProductionLaunched').textContent = `Avg: ${Math.round(totals.productionLaunched / avgDivisor).toLocaleString()}`;
document.getElementById('totalProductionFinished').textContent = totals.productionFinished.toLocaleString();
document.getElementById('avgProductionFinished').textContent = `Avg: ${Math.round(totals.productionFinished / avgDivisor).toLocaleString()}`;
document.getElementById('totalOrdersDelivered').textContent = totals.ordersDelivered.toLocaleString();
document.getElementById('avgOrdersDelivered').textContent = `Avg: ${Math.round(totals.ordersDelivered / avgDivisor).toLocaleString()}`;
}
function updateHistoryTable() {
const tbody = document.getElementById('historyTableBody');
tbody.innerHTML = '';
const startIndex = (currentPage - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const pageData = historyData.slice(startIndex, endIndex);
pageData.forEach(day => {
const row = document.createElement('tr');
const qualityRate = day.quality_scans.approval_rate;
const fgQualityRate = day.fg_quality_scans.approval_rate;
row.innerHTML = `
<td><strong>${day.date}</strong></td>
<td>${day.orders_quantity.toLocaleString()}</td>
<td>${day.production_launched.toLocaleString()}</td>
<td>${day.production_finished.toLocaleString()}</td>
<td>${day.orders_delivered.toLocaleString()}</td>
<td><span class="approval-rate ${getApprovalRateClass(qualityRate)}">${qualityRate}%</span></td>
<td><span class="approval-rate ${getApprovalRateClass(fgQualityRate)}">${fgQualityRate}%</span></td>
<td>
<button class="btn btn-sm btn-outline-primary" onclick="viewDayDetails('${day.date}')">
<i class="fas fa-eye"></i> View
</button>
</td>
`;
tbody.appendChild(row);
});
updatePagination();
}
function getApprovalRateClass(rate) {
if (rate >= 95) return 'high';
if (rate >= 85) return 'medium';
return 'low';
}
function updatePagination() {
const totalPages = Math.ceil(historyData.length / itemsPerPage);
if (totalPages <= 1) {
document.getElementById('historyPagination').style.display = 'none';
return;
}
document.getElementById('historyPagination').style.display = 'block';
// Pagination implementation can be added here
}
function updateTrendChart() {
// Chart implementation using Chart.js can be added here
// For now, we'll show a placeholder
const canvas = document.getElementById('trendChart');
const ctx = canvas.getContext('2d');
// Clear canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw placeholder text
ctx.font = '16px Arial';
ctx.fillStyle = '#6c757d';
ctx.textAlign = 'center';
ctx.fillText('Trend chart visualization will be implemented here', canvas.width / 2, canvas.height / 2);
}
function viewDayDetails(date) {
// Navigate to daily mirror with specific date
window.open(`{{ url_for('daily_mirror.daily_mirror_route') }}?date=${date}`, '_blank');
}
function exportHistoryCSV() {
alert('CSV export functionality will be implemented soon.');
}
function exportHistoryExcel() {
alert('Excel export functionality will be implemented soon.');
}
function showError(message) {
document.getElementById('errorText').textContent = message;
document.getElementById('errorMessage').style.display = 'block';
document.getElementById('loadingIndicator').style.display = 'none';
document.getElementById('summaryStats').style.display = 'none';
document.getElementById('historyTable').style.display = 'none';
document.getElementById('chartVisualization').style.display = 'none';
}
// Auto-load data on page load
document.addEventListener('DOMContentLoaded', function() {
loadHistoryData();
});
</script>
{% endblock %}

View File

@@ -0,0 +1,262 @@
{% extends "base.html" %}
{% block title %}Daily Mirror - Quality Recticel{% endblock %}
{% block content %}
<div class="container-fluid">
<!-- Page Header -->
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<div>
<h1 class="h3 mb-0">📊 Daily Mirror</h1>
<p class="text-muted">Business Intelligence and Production Reporting</p>
</div>
<div>
<!-- Buttons removed; now present in top header -->
</div>
</div>
</div>
</div>
<!-- Daily Mirror Cards -->
<div class="row">
<!-- Card 1: Build Database -->
<div class="col-lg-6 col-md-6 mb-4">
<div class="card h-100 daily-mirror-card">
<div class="card-body d-flex flex-column">
<div class="text-center mb-3">
<div class="feature-icon bg-primary">
<i class="fas fa-database"></i>
</div>
</div>
<h5 class="card-title text-center">Build Database</h5>
<p class="card-text flex-grow-1 text-center">
Upload Excel files to create and populate tables.
</p>
<div class="mt-auto">
<a href="{{ url_for('daily_mirror.daily_mirror_build_database') }}" class="btn btn-primary btn-block w-100">
<i class="fas fa-hammer"></i> Build Database
</a>
</div>
</div>
</div>
</div>
<!-- Card 2: Tune Database -->
<div class="col-lg-6 col-md-6 mb-4">
<div class="card h-100 daily-mirror-card">
<div class="card-body d-flex flex-column">
<div class="text-center mb-3">
<div class="feature-icon bg-warning">
<i class="fas fa-edit"></i>
</div>
</div>
<h5 class="card-title text-center">Tune Database</h5>
<p class="card-text flex-grow-1 text-center">
Edit and update records after import.
</p>
<div class="mt-auto">
<a href="{{ url_for('daily_mirror.tune_production_data') }}" class="btn btn-warning btn-block w-100 btn-sm mb-2">
<i class="fas fa-industry"></i> Production Orders
</a>
<a href="{{ url_for('daily_mirror.tune_orders_data') }}" class="btn btn-warning btn-block w-100 btn-sm mb-2">
<i class="fas fa-shopping-cart"></i> Customer Orders
</a>
<a href="{{ url_for('daily_mirror.tune_delivery_data') }}" class="btn btn-warning btn-block w-100 btn-sm">
<i class="fas fa-truck"></i> Delivery Records
</a>
</div>
</div>
</div>
</div>
<!-- Card 3: Daily Mirror -->
<div class="col-lg-6 col-md-6 mb-4">
<div class="card h-100 daily-mirror-card">
<div class="card-body d-flex flex-column">
<div class="text-center mb-3">
<div class="feature-icon bg-success">
<i class="fas fa-chart-line"></i>
</div>
</div>
<h5 class="card-title text-center">Daily Mirror</h5>
<p class="card-text flex-grow-1 text-center">
Generate daily production reports.
</p>
<div class="mt-auto">
<a href="{{ url_for('daily_mirror.daily_mirror_route') }}" class="btn btn-success btn-block w-100">
<i class="fas fa-plus-circle"></i> Create Daily Report
</a>
</div>
</div>
</div>
</div>
<!-- Card 4: Daily Mirror History -->
<div class="col-lg-6 col-md-6 mb-4">
<div class="card h-100 daily-mirror-card">
<div class="card-body d-flex flex-column">
<div class="text-center mb-3">
<div class="feature-icon bg-info">
<i class="fas fa-history"></i>
</div>
</div>
<h5 class="card-title text-center">Daily Mirror History</h5>
<p class="card-text flex-grow-1 text-center">
View historical production reports.
</p>
<div class="mt-auto">
<a href="{{ url_for('daily_mirror.daily_mirror_history_route') }}" class="btn btn-info btn-block w-100">
<i class="fas fa-chart-bar"></i> View History
</a>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
.daily-mirror-card {
transition: transform 0.2s ease, box-shadow 0.2s ease;
border: none;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
background-color: var(--card-bg);
color: var(--text-color);
}
.daily-mirror-card:hover {
transform: translateY(-5px);
box-shadow: 0 4px 15px rgba(0,0,0,0.15);
}
.feature-icon {
width: 80px;
height: 80px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto;
color: white;
font-size: 2rem;
}
/* Light mode styles */
body:not(.dark-mode) .daily-mirror-card {
background-color: #ffffff;
color: #333333;
border: 1px solid #e0e0e0;
}
body:not(.dark-mode) .card-text {
color: #666666;
}
body:not(.dark-mode) .text-muted {
color: #6c757d !important;
}
/* Dark mode styles */
body.dark-mode .daily-mirror-card {
background-color: #2d3748;
color: #e2e8f0;
border: 1px solid #4a5568;
box-shadow: 0 2px 4px rgba(255,255,255,0.1);
}
body.dark-mode .daily-mirror-card:hover {
box-shadow: 0 4px 15px rgba(255,255,255,0.15);
}
body.dark-mode .card-text {
color: #cbd5e0;
}
body.dark-mode .text-muted {
color: #a0aec0 !important;
}
body.dark-mode .h3 {
color: #e2e8f0;
}
body.dark-mode .container-fluid {
color: #e2e8f0;
}
/* Ensure buttons maintain their intended colors in both themes */
.btn-primary {
background: linear-gradient(135deg, #007bff 0%, #0056b3 100%);
border: none;
color: white;
}
.btn-warning {
background: linear-gradient(135deg, #ffc107 0%, #e0a800 100%);
border: none;
color: #212529;
}
.btn-success {
background: linear-gradient(135deg, #28a745 0%, #1e7e34 100%);
border: none;
color: white;
}
.btn-info {
background: linear-gradient(135deg, #17a2b8 0%, #138496 100%);
border: none;
color: white;
}
.btn-secondary {
background: linear-gradient(135deg, #6c757d 0%, #545b62 100%);
border: none;
color: white;
}
@media (max-width: 768px) {
.feature-icon {
width: 60px;
height: 60px;
font-size: 1.5rem;
}
}</style>
<script>
// Initialize theme on page load
document.addEventListener('DOMContentLoaded', function() {
// Apply saved theme from localStorage
const savedTheme = localStorage.getItem('theme');
const body = document.body;
if (savedTheme === 'dark') {
body.classList.add('dark-mode');
} else {
body.classList.remove('dark-mode');
}
// Update theme toggle button text if it exists
const themeToggleButton = document.getElementById('theme-toggle');
if (themeToggleButton) {
if (body.classList.contains('dark-mode')) {
themeToggleButton.textContent = 'Change to Light Mode';
} else {
themeToggleButton.textContent = 'Change to Dark Mode';
}
}
});
function showComingSoon(feature) {
alert(`${feature} functionality will be available in a future update!\n\nThis feature is currently under development and will include advanced capabilities for enhanced Daily Mirror operations.`);
}
// Auto-refresh quick stats every 5 minutes
setInterval(function() {
// This could be implemented to refresh the quick stats
console.log('Auto-refresh daily stats (not implemented yet)');
}, 300000); // 5 minutes
</script>
{% endblock %}

View File

@@ -0,0 +1,549 @@
{% extends "base.html" %}
{% block title %}Tune Delivery Data - Daily Mirror{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/daily_mirror_tune.css') }}">
{% endblock %}
{% block content %}
<div class="container-fluid">
<!-- Page Header -->
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<div>
<h1 class="h3 mb-0">🚚 Tune Delivery Data</h1>
<p class="text-muted">Edit and update delivery records information</p>
</div>
<div>
<!-- Buttons removed; now present in top header -->
</div>
</div>
</div>
</div>
<!-- Filters Section -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-filter"></i> Filters and Search
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3 mb-3">
<label for="searchInput" class="form-label">Search</label>
<input type="text" class="form-control" id="searchInput"
placeholder="Search by shipment, customer, or article...">
</div>
<div class="col-md-3 mb-3">
<label for="statusFilter" class="form-label">Delivery Status</label>
<select class="form-control" id="statusFilter">
<option value="">All Statuses</option>
<!-- Will be populated dynamically -->
</select>
</div>
<div class="col-md-3 mb-3">
<label for="customerFilter" class="form-label">Customer</label>
<select class="form-control" id="customerFilter">
<option value="">All Customers</option>
<!-- Will be populated dynamically -->
</select>
</div>
<div class="col-md-3 mb-3">
<label for="recordsPerPage" class="form-label">Records per page</label>
<select class="form-control" id="recordsPerPage">
<option value="25">25</option>
<option value="50" selected>50</option>
<option value="100">100</option>
</select>
</div>
</div>
<div class="row">
<div class="col-12">
<button class="btn btn-primary" onclick="loadDeliveryData()">
<i class="fas fa-search"></i> Apply Filters
</button>
<button class="btn btn-secondary" onclick="clearFilters()">
<i class="fas fa-times"></i> Clear
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Data Table Section -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">
<i class="fas fa-table"></i> Delivery Records Data
</h5>
<div class="d-flex align-items-center">
<span id="recordsInfo" class="text-muted me-3"></span>
{% if session.get('role') == 'superadmin' %}
<button class="btn btn-danger btn-sm me-2" onclick="clearAllDelivery()" id="clearAllBtn">
<i class="fas fa-trash-alt"></i> Clear All Delivery
</button>
{% endif %}
<button class="btn btn-success btn-sm" onclick="saveAllChanges()">
<i class="fas fa-save"></i> Save All Changes
</button>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover" id="deliveryTable">
<thead class="table-dark">
<tr>
<th>Shipment ID</th>
<th>Customer</th>
<th>Order ID</th>
<th>Article Code</th>
<th>Description</th>
<th>Quantity</th>
<th>Shipment Date</th>
<th>Delivery Date</th>
<th>Status</th>
<th>Total Value</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="deliveryTableBody">
<!-- Data will be loaded here -->
</tbody>
</table>
</div>
<!-- Loading indicator -->
<div id="loadingIndicator" class="text-center py-4" style="display: none;">
<i class="fas fa-spinner fa-spin fa-2x"></i>
<p class="mt-2">Loading data...</p>
</div>
<!-- No data message -->
<div id="noDataMessage" class="text-center py-4" style="display: none;">
<i class="fas fa-info-circle fa-2x text-muted"></i>
<p class="mt-2 text-muted">No delivery records found</p>
</div>
</div>
<!-- Pagination -->
<div class="card-footer">
<nav aria-label="Delivery data pagination">
<ul class="pagination pagination-sm justify-content-center mb-0" id="pagination">
<!-- Pagination will be generated here -->
</ul>
</nav>
</div>
</div>
</div>
</div>
</div>
<!-- Edit Modal -->
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editModalLabel">Edit Delivery Record</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="editForm">
<input type="hidden" id="editRecordId">
<div class="row">
<div class="col-md-6 mb-3">
<label for="editShipmentId" class="form-label">Shipment ID</label>
<input type="text" class="form-control" id="editShipmentId" readonly>
</div>
<div class="col-md-6 mb-3">
<label for="editOrderId" class="form-label">Order ID</label>
<input type="text" class="form-control" id="editOrderId">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="editCustomerCode" class="form-label">Customer Code</label>
<input type="text" class="form-control" id="editCustomerCode">
</div>
<div class="col-md-6 mb-3">
<label for="editCustomerName" class="form-label">Customer Name</label>
<input type="text" class="form-control" id="editCustomerName">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="editArticleCode" class="form-label">Article Code</label>
<input type="text" class="form-control" id="editArticleCode">
</div>
<div class="col-md-6 mb-3">
<label for="editQuantity" class="form-label">Quantity Delivered</label>
<input type="number" class="form-control" id="editQuantity">
</div>
</div>
<div class="mb-3">
<label for="editDescription" class="form-label">Article Description</label>
<textarea class="form-control" id="editDescription" rows="2"></textarea>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="editShipmentDate" class="form-label">Shipment Date</label>
<input type="date" class="form-control" id="editShipmentDate">
</div>
<div class="col-md-6 mb-3">
<label for="editDeliveryDate" class="form-label">Delivery Date</label>
<input type="date" class="form-control" id="editDeliveryDate">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label for="editDeliveryStatus" class="form-label">Delivery Status</label>
<select class="form-control" id="editDeliveryStatus">
<option value="Finalizat">Finalizat</option>
<option value="Proiect">Proiect</option>
<option value="SHIPPED">Shipped</option>
<option value="DELIVERED">Delivered</option>
<option value="RETURNED">Returned</option>
<option value="PARTIAL">Partial</option>
<option value="CANCELLED">Cancelled</option>
</select>
</div>
<div class="col-md-6 mb-3">
<label for="editTotalValue" class="form-label">Total Value (€)</label>
<input type="number" step="0.01" class="form-control" id="editTotalValue">
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
<i class="fas fa-times"></i> Cancel
</button>
<button type="button" class="btn btn-primary" onclick="saveRecord()">
<i class="fas fa-save"></i> Save Changes
</button>
</div>
</div>
</div>
</div>
<script>
let currentPage = 1;
let currentPerPage = 50;
let currentSearch = '';
let currentStatusFilter = '';
let currentCustomerFilter = '';
document.addEventListener('DOMContentLoaded', function() {
// Initialize theme
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'dark') {
document.body.classList.add('dark-mode');
}
// Load initial data
loadDeliveryData();
// Setup search on enter key
document.getElementById('searchInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
loadDeliveryData();
}
});
});
function loadDeliveryData(page = 1) {
currentPage = page;
currentPerPage = document.getElementById('recordsPerPage').value;
currentSearch = document.getElementById('searchInput').value;
currentStatusFilter = document.getElementById('statusFilter').value;
currentCustomerFilter = document.getElementById('customerFilter').value;
// Show loading indicator
document.getElementById('loadingIndicator').style.display = 'block';
document.getElementById('deliveryTableBody').style.display = 'none';
document.getElementById('noDataMessage').style.display = 'none';
const params = new URLSearchParams({
page: currentPage,
per_page: currentPerPage,
search: currentSearch,
status: currentStatusFilter,
customer: currentCustomerFilter
});
fetch(`/daily_mirror/api/tune/delivery_data?${params}`)
.then(response => response.json())
.then(data => {
document.getElementById('loadingIndicator').style.display = 'none';
if (data.success) {
if (data.data.length === 0) {
document.getElementById('noDataMessage').style.display = 'block';
} else {
displayDeliveryData(data.data);
updatePagination(data);
updateRecordsInfo(data);
// Populate filter dropdowns on first load
if (currentPage === 1) {
populateCustomerFilter(data.customers);
populateStatusFilter(data.statuses);
}
}
} else {
console.error('Error loading data:', data.error);
alert('Error loading delivery data: ' + data.error);
}
})
.catch(error => {
document.getElementById('loadingIndicator').style.display = 'none';
console.error('Error:', error);
alert('Error loading delivery data: ' + error.message);
});
}
function displayDeliveryData(data) {
const tbody = document.getElementById('deliveryTableBody');
tbody.innerHTML = '';
tbody.style.display = 'table-row-group';
data.forEach(record => {
const row = document.createElement('tr');
row.innerHTML = `
<td><strong>${record.shipment_id}</strong></td>
<td>
<small class="text-muted d-block">${record.customer_code}</small>
${record.customer_name}
</td>
<td>${record.order_id || '-'}</td>
<td><code>${record.article_code}</code></td>
<td><small>${record.article_description || '-'}</small></td>
<td><span class="badge bg-info">${record.quantity_delivered}</span></td>
<td>${record.shipment_date || '-'}</td>
<td>${record.delivery_date || '-'}</td>
<td><span class="badge bg-success">${record.delivery_status}</span></td>
<td><strong>€${parseFloat(record.total_value || 0).toFixed(2)}</strong></td>
<td>
<button class="btn btn-sm btn-primary" onclick="editRecord(${record.id})"
title="Edit Delivery">
<i class="fas fa-edit"></i>
</button>
</td>
`;
tbody.appendChild(row);
});
}
function populateCustomerFilter(customers) {
const filter = document.getElementById('customerFilter');
const currentValue = filter.value;
filter.innerHTML = '<option value="">All Customers</option>';
customers.forEach(customer => {
const option = document.createElement('option');
option.value = customer.code;
option.textContent = `${customer.code} - ${customer.name}`;
filter.appendChild(option);
});
filter.value = currentValue;
}
function populateStatusFilter(statuses) {
const filter = document.getElementById('statusFilter');
const currentValue = filter.value;
filter.innerHTML = '<option value="">All Statuses</option>';
statuses.forEach(status => {
const option = document.createElement('option');
option.value = status;
option.textContent = status;
filter.appendChild(option);
});
filter.value = currentValue;
}
function updatePagination(data) {
const pagination = document.getElementById('pagination');
pagination.innerHTML = '';
if (data.total_pages <= 1) return;
// Previous button
const prevLi = document.createElement('li');
prevLi.className = `page-item ${data.page === 1 ? 'disabled' : ''}`;
prevLi.innerHTML = `<a class="page-link" href="#" onclick="loadDeliveryData(${data.page - 1})">Previous</a>`;
pagination.appendChild(prevLi);
// Page numbers
const startPage = Math.max(1, data.page - 2);
const endPage = Math.min(data.total_pages, data.page + 2);
for (let i = startPage; i <= endPage; i++) {
const li = document.createElement('li');
li.className = `page-item ${i === data.page ? 'active' : ''}`;
li.innerHTML = `<a class="page-link" href="#" onclick="loadDeliveryData(${i})">${i}</a>`;
pagination.appendChild(li);
}
// Next button
const nextLi = document.createElement('li');
nextLi.className = `page-item ${data.page === data.total_pages ? 'disabled' : ''}`;
nextLi.innerHTML = `<a class="page-link" href="#" onclick="loadDeliveryData(${data.page + 1})">Next</a>`;
pagination.appendChild(nextLi);
}
function updateRecordsInfo(data) {
const start = (data.page - 1) * data.per_page + 1;
const end = Math.min(data.page * data.per_page, data.total_records);
document.getElementById('recordsInfo').textContent =
`Showing ${start}-${end} of ${data.total_records} deliveries`;
}
function clearFilters() {
document.getElementById('searchInput').value = '';
document.getElementById('statusFilter').value = '';
document.getElementById('customerFilter').value = '';
loadDeliveryData(1);
}
function editRecord(recordId) {
// Get data via API for editing
fetch(`/daily_mirror/api/tune/delivery_data?page=${currentPage}&per_page=${currentPerPage}&search=${currentSearch}&status=${currentStatusFilter}&customer=${currentCustomerFilter}`)
.then(response => response.json())
.then(data => {
if (data.success) {
const recordData = data.data.find(record => record.id === recordId);
if (recordData) {
populateEditModal(recordData);
const editModal = new bootstrap.Modal(document.getElementById('editModal'));
editModal.show();
}
}
})
.catch(error => {
console.error('Error:', error);
alert('Error loading record data: ' + error.message);
});
}
function populateEditModal(record) {
document.getElementById('editRecordId').value = record.id;
document.getElementById('editShipmentId').value = record.shipment_id;
document.getElementById('editOrderId').value = record.order_id || '';
document.getElementById('editCustomerCode').value = record.customer_code;
document.getElementById('editCustomerName').value = record.customer_name;
document.getElementById('editArticleCode').value = record.article_code;
document.getElementById('editDescription').value = record.article_description || '';
document.getElementById('editQuantity').value = record.quantity_delivered;
document.getElementById('editShipmentDate').value = record.shipment_date;
document.getElementById('editDeliveryDate').value = record.delivery_date;
document.getElementById('editDeliveryStatus').value = record.delivery_status;
document.getElementById('editTotalValue').value = record.total_value;
}
function saveRecord() {
const recordId = document.getElementById('editRecordId').value;
const data = {
customer_code: document.getElementById('editCustomerCode').value,
customer_name: document.getElementById('editCustomerName').value,
order_id: document.getElementById('editOrderId').value,
article_code: document.getElementById('editArticleCode').value,
article_description: document.getElementById('editDescription').value,
quantity_delivered: parseInt(document.getElementById('editQuantity').value) || 0,
shipment_date: document.getElementById('editShipmentDate').value,
delivery_date: document.getElementById('editDeliveryDate').value,
delivery_status: document.getElementById('editDeliveryStatus').value,
total_value: parseFloat(document.getElementById('editTotalValue').value) || 0
};
fetch(`/daily_mirror/api/tune/delivery_data/${recordId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(result => {
if (result.success) {
// Close modal
const editModal = bootstrap.Modal.getInstance(document.getElementById('editModal'));
editModal.hide();
// Reload data
loadDeliveryData(currentPage);
// Show success message
alert('Delivery record updated successfully!');
} else {
alert('Error updating delivery record: ' + result.error);
}
})
.catch(error => {
console.error('Error:', error);
alert('Error updating delivery record: ' + error.message);
});
}
function saveAllChanges() {
alert('Save All Changes functionality will be implemented for bulk operations.');
}
function clearAllDelivery() {
if (!confirm('⚠️ WARNING: This will permanently delete ALL delivery records from the database!\n\nAre you absolutely sure you want to continue?')) {
return;
}
// Second confirmation for safety
if (!confirm('This action CANNOT be undone! All delivery data will be lost.\n\nClick OK to proceed with deletion.')) {
return;
}
const clearBtn = document.getElementById('clearAllBtn');
const originalText = clearBtn.innerHTML;
clearBtn.disabled = true;
clearBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Deleting...';
fetch('/daily_mirror/clear_delivery', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('✅ ' + data.message);
// Reload the page to show empty table
loadDeliveryData(1);
} else {
alert('❌ Error: ' + (data.message || 'Failed to clear delivery records'));
}
})
.catch(error => {
console.error('Error:', error);
alert('❌ Error clearing delivery records: ' + error.message);
})
.finally(() => {
clearBtn.disabled = false;
clearBtn.innerHTML = originalText;
});
}
</script>
{% endblock %}

View File

@@ -0,0 +1,753 @@
{% extends "base.html" %}
{% block title %}Tune Orders Data - Daily Mirror{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/daily_mirror_tune.css') }}">
<style>
/* Force modal width - using viewport width for maximum responsiveness */
.modal#editModal .modal-dialog {
max-width: 95vw !important;
width: 95vw !important;
margin: 1.75rem auto !important;
}
.modal#editModal .modal-dialog.modal-xl {
max-width: 95vw !important;
width: 95vw !important;
}
/* Override ALL Bootstrap media queries */
@media (min-width: 576px) {
.modal#editModal .modal-dialog {
max-width: 95vw !important;
width: 95vw !important;
}
}
@media (min-width: 992px) {
.modal#editModal .modal-dialog {
max-width: 95vw !important;
width: 95vw !important;
}
}
@media (min-width: 1200px) {
.modal#editModal .modal-dialog,
.modal#editModal .modal-dialog.modal-xl {
max-width: 95vw !important;
width: 95vw !important;
}
}
/* Ensure pointer events work */
.modal#editModal .modal-dialog {
pointer-events: auto !important;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid">
<!-- Page Header -->
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<div>
<h1 class="h3 mb-0">🛒 Tune Orders Data</h1>
<p class="text-muted">Edit and update customer orders information</p>
</div>
<div>
<!-- Buttons removed; now present in top header -->
</div>
</div>
</div>
</div>
<!-- Filters Section -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-filter"></i> Filters and Search
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3 mb-3">
<label for="searchInput" class="form-label">Search</label>
<input type="text" class="form-control" id="searchInput"
placeholder="Search by order, customer, or article...">
</div>
<div class="col-md-3 mb-3">
<label for="statusFilter" class="form-label">Order Status</label>
<select class="form-control" id="statusFilter">
<option value="">All Statuses</option>
<!-- Will be populated dynamically -->
</select>
</div>
<div class="col-md-3 mb-3">
<label for="customerFilter" class="form-label">Customer</label>
<select class="form-control" id="customerFilter">
<option value="">All Customers</option>
<!-- Will be populated dynamically -->
</select>
</div>
<div class="col-md-3 mb-3">
<label for="recordsPerPage" class="form-label">Records per page</label>
<select class="form-control" id="recordsPerPage">
<option value="25">25</option>
<option value="50" selected>50</option>
<option value="100">100</option>
</select>
</div>
</div>
<div class="row">
<div class="col-12">
<button class="btn btn-primary" onclick="loadOrdersData()">
<i class="fas fa-search"></i> Apply Filters
</button>
<button class="btn btn-secondary" onclick="clearFilters()">
<i class="fas fa-times"></i> Clear
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Data Table Section -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">
<i class="fas fa-table"></i> Customer Orders Data
</h5>
<div class="d-flex align-items-center">
<span id="recordsInfo" class="text-muted me-3"></span>
{% if session.get('role') == 'superadmin' %}
<button class="btn btn-danger btn-sm me-2" onclick="clearAllOrders()" id="clearAllBtn">
<i class="fas fa-trash-alt"></i> Clear All Orders
</button>
{% endif %}
<button class="btn btn-success btn-sm" onclick="saveAllChanges()">
<i class="fas fa-save"></i> Save All Changes
</button>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover" id="ordersTable">
<thead class="table-dark">
<tr>
<th>Order Line</th>
<th>Customer</th>
<th>Client Order Line</th>
<th>Article Code</th>
<th>Description</th>
<th>Quantity</th>
<th>Delivery Date</th>
<th>Status</th>
<th>Priority</th>
<th>Product Group</th>
<th>Order Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="ordersTableBody">
<!-- Data will be loaded here -->
</tbody>
</table>
</div>
<!-- Loading indicator -->
<div id="loadingIndicator" class="text-center py-4" style="display: none;">
<i class="fas fa-spinner fa-spin fa-2x"></i>
<p class="mt-2">Loading data...</p>
</div>
<!-- No data message -->
<div id="noDataMessage" class="text-center py-4" style="display: none;">
<i class="fas fa-info-circle fa-2x text-muted"></i>
<p class="mt-2 text-muted">No orders found</p>
</div>
</div>
<!-- Pagination -->
<div class="card-footer">
<nav aria-label="Orders data pagination">
<ul class="pagination pagination-sm justify-content-center mb-0" id="pagination">
<!-- Pagination will be generated here -->
</ul>
</nav>
</div>
</div>
</div>
</div>
</div>
<!-- Edit Modal -->
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true" data-bs-backdrop="true" data-bs-keyboard="true">
<div class="modal-dialog modal-xl modal-dialog-scrollable" style="max-width: 95vw !important; width: 95vw !important;">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editModalLabel">Edit Customer Order</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="editForm">
<input type="hidden" id="editRecordId">
<div class="row">
<!-- Column 1: Order Information -->
<div class="col-md-4">
<div class="mb-3">
<label for="editOrderLine" class="form-label">Order Line</label>
<input type="text" class="form-control" id="editOrderLine" readonly>
</div>
<div class="mb-3">
<label for="editOrderId" class="form-label">Order ID</label>
<input type="text" class="form-control" id="editOrderId">
</div>
<div class="mb-3">
<label for="editLineNumber" class="form-label">Line Number</label>
<input type="text" class="form-control" id="editLineNumber">
</div>
<div class="mb-3">
<label for="editClientOrderLine" class="form-label">Client Order Line</label>
<input type="text" class="form-control" id="editClientOrderLine" readonly>
</div>
<div class="mb-3">
<label for="editOrderDate" class="form-label">Order Date</label>
<input type="date" class="form-control" id="editOrderDate">
</div>
<div class="mb-3">
<label for="editDeliveryDate" class="form-label">Delivery Date</label>
<input type="date" class="form-control" id="editDeliveryDate">
</div>
<div class="mb-3">
<label for="editOrderStatus" class="form-label">Order Status</label>
<select class="form-control" id="editOrderStatus">
<option value="PENDING">Pending</option>
<option value="CONFIRMED">Confirmed</option>
<option value="Confirmat">Confirmat</option>
<option value="IN_PROGRESS">In Progress</option>
<option value="FINISHED">Finished</option>
<option value="DELIVERED">Delivered</option>
<option value="CANCELLED">Cancelled</option>
</select>
</div>
<div class="mb-3">
<label for="editPriority" class="form-label">Priority</label>
<select class="form-control" id="editPriority">
<option value="LOW">Low</option>
<option value="NORMAL">Normal</option>
<option value="HIGH">High</option>
<option value="URGENT">Urgent</option>
</select>
</div>
</div>
<!-- Column 2: Customer and Article Information -->
<div class="col-md-4">
<div class="mb-3">
<label for="editCustomerCode" class="form-label">Customer Code</label>
<input type="text" class="form-control" id="editCustomerCode">
</div>
<div class="mb-3">
<label for="editCustomerName" class="form-label">Customer Name</label>
<input type="text" class="form-control" id="editCustomerName">
</div>
<div class="mb-3">
<label for="editArticleCode" class="form-label">Article Code</label>
<input type="text" class="form-control" id="editArticleCode">
</div>
<div class="mb-3">
<label for="editDescription" class="form-label">Article Description</label>
<textarea class="form-control" id="editDescription" rows="3"></textarea>
</div>
<div class="mb-3">
<label for="editQuantity" class="form-label">Quantity Requested</label>
<input type="number" class="form-control" id="editQuantity">
</div>
<div class="mb-3">
<label for="editBalance" class="form-label">Balance</label>
<input type="number" step="0.01" class="form-control" id="editBalance">
</div>
<div class="mb-3">
<label for="editUnitOfMeasure" class="form-label">Unit of Measure</label>
<input type="text" class="form-control" id="editUnitOfMeasure">
</div>
<div class="mb-3">
<label for="editArticleStatus" class="form-label">Article Status</label>
<input type="text" class="form-control" id="editArticleStatus">
</div>
</div>
<!-- Column 3: Production Information -->
<div class="col-md-4">
<div class="mb-3">
<label for="editProductGroup" class="form-label">Product Group</label>
<input type="text" class="form-control" id="editProductGroup">
</div>
<div class="mb-3">
<label for="editModel" class="form-label">Model</label>
<input type="text" class="form-control" id="editModel">
</div>
<div class="mb-3">
<label for="editProductionOrder" class="form-label">Production Order</label>
<input type="text" class="form-control" id="editProductionOrder">
</div>
<div class="mb-3">
<label for="editProductionStatus" class="form-label">Production Status</label>
<input type="text" class="form-control" id="editProductionStatus">
</div>
<div class="mb-3">
<label for="editClosed" class="form-label">Closed</label>
<input type="text" class="form-control" id="editClosed">
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer d-flex justify-content-between">
<button type="button" class="btn btn-danger" onclick="deleteRecord()" style="min-width: 150px;">
<i class="fas fa-trash"></i> Delete Record
</button>
<div>
<button type="button" class="btn btn-secondary me-2" data-bs-dismiss="modal" style="min-width: 100px;">Cancel</button>
<button type="button" class="btn btn-primary" onclick="saveRecord()" style="min-width: 150px;">
<i class="fas fa-save"></i> Save Changes
</button>
</div>
</div>
</div>
</div>
</div>
<script>
let currentPage = 1;
let currentPerPage = 50;
let currentSearch = '';
let currentStatusFilter = '';
let currentCustomerFilter = '';
document.addEventListener('DOMContentLoaded', function() {
// Initialize theme
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'dark') {
document.body.classList.add('dark-mode');
}
// Load initial data
loadOrdersData();
// Setup search on enter key
document.getElementById('searchInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
loadOrdersData();
}
});
});
function loadOrdersData(page = 1) {
currentPage = page;
currentPerPage = document.getElementById('recordsPerPage').value;
currentSearch = document.getElementById('searchInput').value;
currentStatusFilter = document.getElementById('statusFilter').value;
currentCustomerFilter = document.getElementById('customerFilter').value;
// Show loading indicator
document.getElementById('loadingIndicator').style.display = 'block';
document.getElementById('ordersTableBody').style.display = 'none';
document.getElementById('noDataMessage').style.display = 'none';
const params = new URLSearchParams({
page: currentPage,
per_page: currentPerPage,
search: currentSearch,
status: currentStatusFilter,
customer: currentCustomerFilter
});
fetch(`/daily_mirror/api/tune/orders_data?${params}`)
.then(response => response.json())
.then(data => {
document.getElementById('loadingIndicator').style.display = 'none';
if (data.success) {
if (data.data.length === 0) {
document.getElementById('noDataMessage').style.display = 'block';
} else {
displayOrdersData(data.data);
updatePagination(data);
updateRecordsInfo(data);
// Populate filter dropdowns on first load
if (currentPage === 1) {
populateCustomerFilter(data.customers);
populateStatusFilter(data.statuses);
}
}
} else {
console.error('Error loading data:', data.error);
alert('Error loading orders data: ' + data.error);
}
})
.catch(error => {
document.getElementById('loadingIndicator').style.display = 'none';
console.error('Error:', error);
alert('Error loading orders data: ' + error.message);
});
}
function displayOrdersData(data) {
const tbody = document.getElementById('ordersTableBody');
tbody.innerHTML = '';
tbody.style.display = 'table-row-group';
data.forEach(record => {
const row = document.createElement('tr');
row.innerHTML = `
<td><strong>${record.order_line}</strong></td>
<td>
<small class="text-muted d-block">${record.customer_code}</small>
${record.customer_name}
</td>
<td>${record.client_order_line || '-'}</td>
<td><code>${record.article_code}</code></td>
<td><small>${record.article_description || '-'}</small></td>
<td><span class="badge bg-info">${record.quantity_requested}</span></td>
<td>${record.delivery_date || '-'}</td>
<td><span class="badge bg-primary">${record.order_status}</span></td>
<td><span class="badge bg-warning">${record.priority || 'NORMAL'}</span></td>
<td><small>${record.product_group || '-'}</small></td>
<td>${record.order_date || '-'}</td>
<td>
<button class="btn btn-sm btn-primary" onclick="editRecord(${record.id})"
title="Edit Order">
<i class="fas fa-edit"></i>
</button>
</td>
`;
tbody.appendChild(row);
});
}
function populateCustomerFilter(customers) {
const filter = document.getElementById('customerFilter');
// Keep the "All Customers" option and add new ones
const currentValue = filter.value;
filter.innerHTML = '<option value="">All Customers</option>';
customers.forEach(customer => {
const option = document.createElement('option');
option.value = customer.code;
option.textContent = `${customer.code} - ${customer.name}`;
filter.appendChild(option);
});
filter.value = currentValue;
}
function populateStatusFilter(statuses) {
const filter = document.getElementById('statusFilter');
const currentValue = filter.value;
filter.innerHTML = '<option value="">All Statuses</option>';
statuses.forEach(status => {
const option = document.createElement('option');
option.value = status;
option.textContent = status;
filter.appendChild(option);
});
filter.value = currentValue;
}
function updatePagination(data) {
const pagination = document.getElementById('pagination');
pagination.innerHTML = '';
if (data.total_pages <= 1) return;
// Previous button
const prevLi = document.createElement('li');
prevLi.className = `page-item ${data.page === 1 ? 'disabled' : ''}`;
prevLi.innerHTML = `<a class="page-link" href="#" onclick="loadOrdersData(${data.page - 1})">Previous</a>`;
pagination.appendChild(prevLi);
// Page numbers
const startPage = Math.max(1, data.page - 2);
const endPage = Math.min(data.total_pages, data.page + 2);
for (let i = startPage; i <= endPage; i++) {
const li = document.createElement('li');
li.className = `page-item ${i === data.page ? 'active' : ''}`;
li.innerHTML = `<a class="page-link" href="#" onclick="loadOrdersData(${i})">${i}</a>`;
pagination.appendChild(li);
}
// Next button
const nextLi = document.createElement('li');
nextLi.className = `page-item ${data.page === data.total_pages ? 'disabled' : ''}`;
nextLi.innerHTML = `<a class="page-link" href="#" onclick="loadOrdersData(${data.page + 1})">Next</a>`;
pagination.appendChild(nextLi);
}
function updateRecordsInfo(data) {
const start = (data.page - 1) * data.per_page + 1;
const end = Math.min(data.page * data.per_page, data.total_records);
document.getElementById('recordsInfo').textContent =
`Showing ${start}-${end} of ${data.total_records} orders`;
}
function clearFilters() {
document.getElementById('searchInput').value = '';
document.getElementById('statusFilter').value = '';
document.getElementById('customerFilter').value = '';
loadOrdersData(1);
}
function editRecord(recordId) {
// Find the record data from the current display
const rows = document.querySelectorAll('#ordersTableBody tr');
let recordData = null;
// Get data via API for editing
fetch(`/daily_mirror/api/tune/orders_data?page=${currentPage}&per_page=${currentPerPage}&search=${currentSearch}&status=${currentStatusFilter}&customer=${currentCustomerFilter}`)
.then(response => response.json())
.then(data => {
if (data.success) {
recordData = data.data.find(record => record.id === recordId);
if (recordData) {
populateEditModal(recordData);
// Get modal elements
const modalElement = document.getElementById('editModal');
const modalDialog = modalElement.querySelector('.modal-dialog');
// Remove any existing modal instances to prevent conflicts
const existingModal = bootstrap.Modal.getInstance(modalElement);
if (existingModal) {
existingModal.dispose();
}
const modal = new bootstrap.Modal(modalElement, {
backdrop: true,
keyboard: true,
focus: true
});
// Show the modal first
modal.show();
// Force the modal dialog width AFTER modal is shown - using 95% of viewport width
setTimeout(() => {
if (modalDialog) {
console.log('Applying width after modal shown');
modalDialog.style.setProperty('max-width', '95vw', 'important');
modalDialog.style.setProperty('width', '95vw', 'important');
modalDialog.style.setProperty('margin', '1.75rem auto', 'important');
// Also apply to modal content for good measure
const modalContent = modalDialog.querySelector('.modal-content');
if (modalContent) {
modalContent.style.setProperty('width', '100%', 'important');
}
console.log('Modal dialog computed width:', window.getComputedStyle(modalDialog).width);
}
}, 100);
}
}
})
.catch(error => {
console.error('Error:', error);
alert('Error loading record data: ' + error.message);
});
}
function populateEditModal(record) {
document.getElementById('editRecordId').value = record.id;
document.getElementById('editOrderLine').value = record.order_line;
document.getElementById('editOrderId').value = record.order_id;
document.getElementById('editLineNumber').value = record.line_number || '';
document.getElementById('editCustomerCode').value = record.customer_code;
document.getElementById('editCustomerName').value = record.customer_name;
document.getElementById('editClientOrderLine').value = record.client_order_line || '';
document.getElementById('editArticleCode').value = record.article_code;
document.getElementById('editDescription').value = record.article_description || '';
document.getElementById('editQuantity').value = record.quantity_requested;
document.getElementById('editBalance').value = record.balance || '';
document.getElementById('editUnitOfMeasure').value = record.unit_of_measure || '';
document.getElementById('editDeliveryDate').value = record.delivery_date;
document.getElementById('editOrderDate').value = record.order_date;
document.getElementById('editOrderStatus').value = record.order_status;
document.getElementById('editArticleStatus').value = record.article_status || '';
document.getElementById('editPriority').value = record.priority || 'NORMAL';
document.getElementById('editProductGroup').value = record.product_group || '';
document.getElementById('editProductionOrder').value = record.production_order || '';
document.getElementById('editProductionStatus').value = record.production_status || '';
document.getElementById('editModel').value = record.model || '';
document.getElementById('editClosed').value = record.closed || '';
}
function saveRecord() {
const recordId = document.getElementById('editRecordId').value;
const data = {
order_line: document.getElementById('editOrderLine').value,
order_id: document.getElementById('editOrderId').value,
line_number: document.getElementById('editLineNumber').value,
customer_code: document.getElementById('editCustomerCode').value,
customer_name: document.getElementById('editCustomerName').value,
client_order_line: document.getElementById('editClientOrderLine').value,
article_code: document.getElementById('editArticleCode').value,
article_description: document.getElementById('editDescription').value,
quantity_requested: parseInt(document.getElementById('editQuantity').value) || 0,
balance: parseFloat(document.getElementById('editBalance').value) || null,
unit_of_measure: document.getElementById('editUnitOfMeasure').value,
delivery_date: document.getElementById('editDeliveryDate').value,
order_date: document.getElementById('editOrderDate').value,
order_status: document.getElementById('editOrderStatus').value,
article_status: document.getElementById('editArticleStatus').value,
priority: document.getElementById('editPriority').value,
product_group: document.getElementById('editProductGroup').value,
production_order: document.getElementById('editProductionOrder').value,
production_status: document.getElementById('editProductionStatus').value,
model: document.getElementById('editModel').value,
closed: document.getElementById('editClosed').value
};
fetch(`/daily_mirror/api/tune/orders_data/${recordId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
})
.then(response => response.json())
.then(result => {
if (result.success) {
// Close modal
const editModal = bootstrap.Modal.getInstance(document.getElementById('editModal'));
editModal.hide();
// Reload data
loadOrdersData(currentPage);
// Show success message
alert('Order updated successfully!');
} else {
alert('Error updating order: ' + result.error);
}
})
.catch(error => {
console.error('Error:', error);
alert('Error updating order: ' + error.message);
});
}
function deleteRecord() {
const recordId = document.getElementById('editRecordId').value;
const orderLine = document.getElementById('editOrderLine').value;
if (!confirm(`⚠️ Are you sure you want to delete order line "${orderLine}"?\n\nThis action cannot be undone.`)) {
return;
}
fetch(`/daily_mirror/api/tune/orders_data/${recordId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.error) {
throw new Error(data.error);
}
// Close modal and reload data
const modal = bootstrap.Modal.getInstance(document.getElementById('editModal'));
modal.hide();
alert('Record deleted successfully!');
loadOrdersData(currentPage);
})
.catch(error => {
console.error('Error deleting record:', error);
alert('Error deleting record: ' + error.message);
});
}
function saveAllChanges() {
alert('Save All Changes functionality will be implemented for bulk operations.');
}
function clearAllOrders() {
if (!confirm('⚠️ WARNING: This will permanently delete ALL customer orders from the database!\n\nAre you absolutely sure you want to continue?')) {
return;
}
// Second confirmation for safety
if (!confirm('This action CANNOT be undone! All customer order data will be lost.\n\nClick OK to proceed with deletion.')) {
return;
}
const clearBtn = document.getElementById('clearAllBtn');
const originalText = clearBtn.innerHTML;
clearBtn.disabled = true;
clearBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Deleting...';
fetch('/daily_mirror/clear_orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('✅ ' + data.message);
// Reload the page to show empty table
loadOrdersData(1);
} else {
alert('❌ Error: ' + (data.message || 'Failed to clear orders'));
}
})
.catch(error => {
console.error('Error:', error);
alert('❌ Error clearing orders: ' + error.message);
})
.finally(() => {
clearBtn.disabled = false;
clearBtn.innerHTML = originalText;
});
}
</script>
{% endblock %}

View File

@@ -0,0 +1,683 @@
{% extends "base.html" %}
{% block title %}Tune Production Data - Daily Mirror{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/daily_mirror_tune.css') }}">
<style>
/* Force modal width - using viewport width for maximum responsiveness */
.modal#editModal .modal-dialog {
max-width: 95vw !important;
width: 95vw !important;
margin: 1.75rem auto !important;
}
.modal#editModal .modal-dialog.modal-xl {
max-width: 95vw !important;
width: 95vw !important;
}
/* Override ALL Bootstrap media queries */
@media (min-width: 576px) {
.modal#editModal .modal-dialog {
max-width: 95vw !important;
width: 95vw !important;
}
}
@media (min-width: 992px) {
.modal#editModal .modal-dialog {
max-width: 95vw !important;
width: 95vw !important;
}
}
@media (min-width: 1200px) {
.modal#editModal .modal-dialog,
.modal#editModal .modal-dialog.modal-xl {
max-width: 95vw !important;
width: 95vw !important;
}
}
/* Ensure pointer events work */
.modal#editModal .modal-dialog {
pointer-events: auto !important;
}
</style>
{% endblock %}
{% block content %}
<div class="container-fluid">
<!-- Page Header -->
<div class="row mb-4">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center">
<div>
<h1 class="h3 mb-0">🏭 Tune Production Data</h1>
<p class="text-muted">Edit and update production orders information</p>
</div>
<div>
<!-- Buttons removed; now present in top header -->
</div>
</div>
</div>
</div>
<!-- Filters Section -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-filter"></i> Filters and Search
</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3 mb-3">
<label for="searchInput" class="form-label">Search</label>
<input type="text" class="form-control" id="searchInput"
placeholder="Search by order, customer, or article...">
</div>
<div class="col-md-3 mb-3">
<label for="statusFilter" class="form-label">Production Status</label>
<select class="form-control" id="statusFilter">
<option value="">All Statuses</option>
<option value="PENDING">Pending</option>
<option value="IN_PROGRESS">In Progress</option>
<option value="FINISHED">Finished</option>
<option value="CANCELLED">Cancelled</option>
</select>
</div>
<div class="col-md-3 mb-3">
<label for="customerFilter" class="form-label">Customer</label>
<select class="form-control" id="customerFilter">
<option value="">All Customers</option>
<!-- Will be populated dynamically -->
</select>
</div>
<div class="col-md-3 mb-3">
<label for="recordsPerPage" class="form-label">Records per page</label>
<select class="form-control" id="recordsPerPage">
<option value="25">25</option>
<option value="50" selected>50</option>
<option value="100">100</option>
</select>
</div>
</div>
<div class="row">
<div class="col-12">
<button class="btn btn-primary" onclick="loadProductionData()">
<i class="fas fa-search"></i> Apply Filters
</button>
<button class="btn btn-secondary" onclick="clearFilters()">
<i class="fas fa-times"></i> Clear
</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Data Table Section -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5 class="card-title mb-0">
<i class="fas fa-table"></i> Production Orders Data
</h5>
<div class="d-flex align-items-center">
<span id="recordsInfo" class="text-muted me-3"></span>
{% if session.get('role') == 'superadmin' %}
<button class="btn btn-danger btn-sm me-2" onclick="clearAllProductionOrders()" id="clearAllBtn">
<i class="fas fa-trash-alt"></i> Clear All Orders
</button>
{% endif %}
<button class="btn btn-success btn-sm" onclick="saveAllChanges()">
<i class="fas fa-save"></i> Save All Changes
</button>
</div>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-striped table-hover" id="productionTable">
<thead class="table-dark">
<tr>
<th>Production Order</th>
<th>Opened for Order</th>
<th>Client Order-Line</th>
<th>Customer</th>
<th>Article Code</th>
<th>Description</th>
<th>Quantity</th>
<th>Delivery Date</th>
<th>Status</th>
<th>Machine</th>
<th>Planning Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="productionTableBody">
<!-- Data will be loaded here -->
</tbody>
</table>
</div>
<!-- Loading indicator -->
<div id="loadingIndicator" class="text-center py-4" style="display: none;">
<i class="fas fa-spinner fa-spin fa-2x"></i>
<p class="mt-2">Loading data...</p>
</div>
<!-- No data message -->
<div id="noDataMessage" class="text-center py-4" style="display: none;">
<i class="fas fa-info-circle fa-2x text-muted"></i>
<p class="mt-2 text-muted">No production orders found</p>
</div>
</div>
<!-- Pagination -->
<div class="card-footer">
<nav aria-label="Production data pagination">
<ul class="pagination pagination-sm justify-content-center mb-0" id="pagination">
<!-- Pagination will be generated here -->
</ul>
</nav>
</div>
</div>
</div>
</div>
</div>
<!-- Edit Modal -->
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true" data-bs-backdrop="true" data-bs-keyboard="true">
<div class="modal-dialog modal-xl modal-dialog-scrollable" style="max-width: 95vw !important; width: 95vw !important;">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="editModalLabel">Edit Production Order</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" id="modalCloseBtn"></button>
</div>
<div class="modal-body">
<form id="editForm">
<input type="hidden" id="editRecordId">
<!-- Production Order (Full Width) -->
<div class="row mb-3">
<div class="col-12">
<label for="editProductionOrder" class="form-label fw-bold">Production Order</label>
<input type="text" class="form-control form-control-lg" id="editProductionOrder" readonly>
</div>
</div>
<hr class="my-3">
<!-- Three Column Layout -->
<div class="row">
<!-- Column 1 -->
<div class="col-md-4">
<div class="mb-3">
<label for="editOpenForOrderLine" class="form-label">Opened for Order-Line</label>
<input type="text" class="form-control" id="editOpenForOrderLine" readonly>
</div>
<div class="mb-3">
<label for="editCustomerCode" class="form-label">Customer Code</label>
<input type="text" class="form-control" id="editCustomerCode">
</div>
<div class="mb-3">
<label for="editArticleCode" class="form-label">Article Code</label>
<input type="text" class="form-control" id="editArticleCode" readonly>
</div>
<div class="mb-3">
<label for="editDeliveryDate" class="form-label">Delivery Date</label>
<input type="date" class="form-control" id="editDeliveryDate">
</div>
</div>
<!-- Column 2 -->
<div class="col-md-4">
<div class="mb-3">
<label for="editClientOrderLine" class="form-label">Client Order-Line</label>
<input type="text" class="form-control" id="editClientOrderLine" readonly>
</div>
<div class="mb-3">
<label for="editCustomerName" class="form-label">Customer Name</label>
<input type="text" class="form-control" id="editCustomerName">
</div>
<div class="mb-3">
<label for="editQuantity" class="form-label">Quantity</label>
<input type="number" class="form-control" id="editQuantity">
</div>
<div class="mb-3">
<label for="editStatus" class="form-label">Production Status</label>
<select class="form-control" id="editStatus">
<option value="PENDING">Pending</option>
<option value="IN_PROGRESS">In Progress</option>
<option value="FINISHED">Finished</option>
<option value="CANCELLED">Cancelled</option>
</select>
</div>
</div>
<!-- Column 3 -->
<div class="col-md-4">
<div class="mb-3">
<label for="editMachine" class="form-label">Machine Code</label>
<input type="text" class="form-control" id="editMachine">
</div>
<div class="mb-3">
<label for="editDescription" class="form-label">Article Description</label>
<textarea class="form-control" id="editDescription" rows="6"></textarea>
</div>
</div>
</div>
</form>
</div>
<div class="modal-footer d-flex justify-content-between">
<button type="button" class="btn btn-danger" onclick="deleteRecord()" style="min-width: 150px;">
<i class="fas fa-trash"></i> Delete Record
</button>
<div>
<button type="button" class="btn btn-secondary me-2" data-bs-dismiss="modal" style="min-width: 100px;">Cancel</button>
<button type="button" class="btn btn-primary" onclick="saveRecord()" style="min-width: 150px;">
<i class="fas fa-save"></i> Save Changes
</button>
</div>
</div>
</div>
</div>
</div>
<script>
let currentPage = 1;
let currentData = [];
let hasChanges = false;
document.addEventListener('DOMContentLoaded', function() {
// Initialize theme
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'dark') {
document.body.classList.add('dark-mode');
}
// Load initial data
loadProductionData();
});
function loadProductionData(page = 1) {
currentPage = page;
// Show loading
document.getElementById('loadingIndicator').style.display = 'block';
document.getElementById('productionTableBody').innerHTML = '';
document.getElementById('noDataMessage').style.display = 'none';
// Get filter values
const search = document.getElementById('searchInput').value;
const status = document.getElementById('statusFilter').value;
const customer = document.getElementById('customerFilter').value;
const perPage = document.getElementById('recordsPerPage').value;
// Build query parameters
const params = new URLSearchParams({
page: page,
per_page: perPage
});
if (search) params.append('search', search);
if (status) params.append('status', status);
if (customer) params.append('customer', customer);
// Fetch data
fetch(`/daily_mirror/api/tune/production_data?${params}`)
.then(response => response.json())
.then(data => {
if (data.error) {
throw new Error(data.error);
}
currentData = data.records;
renderTable(data);
renderPagination(data);
updateRecordsInfo(data);
})
.catch(error => {
console.error('Error loading production data:', error);
alert('Error loading data: ' + error.message);
})
.finally(() => {
document.getElementById('loadingIndicator').style.display = 'none';
});
}
function renderTable(data) {
const tbody = document.getElementById('productionTableBody');
if (data.records.length === 0) {
document.getElementById('noDataMessage').style.display = 'block';
return;
}
tbody.innerHTML = data.records.map((record, index) => `
<tr id="row-${record.id}">
<td><strong>${record.production_order}</strong></td>
<td>${record.open_for_order_line || ''}</td>
<td>${record.client_order_line || ''}</td>
<td>${record.customer_code}<br><small class="text-muted">${record.customer_name || ''}</small></td>
<td>${record.article_code || ''}</td>
<td><small>${record.article_description || ''}</small></td>
<td>${record.quantity_requested || ''}</td>
<td>${record.delivery_date || ''}</td>
<td><span class="badge bg-${getStatusColor(record.production_status)}">${record.production_status || ''}</span></td>
<td>${record.machine_code || ''}</td>
<td>${record.data_planificare || ''}</td>
<td>
<button class="btn btn-primary btn-action btn-sm" onclick="editRecord(${record.id})" title="Edit">
<i class="fas fa-edit"></i>
</button>
</td>
</tr>
`).join('');
}
function getStatusColor(status) {
switch(status) {
case 'PENDING': return 'warning';
case 'IN_PROGRESS': return 'info';
case 'FINISHED': return 'success';
case 'CANCELLED': return 'danger';
default: return 'secondary';
}
}
function renderPagination(data) {
const pagination = document.getElementById('pagination');
if (data.total_pages <= 1) {
pagination.innerHTML = '';
return;
}
let paginationHTML = '';
// Previous button
if (data.page > 1) {
paginationHTML += `<li class="page-item"><a class="page-link" href="#" onclick="loadProductionData(${data.page - 1})">Previous</a></li>`;
}
// Page numbers
for (let i = Math.max(1, data.page - 2); i <= Math.min(data.total_pages, data.page + 2); i++) {
const active = i === data.page ? 'active' : '';
paginationHTML += `<li class="page-item ${active}"><a class="page-link" href="#" onclick="loadProductionData(${i})">${i}</a></li>`;
}
// Next button
if (data.page < data.total_pages) {
paginationHTML += `<li class="page-item"><a class="page-link" href="#" onclick="loadProductionData(${data.page + 1})">Next</a></li>`;
}
pagination.innerHTML = paginationHTML;
}
function updateRecordsInfo(data) {
const info = document.getElementById('recordsInfo');
const start = (data.page - 1) * data.per_page + 1;
const end = Math.min(data.page * data.per_page, data.total);
info.textContent = `Showing ${start}-${end} of ${data.total} records`;
}
function editRecord(recordId) {
const record = currentData.find(r => r.id === recordId);
if (!record) return;
// Populate the edit form
document.getElementById('editRecordId').value = record.id;
document.getElementById('editProductionOrder').value = record.production_order;
document.getElementById('editOpenForOrderLine').value = record.open_for_order_line || '';
document.getElementById('editClientOrderLine').value = record.client_order_line || '';
document.getElementById('editCustomerCode').value = record.customer_code || '';
document.getElementById('editCustomerName').value = record.customer_name || '';
document.getElementById('editArticleCode').value = record.article_code || '';
document.getElementById('editDescription').value = record.article_description || '';
document.getElementById('editQuantity').value = record.quantity_requested || '';
document.getElementById('editDeliveryDate').value = record.delivery_date || '';
document.getElementById('editStatus').value = record.production_status || '';
document.getElementById('editMachine').value = record.machine_code || '';
// Explicitly enable all editable fields
const editableFields = ['editCustomerCode',
'editCustomerName', 'editDescription', 'editQuantity',
'editDeliveryDate', 'editStatus', 'editMachine'];
editableFields.forEach(fieldId => {
const field = document.getElementById(fieldId);
if (field) {
field.disabled = false;
field.removeAttribute('disabled');
field.removeAttribute('readonly');
field.style.backgroundColor = '#ffffff';
field.style.color = '#000000';
field.style.opacity = '1';
field.style.pointerEvents = 'auto';
field.style.cursor = 'text';
field.style.userSelect = 'text';
field.tabIndex = 0;
}
});
// Show the modal with proper configuration
const modalElement = document.getElementById('editModal');
const modalDialog = modalElement.querySelector('.modal-dialog');
// Remove any existing modal instances to prevent conflicts
const existingModal = bootstrap.Modal.getInstance(modalElement);
if (existingModal) {
existingModal.dispose();
}
const modal = new bootstrap.Modal(modalElement, {
backdrop: true,
keyboard: true,
focus: true
});
// Show the modal first
modal.show();
// Force the modal dialog width AFTER modal is shown - using 95% of viewport width
setTimeout(() => {
if (modalDialog) {
console.log('Applying width after modal shown');
modalDialog.style.setProperty('max-width', '95vw', 'important');
modalDialog.style.setProperty('width', '95vw', 'important');
modalDialog.style.setProperty('margin', '1.75rem auto', 'important');
// Also apply to modal content for good measure
const modalContent = modalDialog.querySelector('.modal-content');
if (modalContent) {
modalContent.style.setProperty('width', '100%', 'important');
}
console.log('Modal dialog computed width:', window.getComputedStyle(modalDialog).width);
}
}, 100);
// Ensure form inputs are focusable and interactive after modal is shown
modalElement.addEventListener('shown.bs.modal', function () {
// Re-enable all fields after modal animation completes
editableFields.forEach(fieldId => {
const field = document.getElementById(fieldId);
if (field) {
field.disabled = false;
field.removeAttribute('disabled');
field.style.pointerEvents = 'auto';
}
});
// Focus on the first editable field
const firstField = document.getElementById('editCustomerCode');
if (firstField) {
setTimeout(() => {
firstField.focus();
firstField.select();
}, 100);
}
}, { once: true });
}
function saveRecord() {
const recordId = document.getElementById('editRecordId').value;
const formData = {
open_for_order_line: document.getElementById('editOpenForOrderLine').value,
client_order_line: document.getElementById('editClientOrderLine').value,
customer_code: document.getElementById('editCustomerCode').value,
customer_name: document.getElementById('editCustomerName').value,
article_code: document.getElementById('editArticleCode').value,
article_description: document.getElementById('editDescription').value,
quantity_requested: parseInt(document.getElementById('editQuantity').value) || 0,
delivery_date: document.getElementById('editDeliveryDate').value,
production_status: document.getElementById('editStatus').value,
machine_code: document.getElementById('editMachine').value
};
fetch(`/daily_mirror/api/tune/production_data/${recordId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
})
.then(response => response.json())
.then(data => {
if (data.error) {
throw new Error(data.error);
}
// Close modal and reload data
const modal = bootstrap.Modal.getInstance(document.getElementById('editModal'));
modal.hide();
alert('Record updated successfully!');
loadProductionData(currentPage);
})
.catch(error => {
console.error('Error saving record:', error);
alert('Error saving record: ' + error.message);
});
}
function deleteRecord() {
const recordId = document.getElementById('editRecordId').value;
const productionOrder = document.getElementById('editProductionOrder').value;
if (!confirm(`⚠️ Are you sure you want to delete production order "${productionOrder}"?\n\nThis action cannot be undone.`)) {
return;
}
fetch(`/daily_mirror/api/tune/production_data/${recordId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.error) {
throw new Error(data.error);
}
// Close modal and reload data
const modal = bootstrap.Modal.getInstance(document.getElementById('editModal'));
modal.hide();
alert('Record deleted successfully!');
loadProductionData(currentPage);
})
.catch(error => {
console.error('Error deleting record:', error);
alert('Error deleting record: ' + error.message);
});
}
function clearFilters() {
document.getElementById('searchInput').value = '';
document.getElementById('statusFilter').value = '';
document.getElementById('customerFilter').value = '';
loadProductionData(1);
}
function saveAllChanges() {
alert('Bulk save functionality will be implemented in a future update!');
}
function clearAllProductionOrders() {
if (!confirm('⚠️ WARNING: This will permanently delete ALL production orders from the database!\n\nAre you absolutely sure you want to continue?')) {
return;
}
// Second confirmation for safety
if (!confirm('This action CANNOT be undone! All production order data will be lost.\n\nClick OK to proceed with deletion.')) {
return;
}
const clearBtn = document.getElementById('clearAllBtn');
const originalText = clearBtn.innerHTML;
clearBtn.disabled = true;
clearBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Deleting...';
fetch('/daily_mirror/clear_production_orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('✅ ' + data.message);
// Reload the page to show empty table
loadProductionData(1);
} else {
alert('❌ Error: ' + (data.message || 'Failed to clear production orders'));
}
})
.catch(error => {
console.error('Error:', error);
alert('❌ Error clearing production orders: ' + error.message);
})
.finally(() => {
clearBtn.disabled = false;
clearBtn.innerHTML = originalText;
});
}
// Add event listeners for real-time filtering
document.getElementById('searchInput').addEventListener('input', function() {
clearTimeout(this.searchTimeout);
this.searchTimeout = setTimeout(() => {
loadProductionData(1);
}, 500);
});
document.getElementById('statusFilter').addEventListener('change', function() {
loadProductionData(1);
});
document.getElementById('customerFilter').addEventListener('change', function() {
loadProductionData(1);
});
document.getElementById('recordsPerPage').addEventListener('change', function() {
loadProductionData(1);
});
</script>
{% endblock %}

View File

@@ -50,6 +50,72 @@
Recommended: Use the simplified user management for easier administration
</small>
</div>
{% if session.role in ['superadmin', 'admin'] %}
<div class="card" style="margin-top: 32px;">
<h3>💾 Database Backup Management</h3>
<p><strong>Automated Backup System:</strong> Schedule and manage database backups</p>
<!-- Backup Controls -->
<div style="margin-top: 20px; padding: 15px; background: #f5f5f5; border-radius: 8px;">
<h4 style="margin-top: 0;">Quick Actions</h4>
<button id="backup-now-btn" class="btn" style="background-color: #4caf50; color: white; margin-right: 10px;">
⚡ Backup Now
</button>
<button id="refresh-backups-btn" class="btn" style="background-color: #2196f3; color: white;">
🔄 Refresh List
</button>
</div>
<!-- Schedule Configuration -->
<div style="margin-top: 20px; padding: 15px; background: #f9f9f9; border-radius: 8px;">
<h4 style="margin-top: 0;">Backup Schedule</h4>
<form id="backup-schedule-form" style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px;">
<div>
<label for="schedule-enabled">
<input type="checkbox" id="schedule-enabled" name="enabled"> Enable Scheduled Backups
</label>
</div>
<div>
<label for="schedule-time">Backup Time:</label>
<input type="time" id="schedule-time" name="time" value="02:00">
</div>
<div>
<label for="schedule-frequency">Frequency:</label>
<select id="schedule-frequency" name="frequency">
<option value="daily">Daily</option>
<option value="weekly">Weekly</option>
<option value="monthly">Monthly</option>
</select>
</div>
<div>
<label for="retention-days">Keep backups for (days):</label>
<input type="number" id="retention-days" name="retention_days" value="30" min="1" max="365">
</div>
<div style="grid-column: span 2;">
<button type="submit" class="btn" style="background-color: #ff9800; color: white;">
💾 Save Schedule
</button>
</div>
</form>
</div>
<!-- Backup List -->
<div style="margin-top: 20px;">
<h4>Available Backups</h4>
<div id="backup-list" style="max-height: 400px; overflow-y: auto;">
<p style="text-align: center; color: #999;">Loading backups...</p>
</div>
</div>
<!-- Backup Path Info -->
<div style="margin-top: 15px; padding: 10px; background: #e3f2fd; border-left: 4px solid #2196f3; border-radius: 4px;">
<strong> Backup Location:</strong> <code id="backup-path-display">/srv/quality_app/backups</code>
<br>
<small>Configure backup path in docker-compose.yml (BACKUP_PATH environment variable)</small>
</div>
</div>
{% endif %}
</div>
<!-- Popup for creating/editing a user -->
@@ -136,5 +202,162 @@ Array.from(document.getElementsByClassName('delete-user-btn')).forEach(function(
document.getElementById('close-delete-popup-btn').onclick = function() {
document.getElementById('delete-user-popup').style.display = 'none';
};
// ========================================
// Database Backup Management Functions
// ========================================
// Load backup schedule on page load
function loadBackupSchedule() {
fetch('/api/backup/schedule')
.then(response => response.json())
.then(data => {
if (data.success) {
document.getElementById('schedule-enabled').checked = data.schedule.enabled;
document.getElementById('schedule-time').value = data.schedule.time;
document.getElementById('schedule-frequency').value = data.schedule.frequency;
document.getElementById('retention-days').value = data.schedule.retention_days;
}
})
.catch(error => console.error('Error loading schedule:', error));
}
// Load backup list
function loadBackupList() {
const backupList = document.getElementById('backup-list');
if (!backupList) return;
backupList.innerHTML = '<p style="text-align: center; color: #999;">Loading backups...</p>';
fetch('/api/backup/list')
.then(response => response.json())
.then(data => {
if (data.success && data.backups.length > 0) {
let html = '<table style="width: 100%; border-collapse: collapse;">';
html += '<thead><tr style="background: #f0f0f0;"><th style="padding: 10px; text-align: left;">Filename</th><th>Size</th><th>Created</th><th>Actions</th></tr></thead>';
html += '<tbody>';
data.backups.forEach(backup => {
html += `<tr style="border-bottom: 1px solid #ddd;">
<td style="padding: 10px;">${backup.filename}</td>
<td style="text-align: center;">${backup.size_mb} MB</td>
<td style="text-align: center;">${backup.created}</td>
<td style="text-align: center;">
<button onclick="downloadBackup('${backup.filename}')" class="btn" style="background: #2196f3; color: white; padding: 5px 10px; margin: 2px;">⬇️ Download</button>
<button onclick="deleteBackup('${backup.filename}')" class="btn" style="background: #f44336; color: white; padding: 5px 10px; margin: 2px;">🗑️ Delete</button>
</td>
</tr>`;
});
html += '</tbody></table>';
backupList.innerHTML = html;
} else {
backupList.innerHTML = '<p style="text-align: center; color: #999;">No backups available</p>';
}
})
.catch(error => {
console.error('Error loading backups:', error);
backupList.innerHTML = '<p style="text-align: center; color: #f44336;">Error loading backups</p>';
});
}
// Backup now button
document.getElementById('backup-now-btn')?.addEventListener('click', function() {
const btn = this;
btn.disabled = true;
btn.innerHTML = '⏳ Creating backup...';
fetch('/api/backup/create', {
method: 'POST'
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('✅ ' + data.message + '\nFile: ' + data.filename + '\nSize: ' + data.size);
loadBackupList();
} else {
alert('❌ ' + data.message);
}
btn.disabled = false;
btn.innerHTML = '⚡ Backup Now';
})
.catch(error => {
console.error('Error creating backup:', error);
alert('❌ Failed to create backup');
btn.disabled = false;
btn.innerHTML = '⚡ Backup Now';
});
});
// Refresh backups button
document.getElementById('refresh-backups-btn')?.addEventListener('click', function() {
loadBackupList();
});
// Save schedule form
document.getElementById('backup-schedule-form')?.addEventListener('submit', function(e) {
e.preventDefault();
const formData = {
enabled: document.getElementById('schedule-enabled').checked,
time: document.getElementById('schedule-time').value,
frequency: document.getElementById('schedule-frequency').value,
retention_days: parseInt(document.getElementById('retention-days').value)
};
fetch('/api/backup/schedule', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(formData)
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('✅ ' + data.message);
} else {
alert('❌ ' + data.message);
}
})
.catch(error => {
console.error('Error saving schedule:', error);
alert('❌ Failed to save schedule');
});
});
// Download backup function
function downloadBackup(filename) {
window.location.href = `/api/backup/download/${filename}`;
}
// Delete backup function
function deleteBackup(filename) {
if (confirm(`Are you sure you want to delete backup: ${filename}?`)) {
fetch(`/api/backup/delete/${filename}`, {
method: 'DELETE'
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('✅ ' + data.message);
loadBackupList();
} else {
alert('❌ ' + data.message);
}
})
.catch(error => {
console.error('Error deleting backup:', error);
alert('❌ Failed to delete backup');
});
}
}
// Load backup data on page load
if (document.getElementById('backup-list')) {
loadBackupSchedule();
loadBackupList();
}
</script>
{% endblock %}

View File

@@ -355,6 +355,100 @@
background-color: #2a2a2a !important;
color: #fff !important;
}
/* Modal fixes for accessibility */
.modal {
z-index: 1050 !important;
}
.modal-backdrop {
z-index: 1040 !important;
}
/* Theme-aware modal styling */
body.light-mode .modal-content {
background-color: #fff !important;
color: #000 !important;
}
body.dark-mode .modal-content {
background-color: #2a2a2a !important;
color: #fff !important;
border: 1px solid #444 !important;
}
body.light-mode .modal-header {
background-color: #f8f9fa !important;
border-bottom: 1px solid #dee2e6 !important;
}
body.dark-mode .modal-header {
background-color: #1e1e1e !important;
border-bottom: 1px solid #444 !important;
}
body.light-mode .modal-title {
color: #000 !important;
}
body.dark-mode .modal-title {
color: #fff !important;
}
body.light-mode .modal-body {
background-color: #fff !important;
color: #000 !important;
}
body.dark-mode .modal-body {
background-color: #2a2a2a !important;
color: #fff !important;
}
body.light-mode .modal-footer {
background-color: #f8f9fa !important;
border-top: 1px solid #dee2e6 !important;
}
body.dark-mode .modal-footer {
background-color: #1e1e1e !important;
border-top: 1px solid #444 !important;
}
/* Modal form elements */
body.light-mode .modal .form-control {
background-color: #fff !important;
color: #000 !important;
border-color: #ced4da !important;
}
body.dark-mode .modal .form-control {
background-color: #1a1a1a !important;
color: #fff !important;
border-color: #444 !important;
}
body.light-mode .modal label {
color: #000 !important;
}
body.dark-mode .modal label {
color: #ccc !important;
}
/* Ensure modal is clickable and not greyed out */
.modal.show {
display: block !important;
pointer-events: auto !important;
}
.modal-dialog {
pointer-events: auto !important;
}
.modal-content {
pointer-events: auto !important;
}
</style>
{% endblock %}
@@ -408,6 +502,10 @@
<input type="checkbox" id="module_labels" name="modules" value="labels">
<label for="module_labels">Label Management</label>
</div>
<div class="module-checkbox">
<input type="checkbox" id="module_daily_mirror" name="modules" value="daily_mirror">
<label for="module_daily_mirror">Daily Mirror (BI & Reports)</label>
</div>
</div>
<div id="accessLevelInfo" class="access-level-info" style="display: none;"></div>
</div>
@@ -454,6 +552,10 @@
<input type="checkbox" id="quick_module_labels" name="quick_modules" value="labels">
<label for="quick_module_labels">Label Management</label>
</div>
<div class="module-checkbox">
<input type="checkbox" id="quick_module_daily_mirror" name="quick_modules" value="daily_mirror">
<label for="quick_module_daily_mirror">Daily Mirror (BI & Reports)</label>
</div>
</div>
</div>
@@ -621,6 +723,10 @@
<input type="checkbox" id="edit_module_labels" name="modules" value="labels">
<label for="edit_module_labels">Label Management</label>
</div>
<div class="module-checkbox">
<input type="checkbox" id="edit_module_daily_mirror" name="modules" value="daily_mirror">
<label for="edit_module_daily_mirror">Daily Mirror (BI & Reports)</label>
</div>
</div>
<div id="editAccessLevelInfo" class="access-level-info" style="display: none;"></div>
</div>

View File

@@ -46,8 +46,10 @@ max_requests_jitter = int(os.getenv("GUNICORN_MAX_REQUESTS_JITTER", "100"))
# LOGGING CONFIGURATION
# ============================================================================
# Docker-friendly: logs to stdout/stderr by default, but allow file logging
accesslog = os.getenv("GUNICORN_ACCESS_LOG", "/srv/quality_recticel/logs/access.log")
errorlog = os.getenv("GUNICORN_ERROR_LOG", "/srv/quality_recticel/logs/error.log")
# Automatically detect the correct log path based on current directory
default_log_dir = "/srv/quality_app/logs" if "/srv/quality_app" in os.getcwd() else "/srv/quality_recticel/logs"
accesslog = os.getenv("GUNICORN_ACCESS_LOG", f"{default_log_dir}/access.log")
errorlog = os.getenv("GUNICORN_ERROR_LOG", f"{default_log_dir}/error.log")
# For pure Docker logging (12-factor app), use:
# accesslog = "-" # stdout
@@ -162,4 +164,4 @@ def worker_abort(worker):
def child_exit(server, worker):
"""Called just after a worker has been exited, in the master process."""
server.log.info("👋 Worker %s exited (exit code: %s)", worker.pid, worker.tmp.last_mtime)
server.log.info("👋 Worker %s exited", worker.pid)

View File

@@ -3,3 +3,4 @@ port=3306
database_name=trasabilitate
username=trasabilitate
password=Initial01!
backup_path=/srv/quality_app/backups

View File

@@ -2,7 +2,6 @@ Flask
Flask-SSLify
Werkzeug
gunicorn
flask-sqlalchemy
pyodbc
mariadb
reportlab