starting daily mirror
This commit is contained in:
BIN
files/Articole livrate_returnate.xlsx
Normal file
BIN
files/Articole livrate_returnate.xlsx
Normal file
Binary file not shown.
BIN
files/Comenzi Productie.xlsx
Normal file
BIN
files/Comenzi Productie.xlsx
Normal file
Binary file not shown.
BIN
files/Open .Orders WIZ New.xlsb
Normal file
BIN
files/Open .Orders WIZ New.xlsb
Normal file
Binary file not shown.
BIN
files/Vizual. Artic. Comenzi Deschise.xlsx
Normal file
BIN
files/Vizual. Artic. Comenzi Deschise.xlsx
Normal file
Binary file not shown.
@@ -447,3 +447,20 @@
|
||||
192.168.0.132 - - [22/Oct/2025:21:02:46 +0300] "GET /static/fg_quality.js HTTP/1.1" 200 0 "https://quality.moto-adv.com/fg_quality" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 2489
|
||||
192.168.0.132 - - [22/Oct/2025:21:02:59 +0300] "GET /generate_fg_report?report=6&date=2025-10-16 HTTP/1.1" 200 2422 "https://quality.moto-adv.com/fg_quality" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 35769
|
||||
192.168.0.132 - - [22/Oct/2025:21:03:18 +0300] "GET /quality HTTP/1.1" 200 8860 "https://quality.moto-adv.com/reports" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 10411
|
||||
192.168.0.132 - - [23/Oct/2025:00:18:40 +0300] "GET / HTTP/1.1" 200 1627 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36" 59084
|
||||
192.168.0.132 - - [23/Oct/2025:00:18:41 +0300] "POST / HTTP/1.1" 200 1627 "-" "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.129 Safari/537.36" 5974
|
||||
192.168.0.132 - - [24/Oct/2025:19:45:55 +0300] "GET /.well-known/change-password HTTP/1.1" 404 207 "-" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36" 3174
|
||||
192.168.0.132 - - [24/Oct/2025:19:45:55 +0300] "GET /.well-known/resource-that-should-not-exist-whose-status-code-should-not-be-200 HTTP/1.1" 404 207 "-" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36" 1381
|
||||
192.168.0.132 - - [24/Oct/2025:19:45:55 +0300] "GET / HTTP/1.1" 200 1627 "-" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36" 8205
|
||||
192.168.0.132 - - [24/Oct/2025:19:45:55 +0300] "GET /static/style.css HTTP/1.1" 200 0 "https://quality.moto-adv.com/" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36" 2287
|
||||
192.168.0.132 - - [24/Oct/2025:19:45:55 +0300] "GET /static/css/login.css HTTP/1.1" 200 0 "https://quality.moto-adv.com/" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36" 2370
|
||||
192.168.0.132 - - [24/Oct/2025:19:45:55 +0300] "GET /static/css/base.css HTTP/1.1" 200 0 "https://quality.moto-adv.com/" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36" 2078
|
||||
192.168.0.132 - - [24/Oct/2025:19:45:55 +0300] "GET /static/logo_login.jpg HTTP/1.1" 200 0 "https://quality.moto-adv.com/" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36" 4452
|
||||
192.168.0.132 - - [24/Oct/2025:19:45:55 +0300] "GET /static/script.js HTTP/1.1" 200 0 "https://quality.moto-adv.com/" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36" 1950
|
||||
192.168.0.132 - - [24/Oct/2025:19:45:56 +0300] "GET /favicon.ico HTTP/1.1" 404 207 "https://quality.moto-adv.com/" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36" 1364
|
||||
192.168.0.132 - - [24/Oct/2025:19:45:58 +0300] "GET / HTTP/1.1" 200 1627 "https://www.google.com/" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.52 Mobile Safari/537.36" 45825
|
||||
192.168.0.132 - - [24/Oct/2025:19:45:59 +0300] "GET /static/css/login.css HTTP/1.1" 200 0 "https://quality.moto-adv.com/" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.52 Mobile Safari/537.36" 2111
|
||||
192.168.0.132 - - [24/Oct/2025:19:45:59 +0300] "GET /static/style.css HTTP/1.1" 200 0 "https://quality.moto-adv.com/" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.52 Mobile Safari/537.36" 2136
|
||||
192.168.0.132 - - [24/Oct/2025:19:45:59 +0300] "GET /static/logo_login.jpg HTTP/1.1" 200 0 "https://quality.moto-adv.com/" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.52 Mobile Safari/537.36" 4653
|
||||
192.168.0.132 - - [24/Oct/2025:19:45:59 +0300] "GET /static/script.js HTTP/1.1" 200 0 "https://quality.moto-adv.com/" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.52 Mobile Safari/537.36" 1971
|
||||
192.168.0.132 - - [24/Oct/2025:19:45:59 +0300] "GET /static/css/base.css HTTP/1.1" 200 0 "https://quality.moto-adv.com/" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.7339.52 Mobile Safari/537.36" 3201
|
||||
|
||||
@@ -13,8 +13,10 @@ def create_app():
|
||||
db.init_app(app)
|
||||
|
||||
from app.routes import bp as main_bp, warehouse_bp
|
||||
from app.daily_mirror import daily_mirror_bp
|
||||
app.register_blueprint(main_bp, url_prefix='/')
|
||||
app.register_blueprint(warehouse_bp)
|
||||
app.register_blueprint(daily_mirror_bp)
|
||||
|
||||
# Add 'now' function to Jinja2 globals
|
||||
app.jinja_env.globals['now'] = datetime.now
|
||||
|
||||
@@ -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)
|
||||
1016
py_app/app/daily_mirror.py
Normal file
1016
py_app/app/daily_mirror.py
Normal file
File diff suppressed because it is too large
Load Diff
320
py_app/app/daily_mirror_database_schema.sql
Normal file
320
py_app/app/daily_mirror_database_schema.sql
Normal file
@@ -0,0 +1,320 @@
|
||||
-- Daily Mirror Database Schema
|
||||
-- Quality Recticel Production Tracking System
|
||||
-- Created: October 24, 2025
|
||||
|
||||
-- =============================================
|
||||
-- ORDERS DATA TABLES
|
||||
-- =============================================
|
||||
|
||||
-- Main Orders Table (from Vizual. Artic. Comenzi Deschise)
|
||||
CREATE TABLE IF NOT EXISTS dm_orders (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
order_id VARCHAR(50) UNIQUE NOT NULL,
|
||||
customer_code VARCHAR(50),
|
||||
customer_name VARCHAR(255),
|
||||
client_order VARCHAR(100),
|
||||
article_code VARCHAR(50),
|
||||
article_description TEXT,
|
||||
quantity_requested INT,
|
||||
delivery_date DATE,
|
||||
order_status VARCHAR(50),
|
||||
priority VARCHAR(20),
|
||||
product_group VARCHAR(100),
|
||||
order_date DATE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_order_id (order_id),
|
||||
INDEX idx_customer (customer_code),
|
||||
INDEX idx_article (article_code),
|
||||
INDEX idx_delivery_date (delivery_date),
|
||||
INDEX idx_order_date (order_date),
|
||||
INDEX idx_status (order_status)
|
||||
);
|
||||
|
||||
-- =============================================
|
||||
-- PRODUCTION DATA TABLES
|
||||
-- =============================================
|
||||
|
||||
-- Production Orders Table (from Comenzi Productie)
|
||||
CREATE TABLE IF NOT EXISTS dm_production_orders (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
production_order VARCHAR(50) UNIQUE NOT NULL,
|
||||
order_id VARCHAR(50),
|
||||
customer_code VARCHAR(50),
|
||||
customer_name VARCHAR(255),
|
||||
client_order VARCHAR(100),
|
||||
article_code VARCHAR(50),
|
||||
article_description TEXT,
|
||||
quantity_requested INT,
|
||||
delivery_date DATE,
|
||||
production_status VARCHAR(50),
|
||||
|
||||
-- Production Timeline
|
||||
end_of_quilting DATETIME,
|
||||
end_of_sewing DATETIME,
|
||||
data_deschiderii DATE,
|
||||
data_planificare DATE,
|
||||
|
||||
-- Quality Control Stages
|
||||
t1_status DECIMAL(3,1),
|
||||
t1_registration_date DATETIME,
|
||||
t1_operator_name VARCHAR(100),
|
||||
t2_status DECIMAL(3,1),
|
||||
t2_registration_date DATETIME,
|
||||
t2_operator_name VARCHAR(100),
|
||||
t3_status DECIMAL(3,1),
|
||||
t3_registration_date DATETIME,
|
||||
t3_operator_name VARCHAR(100),
|
||||
|
||||
-- Machine and Production Details
|
||||
machine_code VARCHAR(50),
|
||||
machine_type VARCHAR(50),
|
||||
machine_number VARCHAR(20),
|
||||
classification VARCHAR(100),
|
||||
design_number INT,
|
||||
needle_position INT,
|
||||
total_norm_time DECIMAL(8,2),
|
||||
model_lb2 VARCHAR(255),
|
||||
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_production_order (production_order),
|
||||
INDEX idx_order_id (order_id),
|
||||
INDEX idx_customer (customer_code),
|
||||
INDEX idx_article (article_code),
|
||||
INDEX idx_delivery_date (delivery_date),
|
||||
INDEX idx_status (production_status),
|
||||
INDEX idx_machine (machine_code),
|
||||
INDEX idx_quilting_date (end_of_quilting),
|
||||
INDEX idx_sewing_date (end_of_sewing)
|
||||
);
|
||||
|
||||
-- =============================================
|
||||
-- DELIVERY DATA TABLES
|
||||
-- =============================================
|
||||
|
||||
-- Delivery/Shipment Table (from Articole livrate)
|
||||
CREATE TABLE IF NOT EXISTS dm_deliveries (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
shipment_id VARCHAR(50) UNIQUE NOT NULL,
|
||||
order_id VARCHAR(50),
|
||||
production_order VARCHAR(50),
|
||||
customer_code VARCHAR(50),
|
||||
customer_name VARCHAR(255),
|
||||
article_code VARCHAR(50),
|
||||
article_description TEXT,
|
||||
quantity_delivered INT,
|
||||
quantity_returned INT DEFAULT 0,
|
||||
|
||||
-- Delivery Timeline
|
||||
shipment_date DATE,
|
||||
delivery_date DATE,
|
||||
return_date DATE,
|
||||
|
||||
-- Delivery Status
|
||||
delivery_status VARCHAR(50), -- 'shipped', 'delivered', 'returned', 'partial'
|
||||
shipping_method VARCHAR(100),
|
||||
tracking_number VARCHAR(100),
|
||||
shipping_address TEXT,
|
||||
delivery_notes TEXT,
|
||||
|
||||
-- Financial
|
||||
unit_price DECIMAL(10,2),
|
||||
total_value DECIMAL(12,2),
|
||||
currency VARCHAR(3) DEFAULT 'RON',
|
||||
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_shipment_id (shipment_id),
|
||||
INDEX idx_order_id (order_id),
|
||||
INDEX idx_production_order (production_order),
|
||||
INDEX idx_customer (customer_code),
|
||||
INDEX idx_article (article_code),
|
||||
INDEX idx_shipment_date (shipment_date),
|
||||
INDEX idx_delivery_date (delivery_date),
|
||||
INDEX idx_status (delivery_status)
|
||||
);
|
||||
|
||||
-- =============================================
|
||||
-- DAILY MIRROR AGGREGATION TABLES
|
||||
-- =============================================
|
||||
|
||||
-- Daily Summary Table (for fast reporting)
|
||||
CREATE TABLE IF NOT EXISTS dm_daily_summary (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
report_date DATE UNIQUE NOT NULL,
|
||||
|
||||
-- Orders Metrics
|
||||
orders_received INT DEFAULT 0,
|
||||
orders_quantity INT DEFAULT 0,
|
||||
orders_value DECIMAL(15,2) DEFAULT 0,
|
||||
unique_customers INT DEFAULT 0,
|
||||
|
||||
-- Production Metrics
|
||||
production_launched INT DEFAULT 0,
|
||||
production_finished INT DEFAULT 0,
|
||||
production_in_progress INT DEFAULT 0,
|
||||
quilting_completed INT DEFAULT 0,
|
||||
sewing_completed INT DEFAULT 0,
|
||||
|
||||
-- Delivery Metrics
|
||||
orders_shipped INT DEFAULT 0,
|
||||
orders_delivered INT DEFAULT 0,
|
||||
orders_returned INT DEFAULT 0,
|
||||
delivery_value DECIMAL(15,2) DEFAULT 0,
|
||||
|
||||
-- Efficiency Metrics
|
||||
on_time_deliveries INT DEFAULT 0,
|
||||
late_deliveries INT DEFAULT 0,
|
||||
active_operators INT DEFAULT 0,
|
||||
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_report_date (report_date)
|
||||
);
|
||||
|
||||
|
||||
-- =============================================
|
||||
-- CONFIGURATION AND LOOKUP TABLES
|
||||
-- =============================================
|
||||
|
||||
-- Customer Master
|
||||
CREATE TABLE IF NOT EXISTS dm_customers (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
customer_code VARCHAR(50) UNIQUE NOT NULL,
|
||||
customer_name VARCHAR(255) NOT NULL,
|
||||
customer_group VARCHAR(100),
|
||||
country VARCHAR(50),
|
||||
currency VARCHAR(3) DEFAULT 'RON',
|
||||
payment_terms VARCHAR(100),
|
||||
credit_limit DECIMAL(15,2),
|
||||
active BOOLEAN DEFAULT TRUE,
|
||||
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_customer_code (customer_code),
|
||||
INDEX idx_customer_name (customer_name),
|
||||
INDEX idx_customer_group (customer_group)
|
||||
);
|
||||
|
||||
-- Article Master
|
||||
CREATE TABLE IF NOT EXISTS dm_articles (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
article_code VARCHAR(50) UNIQUE NOT NULL,
|
||||
article_description TEXT NOT NULL,
|
||||
product_group VARCHAR(100),
|
||||
classification VARCHAR(100),
|
||||
unit_of_measure VARCHAR(20),
|
||||
standard_price DECIMAL(10,2),
|
||||
standard_time DECIMAL(8,2),
|
||||
active BOOLEAN DEFAULT TRUE,
|
||||
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_article_code (article_code),
|
||||
INDEX idx_product_group (product_group),
|
||||
INDEX idx_classification (classification)
|
||||
);
|
||||
|
||||
-- Machine Master
|
||||
CREATE TABLE IF NOT EXISTS dm_machines (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
machine_code VARCHAR(50) UNIQUE NOT NULL,
|
||||
machine_name VARCHAR(255),
|
||||
machine_type VARCHAR(50),
|
||||
machine_number VARCHAR(20),
|
||||
department VARCHAR(100),
|
||||
capacity_per_hour DECIMAL(8,2),
|
||||
active BOOLEAN DEFAULT TRUE,
|
||||
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
|
||||
INDEX idx_machine_code (machine_code),
|
||||
INDEX idx_machine_type (machine_type),
|
||||
INDEX idx_department (department)
|
||||
);
|
||||
|
||||
-- =============================================
|
||||
-- DATA IMPORT TRACKING
|
||||
-- =============================================
|
||||
|
||||
-- Track file uploads and data imports
|
||||
CREATE TABLE IF NOT EXISTS dm_import_log (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
file_name VARCHAR(255) NOT NULL,
|
||||
file_type VARCHAR(50) NOT NULL, -- 'orders', 'production', 'delivery'
|
||||
upload_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
uploaded_by VARCHAR(100),
|
||||
records_processed INT DEFAULT 0,
|
||||
records_successful INT DEFAULT 0,
|
||||
records_failed INT DEFAULT 0,
|
||||
status VARCHAR(50) DEFAULT 'processing', -- 'processing', 'completed', 'failed'
|
||||
error_message TEXT,
|
||||
processing_time DECIMAL(8,2), -- seconds
|
||||
|
||||
INDEX idx_upload_date (upload_date),
|
||||
INDEX idx_file_type (file_type),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_uploaded_by (uploaded_by)
|
||||
);
|
||||
|
||||
-- =============================================
|
||||
-- VIEWS FOR DAILY MIRROR REPORTING
|
||||
-- =============================================
|
||||
|
||||
-- View: Current Production Status
|
||||
CREATE OR REPLACE VIEW v_daily_production_status AS
|
||||
SELECT
|
||||
DATE(p.data_planificare) as production_date,
|
||||
COUNT(*) as total_orders,
|
||||
SUM(p.quantity_requested) as total_quantity,
|
||||
SUM(CASE WHEN p.production_status = 'Inchis' THEN 1 ELSE 0 END) as completed_orders,
|
||||
SUM(CASE WHEN p.production_status != 'Inchis' THEN 1 ELSE 0 END) as pending_orders,
|
||||
SUM(CASE WHEN p.end_of_quilting IS NOT NULL THEN 1 ELSE 0 END) as quilting_done,
|
||||
SUM(CASE WHEN p.end_of_sewing IS NOT NULL THEN 1 ELSE 0 END) as sewing_done,
|
||||
COUNT(DISTINCT p.customer_code) as unique_customers,
|
||||
COUNT(DISTINCT p.machine_code) as machines_used
|
||||
FROM dm_production_orders p
|
||||
WHERE p.data_planificare >= CURDATE() - INTERVAL 30 DAY
|
||||
GROUP BY DATE(p.data_planificare)
|
||||
ORDER BY production_date DESC;
|
||||
|
||||
-- View: Quality Performance Summary
|
||||
CREATE OR REPLACE VIEW v_daily_quality_summary AS
|
||||
SELECT
|
||||
DATE(p.t1_registration_date) as scan_date,
|
||||
COUNT(*) as total_t1_scans,
|
||||
SUM(CASE WHEN p.t1_status = 0 THEN 1 ELSE 0 END) as t1_approved,
|
||||
ROUND(SUM(CASE WHEN p.t1_status = 0 THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) as t1_approval_rate,
|
||||
COUNT(CASE WHEN p.t2_registration_date IS NOT NULL THEN 1 END) as total_t2_scans,
|
||||
SUM(CASE WHEN p.t2_status = 0 THEN 1 ELSE 0 END) as t2_approved,
|
||||
ROUND(SUM(CASE WHEN p.t2_status = 0 THEN 1 ELSE 0 END) / COUNT(CASE WHEN p.t2_registration_date IS NOT NULL THEN 1 END) * 100, 2) as t2_approval_rate,
|
||||
COUNT(DISTINCT p.t1_operator_name) as active_operators
|
||||
FROM dm_production_orders p
|
||||
WHERE p.t1_registration_date >= CURDATE() - INTERVAL 30 DAY
|
||||
GROUP BY DATE(p.t1_registration_date)
|
||||
ORDER BY scan_date DESC;
|
||||
|
||||
-- View: Delivery Performance
|
||||
CREATE OR REPLACE VIEW v_daily_delivery_summary AS
|
||||
SELECT
|
||||
d.delivery_date,
|
||||
COUNT(*) as total_deliveries,
|
||||
SUM(d.quantity_delivered) as total_quantity_delivered,
|
||||
SUM(d.total_value) as total_delivery_value,
|
||||
SUM(CASE WHEN d.delivery_date <= o.delivery_date THEN 1 ELSE 0 END) as on_time_deliveries,
|
||||
SUM(CASE WHEN d.delivery_date > o.delivery_date THEN 1 ELSE 0 END) as late_deliveries,
|
||||
COUNT(DISTINCT d.customer_code) as unique_customers
|
||||
FROM dm_deliveries d
|
||||
LEFT JOIN dm_orders o ON d.order_id = o.order_id
|
||||
WHERE d.delivery_date >= CURDATE() - INTERVAL 30 DAY
|
||||
AND d.delivery_status = 'delivered'
|
||||
GROUP BY d.delivery_date
|
||||
ORDER BY d.delivery_date DESC;
|
||||
744
py_app/app/daily_mirror_db_setup.py
Normal file
744
py_app/app/daily_mirror_db_setup.py
Normal file
@@ -0,0 +1,744 @@
|
||||
"""
|
||||
Daily Mirror Database Setup and Management
|
||||
Quality Recticel Application
|
||||
|
||||
This script creates the database schema and provides utilities for
|
||||
data import and Daily Mirror reporting functionality.
|
||||
"""
|
||||
|
||||
import mariadb
|
||||
import pandas as pd
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
import logging
|
||||
|
||||
# Setup logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class DailyMirrorDatabase:
|
||||
def __init__(self, host='localhost', user='trasabilitate', password='Initial01!', database='trasabilitate'):
|
||||
self.host = host
|
||||
self.user = user
|
||||
self.password = password
|
||||
self.database = database
|
||||
self.connection = None
|
||||
|
||||
def connect(self):
|
||||
"""Establish database connection"""
|
||||
try:
|
||||
self.connection = mariadb.connect(
|
||||
host=self.host,
|
||||
user=self.user,
|
||||
password=self.password,
|
||||
database=self.database
|
||||
)
|
||||
logger.info("Database connection established")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Database connection failed: {e}")
|
||||
return False
|
||||
|
||||
def disconnect(self):
|
||||
"""Close database connection"""
|
||||
if self.connection:
|
||||
self.connection.close()
|
||||
logger.info("Database connection closed")
|
||||
|
||||
def create_database_schema(self):
|
||||
"""Create the Daily Mirror database schema"""
|
||||
try:
|
||||
cursor = self.connection.cursor()
|
||||
|
||||
# Read and execute the schema file
|
||||
schema_file = os.path.join(os.path.dirname(__file__), 'daily_mirror_database_schema.sql')
|
||||
|
||||
if not os.path.exists(schema_file):
|
||||
logger.error(f"Schema file not found: {schema_file}")
|
||||
return False
|
||||
|
||||
with open(schema_file, 'r') as file:
|
||||
schema_sql = file.read()
|
||||
|
||||
# Split by statements and execute each one
|
||||
statements = []
|
||||
current_statement = ""
|
||||
|
||||
for line in schema_sql.split('\n'):
|
||||
line = line.strip()
|
||||
if line and not line.startswith('--'):
|
||||
current_statement += line + " "
|
||||
if line.endswith(';'):
|
||||
statements.append(current_statement.strip())
|
||||
current_statement = ""
|
||||
|
||||
# Add any remaining statement
|
||||
if current_statement.strip():
|
||||
statements.append(current_statement.strip())
|
||||
|
||||
for statement in statements:
|
||||
if statement and any(statement.upper().startswith(cmd) for cmd in ['CREATE', 'ALTER', 'DROP', 'INSERT']):
|
||||
try:
|
||||
cursor.execute(statement)
|
||||
logger.info(f"Executed: {statement[:80]}...")
|
||||
except Exception as e:
|
||||
if "already exists" not in str(e).lower():
|
||||
logger.warning(f"Error executing statement: {e}")
|
||||
|
||||
self.connection.commit()
|
||||
logger.info("Database schema created successfully")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error creating database schema: {e}")
|
||||
return False
|
||||
|
||||
def import_production_data(self, file_path):
|
||||
"""Import production data from Excel file (Comenzi Productie format)"""
|
||||
try:
|
||||
# The correct data is in the first sheet (DataSheet)
|
||||
df = None
|
||||
sheet_used = None
|
||||
|
||||
# Get available sheets
|
||||
excel_file = pd.ExcelFile(file_path)
|
||||
logger.info(f"Available sheets: {excel_file.sheet_names}")
|
||||
|
||||
# Try DataSheet first (where the actual production data is), then fallback options
|
||||
sheet_attempts = [
|
||||
('DataSheet', 'openpyxl'),
|
||||
('DataSheet', 'xlrd'),
|
||||
(0, 'openpyxl'),
|
||||
(0, 'xlrd'),
|
||||
('Sheet1', 'openpyxl'), # fallback to Sheet1 if DataSheet fails
|
||||
(1, 'openpyxl')
|
||||
]
|
||||
|
||||
for sheet_name, engine in sheet_attempts:
|
||||
try:
|
||||
logger.info(f"Trying to read sheet '{sheet_name}' with engine '{engine}'")
|
||||
df = pd.read_excel(file_path, sheet_name=sheet_name, engine=engine, header=0)
|
||||
sheet_used = f"{sheet_name} (engine: {engine})"
|
||||
logger.info(f"Successfully read from sheet: {sheet_used}")
|
||||
break
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to read sheet {sheet_name} with {engine}: {e}")
|
||||
continue
|
||||
|
||||
# If all engines fail on DataSheet, try a different approach
|
||||
if df is None:
|
||||
try:
|
||||
logger.info("Trying alternative method: reading without specifying engine")
|
||||
df = pd.read_excel(file_path, sheet_name='DataSheet')
|
||||
sheet_used = "DataSheet (default engine)"
|
||||
logger.info("Successfully read with default engine")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed with default engine: {e}")
|
||||
raise Exception("Could not read the DataSheet from the Excel file. The file may be corrupted.")
|
||||
|
||||
logger.info(f"Loaded production data from {sheet_used}: {len(df)} rows, {len(df.columns)} columns")
|
||||
logger.info(f"Available columns: {list(df.columns)}")
|
||||
|
||||
cursor = self.connection.cursor()
|
||||
success_count = 0
|
||||
created_count = 0
|
||||
updated_count = 0
|
||||
error_count = 0
|
||||
|
||||
# Prepare insert statement
|
||||
insert_sql = """
|
||||
INSERT INTO dm_production_orders (
|
||||
production_order, customer_code, client_order, article_code,
|
||||
article_description, quantity_requested, delivery_date, production_status,
|
||||
end_of_quilting, end_of_sewing, t1_status, t1_registration_date, t1_operator_name,
|
||||
t2_status, t2_registration_date, t2_operator_name, t3_status, t3_registration_date,
|
||||
t3_operator_name, machine_code, machine_type, classification, total_norm_time,
|
||||
data_deschiderii, model_lb2, data_planificare, machine_number, design_number, needle_position
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
customer_code = VALUES(customer_code),
|
||||
client_order = VALUES(client_order),
|
||||
article_code = VALUES(article_code),
|
||||
article_description = VALUES(article_description),
|
||||
quantity_requested = VALUES(quantity_requested),
|
||||
delivery_date = VALUES(delivery_date),
|
||||
production_status = VALUES(production_status),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
"""
|
||||
|
||||
for index, row in df.iterrows():
|
||||
try:
|
||||
# Prepare data tuple
|
||||
data = (
|
||||
row.get('Comanda Productie', ''),
|
||||
row.get('Customer', ''),
|
||||
row.get('Comanda client', ''),
|
||||
row.get('Cod Articol', ''),
|
||||
row.get('Descriere', ''),
|
||||
row.get('Cantitate ceruta', 0),
|
||||
self._parse_date(row.get('Delivery date')),
|
||||
row.get('Status', ''),
|
||||
self._parse_datetime(row.get('End of Quilting')),
|
||||
self._parse_datetime(row.get('End of sewing')),
|
||||
row.get('T1', 0),
|
||||
self._parse_datetime(row.get('Data inregistrare T1')),
|
||||
row.get('Numele Complet T1', ''),
|
||||
row.get('T2', 0),
|
||||
self._parse_datetime(row.get('Data inregistrare T2')),
|
||||
row.get('Numele Complet T2', ''),
|
||||
row.get('T3', 0),
|
||||
self._parse_datetime(row.get('Data inregistrare T3')),
|
||||
row.get('Numele Complet T3', ''),
|
||||
row.get('Masina Cusut ', ''),
|
||||
row.get('Tip Masina', ''),
|
||||
row.get('Clasificare', ''),
|
||||
row.get('Timp normat total', 0),
|
||||
self._parse_date(row.get('Data Deschiderii')),
|
||||
row.get('Model Lb2', ''),
|
||||
self._parse_date(row.get('Data Planific.')),
|
||||
row.get('Numar masina', ''),
|
||||
row.get('Design nr', 0),
|
||||
row.get('Needle position', 0)
|
||||
)
|
||||
|
||||
cursor.execute(insert_sql, data)
|
||||
|
||||
# Check if row was inserted (created) or updated
|
||||
# In MySQL with ON DUPLICATE KEY UPDATE:
|
||||
# - rowcount = 1 means INSERT (new row created)
|
||||
# - rowcount = 2 means UPDATE (existing row updated)
|
||||
# - rowcount = 0 means no change
|
||||
if cursor.rowcount == 1:
|
||||
created_count += 1
|
||||
elif cursor.rowcount == 2:
|
||||
updated_count += 1
|
||||
|
||||
success_count += 1
|
||||
|
||||
except Exception as row_error:
|
||||
logger.warning(f"Error processing row {index}: {row_error}")
|
||||
error_count += 1
|
||||
continue
|
||||
|
||||
self.connection.commit()
|
||||
logger.info(f"Production data import completed: {success_count} successful, {error_count} failed")
|
||||
|
||||
return {
|
||||
'success_count': success_count,
|
||||
'created_count': created_count,
|
||||
'updated_count': updated_count,
|
||||
'error_count': error_count,
|
||||
'total_rows': len(df)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error importing production data: {e}")
|
||||
return None
|
||||
|
||||
def import_orders_data(self, file_path):
|
||||
"""Import orders data from Excel file with enhanced error handling"""
|
||||
try:
|
||||
# Ensure we have a database connection
|
||||
if not self.connection:
|
||||
self.connect()
|
||||
if not self.connection:
|
||||
return {
|
||||
'success_count': 0,
|
||||
'error_count': 1,
|
||||
'total_rows': 0,
|
||||
'error_message': 'Could not establish database connection.'
|
||||
}
|
||||
|
||||
logger.info(f"Attempting to import orders data from: {file_path}")
|
||||
|
||||
# Check if file exists
|
||||
if not os.path.exists(file_path):
|
||||
logger.error(f"Orders file not found: {file_path}")
|
||||
return {
|
||||
'success_count': 0,
|
||||
'error_count': 1,
|
||||
'total_rows': 0,
|
||||
'error_message': f'Orders file not found: {file_path}'
|
||||
}
|
||||
|
||||
# Try to get sheet names first
|
||||
try:
|
||||
excel_file = pd.ExcelFile(file_path)
|
||||
sheet_names = excel_file.sheet_names
|
||||
logger.info(f"Available sheets in orders file: {sheet_names}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not get sheet names: {e}")
|
||||
sheet_names = ['DataSheet', 'Sheet1']
|
||||
|
||||
# Try multiple approaches to read the Excel file
|
||||
df = None
|
||||
sheet_used = None
|
||||
approaches = [
|
||||
('openpyxl', 0, 'read_only'),
|
||||
('openpyxl', 0, 'normal'),
|
||||
('openpyxl', 1, 'normal'),
|
||||
('xlrd', 0, 'normal') if file_path.endswith('.xls') else None,
|
||||
('default', 0, 'normal')
|
||||
]
|
||||
|
||||
for approach in approaches:
|
||||
if approach is None:
|
||||
continue
|
||||
|
||||
engine, sheet_name, mode = approach
|
||||
try:
|
||||
logger.info(f"Trying to read orders with engine: {engine}, sheet: {sheet_name}, mode: {mode}")
|
||||
|
||||
if engine == 'default':
|
||||
df = pd.read_excel(file_path, sheet_name=sheet_name, header=0)
|
||||
elif mode == 'read_only':
|
||||
df = pd.read_excel(file_path, sheet_name=sheet_name, engine=engine, header=0)
|
||||
else:
|
||||
df = pd.read_excel(file_path, sheet_name=sheet_name, engine=engine, header=0)
|
||||
|
||||
sheet_used = f"{engine} (sheet: {sheet_name}, mode: {mode})"
|
||||
logger.info(f"Successfully read orders data with: {sheet_used}")
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed with {engine}, sheet {sheet_name}, mode {mode}: {e}")
|
||||
continue
|
||||
|
||||
if df is None:
|
||||
logger.error("Could not read the orders file with any method")
|
||||
return {
|
||||
'success_count': 0,
|
||||
'error_count': 1,
|
||||
'total_rows': 0,
|
||||
'error_message': 'Could not read the orders Excel file. The file may have formatting issues or be corrupted.'
|
||||
}
|
||||
|
||||
logger.info(f"Loaded orders data from {sheet_used}: {len(df)} rows, {len(df.columns)} columns")
|
||||
logger.info(f"Available columns: {list(df.columns)[:10]}...")
|
||||
|
||||
cursor = self.connection.cursor()
|
||||
success_count = 0
|
||||
created_count = 0
|
||||
updated_count = 0
|
||||
error_count = 0
|
||||
|
||||
# Prepare insert statement for orders
|
||||
insert_sql = """
|
||||
INSERT INTO dm_orders (
|
||||
order_id, customer_code, customer_name, client_order,
|
||||
article_code, article_description, quantity_requested, delivery_date,
|
||||
order_status, product_group, order_date
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
customer_code = VALUES(customer_code),
|
||||
customer_name = VALUES(customer_name),
|
||||
client_order = VALUES(client_order),
|
||||
article_code = VALUES(article_code),
|
||||
article_description = VALUES(article_description),
|
||||
quantity_requested = VALUES(quantity_requested),
|
||||
delivery_date = VALUES(delivery_date),
|
||||
order_status = VALUES(order_status),
|
||||
product_group = VALUES(product_group),
|
||||
order_date = VALUES(order_date),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
"""
|
||||
|
||||
# Process each row with the actual column mapping and better null handling
|
||||
for index, row in df.iterrows():
|
||||
try:
|
||||
# Helper function to safely get values and handle NaN
|
||||
def safe_get(row, column, default=''):
|
||||
value = row.get(column, default)
|
||||
if pd.isna(value) or value == 'nan':
|
||||
return default
|
||||
return str(value).strip() if isinstance(value, str) else value
|
||||
|
||||
def safe_get_int(row, column, default=0):
|
||||
value = row.get(column, default)
|
||||
if pd.isna(value) or value == 'nan':
|
||||
return default
|
||||
try:
|
||||
return int(float(value)) if value != '' else default
|
||||
except (ValueError, TypeError):
|
||||
return default
|
||||
|
||||
# Map columns based on the actual Vizual. Artic. Comenzi Deschise format
|
||||
data = (
|
||||
safe_get(row, 'Comanda', f'ORD_{index:06d}'), # Order ID
|
||||
safe_get(row, 'Cod. Client'), # Customer Code
|
||||
safe_get(row, 'Customer Name'), # Customer Name
|
||||
safe_get(row, 'Com. Achiz. Client'), # Client Order
|
||||
safe_get(row, 'Cod Articol'), # Article Code
|
||||
safe_get(row, 'Part Description', safe_get(row, 'Descr. Articol')), # Article Description
|
||||
safe_get_int(row, 'Cantitate'), # Quantity
|
||||
self._parse_date(row.get('Data livrare')), # Delivery Date
|
||||
safe_get(row, 'Statut Comanda', 'PENDING'), # Order Status
|
||||
safe_get(row, 'Model'), # Product Group
|
||||
self._parse_date(row.get('Data Comenzii')) # Order Date
|
||||
)
|
||||
|
||||
cursor.execute(insert_sql, data)
|
||||
|
||||
# Track created vs updated
|
||||
if cursor.rowcount == 1:
|
||||
created_count += 1
|
||||
elif cursor.rowcount == 2:
|
||||
updated_count += 1
|
||||
|
||||
success_count += 1
|
||||
|
||||
except Exception as row_error:
|
||||
logger.warning(f"Error processing row {index}: {row_error}")
|
||||
error_count += 1
|
||||
continue
|
||||
|
||||
self.connection.commit()
|
||||
logger.info(f"Orders import completed: {success_count} successful, {error_count} errors")
|
||||
|
||||
return {
|
||||
'success_count': success_count,
|
||||
'created_count': created_count,
|
||||
'updated_count': updated_count,
|
||||
'error_count': error_count,
|
||||
'total_rows': len(df),
|
||||
'error_message': None if error_count == 0 else f'{error_count} rows failed to import'
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error importing orders data: {e}")
|
||||
return {
|
||||
'success_count': 0,
|
||||
'error_count': 1,
|
||||
'total_rows': 0,
|
||||
'error_message': str(e)
|
||||
}
|
||||
|
||||
def import_delivery_data(self, file_path):
|
||||
"""Import delivery data from Excel file with enhanced error handling"""
|
||||
try:
|
||||
# Ensure we have a database connection
|
||||
if not self.connection:
|
||||
self.connect()
|
||||
if not self.connection:
|
||||
return {
|
||||
'success_count': 0,
|
||||
'error_count': 1,
|
||||
'total_rows': 0,
|
||||
'error_message': 'Could not establish database connection.'
|
||||
}
|
||||
|
||||
logger.info(f"Attempting to import delivery data from: {file_path}")
|
||||
|
||||
# Check if file exists
|
||||
if not os.path.exists(file_path):
|
||||
logger.error(f"Delivery file not found: {file_path}")
|
||||
return {
|
||||
'success_count': 0,
|
||||
'error_count': 1,
|
||||
'total_rows': 0,
|
||||
'error_message': f'Delivery file not found: {file_path}'
|
||||
}
|
||||
|
||||
# Try to get sheet names first
|
||||
try:
|
||||
excel_file = pd.ExcelFile(file_path)
|
||||
sheet_names = excel_file.sheet_names
|
||||
logger.info(f"Available sheets in delivery file: {sheet_names}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not get sheet names: {e}")
|
||||
sheet_names = ['DataSheet', 'Sheet1']
|
||||
|
||||
# Try multiple approaches to read the Excel file
|
||||
df = None
|
||||
sheet_used = None
|
||||
approaches = [
|
||||
('openpyxl', 0, 'read_only'),
|
||||
('openpyxl', 0, 'normal'),
|
||||
('openpyxl', 1, 'normal'),
|
||||
('xlrd', 0, 'normal') if file_path.endswith('.xls') else None,
|
||||
('default', 0, 'normal')
|
||||
]
|
||||
|
||||
for approach in approaches:
|
||||
if approach is None:
|
||||
continue
|
||||
|
||||
engine, sheet_name, mode = approach
|
||||
try:
|
||||
logger.info(f"Trying to read delivery data with engine: {engine}, sheet: {sheet_name}, mode: {mode}")
|
||||
|
||||
if engine == 'default':
|
||||
df = pd.read_excel(file_path, sheet_name=sheet_name, header=0)
|
||||
elif mode == 'read_only':
|
||||
df = pd.read_excel(file_path, sheet_name=sheet_name, engine=engine, header=0)
|
||||
else:
|
||||
df = pd.read_excel(file_path, sheet_name=sheet_name, engine=engine, header=0)
|
||||
|
||||
sheet_used = f"{engine} (sheet: {sheet_name}, mode: {mode})"
|
||||
logger.info(f"Successfully read delivery data with: {sheet_used}")
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed with {engine}, sheet {sheet_name}, mode {mode}: {e}")
|
||||
continue
|
||||
|
||||
if df is None:
|
||||
logger.error("Could not read the delivery file with any method")
|
||||
return {
|
||||
'success_count': 0,
|
||||
'error_count': 1,
|
||||
'total_rows': 0,
|
||||
'error_message': 'Could not read the delivery Excel file. The file may have formatting issues or be corrupted.'
|
||||
}
|
||||
|
||||
logger.info(f"Loaded delivery data from {sheet_used}: {len(df)} rows, {len(df.columns)} columns")
|
||||
logger.info(f"Available columns: {list(df.columns)[:10]}...")
|
||||
|
||||
cursor = self.connection.cursor()
|
||||
success_count = 0
|
||||
created_count = 0
|
||||
updated_count = 0
|
||||
error_count = 0
|
||||
|
||||
# Prepare insert statement for deliveries
|
||||
insert_sql = """
|
||||
INSERT INTO dm_deliveries (
|
||||
shipment_id, order_id, customer_code, customer_name,
|
||||
article_code, article_description, quantity_delivered,
|
||||
shipment_date, delivery_date, delivery_status, total_value
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
customer_code = VALUES(customer_code),
|
||||
customer_name = VALUES(customer_name),
|
||||
article_code = VALUES(article_code),
|
||||
article_description = VALUES(article_description),
|
||||
quantity_delivered = VALUES(quantity_delivered),
|
||||
shipment_date = VALUES(shipment_date),
|
||||
delivery_date = VALUES(delivery_date),
|
||||
delivery_status = VALUES(delivery_status),
|
||||
total_value = VALUES(total_value),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
"""
|
||||
|
||||
# Process each row with the actual column mapping and better null handling
|
||||
for index, row in df.iterrows():
|
||||
try:
|
||||
# Helper function to safely get values and handle NaN
|
||||
def safe_get(row, column, default=''):
|
||||
value = row.get(column, default)
|
||||
if pd.isna(value) or value == 'nan':
|
||||
return default
|
||||
return str(value).strip() if isinstance(value, str) else value
|
||||
|
||||
def safe_get_float(row, column, default=0.0):
|
||||
value = row.get(column, default)
|
||||
if pd.isna(value) or value == 'nan':
|
||||
return default
|
||||
try:
|
||||
return float(value) if value != '' else default
|
||||
except (ValueError, TypeError):
|
||||
return default
|
||||
|
||||
def safe_get_int(row, column, default=0):
|
||||
value = row.get(column, default)
|
||||
if pd.isna(value) or value == 'nan':
|
||||
return default
|
||||
try:
|
||||
return int(float(value)) if value != '' else default
|
||||
except (ValueError, TypeError):
|
||||
return default
|
||||
|
||||
# Map columns based on the actual Articole livrate_returnate format
|
||||
data = (
|
||||
safe_get(row, 'Document Number', f'SH_{index:06d}'), # Shipment ID
|
||||
safe_get(row, 'Comanda'), # Order ID
|
||||
safe_get(row, 'Cod. Client'), # Customer Code
|
||||
safe_get(row, 'Nume client'), # Customer Name
|
||||
safe_get(row, 'Cod Articol'), # Article Code
|
||||
safe_get(row, 'Part Description'), # Article Description
|
||||
safe_get_int(row, 'Cantitate'), # Quantity Delivered
|
||||
self._parse_date(row.get('Data')), # Shipment Date
|
||||
self._parse_date(row.get('Data')), # Delivery Date (same as shipment for now)
|
||||
safe_get(row, 'Stare', 'DELIVERED'), # Delivery Status
|
||||
safe_get_float(row, 'Total Price') # Total Value
|
||||
)
|
||||
|
||||
cursor.execute(insert_sql, data)
|
||||
|
||||
# Track created vs updated
|
||||
if cursor.rowcount == 1:
|
||||
created_count += 1
|
||||
elif cursor.rowcount == 2:
|
||||
updated_count += 1
|
||||
|
||||
success_count += 1
|
||||
|
||||
except Exception as row_error:
|
||||
logger.warning(f"Error processing delivery row {index}: {row_error}")
|
||||
error_count += 1
|
||||
continue
|
||||
|
||||
self.connection.commit()
|
||||
logger.info(f"Delivery import completed: {success_count} successful, {error_count} errors")
|
||||
|
||||
return {
|
||||
'success_count': success_count,
|
||||
'created_count': created_count,
|
||||
'updated_count': updated_count,
|
||||
'error_count': error_count,
|
||||
'total_rows': len(df),
|
||||
'error_message': None if error_count == 0 else f'{error_count} rows failed to import'
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error importing delivery data: {e}")
|
||||
return {
|
||||
'success_count': 0,
|
||||
'error_count': 1,
|
||||
'total_rows': 0,
|
||||
'error_message': str(e)
|
||||
}
|
||||
|
||||
def generate_daily_summary(self, report_date=None):
|
||||
"""Generate daily summary for Daily Mirror reporting"""
|
||||
if not report_date:
|
||||
report_date = datetime.now().date()
|
||||
|
||||
try:
|
||||
cursor = self.connection.cursor()
|
||||
|
||||
# Check if summary already exists for this date
|
||||
cursor.execute("SELECT id FROM dm_daily_summary WHERE report_date = ?", (report_date,))
|
||||
existing = cursor.fetchone()
|
||||
|
||||
# Get production metrics
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
COUNT(*) as total_orders,
|
||||
SUM(quantity_requested) as total_quantity,
|
||||
SUM(CASE WHEN production_status = 'Inchis' THEN 1 ELSE 0 END) as completed_orders,
|
||||
SUM(CASE WHEN end_of_quilting IS NOT NULL THEN 1 ELSE 0 END) as quilting_done,
|
||||
SUM(CASE WHEN end_of_sewing IS NOT NULL THEN 1 ELSE 0 END) as sewing_done,
|
||||
COUNT(DISTINCT customer_code) as unique_customers
|
||||
FROM dm_production_orders
|
||||
WHERE DATE(data_planificare) = ?
|
||||
""", (report_date,))
|
||||
|
||||
production_metrics = cursor.fetchone()
|
||||
|
||||
# Get active operators count
|
||||
cursor.execute("""
|
||||
SELECT COUNT(DISTINCT CASE
|
||||
WHEN t1_operator_name IS NOT NULL THEN t1_operator_name
|
||||
WHEN t2_operator_name IS NOT NULL THEN t2_operator_name
|
||||
WHEN t3_operator_name IS NOT NULL THEN t3_operator_name
|
||||
END) as active_operators
|
||||
FROM dm_production_orders
|
||||
WHERE DATE(data_planificare) = ?
|
||||
""", (report_date,))
|
||||
|
||||
operator_metrics = cursor.fetchone()
|
||||
active_operators = operator_metrics[0] or 0
|
||||
|
||||
if existing:
|
||||
# Update existing summary
|
||||
update_sql = """
|
||||
UPDATE dm_daily_summary SET
|
||||
orders_quantity = ?, production_launched = ?, production_finished = ?,
|
||||
quilting_completed = ?, sewing_completed = ?, unique_customers = ?,
|
||||
active_operators = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE report_date = ?
|
||||
"""
|
||||
cursor.execute(update_sql, (
|
||||
production_metrics[1] or 0, production_metrics[0] or 0, production_metrics[2] or 0,
|
||||
production_metrics[3] or 0, production_metrics[4] or 0, production_metrics[5] or 0,
|
||||
active_operators, report_date
|
||||
))
|
||||
else:
|
||||
# Insert new summary
|
||||
insert_sql = """
|
||||
INSERT INTO dm_daily_summary (
|
||||
report_date, orders_quantity, production_launched, production_finished,
|
||||
quilting_completed, sewing_completed, unique_customers, active_operators
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"""
|
||||
cursor.execute(insert_sql, (
|
||||
report_date, production_metrics[1] or 0, production_metrics[0] or 0, production_metrics[2] or 0,
|
||||
production_metrics[3] or 0, production_metrics[4] or 0, production_metrics[5] or 0,
|
||||
active_operators
|
||||
))
|
||||
|
||||
self.connection.commit()
|
||||
logger.info(f"Daily summary generated for {report_date}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error generating daily summary: {e}")
|
||||
return False
|
||||
|
||||
def clear_production_orders(self):
|
||||
"""Delete all rows from the Daily Mirror production orders table"""
|
||||
try:
|
||||
cursor = self.connection.cursor()
|
||||
cursor.execute("DELETE FROM dm_production_orders")
|
||||
self.connection.commit()
|
||||
logger.info("All production orders deleted from dm_production_orders table.")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting production orders: {e}")
|
||||
return False
|
||||
|
||||
def _parse_date(self, date_value):
|
||||
"""Parse date with better null handling"""
|
||||
if pd.isna(date_value) or date_value == 'nan' or date_value is None or date_value == '':
|
||||
return None
|
||||
|
||||
try:
|
||||
if isinstance(date_value, str):
|
||||
# Handle various date formats
|
||||
for fmt in ['%Y-%m-%d', '%d/%m/%Y', '%m/%d/%Y', '%d.%m.%Y']:
|
||||
try:
|
||||
return datetime.strptime(date_value, fmt).date()
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
elif hasattr(date_value, 'date'):
|
||||
return date_value.date()
|
||||
elif isinstance(date_value, datetime):
|
||||
return date_value.date()
|
||||
|
||||
return None # If all parsing attempts fail
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error parsing date {date_value}: {e}")
|
||||
return None
|
||||
|
||||
def _parse_datetime(self, datetime_value):
|
||||
"""Parse datetime value from Excel"""
|
||||
if pd.isna(datetime_value):
|
||||
return None
|
||||
if isinstance(datetime_value, str) and datetime_value == '00:00:00':
|
||||
return None
|
||||
return datetime_value
|
||||
|
||||
def setup_daily_mirror_database():
|
||||
"""Setup the Daily Mirror database schema"""
|
||||
db = DailyMirrorDatabase()
|
||||
|
||||
if not db.connect():
|
||||
return False
|
||||
|
||||
try:
|
||||
success = db.create_database_schema()
|
||||
if success:
|
||||
print("✅ Daily Mirror database schema created successfully!")
|
||||
|
||||
# Generate sample daily summary for today
|
||||
db.generate_daily_summary()
|
||||
|
||||
return success
|
||||
finally:
|
||||
db.disconnect()
|
||||
|
||||
if __name__ == "__main__":
|
||||
setup_daily_mirror_database()
|
||||
@@ -23,6 +23,13 @@ MODULES = {
|
||||
'scan_pages': ['move_orders'],
|
||||
'management_pages': ['create_locations', 'warehouse_reports', 'inventory_management'],
|
||||
'worker_access': ['move_orders_only'] # Workers can move orders but not create locations
|
||||
},
|
||||
'daily_mirror': {
|
||||
'name': 'Daily Mirror',
|
||||
'scan_pages': [], # No scanning, purely reporting/analytics
|
||||
'management_pages': ['daily_mirror_main', 'daily_mirror_report', 'daily_mirror_history', 'daily_mirror_analytics'],
|
||||
'worker_access': ['view_only'], # Workers can view daily reports but cannot generate or export
|
||||
'description': 'Business Intelligence and Production Reporting Module'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,7 +103,14 @@ PAGE_ACCESS = {
|
||||
'labels': {'min_level': 50, 'modules': ['labels']},
|
||||
'label_scan': {'min_level': 50, 'modules': ['labels']},
|
||||
'label_creation': {'min_level': 70, 'modules': ['labels']}, # Manager+ only
|
||||
'label_reports': {'min_level': 70, 'modules': ['labels']} # Manager+ only
|
||||
'label_reports': {'min_level': 70, 'modules': ['labels']}, # Manager+ only
|
||||
|
||||
# Daily Mirror module pages
|
||||
'daily_mirror_main': {'min_level': 70, 'modules': ['daily_mirror']}, # Manager+ only
|
||||
'daily_mirror_report': {'min_level': 70, 'modules': ['daily_mirror']}, # Manager+ only
|
||||
'daily_mirror_history': {'min_level': 70, 'modules': ['daily_mirror']}, # Manager+ only
|
||||
'daily_mirror_analytics': {'min_level': 90, 'modules': ['daily_mirror']}, # Admin+ only for advanced analytics
|
||||
'daily_mirror': {'min_level': 70, 'modules': ['daily_mirror']} # Legacy route support
|
||||
}
|
||||
|
||||
def check_access(user_role, user_modules, page):
|
||||
|
||||
@@ -3469,6 +3469,40 @@ def delete_location():
|
||||
return jsonify({'success': False, 'error': str(e)})
|
||||
|
||||
|
||||
# Daily Mirror Route Redirects for Backward Compatibility
|
||||
@bp.route('/daily_mirror_main')
|
||||
def daily_mirror_main_route():
|
||||
"""Redirect to new Daily Mirror main route"""
|
||||
return redirect(url_for('daily_mirror.daily_mirror_main_route'))
|
||||
|
||||
@bp.route('/daily_mirror')
|
||||
def daily_mirror_route():
|
||||
"""Redirect to new Daily Mirror route"""
|
||||
return redirect(url_for('daily_mirror.daily_mirror_route'))
|
||||
|
||||
@bp.route('/daily_mirror_history')
|
||||
def daily_mirror_history_route():
|
||||
"""Redirect to new Daily Mirror history route"""
|
||||
return redirect(url_for('daily_mirror.daily_mirror_history_route'))
|
||||
|
||||
@bp.route('/daily_mirror_build_database', methods=['GET', 'POST'])
|
||||
def daily_mirror_build_database():
|
||||
"""Redirect to new Daily Mirror build database route"""
|
||||
if request.method == 'POST':
|
||||
# For POST requests, we need to forward the data
|
||||
return redirect(url_for('daily_mirror.daily_mirror_build_database'), code=307)
|
||||
return redirect(url_for('daily_mirror.daily_mirror_build_database'))
|
||||
|
||||
@bp.route('/api/daily_mirror_data', methods=['GET'])
|
||||
def api_daily_mirror_data():
|
||||
"""Redirect to new Daily Mirror API data route"""
|
||||
return redirect(url_for('daily_mirror.api_daily_mirror_data') + '?' + request.query_string.decode())
|
||||
|
||||
@bp.route('/api/daily_mirror_history_data', methods=['GET'])
|
||||
def api_daily_mirror_history_data():
|
||||
"""Redirect to new Daily Mirror API history data route"""
|
||||
return redirect(url_for('daily_mirror.api_daily_mirror_history_data') + '?' + request.query_string.decode())
|
||||
|
||||
# NOTE for frontend/extension developers:
|
||||
# To print labels, call the Chrome extension and pass the PDF URL:
|
||||
# /generate_labels_pdf/<order_id>
|
||||
|
||||
241
py_app/app/static/css/daily_mirror_tune.css
Normal file
241
py_app/app/static/css/daily_mirror_tune.css
Normal file
@@ -0,0 +1,241 @@
|
||||
/* Daily Mirror Tune Pages - Modal Styles */
|
||||
/* Fixes for editable modals across tune/production, tune/orders, and tune/delivery pages */
|
||||
|
||||
/* Force Bootstrap modal to have proper z-index */
|
||||
#editModal.modal {
|
||||
z-index: 9999 !important;
|
||||
}
|
||||
|
||||
#editModal .modal-backdrop {
|
||||
z-index: 9998 !important;
|
||||
}
|
||||
|
||||
/* Ensure modal dialog is interactive */
|
||||
#editModal .modal-dialog {
|
||||
pointer-events: auto !important;
|
||||
z-index: 10000 !important;
|
||||
}
|
||||
|
||||
#editModal .modal-content {
|
||||
pointer-events: auto !important;
|
||||
}
|
||||
|
||||
/* Make all inputs in the modal fully interactive */
|
||||
#editModal .form-control:not([readonly]),
|
||||
#editModal .form-select:not([readonly]),
|
||||
#editModal input:not([readonly]):not([type="hidden"]),
|
||||
#editModal select:not([readonly]),
|
||||
#editModal textarea:not([readonly]) {
|
||||
pointer-events: auto !important;
|
||||
user-select: text !important;
|
||||
cursor: text !important;
|
||||
background-color: #ffffff !important;
|
||||
color: #000000 !important;
|
||||
opacity: 1 !important;
|
||||
-webkit-user-select: text !important;
|
||||
-moz-user-select: text !important;
|
||||
-ms-user-select: text !important;
|
||||
}
|
||||
|
||||
#editModal .form-control:focus:not([readonly]),
|
||||
#editModal input:focus:not([readonly]),
|
||||
#editModal select:focus:not([readonly]),
|
||||
#editModal textarea:focus:not([readonly]) {
|
||||
background-color: #ffffff !important;
|
||||
color: #000000 !important;
|
||||
border-color: #007bff !important;
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25) !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
/* Dark mode specific overrides for modal inputs */
|
||||
body.dark-mode #editModal .form-control:not([readonly]),
|
||||
body.dark-mode #editModal input:not([readonly]):not([type="hidden"]),
|
||||
body.dark-mode #editModal select:not([readonly]),
|
||||
body.dark-mode #editModal textarea:not([readonly]) {
|
||||
background-color: #ffffff !important;
|
||||
color: #000000 !important;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
body.dark-mode #editModal .form-control:focus:not([readonly]),
|
||||
body.dark-mode #editModal input:focus:not([readonly]),
|
||||
body.dark-mode #editModal select:focus:not([readonly]),
|
||||
body.dark-mode #editModal textarea:focus:not([readonly]) {
|
||||
background-color: #ffffff !important;
|
||||
color: #000000 !important;
|
||||
border-color: #007bff !important;
|
||||
}
|
||||
|
||||
/* Readonly fields should still look readonly */
|
||||
#editModal .form-control[readonly],
|
||||
#editModal input[readonly] {
|
||||
background-color: #e9ecef !important;
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
|
||||
body.dark-mode #editModal .form-control[readonly],
|
||||
body.dark-mode #editModal input[readonly] {
|
||||
background-color: #6c757d !important;
|
||||
color: #e2e8f0 !important;
|
||||
}
|
||||
|
||||
/* Dark mode styles for cards and tables */
|
||||
body.dark-mode .card {
|
||||
background-color: #2d3748;
|
||||
color: #e2e8f0;
|
||||
border: 1px solid #4a5568;
|
||||
}
|
||||
|
||||
body.dark-mode .card-header {
|
||||
background-color: #4a5568;
|
||||
border-bottom: 1px solid #6b7280;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
body.dark-mode .form-control {
|
||||
background-color: #4a5568;
|
||||
border-color: #6b7280;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
body.dark-mode .form-control:focus {
|
||||
background-color: #4a5568;
|
||||
border-color: #007bff;
|
||||
color: #e2e8f0;
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
body.dark-mode .table {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
body.dark-mode .table-striped tbody tr:nth-of-type(odd) {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
body.dark-mode .table-hover tbody tr:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
body.dark-mode .modal-content {
|
||||
background-color: #2d3748;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
body.dark-mode .modal-header {
|
||||
border-bottom: 1px solid #4a5568;
|
||||
}
|
||||
|
||||
body.dark-mode .modal-footer {
|
||||
border-top: 1px solid #4a5568;
|
||||
}
|
||||
|
||||
body.dark-mode .btn-secondary {
|
||||
background-color: #4a5568;
|
||||
border-color: #6b7280;
|
||||
}
|
||||
|
||||
body.dark-mode .btn-secondary:hover {
|
||||
background-color: #6b7280;
|
||||
}
|
||||
|
||||
body.dark-mode .btn-close {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
/* Table and button styling */
|
||||
.table td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.btn-action {
|
||||
padding: 0.25rem 0.5rem;
|
||||
margin: 0.1rem;
|
||||
}
|
||||
|
||||
/* Editable field highlighting */
|
||||
.editable {
|
||||
background-color: #fff3cd;
|
||||
border: 1px dashed #ffc107;
|
||||
}
|
||||
|
||||
body.dark-mode .editable {
|
||||
background-color: #2d2d00;
|
||||
border: 1px dashed #ffc107;
|
||||
}
|
||||
|
||||
/* Compact table styling */
|
||||
.table-sm th,
|
||||
.table-sm td {
|
||||
padding: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Action button styling */
|
||||
.btn-sm {
|
||||
padding: 0.25rem 0.5rem;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
/* Pagination styling */
|
||||
.pagination {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.page-link {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
body.dark-mode .pagination .page-link {
|
||||
background-color: #4a5568;
|
||||
border: 1px solid #6b7280;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
body.dark-mode .pagination .page-link:hover {
|
||||
background-color: #374151;
|
||||
border-color: #6b7280;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
body.dark-mode .pagination .page-item.active .page-link {
|
||||
background-color: #3b82f6;
|
||||
border-color: #3b82f6;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* Additional dark mode styles */
|
||||
body.dark-mode .container-fluid {
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
body.dark-mode .text-muted {
|
||||
color: #a0aec0 !important;
|
||||
}
|
||||
|
||||
body.dark-mode .table-dark th {
|
||||
background-color: #1a202c;
|
||||
color: #e2e8f0;
|
||||
border-color: #4a5568;
|
||||
}
|
||||
|
||||
body.dark-mode .table-striped > tbody > tr:nth-of-type(odd) > td {
|
||||
background-color: #374151;
|
||||
}
|
||||
|
||||
body.dark-mode .table-hover > tbody > tr:hover > td {
|
||||
background-color: #4a5568;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.table-responsive {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.375rem 0.5rem;
|
||||
}
|
||||
}
|
||||
@@ -39,18 +39,15 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="right-header">
|
||||
<button id="theme-toggle" class="theme-toggle">Change to dark theme</button>
|
||||
{% if request.endpoint in ['main.upload_data', 'main.upload_orders', 'main.print_module', 'main.label_templates', 'main.create_template', 'main.print_lost_labels', 'main.view_orders'] %}
|
||||
<a href="{{ url_for('main.etichete') }}" class="btn go-to-main-etichete-btn">Main Page Etichete</a>
|
||||
{% endif %}
|
||||
{% if request.endpoint in ['main.quality', 'main.fg_quality'] %}
|
||||
<a href="{{ url_for('main.reports') }}" class="btn go-to-main-reports-btn">Main Page Reports</a>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('main.dashboard') }}" class="btn go-to-dashboard-btn">Go to Dashboard</a>
|
||||
{% if 'user' in session %}
|
||||
<span class="user-info">You are logged in as {{ session['user'] }}</span>
|
||||
<a href="{{ url_for('main.logout') }}" class="logout-button">Logout</a>
|
||||
{% endif %}
|
||||
<button id="theme-toggle" class="theme-toggle">Change to dark theme</button>
|
||||
{% if request.endpoint.startswith('daily_mirror') %}
|
||||
<a href="{{ url_for('daily_mirror.daily_mirror_main_route') }}" class="btn btn-info btn-sm ms-2"> <i class="fas fa-home"></i> Daily Mirror Main</a>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('main.dashboard') }}" class="btn go-to-dashboard-btn ms-2">Go to Dashboard</a>
|
||||
{% if 'user' in session %}
|
||||
<span class="user-info ms-2">You are logged in as {{ session['user'] }}</span>
|
||||
<a href="{{ url_for('main.logout') }}" class="logout-button ms-2">Logout</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
447
py_app/app/templates/daily_mirror.html
Normal file
447
py_app/app/templates/daily_mirror.html
Normal 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 %}
|
||||
719
py_app/app/templates/daily_mirror_build_database.html
Normal file
719
py_app/app/templates/daily_mirror_build_database.html
Normal file
@@ -0,0 +1,719 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Build Database - Daily Mirror{% 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>
|
||||
.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 %}
|
||||
449
py_app/app/templates/daily_mirror_history.html
Normal file
449
py_app/app/templates/daily_mirror_history.html
Normal 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 %}
|
||||
262
py_app/app/templates/daily_mirror_main.html
Normal file
262
py_app/app/templates/daily_mirror_main.html
Normal 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 %}
|
||||
503
py_app/app/templates/daily_mirror_tune_delivery.html
Normal file
503
py_app/app/templates/daily_mirror_tune_delivery.html
Normal file
@@ -0,0 +1,503 @@
|
||||
{% 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>
|
||||
<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.');
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
522
py_app/app/templates/daily_mirror_tune_orders.html
Normal file
522
py_app/app/templates/daily_mirror_tune_orders.html
Normal file
@@ -0,0 +1,522 @@
|
||||
{% 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') }}">
|
||||
{% 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>
|
||||
<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 ID</th>
|
||||
<th>Customer</th>
|
||||
<th>Client Order</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">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<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">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="editOrderId" class="form-label">Order ID</label>
|
||||
<input type="text" class="form-control" id="editOrderId" readonly>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<div class="row">
|
||||
<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 class="col-md-6 mb-3">
|
||||
<label for="editClientOrder" class="form-label">Client Order</label>
|
||||
<input type="text" class="form-control" id="editClientOrder">
|
||||
</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</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="editDeliveryDate" class="form-label">Delivery Date</label>
|
||||
<input type="date" class="form-control" id="editDeliveryDate">
|
||||
</div>
|
||||
<div class="col-md-6 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>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 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 class="col-md-6 mb-3">
|
||||
<label for="editProductGroup" class="form-label">Product Group</label>
|
||||
<input type="text" class="form-control" id="editProductGroup">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="editOrderDate" class="form-label">Order Date</label>
|
||||
<input type="date" class="form-control" id="editOrderDate">
|
||||
</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
|
||||
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_id}</strong></td>
|
||||
<td>
|
||||
<small class="text-muted d-block">${record.customer_code}</small>
|
||||
${record.customer_name}
|
||||
</td>
|
||||
<td>${record.client_order || '-'}</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);
|
||||
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('editOrderId').value = record.order_id;
|
||||
document.getElementById('editCustomerCode').value = record.customer_code;
|
||||
document.getElementById('editCustomerName').value = record.customer_name;
|
||||
document.getElementById('editClientOrder').value = record.client_order || '';
|
||||
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('editOrderStatus').value = record.order_status;
|
||||
document.getElementById('editPriority').value = record.priority || 'NORMAL';
|
||||
document.getElementById('editProductGroup').value = record.product_group || '';
|
||||
document.getElementById('editOrderDate').value = record.order_date;
|
||||
}
|
||||
|
||||
function saveRecord() {
|
||||
const recordId = document.getElementById('editRecordId').value;
|
||||
const data = {
|
||||
customer_code: document.getElementById('editCustomerCode').value,
|
||||
customer_name: document.getElementById('editCustomerName').value,
|
||||
client_order: document.getElementById('editClientOrder').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,
|
||||
order_status: document.getElementById('editOrderStatus').value,
|
||||
priority: document.getElementById('editPriority').value,
|
||||
product_group: document.getElementById('editProductGroup').value,
|
||||
order_date: document.getElementById('editOrderDate').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 saveAllChanges() {
|
||||
alert('Save All Changes functionality will be implemented for bulk operations.');
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
516
py_app/app/templates/daily_mirror_tune_production.html
Normal file
516
py_app/app/templates/daily_mirror_tune_production.html
Normal file
@@ -0,0 +1,516 @@
|
||||
{% 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') }}">
|
||||
{% 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>
|
||||
<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>Customer</th>
|
||||
<th>Client Order</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-lg modal-dialog-scrollable">
|
||||
<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">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6 mb-3">
|
||||
<label for="editProductionOrder" class="form-label">Production Order</label>
|
||||
<input type="text" class="form-control" id="editProductionOrder" readonly>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<div class="row">
|
||||
<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 class="col-md-6 mb-3">
|
||||
<label for="editClientOrder" class="form-label">Client Order</label>
|
||||
<input type="text" class="form-control" id="editClientOrder">
|
||||
</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</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="editDeliveryDate" class="form-label">Delivery Date</label>
|
||||
<input type="date" class="form-control" id="editDeliveryDate">
|
||||
</div>
|
||||
<div class="col-md-6 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>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="editMachine" class="form-label">Machine Code</label>
|
||||
<input type="text" class="form-control" id="editMachine">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">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 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.customer_code}<br><small class="text-muted">${record.customer_name || ''}</small></td>
|
||||
<td>${record.client_order || ''}</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('editCustomerCode').value = record.customer_code || '';
|
||||
document.getElementById('editCustomerName').value = record.customer_name || '';
|
||||
document.getElementById('editClientOrder').value = record.client_order || '';
|
||||
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', 'editClientOrder',
|
||||
'editArticleCode', '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');
|
||||
|
||||
// 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
|
||||
});
|
||||
|
||||
modal.show();
|
||||
|
||||
// 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 = {
|
||||
customer_code: document.getElementById('editCustomerCode').value,
|
||||
customer_name: document.getElementById('editCustomerName').value,
|
||||
client_order: document.getElementById('editClientOrder').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 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!');
|
||||
}
|
||||
|
||||
// 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 %}
|
||||
@@ -34,5 +34,17 @@
|
||||
<a href="{{ url_for('main.settings') }}" class="btn">Access Settings Page</a>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-card">
|
||||
<h3>📊 Daily Mirror</h3>
|
||||
<p>Business Intelligence and Production Reporting - Generate comprehensive daily reports including order quantities, production status, and delivery tracking.</p>
|
||||
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
|
||||
<a href="{{ url_for('daily_mirror.daily_mirror_main_route') }}" class="btn">📊 Daily Mirror Hub</a>
|
||||
</div>
|
||||
<div style="margin-top: 8px; font-size: 12px; color: #666;">
|
||||
<strong>Tracks:</strong> Orders quantity • Production launched • Production finished • Orders delivered
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -408,6 +408,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 (Business Intelligence)</label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="accessLevelInfo" class="access-level-info" style="display: none;"></div>
|
||||
</div>
|
||||
@@ -454,6 +458,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</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -621,6 +629,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</label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="editAccessLevelInfo" class="access-level-info" style="display: none;"></div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user