Compare commits
1 Commits
c96039542d
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| b87c398977 |
@@ -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
|
|
||||||
13
.env.example
13
.env.example
@@ -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
15
.gitignore
vendored
@@ -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
|
||||||
|
|||||||
41
Dockerfile
41
Dockerfile
@@ -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"]
|
|
||||||
93
Makefile
93
Makefile
@@ -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!"
|
|
||||||
88
deploy.sh
88
deploy.sh
@@ -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 ""
|
|
||||||
@@ -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
|
|
||||||
@@ -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 "$@"
|
|
||||||
20
init-db.sql
20
init-db.sql
@@ -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
|
|
||||||
154
logs/access.log
154
logs/access.log
@@ -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
|
|
||||||
116
logs/error.log
116
logs/error.log
@@ -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)
|
|
||||||
@@ -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.
|
|
||||||
@@ -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]
|
|
||||||
@@ -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! 🚀**
|
|
||||||
@@ -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**.
|
|
||||||
@@ -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.
|
|
||||||
@@ -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.")
|
|
||||||
@@ -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()
|
|
||||||
@@ -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()
|
|
||||||
263
py_app/ENHANCED_PRINT_CONTROLLER.md
Executable file
263
py_app/ENHANCED_PRINT_CONTROLLER.md
Executable 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
|
||||||
133
py_app/MOBILE_LOGIN_GUIDE.md
Normal file
133
py_app/MOBILE_LOGIN_GUIDE.md
Normal 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
125
py_app/PRINT_PROGRESS_FEATURE.md
Executable 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
|
||||||
@@ -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.
|
|
||||||
Binary file not shown.
Binary file not shown.
@@ -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)
|
|
||||||
151
py_app/app/db_create_scripts/add_printed_labels_column.py
Executable file
151
py_app/app/db_create_scripts/add_printed_labels_column.py
Executable 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)
|
||||||
@@ -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
|
||||||
@@ -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:
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
|
||||||
@@ -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()
|
|
||||||
@@ -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()
|
|
||||||
@@ -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
|
|
||||||
@@ -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()
|
||||||
|
|||||||
1175
py_app/app/routes.py
1175
py_app/app/routes.py
File diff suppressed because it is too large
Load Diff
@@ -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');
|
|
||||||
});
|
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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()">×</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()">×</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 %}
|
||||||
|
|||||||
@@ -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>
|
||||||
<a href="{{ url_for('main.reports') }}" class="btn">Access to Quality reports</a>
|
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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">×</span>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="calendar-container">
|
|
||||||
<div class="calendar-header">
|
|
||||||
<button id="prev-month" class="calendar-nav"><</button>
|
|
||||||
<h3 id="calendar-month-year"></h3>
|
|
||||||
<button id="next-month" class="calendar-nav">></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">×</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 %}
|
|
||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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 %}
|
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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">
|
{% else %}
|
||||||
|
<h3>Preview Table</h3>
|
||||||
|
{% endif %}
|
||||||
|
<table class="scan-table view-orders-table{% if leftover_description %} leftover-table{% endif %}">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
{% for header in headers %}
|
<th>ID</th>
|
||||||
<th>{{ header }}</th>
|
<th>Comanda<br>Productie</th>
|
||||||
{% endfor %}
|
<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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% if preview_data %}
|
{% if orders %}
|
||||||
{% for row in preview_data %}
|
{% for order in orders %}
|
||||||
|
{% if order and (order.get('comanda_productie', '') or order.get('descr_com_prod', '')) %}
|
||||||
<tr>
|
<tr>
|
||||||
{% for header in headers %}
|
<td>{{ order.get('id', '') }}</td>
|
||||||
<td>{{ row.get(header, '') }}</td>
|
<td><strong>{{ order.get('comanda_productie', '') }}</strong></td>
|
||||||
{% endfor %}
|
<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>
|
</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 %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<tr><td colspan="{{ headers|length }}" style="text-align:center;">No data to preview</td></tr>
|
<tr><td colspan="15" style="text-align:center;">No CSV file uploaded yet.</td></tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
{% else %}
|
</div>
|
||||||
<h3>CSV Data Preview</h3>
|
|
||||||
<table class="scan-table">
|
{% if validation_errors or validation_warnings %}
|
||||||
<thead>
|
{% if not leftover_description %}
|
||||||
<tr>
|
<div class="card" style="margin-bottom: 24px;">
|
||||||
<th>Upload a CSV file to see preview</th>
|
<h4>Validation Results</h4>
|
||||||
</tr>
|
{% if validation_errors %}
|
||||||
</thead>
|
<div style="color: #dc3545; margin-bottom: 16px;">
|
||||||
<tbody>
|
<strong>Errors found:</strong>
|
||||||
<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>
|
<ul>
|
||||||
</tbody>
|
{% for error in validation_errors %}
|
||||||
</table>
|
<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 %}
|
{% endif %}
|
||||||
</div>
|
</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>
|
||||||
|
|||||||
@@ -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 %}
|
|
||||||
@@ -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 */
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
|
||||||
@@ -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()
|
|
||||||
@@ -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()
|
|
||||||
@@ -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")
|
|
||||||
@@ -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
BIN
py_app/instance/users.db
Normal file → Executable file
Binary file not shown.
@@ -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
|
|
||||||
@@ -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 ""
|
|
||||||
@@ -7,5 +7,3 @@ pyodbc
|
|||||||
mariadb
|
mariadb
|
||||||
reportlab
|
reportlab
|
||||||
requests
|
requests
|
||||||
pandas
|
|
||||||
openpyxl
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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"
|
|
||||||
@@ -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()
|
|
||||||
@@ -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()
|
|
||||||
@@ -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
|
|
||||||
@@ -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
Reference in New Issue
Block a user