Compare commits

..

1 Commits

Author SHA1 Message Date
b87c398977 new updates 2025-10-10 22:53:04 +03:00
547 changed files with 971 additions and 8131 deletions

View File

@@ -1,61 +0,0 @@
# Git files
.git
.gitignore
# Python cache
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
# Virtual environments
venv/
env/
ENV/
recticel/
# IDE
.vscode/
.idea/
*.swp
*.swo
# Logs
*.log
logs/
# Database
*.db
*.sqlite
*.sqlite3
# Documentation
*.md
!README.md
# Backup files
backup/
*.bak
*.backup
*.old
# OS files
.DS_Store
Thumbs.db
# Application specific
instance/*.db
chrome_extension/
VS code/
tray/
# Scripts not needed in container
*.sh
!docker-entrypoint.sh
# Service files
*.service
# Config that will be generated
instance/external_server.conf

View File

@@ -1,13 +0,0 @@
# Environment Configuration for Recticel Quality Application
# Copy this file to .env and adjust the values as needed
# Database Configuration
MYSQL_ROOT_PASSWORD=rootpassword
DB_PORT=3306
# Application Configuration
APP_PORT=8781
# Initialization Flags (set to "false" after first successful deployment)
INIT_DB=true
SEED_DB=true

15
.gitignore vendored
View File

@@ -28,19 +28,4 @@ VS code/obj/
# Backup files # Backup files
*.backup *.backup
# Docker deployment
.env
*.env
!.env.example
logs/
*.log
app.log
backup_*.sql
instance/external_server.conf
*.db
*.sqlite
*.sqlite3
.docker/
*.backup2 *.backup2

View File

@@ -1,41 +0,0 @@
# Dockerfile for Recticel Quality Application
FROM python:3.10-slim
# Set environment variables
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
FLASK_APP=run.py \
FLASK_ENV=production
# Install system dependencies
RUN apt-get update && apt-get install -y \
gcc \
default-libmysqlclient-dev \
pkg-config \
&& rm -rf /var/lib/apt/lists/*
# Create app directory
WORKDIR /app
# Copy requirements and install Python dependencies
COPY py_app/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY py_app/ .
# Create necessary directories
RUN mkdir -p /app/instance /srv/quality_recticel/logs
# Create a script to wait for database and initialize
COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
# Expose the application port
EXPOSE 8781
# Use the entrypoint script
ENTRYPOINT ["/docker-entrypoint.sh"]
# Run gunicorn
CMD ["gunicorn", "--config", "gunicorn.conf.py", "wsgi:application"]

View File

@@ -1,93 +0,0 @@
.PHONY: help build up down restart logs logs-web logs-db clean reset shell shell-db status health
help: ## Show this help message
@echo "Recticel Quality Application - Docker Commands"
@echo ""
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}'
build: ## Build the Docker images
docker-compose build
up: ## Start all services
docker-compose up -d
@echo "✅ Services started. Access the app at http://localhost:8781"
@echo "Default login: superadmin / superadmin123"
down: ## Stop all services
docker-compose down
restart: ## Restart all services
docker-compose restart
logs: ## View logs from all services
docker-compose logs -f
logs-web: ## View logs from web application
docker-compose logs -f web
logs-db: ## View logs from database
docker-compose logs -f db
status: ## Show status of all services
docker-compose ps
health: ## Check health of services
@echo "=== Service Health Status ==="
@docker inspect recticel-app | grep -A 5 '"Health"' || echo "Web app: Running"
@docker inspect recticel-db | grep -A 5 '"Health"' || echo "Database: Running"
shell: ## Open shell in web application container
docker-compose exec web bash
shell-db: ## Open MariaDB console
docker-compose exec db mariadb -u trasabilitate -p trasabilitate
clean: ## Stop services and remove containers (keeps data)
docker-compose down
reset: ## Complete reset - removes all data including database
@echo "⚠️ WARNING: This will delete all data!"
@read -p "Are you sure? [y/N] " -n 1 -r; \
echo; \
if [[ $$REPLY =~ ^[Yy]$$ ]]; then \
docker-compose down -v; \
rm -rf logs/*; \
rm -f instance/external_server.conf; \
echo "✅ Reset complete"; \
fi
deploy: build up ## Build and deploy (fresh start)
@echo "✅ Deployment complete!"
@sleep 5
@make status
rebuild: ## Rebuild and restart web application
docker-compose up -d --build web
backup-db: ## Backup database to backup.sql
docker-compose exec -T db mariadb-dump -u trasabilitate -pInitial01! trasabilitate > backup_$(shell date +%Y%m%d_%H%M%S).sql
@echo "✅ Database backed up"
restore-db: ## Restore database from backup.sql (provide BACKUP=filename)
@if [ -z "$(BACKUP)" ]; then \
echo "❌ Usage: make restore-db BACKUP=backup_20231215_120000.sql"; \
exit 1; \
fi
docker-compose exec -T db mariadb -u trasabilitate -pInitial01! trasabilitate < $(BACKUP)
@echo "✅ Database restored from $(BACKUP)"
install: ## Initial installation and setup
@echo "=== Installing Recticel Quality Application ==="
@if [ ! -f .env ]; then \
cp .env.example .env; \
echo "✅ Created .env file"; \
fi
@mkdir -p logs instance
@echo "✅ Created directories"
@make deploy
@echo ""
@echo "=== Installation Complete ==="
@echo "Access the application at: http://localhost:8781"
@echo "Default login: superadmin / superadmin123"
@echo ""
@echo "⚠️ Remember to change the default passwords!"

View File

@@ -1,88 +0,0 @@
#!/bin/bash
# Quick deployment script for Recticel Quality Application
set -e
echo "================================================"
echo " Recticel Quality Application"
echo " Docker Deployment"
echo "================================================"
echo ""
# Check if Docker is installed
if ! command -v docker &> /dev/null; then
echo "❌ Docker is not installed. Please install Docker first."
exit 1
fi
# Check if Docker Compose is installed
if ! command -v docker-compose &> /dev/null; then
echo "❌ Docker Compose is not installed. Please install Docker Compose first."
exit 1
fi
echo "✅ Docker and Docker Compose are installed"
echo ""
# Create .env if it doesn't exist
if [ ! -f .env ]; then
echo "Creating .env file from template..."
cp .env.example .env
echo "✅ Created .env file"
echo "⚠️ Please review .env and update passwords before production use"
echo ""
fi
# Create necessary directories
echo "Creating necessary directories..."
mkdir -p logs instance
echo "✅ Directories created"
echo ""
# Stop any existing services
echo "Stopping any existing services..."
docker-compose down 2>/dev/null || true
echo ""
# Build and start services
echo "Building Docker images..."
docker-compose build
echo ""
echo "Starting services..."
docker-compose up -d
echo ""
# Wait for services to be ready
echo "Waiting for services to be ready..."
sleep 10
# Check status
echo ""
echo "================================================"
echo " Deployment Status"
echo "================================================"
docker-compose ps
echo ""
# Show access information
echo "================================================"
echo " ✅ Deployment Complete!"
echo "================================================"
echo ""
echo "Application URL: http://localhost:8781"
echo ""
echo "Default Login Credentials:"
echo " Username: superadmin"
echo " Password: superadmin123"
echo ""
echo "⚠️ IMPORTANT: Change the default password after first login!"
echo ""
echo "Useful Commands:"
echo " View logs: docker-compose logs -f"
echo " Stop services: docker-compose down"
echo " Restart: docker-compose restart"
echo " Shell access: docker-compose exec web bash"
echo ""
echo "For more information, see DOCKER_DEPLOYMENT.md"
echo ""

View File

@@ -1,77 +0,0 @@
version: '3.8'
services:
# MariaDB Database Service
db:
image: mariadb:11.3
container_name: recticel-db
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-rootpassword}
MYSQL_DATABASE: trasabilitate
MYSQL_USER: trasabilitate
MYSQL_PASSWORD: Initial01!
ports:
- "${DB_PORT:-3306}:3306"
volumes:
- /srv/docker-test/mariadb:/var/lib/mysql
- ./init-db.sql:/docker-entrypoint-initdb.d/01-init.sql
networks:
- recticel-network
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
# Flask Web Application Service
web:
build:
context: .
dockerfile: Dockerfile
container_name: recticel-app
restart: unless-stopped
depends_on:
db:
condition: service_healthy
environment:
# Database connection settings
DB_HOST: db
DB_PORT: 3306
DB_NAME: trasabilitate
DB_USER: trasabilitate
DB_PASSWORD: Initial01!
# Application settings
FLASK_ENV: production
FLASK_APP: run.py
# Initialization flags (set to "false" after first run if needed)
INIT_DB: "true"
SEED_DB: "true"
ports:
- "${APP_PORT:-8781}:8781"
volumes:
# Mount logs directory for persistence
- /srv/docker-test/logs:/srv/quality_recticel/logs
# Mount instance directory for config persistence
- /srv/docker-test/instance:/app/instance
# Mount app code for easy updates (DISABLED - causes config issues)
# Uncomment only for development, not production
# - /srv/docker-test/app:/app
networks:
- recticel-network
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8781/"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
networks:
recticel-network:
driver: bridge
# Note: Using bind mounts to /srv/docker-test/ instead of named volumes
# This allows easier access and management of persistent data

View File

@@ -1,72 +0,0 @@
#!/bin/bash
set -e
echo "==================================="
echo "Recticel Quality App - Starting"
echo "==================================="
# Wait for MariaDB to be ready
echo "Waiting for MariaDB to be ready..."
until python3 << END
import mariadb
import sys
import time
max_retries = 30
retry_count = 0
while retry_count < max_retries:
try:
conn = mariadb.connect(
user="${DB_USER}",
password="${DB_PASSWORD}",
host="${DB_HOST}",
port=int("${DB_PORT}"),
database="${DB_NAME}"
)
conn.close()
print("✅ Database connection successful!")
sys.exit(0)
except Exception as e:
retry_count += 1
print(f"Database not ready yet (attempt {retry_count}/{max_retries}). Waiting...")
time.sleep(2)
print("❌ Failed to connect to database after 30 attempts")
sys.exit(1)
END
do
echo "Retrying database connection..."
sleep 2
done
# Create external_server.conf from environment variables
echo "Creating database configuration..."
cat > /app/instance/external_server.conf << EOF
server_domain=${DB_HOST}
port=${DB_PORT}
database_name=${DB_NAME}
username=${DB_USER}
password=${DB_PASSWORD}
EOF
echo "✅ Database configuration created"
# Run database initialization if needed
if [ "${INIT_DB}" = "true" ]; then
echo "Initializing database schema..."
python3 /app/app/db_create_scripts/setup_complete_database.py || echo "⚠️ Database may already be initialized"
fi
# Seed the database with superadmin user
if [ "${SEED_DB}" = "true" ]; then
echo "Seeding database with superadmin user..."
python3 /app/seed.py || echo "⚠️ Database may already be seeded"
fi
echo "==================================="
echo "Starting application..."
echo "==================================="
# Execute the CMD
exec "$@"

View File

@@ -1,20 +0,0 @@
-- MariaDB Initialization Script for Recticel Quality Application
-- This script creates the database and user if they don't exist
-- Create database if it doesn't exist
CREATE DATABASE IF NOT EXISTS trasabilitate CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- Create user if it doesn't exist (MariaDB 10.2+)
CREATE USER IF NOT EXISTS 'trasabilitate'@'%' IDENTIFIED BY 'Initial01!';
-- Grant all privileges on the database to the user
GRANT ALL PRIVILEGES ON trasabilitate.* TO 'trasabilitate'@'%';
-- Flush privileges to ensure they take effect
FLUSH PRIVILEGES;
-- Select the database
USE trasabilitate;
-- The actual table creation will be handled by the Python setup script
-- This ensures compatibility with the existing setup_complete_database.py

View File

@@ -1,154 +0,0 @@
192.168.0.132 - - [15/Oct/2025:00:30:16 +0300] "GET /print_module HTTP/1.1" 200 69964 "https://quality.moto-adv.com/etichete" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 105688
192.168.0.132 - - [15/Oct/2025:00:30:17 +0300] "GET /favicon.ico HTTP/1.1" 404 207 "https://quality.moto-adv.com/print_module" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 2579
192.168.0.132 - - [15/Oct/2025:00:30:17 +0300] "GET /get_unprinted_orders HTTP/1.1" 200 3 "https://quality.moto-adv.com/print_module" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 5560
192.168.0.132 - - [15/Oct/2025:00:30:28 +0300] "GET /dashboard HTTP/1.1" 302 189 "https://quality.moto-adv.com/print_module" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 15566
192.168.0.132 - - [15/Oct/2025:00:30:28 +0300] "GET / HTTP/1.1" 200 1189 "https://quality.moto-adv.com/print_module" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 40767
192.168.0.132 - - [15/Oct/2025:00:30:28 +0300] "GET /static/script.js HTTP/1.1" 200 0 "https://quality.moto-adv.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 12665
192.168.0.132 - - [15/Oct/2025:00:30:28 +0300] "GET /static/style.css HTTP/1.1" 200 0 "https://quality.moto-adv.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 14360
192.168.0.132 - - [15/Oct/2025:00:30:28 +0300] "GET /static/logo_login.jpg HTTP/1.1" 200 0 "https://quality.moto-adv.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 5758
192.168.0.132 - - [15/Oct/2025:00:30:28 +0300] "GET /static/css/base.css HTTP/1.1" 200 0 "https://quality.moto-adv.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 26983
192.168.0.132 - - [15/Oct/2025:00:30:28 +0300] "GET /static/css/login.css HTTP/1.1" 200 0 "https://quality.moto-adv.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 30870
192.168.0.132 - - [15/Oct/2025:00:30:46 +0300] "POST / HTTP/1.1" 200 1189 "https://quality.moto-adv.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 48983
192.168.0.132 - - [15/Oct/2025:00:31:01 +0300] "POST / HTTP/1.1" 200 1189 "https://quality.moto-adv.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 7519
192.168.0.132 - - [15/Oct/2025:00:31:14 +0300] "POST / HTTP/1.1" 200 1189 "https://quality.moto-adv.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 62448
192.168.0.132 - - [15/Oct/2025:00:31:33 +0300] "POST / HTTP/1.1" 302 207 "https://quality.moto-adv.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 24735
192.168.0.132 - - [15/Oct/2025:00:31:33 +0300] "GET /dashboard HTTP/1.1" 200 2527 "https://quality.moto-adv.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 40939
192.168.0.132 - - [15/Oct/2025:00:31:33 +0300] "GET /static/scan_me.jpg HTTP/1.1" 200 0 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 26109
192.168.0.132 - - [15/Oct/2025:00:31:39 +0300] "GET /main_scan HTTP/1.1" 200 1981 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 38217
192.168.0.132 - - [15/Oct/2025:00:31:47 +0300] "GET /fg_scan HTTP/1.1" 200 23188 "https://quality.moto-adv.com/main_scan" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 34772
192.168.0.132 - - [15/Oct/2025:00:31:47 +0300] "GET /static/css/scan.css HTTP/1.1" 200 0 "https://quality.moto-adv.com/fg_scan" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 2544
192.168.0.132 - - [15/Oct/2025:01:29:14 +0300] "GET /quality HTTP/1.1" 302 207 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 17315
192.168.0.132 - - [15/Oct/2025:01:29:15 +0300] "GET /dashboard HTTP/1.1" 302 189 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 2603
192.168.0.132 - - [15/Oct/2025:01:29:15 +0300] "GET / HTTP/1.1" 200 1189 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 40000
192.168.0.132 - - [15/Oct/2025:01:29:19 +0300] "POST / HTTP/1.1" 302 207 "https://quality.moto-adv.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 9291
192.168.0.132 - - [15/Oct/2025:01:29:19 +0300] "GET /dashboard HTTP/1.1" 200 2527 "https://quality.moto-adv.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 9739
192.168.0.132 - - [15/Oct/2025:01:29:21 +0300] "GET /quality HTTP/1.1" 200 8292 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 10366
192.168.0.132 - - [15/Oct/2025:01:29:24 +0300] "GET /dashboard HTTP/1.1" 200 2527 "https://quality.moto-adv.com/quality" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 2422
192.168.0.132 - - [15/Oct/2025:01:29:26 +0300] "GET /main_scan HTTP/1.1" 200 1981 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 6923
192.168.0.132 - - [15/Oct/2025:01:29:27 +0300] "GET /fg_scan HTTP/1.1" 200 23188 "https://quality.moto-adv.com/main_scan" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 30751
192.168.0.132 - - [15/Oct/2025:01:29:31 +0300] "GET /dashboard HTTP/1.1" 200 2527 "https://quality.moto-adv.com/fg_scan" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 2426
192.168.0.132 - - [15/Oct/2025:01:29:33 +0300] "GET /etichete HTTP/1.1" 200 2420 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 54927
192.168.0.132 - - [15/Oct/2025:01:29:35 +0300] "GET /upload_data HTTP/1.1" 200 10558 "https://quality.moto-adv.com/etichete" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 21518
192.168.0.132 - - [15/Oct/2025:01:29:38 +0300] "GET /etichete HTTP/1.1" 200 2420 "https://quality.moto-adv.com/upload_data" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 2995
192.168.0.132 - - [15/Oct/2025:01:29:40 +0300] "GET /print_module HTTP/1.1" 200 70135 "https://quality.moto-adv.com/etichete" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 58228
192.168.0.132 - - [15/Oct/2025:01:29:40 +0300] "GET /static/qz-tray.js HTTP/1.1" 200 0 "https://quality.moto-adv.com/print_module" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 16021
192.168.0.132 - - [15/Oct/2025:01:29:40 +0300] "GET /static/JsBarcode.all.min.js HTTP/1.1" 200 0 "https://quality.moto-adv.com/print_module" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 28667
192.168.0.132 - - [15/Oct/2025:01:29:40 +0300] "GET /static/html2canvas.min.js HTTP/1.1" 200 0 "https://quality.moto-adv.com/print_module" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 35231
192.168.0.132 - - [15/Oct/2025:01:29:40 +0300] "GET /get_pairing_keys HTTP/1.1" 200 118 "https://quality.moto-adv.com/print_module" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 3023
192.168.0.132 - - [15/Oct/2025:01:29:41 +0300] "GET /get_unprinted_orders HTTP/1.1" 200 3 "https://quality.moto-adv.com/print_module" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 8089
192.168.0.132 - - [15/Oct/2025:01:29:45 +0300] "GET /etichete HTTP/1.1" 200 2420 "https://quality.moto-adv.com/print_module" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 40846
192.168.0.132 - - [15/Oct/2025:01:29:49 +0300] "GET /dashboard HTTP/1.1" 200 2527 "https://quality.moto-adv.com/etichete" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 9861
192.168.0.132 - - [15/Oct/2025:01:29:50 +0300] "GET /settings HTTP/1.1" 200 9105 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 29372
192.168.0.132 - - [15/Oct/2025:15:39:41 +0300] "HEAD / HTTP/1.1" 200 0 "-" "-" 51821
192.168.0.132 - - [15/Oct/2025:15:39:41 +0300] "GET / HTTP/1.1" 200 1189 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" 1706
192.168.0.132 - - [15/Oct/2025:15:39:42 +0300] "GET /static/css/login.css HTTP/1.1" 200 0 "https://quality.moto-adv.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" 12219
192.168.0.132 - - [15/Oct/2025:15:39:42 +0300] "GET /static/logo_login.jpg HTTP/1.1" 200 0 "https://quality.moto-adv.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" 5333
192.168.0.132 - - [15/Oct/2025:15:39:42 +0300] "GET /static/style.css HTTP/1.1" 304 0 "https://quality.moto-adv.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" 13946
192.168.0.132 - - [15/Oct/2025:15:39:42 +0300] "GET /static/css/base.css HTTP/1.1" 304 0 "https://quality.moto-adv.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" 2079
192.168.0.132 - - [15/Oct/2025:15:39:42 +0300] "GET /static/script.js HTTP/1.1" 200 0 "https://quality.moto-adv.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" 27067
192.168.0.132 - - [15/Oct/2025:15:39:42 +0300] "GET /favicon.ico HTTP/1.1" 404 207 "https://quality.moto-adv.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" 2104
192.168.0.132 - - [15/Oct/2025:15:39:43 +0300] "POST / HTTP/1.1" 302 207 "https://quality.moto-adv.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" 10666
192.168.0.132 - - [15/Oct/2025:15:39:43 +0300] "GET /dashboard HTTP/1.1" 200 2524 "https://quality.moto-adv.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" 10094
192.168.0.132 - - [15/Oct/2025:15:39:43 +0300] "GET /static/scan_me.jpg HTTP/1.1" 200 0 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" 51915
192.168.0.132 - - [15/Oct/2025:15:39:45 +0300] "GET /main_scan HTTP/1.1" 200 1978 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" 38423
192.168.0.132 - - [15/Oct/2025:15:39:47 +0300] "GET /fg_scan HTTP/1.1" 200 23185 "https://quality.moto-adv.com/main_scan" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" 32657
192.168.0.132 - - [15/Oct/2025:15:39:47 +0300] "GET /static/css/scan.css HTTP/1.1" 304 0 "https://quality.moto-adv.com/fg_scan" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" 2641
192.168.0.132 - - [15/Oct/2025:15:39:52 +0300] "GET / HTTP/1.1" 200 1189 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36" 8104
192.168.0.132 - - [15/Oct/2025:15:39:53 +0300] "GET /static/script.js HTTP/1.1" 200 0 "https://quality.moto-adv.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36" 2178
192.168.0.132 - - [15/Oct/2025:15:39:53 +0300] "GET /static/style.css HTTP/1.1" 200 0 "https://quality.moto-adv.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36" 1912
192.168.0.132 - - [15/Oct/2025:15:39:53 +0300] "GET /static/css/login.css HTTP/1.1" 200 0 "https://quality.moto-adv.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36" 2623
192.168.0.132 - - [15/Oct/2025:15:39:53 +0300] "GET /static/css/base.css HTTP/1.1" 200 0 "https://quality.moto-adv.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36" 2380
192.168.0.132 - - [15/Oct/2025:15:39:53 +0300] "GET /static/logo_login.jpg HTTP/1.1" 200 0 "https://quality.moto-adv.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36" 32438
192.168.0.132 - - [15/Oct/2025:15:39:54 +0300] "GET /favicon.ico HTTP/1.1" 404 207 "https://quality.moto-adv.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36" 3180
192.168.0.132 - - [15/Oct/2025:15:46:15 +0300] "POST /fg_scan HTTP/1.1" 200 23790 "https://quality.moto-adv.com/fg_scan" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" 41768
192.168.0.132 - - [15/Oct/2025:15:46:58 +0300] "POST /fg_scan HTTP/1.1" 200 24394 "https://quality.moto-adv.com/fg_scan" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" 70762
192.168.0.132 - - [15/Oct/2025:15:47:44 +0300] "POST /fg_scan HTTP/1.1" 200 24997 "https://quality.moto-adv.com/fg_scan" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" 15544
192.168.0.132 - - [15/Oct/2025:15:48:03 +0300] "POST /fg_scan HTTP/1.1" 200 25601 "https://quality.moto-adv.com/fg_scan" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" 41106
192.168.0.132 - - [15/Oct/2025:15:48:26 +0300] "POST /fg_scan HTTP/1.1" 200 26205 "https://quality.moto-adv.com/fg_scan" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" 91583
192.168.0.132 - - [15/Oct/2025:15:49:21 +0300] "POST /fg_scan HTTP/1.1" 200 26810 "https://quality.moto-adv.com/fg_scan" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" 12878
192.168.0.132 - - [15/Oct/2025:15:49:38 +0300] "POST /fg_scan HTTP/1.1" 200 27413 "https://quality.moto-adv.com/fg_scan" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" 76806
192.168.0.132 - - [15/Oct/2025:15:49:48 +0300] "POST /fg_scan HTTP/1.1" 200 28017 "https://quality.moto-adv.com/fg_scan" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" 14211
192.168.0.132 - - [15/Oct/2025:15:49:56 +0300] "POST /fg_scan HTTP/1.1" 200 28621 "https://quality.moto-adv.com/fg_scan" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" 13928
192.168.0.132 - - [15/Oct/2025:15:50:02 +0300] "POST /fg_scan HTTP/1.1" 200 29225 "https://quality.moto-adv.com/fg_scan" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" 13590
192.168.0.132 - - [15/Oct/2025:15:50:55 +0300] "GET /dashboard HTTP/1.1" 200 2524 "https://quality.moto-adv.com/fg_scan" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" 10067
192.168.0.132 - - [15/Oct/2025:15:50:57 +0300] "GET /quality HTTP/1.1" 302 207 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" 2988
192.168.0.132 - - [15/Oct/2025:15:50:57 +0300] "GET /dashboard HTTP/1.1" 200 2524 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" 2528
192.168.0.132 - - [15/Oct/2025:15:51:06 +0300] "GET /quality HTTP/1.1" 302 207 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" 3070
192.168.0.132 - - [15/Oct/2025:15:51:06 +0300] "GET /dashboard HTTP/1.1" 200 2524 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" 9653
192.168.0.132 - - [15/Oct/2025:15:51:07 +0300] "GET /quality HTTP/1.1" 302 207 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" 3316
192.168.0.132 - - [15/Oct/2025:15:51:07 +0300] "GET /dashboard HTTP/1.1" 200 2524 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" 2520
192.168.0.132 - - [15/Oct/2025:15:51:11 +0300] "GET /main_scan HTTP/1.1" 200 1978 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" 6865
192.168.0.132 - - [15/Oct/2025:15:51:16 +0300] "GET /fg_scan HTTP/1.1" 200 29225 "https://quality.moto-adv.com/main_scan" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" 5743
192.168.0.132 - - [15/Oct/2025:15:58:53 +0300] "GET /dashboard HTTP/1.1" 200 2524 "https://quality.moto-adv.com/fg_scan" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" 41424
192.168.0.132 - - [15/Oct/2025:15:58:54 +0300] "GET /quality HTTP/1.1" 302 207 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" 2982
192.168.0.132 - - [15/Oct/2025:15:58:54 +0300] "GET /dashboard HTTP/1.1" 200 2524 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" 9731
192.168.0.132 - - [15/Oct/2025:15:58:56 +0300] "GET /quality HTTP/1.1" 302 207 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" 2857
192.168.0.132 - - [15/Oct/2025:15:58:56 +0300] "GET /dashboard HTTP/1.1" 200 2524 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" 2509
192.168.0.132 - - [15/Oct/2025:15:58:57 +0300] "GET /warehouse HTTP/1.1" 200 2421 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Edg/141.0.0.0" 8579
192.168.0.132 - - [15/Oct/2025:16:46:55 +0300] "GET / HTTP/1.1" 200 1189 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36" 1639
192.168.0.132 - - [15/Oct/2025:16:46:56 +0300] "GET /sitemap.xml HTTP/1.1" 404 207 "-" "fasthttp" 1943
192.168.0.132 - - [15/Oct/2025:16:46:56 +0300] "GET /sitemap.xml HTTP/1.1" 404 207 "-" "fasthttp" 1420
192.168.0.132 - - [15/Oct/2025:16:46:56 +0300] "GET /favicon.ico HTTP/1.1" 404 207 "-" "fasthttp" 1322
192.168.0.132 - - [15/Oct/2025:16:46:56 +0300] "GET /robots.txt HTTP/1.1" 404 207 "-" "fasthttp" 1374
192.168.0.132 - - [15/Oct/2025:16:46:57 +0300] "GET / HTTP/1.1" 200 1189 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36" 2550
192.168.0.132 - - [15/Oct/2025:16:46:57 +0300] "GET /sitemap.xml HTTP/1.1" 404 207 "-" "fasthttp" 1346
192.168.0.132 - - [15/Oct/2025:22:42:50 +0300] "GET / HTTP/1.1" 200 1189 "-" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36" 1787
192.168.0.132 - - [15/Oct/2025:22:42:50 +0300] "GET /static/css/login.css HTTP/1.1" 304 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" 1981
192.168.0.132 - - [15/Oct/2025:22:42:50 +0300] "GET /static/script.js HTTP/1.1" 304 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" 2223
192.168.0.132 - - [15/Oct/2025:22:42:50 +0300] "GET /static/style.css HTTP/1.1" 304 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" 2254
192.168.0.132 - - [15/Oct/2025:22:42:50 +0300] "GET /static/logo_login.jpg HTTP/1.1" 304 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" 2182
192.168.0.132 - - [15/Oct/2025:22:42:50 +0300] "GET /static/css/base.css HTTP/1.1" 304 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" 2082
192.168.0.132 - - [15/Oct/2025:22:42:50 +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" 2049
192.168.0.132 - - [15/Oct/2025:22:43:10 +0300] "POST / HTTP/1.1" 302 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" 6531
192.168.0.132 - - [15/Oct/2025:22:43:10 +0300] "GET /dashboard HTTP/1.1" 200 2524 "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" 9664
192.168.0.132 - - [15/Oct/2025:22:43:11 +0300] "GET /static/scan_me.jpg HTTP/1.1" 304 0 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36" 12203
192.168.0.132 - - [15/Oct/2025:22:43:25 +0300] "GET /quality HTTP/1.1" 302 207 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36" 2443
192.168.0.132 - - [15/Oct/2025:22:43:25 +0300] "GET /dashboard HTTP/1.1" 200 2524 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36" 2433
192.168.0.132 - - [15/Oct/2025:22:43:29 +0300] "GET /quality HTTP/1.1" 302 207 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36" 2319
192.168.0.132 - - [15/Oct/2025:22:43:29 +0300] "GET /dashboard HTTP/1.1" 200 2524 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36" 2372
192.168.0.132 - - [15/Oct/2025:22:43:37 +0300] "GET /logout HTTP/1.1" 302 189 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36" 2324
192.168.0.132 - - [15/Oct/2025:22:43:37 +0300] "GET / HTTP/1.1" 200 1189 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36" 2029
192.168.0.132 - - [15/Oct/2025:22:44:04 +0300] "POST / HTTP/1.1" 200 1189 "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" 12509
192.168.0.132 - - [15/Oct/2025:22:44:22 +0300] "POST / HTTP/1.1" 200 1189 "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" 7961
192.168.0.132 - - [15/Oct/2025:22:44:45 +0300] "POST / HTTP/1.1" 302 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" 6237
192.168.0.132 - - [15/Oct/2025:22:44:45 +0300] "GET /dashboard HTTP/1.1" 200 2527 "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" 2471
192.168.0.132 - - [15/Oct/2025:22:44:47 +0300] "GET /quality HTTP/1.1" 200 8292 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36" 10556
192.168.0.132 - - [15/Oct/2025:22:45:00 +0300] "GET /dashboard HTTP/1.1" 200 2527 "https://quality.moto-adv.com/quality" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36" 2462
192.168.0.132 - - [15/Oct/2025:22:45:07 +0300] "GET /settings HTTP/1.1" 200 9105 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36" 26538
192.168.0.132 - - [15/Oct/2025:22:45:11 +0300] "GET /settings HTTP/1.1" 302 207 "-" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Mobile Safari/537.36 (compatible; Google-Read-Aloud; +https://support.google.com/webmasters/answer/1061943)" 2098
192.168.0.132 - - [15/Oct/2025:22:45:11 +0300] "GET /dashboard HTTP/1.1" 302 189 "-" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Mobile Safari/537.36 (compatible; Google-Read-Aloud; +https://support.google.com/webmasters/answer/1061943)" 1426
192.168.0.132 - - [15/Oct/2025:22:45:11 +0300] "GET / HTTP/1.1" 200 1189 "-" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Mobile Safari/537.36 (compatible; Google-Read-Aloud; +https://support.google.com/webmasters/answer/1061943)" 1645
192.168.0.132 - - [15/Oct/2025:22:45:12 +0300] "GET / HTTP/1.1" 200 1189 "-" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Mobile Safari/537.36 (compatible; Google-Read-Aloud; +https://support.google.com/webmasters/answer/1061943)" 1632
192.168.0.132 - - [15/Oct/2025:22:45:12 +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/138.0.0.0 Mobile Safari/537.36 (compatible; Google-Read-Aloud; +https://support.google.com/webmasters/answer/1061943)" 2022
192.168.0.132 - - [15/Oct/2025:22:45:12 +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/138.0.0.0 Mobile Safari/537.36 (compatible; Google-Read-Aloud; +https://support.google.com/webmasters/answer/1061943)" 1980
192.168.0.132 - - [15/Oct/2025:22:45:12 +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/138.0.0.0 Mobile Safari/537.36 (compatible; Google-Read-Aloud; +https://support.google.com/webmasters/answer/1061943)" 2032
192.168.0.132 - - [15/Oct/2025:22:45:12 +0300] "GET / HTTP/1.1" 200 1189 "-" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Mobile Safari/537.36 (compatible; Google-Read-Aloud; +https://support.google.com/webmasters/answer/1061943)" 1610
192.168.0.132 - - [15/Oct/2025:22:45:13 +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/138.0.0.0 Mobile Safari/537.36 (compatible; Google-Read-Aloud; +https://support.google.com/webmasters/answer/1061943)" 2004
192.168.0.132 - - [15/Oct/2025:22:45:13 +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/138.0.0.0 Mobile Safari/537.36 (compatible; Google-Read-Aloud; +https://support.google.com/webmasters/answer/1061943)" 1969
192.168.0.132 - - [15/Oct/2025:22:45:13 +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/138.0.0.0 Mobile Safari/537.36 (compatible; Google-Read-Aloud; +https://support.google.com/webmasters/answer/1061943)" 1959
192.168.0.132 - - [15/Oct/2025:22:45:13 +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/138.0.0.0 Mobile Safari/537.36 (compatible; Google-Read-Aloud; +https://support.google.com/webmasters/answer/1061943)" 1970
192.168.0.132 - - [15/Oct/2025:22:45:39 +0300] "POST /delete_user HTTP/1.1" 302 205 "https://quality.moto-adv.com/settings" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36" 8755
192.168.0.132 - - [15/Oct/2025:22:45:39 +0300] "GET /settings HTTP/1.1" 200 8603 "https://quality.moto-adv.com/settings" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36" 26163
192.168.0.132 - - [15/Oct/2025:22:46:05 +0300] "POST /create_user HTTP/1.1" 302 205 "https://quality.moto-adv.com/settings" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36" 9269
192.168.0.132 - - [15/Oct/2025:22:46:05 +0300] "GET /settings HTTP/1.1" 200 9120 "https://quality.moto-adv.com/settings" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36" 27376
192.168.0.132 - - [15/Oct/2025:22:46:16 +0300] "GET /dashboard HTTP/1.1" 200 2527 "https://quality.moto-adv.com/settings" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36" 2442
192.168.0.132 - - [15/Oct/2025:22:46:17 +0300] "GET /quality HTTP/1.1" 200 8292 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36" 10378
192.168.0.132 - - [15/Oct/2025:22:46:20 +0300] "GET /get_report_data?report=1 HTTP/1.1" 200 151 "https://quality.moto-adv.com/quality" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36" 5109
192.168.0.132 - - [15/Oct/2025:22:46:37 +0300] "GET /generate_report?report=6&date=2025-10-15 HTTP/1.1" 200 260 "https://quality.moto-adv.com/quality" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36" 6128
192.168.0.132 - - [15/Oct/2025:22:46:50 +0300] "GET /get_report_data?report=5 HTTP/1.1" 200 318 "https://quality.moto-adv.com/quality" "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Mobile Safari/537.36" 4085
192.168.0.132 - - [16/Oct/2025:00:04:19 +0300] "GET /settings HTTP/1.1" 302 207 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 1978
192.168.0.132 - - [16/Oct/2025:00:04:19 +0300] "GET /dashboard HTTP/1.1" 302 189 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 2113
192.168.0.132 - - [16/Oct/2025:00:04:19 +0300] "GET / HTTP/1.1" 200 1189 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 1970
192.168.0.132 - - [16/Oct/2025:00:04:19 +0300] "GET /favicon.ico HTTP/1.1" 404 207 "https://quality.moto-adv.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 1712
192.168.0.132 - - [16/Oct/2025:00:04:24 +0300] "POST / HTTP/1.1" 302 207 "https://quality.moto-adv.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 5754
192.168.0.132 - - [16/Oct/2025:00:04:24 +0300] "GET /dashboard HTTP/1.1" 200 2524 "https://quality.moto-adv.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 9594
192.168.0.132 - - [16/Oct/2025:00:04:24 +0300] "GET /static/css/base.css HTTP/1.1" 304 0 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 2385
192.168.0.132 - - [16/Oct/2025:00:04:24 +0300] "GET /static/script.js HTTP/1.1" 304 0 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 2150
192.168.0.132 - - [16/Oct/2025:00:04:24 +0300] "GET /static/scan_me.jpg HTTP/1.1" 304 0 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 2539
192.168.0.132 - - [16/Oct/2025:00:04:24 +0300] "GET /static/style.css HTTP/1.1" 304 0 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 2473
192.168.0.132 - - [16/Oct/2025:00:04:28 +0300] "GET /main_scan HTTP/1.1" 200 1978 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 6850
192.168.0.132 - - [16/Oct/2025:00:04:29 +0300] "GET /fg_scan HTTP/1.1" 200 29225 "https://quality.moto-adv.com/main_scan" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 5666
192.168.0.132 - - [16/Oct/2025:00:04:30 +0300] "GET /static/css/scan.css HTTP/1.1" 304 0 "https://quality.moto-adv.com/fg_scan" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 2455
192.168.0.132 - - [16/Oct/2025:00:05:32 +0300] "GET /dashboard HTTP/1.1" 200 2524 "https://quality.moto-adv.com/fg_scan" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 2311
192.168.0.132 - - [16/Oct/2025:00:05:34 +0300] "GET /quality HTTP/1.1" 200 8289 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 10335
192.168.0.132 - - [16/Oct/2025:00:05:36 +0300] "GET /get_report_data?report=1 HTTP/1.1" 200 151 "https://quality.moto-adv.com/quality" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 4252

View File

@@ -1,116 +0,0 @@
[2025-10-15 00:30:05 +0300] [284017] [INFO] Starting gunicorn 23.0.0
[2025-10-15 00:30:05 +0300] [284017] [INFO] Listening at: http://0.0.0.0:8781 (284017)
[2025-10-15 00:30:05 +0300] [284017] [INFO] Using worker: sync
[2025-10-15 00:30:05 +0300] [284017] [INFO] Trasabilitate Application server is ready. Listening on: [('0.0.0.0', 8781)]
[2025-10-15 00:30:05 +0300] [284017] [INFO] Worker spawned (pid: [booting])
[2025-10-15 00:30:05 +0300] [284040] [INFO] Booting worker with pid: 284040
[2025-10-15 00:30:05 +0300] [284040] [INFO] Worker spawned (pid: 284040)
[2025-10-15 00:30:05 +0300] [284017] [INFO] Worker spawned (pid: [booting])
[2025-10-15 00:30:05 +0300] [284041] [INFO] Booting worker with pid: 284041
[2025-10-15 00:30:05 +0300] [284041] [INFO] Worker spawned (pid: 284041)
[2025-10-15 00:30:05 +0300] [284017] [INFO] Worker spawned (pid: [booting])
[2025-10-15 00:30:05 +0300] [284042] [INFO] Booting worker with pid: 284042
[2025-10-15 00:30:05 +0300] [284042] [INFO] Worker spawned (pid: 284042)
[2025-10-15 00:30:05 +0300] [284017] [INFO] Worker spawned (pid: [booting])
[2025-10-15 00:30:05 +0300] [284043] [INFO] Booting worker with pid: 284043
[2025-10-15 00:30:05 +0300] [284043] [INFO] Worker spawned (pid: 284043)
[2025-10-15 00:30:06 +0300] [284017] [INFO] Worker spawned (pid: [booting])
[2025-10-15 00:30:06 +0300] [284044] [INFO] Booting worker with pid: 284044
[2025-10-15 00:30:06 +0300] [284044] [INFO] Worker spawned (pid: 284044)
[2025-10-15 00:30:06 +0300] [284017] [INFO] Worker spawned (pid: [booting])
[2025-10-15 00:30:06 +0300] [284047] [INFO] Booting worker with pid: 284047
[2025-10-15 00:30:06 +0300] [284047] [INFO] Worker spawned (pid: 284047)
[2025-10-15 00:30:06 +0300] [284017] [INFO] Worker spawned (pid: [booting])
[2025-10-15 00:30:06 +0300] [284052] [INFO] Booting worker with pid: 284052
[2025-10-15 00:30:06 +0300] [284052] [INFO] Worker spawned (pid: 284052)
[2025-10-15 00:30:06 +0300] [284017] [INFO] Worker spawned (pid: [booting])
[2025-10-15 00:30:06 +0300] [284053] [INFO] Booting worker with pid: 284053
[2025-10-15 00:30:06 +0300] [284053] [INFO] Worker spawned (pid: 284053)
[2025-10-15 00:30:06 +0300] [284017] [INFO] Worker spawned (pid: [booting])
[2025-10-15 00:30:06 +0300] [284054] [INFO] Booting worker with pid: 284054
[2025-10-15 00:30:06 +0300] [284054] [INFO] Worker spawned (pid: 284054)
[2025-10-15 01:04:40 +0300] [284041] [INFO] Worker exiting (pid: 284041)
[2025-10-15 01:04:40 +0300] [284040] [INFO] Worker exiting (pid: 284040)
[2025-10-15 01:04:40 +0300] [284054] [INFO] Worker exiting (pid: 284054)
[2025-10-15 01:04:40 +0300] [284017] [INFO] Handling signal: term
[2025-10-15 01:04:40 +0300] [284053] [INFO] Worker exiting (pid: 284053)
[2025-10-15 01:04:40 +0300] [284042] [INFO] Worker exiting (pid: 284042)
[2025-10-15 01:04:40 +0300] [284047] [INFO] Worker exiting (pid: 284047)
[2025-10-15 01:04:40 +0300] [284043] [INFO] Worker exiting (pid: 284043)
[2025-10-15 01:04:40 +0300] [284044] [INFO] Worker exiting (pid: 284044)
[2025-10-15 01:04:40 +0300] [284052] [INFO] Worker exiting (pid: 284052)
[2025-10-15 01:04:40 +0300] [284017] [ERROR] Worker (pid:284041) was sent SIGTERM!
[2025-10-15 01:04:41 +0300] [284017] [INFO] Shutting down: Master
[2025-10-15 01:28:55 +0300] [288316] [INFO] Starting gunicorn 23.0.0
[2025-10-15 01:28:55 +0300] [288316] [INFO] Listening at: http://0.0.0.0:8781 (288316)
[2025-10-15 01:28:55 +0300] [288316] [INFO] Using worker: sync
[2025-10-15 01:28:55 +0300] [288316] [INFO] Trasabilitate Application server is ready. Listening on: [('0.0.0.0', 8781)]
[2025-10-15 01:28:55 +0300] [288316] [INFO] Worker spawned (pid: [booting])
[2025-10-15 01:28:55 +0300] [288317] [INFO] Booting worker with pid: 288317
[2025-10-15 01:28:55 +0300] [288317] [INFO] Worker spawned (pid: 288317)
[2025-10-15 01:28:55 +0300] [288316] [INFO] Worker spawned (pid: [booting])
[2025-10-15 01:28:55 +0300] [288318] [INFO] Booting worker with pid: 288318
[2025-10-15 01:28:55 +0300] [288318] [INFO] Worker spawned (pid: 288318)
[2025-10-15 01:28:55 +0300] [288316] [INFO] Worker spawned (pid: [booting])
[2025-10-15 01:28:55 +0300] [288319] [INFO] Booting worker with pid: 288319
[2025-10-15 01:28:55 +0300] [288319] [INFO] Worker spawned (pid: 288319)
[2025-10-15 01:28:55 +0300] [288316] [INFO] Worker spawned (pid: [booting])
[2025-10-15 01:28:55 +0300] [288320] [INFO] Booting worker with pid: 288320
[2025-10-15 01:28:55 +0300] [288320] [INFO] Worker spawned (pid: 288320)
[2025-10-15 01:28:55 +0300] [288316] [INFO] Worker spawned (pid: [booting])
[2025-10-15 01:28:55 +0300] [288321] [INFO] Booting worker with pid: 288321
[2025-10-15 01:28:55 +0300] [288321] [INFO] Worker spawned (pid: 288321)
[2025-10-15 01:28:55 +0300] [288316] [INFO] Worker spawned (pid: [booting])
[2025-10-15 01:28:55 +0300] [288322] [INFO] Booting worker with pid: 288322
[2025-10-15 01:28:55 +0300] [288322] [INFO] Worker spawned (pid: 288322)
[2025-10-15 01:28:55 +0300] [288316] [INFO] Worker spawned (pid: [booting])
[2025-10-15 01:28:55 +0300] [288323] [INFO] Booting worker with pid: 288323
[2025-10-15 01:28:55 +0300] [288323] [INFO] Worker spawned (pid: 288323)
[2025-10-15 01:28:55 +0300] [288316] [INFO] Worker spawned (pid: [booting])
[2025-10-15 01:28:55 +0300] [288324] [INFO] Booting worker with pid: 288324
[2025-10-15 01:28:55 +0300] [288324] [INFO] Worker spawned (pid: 288324)
[2025-10-15 01:28:55 +0300] [288316] [INFO] Worker spawned (pid: [booting])
[2025-10-15 01:28:55 +0300] [288325] [INFO] Booting worker with pid: 288325
[2025-10-15 01:28:55 +0300] [288325] [INFO] Worker spawned (pid: 288325)
[2025-10-16 00:06:02 +0300] [288316] [INFO] Handling signal: term
[2025-10-16 00:06:02 +0300] [288317] [INFO] Worker exiting (pid: 288317)
[2025-10-16 00:06:02 +0300] [288318] [INFO] Worker exiting (pid: 288318)
[2025-10-16 00:06:02 +0300] [288319] [INFO] Worker exiting (pid: 288319)
[2025-10-16 00:06:02 +0300] [288320] [INFO] Worker exiting (pid: 288320)
[2025-10-16 00:06:02 +0300] [288322] [INFO] Worker exiting (pid: 288322)
[2025-10-16 00:06:02 +0300] [288321] [INFO] Worker exiting (pid: 288321)
[2025-10-16 00:06:02 +0300] [288323] [INFO] Worker exiting (pid: 288323)
[2025-10-16 00:06:02 +0300] [288324] [INFO] Worker exiting (pid: 288324)
[2025-10-16 00:06:02 +0300] [288325] [INFO] Worker exiting (pid: 288325)
[2025-10-16 00:06:03 +0300] [288316] [INFO] Shutting down: Master
[2025-10-16 02:34:31 +0300] [299414] [INFO] Starting gunicorn 23.0.0
[2025-10-16 02:34:31 +0300] [299414] [INFO] Listening at: http://0.0.0.0:8781 (299414)
[2025-10-16 02:34:31 +0300] [299414] [INFO] Using worker: sync
[2025-10-16 02:34:31 +0300] [299414] [INFO] Trasabilitate Application server is ready. Listening on: [('0.0.0.0', 8781)]
[2025-10-16 02:34:31 +0300] [299414] [INFO] Worker spawned (pid: [booting])
[2025-10-16 02:34:31 +0300] [299432] [INFO] Booting worker with pid: 299432
[2025-10-16 02:34:31 +0300] [299432] [INFO] Worker spawned (pid: 299432)
[2025-10-16 02:34:31 +0300] [299414] [INFO] Worker spawned (pid: [booting])
[2025-10-16 02:34:31 +0300] [299438] [INFO] Booting worker with pid: 299438
[2025-10-16 02:34:31 +0300] [299438] [INFO] Worker spawned (pid: 299438)
[2025-10-16 02:34:32 +0300] [299414] [INFO] Worker spawned (pid: [booting])
[2025-10-16 02:34:32 +0300] [299439] [INFO] Booting worker with pid: 299439
[2025-10-16 02:34:32 +0300] [299439] [INFO] Worker spawned (pid: 299439)
[2025-10-16 02:34:32 +0300] [299414] [INFO] Worker spawned (pid: [booting])
[2025-10-16 02:34:32 +0300] [299440] [INFO] Booting worker with pid: 299440
[2025-10-16 02:34:32 +0300] [299440] [INFO] Worker spawned (pid: 299440)
[2025-10-16 02:34:32 +0300] [299414] [INFO] Worker spawned (pid: [booting])
[2025-10-16 02:34:32 +0300] [299441] [INFO] Booting worker with pid: 299441
[2025-10-16 02:34:32 +0300] [299441] [INFO] Worker spawned (pid: 299441)
[2025-10-16 02:34:32 +0300] [299414] [INFO] Worker spawned (pid: [booting])
[2025-10-16 02:34:32 +0300] [299442] [INFO] Booting worker with pid: 299442
[2025-10-16 02:34:32 +0300] [299442] [INFO] Worker spawned (pid: 299442)
[2025-10-16 02:34:32 +0300] [299414] [INFO] Worker spawned (pid: [booting])
[2025-10-16 02:34:32 +0300] [299443] [INFO] Booting worker with pid: 299443
[2025-10-16 02:34:32 +0300] [299443] [INFO] Worker spawned (pid: 299443)
[2025-10-16 02:34:32 +0300] [299414] [INFO] Worker spawned (pid: [booting])
[2025-10-16 02:34:32 +0300] [299444] [INFO] Booting worker with pid: 299444
[2025-10-16 02:34:32 +0300] [299444] [INFO] Worker spawned (pid: 299444)
[2025-10-16 02:34:32 +0300] [299414] [INFO] Worker spawned (pid: [booting])
[2025-10-16 02:34:32 +0300] [299445] [INFO] Booting worker with pid: 299445
[2025-10-16 02:34:32 +0300] [299445] [INFO] Worker spawned (pid: 299445)

View File

@@ -1,133 +0,0 @@
# Quick Database Setup for Trasabilitate Application
This script provides a complete one-step database setup for quick deployment of the Trasabilitate application.
## Prerequisites
Before running the setup script, ensure:
1. **MariaDB is installed and running**
2. **Database and user are created**:
```sql
CREATE DATABASE trasabilitate;
CREATE USER 'trasabilitate'@'localhost' IDENTIFIED BY 'Initial01!';
GRANT ALL PRIVILEGES ON trasabilitate.* TO 'trasabilitate'@'localhost';
FLUSH PRIVILEGES;
```
3. **Python virtual environment is activated**:
```bash
source ../recticel/bin/activate
```
4. **Python dependencies are installed**:
```bash
pip install -r requirements.txt
```
## Usage
### Quick Setup (Recommended)
```bash
cd /srv/quality_recticel/py_app
source ../recticel/bin/activate
python3 app/db_create_scripts/setup_complete_database.py
```
### What the script creates:
#### MariaDB Tables:
- `scan1_orders` - Quality scanning data for process 1
- `scanfg_orders` - Quality scanning data for finished goods
- `order_for_labels` - Label printing orders
- `warehouse_locations` - Warehouse location management
- `permissions` - System permissions
- `role_permissions` - Role-permission mappings
- `role_hierarchy` - User role hierarchy
- `permission_audit_log` - Permission change audit trail
#### Database Triggers:
- Auto-increment approved/rejected quantities based on quality codes
- Triggers for both scan1_orders and scanfg_orders tables
#### SQLite Tables:
- `users` - User authentication (in instance/users.db)
- `roles` - User roles (in instance/users.db)
#### Configuration:
- Updates `instance/external_server.conf` with correct database settings
- Creates default superadmin user (username: `superadmin`, password: `superadmin123`)
#### Permission System:
- 7 user roles (superadmin, admin, manager, quality_manager, warehouse_manager, quality_worker, warehouse_worker)
- 25+ granular permissions for different application areas
- Complete role hierarchy with inheritance
## After Setup
1. **Start the application**:
```bash
python3 run.py
```
2. **Access the application**:
- Local: http://127.0.0.1:8781
- Network: http://192.168.0.205:8781
3. **Login with superadmin**:
- Username: `superadmin`
- Password: `superadmin123`
## Troubleshooting
### Common Issues:
1. **Database connection failed**:
- Check if MariaDB is running: `sudo systemctl status mariadb`
- Verify database exists: `sudo mysql -e "SHOW DATABASES;"`
- Check user privileges: `sudo mysql -e "SHOW GRANTS FOR 'trasabilitate'@'localhost';"`
2. **Import errors**:
- Ensure virtual environment is activated
- Install missing dependencies: `pip install -r requirements.txt`
3. **Permission denied**:
- Make script executable: `chmod +x app/db_create_scripts/setup_complete_database.py`
- Check file ownership: `ls -la app/db_create_scripts/`
### Manual Database Recreation:
If you need to completely reset the database:
```bash
# Drop and recreate database
sudo mysql -e "DROP DATABASE IF EXISTS trasabilitate; CREATE DATABASE trasabilitate; GRANT ALL PRIVILEGES ON trasabilitate.* TO 'trasabilitate'@'localhost'; FLUSH PRIVILEGES;"
# Remove SQLite database
rm -f instance/users.db
# Run setup script
python3 app/db_create_scripts/setup_complete_database.py
```
## Script Features
- ✅ **Comprehensive**: Creates all necessary database structure
- ✅ **Safe**: Uses `IF NOT EXISTS` clauses to prevent conflicts
- ✅ **Verified**: Includes verification step to confirm setup
- ✅ **Informative**: Detailed output showing each step
- ✅ **Error handling**: Clear error messages and troubleshooting hints
- ✅ **Idempotent**: Can be run multiple times safely
## Development Notes
The script combines functionality from these individual scripts:
- `create_scan_1db.py`
- `create_scanfg_orders.py`
- `create_order_for_labels_table.py`
- `create_warehouse_locations_table.py`
- `create_permissions_tables.py`
- `create_roles_table.py`
- `create_triggers.py`
- `create_triggers_fg.py`
- `populate_permissions.py`
For development or debugging, you can still run individual scripts if needed.

View File

@@ -1,319 +0,0 @@
# Recticel Quality Application - Docker Deployment Guide
## 📋 Overview
This is a complete Docker-based deployment solution for the Recticel Quality Application. It includes:
- **Flask Web Application** (Python 3.10)
- **MariaDB 11.3 Database** with automatic initialization
- **Gunicorn WSGI Server** for production-ready performance
- **Automatic database schema setup** using existing setup scripts
- **Superadmin user seeding** for immediate access
## 🚀 Quick Start
### Prerequisites
- Docker Engine 20.10+
- Docker Compose 2.0+
- At least 2GB free disk space
- Ports 8781 and 3306 available (or customize in .env)
### 1. Clone and Prepare
```bash
cd /srv/quality_recticel
```
### 2. Configure Environment (Optional)
Create a `.env` file from the example:
```bash
cp .env.example .env
```
Edit `.env` to customize settings:
```env
MYSQL_ROOT_PASSWORD=your_secure_root_password
DB_PORT=3306
APP_PORT=8781
INIT_DB=true
SEED_DB=true
```
### 3. Build and Deploy
Start all services:
```bash
docker-compose up -d --build
```
This will:
1. ✅ Build the Flask application Docker image
2. ✅ Pull MariaDB 11.3 image
3. ✅ Create and initialize the database
4. ✅ Run all database schema creation scripts
5. ✅ Seed the superadmin user
6. ✅ Start the web application on port 8781
### 4. Verify Deployment
Check service status:
```bash
docker-compose ps
```
View logs:
```bash
# All services
docker-compose logs -f
# Just the web app
docker-compose logs -f web
# Just the database
docker-compose logs -f db
```
### 5. Access the Application
Open your browser and navigate to:
```
http://localhost:8781
```
**Default Login:**
- Username: `superadmin`
- Password: `superadmin123`
## 🔧 Management Commands
### Start Services
```bash
docker-compose up -d
```
### Stop Services
```bash
docker-compose down
```
### Stop and Remove All Data (including database)
```bash
docker-compose down -v
```
### Restart Services
```bash
docker-compose restart
```
### View Real-time Logs
```bash
docker-compose logs -f
```
### Rebuild After Code Changes
```bash
docker-compose up -d --build
```
### Access Database Console
```bash
docker-compose exec db mariadb -u trasabilitate -p trasabilitate
# Password: Initial01!
```
### Execute Commands in App Container
```bash
docker-compose exec web bash
```
## 📁 Data Persistence
The following data is persisted across container restarts:
- **Database Data:** Stored in Docker volume `mariadb_data`
- **Application Logs:** Mapped to `./logs` directory
- **Instance Config:** Mapped to `./instance` directory
## 🔐 Security Considerations
### Production Deployment Checklist:
1. **Change Default Passwords:**
- Update `MYSQL_ROOT_PASSWORD` in `.env`
- Update database password in `docker-compose.yml`
- Change superadmin password after first login
2. **Use Environment Variables:**
- Never commit `.env` file to version control
- Use secrets management for production
3. **Network Security:**
- If database access from host is not needed, remove the port mapping:
```yaml
# Comment out in docker-compose.yml:
# ports:
# - "3306:3306"
```
4. **SSL/TLS:**
- Configure reverse proxy (nginx/traefik) for HTTPS
- Update gunicorn SSL configuration if needed
5. **Firewall:**
- Only expose necessary ports
- Use firewall rules to restrict access
## 🐛 Troubleshooting
### Database Connection Issues
If the app can't connect to the database:
```bash
# Check database health
docker-compose exec db healthcheck.sh --connect
# Check database logs
docker-compose logs db
# Verify database is accessible
docker-compose exec db mariadb -u trasabilitate -p -e "SHOW DATABASES;"
```
### Application Not Starting
```bash
# Check application logs
docker-compose logs web
# Verify database initialization
docker-compose exec web python3 -c "import mariadb; print('MariaDB module OK')"
# Restart with fresh initialization
docker-compose down
docker-compose up -d
```
### Port Already in Use
If port 8781 or 3306 is already in use, edit `.env`:
```env
APP_PORT=8782
DB_PORT=3307
```
Then restart:
```bash
docker-compose down
docker-compose up -d
```
### Reset Everything
To start completely fresh:
```bash
# Stop and remove all containers, networks, and volumes
docker-compose down -v
# Remove any local data
rm -rf logs/* instance/external_server.conf
# Start fresh
docker-compose up -d --build
```
## 🔄 Updating the Application
### Update Application Code
1. Make your code changes
2. Rebuild and restart:
```bash
docker-compose up -d --build web
```
### Update Database Schema
If you need to run migrations or schema updates:
```bash
docker-compose exec web python3 /app/app/db_create_scripts/setup_complete_database.py
```
## 📊 Monitoring
### Health Checks
Both services have health checks configured:
```bash
# Check overall status
docker-compose ps
# Detailed health status
docker inspect recticel-app | grep -A 10 Health
docker inspect recticel-db | grep -A 10 Health
```
### Resource Usage
```bash
# View resource consumption
docker stats recticel-app recticel-db
```
## 🏗️ Architecture
```
┌─────────────────────────────────────┐
│ Docker Compose Network │
│ │
│ ┌──────────────┐ ┌─────────────┐ │
│ │ MariaDB │ │ Flask App │ │
│ │ Container │◄─┤ Container │ │
│ │ │ │ │ │
│ │ Port: 3306 │ │ Port: 8781 │ │
│ └──────┬───────┘ └──────┬──────┘ │
│ │ │ │
└─────────┼─────────────────┼─────────┘
│ │
▼ ▼
[Volume: [Logs &
mariadb_data] Instance]
```
## 📝 Environment Variables
### Database Configuration
- `MYSQL_ROOT_PASSWORD`: MariaDB root password
- `DB_HOST`: Database hostname (default: `db`)
- `DB_PORT`: Database port (default: `3306`)
- `DB_NAME`: Database name (default: `trasabilitate`)
- `DB_USER`: Database user (default: `trasabilitate`)
- `DB_PASSWORD`: Database password (default: `Initial01!`)
### Application Configuration
- `FLASK_ENV`: Flask environment (default: `production`)
- `FLASK_APP`: Flask app entry point (default: `run.py`)
- `APP_PORT`: Application port (default: `8781`)
### Initialization Flags
- `INIT_DB`: Run database initialization (default: `true`)
- `SEED_DB`: Seed superadmin user (default: `true`)
## 🆘 Support
For issues or questions:
1. Check the logs: `docker-compose logs -f`
2. Verify environment configuration
3. Ensure all prerequisites are met
4. Review this documentation
## 📄 License
[Your License Here]

View File

@@ -1,346 +0,0 @@
# Recticel Quality Application - Docker Solution Summary
## 📦 What Has Been Created
A complete, production-ready Docker deployment solution for your Recticel Quality Application with the following components:
### Core Files Created
1. **`Dockerfile`** - Multi-stage Flask application container
- Based on Python 3.10-slim
- Installs all dependencies from requirements.txt
- Configures Gunicorn WSGI server
- Exposes port 8781
2. **`docker-compose.yml`** - Complete orchestration configuration
- MariaDB 11.3 database service
- Flask web application service
- Automatic networking between services
- Health checks for both services
- Volume persistence for database and logs
3. **`docker-entrypoint.sh`** - Smart initialization script
- Waits for database to be ready
- Creates database configuration file
- Runs database schema initialization
- Seeds superadmin user
- Starts the application
4. **`init-db.sql`** - MariaDB initialization
- Creates database and user
- Sets up permissions automatically
5. **`.env.example`** - Configuration template
- Database passwords
- Port configurations
- Initialization flags
6. **`.dockerignore`** - Build optimization
- Excludes unnecessary files from Docker image
- Reduces image size
7. **`deploy.sh`** - One-command deployment script
- Checks prerequisites
- Creates configuration
- Builds and starts services
- Shows deployment status
8. **`Makefile`** - Convenient management commands
- `make install` - First-time installation
- `make up` - Start services
- `make down` - Stop services
- `make logs` - View logs
- `make shell` - Access container
- `make backup-db` - Backup database
- And many more...
9. **`DOCKER_DEPLOYMENT.md`** - Complete documentation
- Quick start guide
- Management commands
- Troubleshooting
- Security considerations
- Architecture diagrams
### Enhanced Files
10. **`setup_complete_database.py`** - Updated to support Docker
- Now reads from environment variables
- Fallback to config file for non-Docker deployments
- Maintains backward compatibility
## 🎯 Key Features
### 1. Single-Command Deployment
```bash
./deploy.sh
```
This single command will:
- ✅ Build Docker images
- ✅ Create MariaDB database
- ✅ Initialize all database tables and triggers
- ✅ Seed superadmin user
- ✅ Start the application
### 2. Complete Isolation
- Application runs in its own container
- Database runs in its own container
- No system dependencies needed except Docker
- No Python/MariaDB installation on host required
### 3. Data Persistence
- Database data persists across restarts (Docker volume)
- Application logs accessible on host
- Configuration preserved
### 4. Production Ready
- Gunicorn WSGI server (not Flask dev server)
- Health checks for monitoring
- Automatic restart on failure
- Proper logging configuration
- Resource isolation
### 5. Easy Management
```bash
# Start
docker compose up -d
# Stop
docker compose down
# View logs
docker compose logs -f
# Backup database
make backup-db
# Restore database
make restore-db BACKUP=backup_20231215.sql
# Access shell
make shell
# Complete reset
make reset
```
## 🚀 Deployment Options
### Option 1: Quick Deploy (Recommended for Testing)
```bash
cd /srv/quality_recticel
./deploy.sh
```
### Option 2: Using Makefile (Recommended for Management)
```bash
cd /srv/quality_recticel
make install # First time only
make up # Start services
make logs # Monitor
```
### Option 3: Using Docker Compose Directly
```bash
cd /srv/quality_recticel
cp .env.example .env
docker compose up -d --build
```
## 📋 Prerequisites
The deployment **requires** Docker to be installed on the target system:
### Installing Docker on Ubuntu/Debian:
```bash
# Update package index
sudo apt-get update
# Install dependencies
sudo apt-get install -y ca-certificates curl gnupg
# Add Docker's official GPG key
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
# Set up the repository
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Install Docker Engine
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# Add current user to docker group (optional, to run without sudo)
sudo usermod -aG docker $USER
```
After installation, log out and back in for group changes to take effect.
### Installing Docker on CentOS/RHEL:
```bash
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo yum install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl start docker
sudo systemctl enable docker
sudo usermod -aG docker $USER
```
## 🏗️ Architecture
```
┌──────────────────────────────────────────────────────┐
│ Docker Compose Stack │
│ │
│ ┌────────────────────┐ ┌───────────────────┐ │
│ │ MariaDB 11.3 │ │ Flask App │ │
│ │ Container │◄─────┤ Container │ │
│ │ │ │ │ │
│ │ - Port: 3306 │ │ - Port: 8781 │ │
│ │ - Volume: DB Data │ │ - Gunicorn WSGI │ │
│ │ - Auto Init │ │ - Python 3.10 │ │
│ │ - Health Checks │ │ - Health Checks │ │
│ └──────────┬─────────┘ └─────────┬─────────┘ │
│ │ │ │
└─────────────┼──────────────────────────┼─────────────┘
│ │
▼ ▼
[mariadb_data] [logs directory]
Docker Volume Host filesystem
```
## 🔐 Security Features
1. **Database Isolation**: Database not exposed to host by default (can be configured)
2. **Password Management**: All passwords in `.env` file (not committed to git)
3. **User Permissions**: Proper MariaDB user with limited privileges
4. **Network Isolation**: Services communicate on private Docker network
5. **Production Mode**: Flask runs in production mode with Gunicorn
## 📊 What Gets Deployed
### Database Schema
All tables from `setup_complete_database.py`:
- `scan1_orders` - First scan orders
- `scanfg_orders` - Final goods scan orders
- `order_for_labels` - Label orders
- `warehouse_locations` - Warehouse locations
- `permissions` - Permission system
- `role_permissions` - Role-based access
- `role_hierarchy` - Role hierarchy
- `permission_audit_log` - Audit logging
- Plus SQLAlchemy tables: `users`, `roles`
### Initial Data
- Superadmin user: `superadmin` / `superadmin123`
### Application Features
- Complete Flask web application
- Gunicorn WSGI server (4-8 workers depending on CPU)
- Static file serving
- Session management
- Database connection pooling
## 🔄 Migration from Existing Deployment
If you have an existing non-Docker deployment:
### 1. Backup Current Data
```bash
# Backup database
mysqldump -u trasabilitate -p trasabilitate > backup.sql
# Backup any uploaded files or custom data
cp -r py_app/instance backup_instance/
```
### 2. Deploy Docker Solution
```bash
cd /srv/quality_recticel
./deploy.sh
```
### 3. Restore Data (if needed)
```bash
# Restore database
docker compose exec -T db mariadb -u trasabilitate -pInitial01! trasabilitate < backup.sql
```
### 4. Stop Old Service
```bash
# Stop systemd service
sudo systemctl stop trasabilitate
sudo systemctl disable trasabilitate
```
## 🎓 Learning Resources
- Docker Compose docs: https://docs.docker.com/compose/
- Gunicorn configuration: https://docs.gunicorn.org/
- MariaDB Docker: https://hub.docker.com/_/mariadb
## ✅ Testing Checklist
After deployment, verify:
- [ ] Services are running: `docker compose ps`
- [ ] App is accessible: http://localhost:8781
- [ ] Can log in with superadmin
- [ ] Database contains tables: `make shell-db` then `SHOW TABLES;`
- [ ] Logs are being written: `ls -la logs/`
- [ ] Can restart services: `docker compose restart`
- [ ] Data persists after restart
## 🆘 Support Commands
```bash
# View all services
docker compose ps
# View logs
docker compose logs -f
# Restart a specific service
docker compose restart web
# Access web container shell
docker compose exec web bash
# Access database
docker compose exec db mariadb -u trasabilitate -p
# Check resource usage
docker stats
# Remove everything and start fresh
docker compose down -v
./deploy.sh
```
## 📝 Next Steps
1. **Install Docker** on the target server (if not already installed)
2. **Review and customize** `.env` file after copying from `.env.example`
3. **Run deployment**: `./deploy.sh`
4. **Change default passwords** after first login
5. **Set up reverse proxy** (nginx/traefik) for HTTPS if needed
6. **Configure backups** using `make backup-db`
7. **Monitor logs** regularly with `make logs`
## 🎉 Benefits of This Solution
1. **Portable**: Works on any system with Docker
2. **Reproducible**: Same deployment every time
3. **Isolated**: No conflicts with system packages
4. **Easy Updates**: Just rebuild and restart
5. **Scalable**: Can easily add more services
6. **Professional**: Production-ready configuration
7. **Documented**: Complete documentation included
8. **Maintainable**: Simple management commands
---
**Your Flask application is now ready for modern, containerized deployment! 🚀**

View File

@@ -1,280 +0,0 @@
# ✅ Docker Solution - Files Created
## 📦 Complete Docker Deployment Package
Your Flask application has been packaged into a complete Docker solution. Here's everything that was created:
### Core Docker Files
```
/srv/quality_recticel/
├── Dockerfile # Flask app container definition
├── docker-compose.yml # Multi-container orchestration
├── docker-entrypoint.sh # Container initialization script
├── init-db.sql # MariaDB initialization
├── .dockerignore # Build optimization
└── .env.example # Configuration template
```
### Deployment & Management
```
├── deploy.sh # One-command deployment script
├── Makefile # Management commands (make up, make down, etc.)
├── README-DOCKER.md # Quick start guide
├── DOCKER_DEPLOYMENT.md # Complete deployment documentation
└── DOCKER_SOLUTION_SUMMARY.md # This comprehensive summary
```
### Modified Files
```
py_app/app/db_create_scripts/
└── setup_complete_database.py # Updated to support Docker env vars
```
## 🎯 What This Deployment Includes
### Services
1. **Flask Web Application**
- Python 3.10
- Gunicorn WSGI server (production-ready)
- Auto-generated database configuration
- Health checks
- Automatic restart on failure
2. **MariaDB 11.3 Database**
- Automatic initialization
- User and database creation
- Data persistence (Docker volume)
- Health checks
### Features
- ✅ Single-command deployment
- ✅ Automatic database schema setup
- ✅ Superadmin user seeding
- ✅ Data persistence across restarts
- ✅ Container health monitoring
- ✅ Log collection and management
- ✅ Production-ready configuration
- ✅ Easy backup and restore
- ✅ Complete isolation from host system
## 🚀 How to Deploy
### Prerequisites
**Install Docker first:**
```bash
# Ubuntu/Debian
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker $USER
# Log out and back in
```
### Deploy
```bash
cd /srv/quality_recticel
./deploy.sh
```
That's it! Your application will be available at http://localhost:8781
## 📋 Usage Examples
### Basic Operations
```bash
# Start services
docker compose up -d
# View logs
docker compose logs -f
# Stop services
docker compose down
# Restart
docker compose restart
# Check status
docker compose ps
```
### Using Makefile (Recommended)
```bash
make install # First-time setup
make up # Start services
make down # Stop services
make logs # View logs
make logs-web # View only web logs
make logs-db # View only database logs
make shell # Access app container
make shell-db # Access database console
make backup-db # Backup database
make status # Show service status
make help # Show all commands
```
### Advanced Operations
```bash
# Rebuild after code changes
docker compose up -d --build web
# Access application shell
docker compose exec web bash
# Run database commands
docker compose exec db mariadb -u trasabilitate -p trasabilitate
# View resource usage
docker stats recticel-app recticel-db
# Complete reset (removes all data!)
docker compose down -v
```
## 🗂️ Data Storage
### Persistent Data
- **Database**: Stored in Docker volume `mariadb_data`
- **Logs**: Mounted to `./logs` directory
- **Config**: Mounted to `./instance` directory
### Backup Database
```bash
docker compose exec -T db mariadb-dump -u trasabilitate -pInitial01! trasabilitate > backup.sql
```
### Restore Database
```bash
docker compose exec -T db mariadb -u trasabilitate -pInitial01! trasabilitate < backup.sql
```
## 🔐 Default Credentials
### Application
- URL: http://localhost:8781
- Username: `superadmin`
- Password: `superadmin123`
- **⚠️ Change after first login!**
### Database
- Host: `localhost:3306` (from host) or `db:3306` (from containers)
- Database: `trasabilitate`
- User: `trasabilitate`
- Password: `Initial01!`
- Root Password: Set in `.env` file
## 📊 Service Architecture
```
┌─────────────────────────────────────────────────────┐
│ recticel-network (Docker) │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ recticel-db │ │ recticel-app │ │
│ │ (MariaDB 11.3) │◄───────┤ (Flask/Python) │ │
│ │ │ │ │ │
│ │ - Internal DB │ │ - Gunicorn │ │
│ │ - Health Check │ │ - Health Check │ │
│ │ - Auto Init │ │ - Auto Config │ │
│ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │
│ │ 3306 (optional) 8781 │ │
└────────────┼──────────────────────────┼────────────┘
│ │
▼ ▼
[mariadb_data] [Host: 8781]
Docker Volume Application Access
```
## 🎓 Quick Reference
### Environment Variables (.env)
```env
MYSQL_ROOT_PASSWORD=rootpassword # MariaDB root password
DB_PORT=3306 # Database port (external)
APP_PORT=8781 # Application port
INIT_DB=true # Run DB initialization
SEED_DB=true # Seed superadmin user
```
### Important Ports
- `8781`: Flask application (web interface)
- `3306`: MariaDB database (optional external access)
### Log Locations
- Application logs: `./logs/access.log` and `./logs/error.log`
- Container logs: `docker compose logs`
## 🔧 Troubleshooting
### Can't connect to application
```bash
# Check if services are running
docker compose ps
# Check web logs
docker compose logs web
# Verify port not in use
netstat -tuln | grep 8781
```
### Database connection issues
```bash
# Check database health
docker compose exec db healthcheck.sh --connect
# View database logs
docker compose logs db
# Test database connection
docker compose exec web python3 -c "import mariadb; print('OK')"
```
### Port already in use
Edit `.env` file:
```env
APP_PORT=8782 # Change to available port
DB_PORT=3307 # Change if needed
```
### Start completely fresh
```bash
docker compose down -v
rm -rf logs/* instance/external_server.conf
./deploy.sh
```
## 📖 Documentation Files
1. **README-DOCKER.md** - Quick start guide (start here!)
2. **DOCKER_DEPLOYMENT.md** - Complete deployment guide
3. **DOCKER_SOLUTION_SUMMARY.md** - Comprehensive overview
4. **FILES_CREATED.md** - This file
## ✨ Benefits
- **No System Dependencies**: Only Docker required
- **Portable**: Deploy on any system with Docker
- **Reproducible**: Consistent deployments every time
- **Isolated**: No conflicts with other applications
- **Production-Ready**: Gunicorn, health checks, proper logging
- **Easy Management**: Simple commands, one-line deployment
- **Persistent**: Data survives container restarts
- **Scalable**: Easy to add more services
## 🎉 Success!
Your Recticel Quality Application is now containerized and ready for deployment!
**Next Steps:**
1. Install Docker (if not already installed)
2. Run `./deploy.sh`
3. Access http://localhost:8781
4. Log in with superadmin credentials
5. Change default passwords
6. Enjoy your containerized application!
For detailed instructions, see **README-DOCKER.md** or **DOCKER_DEPLOYMENT.md**.

View File

@@ -1,73 +0,0 @@
# 🚀 Quick Start - Docker Deployment
## What You Need
- A server with Docker installed
- 2GB free disk space
- Ports 8781 and 3306 available
## Deploy in 3 Steps
### 1⃣ Install Docker (if not already installed)
**Ubuntu/Debian:**
```bash
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker $USER
```
Then log out and back in.
### 2⃣ Deploy the Application
```bash
cd /srv/quality_recticel
./deploy.sh
```
### 3⃣ Access Your Application
Open browser: **http://localhost:8781**
**Login:**
- Username: `superadmin`
- Password: `superadmin123`
## 🎯 Done!
Your complete application with database is now running in Docker containers.
## Common Commands
```bash
# View logs
docker compose logs -f
# Stop services
docker compose down
# Restart services
docker compose restart
# Backup database
docker compose exec -T db mariadb-dump -u trasabilitate -pInitial01! trasabilitate > backup.sql
```
## 📚 Full Documentation
See `DOCKER_DEPLOYMENT.md` for complete documentation.
## 🆘 Problems?
```bash
# Check status
docker compose ps
# View detailed logs
docker compose logs -f web
# Start fresh
docker compose down -v
./deploy.sh
```
---
**Note:** This is a production-ready deployment using Gunicorn WSGI server, MariaDB 11.3, and proper health checks.

View File

@@ -1,121 +0,0 @@
#!/usr/bin/env python3
"""
Script to add modules column to external database and migrate existing users
"""
import os
import sys
import mariadb
def migrate_external_database():
"""Add modules column to external database and update existing users"""
try:
# Read external database configuration from instance folder
config_file = os.path.join(os.path.dirname(__file__), 'instance/external_server.conf')
if not os.path.exists(config_file):
print("External database configuration file not found at instance/external_server.conf")
return False
with open(config_file, 'r') as f:
lines = f.read().strip().split('\n')
# Parse the config file format "key=value"
config = {}
for line in lines:
if '=' in line and not line.strip().startswith('#'):
key, value = line.split('=', 1)
config[key.strip()] = value.strip()
host = config.get('server_domain', 'localhost')
port = int(config.get('port', '3306'))
database = config.get('database_name', '')
user = config.get('username', '')
password = config.get('password', '')
if not all([host, database, user, password]):
print("Missing required database configuration values.")
return False
print(f"Connecting to external database: {host}:{port}/{database}")
# Connect to external database
conn = mariadb.connect(
user=user,
password=password,
host=host,
port=port,
database=database
)
cursor = conn.cursor()
# Check if users table exists
cursor.execute("SHOW TABLES LIKE 'users'")
if not cursor.fetchone():
print("Users table not found in external database.")
conn.close()
return False
# Check if modules column already exists
cursor.execute("DESCRIBE users")
columns = [row[0] for row in cursor.fetchall()]
if 'modules' not in columns:
print("Adding modules column to users table...")
cursor.execute("ALTER TABLE users ADD COLUMN modules TEXT")
print("Modules column added successfully.")
else:
print("Modules column already exists.")
# Get current users and convert their roles
cursor.execute("SELECT id, username, role FROM users")
users = cursor.fetchall()
role_mapping = {
'superadmin': ('superadmin', None),
'administrator': ('admin', None),
'admin': ('admin', None),
'quality': ('manager', '["quality"]'),
'warehouse': ('manager', '["warehouse"]'),
'warehouse_manager': ('manager', '["warehouse"]'),
'scan': ('worker', '["quality"]'),
'etichete': ('manager', '["labels"]'),
'quality_manager': ('manager', '["quality"]'),
'quality_worker': ('worker', '["quality"]'),
}
print(f"Migrating {len(users)} users...")
for user_id, username, old_role in users:
if old_role in role_mapping:
new_role, modules_json = role_mapping[old_role]
cursor.execute("UPDATE users SET role = ?, modules = ? WHERE id = ?",
(new_role, modules_json, user_id))
print(f" {username}: {old_role} -> {new_role} with modules {modules_json}")
else:
print(f" {username}: Unknown role '{old_role}', keeping as-is")
conn.commit()
conn.close()
print("External database migration completed successfully!")
return True
except Exception as e:
print(f"Error migrating external database: {e}")
return False
if __name__ == "__main__":
print("External Database Migration for Simplified 4-Tier Permission System")
print("=" * 70)
success = migrate_external_database()
if success:
print("\n✅ Migration completed successfully!")
print("\nUsers can now log in with the new simplified permission system.")
print("Role structure: superadmin → admin → manager → worker")
print("Modules: quality, warehouse, labels")
else:
print("\n❌ Migration failed. Please check the error messages above.")

View File

@@ -1,172 +0,0 @@
#!/usr/bin/env python3
"""
Migration script to convert from complex permission system to simplified 4-tier system
This script will:
1. Add 'modules' column to users table
2. Convert existing roles to new 4-tier system
3. Assign appropriate modules based on old roles
"""
import sqlite3
import json
import os
import sys
# Add the app directory to Python path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
def get_db_connections():
"""Get both internal SQLite and external database connections"""
connections = {}
# Internal SQLite database
internal_db_path = os.path.join(os.path.dirname(__file__), 'instance/users.db')
if os.path.exists(internal_db_path):
connections['internal'] = sqlite3.connect(internal_db_path)
print(f"Connected to internal SQLite database: {internal_db_path}")
# External database (try to connect using existing method)
try:
import mariadb
# Read external database configuration
config_file = os.path.join(os.path.dirname(__file__), '../external_database_settings')
if os.path.exists(config_file):
with open(config_file, 'r') as f:
lines = f.read().strip().split('\n')
if len(lines) >= 5:
host = lines[0].strip()
port = int(lines[1].strip())
database = lines[2].strip()
user = lines[3].strip()
password = lines[4].strip()
conn = mariadb.connect(
user=user,
password=password,
host=host,
port=port,
database=database
)
connections['external'] = conn
print(f"Connected to external MariaDB database: {host}:{port}/{database}")
except Exception as e:
print(f"Could not connect to external database: {e}")
return connections
def role_mapping():
"""Map old roles to new 4-tier system"""
return {
# Old role -> (new_role, modules)
'superadmin': ('superadmin', []), # All modules by default
'administrator': ('admin', []), # All modules by default
'admin': ('admin', []), # All modules by default
'quality': ('manager', ['quality']),
'warehouse': ('manager', ['warehouse']),
'warehouse_manager': ('manager', ['warehouse']),
'scan': ('worker', ['quality']), # Assume scan users are quality workers
'etichete': ('manager', ['labels']),
'quality_manager': ('manager', ['quality']),
'quality_worker': ('worker', ['quality']),
}
def migrate_database(conn, db_type):
"""Migrate a specific database"""
cursor = conn.cursor()
print(f"Migrating {db_type} database...")
# Check if users table exists
if db_type == 'internal':
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='users'")
else: # external/MariaDB
cursor.execute("SHOW TABLES LIKE 'users'")
if not cursor.fetchone():
print(f"No users table found in {db_type} database")
return
# Check if modules column already exists
try:
if db_type == 'internal':
cursor.execute("PRAGMA table_info(users)")
columns = [row[1] for row in cursor.fetchall()]
else: # external/MariaDB
cursor.execute("DESCRIBE users")
columns = [row[0] for row in cursor.fetchall()]
if 'modules' not in columns:
print(f"Adding modules column to {db_type} database...")
if db_type == 'internal':
cursor.execute("ALTER TABLE users ADD COLUMN modules TEXT")
else: # external/MariaDB
cursor.execute("ALTER TABLE users ADD COLUMN modules TEXT")
else:
print(f"Modules column already exists in {db_type} database")
except Exception as e:
print(f"Error checking/adding modules column in {db_type}: {e}")
return
# Get current users
cursor.execute("SELECT id, username, role FROM users")
users = cursor.fetchall()
print(f"Found {len(users)} users in {db_type} database")
# Convert roles and assign modules
mapping = role_mapping()
updates = []
for user_id, username, old_role in users:
if old_role in mapping:
new_role, modules = mapping[old_role]
modules_json = json.dumps(modules) if modules else None
updates.append((new_role, modules_json, user_id, username))
print(f" {username}: {old_role} -> {new_role} with modules {modules}")
else:
print(f" {username}: Unknown role '{old_role}', keeping as-is")
# Apply updates
for new_role, modules_json, user_id, username in updates:
try:
cursor.execute("UPDATE users SET role = ?, modules = ? WHERE id = ?",
(new_role, modules_json, user_id))
print(f" Updated {username} successfully")
except Exception as e:
print(f" Error updating {username}: {e}")
conn.commit()
print(f"Migration completed for {db_type} database")
def main():
"""Main migration function"""
print("Starting migration to simplified 4-tier permission system...")
print("="*60)
connections = get_db_connections()
if not connections:
print("No database connections available. Please check your configuration.")
return
for db_type, conn in connections.items():
try:
migrate_database(conn, db_type)
print()
except Exception as e:
print(f"Error migrating {db_type} database: {e}")
finally:
conn.close()
print("Migration completed!")
print("\nNew role structure:")
print("- superadmin: Full system access")
print("- admin: Full app access (except role_permissions and download_extension)")
print("- manager: Module-based access (can have multiple modules)")
print("- worker: Limited module access (one module only)")
print("\nAvailable modules: quality, warehouse, labels")
if __name__ == "__main__":
main()

View File

@@ -1,111 +0,0 @@
#!/usr/bin/env python3
"""
Test script for the new simplified 4-tier permission system
"""
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'app'))
from permissions_simple import check_access, validate_user_modules, get_user_accessible_pages
def test_permission_system():
"""Test the new permission system with various scenarios"""
print("Testing Simplified 4-Tier Permission System")
print("=" * 50)
# Test cases: (role, modules, page, expected_result)
test_cases = [
# Superadmin tests
('superadmin', [], 'dashboard', True),
('superadmin', [], 'role_permissions', True),
('superadmin', [], 'quality', True),
('superadmin', [], 'warehouse', True),
# Admin tests
('admin', [], 'dashboard', True),
('admin', [], 'role_permissions', False), # Restricted for admin
('admin', [], 'download_extension', False), # Restricted for admin
('admin', [], 'quality', True),
('admin', [], 'warehouse', True),
# Manager tests
('manager', ['quality'], 'quality', True),
('manager', ['quality'], 'quality_reports', True),
('manager', ['quality'], 'warehouse', False), # No warehouse module
('manager', ['warehouse'], 'warehouse', True),
('manager', ['warehouse'], 'quality', False), # No quality module
('manager', ['quality', 'warehouse'], 'quality', True), # Multiple modules
('manager', ['quality', 'warehouse'], 'warehouse', True),
# Worker tests
('worker', ['quality'], 'quality', True),
('worker', ['quality'], 'quality_reports', False), # Workers can't access reports
('worker', ['quality'], 'warehouse', False), # No warehouse module
('worker', ['warehouse'], 'move_orders', True),
('worker', ['warehouse'], 'create_locations', False), # Workers can't create locations
# Invalid role test
('invalid_role', ['quality'], 'quality', False),
]
print("Testing access control:")
print("-" * 30)
passed = 0
failed = 0
for role, modules, page, expected in test_cases:
result = check_access(role, modules, page)
status = "PASS" if result == expected else "FAIL"
print(f"{status}: {role:12} {str(modules):20} {page:18} -> {result} (expected {expected})")
if result == expected:
passed += 1
else:
failed += 1
print(f"\nResults: {passed} passed, {failed} failed")
# Test module validation
print("\nTesting module validation:")
print("-" * 30)
validation_tests = [
('superadmin', ['quality'], True), # Superadmin can have any modules
('admin', ['warehouse'], True), # Admin can have any modules
('manager', ['quality'], True), # Manager can have one module
('manager', ['quality', 'warehouse'], True), # Manager can have multiple modules
('manager', [], False), # Manager must have at least one module
('worker', ['quality'], True), # Worker can have one module
('worker', ['quality', 'warehouse'], False), # Worker cannot have multiple modules
('worker', [], False), # Worker must have exactly one module
('invalid_role', ['quality'], False), # Invalid role
]
for role, modules, expected in validation_tests:
is_valid, error_msg = validate_user_modules(role, modules)
status = "PASS" if is_valid == expected else "FAIL"
print(f"{status}: {role:12} {str(modules):20} -> {is_valid} (expected {expected})")
if error_msg:
print(f" Error: {error_msg}")
# Test accessible pages for different users
print("\nTesting accessible pages:")
print("-" * 30)
user_tests = [
('superadmin', []),
('admin', []),
('manager', ['quality']),
('manager', ['warehouse']),
('worker', ['quality']),
('worker', ['warehouse']),
]
for role, modules in user_tests:
accessible_pages = get_user_accessible_pages(role, modules)
print(f"{role:12} {str(modules):20} -> {len(accessible_pages)} pages: {', '.join(accessible_pages[:5])}{'...' if len(accessible_pages) > 5 else ''}")
if __name__ == "__main__":
test_permission_system()

View File

@@ -0,0 +1,263 @@
# Enhanced Print Controller - Features & Usage
## Overview
The print module now includes an advanced Print Controller with real-time monitoring, error detection, pause/resume functionality, and automatic reprint capabilities for handling printer issues like paper jams or running out of paper.
## Key Features
### 1. **Real-Time Progress Modal**
- Visual progress bar showing percentage completion
- Live counter showing "X / Y" labels printed
- Status messages updating in real-time
- Detailed event log with timestamps
### 2. **Print Status Log**
- Timestamped entries for all print events
- Color-coded status messages:
- **Green**: Successful operations
- **Yellow**: Warnings and paused states
- **Red**: Errors and failures
- Auto-scrolling to show latest events
- Scrollable history of all print activities
### 3. **Control Buttons**
#### **⏸️ Pause Button**
- Pauses printing between labels
- Useful for:
- Checking printer paper level
- Inspecting print quality
- Loading more paper
- Progress bar turns yellow when paused
#### **▶️ Resume Button**
- Resumes printing from where it was paused
- Appears when print job is paused
- Progress bar returns to normal green
#### **🔄 Reprint Last Button**
- Available after each successful print
- Reprints the last completed label
- Useful when:
- Label came out damaged
- Print quality was poor
- Label fell on the floor
#### **❌ Cancel Button**
- Stops the entire print job
- Shows how many labels were completed
- Progress bar turns red
- Database not updated on cancellation
### 4. **Automatic Error Detection**
- Detects when a label fails to print
- Automatically pauses the job
- Shows error message in log
- Progress bar turns red
- Prompts user to check printer
### 5. **Automatic Recovery**
When a print error occurs:
1. Job pauses automatically
2. Error is logged with timestamp
3. User checks and fixes printer issue (refill paper, clear jam)
4. User clicks "Resume"
5. Failed label is automatically retried
6. If retry succeeds, continues with next label
7. If retry fails, user can cancel or try again
### 6. **Smart Print Management**
- Tracks current label being printed
- Remembers last successfully printed label
- Maintains list of failed labels
- Prevents duplicate printing
- Sequential label numbering (CP00000777/001, 002, 003...)
## Usage Instructions
### Normal Printing Workflow
1. **Start Print Job**
- Select an order from the table
- Click "Print Label (QZ Tray)" button
- Print Controller modal appears
2. **Monitor Progress**
- Watch progress bar fill (green)
- Check "X / Y" counter
- Read status messages
- View timestamped log entries
3. **Completion**
- All labels print successfully
- Database updates automatically
- Table refreshes to show new status
- Modal closes automatically
- Success notification appears
### Handling Paper Running Out Mid-Print
**Scenario**: Printing 20 labels, paper runs out after label 12
1. **Detection**
- Label 13 fails to print
- Controller detects error
- Job pauses automatically
- Progress bar turns red
- Log shows: "✗ Label 13 failed: Print error"
- Status: "⚠️ ERROR - Check printer (paper jam/out of paper)"
2. **User Action**
- Check printer
- See paper is empty
- Load new paper roll
- Ensure paper is feeding correctly
3. **Resume Printing**
- Click "▶️ Resume" button
- Controller automatically retries label 13
- Log shows: "Retrying label 13..."
- If successful: "✓ Label 13 printed successfully (retry)"
- Continues with labels 14-20
- Job completes normally
### Handling Print Quality Issues
**Scenario**: Label 5 of 10 prints too light
1. **During Print**
- Wait for label 5 to complete
- "🔄 Reprint Last" button appears
- Click button
- Label 5 reprints with current settings
2. **Adjust & Continue**
- Adjust printer darkness setting
- Click "▶️ Resume" if paused
- Continue printing labels 6-10
### Manual Pause for Inspection
**Scenario**: Want to check label quality mid-batch
1. Click "⏸️ Pause" button
2. Progress bar turns yellow
3. Remove and inspect last printed label
4. If good: Click "▶️ Resume"
5. If bad:
- Click "🔄 Reprint Last"
- Adjust printer settings
- Click "▶️ Resume"
### Emergency Cancellation
**Scenario**: Wrong order selected or major printer malfunction
1. Click "❌ Cancel" button
2. Printing stops immediately
3. Log shows labels completed (e.g., "7 of 25")
4. Progress bar turns red
5. Modal stays open for 2 seconds
6. Warning notification appears
7. Database NOT updated
8. Can reprint the order later
## Technical Implementation
### Print Controller State
```javascript
{
isPaused: false, // Whether job is paused
isCancelled: false, // Whether job is cancelled
currentLabel: 0, // Current label number being printed
totalLabels: 0, // Total labels in job
lastPrintedLabel: 0, // Last successfully printed label
failedLabels: [], // Array of failed label numbers
orderData: null, // Order information
printerName: null // Selected printer name
}
```
### Event Log Format
```
[17:47:15] Starting print job: 10 labels
[17:47:15] Printer: Thermal Printer A
[17:47:15] Order: CP00000777
[17:47:16] Sending label 1 to printer...
[17:47:16] ✓ Label 1 printed successfully
```
### Error Detection
- Try/catch around each print operation
- Errors trigger automatic pause
- Failed label number recorded
- Automatic retry on resume
### Progress Calculation
```javascript
percentage = (currentLabel / totalLabels) * 100
```
### Color States
- **Green**: Normal printing
- **Yellow**: Paused (manual or automatic)
- **Red**: Error or cancelled
## Benefits
1.**Prevents Wasted Labels**: Automatic recovery from errors
2.**Reduces Operator Stress**: Clear status and easy controls
3.**Handles Paper Depletion**: Auto-pause and retry on paper out
4.**Quality Control**: Easy reprint of damaged labels
5.**Transparency**: Full log of all print activities
6.**Flexibility**: Pause anytime for inspection
7.**Safety**: Cancel button for emergencies
8.**Accuracy**: Sequential numbering maintained even with errors
## Common Scenarios Handled
| Issue | Detection | Solution |
|-------|-----------|----------|
| Paper runs out | Print error on next label | Auto-pause, user refills, resume |
| Paper jam | Print error detected | Auto-pause, user clears jam, resume |
| Poor print quality | Visual inspection | Reprint last label |
| Wrong order selected | User realizes mid-print | Cancel job |
| Need to inspect labels | User decision | Pause, inspect, resume |
| Label falls on floor | Visual observation | Reprint last label |
| Printer offline | Print error | Auto-pause, user fixes, resume |
## Future Enhancements (Possible)
- Printer status monitoring (paper level, online/offline)
- Print queue for multiple orders
- Estimated time remaining
- Sound notifications on completion
- Email/SMS alerts for long jobs
- Print history logging to database
- Batch printing multiple orders
- Automatic printer reconnection
## Testing Recommendations
1. **Normal Job**: Print 5-10 labels, verify all complete
2. **Paper Depletion**: Remove paper mid-job, verify auto-pause and recovery
3. **Pause/Resume**: Manually pause mid-job, wait, resume
4. **Reprint**: Print job, reprint last label multiple times
5. **Cancel**: Start job, cancel after 2-3 labels
6. **Error Recovery**: Simulate error, verify automatic retry
## Browser Compatibility
- Chrome/Edge: ✅ Fully supported
- Firefox: ✅ Fully supported
- Safari: ✅ Fully supported
- Mobile browsers: ⚠️ Desktop recommended for QZ Tray
## Support
For issues or questions:
- Check browser console for detailed error messages
- Verify QZ Tray is running and connected
- Check printer is online and has paper
- Review print status log in modal
- Restart QZ Tray if connection issues persist

View File

@@ -0,0 +1,133 @@
# Mobile-Responsive Login Page
## Overview
The login page has been enhanced with comprehensive mobile-responsive CSS to provide an optimal user experience across all device types and screen sizes.
## Mobile-Responsive Features Added
### 1. **Responsive Breakpoints**
- **Tablet (≤768px)**: Column layout, optimized logo and form sizing
- **Mobile (≤480px)**: Enhanced touch targets, better spacing
- **Small Mobile (≤320px)**: Minimal padding, compact design
- **Landscape (height ≤500px)**: Horizontal layout for landscape phones
### 2. **Layout Adaptations**
#### Desktop (>768px)
- Side-by-side logo and form layout
- Large logo (90vh height)
- Fixed form width (600px)
#### Tablet (≤768px)
- Vertical stacked layout
- Logo height reduced to 30vh
- Form width becomes responsive (100%, max 400px)
#### Mobile (≤480px)
- Optimized touch targets (44px minimum)
- Increased padding and margins
- Better visual hierarchy
- Enhanced shadows and border radius
#### Small Mobile (≤320px)
- Minimal padding to maximize space
- Compact logo (20vh height)
- Reduced font sizes where appropriate
### 3. **Touch Optimizations**
#### iOS/Safari Specific
- `font-size: 16px` on inputs prevents automatic zoom
- Proper touch target sizing (44px minimum)
#### Touch Device Enhancements
- Active states for button presses
- Optimized image rendering for high DPI screens
- Hover effects disabled on touch devices
### 4. **Accessibility Improvements**
- Proper contrast ratios maintained
- Touch targets meet accessibility guidelines
- Readable font sizes across all devices
- Smooth transitions and animations
### 5. **Performance Considerations**
- CSS-only responsive design (no JavaScript required)
- Efficient media queries
- Optimized image rendering for retina displays
## Key CSS Features
### Flexible Layout
```css
.login-page {
display: flex;
flex-direction: column; /* Mobile */
justify-content: center;
}
```
### Responsive Images
```css
.login-logo {
max-height: 25vh; /* Mobile */
max-width: 85vw;
}
```
### Touch-Friendly Inputs
```css
.form-container input {
padding: 12px;
font-size: 16px; /* Prevents iOS zoom */
min-height: 44px; /* Touch target size */
}
```
### Landscape Optimization
```css
@media screen and (max-height: 500px) and (orientation: landscape) {
.login-page {
flex-direction: row; /* Back to horizontal */
}
}
```
## Testing Recommendations
### Device Testing
- [ ] iPhone (various sizes)
- [ ] Android phones (various sizes)
- [ ] iPad/Android tablets
- [ ] Desktop browsers with responsive mode
### Orientation Testing
- [ ] Portrait mode on all devices
- [ ] Landscape mode on phones
- [ ] Landscape mode on tablets
### Browser Testing
- [ ] Safari (iOS)
- [ ] Chrome (Android/iOS)
- [ ] Firefox Mobile
- [ ] Samsung Internet
- [ ] Desktop browsers (Chrome, Firefox, Safari, Edge)
## Browser Support
- Modern browsers (ES6+ support)
- iOS Safari 12+
- Android Chrome 70+
- Desktop browsers (last 2 versions)
## Performance Impact
- **CSS Size**: Increased by ~2KB (compressed)
- **Load Time**: No impact (CSS only)
- **Rendering**: Optimized for mobile GPUs
- **Memory**: Minimal additional usage
## Future Enhancements
1. **Dark mode mobile optimizations**
2. **Progressive Web App (PWA) features**
3. **Biometric authentication UI**
4. **Loading states and animations**
5. **Error message responsive design**

125
py_app/PRINT_PROGRESS_FEATURE.md Executable file
View File

@@ -0,0 +1,125 @@
# Print Progress Modal Feature
## Overview
Added a visual progress modal that displays during label printing operations via QZ Tray. The modal shows real-time progress, updates the database upon completion, and refreshes the table view automatically.
## Features Implemented
### 1. Progress Modal UI
- **Modal Overlay**: Full-screen semi-transparent overlay to focus user attention
- **Progress Bar**: Animated progress bar showing percentage completion
- **Status Messages**: Real-time status updates during printing
- **Label Counter**: Shows "X / Y" format for current progress (e.g., "5 / 10")
### 2. Print Flow Improvements
The printing process now follows these steps:
1. **Validation**: Check QZ Tray connection and printer selection
2. **Modal Display**: Show progress modal immediately
3. **Sequential Printing**: Print each label one by one with progress updates
- Update progress bar after each successful print
- Show current label number being printed
- 500ms delay between labels for printer processing
4. **Database Update**: Call `/update_printed_status/<order_id>` endpoint
- Marks the order as printed in the database
- Handles errors gracefully (prints still succeed even if DB update fails)
5. **Table Refresh**: Automatically click "Load Orders" button to refresh the view
6. **Modal Close**: Hide modal after completion
7. **Notification**: Show success notification to user
### 3. Progress Updates
The modal displays different status messages:
- "Preparing to print..." (initial)
- "Printing label X of Y..." (during printing)
- "✅ All labels printed! Updating database..." (after prints complete)
- "✅ Complete! Refreshing table..." (after DB update)
- "⚠️ Labels printed but database update failed" (on DB error)
### 4. Error Handling
- Modal automatically closes on any error
- Error notifications shown to user
- Database update failures don't prevent successful printing
- Graceful degradation if DB update fails
## Technical Details
### CSS Styling
- **Modal**: Fixed position, z-index 9999, centered layout
- **Content Card**: White background, rounded corners, shadow
- **Progress Bar**: Linear gradient blue, smooth transitions
- **Responsive**: Min-width 400px, max-width 500px
### JavaScript Functions Modified
#### `handleQZTrayPrint(selectedRow)`
**Changes:**
- Added modal element references
- Show modal before printing starts
- Update progress bar and counter in loop
- Call database update endpoint after printing
- Handle database update errors
- Refresh table automatically
- Close modal on completion or error
### Backend Integration
#### Endpoint Used: `/update_printed_status/<int:order_id>`
- **Method**: POST
- **Purpose**: Mark order as printed in database
- **Authentication**: Requires superadmin, warehouse_manager, or etichete role
- **Response**: JSON with success/error message
## User Experience Flow
1. User selects an order row in the table
2. User clicks "Print Label (QZ Tray)" button
3. Modal appears showing "Preparing to print..."
4. Progress bar fills as each label prints
5. Counter shows current progress (e.g., "7 / 10")
6. After all labels print: "✅ All labels printed! Updating database..."
7. Database updates with printed status
8. Modal shows "✅ Complete! Refreshing table..."
9. Modal closes automatically
10. Success notification appears
11. Table refreshes showing updated order status
## Benefits
**Visual Feedback**: Users see real-time progress instead of a frozen UI
**Status Clarity**: Clear messages about what's happening
**Automatic Updates**: Database and UI update without manual intervention
**Error Recovery**: Graceful handling of database update failures
**Professional UX**: Modern, polished user interface
**Non-Blocking**: Progress modal doesn't interfere with printing operation
## Files Modified
1. **print_module.html**
- Added modal HTML structure
- Added modal CSS styles
- Updated `handleQZTrayPrint()` function
- Added database update API call
- Added automatic table refresh
## Testing Checklist
- [ ] Modal appears when printing starts
- [ ] Progress bar animates smoothly
- [ ] Counter updates correctly (1/10, 2/10, etc.)
- [ ] All labels print successfully
- [ ] Database updates after printing
- [ ] Table refreshes automatically
- [ ] Modal closes after completion
- [ ] Success notification appears
- [ ] Error handling works (if DB update fails)
- [ ] Modal closes on printing errors
## Future Enhancements
Potential improvements:
- Add "Cancel" button to stop printing mid-process
- Show estimated time remaining
- Add sound notification on completion
- Log printing history with timestamps
- Add printer status monitoring
- Show print queue if multiple orders selected

View File

@@ -1,4 +0,0 @@
* Serving Flask app 'app'
* Debug mode: on
Address already in use
Port 8781 is in use by another program. Either identify and stop that program, or start the server with a different port.

View File

@@ -1,90 +0,0 @@
"""
Simple access control decorators for the 4-tier system
"""
from functools import wraps
from flask import session, redirect, url_for, flash, request
from .permissions_simple import check_access, ROLES
def requires_role(min_role_level=None, required_modules=None, page=None):
"""
Simple role-based access decorator
Args:
min_role_level (int): Minimum role level required (50, 70, 90, 100)
required_modules (list): Required modules for access
page (str): Page name for automatic access checking
"""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
# Check if user is logged in
if 'user' not in session:
flash('Please log in to access this page.')
return redirect(url_for('main.login'))
user_role = session.get('role')
user_modules = session.get('modules', [])
# If page is specified, use automatic access checking
if page:
if not check_access(user_role, user_modules, page):
flash('Access denied: You do not have permission to access this page.')
return redirect(url_for('main.dashboard'))
return f(*args, **kwargs)
# Manual role level checking
if min_role_level:
user_level = ROLES.get(user_role, {}).get('level', 0)
if user_level < min_role_level:
flash('Access denied: Insufficient privileges.')
return redirect(url_for('main.dashboard'))
# Module requirement checking
if required_modules:
if user_role in ['superadmin', 'admin']:
# Superadmin and admin have access to all modules
pass
else:
if not any(module in user_modules for module in required_modules):
flash('Access denied: You do not have access to this module.')
return redirect(url_for('main.dashboard'))
return f(*args, **kwargs)
return decorated_function
return decorator
def superadmin_only(f):
"""Decorator for superadmin-only pages"""
return requires_role(min_role_level=100)(f)
def admin_plus(f):
"""Decorator for admin and superadmin access"""
return requires_role(min_role_level=90)(f)
def manager_plus(f):
"""Decorator for manager, admin, and superadmin access"""
return requires_role(min_role_level=70)(f)
def requires_quality_module(f):
"""Decorator for quality module access"""
return requires_role(required_modules=['quality'])(f)
def requires_warehouse_module(f):
"""Decorator for warehouse module access"""
return requires_role(required_modules=['warehouse'])(f)
def requires_labels_module(f):
"""Decorator for labels module access"""
return requires_role(required_modules=['labels'])(f)
def quality_manager_plus(f):
"""Decorator for quality module manager+ access"""
return requires_role(min_role_level=70, required_modules=['quality'])(f)
def warehouse_manager_plus(f):
"""Decorator for warehouse module manager+ access"""
return requires_role(min_role_level=70, required_modules=['warehouse'])(f)
def labels_manager_plus(f):
"""Decorator for labels module manager+ access"""
return requires_role(min_role_level=70, required_modules=['labels'])(f)

View File

@@ -0,0 +1,151 @@
#!/usr/bin/env python3
"""
Database script to add the printed_labels column to the order_for_labels table
This column will track whether labels have been printed for each order (boolean: 0=false, 1=true)
Default value: 0 (false)
"""
import sys
import os
import mariadb
from flask import Flask
# Add the app directory to the path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
def get_db_connection():
"""Get database connection using settings from external_server.conf"""
# Go up two levels from this script to reach py_app directory, then to instance
app_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
settings_file = os.path.join(app_root, 'instance', 'external_server.conf')
settings = {}
with open(settings_file, 'r') as f:
for line in f:
key, value = line.strip().split('=', 1)
settings[key] = value
return mariadb.connect(
user=settings['username'],
password=settings['password'],
host=settings['server_domain'],
port=int(settings['port']),
database=settings['database_name']
)
def add_printed_labels_column():
"""
Adds the printed_labels column to the order_for_labels table after the line_number column
Column type: TINYINT(1) (boolean: 0=false, 1=true)
Default value: 0 (false)
"""
try:
conn = get_db_connection()
cursor = conn.cursor()
# Check if table exists
cursor.execute("SHOW TABLES LIKE 'order_for_labels'")
result = cursor.fetchone()
if not result:
print("❌ Table 'order_for_labels' does not exist. Please create it first.")
return False
# Check if column already exists
cursor.execute("SHOW COLUMNS FROM order_for_labels LIKE 'printed_labels'")
column_exists = cursor.fetchone()
if column_exists:
print(" Column 'printed_labels' already exists.")
# Show current structure
cursor.execute("DESCRIBE order_for_labels")
columns = cursor.fetchall()
print("\n📋 Current table structure:")
for col in columns:
null_info = 'NULL' if col[2] == 'YES' else 'NOT NULL'
default_info = f" DEFAULT {col[4]}" if col[4] else ""
print(f" 📌 {col[0]:<25} {col[1]:<20} {null_info}{default_info}")
else:
# Add the column after line_number
alter_table_sql = """
ALTER TABLE order_for_labels
ADD COLUMN printed_labels TINYINT(1) NOT NULL DEFAULT 0
COMMENT 'Boolean flag: 0=labels not printed, 1=labels printed'
AFTER line_number
"""
cursor.execute(alter_table_sql)
conn.commit()
print("✅ Column 'printed_labels' added successfully!")
# Show the updated structure
cursor.execute("DESCRIBE order_for_labels")
columns = cursor.fetchall()
print("\n📋 Updated table structure:")
for col in columns:
null_info = 'NULL' if col[2] == 'YES' else 'NOT NULL'
default_info = f" DEFAULT {col[4]}" if col[4] else ""
highlight = "🆕 " if col[0] == 'printed_labels' else " "
print(f"{highlight}{col[0]:<25} {col[1]:<20} {null_info}{default_info}")
# Show count of existing records that will have printed_labels = 0
cursor.execute("SELECT COUNT(*) FROM order_for_labels")
count = cursor.fetchone()[0]
if count > 0:
print(f"\n📊 {count} existing records now have printed_labels = 0 (false)")
conn.close()
except mariadb.Error as e:
print(f"❌ Database error: {e}")
return False
except Exception as e:
print(f"❌ Error: {e}")
return False
return True
def verify_column():
"""Verify the column was added correctly"""
try:
conn = get_db_connection()
cursor = conn.cursor()
# Test the column functionality
cursor.execute("SELECT COUNT(*) as total, SUM(printed_labels) as printed FROM order_for_labels")
result = cursor.fetchone()
if result:
total, printed = result
print(f"\n🔍 Verification:")
print(f" 📦 Total orders: {total}")
print(f" 🖨️ Printed orders: {printed or 0}")
print(f" 📄 Unprinted orders: {total - (printed or 0)}")
conn.close()
return True
except Exception as e:
print(f"❌ Verification failed: {e}")
return False
if __name__ == "__main__":
print("🔧 Adding printed_labels column to order_for_labels table...")
print("="*60)
success = add_printed_labels_column()
if success:
print("\n🔍 Verifying column addition...")
verify_column()
print("\n✅ Database modification completed successfully!")
print("\n📝 Column Details:")
print(" • Name: printed_labels")
print(" • Type: TINYINT(1) (boolean)")
print(" • Default: 0 (false - labels not printed)")
print(" • Values: 0 = not printed, 1 = printed")
print(" • Position: After line_number column")
else:
print("\n❌ Database modification failed!")
print("="*60)

View File

@@ -5,7 +5,7 @@ db_config = {
"user": "trasabilitate", "user": "trasabilitate",
"password": "Initial01!", "password": "Initial01!",
"host": "localhost", "host": "localhost",
"database": "trasabilitate" "database": "trasabilitate_database"
} }
# Connect to the database # Connect to the database

View File

@@ -6,7 +6,7 @@ db_config = {
"user": "trasabilitate", "user": "trasabilitate",
"password": "Initial01!", "password": "Initial01!",
"host": "localhost", "host": "localhost",
"database": "trasabilitate" "database": "trasabilitate_database"
} }
try: try:

View File

@@ -5,7 +5,7 @@ db_config = {
"user": "trasabilitate", "user": "trasabilitate",
"password": "Initial01!", "password": "Initial01!",
"host": "localhost", "host": "localhost",
"database": "trasabilitate" "database": "trasabilitate_database"
} }
# Connect to the database # Connect to the database

View File

@@ -5,7 +5,7 @@ db_config = {
"user": "trasabilitate", "user": "trasabilitate",
"password": "Initial01!", "password": "Initial01!",
"host": "localhost", "host": "localhost",
"database": "trasabilitate" "database": "trasabilitate_database"
} }
# Connect to the database # Connect to the database

View File

@@ -1,70 +0,0 @@
#!/usr/bin/env python3
"""
Docker-compatible Database Setup Script
Reads configuration from environment variables or config file
"""
import mariadb
import os
import sys
from datetime import datetime
def get_db_config():
"""Get database configuration from environment or config file"""
# Try environment variables first (Docker)
if os.getenv('DB_HOST'):
return {
"user": os.getenv('DB_USER', 'trasabilitate'),
"password": os.getenv('DB_PASSWORD', 'Initial01!'),
"host": os.getenv('DB_HOST', 'localhost'),
"port": int(os.getenv('DB_PORT', '3306')),
"database": os.getenv('DB_NAME', 'trasabilitate')
}
# Fallback to config file (traditional deployment)
config_file = os.path.join(os.path.dirname(__file__), '../../instance/external_server.conf')
if os.path.exists(config_file):
settings = {}
with open(config_file, 'r') as f:
for line in f:
if '=' in line:
key, value = line.strip().split('=', 1)
settings[key] = value
return {
"user": settings.get('username', 'trasabilitate'),
"password": settings.get('password', 'Initial01!'),
"host": settings.get('server_domain', 'localhost'),
"port": int(settings.get('port', '3306')),
"database": settings.get('database_name', 'trasabilitate')
}
# Default configuration
return {
"user": "trasabilitate",
"password": "Initial01!",
"host": "localhost",
"port": 3306,
"database": "trasabilitate"
}
def print_step(step_num, description):
"""Print formatted step information"""
print(f"\n{'='*60}")
print(f"Step {step_num}: {description}")
print('='*60)
def print_success(message):
"""Print success message"""
print(f"{message}")
def print_error(message):
"""Print error message"""
print(f"{message}")
# Get configuration
DB_CONFIG = get_db_config()
print(f"Using database configuration: {DB_CONFIG['user']}@{DB_CONFIG['host']}:{DB_CONFIG['port']}/{DB_CONFIG['database']}")
# Import the rest from the original setup script

View File

@@ -1,722 +0,0 @@
#!/usr/bin/env python3
"""
Complete Database Setup Script for Trasabilitate Application
This script creates all necessary database tables, triggers, and initial data
for quick deployment of the application.
Usage: python3 setup_complete_database.py
Supports both traditional and Docker deployments via environment variables.
"""
import mariadb
import sqlite3
import os
import sys
from datetime import datetime
# Database configuration - supports environment variables (Docker) or defaults
DB_CONFIG = {
"user": os.getenv("DB_USER", "trasabilitate"),
"password": os.getenv("DB_PASSWORD", "Initial01!"),
"host": os.getenv("DB_HOST", "localhost"),
"port": int(os.getenv("DB_PORT", "3306")),
"database": os.getenv("DB_NAME", "trasabilitate")
}
def print_step(step_num, description):
"""Print formatted step information"""
print(f"\n{'='*60}")
print(f"Step {step_num}: {description}")
print('='*60)
def print_success(message):
"""Print success message"""
print(f"{message}")
def print_error(message):
"""Print error message"""
print(f"{message}")
def test_database_connection():
"""Test if we can connect to the database"""
print_step(1, "Testing Database Connection")
try:
conn = mariadb.connect(**DB_CONFIG)
print_success("Successfully connected to MariaDB database 'trasabilitate'")
conn.close()
return True
except Exception as e:
print_error(f"Failed to connect to database: {e}")
print("\nPlease ensure:")
print("1. MariaDB is running")
print("2. Database 'trasabilitate' exists")
print("3. User 'trasabilitate' has been created with password 'Initial01!'")
print("4. User has all privileges on the database")
return False
def create_scan_tables():
"""Create scan1_orders and scanfg_orders tables"""
print_step(2, "Creating Scan Tables (scan1_orders & scanfg_orders)")
try:
conn = mariadb.connect(**DB_CONFIG)
cursor = conn.cursor()
# Create scan1_orders table
scan1_table_query = """
CREATE TABLE IF NOT EXISTS scan1_orders (
Id INT AUTO_INCREMENT PRIMARY KEY,
operator_code VARCHAR(4) NOT NULL,
CP_full_code VARCHAR(15) NOT NULL UNIQUE,
OC1_code VARCHAR(4) NOT NULL,
OC2_code VARCHAR(4) NOT NULL,
CP_base_code VARCHAR(10) GENERATED ALWAYS AS (LEFT(CP_full_code, 10)) STORED,
quality_code INT(3) NOT NULL,
date DATE NOT NULL,
time TIME NOT NULL,
approved_quantity INT DEFAULT 0,
rejected_quantity INT DEFAULT 0
);
"""
cursor.execute(scan1_table_query)
print_success("Table 'scan1_orders' created successfully")
# Create scanfg_orders table
scanfg_table_query = """
CREATE TABLE IF NOT EXISTS scanfg_orders (
Id INT AUTO_INCREMENT PRIMARY KEY,
operator_code VARCHAR(4) NOT NULL,
CP_full_code VARCHAR(15) NOT NULL UNIQUE,
OC1_code VARCHAR(4) NOT NULL,
OC2_code VARCHAR(4) NOT NULL,
CP_base_code VARCHAR(10) GENERATED ALWAYS AS (LEFT(CP_full_code, 10)) STORED,
quality_code INT(3) NOT NULL,
date DATE NOT NULL,
time TIME NOT NULL,
approved_quantity INT DEFAULT 0,
rejected_quantity INT DEFAULT 0
);
"""
cursor.execute(scanfg_table_query)
print_success("Table 'scanfg_orders' created successfully")
conn.commit()
cursor.close()
conn.close()
return True
except Exception as e:
print_error(f"Failed to create scan tables: {e}")
return False
def create_order_for_labels_table():
"""Create order_for_labels table
This table stores production orders for label generation.
Includes columns added for print module functionality:
- printed_labels: Track if labels have been printed (0=no, 1=yes)
- data_livrare: Delivery date from CSV uploads
- dimensiune: Product dimensions from CSV uploads
"""
print_step(3, "Creating Order for Labels Table")
try:
conn = mariadb.connect(**DB_CONFIG)
cursor = conn.cursor()
order_labels_query = """
CREATE TABLE IF NOT EXISTS order_for_labels (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
comanda_productie VARCHAR(15) NOT NULL,
cod_articol VARCHAR(15) NULL,
descr_com_prod VARCHAR(50) NOT NULL,
cantitate INT(3) NOT NULL,
com_achiz_client VARCHAR(25) NULL,
nr_linie_com_client INT(3) NULL,
customer_name VARCHAR(50) NULL,
customer_article_number VARCHAR(25) NULL,
open_for_order VARCHAR(25) NULL,
line_number INT(3) NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
printed_labels INT(1) DEFAULT 0,
data_livrare DATE NULL,
dimensiune VARCHAR(20) NULL
);
"""
cursor.execute(order_labels_query)
print_success("Table 'order_for_labels' created successfully")
conn.commit()
cursor.close()
conn.close()
return True
except Exception as e:
print_error(f"Failed to create order_for_labels table: {e}")
return False
def create_warehouse_locations_table():
"""Create warehouse_locations table"""
print_step(4, "Creating Warehouse Locations Table")
try:
conn = mariadb.connect(**DB_CONFIG)
cursor = conn.cursor()
warehouse_query = """
CREATE TABLE IF NOT EXISTS warehouse_locations (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
location_code VARCHAR(12) NOT NULL UNIQUE,
size INT,
description VARCHAR(250)
);
"""
cursor.execute(warehouse_query)
print_success("Table 'warehouse_locations' created successfully")
conn.commit()
cursor.close()
conn.close()
return True
except Exception as e:
print_error(f"Failed to create warehouse_locations table: {e}")
return False
def create_permissions_tables():
"""Create permission management tables"""
print_step(5, "Creating Permission Management Tables")
try:
conn = mariadb.connect(**DB_CONFIG)
cursor = conn.cursor()
# Create permissions table
permissions_query = """
CREATE TABLE IF NOT EXISTS permissions (
id INT AUTO_INCREMENT PRIMARY KEY,
permission_key VARCHAR(255) UNIQUE NOT NULL,
page VARCHAR(100) NOT NULL,
page_name VARCHAR(255) NOT NULL,
section VARCHAR(100) NOT NULL,
section_name VARCHAR(255) NOT NULL,
action VARCHAR(50) NOT NULL,
action_name VARCHAR(255) NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
"""
cursor.execute(permissions_query)
print_success("Table 'permissions' created successfully")
# Create role_permissions table
role_permissions_query = """
CREATE TABLE IF NOT EXISTS role_permissions (
id INT AUTO_INCREMENT PRIMARY KEY,
role_name VARCHAR(100) NOT NULL,
permission_id INT NOT NULL,
granted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
granted_by VARCHAR(100),
FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE,
UNIQUE KEY unique_role_permission (role_name, permission_id)
);
"""
cursor.execute(role_permissions_query)
print_success("Table 'role_permissions' created successfully")
# Create role_hierarchy table
role_hierarchy_query = """
CREATE TABLE IF NOT EXISTS role_hierarchy (
id INT AUTO_INCREMENT PRIMARY KEY,
role_name VARCHAR(100) UNIQUE NOT NULL,
role_display_name VARCHAR(255) NOT NULL,
level INT NOT NULL,
parent_role VARCHAR(100),
description TEXT,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
"""
cursor.execute(role_hierarchy_query)
print_success("Table 'role_hierarchy' created successfully")
# Create permission_audit_log table
audit_log_query = """
CREATE TABLE IF NOT EXISTS permission_audit_log (
id INT AUTO_INCREMENT PRIMARY KEY,
action VARCHAR(50) NOT NULL,
role_name VARCHAR(100),
permission_key VARCHAR(255),
user_id VARCHAR(100),
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
details TEXT,
ip_address VARCHAR(45)
);
"""
cursor.execute(audit_log_query)
print_success("Table 'permission_audit_log' created successfully")
conn.commit()
cursor.close()
conn.close()
return True
except Exception as e:
print_error(f"Failed to create permissions tables: {e}")
return False
def create_users_table_mariadb():
"""Create users and roles tables in MariaDB and seed superadmin"""
print_step(6, "Creating MariaDB Users and Roles Tables")
try:
conn = mariadb.connect(**DB_CONFIG)
cursor = conn.cursor()
# Create users table in MariaDB
users_table_query = """
CREATE TABLE IF NOT EXISTS users (
id INT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(100) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
role VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
"""
cursor.execute(users_table_query)
print_success("Table 'users' created successfully")
# Create roles table in MariaDB
roles_table_query = """
CREATE TABLE IF NOT EXISTS roles (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) UNIQUE NOT NULL,
access_level VARCHAR(50) NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
"""
cursor.execute(roles_table_query)
print_success("Table 'roles' created successfully")
# Insert superadmin role if not exists
cursor.execute("SELECT COUNT(*) FROM roles WHERE name = %s", ('superadmin',))
if cursor.fetchone()[0] == 0:
cursor.execute("""
INSERT INTO roles (name, access_level, description)
VALUES (%s, %s, %s)
""", ('superadmin', 'full', 'Full access to all app areas and functions'))
print_success("Superadmin role created")
# Insert superadmin user if not exists
cursor.execute("SELECT COUNT(*) FROM users WHERE username = %s", ('superadmin',))
if cursor.fetchone()[0] == 0:
cursor.execute("""
INSERT INTO users (username, password, role)
VALUES (%s, %s, %s)
""", ('superadmin', 'superadmin123', 'superadmin'))
print_success("Superadmin user created (username: superadmin, password: superadmin123)")
else:
print_success("Superadmin user already exists")
conn.commit()
cursor.close()
conn.close()
return True
except Exception as e:
print_error(f"Failed to create users tables in MariaDB: {e}")
return False
def create_sqlite_tables():
"""Create SQLite tables for users and roles (legacy/backup)"""
print_step(7, "Creating SQLite User and Role Tables (Backup)")
try:
# Create instance folder if it doesn't exist
instance_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../instance'))
if not os.path.exists(instance_folder):
os.makedirs(instance_folder)
db_path = os.path.join(instance_folder, 'users.db')
conn = sqlite3.connect(db_path)
cursor = conn.cursor()
# Create users table
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
role TEXT NOT NULL
)
''')
# Insert superadmin user if not exists
cursor.execute('''
INSERT OR IGNORE INTO users (username, password, role)
VALUES (?, ?, ?)
''', ('superadmin', 'superadmin123', 'superadmin'))
# Create roles table
cursor.execute('''
CREATE TABLE IF NOT EXISTS roles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
access_level TEXT NOT NULL,
description TEXT
)
''')
# Insert superadmin role if not exists
cursor.execute('''
INSERT OR IGNORE INTO roles (name, access_level, description)
VALUES (?, ?, ?)
''', ('superadmin', 'full', 'Full access to all app areas and functions'))
conn.commit()
conn.close()
print_success("SQLite tables created and superadmin user initialized")
return True
except Exception as e:
print_error(f"Failed to create SQLite tables: {e}")
return False
def create_database_triggers():
"""Create database triggers for automatic quantity calculations"""
print_step(8, "Creating Database Triggers")
try:
conn = mariadb.connect(**DB_CONFIG)
cursor = conn.cursor()
# Drop existing triggers if they exist
trigger_drops = [
"DROP TRIGGER IF EXISTS increment_approved_quantity;",
"DROP TRIGGER IF EXISTS increment_rejected_quantity;",
"DROP TRIGGER IF EXISTS increment_approved_quantity_fg;",
"DROP TRIGGER IF EXISTS increment_rejected_quantity_fg;"
]
for drop_query in trigger_drops:
cursor.execute(drop_query)
# Create trigger for scan1_orders approved quantity
scan1_approved_trigger = """
CREATE TRIGGER increment_approved_quantity
AFTER INSERT ON scan1_orders
FOR EACH ROW
BEGIN
IF NEW.quality_code = 000 THEN
UPDATE scan1_orders
SET approved_quantity = approved_quantity + 1
WHERE CP_base_code = NEW.CP_base_code;
ELSE
UPDATE scan1_orders
SET rejected_quantity = rejected_quantity + 1
WHERE CP_base_code = NEW.CP_base_code;
END IF;
END;
"""
cursor.execute(scan1_approved_trigger)
print_success("Trigger 'increment_approved_quantity' created for scan1_orders")
# Create trigger for scanfg_orders approved quantity
scanfg_approved_trigger = """
CREATE TRIGGER increment_approved_quantity_fg
AFTER INSERT ON scanfg_orders
FOR EACH ROW
BEGIN
IF NEW.quality_code = 000 THEN
UPDATE scanfg_orders
SET approved_quantity = approved_quantity + 1
WHERE CP_base_code = NEW.CP_base_code;
ELSE
UPDATE scanfg_orders
SET rejected_quantity = rejected_quantity + 1
WHERE CP_base_code = NEW.CP_base_code;
END IF;
END;
"""
cursor.execute(scanfg_approved_trigger)
print_success("Trigger 'increment_approved_quantity_fg' created for scanfg_orders")
conn.commit()
cursor.close()
conn.close()
return True
except Exception as e:
print_error(f"Failed to create database triggers: {e}")
return False
def populate_permissions_data():
"""Populate permissions and roles with default data"""
print_step(9, "Populating Permissions and Roles Data")
try:
conn = mariadb.connect(**DB_CONFIG)
cursor = conn.cursor()
# Define all permissions
permissions_data = [
# Home page permissions
('home.view', 'home', 'Home Page', 'navigation', 'Navigation', 'view', 'View Home Page', 'Access to home page'),
# Scan1 permissions
('scan1.view', 'scan1', 'Scan1 Page', 'scanning', 'Scanning Operations', 'view', 'View Scan1', 'Access to scan1 page'),
('scan1.scan', 'scan1', 'Scan1 Page', 'scanning', 'Scanning Operations', 'scan', 'Perform Scan1', 'Ability to perform scan1 operations'),
('scan1.history', 'scan1', 'Scan1 Page', 'scanning', 'Scanning Operations', 'history', 'View Scan1 History', 'View scan1 operation history'),
# ScanFG permissions
('scanfg.view', 'scanfg', 'ScanFG Page', 'scanning', 'Scanning Operations', 'view', 'View ScanFG', 'Access to scanfg page'),
('scanfg.scan', 'scanfg', 'ScanFG Page', 'scanning', 'Scanning Operations', 'scan', 'Perform ScanFG', 'Ability to perform scanfg operations'),
('scanfg.history', 'scanfg', 'ScanFG Page', 'scanning', 'Scanning Operations', 'history', 'View ScanFG History', 'View scanfg operation history'),
# Warehouse permissions
('warehouse.view', 'warehouse', 'Warehouse Management', 'warehouse', 'Warehouse Operations', 'view', 'View Warehouse', 'Access to warehouse page'),
('warehouse.manage_locations', 'warehouse', 'Warehouse Management', 'warehouse', 'Warehouse Operations', 'manage', 'Manage Locations', 'Add, edit, delete warehouse locations'),
('warehouse.view_locations', 'warehouse', 'Warehouse Management', 'warehouse', 'Warehouse Operations', 'view_locations', 'View Locations', 'View warehouse locations'),
# Labels permissions
('labels.view', 'labels', 'Label Management', 'labels', 'Label Operations', 'view', 'View Labels', 'Access to labels page'),
('labels.print', 'labels', 'Label Management', 'labels', 'Label Operations', 'print', 'Print Labels', 'Print labels'),
('labels.manage_orders', 'labels', 'Label Management', 'labels', 'Label Operations', 'manage', 'Manage Label Orders', 'Manage label orders'),
# Print Module permissions
('print.view', 'print', 'Print Module', 'printing', 'Printing Operations', 'view', 'View Print Module', 'Access to print module'),
('print.execute', 'print', 'Print Module', 'printing', 'Printing Operations', 'execute', 'Execute Print', 'Execute print operations'),
('print.manage_queue', 'print', 'Print Module', 'printing', 'Printing Operations', 'manage_queue', 'Manage Print Queue', 'Manage print queue'),
# Settings permissions
('settings.view', 'settings', 'Settings', 'system', 'System Management', 'view', 'View Settings', 'Access to settings page'),
('settings.edit', 'settings', 'Settings', 'system', 'System Management', 'edit', 'Edit Settings', 'Modify application settings'),
('settings.database', 'settings', 'Settings', 'system', 'System Management', 'database', 'Database Settings', 'Manage database settings'),
# User Management permissions
('users.view', 'users', 'User Management', 'admin', 'Administration', 'view', 'View Users', 'View user list'),
('users.create', 'users', 'User Management', 'admin', 'Administration', 'create', 'Create Users', 'Create new users'),
('users.edit', 'users', 'User Management', 'admin', 'Administration', 'edit', 'Edit Users', 'Edit existing users'),
('users.delete', 'users', 'User Management', 'admin', 'Administration', 'delete', 'Delete Users', 'Delete users'),
# Permission Management permissions
('permissions.view', 'permissions', 'Permission Management', 'admin', 'Administration', 'view', 'View Permissions', 'View permissions'),
('permissions.assign', 'permissions', 'Permission Management', 'admin', 'Administration', 'assign', 'Assign Permissions', 'Assign permissions to roles'),
('permissions.audit', 'permissions', 'Permission Management', 'admin', 'Administration', 'audit', 'View Audit Log', 'View permission audit log'),
]
# Insert permissions
permission_insert_query = """
INSERT IGNORE INTO permissions
(permission_key, page, page_name, section, section_name, action, action_name, description)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s)
"""
cursor.executemany(permission_insert_query, permissions_data)
print_success(f"Inserted {len(permissions_data)} permissions")
# Define role hierarchy
roles_data = [
('superadmin', 'Super Administrator', 1, None, 'Full system access with all permissions'),
('admin', 'Administrator', 2, 'superadmin', 'Administrative access with most permissions'),
('manager', 'Manager', 3, 'admin', 'Management level access'),
('quality_manager', 'Quality Manager', 4, 'manager', 'Quality control and scanning operations'),
('warehouse_manager', 'Warehouse Manager', 4, 'manager', 'Warehouse operations and management'),
('quality_worker', 'Quality Worker', 5, 'quality_manager', 'Basic quality scanning operations'),
('warehouse_worker', 'Warehouse Worker', 5, 'warehouse_manager', 'Basic warehouse operations'),
]
# Insert roles
role_insert_query = """
INSERT IGNORE INTO role_hierarchy
(role_name, role_display_name, level, parent_role, description)
VALUES (%s, %s, %s, %s, %s)
"""
cursor.executemany(role_insert_query, roles_data)
print_success(f"Inserted {len(roles_data)} roles")
# Assign permissions to roles
# Get all permission IDs
cursor.execute("SELECT id, permission_key FROM permissions")
permissions = {key: id for id, key in cursor.fetchall()}
# Define role-permission mappings
role_permissions = {
'superadmin': list(permissions.values()), # All permissions
'admin': [pid for key, pid in permissions.items() if not key.startswith('permissions.audit')], # All except audit
'manager': [permissions[key] for key in permissions.keys() if any(key.startswith(prefix) for prefix in ['home.', 'settings.view', 'users.view'])],
'quality_manager': [permissions[key] for key in permissions.keys() if any(key.startswith(prefix) for prefix in ['home.', 'scan1.', 'scanfg.', 'labels.', 'print.'])],
'warehouse_manager': [permissions[key] for key in permissions.keys() if any(key.startswith(prefix) for prefix in ['home.', 'warehouse.', 'labels.'])],
'quality_worker': [permissions[key] for key in permissions.keys() if any(key.startswith(prefix) for prefix in ['home.', 'scan1.view', 'scan1.scan', 'scanfg.view', 'scanfg.scan'])],
'warehouse_worker': [permissions[key] for key in permissions.keys() if any(key.startswith(prefix) for prefix in ['home.', 'warehouse.view', 'warehouse.view_locations'])],
}
# Insert role permissions
for role, permission_ids in role_permissions.items():
for permission_id in permission_ids:
cursor.execute("""
INSERT IGNORE INTO role_permissions (role_name, permission_id)
VALUES (%s, %s)
""", (role, permission_id))
print_success("Role permissions assigned successfully")
conn.commit()
cursor.close()
conn.close()
return True
except Exception as e:
print_error(f"Failed to populate permissions data: {e}")
return False
def update_external_config():
"""Update external_server.conf with correct database settings"""
print_step(10, "Updating External Server Configuration")
try:
config_path = os.path.join(os.path.dirname(__file__), '../../instance/external_server.conf')
# Use environment variables if available (Docker), otherwise use defaults
db_host = os.getenv('DB_HOST', 'localhost')
db_port = os.getenv('DB_PORT', '3306')
db_name = os.getenv('DB_NAME', 'trasabilitate')
db_user = os.getenv('DB_USER', 'trasabilitate')
db_password = os.getenv('DB_PASSWORD', 'Initial01!')
config_content = f"""server_domain={db_host}
port={db_port}
database_name={db_name}
username={db_user}
password={db_password}
"""
# Create instance directory if it doesn't exist
os.makedirs(os.path.dirname(config_path), exist_ok=True)
with open(config_path, 'w') as f:
f.write(config_content)
print_success(f"External server configuration updated (host: {db_host})")
return True
except Exception as e:
print_error(f"Failed to update external config: {e}")
return False
def verify_database_setup():
"""Verify that all tables were created successfully"""
print_step(11, "Verifying Database Setup")
try:
conn = mariadb.connect(**DB_CONFIG)
cursor = conn.cursor()
# Check MariaDB tables
cursor.execute("SHOW TABLES")
tables = [table[0] for table in cursor.fetchall()]
expected_tables = [
'scan1_orders',
'scanfg_orders',
'order_for_labels',
'warehouse_locations',
'permissions',
'role_permissions',
'role_hierarchy',
'permission_audit_log',
'users',
'roles'
]
print("\n📊 MariaDB Tables Status:")
for table in expected_tables:
if table in tables:
print_success(f"Table '{table}' exists")
else:
print_error(f"Table '{table}' missing")
# Check triggers
cursor.execute("SHOW TRIGGERS")
triggers = [trigger[0] for trigger in cursor.fetchall()]
expected_triggers = [
'increment_approved_quantity',
'increment_approved_quantity_fg'
]
print("\n🔧 Database Triggers Status:")
for trigger in expected_triggers:
if trigger in triggers:
print_success(f"Trigger '{trigger}' exists")
else:
print_error(f"Trigger '{trigger}' missing")
cursor.close()
conn.close()
# Check SQLite database
instance_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), '../../instance'))
sqlite_path = os.path.join(instance_folder, 'users.db')
if os.path.exists(sqlite_path):
print_success("SQLite database 'users.db' exists")
else:
print_error("SQLite database 'users.db' missing")
return True
except Exception as e:
print_error(f"Failed to verify database setup: {e}")
return False
def main():
"""Main function to orchestrate the complete database setup"""
print("🚀 Trasabilitate Application - Complete Database Setup")
print(f"Started at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
steps = [
test_database_connection,
create_scan_tables,
create_order_for_labels_table,
create_warehouse_locations_table,
create_permissions_tables,
create_users_table_mariadb,
create_sqlite_tables,
create_database_triggers,
populate_permissions_data,
update_external_config,
verify_database_setup
]
success_count = 0
for step in steps:
if step():
success_count += 1
else:
print(f"\n❌ Setup failed at step: {step.__name__}")
print("Please check the error messages above and resolve the issues.")
sys.exit(1)
print(f"\n{'='*60}")
print("🎉 DATABASE SETUP COMPLETED SUCCESSFULLY!")
print(f"{'='*60}")
print(f"✅ All {success_count} steps completed successfully")
print(f"📅 Completed at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print("\n📋 Setup Summary:")
print(" • MariaDB tables created with triggers")
print(" • SQLite user database initialized")
print(" • Permissions system fully configured")
print(" • Default superadmin user created (username: superadmin, password: superadmin123)")
print(" • Configuration files updated")
print("\n🚀 Your application is ready to run!")
print(" Run: python3 run.py")
if __name__ == "__main__":
main()

View File

@@ -4,47 +4,7 @@ class User(db.Model):
id = db.Column(db.Integer, primary_key=True) id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False) username = db.Column(db.String(80), unique=True, nullable=False)
password = db.Column(db.String(120), nullable=False) password = db.Column(db.String(120), nullable=False)
role = db.Column(db.String(20), nullable=False) # Role: superadmin, admin, manager, worker role = db.Column(db.String(20), nullable=False) # Role: superadmin, administrator, quality, warehouse, scan
modules = db.Column(db.Text, nullable=True) # JSON string of assigned modules: ["quality", "warehouse"]
def __repr__(self): def __repr__(self):
return f'<User {self.username}>' return f'<User {self.username}>'
def get_modules(self):
"""Get user's assigned modules as a list"""
if not self.modules:
return []
try:
import json
return json.loads(self.modules)
except:
return []
def set_modules(self, module_list):
"""Set user's assigned modules from a list"""
if not module_list:
self.modules = None
else:
import json
self.modules = json.dumps(module_list)
def add_module(self, module):
"""Add a module to user's assignments"""
current_modules = self.get_modules()
if module not in current_modules:
current_modules.append(module)
self.set_modules(current_modules)
def remove_module(self, module):
"""Remove a module from user's assignments"""
current_modules = self.get_modules()
if module in current_modules:
current_modules.remove(module)
self.set_modules(current_modules)
def has_module(self, module):
"""Check if user has access to a specific module"""
# Superadmin and admin have access to all modules
if self.role in ['superadmin', 'admin']:
return True
return module in self.get_modules()

View File

@@ -1,226 +0,0 @@
"""
Simplified 4-Tier Role-Based Access Control System
Clear hierarchy: Superadmin → Admin → Manager → Worker
Module-based permissions: Quality, Labels, Warehouse
"""
# APPLICATION MODULES
MODULES = {
'quality': {
'name': 'Quality Control',
'scan_pages': ['quality', 'fg_quality'],
'management_pages': ['quality_reports', 'quality_settings'],
'worker_access': ['scan_only'] # Workers can only scan, no reports
},
'labels': {
'name': 'Label Management',
'scan_pages': ['label_scan'],
'management_pages': ['label_creation', 'label_reports'],
'worker_access': ['scan_only']
},
'warehouse': {
'name': 'Warehouse Management',
'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
}
}
# 4-TIER ROLE STRUCTURE
ROLES = {
'superadmin': {
'name': 'Super Administrator',
'level': 100,
'description': 'Full system access - complete control over all modules and system settings',
'access': {
'all_modules': True,
'all_pages': True,
'restricted_pages': [] # No restrictions
}
},
'admin': {
'name': 'Administrator',
'level': 90,
'description': 'Full app access except role permissions and extension download',
'access': {
'all_modules': True,
'all_pages': True,
'restricted_pages': ['role_permissions', 'download_extension']
}
},
'manager': {
'name': 'Manager',
'level': 70,
'description': 'Complete module access - can manage one or more modules (quality/labels/warehouse)',
'access': {
'all_modules': False, # Only assigned modules
'module_access': 'full', # Full access to assigned modules
'can_cumulate': True, # Can have multiple modules
'restricted_pages': ['role_permissions', 'download_extension', 'system_settings']
}
},
'worker': {
'name': 'Worker',
'level': 50,
'description': 'Limited module access - can perform basic operations in assigned modules',
'access': {
'all_modules': False, # Only assigned modules
'module_access': 'limited', # Limited access (scan pages only)
'can_cumulate': True, # Can have multiple modules
'restricted_pages': ['role_permissions', 'download_extension', 'system_settings', 'reports']
}
}
}
# PAGE ACCESS RULES
PAGE_ACCESS = {
# System pages accessible by role level
'dashboard': {'min_level': 50, 'modules': []},
'settings': {'min_level': 90, 'modules': []},
'role_permissions': {'min_level': 100, 'modules': []}, # Superadmin only
'download_extension': {'min_level': 100, 'modules': []}, # Superadmin only
# Quality module pages
'quality': {'min_level': 50, 'modules': ['quality']},
'fg_quality': {'min_level': 50, 'modules': ['quality']},
'quality_reports': {'min_level': 70, 'modules': ['quality']}, # Manager+ only
'reports': {'min_level': 70, 'modules': ['quality']}, # Manager+ only for quality reports
# Warehouse module pages
'warehouse': {'min_level': 50, 'modules': ['warehouse']},
'move_orders': {'min_level': 50, 'modules': ['warehouse']},
'create_locations': {'min_level': 70, 'modules': ['warehouse']}, # Manager+ only
'warehouse_reports': {'min_level': 70, 'modules': ['warehouse']}, # Manager+ only
# Labels module pages
'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
}
def check_access(user_role, user_modules, page):
"""
Simple access check for the 4-tier system
Args:
user_role (str): User's role (superadmin, admin, manager, worker)
user_modules (list): User's assigned modules ['quality', 'warehouse']
page (str): Page being accessed
Returns:
bool: True if access granted, False otherwise
"""
if user_role not in ROLES:
return False
user_level = ROLES[user_role]['level']
# Check if page exists in our access rules
if page not in PAGE_ACCESS:
return False
page_config = PAGE_ACCESS[page]
# Check minimum level requirement
if user_level < page_config['min_level']:
return False
# Check restricted pages for this role
if page in ROLES[user_role]['access']['restricted_pages']:
return False
# Check module requirements
required_modules = page_config['modules']
if required_modules:
# Page requires specific modules
# Superadmin and admin have access to all modules by default
if ROLES[user_role]['access']['all_modules']:
return True
# Other roles need to have the required module assigned
if not any(module in user_modules for module in required_modules):
return False
return True
def get_user_accessible_pages(user_role, user_modules):
"""
Get list of pages accessible to a user
Args:
user_role (str): User's role
user_modules (list): User's assigned modules
Returns:
list: List of accessible page names
"""
accessible_pages = []
for page in PAGE_ACCESS.keys():
if check_access(user_role, user_modules, page):
accessible_pages.append(page)
return accessible_pages
def validate_user_modules(user_role, user_modules):
"""
Validate that user's module assignment is valid for their role
Args:
user_role (str): User's role
user_modules (list): User's assigned modules
Returns:
tuple: (is_valid, error_message)
"""
if user_role not in ROLES:
return False, "Invalid role"
role_config = ROLES[user_role]
# Superadmin and admin have access to all modules by default
if role_config['access']['all_modules']:
return True, ""
# Manager can have multiple modules
if user_role == 'manager':
if not user_modules:
return False, "Managers must have at least one module assigned"
valid_modules = list(MODULES.keys())
for module in user_modules:
if module not in valid_modules:
return False, f"Invalid module: {module}"
return True, ""
# Worker can have multiple modules now
if user_role == 'worker':
if not user_modules:
return False, "Workers must have at least one module assigned"
valid_modules = list(MODULES.keys())
for module in user_modules:
if module not in valid_modules:
return False, f"Invalid module: {module}"
return True, ""
return True, ""
def get_role_description(role):
"""Get human-readable role description"""
return ROLES.get(role, {}).get('description', 'Unknown role')
def get_available_modules():
"""Get list of available modules"""
return list(MODULES.keys())
def can_access_reports(user_role, user_modules, module):
"""
Check if user can access reports for a specific module
Worker level users cannot access reports
"""
if user_role == 'worker':
return False
if module in user_modules or ROLES[user_role]['access']['all_modules']:
return True
return False

View File

@@ -34,9 +34,9 @@ def get_unprinted_orders_data(limit=100):
# Use printed_labels column # Use printed_labels column
cursor.execute(""" cursor.execute("""
SELECT id, comanda_productie, cod_articol, descr_com_prod, cantitate, SELECT id, comanda_productie, cod_articol, descr_com_prod, cantitate,
com_achiz_client, nr_linie_com_client, customer_name, data_livrare, dimensiune, com_achiz_client, nr_linie_com_client, customer_name,
customer_article_number, open_for_order, line_number, customer_article_number, open_for_order, line_number,
created_at, updated_at, printed_labels, data_livrare, dimensiune printed_labels, created_at, updated_at
FROM order_for_labels FROM order_for_labels
WHERE printed_labels != 1 WHERE printed_labels != 1
ORDER BY created_at DESC ORDER BY created_at DESC
@@ -46,7 +46,7 @@ def get_unprinted_orders_data(limit=100):
# Fallback: get all orders if no printed_labels column # Fallback: get all orders if no printed_labels column
cursor.execute(""" cursor.execute("""
SELECT id, comanda_productie, cod_articol, descr_com_prod, cantitate, SELECT id, comanda_productie, cod_articol, descr_com_prod, cantitate,
com_achiz_client, nr_linie_com_client, customer_name, data_livrare, dimensiune, com_achiz_client, nr_linie_com_client, customer_name,
customer_article_number, open_for_order, line_number, customer_article_number, open_for_order, line_number,
created_at, updated_at created_at, updated_at
FROM order_for_labels FROM order_for_labels
@@ -63,17 +63,17 @@ def get_unprinted_orders_data(limit=100):
'cod_articol': row[2], 'cod_articol': row[2],
'descr_com_prod': row[3], 'descr_com_prod': row[3],
'cantitate': row[4], 'cantitate': row[4],
'com_achiz_client': row[5], 'data_livrare': row[5],
'nr_linie_com_client': row[6], 'dimensiune': row[6],
'customer_name': row[7], 'com_achiz_client': row[7],
'customer_article_number': row[8], 'nr_linie_com_client': row[8],
'open_for_order': row[9], 'customer_name': row[9],
'line_number': row[10], 'customer_article_number': row[10],
'created_at': row[11], 'open_for_order': row[11],
'updated_at': row[12], 'line_number': row[12],
'printed_labels': row[13], 'printed_labels': row[13],
'data_livrare': row[14] or '-', 'created_at': row[14],
'dimensiune': row[15] or '-' 'updated_at': row[15]
}) })
else: else:
orders.append({ orders.append({
@@ -82,18 +82,17 @@ def get_unprinted_orders_data(limit=100):
'cod_articol': row[2], 'cod_articol': row[2],
'descr_com_prod': row[3], 'descr_com_prod': row[3],
'cantitate': row[4], 'cantitate': row[4],
'com_achiz_client': row[5], 'data_livrare': row[5],
'nr_linie_com_client': row[6], 'dimensiune': row[6],
'customer_name': row[7], 'com_achiz_client': row[7],
'customer_article_number': row[8], 'nr_linie_com_client': row[8],
'open_for_order': row[9], 'customer_name': row[9],
'line_number': row[10], 'customer_article_number': row[10],
'created_at': row[11], 'open_for_order': row[11],
'updated_at': row[12], 'line_number': row[12],
# Add default values for missing columns 'printed_labels': 0, # Default to not printed
'data_livrare': '-', 'created_at': row[13],
'dimensiune': '-', 'updated_at': row[14]
'printed_labels': 0
}) })
conn.close() conn.close()

File diff suppressed because it is too large Load Diff

View File

@@ -1,539 +0,0 @@
// FG Quality specific JavaScript - Standalone version
document.addEventListener('DOMContentLoaded', function() {
// Prevent conflicts with main script.js by removing existing listeners
console.log('FG Quality JavaScript loaded');
const reportButtons = document.querySelectorAll('.report-btn');
const reportTable = document.getElementById('report-table');
const reportTitle = document.getElementById('report-title');
const exportCsvButton = document.getElementById('export-csv');
// Calendar elements
const calendarModal = document.getElementById('calendar-modal');
const dateRangeModal = document.getElementById('date-range-modal');
const selectDayReport = document.getElementById('select-day-report');
const selectDayDefectsReport = document.getElementById('select-day-defects-report');
const dateRangeReport = document.getElementById('date-range-report');
const dateRangeDefectsReport = document.getElementById('date-range-defects-report');
let currentReportType = null;
let currentDate = new Date();
let selectedDate = null;
// Clear any existing event listeners by cloning elements
function clearExistingListeners() {
if (selectDayReport) {
const newSelectDayReport = selectDayReport.cloneNode(true);
selectDayReport.parentNode.replaceChild(newSelectDayReport, selectDayReport);
}
if (selectDayDefectsReport) {
const newSelectDayDefectsReport = selectDayDefectsReport.cloneNode(true);
selectDayDefectsReport.parentNode.replaceChild(newSelectDayDefectsReport, selectDayDefectsReport);
}
if (dateRangeReport) {
const newDateRangeReport = dateRangeReport.cloneNode(true);
dateRangeReport.parentNode.replaceChild(newDateRangeReport, dateRangeReport);
}
if (dateRangeDefectsReport) {
const newDateRangeDefectsReport = dateRangeDefectsReport.cloneNode(true);
dateRangeDefectsReport.parentNode.replaceChild(newDateRangeDefectsReport, dateRangeDefectsReport);
}
}
// Clear existing listeners first
clearExistingListeners();
// Re-get elements after cloning
const newSelectDayReport = document.getElementById('select-day-report');
const newSelectDayDefectsReport = document.getElementById('select-day-defects-report');
const newDateRangeReport = document.getElementById('date-range-report');
const newDateRangeDefectsReport = document.getElementById('date-range-defects-report');
// Add event listeners to report buttons
reportButtons.forEach(button => {
const reportType = button.getAttribute('data-report');
if (reportType) {
// Clone to remove existing listeners
const newButton = button.cloneNode(true);
button.parentNode.replaceChild(newButton, button);
newButton.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
console.log('FG Report button clicked:', reportType);
fetchFGReportData(reportType);
});
}
});
// Calendar-based report buttons with FG-specific handlers
if (newSelectDayReport) {
newSelectDayReport.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
console.log('FG Select Day Report clicked');
currentReportType = '6';
showCalendarModal();
});
}
if (newSelectDayDefectsReport) {
newSelectDayDefectsReport.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
console.log('FG Select Day Defects Report clicked');
currentReportType = '8';
showCalendarModal();
});
}
if (newDateRangeReport) {
newDateRangeReport.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
console.log('FG Date Range Report clicked');
currentReportType = '7';
showDateRangeModal();
});
}
if (newDateRangeDefectsReport) {
newDateRangeDefectsReport.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
console.log('FG Date Range Defects Report clicked');
currentReportType = '9';
showDateRangeModal();
});
}
// Function to fetch FG report data
function fetchFGReportData(reportType) {
const url = `/get_fg_report_data?report=${reportType}`;
console.log('Fetching FG data from:', url);
reportTitle.textContent = 'Loading FG data...';
fetch(url)
.then(response => response.json())
.then(data => {
console.log('FG Report data received:', data);
if (data.error) {
reportTitle.textContent = data.error;
return;
}
populateFGTable(data);
updateReportTitle(reportType);
})
.catch(error => {
console.error('Error fetching FG report data:', error);
reportTitle.textContent = 'Error loading FG data.';
});
}
// Function to fetch FG report data for specific dates
function fetchFGDateReportData(reportType, date, startDate = null, endDate = null) {
let url = `/generate_fg_report?report=${reportType}`;
if (date) {
url += `&date=${date}`;
}
if (startDate && endDate) {
url += `&start_date=${startDate}&end_date=${endDate}`;
}
console.log('Fetching FG date report from:', url);
reportTitle.textContent = 'Loading FG data...';
fetch(url)
.then(response => response.json())
.then(data => {
console.log('FG Date report data received:', data);
if (data.error) {
reportTitle.textContent = data.error;
return;
}
populateFGTable(data);
updateDateReportTitle(reportType, date, startDate, endDate);
})
.catch(error => {
console.error('Error fetching FG date report data:', error);
reportTitle.textContent = 'Error loading FG data.';
});
}
// Function to populate the table with FG data
function populateFGTable(data) {
const thead = reportTable.querySelector('thead tr');
const tbody = reportTable.querySelector('tbody');
// Clear existing content
thead.innerHTML = '';
tbody.innerHTML = '';
// Add headers
if (data.headers && data.headers.length > 0) {
data.headers.forEach(header => {
const th = document.createElement('th');
th.textContent = header;
thead.appendChild(th);
});
}
// Add rows
if (data.rows && data.rows.length > 0) {
data.rows.forEach(row => {
const tr = document.createElement('tr');
row.forEach(cell => {
const td = document.createElement('td');
td.textContent = cell || '';
tr.appendChild(td);
});
tbody.appendChild(tr);
});
} else {
// Show no data message
const tr = document.createElement('tr');
const td = document.createElement('td');
td.colSpan = data.headers ? data.headers.length : 1;
td.textContent = data.message || 'No FG data found for the selected criteria.';
td.style.textAlign = 'center';
td.style.fontStyle = 'italic';
td.style.padding = '20px';
tr.appendChild(td);
tbody.appendChild(tr);
}
}
// Function to update report title based on type
function updateReportTitle(reportType) {
const titles = {
'1': 'Daily Complete FG Orders Report',
'2': '5-Day Complete FG Orders Report',
'3': 'FG Items with Defects for Current Day',
'4': 'FG Items with Defects for Last 5 Days',
'5': 'Complete FG Database Report'
};
reportTitle.textContent = titles[reportType] || 'FG Quality Report';
}
// Function to update report title for date-based reports
function updateDateReportTitle(reportType, date, startDate, endDate) {
const titles = {
'6': `FG Daily Report for ${date}`,
'7': `FG Date Range Report (${startDate} to ${endDate})`,
'8': `FG Quality Defects Report for ${date}`,
'9': `FG Quality Defects Range Report (${startDate} to ${endDate})`
};
reportTitle.textContent = titles[reportType] || 'FG Quality Report';
}
// Calendar functionality
function showCalendarModal() {
if (calendarModal) {
calendarModal.style.display = 'block';
generateCalendar();
}
}
function hideCalendarModal() {
if (calendarModal) {
calendarModal.style.display = 'none';
selectedDate = null;
updateConfirmButton();
}
}
function showDateRangeModal() {
if (dateRangeModal) {
dateRangeModal.style.display = 'block';
const today = new Date().toISOString().split('T')[0];
document.getElementById('start-date').value = today;
document.getElementById('end-date').value = today;
}
}
function hideDataRangeModal() {
if (dateRangeModal) {
dateRangeModal.style.display = 'none';
}
}
function generateCalendar() {
const calendarDays = document.getElementById('calendar-days');
const monthYear = document.getElementById('calendar-month-year');
if (!calendarDays || !monthYear) return;
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
monthYear.textContent = `${currentDate.toLocaleString('default', { month: 'long' })} ${year}`;
// Clear previous days
calendarDays.innerHTML = '';
// Get first day of month and number of days
const firstDay = new Date(year, month, 1).getDay();
const daysInMonth = new Date(year, month + 1, 0).getDate();
// Add empty cells for previous month
for (let i = 0; i < firstDay; i++) {
const emptyDay = document.createElement('div');
emptyDay.className = 'calendar-day empty';
calendarDays.appendChild(emptyDay);
}
// Add days of current month
for (let day = 1; day <= daysInMonth; day++) {
const dayElement = document.createElement('div');
dayElement.className = 'calendar-day';
dayElement.textContent = day;
// Check if it's today
const today = new Date();
if (year === today.getFullYear() && month === today.getMonth() && day === today.getDate()) {
dayElement.classList.add('today');
}
dayElement.addEventListener('click', () => {
// Remove previous selection
document.querySelectorAll('.calendar-day.selected').forEach(el => {
el.classList.remove('selected');
});
// Add selection to clicked day
dayElement.classList.add('selected');
// Set selected date
selectedDate = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
console.log('FG Calendar date selected:', selectedDate);
updateConfirmButton();
});
calendarDays.appendChild(dayElement);
}
}
function updateConfirmButton() {
const confirmButton = document.getElementById('confirm-date');
if (confirmButton) {
confirmButton.disabled = !selectedDate;
}
}
// Calendar navigation
const prevMonthBtn = document.getElementById('prev-month');
const nextMonthBtn = document.getElementById('next-month');
if (prevMonthBtn) {
// Clone to remove existing listeners
const newPrevBtn = prevMonthBtn.cloneNode(true);
prevMonthBtn.parentNode.replaceChild(newPrevBtn, prevMonthBtn);
newPrevBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
currentDate.setMonth(currentDate.getMonth() - 1);
generateCalendar();
});
}
if (nextMonthBtn) {
// Clone to remove existing listeners
const newNextBtn = nextMonthBtn.cloneNode(true);
nextMonthBtn.parentNode.replaceChild(newNextBtn, nextMonthBtn);
newNextBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
currentDate.setMonth(currentDate.getMonth() + 1);
generateCalendar();
});
}
// Calendar modal buttons
const cancelDateBtn = document.getElementById('cancel-date');
const confirmDateBtn = document.getElementById('confirm-date');
if (cancelDateBtn) {
// Clone to remove existing listeners
const newCancelBtn = cancelDateBtn.cloneNode(true);
cancelDateBtn.parentNode.replaceChild(newCancelBtn, cancelDateBtn);
newCancelBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
hideCalendarModal();
});
}
if (confirmDateBtn) {
// Clone to remove existing listeners
const newConfirmBtn = confirmDateBtn.cloneNode(true);
confirmDateBtn.parentNode.replaceChild(newConfirmBtn, confirmDateBtn);
newConfirmBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
console.log('FG Calendar confirm clicked with date:', selectedDate, 'report type:', currentReportType);
if (selectedDate && currentReportType) {
fetchFGDateReportData(currentReportType, selectedDate);
hideCalendarModal();
}
});
}
// Date range modal buttons
const cancelDateRangeBtn = document.getElementById('cancel-date-range');
const confirmDateRangeBtn = document.getElementById('confirm-date-range');
if (cancelDateRangeBtn) {
// Clone to remove existing listeners
const newCancelRangeBtn = cancelDateRangeBtn.cloneNode(true);
cancelDateRangeBtn.parentNode.replaceChild(newCancelRangeBtn, cancelDateRangeBtn);
newCancelRangeBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
hideDataRangeModal();
});
}
if (confirmDateRangeBtn) {
// Clone to remove existing listeners
const newConfirmRangeBtn = confirmDateRangeBtn.cloneNode(true);
confirmDateRangeBtn.parentNode.replaceChild(newConfirmRangeBtn, confirmDateRangeBtn);
newConfirmRangeBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const startDate = document.getElementById('start-date').value;
const endDate = document.getElementById('end-date').value;
console.log('FG Date range confirm clicked:', startDate, 'to', endDate, 'report type:', currentReportType);
if (startDate && endDate && currentReportType) {
fetchFGDateReportData(currentReportType, null, startDate, endDate);
hideDataRangeModal();
}
});
}
// Enable/disable date range confirm button
const startDateInput = document.getElementById('start-date');
const endDateInput = document.getElementById('end-date');
function updateDateRangeConfirmButton() {
const confirmBtn = document.getElementById('confirm-date-range');
if (confirmBtn && startDateInput && endDateInput) {
confirmBtn.disabled = !startDateInput.value || !endDateInput.value;
}
}
if (startDateInput) {
startDateInput.addEventListener('change', updateDateRangeConfirmButton);
}
if (endDateInput) {
endDateInput.addEventListener('change', updateDateRangeConfirmButton);
}
// Close modals when clicking outside
window.addEventListener('click', (event) => {
if (event.target === calendarModal) {
hideCalendarModal();
}
if (event.target === dateRangeModal) {
hideDataRangeModal();
}
});
// Close modals with X button
document.querySelectorAll('.close-modal').forEach(closeBtn => {
// Clone to remove existing listeners
const newCloseBtn = closeBtn.cloneNode(true);
closeBtn.parentNode.replaceChild(newCloseBtn, closeBtn);
newCloseBtn.addEventListener('click', (event) => {
event.preventDefault();
event.stopPropagation();
const modal = event.target.closest('.modal');
if (modal) {
modal.style.display = 'none';
}
});
});
// Export functionality
if (exportCsvButton) {
exportCsvButton.addEventListener('click', () => {
const rows = reportTable.querySelectorAll('tr');
if (rows.length === 0) {
alert('No FG data available to export.');
return;
}
const reportTitleText = reportTitle.textContent.trim();
const filename = `${reportTitleText.replace(/\s+/g, '_')}.csv`;
exportTableToCSV(filename);
});
}
// Export to CSV function
function exportTableToCSV(filename) {
const table = reportTable;
const rows = Array.from(table.querySelectorAll('tr'));
const csvContent = rows.map(row => {
const cells = Array.from(row.querySelectorAll('th, td'));
return cells.map(cell => {
let text = cell.textContent.trim();
// Escape quotes and wrap in quotes if necessary
if (text.includes(',') || text.includes('"') || text.includes('\n')) {
text = '"' + text.replace(/"/g, '""') + '"';
}
return text;
}).join(',');
}).join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const link = document.createElement('a');
const url = URL.createObjectURL(blob);
link.setAttribute('href', url);
link.setAttribute('download', filename);
link.style.visibility = 'hidden';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
// Test Database Button
const testDatabaseBtn = document.getElementById('test-database');
if (testDatabaseBtn) {
testDatabaseBtn.addEventListener('click', () => {
console.log('Testing FG database connection...');
reportTitle.textContent = 'Testing FG Database Connection...';
fetch('/test_fg_database')
.then(response => response.json())
.then(data => {
console.log('FG Database test results:', data);
if (data.success) {
reportTitle.textContent = `FG Database Test Results - ${data.total_records} records found`;
// Show alert with summary
alert(`FG Database Test Complete!\n\nConnection: ${data.database_connection}\nTable exists: ${data.table_exists}\nTotal records: ${data.total_records}\nMessage: ${data.message}`);
} else {
reportTitle.textContent = 'FG Database Test Failed';
alert(`FG Database test failed: ${data.message}`);
}
})
.catch(error => {
console.error('FG Database test error:', error);
reportTitle.textContent = 'Error testing FG database.';
alert('Error testing FG database connection.');
});
});
}
console.log('FG Quality JavaScript setup complete');
});

View File

@@ -806,20 +806,6 @@ body.dark-mode .export-description {
background-color: #218838; /* Darker green on hover */ background-color: #218838; /* Darker green on hover */
} }
.go-to-main-reports-btn {
background-color: #007bff; /* Blue background */
color: #fff; /* White text */
padding: 10px 20px;
border-radius: 5px;
text-decoration: none;
margin-left: 10px;
transition: background-color 0.3s ease;
}
.go-to-main-reports-btn:hover {
background-color: #0056b3; /* Darker blue on hover */
}
.draggable-field { .draggable-field {
cursor: move; cursor: move;
padding: 5px; padding: 5px;

View File

@@ -4,16 +4,11 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Flask App{% endblock %}</title> <title>{% block title %}Flask App{% endblock %}</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Font Awesome for icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<!-- Base CSS for common styles --> <!-- Base CSS for common styles -->
<link rel="stylesheet" href="{{ url_for('static', filename='css/base.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/base.css') }}">
<!-- Legacy CSS for backward compatibility (temporarily) --> <!-- Legacy CSS for backward compatibility (temporarily) -->
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<!-- Page-specific CSS --> <!-- Page-specific CSS -->
{% block extra_css %}{% endblock %}
{% block head %}{% endblock %} {% block head %}{% endblock %}
</head> </head>
<body class="light-mode"> <body class="light-mode">
@@ -26,12 +21,8 @@
<span class="page-title">Welcome to Dashboard</span> <span class="page-title">Welcome to Dashboard</span>
{% elif request.endpoint == 'main.settings' %} {% elif request.endpoint == 'main.settings' %}
<span class="page-title">Settings</span> <span class="page-title">Settings</span>
{% elif request.endpoint == 'main.reports' %}
<span class="page-title">Reports Module</span>
{% elif request.endpoint == 'main.quality' %} {% elif request.endpoint == 'main.quality' %}
<span class="page-title">Quality Module</span> <span class="page-title">Quality Module</span>
{% elif request.endpoint == 'main.fg_quality' %}
<span class="page-title">FG Quality Module</span>
{% elif request.endpoint == 'main.warehouse' %} {% elif request.endpoint == 'main.warehouse' %}
<span class="page-title">Warehouse Module</span> <span class="page-title">Warehouse Module</span>
{% elif request.endpoint == 'main.scan' %} {% elif request.endpoint == 'main.scan' %}
@@ -43,9 +34,6 @@
{% 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'] %} {% 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> <a href="{{ url_for('main.etichete') }}" class="btn go-to-main-etichete-btn">Main Page Etichete</a>
{% endif %} {% 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> <a href="{{ url_for('main.dashboard') }}" class="btn go-to-dashboard-btn">Go to Dashboard</a>
{% if 'user' in session %} {% if 'user' in session %}
<span class="user-info">You are logged in as {{ session['user'] }}</span> <span class="user-info">You are logged in as {{ session['user'] }}</span>
@@ -58,11 +46,6 @@
<div class="main-content"> <div class="main-content">
{% block content %}{% endblock %} {% block content %}{% endblock %}
</div> </div>
{% if request.endpoint != 'main.fg_quality' %}
<script src="{{ url_for('static', filename='script.js') }}"></script> <script src="{{ url_for('static', filename='script.js') }}"></script>
{% endif %}
<!-- Bootstrap JavaScript -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
</body> </body>
</html> </html>

View File

@@ -2,7 +2,6 @@
{% block title %}Create Warehouse Locations{% endblock %} {% block title %}Create Warehouse Locations{% endblock %}
{% block head %} {% block head %}
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/warehouse.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/warehouse.css') }}">
{% endblock %} {% endblock %}
@@ -401,72 +400,20 @@ document.addEventListener('DOMContentLoaded', function() {
// Edit button functionality // Edit button functionality
editButton.addEventListener('click', function() { editButton.addEventListener('click', function() {
console.log('Edit button clicked', selectedLocation);
if (selectedLocation) { if (selectedLocation) {
openEditModal(selectedLocation); openEditModal(selectedLocation);
} else {
showNotification('❌ No location selected', 'error');
} }
}); });
// Delete button functionality // Delete button functionality
deleteButton.addEventListener('click', function() { deleteButton.addEventListener('click', function() {
console.log('Delete button clicked', selectedLocation);
if (selectedLocation) { if (selectedLocation) {
openDeleteModal(selectedLocation); openDeleteModal(selectedLocation);
} else {
showNotification('❌ No location selected', 'error');
} }
}); });
// Initialize QZ Tray // Initialize QZ Tray
initializeQZTray(); initializeQZTray();
// Handle edit form submission
document.getElementById('edit-form').addEventListener('submit', function(e) {
e.preventDefault();
const formData = new FormData(this);
const data = {
location_id: parseInt(formData.get('location_id')),
location_code: formData.get('location_code'),
size: formData.get('size') ? parseInt(formData.get('size')) : null,
description: formData.get('description') || null
};
console.log('Attempting to update location:', data);
// Send update request
fetch("{{ url_for('warehouse.update_location') }}", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data)
})
.then(response => {
console.log('Update response status:', response.status);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(result => {
console.log('Update result:', result);
if (result.success) {
showNotification('✅ Location updated successfully!', 'success');
closeEditModal();
// Reload page to show changes
setTimeout(() => window.location.reload(), 1000);
} else {
showNotification('❌ Error updating location: ' + result.error, 'error');
}
})
.catch(error => {
console.error('Error:', error);
showNotification('❌ Error updating location: ' + error.message, 'error');
});
});
}); });
// Print barcode function with enhanced QZ Tray support // Print barcode function with enhanced QZ Tray support
@@ -490,7 +437,7 @@ async function printLocationBarcode() {
printStatus.textContent = 'Generating label...'; printStatus.textContent = 'Generating label...';
// Generate PDF for the 4x8cm label // Generate PDF for the 4x8cm label
const response = await fetch("{{ url_for('warehouse.generate_location_label_pdf') }}", { const response = await fetch('/generate_location_label_pdf', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -622,31 +569,22 @@ document.getElementById('edit-form').addEventListener('submit', function(e) {
const formData = new FormData(this); const formData = new FormData(this);
const data = { const data = {
location_id: parseInt(formData.get('location_id')), location_id: formData.get('location_id'),
location_code: formData.get('location_code'), location_code: formData.get('location_code'),
size: formData.get('size') ? parseInt(formData.get('size')) : null, size: formData.get('size'),
description: formData.get('description') || null description: formData.get('description')
}; };
console.log('Attempting to update location:', data);
// Send update request // Send update request
fetch("{{ url_for('warehouse.update_location') }}", { fetch('/update_location', {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
body: JSON.stringify(data) body: JSON.stringify(data)
}) })
.then(response => { .then(response => response.json())
console.log('Update response status:', response.status);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(result => { .then(result => {
console.log('Update result:', result);
if (result.success) { if (result.success) {
showNotification('✅ Location updated successfully!', 'success'); showNotification('✅ Location updated successfully!', 'success');
closeEditModal(); closeEditModal();
@@ -662,46 +600,6 @@ document.getElementById('edit-form').addEventListener('submit', function(e) {
}); });
}); });
// Handle delete confirmation
function confirmDelete() {
const locationId = document.getElementById('delete-confirm-id').textContent;
console.log('Attempting to delete location ID:', locationId);
// Send delete request
fetch("{{ url_for('warehouse.delete_location') }}", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
location_id: parseInt(locationId)
})
})
.then(response => {
console.log('Delete response status:', response.status);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(result => {
console.log('Delete result:', result);
if (result.success) {
showNotification('✅ Location deleted successfully!', 'success');
closeDeleteModal();
// Reload page to show changes
setTimeout(() => window.location.reload(), 1000);
} else {
showNotification('❌ Error deleting location: ' + result.error, 'error');
}
})
.catch(error => {
console.error('Error:', error);
showNotification('❌ Error deleting location: ' + error.message, 'error');
});
}
// Handle delete confirmation // Handle delete confirmation
function confirmDelete() { function confirmDelete() {
const locationId = document.getElementById('delete-confirm-id').textContent; const locationId = document.getElementById('delete-confirm-id').textContent;
@@ -746,271 +644,4 @@ window.addEventListener('click', function(event) {
</script> </script>
</div> </div>
</div> </div>
<!-- Edit Location Modal -->
<div id="edit-modal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3>Edit Location</h3>
<span class="close" onclick="closeEditModal()">&times;</span>
</div>
<div class="modal-body">
<form id="edit-form">
<input type="hidden" id="edit-location-id" name="location_id">
<div class="form-group">
<label for="edit-location-code">Location Code:</label>
<input type="text" id="edit-location-code" name="location_code" maxlength="12" required>
</div>
<div class="form-group">
<label for="edit-size">Size:</label>
<input type="number" id="edit-size" name="size">
</div>
<div class="form-group">
<label for="edit-description">Description:</label>
<input type="text" id="edit-description" name="description" maxlength="250">
</div>
<div class="modal-buttons">
<button type="button" class="btn btn-secondary" onclick="closeEditModal()">Cancel</button>
<button type="submit" class="btn btn-primary">Update Location</button>
</div>
</form>
</div>
</div>
</div>
<!-- Delete Location Modal -->
<div id="delete-modal" class="modal" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3>Delete Location</h3>
<span class="close" onclick="closeDeleteModal()">&times;</span>
</div>
<div class="modal-body">
<p>Are you sure you want to delete this location?</p>
<div class="delete-info">
<strong>ID:</strong> <span id="delete-confirm-id"></span><br>
<strong>Code:</strong> <span id="delete-confirm-code"></span><br>
<strong>Size:</strong> <span id="delete-confirm-size"></span><br>
<strong>Description:</strong> <span id="delete-confirm-description"></span>
</div>
<p style="color: #d32f2f; font-weight: bold;">This action cannot be undone!</p>
</div>
<div class="modal-buttons">
<button type="button" class="btn btn-secondary" onclick="closeDeleteModal()">Cancel</button>
<button type="button" class="btn btn-danger" onclick="confirmDelete()">Delete Location</button>
</div>
</div>
</div>
<style>
/* Modal Styles */
.modal {
display: none;
position: fixed;
z-index: 1000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: var(--app-overlay-bg, rgba(0, 0, 0, 0.5));
align-items: center;
justify-content: center;
}
.modal-content {
background-color: var(--app-card-bg, #f8fafc);
color: var(--app-card-text, #1e293b);
padding: 0;
border-radius: 8px;
width: 90%;
max-width: 500px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
animation: modalFadeIn 0.3s ease-out;
}
@keyframes modalFadeIn {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 25px;
border-bottom: 1px solid #cbd5e1;
background-color: var(--app-card-bg, #f8fafc);
}
.modal-header h3 {
margin: 0;
font-size: 1.3em;
font-weight: 600;
color: var(--app-card-text, #1e293b);
}
.close {
color: var(--app-card-text, #1e293b);
font-size: 28px;
font-weight: bold;
cursor: pointer;
line-height: 1;
padding: 0;
background: none;
border: none;
border-radius: 50%;
width: 35px;
height: 35px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
}
.close:hover,
.close:focus {
color: #e11d48;
background-color: #f1f5f9;
transform: scale(1.1);
}
.modal-body {
padding: 25px;
background-color: var(--app-card-bg, #f8fafc);
}
.form-group {
margin-bottom: 20px;
}
.form-group:last-of-type {
margin-bottom: 0;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: var(--app-label-text, #334155);
font-size: 0.95em;
}
.form-group input {
width: 100%;
padding: 12px 15px;
border: 1px solid #cbd5e1;
border-radius: 6px;
background-color: var(--app-input-bg, #e2e8f0);
color: var(--app-input-text, #1e293b);
font-size: 14px;
box-sizing: border-box;
transition: border-color 0.2s ease;
}
.form-group input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.modal-buttons {
display: flex;
gap: 12px;
justify-content: flex-end;
padding: 20px 25px;
border-top: 1px solid #cbd5e1;
background-color: var(--app-card-bg, #f8fafc);
}
.modal-buttons .btn {
padding: 10px 20px;
border-radius: 6px;
font-weight: 600;
min-width: 100px;
transition: all 0.2s ease;
border: none;
cursor: pointer;
}
.modal-buttons .btn-primary {
background: #1e293b;
color: #f8fafc;
}
.modal-buttons .btn-primary:hover {
background: #0f172a;
}
.modal-buttons .btn-secondary {
background: #e11d48;
color: #fff;
}
.modal-buttons .btn-secondary:hover {
background: #be185d;
}
.delete-info {
background-color: #fef3c7;
border: 1px solid #f59e0b;
padding: 15px;
border-radius: 6px;
margin: 15px 0;
border-left: 4px solid #f59e0b;
color: var(--app-card-text, #1e293b);
}
.delete-info strong {
color: var(--text-color, #333);
}
.btn-primary {
background-color: var(--primary-color, #007bff);
color: white;
border: 2px solid var(--primary-color, #007bff);
}
.btn-primary:hover {
background-color: var(--primary-hover-color, #0056b3);
border-color: var(--primary-hover-color, #0056b3);
transform: translateY(-1px);
}
.btn-secondary {
background-color: var(--secondary-color, #6c757d);
color: white;
border: 2px solid var(--secondary-color, #6c757d);
}
.btn-secondary:hover {
background-color: var(--secondary-hover-color, #545b62);
border-color: var(--secondary-hover-color, #545b62);
transform: translateY(-1px);
}
.btn-danger {
background-color: var(--danger-color, #dc3545);
color: white;
border: 2px solid var(--danger-color, #dc3545);
}
.btn-danger:hover {
background-color: var(--danger-hover-color, #c82333);
border-color: var(--danger-hover-color, #c82333);
transform: translateY(-1px);
}
</style>
{% endblock %} {% endblock %}

View File

@@ -7,12 +7,15 @@
<!-- Row of evenly distributed cards --> <!-- Row of evenly distributed cards -->
<div class="dashboard-card"> <div class="dashboard-card">
<h3>Quality Module</h3> <h3>Access Scanning Module</h3>
<p>Final scanning module for production orders and quality reports access.</p> <p>Final scanning module for production orders</p>
<div style="display: flex; gap: 10px; flex-wrap: wrap;"> <a href="{{ url_for('main.main_scan') }}" class="btn">Launch Scanning Module</a>
<a href="{{ url_for('main.main_scan') }}" class="btn">Launch Scanning Module</a> </div>
<a href="{{ url_for('main.reports') }}" class="btn">Access to Quality reports</a>
</div> <div class="dashboard-card">
<h3>Access Reports Module</h3>
<p>Module for verification and quality settings configuration.</p>
<a href="{{ url_for('main.quality') }}" class="btn">Launch Reports Module</a>
</div> </div>

View File

@@ -1,440 +0,0 @@
{% extends "base.html" %}
{% block title %}FG Quality Module{% endblock %}
{% block content %}
<div class="scan-container">
<!-- Reports Card -->
<div class="card report-form-card">
<h3>FG Quality Reports</h3>
<!-- Reports List with Label-Button Layout -->
<div class="reports-grid">
<div class="form-centered">
<label class="report-description">Daily report will export all FG orders scanned at quality scanning points</label>
<button class="btn report-btn" data-report="1">Daily Complete FG Orders Report</button>
</div>
<div class="form-centered">
<label class="report-description">Select day for daily FG report will export all orders scanned at quality scanning points</label>
<button class="btn report-btn" id="select-day-report">Select Day Daily FG Report</button>
</div>
<div class="form-centered">
<label class="report-description">Select date range for custom FG report - from start date 00:00 to end date 23:59</label>
<button class="btn report-btn" id="date-range-report">Date Range FG Report</button>
</div>
<div class="form-centered">
<label class="report-description">5-day report will export all FG orders scanned at quality scanning points</label>
<button class="btn report-btn" data-report="2">5-Day Complete FG Orders Report</button>
</div>
<div class="form-centered">
<label class="report-description">Report on FG items with quality issues for the current day</label>
<button class="btn report-btn" data-report="3">Report on FG Items with Defects for the Current Day</button>
</div>
<div class="form-centered">
<label class="report-description">Select specific day for FG quality defects report - items with quality issues</label>
<button class="btn report-btn" id="select-day-defects-report">Select Day FG Quality Defects Report</button>
</div>
<div class="form-centered">
<label class="report-description">Select date range for FG quality defects report - items with quality issues between two dates</label>
<button class="btn report-btn" id="date-range-defects-report">Date Range FG Quality Defects Report</button>
</div>
<div class="form-centered">
<label class="report-description">Report on FG items with quality issues for the last 5 days</label>
<button class="btn report-btn" data-report="4">Report on FG Items with Defects for the Last 5 Days</button>
</div>
<div class="form-centered">
<label class="report-description">Report all FG entries from the database</label>
<button class="btn report-btn" data-report="5">Report FG Database</button>
</div>
</div>
<!-- Separator -->
<div class="report-separator"></div>
<!-- Export Section -->
<div class="export-section">
<div class="form-centered last-buttons">
<label class="export-description">Export current report as:</label>
<div class="button-row">
<button class="btn export-btn" id="export-csv">Export CSV</button>
<!-- <button class="btn export-btn" id="export-excel">Export excell</button> -->
{% if session.get('role') == 'superadmin' %}
<button class="btn export-btn test-db-btn" id="test-database">Test FG Database</button>
{% endif %}
</div>
</div>
</div>
</div>
<!-- Data Display Card -->
<div class="card report-table-card">
<h3 id="report-title">No data to display, please select a report.</h3>
<div class="report-table-container">
<table class="scan-table" id="report-table">
<thead>
<tr>
<!-- Table headers will be dynamically populated -->
</tr>
</thead>
<tbody>
<!-- Table data will be dynamically populated -->
</tbody>
</table>
</div>
</div>
</div>
<!-- Calendar Popup Modal -->
<div id="calendar-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h4>Select Date for Daily FG Report</h4>
<span class="close-modal">&times;</span>
</div>
<div class="modal-body">
<div class="calendar-container">
<div class="calendar-header">
<button id="prev-month" class="calendar-nav">&lt;</button>
<h3 id="calendar-month-year"></h3>
<button id="next-month" class="calendar-nav">&gt;</button>
</div>
<div class="calendar-grid">
<div class="calendar-weekdays">
<div>Sun</div>
<div>Mon</div>
<div>Tue</div>
<div>Wed</div>
<div>Thu</div>
<div>Fri</div>
<div>Sat</div>
</div>
<div class="calendar-days" id="calendar-days">
<!-- Days will be populated by JavaScript -->
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="cancel-date">Cancel</button>
<button class="btn btn-primary" id="confirm-date" disabled>Generate Report</button>
</div>
</div>
</div>
<!-- Date Range Popup Modal -->
<div id="date-range-modal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h4>Select Date Range for FG Report</h4>
<span class="close-modal" id="close-date-range">&times;</span>
</div>
<div class="modal-body">
<div class="date-range-container">
<div class="date-input-group">
<label for="start-date">Start Date:</label>
<input type="date" id="start-date" class="date-input" />
<small class="date-help">Report will include data from 00:00:00</small>
</div>
<div class="date-input-group">
<label for="end-date">End Date:</label>
<input type="date" id="end-date" class="date-input" />
<small class="date-help">Report will include data until 23:59:59</small>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="cancel-date-range">Cancel</button>
<button class="btn btn-primary" id="confirm-date-range" disabled>Generate Report</button>
</div>
</div>
</div>
{% endblock %}
{% block head %}
<script>
// Debug check - this will run immediately when the page loads
console.log('FG Quality page loaded - checking script conflicts');
window.addEventListener('DOMContentLoaded', function() {
console.log('FG Quality DOM loaded');
// Add a visible indicator that our script loaded
setTimeout(() => {
const titleElement = document.getElementById('report-title');
if (titleElement && titleElement.textContent === 'No data to display, please select a report.') {
titleElement.textContent = '🟢 FG Quality Module Ready - Select a report';
}
}, 100);
});
</script>
<script src="{{ url_for('static', filename='fg_quality.js') }}"></script>
<script>
// Theme functionality for FG Quality page
document.addEventListener('DOMContentLoaded', function() {
const body = document.body;
const themeToggleButton = document.getElementById('theme-toggle');
// Helper function to update the theme toggle button text
function updateThemeToggleButtonText() {
if (body.classList.contains('dark-mode')) {
themeToggleButton.textContent = 'Change to Light Mode';
} else {
themeToggleButton.textContent = 'Change to Dark Mode';
}
}
// Check and apply the saved theme from localStorage
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
body.classList.toggle('dark-mode', savedTheme === 'dark');
}
// Update the button text based on the current theme
if (themeToggleButton) {
updateThemeToggleButtonText();
// Toggle the theme on button click
themeToggleButton.addEventListener('click', () => {
const isDarkMode = body.classList.toggle('dark-mode');
localStorage.setItem('theme', isDarkMode ? 'dark' : 'light');
updateThemeToggleButtonText(); // Update the button text after toggling
});
}
});
// Override script - runs after both script.js and fg_quality.js
window.addEventListener('DOMContentLoaded', function() {
// Wait a bit to ensure all scripts have loaded
setTimeout(() => {
console.log('🔧 FG Quality Override Script - Fixing event handlers');
// Override the problematic calendar functionality
const selectDayReport = document.getElementById('select-day-report');
const selectDayDefectsReport = document.getElementById('select-day-defects-report');
const dateRangeReport = document.getElementById('date-range-report');
const dateRangeDefectsReport = document.getElementById('date-range-defects-report');
let currentFGReportType = null;
let selectedFGDate = null;
// Clear and re-add event listeners with FG-specific functionality
if (selectDayReport) {
// Remove all existing listeners by cloning
const newSelectDayReport = selectDayReport.cloneNode(true);
selectDayReport.parentNode.replaceChild(newSelectDayReport, selectDayReport);
newSelectDayReport.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
console.log('🎯 FG Select Day Report clicked');
currentFGReportType = '6';
showFGCalendarModal();
});
}
if (dateRangeReport) {
const newDateRangeReport = dateRangeReport.cloneNode(true);
dateRangeReport.parentNode.replaceChild(newDateRangeReport, dateRangeReport);
newDateRangeReport.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
console.log('🎯 FG Date Range Report clicked');
currentFGReportType = '7';
showFGDateRangeModal();
});
}
// FG-specific calendar modal functionality
function showFGCalendarModal() {
const calendarModal = document.getElementById('calendar-modal');
if (calendarModal) {
calendarModal.style.display = 'block';
generateFGCalendar();
}
}
function showFGDateRangeModal() {
const dateRangeModal = document.getElementById('date-range-modal');
if (dateRangeModal) {
dateRangeModal.style.display = 'block';
const today = new Date().toISOString().split('T')[0];
document.getElementById('start-date').value = today;
document.getElementById('end-date').value = today;
}
}
function generateFGCalendar() {
const calendarDays = document.getElementById('calendar-days');
const monthYear = document.getElementById('calendar-month-year');
if (!calendarDays || !monthYear) return;
const currentDate = new Date();
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
monthYear.textContent = `${currentDate.toLocaleString('default', { month: 'long' })} ${year}`;
// Clear previous days
calendarDays.innerHTML = '';
// Get first day of month and number of days
const firstDay = new Date(year, month, 1).getDay();
const daysInMonth = new Date(year, month + 1, 0).getDate();
// Add empty cells for previous month
for (let i = 0; i < firstDay; i++) {
const emptyDay = document.createElement('div');
emptyDay.className = 'calendar-day empty';
calendarDays.appendChild(emptyDay);
}
// Add days of current month
for (let day = 1; day <= daysInMonth; day++) {
const dayElement = document.createElement('div');
dayElement.className = 'calendar-day';
dayElement.textContent = day;
// Highlight October 15 as it has FG data
if (month === 9 && day === 15) { // October is month 9
dayElement.style.backgroundColor = '#e3f2fd';
dayElement.style.border = '2px solid #2196f3';
dayElement.title = 'FG data available';
}
dayElement.addEventListener('click', () => {
// Remove previous selection
document.querySelectorAll('.calendar-day.selected').forEach(el => {
el.classList.remove('selected');
});
// Add selection to clicked day
dayElement.classList.add('selected');
// Set selected date
selectedFGDate = `${year}-${String(month + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
console.log('🗓️ FG Calendar date selected:', selectedFGDate);
// Enable confirm button
const confirmButton = document.getElementById('confirm-date');
if (confirmButton) {
confirmButton.disabled = false;
}
});
calendarDays.appendChild(dayElement);
}
}
// Override confirm button
const confirmDateBtn = document.getElementById('confirm-date');
if (confirmDateBtn) {
const newConfirmBtn = confirmDateBtn.cloneNode(true);
confirmDateBtn.parentNode.replaceChild(newConfirmBtn, confirmDateBtn);
newConfirmBtn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
console.log('✅ FG Calendar confirm clicked:', selectedFGDate, 'Report type:', currentFGReportType);
if (selectedFGDate && currentFGReportType) {
const url = `/generate_fg_report?report=${currentFGReportType}&date=${selectedFGDate}`;
console.log('🚀 Calling FG endpoint:', url);
document.getElementById('report-title').textContent = 'Loading FG data...';
fetch(url)
.then(response => response.json())
.then(data => {
console.log('📊 FG Data received:', data);
const table = document.getElementById('report-table');
const thead = table.querySelector('thead tr');
const tbody = table.querySelector('tbody');
// Clear existing content
thead.innerHTML = '';
tbody.innerHTML = '';
// Add headers
if (data.headers) {
data.headers.forEach(header => {
const th = document.createElement('th');
th.textContent = header;
thead.appendChild(th);
});
}
// Add rows
if (data.rows && data.rows.length > 0) {
data.rows.forEach(row => {
const tr = document.createElement('tr');
row.forEach(cell => {
const td = document.createElement('td');
td.textContent = cell || '';
tr.appendChild(td);
});
tbody.appendChild(tr);
});
document.getElementById('report-title').textContent = `FG Daily Report for ${selectedFGDate} (${data.rows.length} records)`;
} else {
const tr = document.createElement('tr');
const td = document.createElement('td');
td.colSpan = data.headers ? data.headers.length : 1;
td.textContent = data.message || 'No FG data found';
td.style.textAlign = 'center';
tr.appendChild(td);
tbody.appendChild(tr);
document.getElementById('report-title').textContent = `FG Daily Report for ${selectedFGDate} - No data`;
}
})
.catch(error => {
console.error('❌ Error fetching FG data:', error);
document.getElementById('report-title').textContent = 'Error loading FG data';
});
// Hide modal
document.getElementById('calendar-modal').style.display = 'none';
}
});
}
// Override date range confirm button
const confirmDateRangeBtn = document.getElementById('confirm-date-range');
if (confirmDateRangeBtn) {
const newConfirmRangeBtn = confirmDateRangeBtn.cloneNode(true);
confirmDateRangeBtn.parentNode.replaceChild(newConfirmRangeBtn, confirmDateRangeBtn);
newConfirmRangeBtn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
const startDate = document.getElementById('start-date').value;
const endDate = document.getElementById('end-date').value;
console.log('📅 FG Date range confirm:', startDate, 'to', endDate);
if (startDate && endDate && currentFGReportType) {
const url = `/generate_fg_report?report=${currentFGReportType}&start_date=${startDate}&end_date=${endDate}`;
console.log('🚀 Calling FG range endpoint:', url);
fetch(url)
.then(response => response.json())
.then(data => {
console.log('📊 FG Range data received:', data);
// Handle response similar to above
document.getElementById('report-title').textContent = `FG Date Range Report (${startDate} to ${endDate})`;
})
.catch(error => {
console.error('❌ Error fetching FG range data:', error);
});
// Hide modal
document.getElementById('date-range-modal').style.display = 'none';
}
});
}
console.log('✅ FG Quality Override Script - Event handlers fixed');
}, 500); // Wait 500ms to ensure all other scripts are loaded
});
</script>
{% endblock %}

View File

@@ -23,80 +23,6 @@ document.addEventListener('DOMContentLoaded', function() {
const defectCodeInput = document.getElementById('defect_code'); const defectCodeInput = document.getElementById('defect_code');
const form = document.getElementById('fg-scan-form'); const form = document.getElementById('fg-scan-form');
// Restore saved operator code from localStorage (only Quality Operator Code)
const savedOperatorCode = localStorage.getItem('fg_scan_operator_code');
if (savedOperatorCode) {
operatorCodeInput.value = savedOperatorCode;
}
// Check if we need to clear fields after a successful submission
const shouldClearAfterSubmit = localStorage.getItem('fg_scan_clear_after_submit');
if (shouldClearAfterSubmit === 'true') {
// Clear the flag
localStorage.removeItem('fg_scan_clear_after_submit');
localStorage.removeItem('fg_scan_last_cp');
localStorage.removeItem('fg_scan_last_defect');
// Clear CP code, OC1, OC2, and defect code for next scan
cpCodeInput.value = '';
oc1CodeInput.value = '';
oc2CodeInput.value = '';
defectCodeInput.value = '';
// Show success indicator
setTimeout(function() {
// Focus on CP code field for next scan
cpCodeInput.focus();
// Add visual feedback
const successIndicator = document.createElement('div');
successIndicator.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #4CAF50;
color: white;
padding: 10px 20px;
border-radius: 5px;
z-index: 1000;
box-shadow: 0 2px 10px rgba(0,0,0,0.2);
font-weight: bold;
`;
successIndicator.textContent = '✅ Scan recorded! Ready for next scan';
document.body.appendChild(successIndicator);
// Remove success indicator after 3 seconds
setTimeout(function() {
if (successIndicator.parentNode) {
successIndicator.parentNode.removeChild(successIndicator);
}
}, 3000);
}, 100);
}
// Focus on the first empty required field (only if not clearing after submit)
if (shouldClearAfterSubmit !== 'true') {
if (!operatorCodeInput.value) {
operatorCodeInput.focus();
} else if (!oc1CodeInput.value) {
oc1CodeInput.focus();
} else if (!oc2CodeInput.value) {
oc2CodeInput.focus();
} else if (!cpCodeInput.value) {
cpCodeInput.focus();
} else {
defectCodeInput.focus();
}
}
// Save operator codes to localStorage when they change (only Quality Operator Code)
operatorCodeInput.addEventListener('input', function() {
if (this.value.startsWith('OP') && this.value.length >= 3) {
localStorage.setItem('fg_scan_operator_code', this.value);
}
});
// Create error message element for operator code // Create error message element for operator code
const operatorErrorMessage = document.createElement('div'); const operatorErrorMessage = document.createElement('div');
operatorErrorMessage.className = 'error-message'; operatorErrorMessage.className = 'error-message';
@@ -412,11 +338,6 @@ document.addEventListener('DOMContentLoaded', function() {
const seconds = String(now.getSeconds()).padStart(2, '0'); const seconds = String(now.getSeconds()).padStart(2, '0');
timeInput.value = `${hours}:${minutes}:${seconds}`; timeInput.value = `${hours}:${minutes}:${seconds}`;
// Save current CP code and defect code to localStorage for clearing after reload
localStorage.setItem('fg_scan_clear_after_submit', 'true');
localStorage.setItem('fg_scan_last_cp', cpCodeInput.value);
localStorage.setItem('fg_scan_last_defect', defectCodeInput.value);
// Submit the form // Submit the form
form.submit(); form.submit();
} }
@@ -478,27 +399,6 @@ document.addEventListener('DOMContentLoaded', function() {
} }
} }
}); });
// Add functionality for clear saved codes button
const clearSavedBtn = document.getElementById('clear-saved-btn');
clearSavedBtn.addEventListener('click', function() {
if (confirm('Clear saved Quality Operator code? You will need to re-enter it.')) {
// Clear localStorage (only Quality Operator Code)
localStorage.removeItem('fg_scan_operator_code');
localStorage.removeItem('fg_scan_clear_after_submit');
localStorage.removeItem('fg_scan_last_cp');
localStorage.removeItem('fg_scan_last_defect');
// Clear Quality Operator Code field only
operatorCodeInput.value = '';
// Focus on operator code field
operatorCodeInput.focus();
// Show confirmation
alert('✅ Saved Quality Operator code cleared! Please re-enter your operator code.');
}
});
}); });
</script> </script>
{% endblock %} {% endblock %}
@@ -530,7 +430,6 @@ document.addEventListener('DOMContentLoaded', function() {
<input type="text" id="time" name="time" value="{{ now().strftime('%H:%M:%S') }}" readonly> <input type="text" id="time" name="time" value="{{ now().strftime('%H:%M:%S') }}" readonly>
<button type="submit" class="btn">Submit</button> <button type="submit" class="btn">Submit</button>
<button type="button" class="btn" id="clear-saved-btn" style="background-color: #ff6b6b; margin-left: 10px;">Clear Quality Operator</button>
</form> </form>
</div> </div>

View File

@@ -15,7 +15,7 @@
<p>Upload new orders or view existing orders and manage label data for printing.</p> <p>Upload new orders or view existing orders and manage label data for printing.</p>
<div style="display: flex; gap: 10px; flex-wrap: wrap;"> <div style="display: flex; gap: 10px; flex-wrap: wrap;">
<a href="{{ url_for('main.upload_data') }}" class="btn">Upload Orders</a> <a href="{{ url_for('main.upload_data') }}" class="btn">Upload Orders</a>
<a href="{{ url_for('main.view_orders') }}" class="btn">View Orders</a> <a href="{{ url_for('main.get_unprinted_orders') }}" class="btn">View Orders</a>
</div> </div>
</div> </div>

View File

@@ -1,40 +0,0 @@
{% extends "base.html" %}
{% block title %}Reports Module{% endblock %}
{% block content %}
<div class="reports-container">
<h1>Reports Module</h1>
<p>Access different quality and production reporting modules for data analysis and verification.</p>
<!-- Row of evenly distributed cards -->
<div class="dashboard-container">
<!-- Card 1: Quality Reports -->
<div class="dashboard-card">
<h3>Quality Reports</h3>
<p>Access quality scanning reports and analysis for production process verification.</p>
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<a href="{{ url_for('main.quality') }}" class="btn">Launch Quality Reports</a>
</div>
</div>
<!-- Card 2: FG Quality Reports -->
<div class="dashboard-card">
<h3>FG Quality Reports</h3>
<p>Finished Goods quality reports and analysis for final product verification.</p>
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<a href="{{ url_for('main.fg_quality') }}" class="btn">Launch FG Quality Reports</a>
</div>
</div>
<!-- Card 3: Additional Reports (Placeholder for future expansion) -->
<div class="dashboard-card">
<h3>Additional Reports</h3>
<p>Access additional reporting modules and data analysis tools.</p>
<div style="display: flex; gap: 10px; flex-wrap: wrap;">
<button class="btn" disabled style="opacity: 0.5;">Coming Soon</button>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -10,9 +10,8 @@
/* Enhanced table styling */ /* Enhanced table styling */
.card.scan-table-card table.print-module-table.scan-table thead th { .card.scan-table-card table.print-module-table.scan-table thead th {
border-bottom: 2px solid var(--app-border-color, #dee2e6) !important; border-bottom: 2px solid #dee2e6 !important;
background-color: var(--app-table-header-bg, #2a3441) !important; background-color: #f8f9fa !important;
color: var(--app-text-color, #ffffff) !important;
padding: 0.25rem 0.4rem !important; padding: 0.25rem 0.4rem !important;
text-align: left !important; text-align: left !important;
font-weight: 600 !important; font-weight: 600 !important;
@@ -23,21 +22,13 @@
.card.scan-table-card table.print-module-table.scan-table { .card.scan-table-card table.print-module-table.scan-table {
width: 100% !important; width: 100% !important;
border-collapse: collapse !important; border-collapse: collapse !important;
background-color: var(--app-card-bg, #2a3441) !important;
} }
.card.scan-table-card table.print-module-table.scan-table tbody tr:hover td { .card.scan-table-card table.print-module-table.scan-table tbody tr:hover td {
background-color: var(--app-hover-bg, #3a4451) !important; background-color: #f8f9fa !important;
cursor: pointer !important; cursor: pointer !important;
} }
.card.scan-table-card table.print-module-table.scan-table tbody td {
background-color: var(--app-card-bg, #2a3441) !important;
color: var(--app-text-color, #ffffff) !important;
border: 1px solid var(--app-border-color, #495057) !important;
padding: 0.25rem 0.4rem !important;
}
.card.scan-table-card table.print-module-table.scan-table tbody tr.selected td { .card.scan-table-card table.print-module-table.scan-table tbody tr.selected td {
background-color: #007bff !important; background-color: #007bff !important;
color: white !important; color: white !important;
@@ -149,13 +140,13 @@
</div> </div>
</div> </div>
<!-- Barcode Frame - positioned 10px below rectangle, centered, constrained to label width --> <!-- Barcode Frame - positioned 10px below rectangle, centered, 90% of label width -->
<div id="barcode-frame" style="position: absolute; top: 395px; left: 50%; transform: translateX(-50%); width: 220px; max-width: 220px; height: 50px; background: white; display: flex; flex-direction: column; align-items: center; justify-content: center; overflow: hidden;"> <div id="barcode-frame" style="position: absolute; top: 395px; left: 50%; transform: translateX(-50%); width: 90%; max-width: 270px; height: 50px; background: white; display: flex; flex-direction: column; align-items: center; justify-content: center;">
<!-- Code 128 Barcode representation --> <!-- Code 128 Barcode representation -->
<svg id="barcode-display" style="width: 100%; height: 40px; max-width: 220px;"></svg> <svg id="barcode-display" style="width: 100%; height: 40px;"></svg>
<!-- Barcode text below the bars (hidden in preview) --> <!-- Barcode text below the bars -->
<div id="barcode-text" style="font-size: 8px; font-family: 'Courier New', monospace; margin-top: 2px; text-align: center; font-weight: bold; display: none;"> <div id="barcode-text" style="font-size: 8px; font-family: 'Courier New', monospace; margin-top: 2px; text-align: center; font-weight: bold;">
<!-- Barcode text will be populated here --> <!-- Barcode text will be populated here -->
</div> </div>
</div> </div>
@@ -165,8 +156,8 @@
<!-- Vertical Code 128 Barcode representation --> <!-- Vertical Code 128 Barcode representation -->
<svg id="vertical-barcode-display" style="width: 100%; height: 35px;"></svg> <svg id="vertical-barcode-display" style="width: 100%; height: 35px;"></svg>
<!-- Vertical barcode text (hidden in preview) --> <!-- Vertical barcode text -->
<div id="vertical-barcode-text" style="position: absolute; bottom: -15px; font-size: 7px; font-family: 'Courier New', monospace; text-align: center; font-weight: bold; width: 100%; display: none;"> <div id="vertical-barcode-text" style="position: absolute; bottom: -15px; font-size: 7px; font-family: 'Courier New', monospace; text-align: center; font-weight: bold; width: 100%;">
<!-- Vertical barcode text will be populated here --> <!-- Vertical barcode text will be populated here -->
</div> </div>
</div> </div>
@@ -484,12 +475,10 @@ function updateLabelPreview(order) {
JsBarcode("#barcode-display", horizontalBarcodeData, { JsBarcode("#barcode-display", horizontalBarcodeData, {
format: "CODE128", format: "CODE128",
width: 1.2, width: 2,
height: 40, height: 40,
displayValue: false, displayValue: false,
margin: 0, margin: 2
fontSize: 0,
textMargin: 0
}); });
console.log('✅ Horizontal barcode generated successfully'); console.log('✅ Horizontal barcode generated successfully');
} catch (e) { } catch (e) {

View File

@@ -22,74 +22,6 @@ document.addEventListener('DOMContentLoaded', function() {
const defectCodeInput = document.getElementById('defect_code'); const defectCodeInput = document.getElementById('defect_code');
const form = document.querySelector('.form-centered'); const form = document.querySelector('.form-centered');
// Load saved operator codes from localStorage (only Quality Operator Code)
function loadSavedCodes() {
const savedOperatorCode = localStorage.getItem('scan_operator_code');
if (savedOperatorCode) {
operatorCodeInput.value = savedOperatorCode;
}
}
// Save operator codes to localStorage (only Quality Operator Code)
function saveCodes() {
if (operatorCodeInput.value.startsWith('OP')) {
localStorage.setItem('scan_operator_code', operatorCodeInput.value);
}
}
// Clear saved codes from localStorage (only Quality Operator Code)
function clearSavedCodes() {
localStorage.removeItem('scan_operator_code');
operatorCodeInput.value = '';
showSuccessMessage('Quality Operator code cleared!');
operatorCodeInput.focus();
}
// Show success message
function showSuccessMessage(message) {
const successDiv = document.createElement('div');
successDiv.className = 'success-message';
successDiv.textContent = message;
successDiv.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
background: #4CAF50;
color: white;
padding: 15px 20px;
border-radius: 4px;
z-index: 1000;
font-weight: bold;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
`;
document.body.appendChild(successDiv);
setTimeout(() => {
if (document.body.contains(successDiv)) {
document.body.removeChild(successDiv);
}
}, 3000);
}
// Load saved codes on page load
loadSavedCodes();
// Focus on the first empty field
setTimeout(() => {
if (!operatorCodeInput.value) {
operatorCodeInput.focus();
} else if (!cpCodeInput.value) {
cpCodeInput.focus();
} else if (!oc1CodeInput.value) {
oc1CodeInput.focus();
} else if (!oc2CodeInput.value) {
oc2CodeInput.focus();
} else {
defectCodeInput.focus();
}
}, 100);
// Create error message element for operator code // Create error message element for operator code
const operatorErrorMessage = document.createElement('div'); const operatorErrorMessage = document.createElement('div');
operatorErrorMessage.className = 'error-message'; operatorErrorMessage.className = 'error-message';
@@ -401,21 +333,8 @@ document.addEventListener('DOMContentLoaded', function() {
const seconds = String(now.getSeconds()).padStart(2, '0'); const seconds = String(now.getSeconds()).padStart(2, '0');
timeInput.value = `${hours}:${minutes}:${seconds}`; timeInput.value = `${hours}:${minutes}:${seconds}`;
// Save operator codes before submitting
saveCodes();
// Submit the form // Submit the form
form.submit(); form.submit();
// Clear CP, OC1, OC2, and defect code fields after successful submission
setTimeout(() => {
cpCodeInput.value = '';
oc1CodeInput.value = '';
oc2CodeInput.value = '';
defectCodeInput.value = '';
showSuccessMessage('Scan submitted successfully!');
cpCodeInput.focus();
}, 100);
} }
}); });
@@ -506,7 +425,6 @@ document.addEventListener('DOMContentLoaded', function() {
<input type="text" id="time" name="time" value="{{ now().strftime('%H:%M:%S') }}" readonly> <input type="text" id="time" name="time" value="{{ now().strftime('%H:%M:%S') }}" readonly>
<button type="submit" class="btn">Submit</button> <button type="submit" class="btn">Submit</button>
<button type="button" class="btn btn-secondary" onclick="clearSavedCodes()" style="margin-top: 10px; background-color: #6c757d;">Clear Quality Operator</button>
</form> </form>
</div> </div>

View File

@@ -5,7 +5,7 @@
{% block content %} {% block content %}
<div class="card-container"> <div class="card-container">
<div class="card"> <div class="card">
<h3>Manage Users (Legacy)</h3> <h3>Manage Users</h3>
<ul class="user-list"> <ul class="user-list">
{% for user in users %} {% for user in users %}
<li data-user-id="{{ user.id }}" data-username="{{ user.username }}" data-email="{{ user.email if user.email else '' }}" data-role="{{ user.role }}"> <li data-user-id="{{ user.id }}" data-username="{{ user.username }}" data-email="{{ user.email if user.email else '' }}" data-role="{{ user.role }}">
@@ -37,18 +37,9 @@
</div> </div>
<div class="card" style="margin-top: 32px;"> <div class="card" style="margin-top: 32px;">
<h3>🎯 User & Permissions Management</h3> <h3>Role & Permissions Management</h3>
<p><strong>Simplified 4-Tier System:</strong> Superadmin → Admin → Manager → Worker</p> <p>Configure granular permissions for each role in the system with expandable sections and detailed access control.</p>
<p>Streamlined interface with module-based permissions (Quality, Warehouse, Labels)</p> <a href="{{ url_for('main.role_permissions') }}" class="btn">Manage Role Permissions</a>
<div style="margin-top: 15px;">
<a href="{{ url_for('main.user_management_simple') }}" class="btn" style="background-color: #2196f3; color: white; margin-right: 10px;">
🎯 Manage Users (Simplified)
</a>
</div>
<small style="display: block; margin-top: 10px; color: #666;">
Recommended: Use the simplified user management for easier administration
</small>
</div> </div>
</div> </div>

View File

@@ -82,35 +82,15 @@ table.view-orders-table.scan-table tbody tr:hover td {
<h3>Upload Order Data for Labels</h3> <h3>Upload Order Data for Labels</h3>
{% endif %} {% endif %}
<form method="POST" enctype="multipart/form-data" class="form-centered" id="csv-upload-form"> <form method="POST" enctype="multipart/form-data" class="form-centered" id="csv-upload-form">
{% if show_preview %} <label for="csv_file">Choose CSV file:</label>
<!-- Show preview controls --> {% if leftover_description %}
<input type="hidden" name="action" value="save"> <button type="submit" class="btn btn-danger" name="clear_table" value="1">Clear Table</button>
<label style="font-weight: bold;">Preview of: {{ filename }}</label><br> {% elif not orders %}
<p style="color: #666; font-size: 14px; margin: 10px 0;"> <input type="file" name="csv_file" accept=".csv" required><br>
Showing first 10 rows. Review the data below and click "Save to Database" to confirm. <button type="submit" class="btn">Upload & Review</button>
</p>
<button type="submit" class="btn" style="background-color: #28a745;">Save to Database</button>
<a href="{{ url_for('main.upload_data') }}" class="btn" style="background-color: #6c757d; margin-left: 10px;">Cancel</a>
{% else %} {% else %}
<!-- Show file upload --> <label style="font-weight: bold;">Selected file: {{ session['csv_filename'] if session['csv_filename'] else 'Unknown' }}</label><br>
<input type="hidden" name="action" value="preview"> <button type="button" class="btn" onclick="showPopupAndSubmit()">Upload to Database</button>
<label for="file">Choose CSV file:</label>
<input type="file" name="file" accept=".csv" required><br>
<button type="submit" class="btn">Upload & Preview</button>
<!-- CSV Format Information -->
<div style="margin-top: 20px; padding: 15px; background-color: var(--app-card-bg, #2a3441); border-radius: 5px; border-left: 4px solid var(--app-accent-color, #007bff); color: var(--app-text-color, #ffffff);">
<h5 style="margin-top: 0; color: var(--app-accent-color, #007bff);">Expected CSV Format</h5>
<p style="margin-bottom: 10px; color: var(--app-text-color, #ffffff);">Your CSV file should contain columns such as:</p>
<ul style="margin-bottom: 10px; color: var(--app-text-color, #ffffff);">
<li><strong>order_number</strong> - The order/production number</li>
<li><strong>quantity</strong> - Number of items</li>
<li><strong>warehouse_location</strong> - Storage location</li>
</ul>
<p style="color: var(--app-secondary-text, #b8c5d1); font-size: 14px; margin-bottom: 0;">
Column names are case-insensitive and can have variations like "Order Number", "Quantity", "Location", etc.
</p>
</div>
{% endif %} {% endif %}
</form> </form>
@@ -144,45 +124,108 @@ table.view-orders-table.scan-table tbody tr:hover td {
</div> </div>
<!-- Preview Table Card (expandable height, scrollable) --> <!-- Preview Table Card (expandable height, scrollable) -->
<div class="card scan-table-card" style="margin-bottom: 24px; max-height: 480px; overflow-y: auto;"> <div class="card scan-table-card{% if leftover_description %} leftover-table-card{% endif %}" style="margin-bottom: 24px; max-height: 480px; overflow-y: auto;">
{% if show_preview %} {% if leftover_description %}
<h3>CSV Data Preview - {{ filename }}</h3> <h3>Left over orders</h3>
<table class="scan-table">
<thead>
<tr>
{% for header in headers %}
<th>{{ header }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% if preview_data %}
{% for row in preview_data %}
<tr>
{% for header in headers %}
<td>{{ row.get(header, '') }}</td>
{% endfor %}
</tr>
{% endfor %}
{% else %}
<tr><td colspan="{{ headers|length }}" style="text-align:center;">No data to preview</td></tr>
{% endif %}
</tbody>
</table>
{% else %} {% else %}
<h3>CSV Data Preview</h3> <h3>Preview Table</h3>
<table class="scan-table">
<thead>
<tr>
<th>Upload a CSV file to see preview</th>
</tr>
</thead>
<tbody>
<tr><td style="text-align:center; padding: 40px;">No CSV file uploaded yet. Use the form above to upload and preview your data.</td></tr>
</tbody>
</table>
{% endif %} {% endif %}
<table class="scan-table view-orders-table{% if leftover_description %} leftover-table{% endif %}">
<thead>
<tr>
<th>ID</th>
<th>Comanda<br>Productie</th>
<th>Cod<br>Articol</th>
<th>Descr. Com.<br>Prod</th>
<th>Cantitate</th>
<th>Data<br>Livrare</th>
<th>Dimensiune</th>
<th>Com.Achiz.<br>Client</th>
<th>Nr.<br>Linie</th>
<th>Customer<br>Name</th>
<th>Customer<br>Art. Nr.</th>
<th>Open<br>Order</th>
<th>Line</th>
<th>Printed</th>
<th>Created</th>
</tr>
</thead>
<tbody>
{% if orders %}
{% for order in orders %}
{% if order and (order.get('comanda_productie', '') or order.get('descr_com_prod', '')) %}
<tr>
<td>{{ order.get('id', '') }}</td>
<td><strong>{{ order.get('comanda_productie', '') }}</strong></td>
<td>{{ order.get('cod_articol', '-') }}</td>
<td>{{ order.get('descr_com_prod', '') }}</td>
<td style="text-align: right; font-weight: 600;">{{ order.get('cantitate', '') }}</td>
<td style="text-align: center;">{{ order.get('data_livrare', '') }}</td>
<td style="text-align: center;">{{ order.get('dimensiune', '-') }}</td>
<td>{{ order.get('com_achiz_client', '-') }}</td>
<td style="text-align: right;">{{ order.get('nr_linie_com_client', '-') }}</td>
<td>{{ order.get('customer_name', '-') }}</td>
<td>{{ order.get('customer_article_number', '-') }}</td>
<td>{{ order.get('open_for_order', '-') }}</td>
<td style="text-align: right;">{{ order.get('line_number', '-') }}</td>
<td style="text-align: center;">
{% if order.get('printed_labels', 0) == 1 %}
<span style="color: #28a745; font-weight: bold;">✓ Yes</span>
{% else %}
<span style="color: #dc3545;">✗ No</span>
{% endif %}
</td>
<td style="font-size: 11px; color: #6c757d;">{{ order.get('created_at', '-') }}</td>
</tr>
{% if order.error_message %}
<tr>
<td colspan="15" style="color: #dc3545; font-size: 12px; background: #fff3f3;">
<strong>Error:</strong> {{ order.error_message }}
</td>
</tr>
{% endif %}
{% endif %}
{% endfor %}
{% else %}
<tr><td colspan="15" style="text-align:center;">No CSV file uploaded yet.</td></tr>
{% endif %}
</tbody>
</table>
</div> </div>
{% if validation_errors or validation_warnings %}
{% if not leftover_description %}
<div class="card" style="margin-bottom: 24px;">
<h4>Validation Results</h4>
{% if validation_errors %}
<div style="color: #dc3545; margin-bottom: 16px;">
<strong>Errors found:</strong>
<ul>
{% for error in validation_errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
{% if validation_warnings %}
<div style="color: #ffc107;">
<strong>Warnings:</strong>
<ul>
{% for warning in validation_warnings %}
<li>{{ warning }}</li>
{% endfor %}
</ul>
</div>
{% endif %}
</div>
{% if report %}
<div class="card" style="margin-bottom: 24px;">
<h4>Import Report</h4>
<p>{{ report }}</p>
</div>
{% endif %}
{% endif %}
{% endif %}
</div> </div>
<style> <style>

View File

@@ -1,890 +0,0 @@
{% extends "base.html" %}
{% block title %}User Management - Simplified{% endblock %}
{% block extra_css %}
<style>
/* Theme-aware card styles */
.user-management-page .card {
text-align: left !important;
flex: none !important;
max-width: 100% !important;
padding: 0 !important;
border-radius: 5px !important;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1) !important;
}
/* Light mode card styles */
body.light-mode .user-management-page .card {
background: #fff !important;
color: #000 !important;
border: 1px solid #ddd !important;
}
body.light-mode .user-management-page .card-body {
background: #fff !important;
color: #000 !important;
padding: 1.25rem !important;
}
body.light-mode .user-management-page .card-header {
background-color: #f8f9fa !important;
color: #333 !important;
border-bottom: 1px solid #ddd !important;
padding: 0.75rem 1.25rem !important;
border-top-left-radius: 4px !important;
border-top-right-radius: 4px !important;
}
/* Dark mode card styles */
body.dark-mode .user-management-page .card {
background: #1e1e1e !important;
color: #fff !important;
border: 1px solid #444 !important;
}
body.dark-mode .user-management-page .card-body {
background: #1e1e1e !important;
color: #fff !important;
padding: 1.25rem !important;
}
body.dark-mode .user-management-page .card-header {
background-color: #2a2a2a !important;
color: #ccc !important;
border-bottom: 1px solid #444 !important;
padding: 0.75rem 1.25rem !important;
border-top-left-radius: 4px !important;
border-top-right-radius: 4px !important;
}
/* Theme-aware header text */
.user-management-page .card-header h5 {
margin: 0 !important;
font-weight: 600 !important;
}
body.light-mode .user-management-page .card-header h5 {
color: #333 !important;
}
body.dark-mode .user-management-page .card-header h5 {
color: #ccc !important;
}
/* Theme-aware role badges */
.user-role-badge {
padding: 4px 8px;
border-radius: 4px;
font-size: 0.8em;
font-weight: bold;
}
.role-superadmin { background-color: #dc3545; color: white; }
.role-admin { background-color: #fd7e14; color: white; }
.role-manager { background-color: #007bff; color: white; }
.role-worker { background-color: #28a745; color: white; }
/* Dark mode badge adjustments */
body.dark-mode .role-manager { background-color: #0056b3; }
body.dark-mode .role-worker { background-color: #1e7e34; }
/* Theme-aware module badges */
.module-badges .badge {
margin-right: 5px;
margin-bottom: 3px;
}
body.light-mode .module-badges .badge {
background-color: #007bff;
color: white;
}
body.dark-mode .module-badges .badge {
background-color: #0056b3;
color: white;
}
.form-group {
margin-bottom: 15px;
}
.module-checkboxes {
display: flex;
flex-wrap: wrap;
gap: 15px;
margin-top: 5px;
}
.module-checkbox {
display: flex;
align-items: center;
gap: 5px;
}
/* Theme-aware info panels */
.access-level-info {
border-left: 4px solid #007bff;
padding: 10px;
margin-top: 10px;
font-size: 0.9em;
}
body.light-mode .access-level-info {
background-color: #f8f9fa;
color: #333;
}
body.dark-mode .access-level-info {
background-color: #2a2a2a;
color: #ccc;
border-left-color: #0056b3;
}
.table-warning {
background-color: #fff3cd !important;
}
body.dark-mode .table-warning {
background-color: #664d03 !important;
color: #fff !important;
}
/* Colored card headers - respect theme but use accent colors */
.card-header.bg-primary {
background-color: #007bff !important;
color: white !important;
border-color: #007bff !important;
}
.card-header.bg-primary h5 {
color: white !important;
margin: 0 !important;
}
.card-header.bg-success {
background-color: #28a745 !important;
color: white !important;
border-color: #28a745 !important;
}
.card-header.bg-success h5 {
color: white !important;
margin: 0 !important;
}
/* Dark mode adjustments for colored headers */
body.dark-mode .card-header.bg-primary {
background-color: #0056b3 !important;
border-color: #0056b3 !important;
}
body.dark-mode .card-header.bg-success {
background-color: #1e7e34 !important;
border-color: #1e7e34 !important;
}
.btn-block {
width: 100%;
}
#quickModuleEdit {
border: 2px solid #198754;
border-radius: 8px;
padding: 15px;
background-color: #f8fff8;
}
/* Theme-aware table styling */
.thead-dark {
background-color: #343a40 !important;
color: white !important;
}
body.dark-mode .thead-dark {
background-color: #1a1a1a !important;
}
.user-management-page .table {
background-color: transparent !important;
}
body.light-mode .user-management-page .table {
color: #000 !important;
}
body.dark-mode .user-management-page .table {
color: #fff !important;
}
.user-management-page .table th,
.user-management-page .table td {
border-top: 1px solid #dee2e6 !important;
padding: 0.75rem !important;
vertical-align: top !important;
}
body.dark-mode .user-management-page .table th,
body.dark-mode .user-management-page .table td {
border-top: 1px solid #444 !important;
}
.user-management-page .table-striped tbody tr:nth-of-type(odd) {
background-color: rgba(0, 0, 0, 0.05) !important;
}
body.dark-mode .user-management-page .table-striped tbody tr:nth-of-type(odd) {
background-color: rgba(255, 255, 255, 0.05) !important;
}
.user-management-page .table-hover tbody tr:hover {
background-color: rgba(0, 0, 0, 0.075) !important;
cursor: pointer;
}
body.dark-mode .user-management-page .table-hover tbody tr:hover {
background-color: rgba(255, 255, 255, 0.075) !important;
}
/* Theme-aware form elements */
.user-management-page .form-control {
border: 1px solid #ced4da !important;
}
body.light-mode .user-management-page .form-control {
background-color: #fff !important;
color: #000 !important;
border-color: #ced4da !important;
}
body.dark-mode .user-management-page .form-control {
background-color: #2a2a2a !important;
color: #fff !important;
border-color: #444 !important;
}
.user-management-page .form-control:focus {
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25) !important;
}
body.light-mode .user-management-page .form-control:focus {
border-color: #80bdff !important;
}
body.dark-mode .user-management-page .form-control:focus {
border-color: #0056b3 !important;
}
.user-management-page label {
font-weight: 500 !important;
margin-bottom: 0.5rem !important;
}
body.light-mode .user-management-page label {
color: #333 !important;
}
body.dark-mode .user-management-page label {
color: #ccc !important;
}
/* Theme-aware buttons */
.user-management-page .btn-primary {
background-color: #007bff !important;
border-color: #007bff !important;
}
.user-management-page .btn-primary:hover {
background-color: #0056b3 !important;
border-color: #0056b3 !important;
}
body.dark-mode .user-management-page .btn-primary {
background-color: #0056b3 !important;
border-color: #0056b3 !important;
}
body.dark-mode .user-management-page .btn-primary:hover {
background-color: #004085 !important;
border-color: #004085 !important;
}
/* Additional theme-aware elements */
#quickModuleEdit {
border-radius: 8px;
padding: 15px;
border: 2px solid #28a745;
}
body.light-mode #quickModuleEdit {
background-color: #f8fff8;
color: #333;
}
body.dark-mode #quickModuleEdit {
background-color: #1a2f1a;
color: #ccc;
border-color: #1e7e34;
}
/* Theme-aware text colors */
body.light-mode .user-management-page h2,
body.light-mode .user-management-page p {
color: #000 !important;
}
body.dark-mode .user-management-page h2,
body.dark-mode .user-management-page p {
color: #fff !important;
}
body.light-mode .user-management-page .text-muted {
color: #6c757d !important;
}
body.dark-mode .user-management-page .text-muted {
color: #adb5bd !important;
}
/* Theme-aware select dropdown */
body.light-mode .user-management-page select.form-control {
background-color: #fff !important;
color: #000 !important;
}
body.dark-mode .user-management-page select.form-control {
background-color: #2a2a2a !important;
color: #fff !important;
}
</style>
{% endblock %}
{% block content %}
<div class="container mt-4 user-management-page">
<h2>Simplified User Management</h2>
<p class="text-muted">Manage users with the new 4-tier permission system: Superadmin → Admin → Manager → Worker</p>
<div class="row">
<!-- Create User Card -->
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header bg-primary text-white">
<h5 class="mb-0">👤 Create New User</h5>
</div>
<div class="card-body">
<form id="addUserForm" method="POST" action="/create_user_simple">
<div class="form-group">
<label for="username">Username:</label>
<input type="text" id="username" name="username" class="form-control" required>
</div>
<div class="form-group">
<label for="password">Password:</label>
<input type="password" id="password" name="password" class="form-control" required>
</div>
<div class="form-group">
<label for="role">Role:</label>
<select id="role" name="role" class="form-control" required onchange="updateModuleSelection()">
<option value="">Select a role...</option>
<option value="superadmin">Superadmin - Full system access</option>
<option value="admin">Admin - Full app access (except role permissions)</option>
<option value="manager">Manager - Module-based access (can have multiple modules)</option>
<option value="worker">Worker - Limited module access (can have multiple modules)</option>
</select>
</div>
<div class="form-group" id="moduleSelection" style="display: none;">
<label>Modules:</label>
<div class="module-checkboxes">
<div class="module-checkbox">
<input type="checkbox" id="module_quality" name="modules" value="quality">
<label for="module_quality">Quality Control</label>
</div>
<div class="module-checkbox">
<input type="checkbox" id="module_warehouse" name="modules" value="warehouse">
<label for="module_warehouse">Warehouse Management</label>
</div>
<div class="module-checkbox">
<input type="checkbox" id="module_labels" name="modules" value="labels">
<label for="module_labels">Label Management</label>
</div>
</div>
<div id="accessLevelInfo" class="access-level-info" style="display: none;"></div>
</div>
<button type="submit" class="btn btn-primary btn-block">Create User</button>
</form>
</div>
</div>
</div>
<!-- Edit User Rights Card -->
<div class="col-md-6">
<div class="card mb-4">
<div class="card-header bg-success text-white">
<h5 class="mb-0">⚙️ Edit User Rights</h5>
</div>
<div class="card-body">
<div id="userRightsPanel">
<div class="text-center text-muted py-4">
<i class="fas fa-user-edit fa-3x mb-3"></i>
<p>Select a user from the table below to edit their rights and module access.</p>
</div>
<!-- Quick Module Management for Selected User -->
<div id="quickModuleEdit" style="display: none;">
<h6>Quick Module Management</h6>
<div class="mb-3">
<strong>User:</strong> <span id="selectedUserName"></span>
<br><small class="text-muted">Role: <span id="selectedUserRole"></span></small>
</div>
<div class="form-group">
<label>Current Modules:</label>
<div id="currentModules" class="module-checkboxes">
<div class="module-checkbox">
<input type="checkbox" id="quick_module_quality" name="quick_modules" value="quality">
<label for="quick_module_quality">Quality Control</label>
</div>
<div class="module-checkbox">
<input type="checkbox" id="quick_module_warehouse" name="quick_modules" value="warehouse">
<label for="quick_module_warehouse">Warehouse Management</label>
</div>
<div class="module-checkbox">
<input type="checkbox" id="quick_module_labels" name="quick_modules" value="labels">
<label for="quick_module_labels">Label Management</label>
</div>
</div>
</div>
<div class="btn-group btn-group-sm w-100">
<button type="button" class="btn btn-success" onclick="updateUserModules()">
💾 Save Module Changes
</button>
<button type="button" class="btn btn-info" onclick="showFullEditModal()">
✏️ Full Edit
</button>
<button type="button" class="btn btn-danger" onclick="deleteSelectedUser()">
🗑️ Delete User
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Current Users Table -->
<div class="card">
<div class="card-header">
<h5>👥 Current Users</h5>
</div>
<div class="card-body">
{% if users %}
<div class="table-responsive">
<table class="table table-striped table-hover">
<thead class="thead-dark">
<tr>
<th>Username</th>
<th>Role</th>
<th>Modules</th>
<th>Access Level</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr class="user-row" data-user-id="{{ user.id }}" data-username="{{ user.username }}" data-role="{{ user.role }}" data-modules="{{ (user.get_modules() or []) | tojson }}">
<td>
<strong>{{ user.username }}</strong>
</td>
<td>
<span class="user-role-badge role-{{ user.role }}">
{{ user.role.title() }}
</span>
</td>
<td>
<div class="module-badges">
{% if user.role in ['superadmin', 'admin'] %}
<span class="badge bg-secondary">All Modules</span>
{% elif user.modules %}
{% for module in user.get_modules() %}
<span class="badge bg-info">{{ module.title() }}</span>
{% endfor %}
{% else %}
<span class="text-muted">No modules assigned</span>
{% endif %}
</div>
</td>
<td>
<small class="text-muted">
{% if user.role == 'superadmin' %}
Full system access
{% elif user.role == 'admin' %}
Full app access
{% elif user.role == 'manager' %}
Full module access + reports
{% elif user.role == 'worker' %}
Basic operations only (no reports) - Can have multiple modules
{% endif %}
</small>
</td>
<td>
<div class="btn-group btn-group-sm">
<button class="btn btn-outline-primary select-user-btn"
data-user-id="{{ user.id }}"
data-username="{{ user.username }}"
data-role="{{ user.role }}"
data-modules="{{ (user.get_modules() or []) | tojson }}"
title="Select for quick edit">
📝 Select
</button>
<button class="btn btn-outline-info edit-user-btn"
data-user-id="{{ user.id }}"
data-username="{{ user.username }}"
data-role="{{ user.role }}"
data-modules="{{ (user.get_modules() or []) | tojson }}"
title="Full edit">
⚙️ Edit
</button>
{% if user.username != session.get('user') %}
<button class="btn btn-outline-danger delete-user-btn"
data-user-id="{{ user.id }}"
data-username="{{ user.username }}"
title="Delete user">
🗑️
</button>
{% endif %}
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<div class="text-center text-muted py-4">
<i class="fas fa-users fa-3x mb-3"></i>
<p>No users found. Create your first user above!</p>
</div>
{% endif %}
</div>
</div>
</div>
<!-- Edit User Modal -->
<div class="modal fade" id="editUserModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Edit User</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form id="editUserForm" method="POST" action="/edit_user_simple">
<div class="modal-body">
<input type="hidden" id="edit_user_id" name="user_id">
<div class="form-group">
<label for="edit_username">Username:</label>
<input type="text" id="edit_username" name="username" class="form-control" required>
</div>
<div class="form-group">
<label for="edit_password">Password (leave blank to keep current):</label>
<input type="password" id="edit_password" name="password" class="form-control">
</div>
<div class="form-group">
<label for="edit_role">Role:</label>
<select id="edit_role" name="role" class="form-control" required onchange="updateEditModuleSelection()">
<option value="">Select a role...</option>
<option value="superadmin">Superadmin - Full system access</option>
<option value="admin">Admin - Full app access (except role permissions)</option>
<option value="manager">Manager - Module-based access (can have multiple modules)</option>
<option value="worker">Worker - Limited module access (can have multiple modules)</option>
</select>
</div>
<div class="form-group" id="editModuleSelection" style="display: none;">
<label>Modules:</label>
<div class="module-checkboxes">
<div class="module-checkbox">
<input type="checkbox" id="edit_module_quality" name="modules" value="quality">
<label for="edit_module_quality">Quality Control</label>
</div>
<div class="module-checkbox">
<input type="checkbox" id="edit_module_warehouse" name="modules" value="warehouse">
<label for="edit_module_warehouse">Warehouse Management</label>
</div>
<div class="module-checkbox">
<input type="checkbox" id="edit_module_labels" name="modules" value="labels">
<label for="edit_module_labels">Label Management</label>
</div>
</div>
<div id="editAccessLevelInfo" class="access-level-info" style="display: none;"></div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="submit" class="btn btn-primary">Save Changes</button>
</div>
</form>
</div>
</div>
</div>
<script>
function updateModuleSelection() {
const role = document.getElementById('role').value;
const moduleSelection = document.getElementById('moduleSelection');
const accessLevelInfo = document.getElementById('accessLevelInfo');
const checkboxes = document.querySelectorAll('#moduleSelection input[type="checkbox"]');
if (role === 'superadmin' || role === 'admin') {
moduleSelection.style.display = 'none';
checkboxes.forEach(cb => cb.checked = false);
accessLevelInfo.style.display = 'none';
} else if (role === 'manager' || role === 'worker') {
moduleSelection.style.display = 'block';
checkboxes.forEach(cb => cb.checked = false);
if (role === 'manager') {
accessLevelInfo.style.display = 'block';
accessLevelInfo.innerHTML = '<strong>Manager Access:</strong> Can have multiple modules. Full access to assigned modules including reports and management functions.';
} else if (role === 'worker') {
accessLevelInfo.style.display = 'block';
accessLevelInfo.innerHTML = '<strong>Worker Access:</strong> Can have multiple modules. Limited to basic operations only (no reports or management functions). <br><small>Examples: Quality worker can scan, Warehouse worker can move orders, etc.</small>';
}
} else {
moduleSelection.style.display = 'none';
accessLevelInfo.style.display = 'none';
}
}
function updateEditModuleSelection() {
const role = document.getElementById('edit_role').value;
const moduleSelection = document.getElementById('editModuleSelection');
const accessLevelInfo = document.getElementById('editAccessLevelInfo');
const checkboxes = document.querySelectorAll('#editModuleSelection input[type="checkbox"]');
if (role === 'superadmin' || role === 'admin') {
moduleSelection.style.display = 'none';
checkboxes.forEach(cb => cb.checked = false);
accessLevelInfo.style.display = 'none';
} else if (role === 'manager' || role === 'worker') {
moduleSelection.style.display = 'block';
if (role === 'manager') {
accessLevelInfo.style.display = 'block';
accessLevelInfo.innerHTML = '<strong>Manager Access:</strong> Can have multiple modules. Full access to assigned modules including reports and management functions.';
} else if (role === 'worker') {
accessLevelInfo.style.display = 'block';
accessLevelInfo.innerHTML = '<strong>Worker Access:</strong> Can have multiple modules. Limited to basic operations only (no reports or management functions). <br><small>Examples: Quality worker can scan, Warehouse worker can move orders, etc.</small>';
}
} else {
moduleSelection.style.display = 'none';
accessLevelInfo.style.display = 'none';
}
}
// Global variable to store selected user
let selectedUser = null;
function selectUserForQuickEdit(userId, username, role, modules) {
selectedUser = {id: userId, username: username, role: role, modules: modules};
// Update the quick edit panel
document.getElementById('selectedUserName').textContent = username;
document.getElementById('selectedUserRole').textContent = role;
// Clear all module checkboxes first
const checkboxes = document.querySelectorAll('#currentModules input[type="checkbox"]');
checkboxes.forEach(cb => cb.checked = false);
// Check the appropriate modules
if (modules && Array.isArray(modules)) {
modules.forEach(module => {
const checkbox = document.getElementById('quick_module_' + module);
if (checkbox) checkbox.checked = true;
});
}
// Show the quick edit panel
document.getElementById('quickModuleEdit').style.display = 'block';
document.querySelector('#userRightsPanel .text-center').style.display = 'none';
// Highlight selected row
document.querySelectorAll('.user-row').forEach(row => {
row.classList.remove('table-warning');
});
document.querySelector(`[data-user-id="${userId}"]`).classList.add('table-warning');
}
function updateUserModules() {
if (!selectedUser) {
alert('No user selected');
return;
}
// Get selected modules
const checkboxes = document.querySelectorAll('#currentModules input[type="checkbox"]:checked');
const modules = Array.from(checkboxes).map(cb => cb.value);
// Validate modules for the role
if ((selectedUser.role === 'manager' || selectedUser.role === 'worker') && modules.length === 0) {
alert(`${selectedUser.role}s must have at least one module assigned.`);
return;
}
// Create and submit form
const form = document.createElement('form');
form.method = 'POST';
form.action = '/quick_update_modules';
// Add user ID
const userIdInput = document.createElement('input');
userIdInput.type = 'hidden';
userIdInput.name = 'user_id';
userIdInput.value = selectedUser.id;
form.appendChild(userIdInput);
// Add modules
modules.forEach(module => {
const input = document.createElement('input');
input.type = 'hidden';
input.name = 'modules';
input.value = module;
form.appendChild(input);
});
document.body.appendChild(form);
form.submit();
}
function showFullEditModal() {
if (!selectedUser) {
alert('No user selected');
return;
}
editUser(selectedUser.id, selectedUser.username, selectedUser.role, selectedUser.modules);
}
function deleteSelectedUser() {
if (!selectedUser) {
alert('No user selected');
return;
}
deleteUser(selectedUser.id, selectedUser.username);
}
function editUser(userId, username, role, modules) {
document.getElementById('edit_user_id').value = userId;
document.getElementById('edit_username').value = username;
document.getElementById('edit_role').value = role;
document.getElementById('edit_password').value = '';
// Clear all module checkboxes first
const checkboxes = document.querySelectorAll('#editModuleSelection input[type="checkbox"]');
checkboxes.forEach(cb => cb.checked = false);
// Check the appropriate modules
if (modules && Array.isArray(modules)) {
modules.forEach(module => {
const checkbox = document.getElementById('edit_module_' + module);
if (checkbox) checkbox.checked = true;
});
}
updateEditModuleSelection();
const modal = new bootstrap.Modal(document.getElementById('editUserModal'));
modal.show();
}
function deleteUser(userId, username) {
if (confirm(`Are you sure you want to delete user "${username}"?`)) {
const form = document.createElement('form');
form.method = 'POST';
form.action = '/delete_user_simple';
const userIdInput = document.createElement('input');
userIdInput.type = 'hidden';
userIdInput.name = 'user_id';
userIdInput.value = userId;
form.appendChild(userIdInput);
document.body.appendChild(form);
form.submit();
}
}
function deleteUser(userId, username) {
if (confirm(`Are you sure you want to delete user "${username}"?`)) {
const form = document.createElement('form');
form.method = 'POST';
form.action = '/delete_user_simple';
const userIdInput = document.createElement('input');
userIdInput.type = 'hidden';
userIdInput.name = 'user_id';
userIdInput.value = userId;
form.appendChild(userIdInput);
document.body.appendChild(form);
form.submit();
}
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', function() {
updateModuleSelection();
// Add event listeners for select user buttons
document.querySelectorAll('.select-user-btn').forEach(button => {
button.addEventListener('click', function() {
const userId = this.dataset.userId;
const username = this.dataset.username;
const role = this.dataset.role;
let modules = [];
try {
modules = JSON.parse(this.dataset.modules) || [];
} catch (e) {
console.log('Error parsing modules for user:', username, e);
modules = [];
}
selectUserForQuickEdit(userId, username, role, modules);
});
});
// Add event listeners for edit buttons
document.querySelectorAll('.edit-user-btn').forEach(button => {
button.addEventListener('click', function() {
const userId = this.dataset.userId;
const username = this.dataset.username;
const role = this.dataset.role;
let modules = [];
try {
modules = JSON.parse(this.dataset.modules) || [];
} catch (e) {
console.log('Error parsing modules for user:', username, e);
modules = [];
}
editUser(userId, username, role, modules);
});
});
// Add event listeners for delete buttons
document.querySelectorAll('.delete-user-btn').forEach(button => {
button.addEventListener('click', function() {
const userId = this.dataset.userId;
const username = this.dataset.username;
deleteUser(userId, username);
});
});
});
</script>
{% endblock %}

View File

@@ -25,14 +25,13 @@ table.view-orders-table.scan-table thead th {
line-height: 1.3 !important; line-height: 1.3 !important;
padding: 6px 3px !important; padding: 6px 3px !important;
font-size: 11px !important; font-size: 11px !important;
background-color: var(--header-bg-color) !important; background-color: #e9ecef !important;
color: var(--header-text-color) !important;
font-weight: bold !important; font-weight: bold !important;
text-transform: none !important; text-transform: none !important;
letter-spacing: 0 !important; letter-spacing: 0 !important;
overflow: visible !important; overflow: visible !important;
box-sizing: border-box !important; box-sizing: border-box !important;
border: 1px solid var(--border-color) !important; border: 1px solid #ddd !important;
text-overflow: clip !important; text-overflow: clip !important;
position: relative !important; position: relative !important;
} }
@@ -42,9 +41,7 @@ table.view-orders-table.scan-table tbody td {
padding: 4px 2px !important; padding: 4px 2px !important;
font-size: 10px !important; font-size: 10px !important;
text-align: center !important; text-align: center !important;
border: 1px solid var(--border-color) !important; border: 1px solid #ddd !important;
background-color: var(--card-bg-color) !important;
color: var(--text-color) !important;
white-space: nowrap !important; white-space: nowrap !important;
overflow: hidden !important; overflow: hidden !important;
text-overflow: ellipsis !important; text-overflow: ellipsis !important;
@@ -71,7 +68,7 @@ table.view-orders-table.scan-table tbody td {
/* HOVER EFFECTS */ /* HOVER EFFECTS */
table.view-orders-table.scan-table tbody tr:hover td { table.view-orders-table.scan-table tbody tr:hover td {
background-color: var(--hover-color) !important; background-color: #f8f9fa !important;
} }
/* COLUMN WIDTH SPECIFICATIONS */ /* COLUMN WIDTH SPECIFICATIONS */

View File

@@ -89,7 +89,7 @@ def delete_locations_by_ids(ids_str):
cursor = conn.cursor() cursor = conn.cursor()
deleted = 0 deleted = 0
for id in ids: for id in ids:
cursor.execute("DELETE FROM warehouse_locations WHERE id = %s", (id,)) cursor.execute("DELETE FROM warehouse_locations WHERE id = ?", (id,))
if cursor.rowcount: if cursor.rowcount:
deleted += 1 deleted += 1
conn.commit() conn.commit()
@@ -226,20 +226,20 @@ def update_location(location_id, location_code, size, description):
cursor = conn.cursor() cursor = conn.cursor()
# Check if location exists # Check if location exists
cursor.execute("SELECT id FROM warehouse_locations WHERE id = %s", (location_id,)) cursor.execute("SELECT id FROM warehouse_locations WHERE id = ?", (location_id,))
if not cursor.fetchone(): if not cursor.fetchone():
conn.close() conn.close()
return {"success": False, "error": "Location not found"} return {"success": False, "error": "Location not found"}
# Check if location code already exists for different location # Check if location code already exists for different location
cursor.execute("SELECT id FROM warehouse_locations WHERE location_code = %s AND id != %s", (location_code, location_id)) cursor.execute("SELECT id FROM warehouse_locations WHERE location_code = ? AND id != ?", (location_code, location_id))
if cursor.fetchone(): if cursor.fetchone():
conn.close() conn.close()
return {"success": False, "error": "Location code already exists"} return {"success": False, "error": "Location code already exists"}
# Update location # Update location
cursor.execute( cursor.execute(
"UPDATE warehouse_locations SET location_code = %s, size = %s, description = %s WHERE id = %s", "UPDATE warehouse_locations SET location_code = ?, size = ?, description = ? WHERE id = ?",
(location_code, size if size else None, description, location_id) (location_code, size if size else None, description, location_id)
) )
conn.commit() conn.commit()
@@ -250,8 +250,6 @@ def update_location(location_id, location_code, size, description):
except Exception as e: except Exception as e:
print(f"Error updating location: {e}") print(f"Error updating location: {e}")
return {"success": False, "error": str(e)} return {"success": False, "error": str(e)}
print(f"Error updating location: {e}")
return {"success": False, "error": str(e)}
def delete_location_by_id(location_id): def delete_location_by_id(location_id):
"""Delete a warehouse location by ID""" """Delete a warehouse location by ID"""
@@ -260,14 +258,14 @@ def delete_location_by_id(location_id):
cursor = conn.cursor() cursor = conn.cursor()
# Check if location exists # Check if location exists
cursor.execute("SELECT location_code FROM warehouse_locations WHERE id = %s", (location_id,)) cursor.execute("SELECT location_code FROM warehouse_locations WHERE id = ?", (location_id,))
location = cursor.fetchone() location = cursor.fetchone()
if not location: if not location:
conn.close() conn.close()
return {"success": False, "error": "Location not found"} return {"success": False, "error": "Location not found"}
# Delete location # Delete location
cursor.execute("DELETE FROM warehouse_locations WHERE id = %s", (location_id,)) cursor.execute("DELETE FROM warehouse_locations WHERE id = ?", (location_id,))
conn.commit() conn.commit()
conn.close() conn.close()

View File

@@ -1,29 +0,0 @@
#!/usr/bin/env python3
import pymysql
try:
# Connect to the database
conn = pymysql.connect(
host='localhost',
database='trasabilitate',
user='trasabilitate',
password='Initial01!',
cursorclass=pymysql.cursors.DictCursor
)
with conn.cursor() as cursor:
cursor.execute("SELECT id, username, role, modules FROM users")
users = cursor.fetchall()
print("Current users in database:")
print("-" * 50)
for user in users:
print(f"ID: {user['id']}")
print(f"Username: {user['username']}")
print(f"Role: {user['role']}")
print(f"Modules: {user['modules']}")
print("-" * 30)
finally:
conn.close()

View File

@@ -1,43 +0,0 @@
#!/usr/bin/env python3
import pymysql
import json
try:
# Connect to the database
conn = pymysql.connect(
host='localhost',
database='trasabilitate',
user='trasabilitate',
password='Initial01!',
cursorclass=pymysql.cursors.DictCursor
)
with conn.cursor() as cursor:
cursor.execute("SELECT id, username, role, modules FROM users")
users = cursor.fetchall()
print("Debug: User data and get_modules() output:")
print("=" * 60)
for user_data in users:
print(f"Username: {user_data['username']}")
print(f"Role: {user_data['role']}")
print(f"Raw modules: {user_data['modules']} (type: {type(user_data['modules'])})")
# Simulate the get_modules() method
modules = user_data['modules']
if not modules:
parsed_modules = []
else:
try:
parsed_modules = json.loads(modules)
except:
parsed_modules = []
print(f"Parsed modules: {parsed_modules} (type: {type(parsed_modules)})")
print(f"JSON output: {json.dumps(parsed_modules)}")
print("-" * 40)
finally:
conn.close()

View File

@@ -1,44 +0,0 @@
#!/usr/bin/env python3
import pymysql
import json
try:
# Connect to the database
conn = pymysql.connect(
host='localhost',
database='trasabilitate',
user='trasabilitate',
password='Initial01!',
cursorclass=pymysql.cursors.DictCursor
)
with conn.cursor() as cursor:
# Update Ciprian's role from quality_manager to manager
print("Updating Ciprian's role from 'quality_manager' to 'manager'...")
cursor.execute("UPDATE users SET role = 'manager' WHERE username = 'Ciprian'")
# Assign quality module to Ciprian since he was a quality manager
quality_modules = json.dumps(['quality'])
print(f"Assigning quality module to Ciprian: {quality_modules}")
cursor.execute("UPDATE users SET modules = %s WHERE username = 'Ciprian'", (quality_modules,))
# Commit the changes
conn.commit()
print("Database updated successfully!")
# Show updated users
print("\nUpdated users:")
print("-" * 50)
cursor.execute("SELECT id, username, role, modules FROM users")
users = cursor.fetchall()
for user in users:
print(f"ID: {user['id']}")
print(f"Username: {user['username']}")
print(f"Role: {user['role']}")
print(f"Modules: {user['modules']}")
print("-" * 30)
finally:
conn.close()

View File

@@ -1,72 +0,0 @@
# Gunicorn Configuration File for Trasabilitate Application
# Production-ready WSGI server configuration
import multiprocessing
import os
# Server socket
bind = "0.0.0.0:8781"
backlog = 2048
# Worker processes
workers = multiprocessing.cpu_count() * 2 + 1
worker_class = "sync"
worker_connections = 1000
timeout = 30
keepalive = 2
# Restart workers after this many requests, to prevent memory leaks
max_requests = 1000
max_requests_jitter = 50
# Logging
accesslog = "/srv/quality_recticel/logs/access.log"
errorlog = "/srv/quality_recticel/logs/error.log"
loglevel = "info"
access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(D)s'
# Process naming
proc_name = 'trasabilitate_app'
# Daemon mode (set to True for production deployment)
daemon = False
# User/group to run worker processes
# user = "www-data"
# group = "www-data"
# Preload application for better performance
preload_app = True
# Enable automatic worker restarts
max_requests = 1000
max_requests_jitter = 100
# SSL Configuration (uncomment if using HTTPS)
# keyfile = "/path/to/ssl/private.key"
# certfile = "/path/to/ssl/certificate.crt"
# Security
limit_request_line = 4094
limit_request_fields = 100
limit_request_field_size = 8190
def when_ready(server):
"""Called just after the server is started."""
server.log.info("Trasabilitate Application server is ready. Listening on: %s", server.address)
def worker_int(worker):
"""Called just after a worker exited on SIGINT or SIGQUIT."""
worker.log.info("Worker received INT or QUIT signal")
def pre_fork(server, worker):
"""Called just before a worker is forked."""
server.log.info("Worker spawned (pid: %s)", worker.pid)
def post_fork(server, worker):
"""Called just after a worker has been forked."""
server.log.info("Worker spawned (pid: %s)", worker.pid)
def worker_abort(worker):
"""Called when a worker received the SIGABRT signal."""
worker.log.info("Worker received SIGABRT signal")

View File

@@ -1,5 +1,5 @@
server_domain=localhost server_domain=localhost
port=3306 port=3602
database_name=trasabilitate database_name=trasabilitate_database
username=trasabilitate username=trasabilitate
password=Initial01! password=Initial01!

BIN
py_app/instance/users.db Normal file → Executable file

Binary file not shown.

View File

@@ -1,114 +0,0 @@
#!/bin/bash
# Production Management Script for Trasabilitate Application
# Usage: ./manage_production.sh {start|stop|restart|status|logs|install-service}
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
print_success() {
echo -e "${GREEN}$1${NC}"
}
print_warning() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
print_error() {
echo -e "${RED}$1${NC}"
}
print_info() {
echo -e "${BLUE} $1${NC}"
}
usage() {
echo "Usage: $0 {start|stop|restart|status|logs|install-service|help}"
echo ""
echo "Commands:"
echo " start Start the production server"
echo " stop Stop the production server"
echo " restart Restart the production server"
echo " status Show server status"
echo " logs Show recent logs"
echo " install-service Install systemd service"
echo " help Show this help message"
echo ""
}
case "$1" in
start)
./start_production.sh
;;
stop)
./stop_production.sh
;;
restart)
echo -e "${BLUE}🔄 Restarting Trasabilitate Application${NC}"
echo "=============================================="
./stop_production.sh
sleep 2
./start_production.sh
;;
status)
./status_production.sh
;;
logs)
echo -e "${BLUE}📋 Recent Error Logs${NC}"
echo "=============================================="
if [[ -f "/srv/quality_recticel/logs/error.log" ]]; then
tail -20 /srv/quality_recticel/logs/error.log
else
print_error "Error log file not found"
fi
echo ""
echo -e "${BLUE}📋 Recent Access Logs${NC}"
echo "=============================================="
if [[ -f "/srv/quality_recticel/logs/access.log" ]]; then
tail -10 /srv/quality_recticel/logs/access.log
else
print_error "Access log file not found"
fi
;;
install-service)
echo -e "${BLUE}📦 Installing Systemd Service${NC}"
echo "=============================================="
if [[ ! -f "trasabilitate.service" ]]; then
print_error "Service file not found"
exit 1
fi
print_info "Installing service file..."
sudo cp trasabilitate.service /etc/systemd/system/
print_info "Reloading systemd daemon..."
sudo systemctl daemon-reload
print_info "Enabling service..."
sudo systemctl enable trasabilitate.service
print_success "Service installed successfully!"
echo ""
echo "Systemd commands:"
echo " sudo systemctl start trasabilitate # Start service"
echo " sudo systemctl stop trasabilitate # Stop service"
echo " sudo systemctl restart trasabilitate # Restart service"
echo " sudo systemctl status trasabilitate # Check status"
echo " sudo systemctl enable trasabilitate # Enable auto-start"
echo " sudo systemctl disable trasabilitate # Disable auto-start"
;;
help|--help|-h)
usage
;;
*)
print_error "Invalid command: $1"
echo ""
usage
exit 1
;;
esac

View File

@@ -1,142 +0,0 @@
#!/bin/bash
# Trasabilitate Application - Quick Deployment Script
# This script handles the complete deployment process
set -e # Exit on any error
echo "🚀 Trasabilitate Application - Quick Deployment"
echo "=============================================="
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
print_step() {
echo -e "\n${BLUE}📋 Step $1: $2${NC}"
echo "----------------------------------------"
}
print_success() {
echo -e "${GREEN}$1${NC}"
}
print_warning() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
print_error() {
echo -e "${RED}$1${NC}"
}
# Check if running as root
if [[ $EUID -eq 0 ]]; then
print_error "This script should not be run as root for security reasons"
exit 1
fi
print_step 1 "Checking Prerequisites"
# Check if we're in the right directory
if [[ ! -f "run.py" ]]; then
print_error "Please run this script from the py_app directory"
print_error "Expected location: /srv/quality_recticel/py_app"
exit 1
fi
print_success "Running from correct directory"
# Check if MariaDB is running
if ! systemctl is-active --quiet mariadb; then
print_warning "MariaDB is not running. Attempting to start..."
if sudo systemctl start mariadb; then
print_success "MariaDB started successfully"
else
print_error "Failed to start MariaDB. Please start it manually:"
print_error "sudo systemctl start mariadb"
exit 1
fi
else
print_success "MariaDB is running"
fi
# Check if virtual environment exists
if [[ ! -d "../recticel" ]]; then
print_error "Virtual environment 'recticel' not found"
print_error "Please create it first:"
print_error "python3 -m venv ../recticel"
exit 1
fi
print_success "Virtual environment found"
print_step 2 "Setting up Database and User"
# Create database and user
print_warning "You may be prompted for the MySQL root password"
if sudo mysql -e "CREATE DATABASE IF NOT EXISTS trasabilitate; CREATE USER IF NOT EXISTS 'trasabilitate'@'localhost' IDENTIFIED BY 'Initial01!'; GRANT ALL PRIVILEGES ON trasabilitate.* TO 'trasabilitate'@'localhost'; FLUSH PRIVILEGES;" 2>/dev/null; then
print_success "Database 'trasabilitate' and user created successfully"
else
print_error "Failed to create database or user. Please check MySQL root access"
exit 1
fi
print_step 3 "Activating Virtual Environment and Installing Dependencies"
# Activate virtual environment and install dependencies
source ../recticel/bin/activate
if pip install -r requirements.txt > /dev/null 2>&1; then
print_success "Python dependencies installed/verified"
else
print_error "Failed to install Python dependencies"
exit 1
fi
print_step 4 "Running Complete Database Setup"
# Run the comprehensive database setup script
if python3 app/db_create_scripts/setup_complete_database.py; then
print_success "Database setup completed successfully"
else
print_error "Database setup failed"
exit 1
fi
print_step 5 "Testing Application Startup"
# Test if the application can start (run for 3 seconds then kill)
print_warning "Testing application startup (will stop after 3 seconds)..."
timeout 3s python3 run.py > /dev/null 2>&1 || true
print_success "Application startup test completed"
echo ""
echo "=============================================="
echo -e "${GREEN}🎉 DEPLOYMENT COMPLETED SUCCESSFULLY!${NC}"
echo "=============================================="
echo ""
echo "📋 Deployment Summary:"
echo " • MariaDB database and user configured"
echo " • All database tables and triggers created"
echo " • Permissions system initialized"
echo " • Default superadmin user ready"
echo ""
echo "🚀 To start the application:"
echo " cd /srv/quality_recticel/py_app"
echo " source ../recticel/bin/activate"
echo " python3 run.py"
echo ""
echo "🌐 Application URLs:"
echo " • Local: http://127.0.0.1:8781"
echo " • Network: http://$(hostname -I | awk '{print $1}'):8781"
echo ""
echo "👤 Default Login:"
echo " • Username: superadmin"
echo " • Password: superadmin123"
echo ""
echo -e "${YELLOW}⚠️ Remember to change the default password after first login!${NC}"
echo ""

View File

@@ -6,6 +6,4 @@ flask-sqlalchemy
pyodbc pyodbc
mariadb mariadb
reportlab reportlab
requests requests
pandas
openpyxl

View File

@@ -1,163 +0,0 @@
#!/bin/bash
# Production Startup Script for Trasabilitate Application
# This script starts the application using Gunicorn WSGI server
set -e # Exit on any error
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
print_step() {
echo -e "\n${BLUE}📋 $1${NC}"
echo "----------------------------------------"
}
print_success() {
echo -e "${GREEN}$1${NC}"
}
print_warning() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
print_error() {
echo -e "${RED}$1${NC}"
}
echo -e "${BLUE}🚀 Trasabilitate Application - Production Startup${NC}"
echo "=============================================="
# Check if we're in the right directory
if [[ ! -f "wsgi.py" ]]; then
print_error "Please run this script from the py_app directory"
print_error "Expected location: /srv/quality_recticel/py_app"
exit 1
fi
print_step "Checking Prerequisites"
# Check if virtual environment exists
if [[ ! -d "../recticel" ]]; then
print_error "Virtual environment 'recticel' not found"
print_error "Please create it first or run './quick_deploy.sh'"
exit 1
fi
print_success "Virtual environment found"
# Activate virtual environment
print_step "Activating Virtual Environment"
source ../recticel/bin/activate
print_success "Virtual environment activated"
# Check if Gunicorn is installed
if ! command -v gunicorn &> /dev/null; then
print_error "Gunicorn not found. Installing..."
pip install gunicorn
fi
print_success "Gunicorn is available"
# Check database connection
print_step "Testing Database Connection"
if python3 -c "
import mariadb
try:
conn = mariadb.connect(user='trasabilitate', password='Initial01!', host='localhost', database='trasabilitate')
conn.close()
print('Database connection successful')
except Exception as e:
print(f'Database connection failed: {e}')
exit(1)
" > /dev/null 2>&1; then
print_success "Database connection verified"
else
print_error "Database connection failed. Please run database setup first:"
print_error "python3 app/db_create_scripts/setup_complete_database.py"
exit 1
fi
# Create PID file directory
print_step "Setting up Runtime Environment"
mkdir -p ../run
print_success "Runtime directory created"
# Check if already running
PID_FILE="../run/trasabilitate.pid"
if [[ -f "$PID_FILE" ]]; then
PID=$(cat "$PID_FILE")
if ps -p "$PID" > /dev/null 2>&1; then
print_warning "Application is already running (PID: $PID)"
echo "To stop the application, run:"
echo "kill $PID"
echo "rm $PID_FILE"
exit 1
else
print_warning "Stale PID file found, removing..."
rm -f "$PID_FILE"
fi
fi
# Start Gunicorn
print_step "Starting Production Server"
echo "Starting Gunicorn WSGI server..."
echo "Configuration: gunicorn.conf.py"
echo "Workers: $(python3 -c 'import multiprocessing; print(multiprocessing.cpu_count() * 2 + 1)')"
echo "Binding to: 0.0.0.0:8781"
echo ""
# Start Gunicorn with configuration file
gunicorn --config gunicorn.conf.py \
--pid "$PID_FILE" \
--daemon \
wsgi:application
# Wait a moment for startup
sleep 2
# Check if the process started successfully
if [[ -f "$PID_FILE" ]]; then
PID=$(cat "$PID_FILE")
if ps -p "$PID" > /dev/null 2>&1; then
print_success "Application started successfully!"
echo ""
echo "=============================================="
echo -e "${GREEN}🎉 PRODUCTION SERVER RUNNING${NC}"
echo "=============================================="
echo ""
echo "📋 Server Information:"
echo " • Process ID: $PID"
echo " • Configuration: gunicorn.conf.py"
echo " • Access Log: /srv/quality_recticel/logs/access.log"
echo " • Error Log: /srv/quality_recticel/logs/error.log"
echo ""
echo "🌐 Application URLs:"
echo " • Local: http://127.0.0.1:8781"
echo " • Network: http://$(hostname -I | awk '{print $1}'):8781"
echo ""
echo "👤 Default Login:"
echo " • Username: superadmin"
echo " • Password: superadmin123"
echo ""
echo "🔧 Management Commands:"
echo " • Stop server: kill $PID && rm $PID_FILE"
echo " • View logs: tail -f /srv/quality_recticel/logs/error.log"
echo " • Monitor access: tail -f /srv/quality_recticel/logs/access.log"
echo " • Server status: ps -p $PID"
echo ""
print_warning "Server is running in daemon mode (background)"
else
print_error "Failed to start application. Check logs:"
print_error "tail /srv/quality_recticel/logs/error.log"
exit 1
fi
else
print_error "Failed to create PID file. Check permissions and logs."
exit 1
fi

View File

@@ -1,78 +0,0 @@
#!/bin/bash
# Production Status Script for Trasabilitate Application
# This script shows the current status of the Gunicorn WSGI server
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
print_success() {
echo -e "${GREEN}$1${NC}"
}
print_warning() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
print_error() {
echo -e "${RED}$1${NC}"
}
echo -e "${BLUE}📊 Trasabilitate Application - Status Check${NC}"
echo "=============================================="
PID_FILE="../run/trasabilitate.pid"
if [[ ! -f "$PID_FILE" ]]; then
print_error "Application is not running (no PID file found)"
echo "To start the application, run: ./start_production.sh"
exit 1
fi
PID=$(cat "$PID_FILE")
if ps -p "$PID" > /dev/null 2>&1; then
print_success "Application is running (PID: $PID)"
echo ""
echo "📋 Process Information:"
ps -p "$PID" -o pid,ppid,pcpu,pmem,etime,cmd --no-headers | while read line; do
echo " $line"
done
echo ""
echo "🌐 Server Information:"
echo " • Listening on: 0.0.0.0:8781"
echo " • Local URL: http://127.0.0.1:8781"
echo " • Network URL: http://$(hostname -I | awk '{print $1}'):8781"
echo ""
echo "📁 Log Files:"
echo " • Access Log: /srv/quality_recticel/logs/access.log"
echo " • Error Log: /srv/quality_recticel/logs/error.log"
echo ""
echo "🔧 Quick Commands:"
echo " • Stop server: ./stop_production.sh"
echo " • Restart server: ./stop_production.sh && ./start_production.sh"
echo " • View error log: tail -f /srv/quality_recticel/logs/error.log"
echo " • View access log: tail -f /srv/quality_recticel/logs/access.log"
echo ""
# Check if the web server is responding
if command -v curl > /dev/null 2>&1; then
echo "🌐 Connection Test:"
if curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8781 | grep -q "200\|302\|401"; then
print_success "Web server is responding"
else
print_warning "Web server may not be responding properly"
fi
fi
else
print_error "Process $PID not found (stale PID file)"
print_warning "Cleaning up stale PID file..."
rm -f "$PID_FILE"
echo "To start the application, run: ./start_production.sh"
exit 1
fi

View File

@@ -1,69 +0,0 @@
#!/bin/bash
# Production Stop Script for Trasabilitate Application
# This script stops the Gunicorn WSGI server
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
print_success() {
echo -e "${GREEN}$1${NC}"
}
print_warning() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
print_error() {
echo -e "${RED}$1${NC}"
}
echo -e "${BLUE}🛑 Trasabilitate Application - Production Stop${NC}"
echo "=============================================="
PID_FILE="../run/trasabilitate.pid"
if [[ ! -f "$PID_FILE" ]]; then
print_warning "No PID file found. Server may not be running."
echo "PID file location: $PID_FILE"
exit 1
fi
PID=$(cat "$PID_FILE")
if ps -p "$PID" > /dev/null 2>&1; then
echo "Stopping Trasabilitate application (PID: $PID)..."
# Send SIGTERM first (graceful shutdown)
kill "$PID"
# Wait for graceful shutdown
sleep 3
# Check if still running
if ps -p "$PID" > /dev/null 2>&1; then
print_warning "Process still running, sending SIGKILL..."
kill -9 "$PID"
sleep 1
fi
# Check if process is finally stopped
if ! ps -p "$PID" > /dev/null 2>&1; then
print_success "Application stopped successfully"
rm -f "$PID_FILE"
else
print_error "Failed to stop application (PID: $PID)"
exit 1
fi
else
print_warning "Process $PID not found. Cleaning up PID file..."
rm -f "$PID_FILE"
print_success "PID file cleaned up"
fi
echo ""
print_success "Trasabilitate application has been stopped"

View File

@@ -1,73 +0,0 @@
#!/usr/bin/env python3
import pymysql
import json
def test_login_data():
try:
# Connect to the database
conn = pymysql.connect(
host='localhost',
database='trasabilitate',
user='trasabilitate',
password='Initial01!',
cursorclass=pymysql.cursors.DictCursor
)
with conn.cursor() as cursor:
# Simulate login for Ciprian
cursor.execute("SELECT username, password, role, modules FROM users WHERE username = 'Ciprian'")
user = cursor.fetchone()
if user:
print("Ciprian's database record:")
print(f"Username: {user['username']}")
print(f"Role: {user['role']}")
print(f"Raw modules: {user['modules']}")
# Simulate what happens in login
user_modules = []
if user['modules']:
try:
user_modules = json.loads(user['modules'])
print(f"Parsed modules: {user_modules}")
except Exception as e:
print(f"Error parsing modules: {e}")
user_modules = []
# Check if user should have quality access
has_quality = 'quality' in user_modules
print(f"Has quality module access: {has_quality}")
# Check role level
ROLES = {
'superadmin': {'level': 100},
'admin': {'level': 90},
'manager': {'level': 70},
'worker': {'level': 50}
}
user_level = ROLES.get(user['role'], {}).get('level', 0)
print(f"Role level: {user_level}")
# Test access control logic
print("\nAccess Control Test:")
print(f"Required modules: ['quality']")
print(f"User role: {user['role']}")
print(f"User modules: {user_modules}")
if user['role'] in ['superadmin', 'admin']:
print("✅ Access granted: Superadmin/Admin has access to all modules")
elif any(module in user_modules for module in ['quality']):
print("✅ Access granted: User has required quality module")
else:
print("❌ Access denied: User does not have quality module")
else:
print("User 'Ciprian' not found!")
finally:
conn.close()
if __name__ == "__main__":
test_login_data()

View File

@@ -1,49 +0,0 @@
#!/usr/bin/env python3
"""
Quick test for updated worker permissions
"""
import sys
import os
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'app'))
from permissions_simple import validate_user_modules
def test_worker_multiple_modules():
"""Test that workers can now have multiple modules"""
print("Testing Updated Worker Module Permissions")
print("=" * 45)
test_cases = [
# (role, modules, expected_result, description)
('worker', ['quality'], True, "Worker with quality module"),
('worker', ['warehouse'], True, "Worker with warehouse module"),
('worker', ['quality', 'warehouse'], True, "Worker with multiple modules (NEW)"),
('worker', ['quality', 'warehouse', 'labels'], True, "Worker with all modules (NEW)"),
('worker', [], False, "Worker with no modules"),
('manager', ['quality', 'warehouse'], True, "Manager with multiple modules"),
]
passed = 0
failed = 0
for role, modules, expected, description in test_cases:
is_valid, error_msg = validate_user_modules(role, modules)
status = "PASS" if is_valid == expected else "FAIL"
print(f"{status}: {description}")
print(f" Role: {role}, Modules: {modules} -> {is_valid} (expected {expected})")
if error_msg:
print(f" Error: {error_msg}")
print()
if is_valid == expected:
passed += 1
else:
failed += 1
print(f"Results: {passed} passed, {failed} failed")
print("\n✅ Workers can now have multiple modules!" if failed == 0 else "❌ Some tests failed")
if __name__ == "__main__":
test_worker_multiple_modules()

View File

@@ -1,27 +0,0 @@
[Unit]
Description=Trasabilitate Quality Management Application
After=network.target mariadb.service
Wants=mariadb.service
[Service]
Type=forking
User=ske087
Group=ske087
WorkingDirectory=/srv/quality_recticel/py_app
Environment="PATH=/srv/quality_recticel/recticel/bin"
ExecStart=/srv/quality_recticel/recticel/bin/gunicorn --config gunicorn.conf.py --pid /srv/quality_recticel/run/trasabilitate.pid --daemon wsgi:application
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s TERM $MAINPID
PIDFile=/srv/quality_recticel/run/trasabilitate.pid
Restart=always
RestartSec=10
# Security settings
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/srv/quality_recticel
ProtectHome=true
[Install]
WantedBy=multi-user.target

View File

@@ -1,19 +0,0 @@
#!/usr/bin/env python3
"""
WSGI Entry Point for Trasabilitate Application
This file serves as the entry point for WSGI servers like Gunicorn.
"""
import os
import sys
from app import create_app
# Add the current directory to Python path
sys.path.insert(0, os.path.dirname(__file__))
# Create the Flask application instance
application = create_app()
# For debugging purposes (will be ignored in production)
if __name__ == "__main__":
application.run(debug=False, host='0.0.0.0', port=8781)

Some files were not shown because too many files have changed in this diff Show More