updated documentation folder
This commit is contained in:
@@ -27,6 +27,7 @@ DB_RETRY_INTERVAL=2
|
|||||||
DB_DATA_PATH=/srv/docker-test/mariadb
|
DB_DATA_PATH=/srv/docker-test/mariadb
|
||||||
LOGS_PATH=/srv/docker-test/logs
|
LOGS_PATH=/srv/docker-test/logs
|
||||||
INSTANCE_PATH=/srv/docker-test/instance
|
INSTANCE_PATH=/srv/docker-test/instance
|
||||||
|
BACKUP_PATH=/srv/docker-test/backups
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# APPLICATION CONFIGURATION
|
# APPLICATION CONFIGURATION
|
||||||
|
|||||||
74
README.md
Normal file
74
README.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
# Quality Recticel Application
|
||||||
|
|
||||||
|
Production traceability and quality management system.
|
||||||
|
|
||||||
|
## 📚 Documentation
|
||||||
|
|
||||||
|
All development and deployment documentation has been moved to the **[documentation](./documentation/)** folder.
|
||||||
|
|
||||||
|
### Quick Links
|
||||||
|
|
||||||
|
- **[Documentation Index](./documentation/README.md)** - Complete documentation overview
|
||||||
|
- **[Database Setup](./documentation/DATABASE_DOCKER_SETUP.md)** - Database configuration guide
|
||||||
|
- **[Docker Guide](./documentation/DOCKER_QUICK_REFERENCE.md)** - Docker commands reference
|
||||||
|
- **[Backup System](./documentation/BACKUP_SYSTEM.md)** - Database backup documentation
|
||||||
|
|
||||||
|
## 🚀 Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start application
|
||||||
|
cd /srv/quality_app/py_app
|
||||||
|
bash start_production.sh
|
||||||
|
|
||||||
|
# Stop application
|
||||||
|
bash stop_production.sh
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
tail -f /srv/quality_app/logs/error.log
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📦 Docker Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Start with Docker Compose
|
||||||
|
docker-compose up -d
|
||||||
|
|
||||||
|
# View logs
|
||||||
|
docker-compose logs -f web
|
||||||
|
|
||||||
|
# Stop services
|
||||||
|
docker-compose down
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🔐 Default Access
|
||||||
|
|
||||||
|
- **URL**: http://localhost:8781
|
||||||
|
- **Username**: superadmin
|
||||||
|
- **Password**: superadmin123
|
||||||
|
|
||||||
|
## 📁 Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
quality_app/
|
||||||
|
├── documentation/ # All documentation files
|
||||||
|
├── py_app/ # Flask application
|
||||||
|
├── backups/ # Database backups
|
||||||
|
├── logs/ # Application logs
|
||||||
|
├── docker-compose.yml # Docker configuration
|
||||||
|
└── Dockerfile # Container image definition
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📖 For More Information
|
||||||
|
|
||||||
|
See the **[documentation](./documentation/)** folder for comprehensive guides on:
|
||||||
|
|
||||||
|
- Setup and deployment
|
||||||
|
- Docker configuration
|
||||||
|
- Database management
|
||||||
|
- Backup and restore procedures
|
||||||
|
- Application features
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Version**: 1.0.0
|
||||||
|
**Last Updated**: November 3, 2025
|
||||||
@@ -145,6 +145,11 @@ services:
|
|||||||
TZ: ${TZ:-Europe/Bucharest}
|
TZ: ${TZ:-Europe/Bucharest}
|
||||||
LANG: ${LANG:-en_US.UTF-8}
|
LANG: ${LANG:-en_US.UTF-8}
|
||||||
|
|
||||||
|
# ======================================================================
|
||||||
|
# Backup Configuration
|
||||||
|
# ======================================================================
|
||||||
|
BACKUP_PATH: ${BACKUP_PATH:-/srv/quality_recticel/backups}
|
||||||
|
|
||||||
ports:
|
ports:
|
||||||
- "${APP_PORT:-8781}:8781"
|
- "${APP_PORT:-8781}:8781"
|
||||||
|
|
||||||
@@ -155,6 +160,9 @@ services:
|
|||||||
# Instance configuration directory
|
# Instance configuration directory
|
||||||
- ${INSTANCE_PATH:-/srv/docker-test/instance}:/app/instance
|
- ${INSTANCE_PATH:-/srv/docker-test/instance}:/app/instance
|
||||||
|
|
||||||
|
# Database backups directory
|
||||||
|
- ${BACKUP_PATH:-/srv/docker-test/backups}:/srv/quality_recticel/backups
|
||||||
|
|
||||||
# ⚠️ DEVELOPMENT ONLY: Mount application code for live updates
|
# ⚠️ DEVELOPMENT ONLY: Mount application code for live updates
|
||||||
# DISABLE IN PRODUCTION - causes configuration and security issues
|
# DISABLE IN PRODUCTION - causes configuration and security issues
|
||||||
# - ./py_app:/app
|
# - ./py_app:/app
|
||||||
|
|||||||
205
documentation/BACKUP_SYSTEM.md
Normal file
205
documentation/BACKUP_SYSTEM.md
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
# Database Backup System Documentation
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
The Quality Recticel application now includes a comprehensive database backup management system accessible from the Settings page for superadmin and admin users.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### 1. Manual Backup
|
||||||
|
- **Backup Now** button creates an immediate full database backup
|
||||||
|
- Uses `mysqldump` to create complete SQL export
|
||||||
|
- Includes all tables, triggers, routines, and events
|
||||||
|
- Each backup is timestamped: `backup_trasabilitate_YYYYMMDD_HHMMSS.sql`
|
||||||
|
|
||||||
|
### 2. Scheduled Backups
|
||||||
|
Configure automated backups with:
|
||||||
|
- **Enable/Disable**: Toggle scheduled backups on/off
|
||||||
|
- **Backup Time**: Set time of day for automatic backup (default: 02:00)
|
||||||
|
- **Frequency**: Choose Daily, Weekly, or Monthly backups
|
||||||
|
- **Retention Period**: Automatically delete backups older than N days (default: 30 days)
|
||||||
|
|
||||||
|
### 3. Backup Management
|
||||||
|
- **List Backups**: View all available backup files with size and creation date
|
||||||
|
- **Download**: Download any backup file to your local computer
|
||||||
|
- **Delete**: Remove old or unnecessary backup files
|
||||||
|
- **Restore**: (Superadmin only) Restore database from a backup file
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Backup Path
|
||||||
|
The backup location can be configured in three ways (priority order):
|
||||||
|
|
||||||
|
1. **Environment Variable** (Docker):
|
||||||
|
```yaml
|
||||||
|
# docker-compose.yml
|
||||||
|
environment:
|
||||||
|
BACKUP_PATH: /srv/quality_recticel/backups
|
||||||
|
volumes:
|
||||||
|
- /srv/docker-test/backups:/srv/quality_recticel/backups
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Configuration File**:
|
||||||
|
```ini
|
||||||
|
# py_app/instance/external_server.conf
|
||||||
|
backup_path=/srv/quality_app/backups
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Default Path**: `/srv/quality_app/backups`
|
||||||
|
|
||||||
|
### .env Configuration
|
||||||
|
Add to your `.env` file:
|
||||||
|
```bash
|
||||||
|
BACKUP_PATH=/srv/docker-test/backups
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Access Backup Management
|
||||||
|
1. Login as **superadmin** or **admin**
|
||||||
|
2. Navigate to **Settings** page
|
||||||
|
3. Scroll to **💾 Database Backup Management** card
|
||||||
|
4. The backup management interface is only visible to superadmin/admin users
|
||||||
|
|
||||||
|
### Create Manual Backup
|
||||||
|
1. Click **⚡ Backup Now** button
|
||||||
|
2. Wait for confirmation message
|
||||||
|
3. New backup appears in the list
|
||||||
|
|
||||||
|
### Configure Scheduled Backups
|
||||||
|
1. Check **Enable Scheduled Backups**
|
||||||
|
2. Set desired backup time (24-hour format)
|
||||||
|
3. Select frequency (Daily/Weekly/Monthly)
|
||||||
|
4. Set retention period (days to keep backups)
|
||||||
|
5. Click **💾 Save Schedule**
|
||||||
|
|
||||||
|
### Download Backup
|
||||||
|
1. Locate backup in the list
|
||||||
|
2. Click **⬇️ Download** button
|
||||||
|
3. File downloads to your computer
|
||||||
|
|
||||||
|
### Delete Backup
|
||||||
|
1. Locate backup in the list
|
||||||
|
2. Click **🗑️ Delete** button
|
||||||
|
3. Confirm deletion
|
||||||
|
|
||||||
|
### Restore Backup (Superadmin Only)
|
||||||
|
⚠️ **WARNING**: Restore will replace current database!
|
||||||
|
1. This feature requires superadmin privileges
|
||||||
|
2. API endpoint: `/api/backup/restore/<filename>`
|
||||||
|
3. Use with extreme caution
|
||||||
|
|
||||||
|
## Technical Details
|
||||||
|
|
||||||
|
### Backup Module
|
||||||
|
Location: `py_app/app/database_backup.py`
|
||||||
|
|
||||||
|
Key Class: `DatabaseBackupManager`
|
||||||
|
|
||||||
|
Methods:
|
||||||
|
- `create_backup()`: Create new backup
|
||||||
|
- `list_backups()`: Get all backup files
|
||||||
|
- `delete_backup(filename)`: Remove backup file
|
||||||
|
- `restore_backup(filename)`: Restore from backup
|
||||||
|
- `get_backup_schedule()`: Get current schedule
|
||||||
|
- `save_backup_schedule(schedule)`: Update schedule
|
||||||
|
- `cleanup_old_backups(days)`: Remove old backups
|
||||||
|
|
||||||
|
### API Endpoints
|
||||||
|
|
||||||
|
| Endpoint | Method | Access | Description |
|
||||||
|
|----------|--------|--------|-------------|
|
||||||
|
| `/api/backup/create` | POST | Admin+ | Create new backup |
|
||||||
|
| `/api/backup/list` | GET | Admin+ | List all backups |
|
||||||
|
| `/api/backup/download/<filename>` | GET | Admin+ | Download backup file |
|
||||||
|
| `/api/backup/delete/<filename>` | DELETE | Admin+ | Delete backup file |
|
||||||
|
| `/api/backup/schedule` | GET/POST | Admin+ | Get/Set backup schedule |
|
||||||
|
| `/api/backup/restore/<filename>` | POST | Superadmin | Restore from backup |
|
||||||
|
|
||||||
|
### Backup File Format
|
||||||
|
- **Format**: SQL dump file (`.sql`)
|
||||||
|
- **Compression**: Not compressed (can be gzip manually if needed)
|
||||||
|
- **Contents**: Complete database with structure and data
|
||||||
|
- **Metadata**: Stored in `backups_metadata.json`
|
||||||
|
|
||||||
|
### Schedule Storage
|
||||||
|
Schedule configuration stored in: `{BACKUP_PATH}/backup_schedule.json`
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"enabled": true,
|
||||||
|
"time": "02:00",
|
||||||
|
"frequency": "daily",
|
||||||
|
"retention_days": 30
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
1. **Access Control**: Backup features restricted to admin and superadmin users
|
||||||
|
2. **Path Traversal Protection**: Filenames validated to prevent directory traversal attacks
|
||||||
|
3. **Credentials**: Database credentials read from `external_server.conf`
|
||||||
|
4. **Backup Location**: Should be on different mount point than application for safety
|
||||||
|
|
||||||
|
## Maintenance
|
||||||
|
|
||||||
|
### Disk Space
|
||||||
|
Monitor backup directory size:
|
||||||
|
```bash
|
||||||
|
du -sh /srv/quality_app/backups
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Cleanup
|
||||||
|
Remove old backups manually:
|
||||||
|
```bash
|
||||||
|
find /srv/quality_app/backups -name "*.sql" -mtime +30 -delete
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backup Verification
|
||||||
|
Test restore in development environment:
|
||||||
|
```bash
|
||||||
|
mysql -u root -p trasabilitate < backup_trasabilitate_20251103_020000.sql
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Backup Fails
|
||||||
|
- Check database credentials in `external_server.conf`
|
||||||
|
- Ensure `mysqldump` is installed
|
||||||
|
- Verify write permissions on backup directory
|
||||||
|
- Check disk space availability
|
||||||
|
|
||||||
|
### Scheduled Backups Not Running
|
||||||
|
- TODO: Implement scheduled backup daemon/cron job
|
||||||
|
- Check backup schedule is enabled
|
||||||
|
- Verify time format is correct (HH:MM)
|
||||||
|
|
||||||
|
### Cannot Download Backup
|
||||||
|
- Check backup file exists
|
||||||
|
- Verify file permissions
|
||||||
|
- Ensure adequate network bandwidth
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
|
||||||
|
### Planned Features (Task 4)
|
||||||
|
- [ ] Implement APScheduler for automated scheduled backups
|
||||||
|
- [ ] Add backup to external storage (S3, FTP, etc.)
|
||||||
|
- [ ] Email notifications for backup success/failure
|
||||||
|
- [ ] Backup compression (gzip)
|
||||||
|
- [ ] Incremental backups
|
||||||
|
- [ ] Backup encryption
|
||||||
|
- [ ] Backup verification tool
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues or questions about the backup system:
|
||||||
|
1. Check application logs: `/srv/quality_app/logs/error.log`
|
||||||
|
2. Verify backup directory permissions
|
||||||
|
3. Test manual backup first before relying on scheduled backups
|
||||||
|
4. Keep at least 2 recent backups before deleting old ones
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Created**: November 3, 2025
|
||||||
|
**Module**: Database Backup Management
|
||||||
|
**Version**: 1.0.0
|
||||||
148
documentation/README.md
Normal file
148
documentation/README.md
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
# Quality Recticel Application - Documentation
|
||||||
|
|
||||||
|
This folder contains all development and deployment documentation for the Quality Recticel application.
|
||||||
|
|
||||||
|
## Documentation Index
|
||||||
|
|
||||||
|
### Setup & Deployment
|
||||||
|
|
||||||
|
- **[DATABASE_DOCKER_SETUP.md](./DATABASE_DOCKER_SETUP.md)** - Complete guide for database configuration and Docker setup
|
||||||
|
- **[DOCKER_IMPROVEMENTS.md](./DOCKER_IMPROVEMENTS.md)** - Detailed changelog of Docker-related improvements and optimizations
|
||||||
|
- **[DOCKER_QUICK_REFERENCE.md](./DOCKER_QUICK_REFERENCE.md)** - Quick reference guide for common Docker commands and operations
|
||||||
|
|
||||||
|
### Features & Systems
|
||||||
|
|
||||||
|
- **[BACKUP_SYSTEM.md](./BACKUP_SYSTEM.md)** - Database backup management system documentation
|
||||||
|
- Manual and scheduled backups
|
||||||
|
- Backup configuration and management
|
||||||
|
- Restore procedures
|
||||||
|
|
||||||
|
## Quick Links
|
||||||
|
|
||||||
|
### Application Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
quality_app/
|
||||||
|
├── py_app/ # Python application code
|
||||||
|
│ ├── app/ # Flask application modules
|
||||||
|
│ │ ├── __init__.py # App factory
|
||||||
|
│ │ ├── routes.py # Main routes
|
||||||
|
│ │ ├── daily_mirror.py # Daily Mirror module
|
||||||
|
│ │ ├── database_backup.py # Backup system
|
||||||
|
│ │ ├── templates/ # HTML templates
|
||||||
|
│ │ └── static/ # CSS, JS, images
|
||||||
|
│ ├── instance/ # Configuration files
|
||||||
|
│ └── requirements.txt # Python dependencies
|
||||||
|
├── backups/ # Database backups
|
||||||
|
├── logs/ # Application logs
|
||||||
|
├── documentation/ # This folder
|
||||||
|
└── docker-compose.yml # Docker configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Configuration Files
|
||||||
|
|
||||||
|
- `py_app/instance/external_server.conf` - Database connection settings
|
||||||
|
- `docker-compose.yml` - Docker services configuration
|
||||||
|
- `.env` - Environment variables (create from .env.example)
|
||||||
|
- `py_app/gunicorn.conf.py` - Gunicorn WSGI server settings
|
||||||
|
|
||||||
|
### Access Levels
|
||||||
|
|
||||||
|
The application uses a 4-tier role system:
|
||||||
|
|
||||||
|
1. **Superadmin** (Level 100) - Full system access
|
||||||
|
2. **Admin** (Level 90) - Administrative access
|
||||||
|
3. **Manager** (Level 70) - Module management
|
||||||
|
4. **Worker** (Level 50) - Basic operations
|
||||||
|
|
||||||
|
### Modules
|
||||||
|
|
||||||
|
- **Quality** - Production scanning and quality reports
|
||||||
|
- **Warehouse** - Warehouse management
|
||||||
|
- **Labels** - Label printing and management
|
||||||
|
- **Daily Mirror** - Business intelligence and reporting
|
||||||
|
|
||||||
|
## Development Notes
|
||||||
|
|
||||||
|
### Recent Changes (November 2025)
|
||||||
|
|
||||||
|
1. **SQLAlchemy Removal** - Simplified to direct MariaDB connections
|
||||||
|
2. **Daily Mirror Module** - Fully integrated with access control
|
||||||
|
3. **Backup System** - Complete database backup management
|
||||||
|
4. **Access Control** - Superadmin gets automatic full access
|
||||||
|
5. **Docker Optimization** - Production-ready configuration
|
||||||
|
|
||||||
|
### Common Tasks
|
||||||
|
|
||||||
|
**Start Application:**
|
||||||
|
```bash
|
||||||
|
cd /srv/quality_app/py_app
|
||||||
|
bash start_production.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Stop Application:**
|
||||||
|
```bash
|
||||||
|
cd /srv/quality_app/py_app
|
||||||
|
bash stop_production.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**View Logs:**
|
||||||
|
```bash
|
||||||
|
tail -f /srv/quality_app/logs/error.log
|
||||||
|
tail -f /srv/quality_app/logs/access.log
|
||||||
|
```
|
||||||
|
|
||||||
|
**Create Backup:**
|
||||||
|
- Login as superadmin/admin
|
||||||
|
- Go to Settings page
|
||||||
|
- Click "Backup Now" button
|
||||||
|
|
||||||
|
**Check Application Status:**
|
||||||
|
```bash
|
||||||
|
ps aux | grep gunicorn | grep trasabilitate
|
||||||
|
```
|
||||||
|
|
||||||
|
## Support & Maintenance
|
||||||
|
|
||||||
|
### Log Locations
|
||||||
|
|
||||||
|
- **Access Log**: `/srv/quality_app/logs/access.log`
|
||||||
|
- **Error Log**: `/srv/quality_app/logs/error.log`
|
||||||
|
- **Backup Location**: `/srv/quality_app/backups/`
|
||||||
|
|
||||||
|
### Database
|
||||||
|
|
||||||
|
- **Host**: localhost (or as configured)
|
||||||
|
- **Port**: 3306
|
||||||
|
- **Database**: trasabilitate
|
||||||
|
- **User**: trasabilitate
|
||||||
|
|
||||||
|
### Default Login
|
||||||
|
|
||||||
|
- **Username**: superadmin
|
||||||
|
- **Password**: superadmin123
|
||||||
|
|
||||||
|
⚠️ **Change default credentials in production!**
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
When adding new documentation:
|
||||||
|
|
||||||
|
1. Place markdown files in this folder
|
||||||
|
2. Update this README with links
|
||||||
|
3. Use clear, descriptive filenames
|
||||||
|
4. Include date and version when applicable
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
|
||||||
|
- **v1.0.0** (November 2025) - Initial production release
|
||||||
|
- Docker deployment ready
|
||||||
|
- Backup system implemented
|
||||||
|
- Daily Mirror module integrated
|
||||||
|
- SQLAlchemy removed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated**: November 3, 2025
|
||||||
|
**Application**: Quality Recticel Traceability System
|
||||||
|
**Technology Stack**: Flask, MariaDB, Gunicorn, Docker
|
||||||
@@ -433,3 +433,72 @@
|
|||||||
192.168.0.132 - - [22/Oct/2025:11:22:49 +0300] "GET / HTTP/1.1" 200 1627 "https://quality.moto-adv.com/settings" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 2032
|
192.168.0.132 - - [22/Oct/2025:11:22:49 +0300] "GET / HTTP/1.1" 200 1627 "https://quality.moto-adv.com/settings" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36" 2032
|
||||||
192.168.0.132 - - [22/Oct/2025:11:22:50 +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" 1773
|
192.168.0.132 - - [22/Oct/2025:11:22:50 +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" 1773
|
||||||
192.168.0.132 - - [22/Oct/2025:18:45:12 +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" 1959
|
192.168.0.132 - - [22/Oct/2025:18:45:12 +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" 1959
|
||||||
|
127.0.0.1 - - [03/Nov/2025:20:06:54 +0200] "GET / HTTP/1.1" 200 1688 "-" "curl/8.14.1" 63649 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:09:42 +0200] "GET /user_management_simple HTTP/1.1" 200 45867 "https://quality.moto-adv.com/settings" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 113475 µs
|
||||||
|
127.0.0.1 - - [03/Nov/2025:20:16:12 +0200] "GET / HTTP/1.1" 200 1688 "-" "curl/8.14.1" 64029 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:16:45 +0200] "GET /user_management_simple HTTP/1.1" 200 46876 "https://quality.moto-adv.com/settings" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 62091 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:17:23 +0200] "POST /edit_user_simple HTTP/1.1" 302 233 "https://quality.moto-adv.com/user_management_simple" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 31144 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:17:24 +0200] "GET /user_management_simple HTTP/1.1" 200 47317 "https://quality.moto-adv.com/user_management_simple" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 96688 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:17:44 +0200] "POST /edit_user_simple HTTP/1.1" 302 233 "https://quality.moto-adv.com/user_management_simple" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 9053 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:17:44 +0200] "GET /user_management_simple HTTP/1.1" 200 47635 "https://quality.moto-adv.com/user_management_simple" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 6762 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:18:17 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/user_management_simple" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 12037 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:18:20 +0200] "GET /warehouse HTTP/1.1" 200 2987 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 65997 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:18:29 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/warehouse" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 13728 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:18:31 +0200] "GET /etichete HTTP/1.1" 200 3204 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 9236 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:18:31 +0200] "GET /static/css/print_module.css HTTP/1.1" 200 0 "https://quality.moto-adv.com/etichete" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 13002 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:18:33 +0200] "GET /upload_data HTTP/1.1" 200 11226 "https://quality.moto-adv.com/etichete" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 21495 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:18:43 +0200] "GET /logout HTTP/1.1" 302 189 "https://quality.moto-adv.com/upload_data" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 2430 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:18:43 +0200] "GET / HTTP/1.1" 200 1688 "https://quality.moto-adv.com/upload_data" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 8582 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:18:49 +0200] "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/142.0.0.0 Safari/537.36" 6791 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:18:49 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 2657 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:18:54 +0200] "GET /daily_mirror/main 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/142.0.0.0 Safari/537.36" 7222 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:18:54 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 2610 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:18:59 +0200] "GET /daily_mirror/main 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/142.0.0.0 Safari/537.36" 29065 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:18:59 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 51296 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:19:45 +0200] "GET /daily_mirror/main 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/142.0.0.0 Safari/537.36" 8535 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:19:45 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 12117 µs
|
||||||
|
127.0.0.1 - - [03/Nov/2025:20:21:14 +0200] "GET /daily_mirror/ HTTP/1.1" 302 189 "-" "curl/8.14.1" 2105 µs
|
||||||
|
127.0.0.1 - - [03/Nov/2025:20:24:47 +0200] "GET /daily_mirror/ HTTP/1.1" 302 189 "-" "curl/8.14.1" 20248 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:24:57 +0200] "GET /daily_mirror/main 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/142.0.0.0 Safari/537.36" 29323 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:24:57 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 51985 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:25:00 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 2846 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:25:02 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 2657 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:25:03 +0200] "GET /daily_mirror/main 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/142.0.0.0 Safari/537.36" 7317 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:25:03 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 70160 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:25:10 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 3335 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:26:57 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 69436 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:26:58 +0200] "GET /daily_mirror/main 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/142.0.0.0 Safari/537.36" 11113 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:26:58 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 2802 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:27:00 +0200] "GET /daily_mirror/main 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/142.0.0.0 Safari/537.36" 29247 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:27:00 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 50880 µs
|
||||||
|
127.0.0.1 - - [03/Nov/2025:20:28:14 +0200] "HEAD / HTTP/1.1" 200 0 "-" "curl/8.14.1" 63789 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:30:34 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 69748 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:30:36 +0200] "GET /daily_mirror/main 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/142.0.0.0 Safari/537.36" 20964 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:30:36 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 51986 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:30:39 +0200] "GET /settings HTTP/1.1" 200 10631 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 31292 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:31:04 +0200] "GET /user_management_simple HTTP/1.1" 200 47635 "https://quality.moto-adv.com/settings" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 117040 µs
|
||||||
|
127.0.0.1 - - [03/Nov/2025:20:42:01 +0200] "HEAD / HTTP/1.1" 200 0 "-" "curl/8.14.1" 63181 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:42:16 +0200] "GET /settings HTTP/1.1" 200 10631 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 87361 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:42:22 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/settings" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 12878 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:42:23 +0200] "GET /daily_mirror/main 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/142.0.0.0 Safari/537.36" 8459 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:42:24 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 2708 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:42:25 +0200] "GET /daily_mirror/main 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/142.0.0.0 Safari/537.36" 7038 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:42:25 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 2642 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:42:30 +0200] "GET /logout 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/142.0.0.0 Safari/537.36" 2612 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:42:30 +0200] "GET / HTTP/1.1" 200 1688 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 8570 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:42:35 +0200] "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/142.0.0.0 Safari/537.36" 7549 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:42:35 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 2668 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:42:37 +0200] "GET /daily_mirror/main 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/142.0.0.0 Safari/537.36" 6891 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:42:37 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 2666 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:45:16 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 2631 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:45:18 +0200] "GET /daily_mirror/main HTTP/1.1" 200 10549 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 18688 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:45:21 +0200] "GET /daily_mirror/build_database HTTP/1.1" 200 31802 "https://quality.moto-adv.com/daily_mirror/main" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 56457 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:45:21 +0200] "GET /static/daily_mirror_tune.css HTTP/1.1" 404 207 "https://quality.moto-adv.com/daily_mirror/build_database" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 5332 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:45:30 +0200] "GET /daily_mirror/main HTTP/1.1" 200 10549 "https://quality.moto-adv.com/daily_mirror/main" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 77877 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:45:32 +0200] "GET /daily_mirror/main HTTP/1.1" 200 10549 "https://quality.moto-adv.com/daily_mirror/main" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 6436 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:45:35 +0200] "GET /dashboard HTTP/1.1" 200 3827 "https://quality.moto-adv.com/daily_mirror/main" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 2671 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:20:46:50 +0200] "GET /settings HTTP/1.1" 200 10631 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 87865 µs
|
||||||
|
127.0.0.1 - - [03/Nov/2025:21:08:25 +0200] "GET /settings HTTP/1.1" 302 189 "-" "curl/8.14.1" 19562 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:21:08:33 +0200] "GET /settings HTTP/1.1" 200 19546 "https://quality.moto-adv.com/dashboard" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 94529 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:21:08:33 +0200] "GET /api/backup/list HTTP/1.1" 200 30 "https://quality.moto-adv.com/settings" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 21173 µs
|
||||||
|
192.168.0.132 - - [03/Nov/2025:21:08:33 +0200] "GET /api/backup/schedule HTTP/1.1" 200 101 "https://quality.moto-adv.com/settings" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36" 23522 µs
|
||||||
|
|||||||
770
logs/error.log
770
logs/error.log
@@ -125,3 +125,773 @@
|
|||||||
[2025-10-22 20:52:12 +0300] [299445] [INFO] Worker exiting (pid: 299445)
|
[2025-10-22 20:52:12 +0300] [299445] [INFO] Worker exiting (pid: 299445)
|
||||||
[2025-10-22 20:52:12 +0300] [299443] [INFO] Worker exiting (pid: 299443)
|
[2025-10-22 20:52:12 +0300] [299443] [INFO] Worker exiting (pid: 299443)
|
||||||
[2025-10-22 20:52:13 +0300] [299414] [INFO] Shutting down: Master
|
[2025-10-22 20:52:13 +0300] [299414] [INFO] Shutting down: Master
|
||||||
|
[2025-11-03 20:05:59 +0200] [395583] [INFO] Starting gunicorn 23.0.0
|
||||||
|
[2025-11-03 20:05:59 +0200] [395583] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:05:59 +0200] [395583] [INFO] 🚀 Trasabilitate Application - Starting Server
|
||||||
|
[2025-11-03 20:05:59 +0200] [395583] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:05:59 +0200] [395583] [INFO] 📍 Configuration:
|
||||||
|
[2025-11-03 20:05:59 +0200] [395583] [INFO] • Workers: 9
|
||||||
|
[2025-11-03 20:05:59 +0200] [395583] [INFO] • Worker Class: sync
|
||||||
|
[2025-11-03 20:05:59 +0200] [395583] [INFO] • Timeout: 120s
|
||||||
|
[2025-11-03 20:05:59 +0200] [395583] [INFO] • Bind: 0.0.0.0:8781
|
||||||
|
[2025-11-03 20:05:59 +0200] [395583] [INFO] • Preload App: True
|
||||||
|
[2025-11-03 20:05:59 +0200] [395583] [INFO] • Max Requests: 1000 (+/- 100)
|
||||||
|
[2025-11-03 20:05:59 +0200] [395583] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:05:59 +0200] [395583] [INFO] Listening at: http://0.0.0.0:8781 (395583)
|
||||||
|
[2025-11-03 20:05:59 +0200] [395583] [INFO] Using worker: sync
|
||||||
|
[2025-11-03 20:05:59 +0200] [395583] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:05:59 +0200] [395583] [INFO] ✅ Trasabilitate Application Server is READY!
|
||||||
|
[2025-11-03 20:05:59 +0200] [395583] [INFO] 📡 Listening on: [('0.0.0.0', 8781)]
|
||||||
|
[2025-11-03 20:05:59 +0200] [395583] [INFO] 🌐 Access the application at: http://0.0.0.0:8781
|
||||||
|
[2025-11-03 20:05:59 +0200] [395583] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:05:59 +0200] [395583] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:05:59 +0200] [395608] [INFO] Booting worker with pid: 395608
|
||||||
|
[2025-11-03 20:05:59 +0200] [395608] [INFO] ✨ Worker spawned successfully (pid: 395608)
|
||||||
|
[2025-11-03 20:05:59 +0200] [395583] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:05:59 +0200] [395609] [INFO] Booting worker with pid: 395609
|
||||||
|
[2025-11-03 20:05:59 +0200] [395609] [INFO] ✨ Worker spawned successfully (pid: 395609)
|
||||||
|
[2025-11-03 20:05:59 +0200] [395583] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:05:59 +0200] [395610] [INFO] Booting worker with pid: 395610
|
||||||
|
[2025-11-03 20:05:59 +0200] [395610] [INFO] ✨ Worker spawned successfully (pid: 395610)
|
||||||
|
[2025-11-03 20:05:59 +0200] [395583] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:05:59 +0200] [395611] [INFO] Booting worker with pid: 395611
|
||||||
|
[2025-11-03 20:05:59 +0200] [395611] [INFO] ✨ Worker spawned successfully (pid: 395611)
|
||||||
|
[2025-11-03 20:05:59 +0200] [395583] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:05:59 +0200] [395612] [INFO] Booting worker with pid: 395612
|
||||||
|
[2025-11-03 20:05:59 +0200] [395612] [INFO] ✨ Worker spawned successfully (pid: 395612)
|
||||||
|
[2025-11-03 20:05:59 +0200] [395583] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:05:59 +0200] [395613] [INFO] Booting worker with pid: 395613
|
||||||
|
[2025-11-03 20:05:59 +0200] [395613] [INFO] ✨ Worker spawned successfully (pid: 395613)
|
||||||
|
[2025-11-03 20:06:00 +0200] [395583] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:06:00 +0200] [395614] [INFO] Booting worker with pid: 395614
|
||||||
|
[2025-11-03 20:06:00 +0200] [395614] [INFO] ✨ Worker spawned successfully (pid: 395614)
|
||||||
|
[2025-11-03 20:06:00 +0200] [395583] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:06:00 +0200] [395615] [INFO] Booting worker with pid: 395615
|
||||||
|
[2025-11-03 20:06:00 +0200] [395615] [INFO] ✨ Worker spawned successfully (pid: 395615)
|
||||||
|
[2025-11-03 20:06:00 +0200] [395583] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:06:00 +0200] [395616] [INFO] Booting worker with pid: 395616
|
||||||
|
[2025-11-03 20:06:00 +0200] [395616] [INFO] ✨ Worker spawned successfully (pid: 395616)
|
||||||
|
[2025-11-03 20:16:00 +0200] [395610] [INFO] Worker exiting (pid: 395610)
|
||||||
|
[2025-11-03 20:16:00 +0200] [395609] [INFO] Worker exiting (pid: 395609)
|
||||||
|
[2025-11-03 20:16:00 +0200] [395608] [INFO] Worker exiting (pid: 395608)
|
||||||
|
[2025-11-03 20:16:00 +0200] [395583] [INFO] Handling signal: term
|
||||||
|
[2025-11-03 20:16:00 +0200] [395613] [INFO] Worker exiting (pid: 395613)
|
||||||
|
[2025-11-03 20:16:00 +0200] [395611] [INFO] Worker exiting (pid: 395611)
|
||||||
|
[2025-11-03 20:16:00 +0200] [395614] [INFO] Worker exiting (pid: 395614)
|
||||||
|
[2025-11-03 20:16:00 +0200] [395612] [INFO] Worker exiting (pid: 395612)
|
||||||
|
[2025-11-03 20:16:00 +0200] [395615] [INFO] Worker exiting (pid: 395615)
|
||||||
|
[2025-11-03 20:16:00 +0200] [395616] [INFO] Worker exiting (pid: 395616)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/arbiter.py", line 223, in run
|
||||||
|
handler()
|
||||||
|
~~~~~~~^^
|
||||||
|
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/arbiter.py", line 256, in handle_term
|
||||||
|
raise StopIteration
|
||||||
|
StopIteration
|
||||||
|
|
||||||
|
During handling of the above exception, another exception occurred:
|
||||||
|
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "/srv/quality_recticel/recticel/bin/gunicorn", line 8, in <module>
|
||||||
|
sys.exit(run())
|
||||||
|
~~~^^
|
||||||
|
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/app/wsgiapp.py", line 66, in run
|
||||||
|
WSGIApplication("%(prog)s [OPTIONS] [APP_MODULE]", prog=prog).run()
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
|
||||||
|
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/app/base.py", line 235, in run
|
||||||
|
super().run()
|
||||||
|
~~~~~~~~~~~^^
|
||||||
|
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/app/base.py", line 71, in run
|
||||||
|
Arbiter(self).run()
|
||||||
|
~~~~~~~~~~~~~~~~~^^
|
||||||
|
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/arbiter.py", line 226, in run
|
||||||
|
self.halt()
|
||||||
|
~~~~~~~~~^^
|
||||||
|
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/arbiter.py", line 341, in halt
|
||||||
|
self.stop()
|
||||||
|
~~~~~~~~~^^
|
||||||
|
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/arbiter.py", line 395, in stop
|
||||||
|
time.sleep(0.1)
|
||||||
|
~~~~~~~~~~^^^^^
|
||||||
|
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/arbiter.py", line 241, in handle_chld
|
||||||
|
self.reap_workers()
|
||||||
|
~~~~~~~~~~~~~~~~~^^
|
||||||
|
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/arbiter.py", line 559, in reap_workers
|
||||||
|
self.cfg.child_exit(self, worker)
|
||||||
|
~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^
|
||||||
|
File "/srv/quality_app/py_app/gunicorn.conf.py", line 167, in child_exit
|
||||||
|
server.log.info("👋 Worker %s exited", worker.pid)
|
||||||
|
|
||||||
|
AttributeError: 'WorkerTmp' object has no attribute 'last_mtime'
|
||||||
|
[2025-11-03 20:16:05 +0200] [395971] [INFO] Starting gunicorn 23.0.0
|
||||||
|
[2025-11-03 20:16:05 +0200] [395971] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:16:05 +0200] [395971] [INFO] 🚀 Trasabilitate Application - Starting Server
|
||||||
|
[2025-11-03 20:16:05 +0200] [395971] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:16:05 +0200] [395971] [INFO] 📍 Configuration:
|
||||||
|
[2025-11-03 20:16:05 +0200] [395971] [INFO] • Workers: 9
|
||||||
|
[2025-11-03 20:16:05 +0200] [395971] [INFO] • Worker Class: sync
|
||||||
|
[2025-11-03 20:16:05 +0200] [395971] [INFO] • Timeout: 120s
|
||||||
|
[2025-11-03 20:16:05 +0200] [395971] [INFO] • Bind: 0.0.0.0:8781
|
||||||
|
[2025-11-03 20:16:05 +0200] [395971] [INFO] • Preload App: True
|
||||||
|
[2025-11-03 20:16:05 +0200] [395971] [INFO] • Max Requests: 1000 (+/- 100)
|
||||||
|
[2025-11-03 20:16:05 +0200] [395971] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:16:05 +0200] [395971] [INFO] Listening at: http://0.0.0.0:8781 (395971)
|
||||||
|
[2025-11-03 20:16:05 +0200] [395971] [INFO] Using worker: sync
|
||||||
|
[2025-11-03 20:16:05 +0200] [395971] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:16:05 +0200] [395971] [INFO] ✅ Trasabilitate Application Server is READY!
|
||||||
|
[2025-11-03 20:16:05 +0200] [395971] [INFO] 📡 Listening on: [('0.0.0.0', 8781)]
|
||||||
|
[2025-11-03 20:16:05 +0200] [395971] [INFO] 🌐 Access the application at: http://0.0.0.0:8781
|
||||||
|
[2025-11-03 20:16:05 +0200] [395971] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:16:05 +0200] [395971] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:16:05 +0200] [395976] [INFO] Booting worker with pid: 395976
|
||||||
|
[2025-11-03 20:16:05 +0200] [395976] [INFO] ✨ Worker spawned successfully (pid: 395976)
|
||||||
|
[2025-11-03 20:16:05 +0200] [395971] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:16:05 +0200] [395977] [INFO] Booting worker with pid: 395977
|
||||||
|
[2025-11-03 20:16:05 +0200] [395977] [INFO] ✨ Worker spawned successfully (pid: 395977)
|
||||||
|
[2025-11-03 20:16:05 +0200] [395971] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:16:05 +0200] [395978] [INFO] Booting worker with pid: 395978
|
||||||
|
[2025-11-03 20:16:05 +0200] [395978] [INFO] ✨ Worker spawned successfully (pid: 395978)
|
||||||
|
[2025-11-03 20:16:05 +0200] [395971] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:16:05 +0200] [395979] [INFO] Booting worker with pid: 395979
|
||||||
|
[2025-11-03 20:16:05 +0200] [395979] [INFO] ✨ Worker spawned successfully (pid: 395979)
|
||||||
|
[2025-11-03 20:16:05 +0200] [395971] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:16:05 +0200] [395980] [INFO] Booting worker with pid: 395980
|
||||||
|
[2025-11-03 20:16:05 +0200] [395980] [INFO] ✨ Worker spawned successfully (pid: 395980)
|
||||||
|
[2025-11-03 20:16:05 +0200] [395971] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:16:05 +0200] [395981] [INFO] Booting worker with pid: 395981
|
||||||
|
[2025-11-03 20:16:05 +0200] [395981] [INFO] ✨ Worker spawned successfully (pid: 395981)
|
||||||
|
[2025-11-03 20:16:06 +0200] [395971] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:16:06 +0200] [395982] [INFO] Booting worker with pid: 395982
|
||||||
|
[2025-11-03 20:16:06 +0200] [395982] [INFO] ✨ Worker spawned successfully (pid: 395982)
|
||||||
|
[2025-11-03 20:16:06 +0200] [395971] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:16:06 +0200] [395983] [INFO] Booting worker with pid: 395983
|
||||||
|
[2025-11-03 20:16:06 +0200] [395983] [INFO] ✨ Worker spawned successfully (pid: 395983)
|
||||||
|
[2025-11-03 20:16:06 +0200] [395971] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:16:06 +0200] [395984] [INFO] Booting worker with pid: 395984
|
||||||
|
[2025-11-03 20:16:06 +0200] [395984] [INFO] ✨ Worker spawned successfully (pid: 395984)
|
||||||
|
Session user: superadmin superadmin
|
||||||
|
Session user: superadmin superadmin
|
||||||
|
All form data received: {'username': 'superadmin', 'password': 'Vanessa_13/05'}
|
||||||
|
Raw form input: 'superadmin' 'Vanessa_13/05'
|
||||||
|
External DB query result (with modules): ('superadmin', 'Vanessa_13/05', 'superadmin', None)
|
||||||
|
Logged in as: superadmin superadmin modules: ['quality', 'warehouse', 'labels']
|
||||||
|
Session user: superadmin superadmin
|
||||||
|
Error loading Daily Mirror main page: daily_mirror_main.html
|
||||||
|
Session user: superadmin superadmin
|
||||||
|
Error loading Daily Mirror main page: daily_mirror_main.html
|
||||||
|
Session user: superadmin superadmin
|
||||||
|
Error loading Daily Mirror main page: daily_mirror_main.html
|
||||||
|
Session user: superadmin superadmin
|
||||||
|
[2025-11-03 20:23:46 +0200] [395971] [INFO] Handling signal: term
|
||||||
|
[2025-11-03 20:23:46 +0200] [395977] [INFO] Worker exiting (pid: 395977)
|
||||||
|
[2025-11-03 20:23:46 +0200] [395976] [INFO] Worker exiting (pid: 395976)
|
||||||
|
[2025-11-03 20:23:46 +0200] [395978] [INFO] Worker exiting (pid: 395978)
|
||||||
|
[2025-11-03 20:23:46 +0200] [395979] [INFO] Worker exiting (pid: 395979)
|
||||||
|
[2025-11-03 20:23:46 +0200] [395980] [INFO] Worker exiting (pid: 395980)
|
||||||
|
[2025-11-03 20:23:46 +0200] [395981] [INFO] Worker exiting (pid: 395981)
|
||||||
|
[2025-11-03 20:23:46 +0200] [395982] [INFO] Worker exiting (pid: 395982)
|
||||||
|
[2025-11-03 20:23:46 +0200] [395983] [INFO] Worker exiting (pid: 395983)
|
||||||
|
[2025-11-03 20:23:46 +0200] [395984] [INFO] Worker exiting (pid: 395984)
|
||||||
|
[2025-11-03 20:23:47 +0200] [395971] [INFO] 👋 Worker 395980 exited
|
||||||
|
[2025-11-03 20:23:47 +0200] [395971] [INFO] 👋 Worker 395977 exited
|
||||||
|
[2025-11-03 20:23:47 +0200] [395971] [INFO] 👋 Worker 395976 exited
|
||||||
|
[2025-11-03 20:23:47 +0200] [395971] [INFO] 👋 Worker 395981 exited
|
||||||
|
[2025-11-03 20:23:47 +0200] [395971] [INFO] 👋 Worker 395982 exited
|
||||||
|
[2025-11-03 20:23:48 +0200] [395971] [INFO] 👋 Worker 395979 exited
|
||||||
|
[2025-11-03 20:23:48 +0200] [395971] [INFO] 👋 Worker 395984 exited
|
||||||
|
[2025-11-03 20:23:48 +0200] [395971] [INFO] 👋 Worker 395978 exited
|
||||||
|
[2025-11-03 20:23:48 +0200] [395971] [INFO] 👋 Worker 395983 exited
|
||||||
|
[2025-11-03 20:23:48 +0200] [395971] [INFO] Shutting down: Master
|
||||||
|
[2025-11-03 20:23:48 +0200] [395971] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:23:48 +0200] [395971] [INFO] 👋 Trasabilitate Application - Shutting Down
|
||||||
|
[2025-11-03 20:23:48 +0200] [395971] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:23:54 +0200] [396278] [INFO] Starting gunicorn 23.0.0
|
||||||
|
[2025-11-03 20:23:54 +0200] [396278] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:23:54 +0200] [396278] [INFO] 🚀 Trasabilitate Application - Starting Server
|
||||||
|
[2025-11-03 20:23:54 +0200] [396278] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:23:54 +0200] [396278] [INFO] 📍 Configuration:
|
||||||
|
[2025-11-03 20:23:54 +0200] [396278] [INFO] • Workers: 9
|
||||||
|
[2025-11-03 20:23:54 +0200] [396278] [INFO] • Worker Class: sync
|
||||||
|
[2025-11-03 20:23:54 +0200] [396278] [INFO] • Timeout: 120s
|
||||||
|
[2025-11-03 20:23:54 +0200] [396278] [INFO] • Bind: 0.0.0.0:8781
|
||||||
|
[2025-11-03 20:23:54 +0200] [396278] [INFO] • Preload App: True
|
||||||
|
[2025-11-03 20:23:54 +0200] [396278] [INFO] • Max Requests: 1000 (+/- 100)
|
||||||
|
[2025-11-03 20:23:54 +0200] [396278] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:23:54 +0200] [396278] [INFO] Listening at: http://0.0.0.0:8781 (396278)
|
||||||
|
[2025-11-03 20:23:54 +0200] [396278] [INFO] Using worker: sync
|
||||||
|
[2025-11-03 20:23:54 +0200] [396278] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:23:54 +0200] [396278] [INFO] ✅ Trasabilitate Application Server is READY!
|
||||||
|
[2025-11-03 20:23:54 +0200] [396278] [INFO] 📡 Listening on: [('0.0.0.0', 8781)]
|
||||||
|
[2025-11-03 20:23:54 +0200] [396278] [INFO] 🌐 Access the application at: http://0.0.0.0:8781
|
||||||
|
[2025-11-03 20:23:54 +0200] [396278] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:23:54 +0200] [396278] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:23:54 +0200] [396278] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:23:54 +0200] [396305] [INFO] Booting worker with pid: 396305
|
||||||
|
[2025-11-03 20:23:54 +0200] [396305] [INFO] ✨ Worker spawned successfully (pid: 396305)
|
||||||
|
[2025-11-03 20:23:54 +0200] [396306] [INFO] Booting worker with pid: 396306
|
||||||
|
[2025-11-03 20:23:54 +0200] [396306] [INFO] ✨ Worker spawned successfully (pid: 396306)
|
||||||
|
[2025-11-03 20:23:54 +0200] [396278] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:23:54 +0200] [396307] [INFO] Booting worker with pid: 396307
|
||||||
|
[2025-11-03 20:23:54 +0200] [396278] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:23:54 +0200] [396307] [INFO] ✨ Worker spawned successfully (pid: 396307)
|
||||||
|
[2025-11-03 20:23:54 +0200] [396308] [INFO] Booting worker with pid: 396308
|
||||||
|
[2025-11-03 20:23:54 +0200] [396308] [INFO] ✨ Worker spawned successfully (pid: 396308)
|
||||||
|
[2025-11-03 20:23:55 +0200] [396278] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:23:55 +0200] [396309] [INFO] Booting worker with pid: 396309
|
||||||
|
[2025-11-03 20:23:55 +0200] [396309] [INFO] ✨ Worker spawned successfully (pid: 396309)
|
||||||
|
[2025-11-03 20:23:55 +0200] [396278] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:23:55 +0200] [396310] [INFO] Booting worker with pid: 396310
|
||||||
|
[2025-11-03 20:23:55 +0200] [396310] [INFO] ✨ Worker spawned successfully (pid: 396310)
|
||||||
|
[2025-11-03 20:23:55 +0200] [396278] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:23:55 +0200] [396311] [INFO] Booting worker with pid: 396311
|
||||||
|
[2025-11-03 20:23:55 +0200] [396311] [INFO] ✨ Worker spawned successfully (pid: 396311)
|
||||||
|
[2025-11-03 20:23:55 +0200] [396278] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:23:55 +0200] [396312] [INFO] Booting worker with pid: 396312
|
||||||
|
[2025-11-03 20:23:55 +0200] [396312] [INFO] ✨ Worker spawned successfully (pid: 396312)
|
||||||
|
[2025-11-03 20:23:55 +0200] [396278] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:23:55 +0200] [396313] [INFO] Booting worker with pid: 396313
|
||||||
|
[2025-11-03 20:23:55 +0200] [396313] [INFO] ✨ Worker spawned successfully (pid: 396313)
|
||||||
|
Error loading Daily Mirror main page: daily_mirror_main.html
|
||||||
|
Session user: superadmin superadmin
|
||||||
|
Session user: superadmin superadmin
|
||||||
|
Session user: superadmin superadmin
|
||||||
|
Error loading Daily Mirror main page: daily_mirror_main.html
|
||||||
|
Session user: superadmin superadmin
|
||||||
|
Session user: superadmin superadmin
|
||||||
|
[2025-11-03 20:26:06 +0200] [396278] [INFO] Handling signal: term
|
||||||
|
[2025-11-03 20:26:06 +0200] [396307] [INFO] Worker exiting (pid: 396307)
|
||||||
|
[2025-11-03 20:26:06 +0200] [396305] [INFO] Worker exiting (pid: 396305)
|
||||||
|
[2025-11-03 20:26:06 +0200] [396306] [INFO] Worker exiting (pid: 396306)
|
||||||
|
[2025-11-03 20:26:06 +0200] [396308] [INFO] Worker exiting (pid: 396308)
|
||||||
|
[2025-11-03 20:26:06 +0200] [396309] [INFO] Worker exiting (pid: 396309)
|
||||||
|
[2025-11-03 20:26:06 +0200] [396310] [INFO] Worker exiting (pid: 396310)
|
||||||
|
[2025-11-03 20:26:06 +0200] [396311] [INFO] Worker exiting (pid: 396311)
|
||||||
|
[2025-11-03 20:26:06 +0200] [396312] [INFO] Worker exiting (pid: 396312)
|
||||||
|
[2025-11-03 20:26:06 +0200] [396313] [INFO] Worker exiting (pid: 396313)
|
||||||
|
[2025-11-03 20:26:06 +0200] [396278] [INFO] 👋 Worker 396305 exited
|
||||||
|
[2025-11-03 20:26:06 +0200] [396278] [INFO] 👋 Worker 396306 exited
|
||||||
|
[2025-11-03 20:26:06 +0200] [396278] [INFO] 👋 Worker 396308 exited
|
||||||
|
[2025-11-03 20:26:07 +0200] [396278] [INFO] 👋 Worker 396312 exited
|
||||||
|
[2025-11-03 20:26:07 +0200] [396278] [INFO] 👋 Worker 396309 exited
|
||||||
|
[2025-11-03 20:26:07 +0200] [396278] [INFO] 👋 Worker 396307 exited
|
||||||
|
[2025-11-03 20:26:07 +0200] [396278] [INFO] 👋 Worker 396310 exited
|
||||||
|
[2025-11-03 20:26:07 +0200] [396278] [INFO] 👋 Worker 396311 exited
|
||||||
|
[2025-11-03 20:26:07 +0200] [396278] [INFO] 👋 Worker 396313 exited
|
||||||
|
[2025-11-03 20:26:07 +0200] [396278] [INFO] Shutting down: Master
|
||||||
|
[2025-11-03 20:26:07 +0200] [396278] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:26:07 +0200] [396278] [INFO] 👋 Trasabilitate Application - Shutting Down
|
||||||
|
[2025-11-03 20:26:07 +0200] [396278] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:26:14 +0200] [396699] [INFO] Starting gunicorn 23.0.0
|
||||||
|
[2025-11-03 20:26:14 +0200] [396699] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:26:14 +0200] [396699] [INFO] 🚀 Trasabilitate Application - Starting Server
|
||||||
|
[2025-11-03 20:26:14 +0200] [396699] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:26:14 +0200] [396699] [INFO] 📍 Configuration:
|
||||||
|
[2025-11-03 20:26:14 +0200] [396699] [INFO] • Workers: 9
|
||||||
|
[2025-11-03 20:26:14 +0200] [396699] [INFO] • Worker Class: sync
|
||||||
|
[2025-11-03 20:26:14 +0200] [396699] [INFO] • Timeout: 120s
|
||||||
|
[2025-11-03 20:26:14 +0200] [396699] [INFO] • Bind: 0.0.0.0:8781
|
||||||
|
[2025-11-03 20:26:14 +0200] [396699] [INFO] • Preload App: True
|
||||||
|
[2025-11-03 20:26:14 +0200] [396699] [INFO] • Max Requests: 1000 (+/- 100)
|
||||||
|
[2025-11-03 20:26:14 +0200] [396699] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:26:14 +0200] [396699] [INFO] Listening at: http://0.0.0.0:8781 (396699)
|
||||||
|
[2025-11-03 20:26:14 +0200] [396699] [INFO] Using worker: sync
|
||||||
|
[2025-11-03 20:26:14 +0200] [396699] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:26:14 +0200] [396699] [INFO] ✅ Trasabilitate Application Server is READY!
|
||||||
|
[2025-11-03 20:26:14 +0200] [396699] [INFO] 📡 Listening on: [('0.0.0.0', 8781)]
|
||||||
|
[2025-11-03 20:26:14 +0200] [396699] [INFO] 🌐 Access the application at: http://0.0.0.0:8781
|
||||||
|
[2025-11-03 20:26:14 +0200] [396699] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:26:14 +0200] [396699] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:26:14 +0200] [396708] [INFO] Booting worker with pid: 396708
|
||||||
|
[2025-11-03 20:26:14 +0200] [396708] [INFO] ✨ Worker spawned successfully (pid: 396708)
|
||||||
|
[2025-11-03 20:26:14 +0200] [396699] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:26:14 +0200] [396709] [INFO] Booting worker with pid: 396709
|
||||||
|
[2025-11-03 20:26:14 +0200] [396709] [INFO] ✨ Worker spawned successfully (pid: 396709)
|
||||||
|
[2025-11-03 20:26:14 +0200] [396699] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:26:14 +0200] [396710] [INFO] Booting worker with pid: 396710
|
||||||
|
[2025-11-03 20:26:14 +0200] [396710] [INFO] ✨ Worker spawned successfully (pid: 396710)
|
||||||
|
[2025-11-03 20:26:14 +0200] [396699] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:26:14 +0200] [396711] [INFO] Booting worker with pid: 396711
|
||||||
|
[2025-11-03 20:26:14 +0200] [396711] [INFO] ✨ Worker spawned successfully (pid: 396711)
|
||||||
|
[2025-11-03 20:26:14 +0200] [396699] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:26:14 +0200] [396712] [INFO] Booting worker with pid: 396712
|
||||||
|
[2025-11-03 20:26:14 +0200] [396712] [INFO] ✨ Worker spawned successfully (pid: 396712)
|
||||||
|
[2025-11-03 20:26:14 +0200] [396699] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:26:14 +0200] [396713] [INFO] Booting worker with pid: 396713
|
||||||
|
[2025-11-03 20:26:14 +0200] [396713] [INFO] ✨ Worker spawned successfully (pid: 396713)
|
||||||
|
[2025-11-03 20:26:14 +0200] [396699] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:26:14 +0200] [396714] [INFO] Booting worker with pid: 396714
|
||||||
|
[2025-11-03 20:26:14 +0200] [396714] [INFO] ✨ Worker spawned successfully (pid: 396714)
|
||||||
|
[2025-11-03 20:26:14 +0200] [396699] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:26:14 +0200] [396719] [INFO] Booting worker with pid: 396719
|
||||||
|
[2025-11-03 20:26:14 +0200] [396719] [INFO] ✨ Worker spawned successfully (pid: 396719)
|
||||||
|
[2025-11-03 20:26:14 +0200] [396699] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:26:14 +0200] [396725] [INFO] Booting worker with pid: 396725
|
||||||
|
[2025-11-03 20:26:14 +0200] [396725] [INFO] ✨ Worker spawned successfully (pid: 396725)
|
||||||
|
Session user: superadmin superadmin
|
||||||
|
Error loading Daily Mirror main page: daily_mirror_main.html
|
||||||
|
Session user: superadmin superadmin
|
||||||
|
Error loading Daily Mirror main page: daily_mirror_main.html
|
||||||
|
Session user: superadmin superadmin
|
||||||
|
[2025-11-03 20:29:44 +0200] [396699] [INFO] Handling signal: term
|
||||||
|
[2025-11-03 20:29:44 +0200] [396710] [INFO] Worker exiting (pid: 396710)
|
||||||
|
[2025-11-03 20:29:44 +0200] [396709] [INFO] Worker exiting (pid: 396709)
|
||||||
|
[2025-11-03 20:29:44 +0200] [396708] [INFO] Worker exiting (pid: 396708)
|
||||||
|
[2025-11-03 20:29:44 +0200] [396711] [INFO] Worker exiting (pid: 396711)
|
||||||
|
[2025-11-03 20:29:44 +0200] [396712] [INFO] Worker exiting (pid: 396712)
|
||||||
|
[2025-11-03 20:29:44 +0200] [396713] [INFO] Worker exiting (pid: 396713)
|
||||||
|
[2025-11-03 20:29:44 +0200] [396714] [INFO] Worker exiting (pid: 396714)
|
||||||
|
[2025-11-03 20:29:44 +0200] [396719] [INFO] Worker exiting (pid: 396719)
|
||||||
|
[2025-11-03 20:29:44 +0200] [396725] [INFO] Worker exiting (pid: 396725)
|
||||||
|
[2025-11-03 20:29:45 +0200] [396699] [INFO] 👋 Worker 396709 exited
|
||||||
|
[2025-11-03 20:29:45 +0200] [396699] [INFO] 👋 Worker 396708 exited
|
||||||
|
[2025-11-03 20:29:45 +0200] [396699] [INFO] 👋 Worker 396711 exited
|
||||||
|
[2025-11-03 20:29:45 +0200] [396699] [INFO] 👋 Worker 396713 exited
|
||||||
|
[2025-11-03 20:29:45 +0200] [396699] [INFO] 👋 Worker 396714 exited
|
||||||
|
[2025-11-03 20:29:45 +0200] [396699] [INFO] 👋 Worker 396719 exited
|
||||||
|
[2025-11-03 20:29:46 +0200] [396699] [INFO] 👋 Worker 396710 exited
|
||||||
|
[2025-11-03 20:29:46 +0200] [396699] [INFO] 👋 Worker 396725 exited
|
||||||
|
[2025-11-03 20:29:46 +0200] [396699] [INFO] 👋 Worker 396712 exited
|
||||||
|
[2025-11-03 20:29:46 +0200] [396699] [INFO] Shutting down: Master
|
||||||
|
[2025-11-03 20:29:46 +0200] [396699] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:29:46 +0200] [396699] [INFO] 👋 Trasabilitate Application - Shutting Down
|
||||||
|
[2025-11-03 20:29:46 +0200] [396699] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:29:52 +0200] [397053] [INFO] Starting gunicorn 23.0.0
|
||||||
|
[2025-11-03 20:29:52 +0200] [397053] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:29:52 +0200] [397053] [INFO] 🚀 Trasabilitate Application - Starting Server
|
||||||
|
[2025-11-03 20:29:52 +0200] [397053] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:29:52 +0200] [397053] [INFO] 📍 Configuration:
|
||||||
|
[2025-11-03 20:29:52 +0200] [397053] [INFO] • Workers: 9
|
||||||
|
[2025-11-03 20:29:52 +0200] [397053] [INFO] • Worker Class: sync
|
||||||
|
[2025-11-03 20:29:52 +0200] [397053] [INFO] • Timeout: 120s
|
||||||
|
[2025-11-03 20:29:52 +0200] [397053] [INFO] • Bind: 0.0.0.0:8781
|
||||||
|
[2025-11-03 20:29:52 +0200] [397053] [INFO] • Preload App: True
|
||||||
|
[2025-11-03 20:29:52 +0200] [397053] [INFO] • Max Requests: 1000 (+/- 100)
|
||||||
|
[2025-11-03 20:29:52 +0200] [397053] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:29:52 +0200] [397053] [INFO] Listening at: http://0.0.0.0:8781 (397053)
|
||||||
|
[2025-11-03 20:29:52 +0200] [397053] [INFO] Using worker: sync
|
||||||
|
[2025-11-03 20:29:52 +0200] [397053] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:29:52 +0200] [397053] [INFO] ✅ Trasabilitate Application Server is READY!
|
||||||
|
[2025-11-03 20:29:52 +0200] [397053] [INFO] 📡 Listening on: [('0.0.0.0', 8781)]
|
||||||
|
[2025-11-03 20:29:52 +0200] [397053] [INFO] 🌐 Access the application at: http://0.0.0.0:8781
|
||||||
|
[2025-11-03 20:29:52 +0200] [397053] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:29:52 +0200] [397053] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:29:52 +0200] [397076] [INFO] Booting worker with pid: 397076
|
||||||
|
[2025-11-03 20:29:52 +0200] [397076] [INFO] ✨ Worker spawned successfully (pid: 397076)
|
||||||
|
[2025-11-03 20:29:52 +0200] [397053] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:29:52 +0200] [397077] [INFO] Booting worker with pid: 397077
|
||||||
|
[2025-11-03 20:29:52 +0200] [397077] [INFO] ✨ Worker spawned successfully (pid: 397077)
|
||||||
|
[2025-11-03 20:29:52 +0200] [397053] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:29:52 +0200] [397078] [INFO] Booting worker with pid: 397078
|
||||||
|
[2025-11-03 20:29:52 +0200] [397078] [INFO] ✨ Worker spawned successfully (pid: 397078)
|
||||||
|
[2025-11-03 20:29:52 +0200] [397053] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:29:52 +0200] [397079] [INFO] Booting worker with pid: 397079
|
||||||
|
[2025-11-03 20:29:52 +0200] [397079] [INFO] ✨ Worker spawned successfully (pid: 397079)
|
||||||
|
[2025-11-03 20:29:52 +0200] [397053] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:29:52 +0200] [397080] [INFO] Booting worker with pid: 397080
|
||||||
|
[2025-11-03 20:29:52 +0200] [397080] [INFO] ✨ Worker spawned successfully (pid: 397080)
|
||||||
|
[2025-11-03 20:29:53 +0200] [397053] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:29:53 +0200] [397081] [INFO] Booting worker with pid: 397081
|
||||||
|
[2025-11-03 20:29:53 +0200] [397081] [INFO] ✨ Worker spawned successfully (pid: 397081)
|
||||||
|
[2025-11-03 20:29:53 +0200] [397053] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:29:53 +0200] [397082] [INFO] Booting worker with pid: 397082
|
||||||
|
[2025-11-03 20:29:53 +0200] [397082] [INFO] ✨ Worker spawned successfully (pid: 397082)
|
||||||
|
[2025-11-03 20:29:53 +0200] [397053] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:29:53 +0200] [397083] [INFO] Booting worker with pid: 397083
|
||||||
|
[2025-11-03 20:29:53 +0200] [397083] [INFO] ✨ Worker spawned successfully (pid: 397083)
|
||||||
|
[2025-11-03 20:29:53 +0200] [397053] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:29:53 +0200] [397084] [INFO] Booting worker with pid: 397084
|
||||||
|
[2025-11-03 20:29:53 +0200] [397084] [INFO] ✨ Worker spawned successfully (pid: 397084)
|
||||||
|
Session user: superadmin superadmin
|
||||||
|
Session user: superadmin superadmin
|
||||||
|
[2025-11-03 20:35:12 +0200] [397053] [INFO] Handling signal: term
|
||||||
|
[2025-11-03 20:35:12 +0200] [397078] [INFO] Worker exiting (pid: 397078)
|
||||||
|
[2025-11-03 20:35:12 +0200] [397076] [INFO] Worker exiting (pid: 397076)
|
||||||
|
[2025-11-03 20:35:12 +0200] [397079] [INFO] Worker exiting (pid: 397079)
|
||||||
|
[2025-11-03 20:35:12 +0200] [397081] [INFO] Worker exiting (pid: 397081)
|
||||||
|
[2025-11-03 20:35:12 +0200] [397077] [INFO] Worker exiting (pid: 397077)
|
||||||
|
[2025-11-03 20:35:12 +0200] [397082] [INFO] Worker exiting (pid: 397082)
|
||||||
|
[2025-11-03 20:35:12 +0200] [397083] [INFO] Worker exiting (pid: 397083)
|
||||||
|
[2025-11-03 20:35:12 +0200] [397080] [INFO] Worker exiting (pid: 397080)
|
||||||
|
[2025-11-03 20:35:12 +0200] [397084] [INFO] Worker exiting (pid: 397084)
|
||||||
|
[2025-11-03 20:35:13 +0200] [397053] [INFO] 👋 Worker 397081 exited
|
||||||
|
[2025-11-03 20:35:13 +0200] [397053] [INFO] 👋 Worker 397079 exited
|
||||||
|
[2025-11-03 20:35:14 +0200] [397053] [INFO] 👋 Worker 397076 exited
|
||||||
|
[2025-11-03 20:35:14 +0200] [397053] [INFO] 👋 Worker 397083 exited
|
||||||
|
[2025-11-03 20:35:14 +0200] [397053] [INFO] 👋 Worker 397084 exited
|
||||||
|
[2025-11-03 20:35:14 +0200] [397053] [INFO] 👋 Worker 397077 exited
|
||||||
|
[2025-11-03 20:35:14 +0200] [397053] [INFO] 👋 Worker 397078 exited
|
||||||
|
[2025-11-03 20:35:14 +0200] [397053] [INFO] 👋 Worker 397082 exited
|
||||||
|
[2025-11-03 20:35:14 +0200] [397053] [INFO] 👋 Worker 397080 exited
|
||||||
|
[2025-11-03 20:35:14 +0200] [397053] [INFO] Shutting down: Master
|
||||||
|
[2025-11-03 20:35:14 +0200] [397053] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:35:14 +0200] [397053] [INFO] 👋 Trasabilitate Application - Shutting Down
|
||||||
|
[2025-11-03 20:35:14 +0200] [397053] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:35:21 +0200] [397553] [INFO] Starting gunicorn 23.0.0
|
||||||
|
[2025-11-03 20:35:21 +0200] [397553] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:35:21 +0200] [397553] [INFO] 🚀 Trasabilitate Application - Starting Server
|
||||||
|
[2025-11-03 20:35:21 +0200] [397553] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:35:21 +0200] [397553] [INFO] 📍 Configuration:
|
||||||
|
[2025-11-03 20:35:21 +0200] [397553] [INFO] • Workers: 9
|
||||||
|
[2025-11-03 20:35:21 +0200] [397553] [INFO] • Worker Class: sync
|
||||||
|
[2025-11-03 20:35:21 +0200] [397553] [INFO] • Timeout: 120s
|
||||||
|
[2025-11-03 20:35:21 +0200] [397553] [INFO] • Bind: 0.0.0.0:8781
|
||||||
|
[2025-11-03 20:35:21 +0200] [397553] [INFO] • Preload App: True
|
||||||
|
[2025-11-03 20:35:21 +0200] [397553] [INFO] • Max Requests: 1000 (+/- 100)
|
||||||
|
[2025-11-03 20:35:21 +0200] [397553] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:35:21 +0200] [397553] [INFO] Listening at: http://0.0.0.0:8781 (397553)
|
||||||
|
[2025-11-03 20:35:21 +0200] [397553] [INFO] Using worker: sync
|
||||||
|
[2025-11-03 20:35:21 +0200] [397553] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:35:21 +0200] [397553] [INFO] ✅ Trasabilitate Application Server is READY!
|
||||||
|
[2025-11-03 20:35:21 +0200] [397553] [INFO] 📡 Listening on: [('0.0.0.0', 8781)]
|
||||||
|
[2025-11-03 20:35:21 +0200] [397553] [INFO] 🌐 Access the application at: http://0.0.0.0:8781
|
||||||
|
[2025-11-03 20:35:21 +0200] [397553] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:35:21 +0200] [397553] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:35:21 +0200] [397580] [INFO] Booting worker with pid: 397580
|
||||||
|
[2025-11-03 20:35:21 +0200] [397580] [INFO] ✨ Worker spawned successfully (pid: 397580)
|
||||||
|
[2025-11-03 20:35:21 +0200] [397553] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:35:21 +0200] [397581] [INFO] Booting worker with pid: 397581
|
||||||
|
[2025-11-03 20:35:21 +0200] [397581] [INFO] ✨ Worker spawned successfully (pid: 397581)
|
||||||
|
[2025-11-03 20:35:21 +0200] [397553] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:35:21 +0200] [397582] [INFO] Booting worker with pid: 397582
|
||||||
|
[2025-11-03 20:35:21 +0200] [397582] [INFO] ✨ Worker spawned successfully (pid: 397582)
|
||||||
|
[2025-11-03 20:35:21 +0200] [397553] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:35:21 +0200] [397583] [INFO] Booting worker with pid: 397583
|
||||||
|
[2025-11-03 20:35:21 +0200] [397583] [INFO] ✨ Worker spawned successfully (pid: 397583)
|
||||||
|
[2025-11-03 20:35:21 +0200] [397553] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:35:21 +0200] [397584] [INFO] Booting worker with pid: 397584
|
||||||
|
[2025-11-03 20:35:21 +0200] [397584] [INFO] ✨ Worker spawned successfully (pid: 397584)
|
||||||
|
[2025-11-03 20:35:21 +0200] [397553] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:35:21 +0200] [397585] [INFO] Booting worker with pid: 397585
|
||||||
|
[2025-11-03 20:35:21 +0200] [397585] [INFO] ✨ Worker spawned successfully (pid: 397585)
|
||||||
|
[2025-11-03 20:35:21 +0200] [397553] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:35:21 +0200] [397586] [INFO] Booting worker with pid: 397586
|
||||||
|
[2025-11-03 20:35:21 +0200] [397586] [INFO] ✨ Worker spawned successfully (pid: 397586)
|
||||||
|
[2025-11-03 20:35:21 +0200] [397553] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:35:21 +0200] [397587] [INFO] Booting worker with pid: 397587
|
||||||
|
[2025-11-03 20:35:21 +0200] [397587] [INFO] ✨ Worker spawned successfully (pid: 397587)
|
||||||
|
[2025-11-03 20:35:21 +0200] [397553] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:35:21 +0200] [397588] [INFO] Booting worker with pid: 397588
|
||||||
|
[2025-11-03 20:35:21 +0200] [397588] [INFO] ✨ Worker spawned successfully (pid: 397588)
|
||||||
|
[2025-11-03 20:39:58 +0200] [397553] [INFO] Handling signal: term
|
||||||
|
[2025-11-03 20:39:58 +0200] [397580] [INFO] Worker exiting (pid: 397580)
|
||||||
|
[2025-11-03 20:39:58 +0200] [397581] [INFO] Worker exiting (pid: 397581)
|
||||||
|
[2025-11-03 20:39:58 +0200] [397584] [INFO] Worker exiting (pid: 397584)
|
||||||
|
[2025-11-03 20:39:58 +0200] [397582] [INFO] Worker exiting (pid: 397582)
|
||||||
|
[2025-11-03 20:39:58 +0200] [397585] [INFO] Worker exiting (pid: 397585)
|
||||||
|
[2025-11-03 20:39:58 +0200] [397587] [INFO] Worker exiting (pid: 397587)
|
||||||
|
[2025-11-03 20:39:58 +0200] [397583] [INFO] Worker exiting (pid: 397583)
|
||||||
|
[2025-11-03 20:39:58 +0200] [397586] [INFO] Worker exiting (pid: 397586)
|
||||||
|
[2025-11-03 20:39:58 +0200] [397588] [INFO] Worker exiting (pid: 397588)
|
||||||
|
[2025-11-03 20:39:59 +0200] [397553] [INFO] 👋 Worker 397581 exited
|
||||||
|
[2025-11-03 20:39:59 +0200] [397553] [INFO] 👋 Worker 397585 exited
|
||||||
|
[2025-11-03 20:39:59 +0200] [397553] [INFO] 👋 Worker 397580 exited
|
||||||
|
[2025-11-03 20:39:59 +0200] [397553] [INFO] 👋 Worker 397588 exited
|
||||||
|
[2025-11-03 20:39:59 +0200] [397553] [INFO] 👋 Worker 397583 exited
|
||||||
|
[2025-11-03 20:39:59 +0200] [397553] [INFO] 👋 Worker 397586 exited
|
||||||
|
[2025-11-03 20:40:00 +0200] [397553] [INFO] 👋 Worker 397582 exited
|
||||||
|
[2025-11-03 20:40:00 +0200] [397553] [INFO] 👋 Worker 397584 exited
|
||||||
|
[2025-11-03 20:40:00 +0200] [397553] [INFO] 👋 Worker 397587 exited
|
||||||
|
[2025-11-03 20:40:00 +0200] [397553] [INFO] Shutting down: Master
|
||||||
|
[2025-11-03 20:40:00 +0200] [397553] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:40:00 +0200] [397553] [INFO] 👋 Trasabilitate Application - Shutting Down
|
||||||
|
[2025-11-03 20:40:00 +0200] [397553] [INFO] ============================================================
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "/srv/quality_recticel/recticel/bin/gunicorn", line 8, in <module>
|
||||||
|
sys.exit(run())
|
||||||
|
~~~^^
|
||||||
|
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/app/wsgiapp.py", line 66, in run
|
||||||
|
WSGIApplication("%(prog)s [OPTIONS] [APP_MODULE]", prog=prog).run()
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
|
||||||
|
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/app/base.py", line 235, in run
|
||||||
|
super().run()
|
||||||
|
~~~~~~~~~~~^^
|
||||||
|
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/app/base.py", line 71, in run
|
||||||
|
Arbiter(self).run()
|
||||||
|
~~~~~~~^^^^^^
|
||||||
|
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/arbiter.py", line 57, in __init__
|
||||||
|
self.setup(app)
|
||||||
|
~~~~~~~~~~^^^^^
|
||||||
|
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/arbiter.py", line 117, in setup
|
||||||
|
self.app.wsgi()
|
||||||
|
~~~~~~~~~~~~~^^
|
||||||
|
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/app/base.py", line 66, in wsgi
|
||||||
|
self.callable = self.load()
|
||||||
|
~~~~~~~~~^^
|
||||||
|
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/app/wsgiapp.py", line 57, in load
|
||||||
|
return self.load_wsgiapp()
|
||||||
|
~~~~~~~~~~~~~~~~~^^
|
||||||
|
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/app/wsgiapp.py", line 47, in load_wsgiapp
|
||||||
|
return util.import_app(self.app_uri)
|
||||||
|
~~~~~~~~~~~~~~~^^^^^^^^^^^^^^
|
||||||
|
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/util.py", line 370, in import_app
|
||||||
|
mod = importlib.import_module(module)
|
||||||
|
File "/usr/lib/python3.13/importlib/__init__.py", line 88, in import_module
|
||||||
|
return _bootstrap._gcd_import(name[level:], package, level)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
|
||||||
|
File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
|
||||||
|
File "<frozen importlib._bootstrap>", line 1331, in _find_and_load_unlocked
|
||||||
|
File "<frozen importlib._bootstrap>", line 935, in _load_unlocked
|
||||||
|
File "<frozen importlib._bootstrap_external>", line 1026, in exec_module
|
||||||
|
File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
|
||||||
|
File "/srv/quality_app/py_app/wsgi.py", line 15, in <module>
|
||||||
|
application = create_app()
|
||||||
|
File "/srv/quality_app/py_app/app/__init__.py", line 11, in create_app
|
||||||
|
from app.routes import bp as main_bp, warehouse_bp
|
||||||
|
File "/srv/quality_app/py_app/app/routes.py", line 6, in <module>
|
||||||
|
from .models import User
|
||||||
|
File "/srv/quality_app/py_app/app/models.py", line 1, in <module>
|
||||||
|
from . import db
|
||||||
|
ImportError: cannot import name 'db' from 'app' (/srv/quality_app/py_app/app/__init__.py)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "/srv/quality_recticel/recticel/bin/gunicorn", line 8, in <module>
|
||||||
|
sys.exit(run())
|
||||||
|
~~~^^
|
||||||
|
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/app/wsgiapp.py", line 66, in run
|
||||||
|
WSGIApplication("%(prog)s [OPTIONS] [APP_MODULE]", prog=prog).run()
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
|
||||||
|
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/app/base.py", line 235, in run
|
||||||
|
super().run()
|
||||||
|
~~~~~~~~~~~^^
|
||||||
|
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/app/base.py", line 71, in run
|
||||||
|
Arbiter(self).run()
|
||||||
|
~~~~~~~^^^^^^
|
||||||
|
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/arbiter.py", line 57, in __init__
|
||||||
|
self.setup(app)
|
||||||
|
~~~~~~~~~~^^^^^
|
||||||
|
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/arbiter.py", line 117, in setup
|
||||||
|
self.app.wsgi()
|
||||||
|
~~~~~~~~~~~~~^^
|
||||||
|
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/app/base.py", line 66, in wsgi
|
||||||
|
self.callable = self.load()
|
||||||
|
~~~~~~~~~^^
|
||||||
|
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/app/wsgiapp.py", line 57, in load
|
||||||
|
return self.load_wsgiapp()
|
||||||
|
~~~~~~~~~~~~~~~~~^^
|
||||||
|
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/app/wsgiapp.py", line 47, in load_wsgiapp
|
||||||
|
return util.import_app(self.app_uri)
|
||||||
|
~~~~~~~~~~~~~~~^^^^^^^^^^^^^^
|
||||||
|
File "/srv/quality_recticel/recticel/lib/python3.13/site-packages/gunicorn/util.py", line 370, in import_app
|
||||||
|
mod = importlib.import_module(module)
|
||||||
|
File "/usr/lib/python3.13/importlib/__init__.py", line 88, in import_module
|
||||||
|
return _bootstrap._gcd_import(name[level:], package, level)
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "<frozen importlib._bootstrap>", line 1387, in _gcd_import
|
||||||
|
File "<frozen importlib._bootstrap>", line 1360, in _find_and_load
|
||||||
|
File "<frozen importlib._bootstrap>", line 1331, in _find_and_load_unlocked
|
||||||
|
File "<frozen importlib._bootstrap>", line 935, in _load_unlocked
|
||||||
|
File "<frozen importlib._bootstrap_external>", line 1026, in exec_module
|
||||||
|
File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
|
||||||
|
File "/srv/quality_app/py_app/wsgi.py", line 15, in <module>
|
||||||
|
application = create_app()
|
||||||
|
File "/srv/quality_app/py_app/app/__init__.py", line 11, in create_app
|
||||||
|
from app.routes import bp as main_bp, warehouse_bp
|
||||||
|
File "/srv/quality_app/py_app/app/routes.py", line 10, in <module>
|
||||||
|
from app.settings import (
|
||||||
|
...<10 lines>...
|
||||||
|
)
|
||||||
|
File "/srv/quality_app/py_app/app/settings.py", line 2, in <module>
|
||||||
|
from .models import User
|
||||||
|
File "/srv/quality_app/py_app/app/models.py", line 1, in <module>
|
||||||
|
from . import db
|
||||||
|
ImportError: cannot import name 'db' from 'app' (/srv/quality_app/py_app/app/__init__.py)
|
||||||
|
[2025-11-03 20:41:50 +0200] [398202] [INFO] Starting gunicorn 23.0.0
|
||||||
|
[2025-11-03 20:41:50 +0200] [398202] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:41:50 +0200] [398202] [INFO] 🚀 Trasabilitate Application - Starting Server
|
||||||
|
[2025-11-03 20:41:50 +0200] [398202] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:41:50 +0200] [398202] [INFO] 📍 Configuration:
|
||||||
|
[2025-11-03 20:41:50 +0200] [398202] [INFO] • Workers: 9
|
||||||
|
[2025-11-03 20:41:50 +0200] [398202] [INFO] • Worker Class: sync
|
||||||
|
[2025-11-03 20:41:50 +0200] [398202] [INFO] • Timeout: 120s
|
||||||
|
[2025-11-03 20:41:50 +0200] [398202] [INFO] • Bind: 0.0.0.0:8781
|
||||||
|
[2025-11-03 20:41:50 +0200] [398202] [INFO] • Preload App: True
|
||||||
|
[2025-11-03 20:41:50 +0200] [398202] [INFO] • Max Requests: 1000 (+/- 100)
|
||||||
|
[2025-11-03 20:41:50 +0200] [398202] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:41:50 +0200] [398202] [INFO] Listening at: http://0.0.0.0:8781 (398202)
|
||||||
|
[2025-11-03 20:41:50 +0200] [398202] [INFO] Using worker: sync
|
||||||
|
[2025-11-03 20:41:50 +0200] [398202] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:41:50 +0200] [398202] [INFO] ✅ Trasabilitate Application Server is READY!
|
||||||
|
[2025-11-03 20:41:50 +0200] [398202] [INFO] 📡 Listening on: [('0.0.0.0', 8781)]
|
||||||
|
[2025-11-03 20:41:50 +0200] [398202] [INFO] 🌐 Access the application at: http://0.0.0.0:8781
|
||||||
|
[2025-11-03 20:41:50 +0200] [398202] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:41:50 +0200] [398202] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:41:50 +0200] [398224] [INFO] Booting worker with pid: 398224
|
||||||
|
[2025-11-03 20:41:50 +0200] [398224] [INFO] ✨ Worker spawned successfully (pid: 398224)
|
||||||
|
[2025-11-03 20:41:50 +0200] [398202] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:41:50 +0200] [398225] [INFO] Booting worker with pid: 398225
|
||||||
|
[2025-11-03 20:41:50 +0200] [398225] [INFO] ✨ Worker spawned successfully (pid: 398225)
|
||||||
|
[2025-11-03 20:41:50 +0200] [398202] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:41:50 +0200] [398226] [INFO] Booting worker with pid: 398226
|
||||||
|
[2025-11-03 20:41:50 +0200] [398226] [INFO] ✨ Worker spawned successfully (pid: 398226)
|
||||||
|
[2025-11-03 20:41:50 +0200] [398202] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:41:50 +0200] [398227] [INFO] Booting worker with pid: 398227
|
||||||
|
[2025-11-03 20:41:50 +0200] [398227] [INFO] ✨ Worker spawned successfully (pid: 398227)
|
||||||
|
[2025-11-03 20:41:51 +0200] [398202] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:41:51 +0200] [398228] [INFO] Booting worker with pid: 398228
|
||||||
|
[2025-11-03 20:41:51 +0200] [398228] [INFO] ✨ Worker spawned successfully (pid: 398228)
|
||||||
|
[2025-11-03 20:41:51 +0200] [398202] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:41:51 +0200] [398235] [INFO] Booting worker with pid: 398235
|
||||||
|
[2025-11-03 20:41:51 +0200] [398235] [INFO] ✨ Worker spawned successfully (pid: 398235)
|
||||||
|
[2025-11-03 20:41:51 +0200] [398202] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:41:51 +0200] [398236] [INFO] Booting worker with pid: 398236
|
||||||
|
[2025-11-03 20:41:51 +0200] [398236] [INFO] ✨ Worker spawned successfully (pid: 398236)
|
||||||
|
[2025-11-03 20:41:51 +0200] [398202] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:41:51 +0200] [398237] [INFO] Booting worker with pid: 398237
|
||||||
|
[2025-11-03 20:41:51 +0200] [398237] [INFO] ✨ Worker spawned successfully (pid: 398237)
|
||||||
|
[2025-11-03 20:41:51 +0200] [398202] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:41:51 +0200] [398238] [INFO] Booting worker with pid: 398238
|
||||||
|
[2025-11-03 20:41:51 +0200] [398238] [INFO] ✨ Worker spawned successfully (pid: 398238)
|
||||||
|
Session user: superadmin superadmin
|
||||||
|
Error loading Daily Mirror main page: daily_mirror_main.html
|
||||||
|
Session user: superadmin superadmin
|
||||||
|
Error loading Daily Mirror main page: daily_mirror_main.html
|
||||||
|
Session user: superadmin superadmin
|
||||||
|
All form data received: {'username': 'superadmin', 'password': 'Vanessa_13/05'}
|
||||||
|
Raw form input: 'superadmin' 'Vanessa_13/05'
|
||||||
|
External DB query result (with modules): ('superadmin', 'Vanessa_13/05', 'superadmin', 'quality,warehouse,labels,daily_mirror')
|
||||||
|
Logged in as: superadmin superadmin modules: ['quality', 'warehouse', 'labels']
|
||||||
|
Session user: superadmin superadmin
|
||||||
|
Error loading Daily Mirror main page: daily_mirror_main.html
|
||||||
|
Session user: superadmin superadmin
|
||||||
|
Session user: superadmin superadmin
|
||||||
|
Session user: superadmin superadmin
|
||||||
|
[2025-11-03 20:45:46 +0200] [398202] [INFO] Handling signal: term
|
||||||
|
[2025-11-03 20:45:46 +0200] [398226] [INFO] Worker exiting (pid: 398226)
|
||||||
|
[2025-11-03 20:45:46 +0200] [398225] [INFO] Worker exiting (pid: 398225)
|
||||||
|
[2025-11-03 20:45:46 +0200] [398224] [INFO] Worker exiting (pid: 398224)
|
||||||
|
[2025-11-03 20:45:46 +0200] [398237] [INFO] Worker exiting (pid: 398237)
|
||||||
|
[2025-11-03 20:45:46 +0200] [398228] [INFO] Worker exiting (pid: 398228)
|
||||||
|
[2025-11-03 20:45:46 +0200] [398236] [INFO] Worker exiting (pid: 398236)
|
||||||
|
[2025-11-03 20:45:46 +0200] [398227] [INFO] Worker exiting (pid: 398227)
|
||||||
|
[2025-11-03 20:45:46 +0200] [398235] [INFO] Worker exiting (pid: 398235)
|
||||||
|
[2025-11-03 20:45:46 +0200] [398238] [INFO] Worker exiting (pid: 398238)
|
||||||
|
[2025-11-03 20:45:46 +0200] [398202] [INFO] 👋 Worker 398237 exited
|
||||||
|
[2025-11-03 20:45:47 +0200] [398202] [INFO] 👋 Worker 398238 exited
|
||||||
|
[2025-11-03 20:45:47 +0200] [398202] [INFO] 👋 Worker 398226 exited
|
||||||
|
[2025-11-03 20:45:47 +0200] [398202] [INFO] 👋 Worker 398235 exited
|
||||||
|
[2025-11-03 20:45:47 +0200] [398202] [INFO] 👋 Worker 398225 exited
|
||||||
|
[2025-11-03 20:45:47 +0200] [398202] [INFO] 👋 Worker 398236 exited
|
||||||
|
[2025-11-03 20:45:47 +0200] [398202] [INFO] 👋 Worker 398228 exited
|
||||||
|
[2025-11-03 20:45:47 +0200] [398202] [INFO] 👋 Worker 398224 exited
|
||||||
|
[2025-11-03 20:45:47 +0200] [398202] [INFO] 👋 Worker 398227 exited
|
||||||
|
[2025-11-03 20:45:47 +0200] [398202] [INFO] Shutting down: Master
|
||||||
|
[2025-11-03 20:45:47 +0200] [398202] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:45:47 +0200] [398202] [INFO] 👋 Trasabilitate Application - Shutting Down
|
||||||
|
[2025-11-03 20:45:47 +0200] [398202] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:45:53 +0200] [398661] [INFO] Starting gunicorn 23.0.0
|
||||||
|
[2025-11-03 20:45:53 +0200] [398661] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:45:53 +0200] [398661] [INFO] 🚀 Trasabilitate Application - Starting Server
|
||||||
|
[2025-11-03 20:45:53 +0200] [398661] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:45:53 +0200] [398661] [INFO] 📍 Configuration:
|
||||||
|
[2025-11-03 20:45:53 +0200] [398661] [INFO] • Workers: 9
|
||||||
|
[2025-11-03 20:45:53 +0200] [398661] [INFO] • Worker Class: sync
|
||||||
|
[2025-11-03 20:45:53 +0200] [398661] [INFO] • Timeout: 120s
|
||||||
|
[2025-11-03 20:45:53 +0200] [398661] [INFO] • Bind: 0.0.0.0:8781
|
||||||
|
[2025-11-03 20:45:53 +0200] [398661] [INFO] • Preload App: True
|
||||||
|
[2025-11-03 20:45:53 +0200] [398661] [INFO] • Max Requests: 1000 (+/- 100)
|
||||||
|
[2025-11-03 20:45:53 +0200] [398661] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:45:53 +0200] [398661] [INFO] Listening at: http://0.0.0.0:8781 (398661)
|
||||||
|
[2025-11-03 20:45:53 +0200] [398661] [INFO] Using worker: sync
|
||||||
|
[2025-11-03 20:45:53 +0200] [398661] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:45:53 +0200] [398661] [INFO] ✅ Trasabilitate Application Server is READY!
|
||||||
|
[2025-11-03 20:45:53 +0200] [398661] [INFO] 📡 Listening on: [('0.0.0.0', 8781)]
|
||||||
|
[2025-11-03 20:45:53 +0200] [398661] [INFO] 🌐 Access the application at: http://0.0.0.0:8781
|
||||||
|
[2025-11-03 20:45:53 +0200] [398661] [INFO] ============================================================
|
||||||
|
[2025-11-03 20:45:53 +0200] [398661] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:45:53 +0200] [398683] [INFO] Booting worker with pid: 398683
|
||||||
|
[2025-11-03 20:45:53 +0200] [398683] [INFO] ✨ Worker spawned successfully (pid: 398683)
|
||||||
|
[2025-11-03 20:45:53 +0200] [398661] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:45:53 +0200] [398684] [INFO] Booting worker with pid: 398684
|
||||||
|
[2025-11-03 20:45:53 +0200] [398684] [INFO] ✨ Worker spawned successfully (pid: 398684)
|
||||||
|
[2025-11-03 20:45:54 +0200] [398661] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:45:54 +0200] [398685] [INFO] Booting worker with pid: 398685
|
||||||
|
[2025-11-03 20:45:54 +0200] [398685] [INFO] ✨ Worker spawned successfully (pid: 398685)
|
||||||
|
[2025-11-03 20:45:54 +0200] [398661] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:45:54 +0200] [398686] [INFO] Booting worker with pid: 398686
|
||||||
|
[2025-11-03 20:45:54 +0200] [398686] [INFO] ✨ Worker spawned successfully (pid: 398686)
|
||||||
|
[2025-11-03 20:45:54 +0200] [398661] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:45:54 +0200] [398687] [INFO] Booting worker with pid: 398687
|
||||||
|
[2025-11-03 20:45:54 +0200] [398687] [INFO] ✨ Worker spawned successfully (pid: 398687)
|
||||||
|
[2025-11-03 20:45:54 +0200] [398661] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:45:54 +0200] [398690] [INFO] Booting worker with pid: 398690
|
||||||
|
[2025-11-03 20:45:54 +0200] [398690] [INFO] ✨ Worker spawned successfully (pid: 398690)
|
||||||
|
[2025-11-03 20:45:54 +0200] [398661] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:45:54 +0200] [398695] [INFO] Booting worker with pid: 398695
|
||||||
|
[2025-11-03 20:45:54 +0200] [398695] [INFO] ✨ Worker spawned successfully (pid: 398695)
|
||||||
|
[2025-11-03 20:45:54 +0200] [398661] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:45:54 +0200] [398696] [INFO] Booting worker with pid: 398696
|
||||||
|
[2025-11-03 20:45:54 +0200] [398696] [INFO] ✨ Worker spawned successfully (pid: 398696)
|
||||||
|
[2025-11-03 20:45:54 +0200] [398661] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 20:45:54 +0200] [398697] [INFO] Booting worker with pid: 398697
|
||||||
|
[2025-11-03 20:45:54 +0200] [398697] [INFO] ✨ Worker spawned successfully (pid: 398697)
|
||||||
|
[2025-11-03 21:06:13 +0200] [398661] [INFO] Handling signal: term
|
||||||
|
[2025-11-03 21:06:13 +0200] [398683] [INFO] Worker exiting (pid: 398683)
|
||||||
|
[2025-11-03 21:06:13 +0200] [398684] [INFO] Worker exiting (pid: 398684)
|
||||||
|
[2025-11-03 21:06:13 +0200] [398685] [INFO] Worker exiting (pid: 398685)
|
||||||
|
[2025-11-03 21:06:13 +0200] [398686] [INFO] Worker exiting (pid: 398686)
|
||||||
|
[2025-11-03 21:06:13 +0200] [398687] [INFO] Worker exiting (pid: 398687)
|
||||||
|
[2025-11-03 21:06:13 +0200] [398695] [INFO] Worker exiting (pid: 398695)
|
||||||
|
[2025-11-03 21:06:13 +0200] [398690] [INFO] Worker exiting (pid: 398690)
|
||||||
|
[2025-11-03 21:06:13 +0200] [398696] [INFO] Worker exiting (pid: 398696)
|
||||||
|
[2025-11-03 21:06:13 +0200] [398697] [INFO] Worker exiting (pid: 398697)
|
||||||
|
[2025-11-03 21:06:13 +0200] [398661] [INFO] 👋 Worker 398683 exited
|
||||||
|
[2025-11-03 21:06:13 +0200] [398661] [INFO] 👋 Worker 398686 exited
|
||||||
|
[2025-11-03 21:06:13 +0200] [398661] [INFO] 👋 Worker 398697 exited
|
||||||
|
[2025-11-03 21:06:14 +0200] [398661] [INFO] 👋 Worker 398685 exited
|
||||||
|
[2025-11-03 21:06:14 +0200] [398661] [INFO] 👋 Worker 398695 exited
|
||||||
|
[2025-11-03 21:06:14 +0200] [398661] [INFO] 👋 Worker 398684 exited
|
||||||
|
[2025-11-03 21:06:14 +0200] [398661] [INFO] 👋 Worker 398690 exited
|
||||||
|
[2025-11-03 21:06:14 +0200] [398661] [INFO] 👋 Worker 398696 exited
|
||||||
|
[2025-11-03 21:06:14 +0200] [398661] [INFO] 👋 Worker 398687 exited
|
||||||
|
[2025-11-03 21:06:14 +0200] [398661] [INFO] Shutting down: Master
|
||||||
|
[2025-11-03 21:06:14 +0200] [398661] [INFO] ============================================================
|
||||||
|
[2025-11-03 21:06:14 +0200] [398661] [INFO] 👋 Trasabilitate Application - Shutting Down
|
||||||
|
[2025-11-03 21:06:14 +0200] [398661] [INFO] ============================================================
|
||||||
|
[2025-11-03 21:06:20 +0200] [399048] [INFO] Starting gunicorn 23.0.0
|
||||||
|
[2025-11-03 21:06:20 +0200] [399048] [INFO] ============================================================
|
||||||
|
[2025-11-03 21:06:20 +0200] [399048] [INFO] 🚀 Trasabilitate Application - Starting Server
|
||||||
|
[2025-11-03 21:06:20 +0200] [399048] [INFO] ============================================================
|
||||||
|
[2025-11-03 21:06:20 +0200] [399048] [INFO] 📍 Configuration:
|
||||||
|
[2025-11-03 21:06:20 +0200] [399048] [INFO] • Workers: 9
|
||||||
|
[2025-11-03 21:06:20 +0200] [399048] [INFO] • Worker Class: sync
|
||||||
|
[2025-11-03 21:06:20 +0200] [399048] [INFO] • Timeout: 120s
|
||||||
|
[2025-11-03 21:06:20 +0200] [399048] [INFO] • Bind: 0.0.0.0:8781
|
||||||
|
[2025-11-03 21:06:20 +0200] [399048] [INFO] • Preload App: True
|
||||||
|
[2025-11-03 21:06:20 +0200] [399048] [INFO] • Max Requests: 1000 (+/- 100)
|
||||||
|
[2025-11-03 21:06:20 +0200] [399048] [INFO] ============================================================
|
||||||
|
[2025-11-03 21:06:20 +0200] [399048] [INFO] Listening at: http://0.0.0.0:8781 (399048)
|
||||||
|
[2025-11-03 21:06:20 +0200] [399048] [INFO] Using worker: sync
|
||||||
|
[2025-11-03 21:06:20 +0200] [399048] [INFO] ============================================================
|
||||||
|
[2025-11-03 21:06:20 +0200] [399048] [INFO] ✅ Trasabilitate Application Server is READY!
|
||||||
|
[2025-11-03 21:06:20 +0200] [399048] [INFO] 📡 Listening on: [('0.0.0.0', 8781)]
|
||||||
|
[2025-11-03 21:06:20 +0200] [399048] [INFO] 🌐 Access the application at: http://0.0.0.0:8781
|
||||||
|
[2025-11-03 21:06:20 +0200] [399048] [INFO] ============================================================
|
||||||
|
[2025-11-03 21:06:20 +0200] [399048] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 21:06:20 +0200] [399070] [INFO] Booting worker with pid: 399070
|
||||||
|
[2025-11-03 21:06:20 +0200] [399070] [INFO] ✨ Worker spawned successfully (pid: 399070)
|
||||||
|
[2025-11-03 21:06:20 +0200] [399048] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 21:06:20 +0200] [399071] [INFO] Booting worker with pid: 399071
|
||||||
|
[2025-11-03 21:06:20 +0200] [399071] [INFO] ✨ Worker spawned successfully (pid: 399071)
|
||||||
|
[2025-11-03 21:06:20 +0200] [399048] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 21:06:20 +0200] [399072] [INFO] Booting worker with pid: 399072
|
||||||
|
[2025-11-03 21:06:20 +0200] [399072] [INFO] ✨ Worker spawned successfully (pid: 399072)
|
||||||
|
[2025-11-03 21:06:20 +0200] [399048] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 21:06:20 +0200] [399073] [INFO] Booting worker with pid: 399073
|
||||||
|
[2025-11-03 21:06:20 +0200] [399073] [INFO] ✨ Worker spawned successfully (pid: 399073)
|
||||||
|
[2025-11-03 21:06:20 +0200] [399048] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 21:06:20 +0200] [399076] [INFO] Booting worker with pid: 399076
|
||||||
|
[2025-11-03 21:06:20 +0200] [399076] [INFO] ✨ Worker spawned successfully (pid: 399076)
|
||||||
|
[2025-11-03 21:06:20 +0200] [399048] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 21:06:20 +0200] [399080] [INFO] Booting worker with pid: 399080
|
||||||
|
[2025-11-03 21:06:20 +0200] [399080] [INFO] ✨ Worker spawned successfully (pid: 399080)
|
||||||
|
[2025-11-03 21:06:20 +0200] [399048] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 21:06:21 +0200] [399082] [INFO] Booting worker with pid: 399082
|
||||||
|
[2025-11-03 21:06:21 +0200] [399082] [INFO] ✨ Worker spawned successfully (pid: 399082)
|
||||||
|
[2025-11-03 21:06:21 +0200] [399048] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 21:06:21 +0200] [399083] [INFO] Booting worker with pid: 399083
|
||||||
|
[2025-11-03 21:06:21 +0200] [399083] [INFO] ✨ Worker spawned successfully (pid: 399083)
|
||||||
|
[2025-11-03 21:06:21 +0200] [399048] [INFO] 🔄 Forking new worker (pid: [booting])
|
||||||
|
[2025-11-03 21:06:21 +0200] [399084] [INFO] Booting worker with pid: 399084
|
||||||
|
[2025-11-03 21:06:21 +0200] [399084] [INFO] ✨ Worker spawned successfully (pid: 399084)
|
||||||
|
Backup directory ensured: /srv/quality_app/backups
|
||||||
|
Backup directory ensured: /srv/quality_app/backups
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
from flask import Flask
|
from flask import Flask
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
db = SQLAlchemy()
|
|
||||||
|
|
||||||
def create_app():
|
def create_app():
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config['SECRET_KEY'] = 'your_secret_key'
|
app.config['SECRET_KEY'] = 'your_secret_key'
|
||||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db'
|
|
||||||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
|
||||||
|
|
||||||
db.init_app(app)
|
# Application uses direct MariaDB connections via external_server.conf
|
||||||
|
# No SQLAlchemy ORM needed - all database operations use raw SQL
|
||||||
|
|
||||||
from app.routes import bp as main_bp, warehouse_bp
|
from app.routes import bp as main_bp, warehouse_bp
|
||||||
from app.daily_mirror import daily_mirror_bp
|
from app.daily_mirror import daily_mirror_bp
|
||||||
@@ -21,7 +17,4 @@ def create_app():
|
|||||||
# Add 'now' function to Jinja2 globals
|
# Add 'now' function to Jinja2 globals
|
||||||
app.jinja_env.globals['now'] = datetime.now
|
app.jinja_env.globals['now'] = datetime.now
|
||||||
|
|
||||||
with app.app_context():
|
|
||||||
db.create_all() # Create database tables if they don't exist
|
|
||||||
|
|
||||||
return app
|
return app
|
||||||
@@ -41,8 +41,8 @@ def requires_role(min_role_level=None, required_modules=None, page=None):
|
|||||||
|
|
||||||
# Module requirement checking
|
# Module requirement checking
|
||||||
if required_modules:
|
if required_modules:
|
||||||
if user_role in ['superadmin', 'admin']:
|
if user_role == 'superadmin':
|
||||||
# Superadmin and admin have access to all modules
|
# Superadmin has access to all modules
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
if not any(module in user_modules for module in required_modules):
|
if not any(module in user_modules for module in required_modules):
|
||||||
@@ -77,6 +77,10 @@ def requires_labels_module(f):
|
|||||||
"""Decorator for labels module access"""
|
"""Decorator for labels module access"""
|
||||||
return requires_role(required_modules=['labels'])(f)
|
return requires_role(required_modules=['labels'])(f)
|
||||||
|
|
||||||
|
def requires_daily_mirror_module(f):
|
||||||
|
"""Decorator for daily mirror module access"""
|
||||||
|
return requires_role(required_modules=['daily_mirror'])(f)
|
||||||
|
|
||||||
def quality_manager_plus(f):
|
def quality_manager_plus(f):
|
||||||
"""Decorator for quality module manager+ access"""
|
"""Decorator for quality module manager+ access"""
|
||||||
return requires_role(min_role_level=70, required_modules=['quality'])(f)
|
return requires_role(min_role_level=70, required_modules=['quality'])(f)
|
||||||
@@ -88,3 +92,7 @@ def warehouse_manager_plus(f):
|
|||||||
def labels_manager_plus(f):
|
def labels_manager_plus(f):
|
||||||
"""Decorator for labels module manager+ access"""
|
"""Decorator for labels module manager+ access"""
|
||||||
return requires_role(min_role_level=70, required_modules=['labels'])(f)
|
return requires_role(min_role_level=70, required_modules=['labels'])(f)
|
||||||
|
|
||||||
|
def daily_mirror_manager_plus(f):
|
||||||
|
"""Decorator for daily mirror module manager+ access"""
|
||||||
|
return requires_role(min_role_level=70, required_modules=['daily_mirror'])(f)
|
||||||
@@ -26,10 +26,15 @@ def check_daily_mirror_access():
|
|||||||
flash('Please log in to access this page.')
|
flash('Please log in to access this page.')
|
||||||
return redirect(url_for('main.login'))
|
return redirect(url_for('main.login'))
|
||||||
|
|
||||||
# Check if user has admin+ access
|
# Superadmin has access to everything
|
||||||
user_role = session.get('role', '')
|
user_role = session.get('role', '')
|
||||||
if user_role not in ['superadmin', 'admin']:
|
if user_role == 'superadmin':
|
||||||
flash('Access denied: Admin privileges required for Daily Mirror.')
|
return None # Access granted
|
||||||
|
|
||||||
|
# Check if user has daily_mirror module access
|
||||||
|
user_modules = session.get('modules', [])
|
||||||
|
if 'daily_mirror' not in user_modules:
|
||||||
|
flash('Access denied: Daily Mirror module access required.')
|
||||||
return redirect(url_for('main.dashboard'))
|
return redirect(url_for('main.dashboard'))
|
||||||
|
|
||||||
return None # Access granted
|
return None # Access granted
|
||||||
@@ -37,13 +42,19 @@ def check_daily_mirror_access():
|
|||||||
|
|
||||||
def check_daily_mirror_api_access():
|
def check_daily_mirror_api_access():
|
||||||
"""Helper function to check API access for Daily Mirror"""
|
"""Helper function to check API access for Daily Mirror"""
|
||||||
# Check if user is logged in and has admin+ access
|
# Check if user is logged in
|
||||||
if 'user' not in session:
|
if 'user' not in session:
|
||||||
return jsonify({'error': 'Authentication required'}), 401
|
return jsonify({'error': 'Authentication required'}), 401
|
||||||
|
|
||||||
|
# Superadmin has access to everything
|
||||||
user_role = session.get('role', '')
|
user_role = session.get('role', '')
|
||||||
if user_role not in ['superadmin', 'admin']:
|
if user_role == 'superadmin':
|
||||||
return jsonify({'error': 'Admin privileges required'}), 403
|
return None # Access granted
|
||||||
|
|
||||||
|
# Check if user has daily_mirror module access
|
||||||
|
user_modules = session.get('modules', [])
|
||||||
|
if 'daily_mirror' not in user_modules:
|
||||||
|
return jsonify({'error': 'Daily Mirror module access required'}), 403
|
||||||
|
|
||||||
return None # Access granted
|
return None # Access granted
|
||||||
|
|
||||||
|
|||||||
431
py_app/app/database_backup.py
Normal file
431
py_app/app/database_backup.py
Normal file
@@ -0,0 +1,431 @@
|
|||||||
|
"""
|
||||||
|
Database Backup Management Module
|
||||||
|
Quality Recticel Application
|
||||||
|
|
||||||
|
This module provides functionality for backing up and restoring the MariaDB database,
|
||||||
|
including scheduled backups, manual backups, and backup file management.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from pathlib import Path
|
||||||
|
import configparser
|
||||||
|
from flask import current_app
|
||||||
|
import mariadb
|
||||||
|
|
||||||
|
class DatabaseBackupManager:
|
||||||
|
"""Manages database backup operations"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""Initialize the backup manager with configuration from external_server.conf"""
|
||||||
|
self.config = self._load_database_config()
|
||||||
|
self.backup_path = self._get_backup_path()
|
||||||
|
self._ensure_backup_directory()
|
||||||
|
|
||||||
|
def _load_database_config(self):
|
||||||
|
"""Load database configuration from external_server.conf"""
|
||||||
|
try:
|
||||||
|
settings_file = os.path.join(current_app.instance_path, 'external_server.conf')
|
||||||
|
config = {}
|
||||||
|
|
||||||
|
if os.path.exists(settings_file):
|
||||||
|
with open(settings_file, 'r') as f:
|
||||||
|
for line in f:
|
||||||
|
if '=' in line:
|
||||||
|
key, value = line.strip().split('=', 1)
|
||||||
|
config[key] = value
|
||||||
|
|
||||||
|
return {
|
||||||
|
'host': config.get('server_domain', 'localhost'),
|
||||||
|
'port': config.get('port', '3306'),
|
||||||
|
'database': config.get('database_name', 'trasabilitate'),
|
||||||
|
'user': config.get('username', 'trasabilitate'),
|
||||||
|
'password': config.get('password', '')
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading database config: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _get_backup_path(self):
|
||||||
|
"""Get backup path from environment or use default"""
|
||||||
|
# Check environment variable (set in docker-compose)
|
||||||
|
backup_path = os.environ.get('BACKUP_PATH', '/srv/quality_app/backups')
|
||||||
|
|
||||||
|
# Check if custom path is set in config
|
||||||
|
try:
|
||||||
|
settings_file = os.path.join(current_app.instance_path, 'external_server.conf')
|
||||||
|
if os.path.exists(settings_file):
|
||||||
|
with open(settings_file, 'r') as f:
|
||||||
|
for line in f:
|
||||||
|
if line.startswith('backup_path='):
|
||||||
|
backup_path = line.strip().split('=', 1)[1]
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error reading backup path from config: {e}")
|
||||||
|
|
||||||
|
return backup_path
|
||||||
|
|
||||||
|
def _ensure_backup_directory(self):
|
||||||
|
"""Ensure backup directory exists"""
|
||||||
|
try:
|
||||||
|
Path(self.backup_path).mkdir(parents=True, exist_ok=True)
|
||||||
|
print(f"Backup directory ensured: {self.backup_path}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error creating backup directory: {e}")
|
||||||
|
|
||||||
|
def create_backup(self, backup_name=None):
|
||||||
|
"""
|
||||||
|
Create a complete backup of the database
|
||||||
|
|
||||||
|
Args:
|
||||||
|
backup_name (str, optional): Custom name for the backup file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Result with success status, message, and backup file path
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not self.config:
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'message': 'Database configuration not loaded'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generate backup filename
|
||||||
|
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||||
|
if backup_name:
|
||||||
|
filename = f"{backup_name}_{timestamp}.sql"
|
||||||
|
else:
|
||||||
|
filename = f"backup_{self.config['database']}_{timestamp}.sql"
|
||||||
|
|
||||||
|
backup_file = os.path.join(self.backup_path, filename)
|
||||||
|
|
||||||
|
# Build mysqldump command
|
||||||
|
cmd = [
|
||||||
|
'mysqldump',
|
||||||
|
f"--host={self.config['host']}",
|
||||||
|
f"--port={self.config['port']}",
|
||||||
|
f"--user={self.config['user']}",
|
||||||
|
f"--password={self.config['password']}",
|
||||||
|
'--single-transaction',
|
||||||
|
'--routines',
|
||||||
|
'--triggers',
|
||||||
|
'--events',
|
||||||
|
'--add-drop-database',
|
||||||
|
'--databases',
|
||||||
|
self.config['database']
|
||||||
|
]
|
||||||
|
|
||||||
|
# Execute mysqldump and save to file
|
||||||
|
with open(backup_file, 'w') as f:
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
stdout=f,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
# Get file size
|
||||||
|
file_size = os.path.getsize(backup_file)
|
||||||
|
file_size_mb = file_size / (1024 * 1024)
|
||||||
|
|
||||||
|
# Save backup metadata
|
||||||
|
self._save_backup_metadata(filename, file_size)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'message': f'Backup created successfully',
|
||||||
|
'filename': filename,
|
||||||
|
'file_path': backup_file,
|
||||||
|
'size': f"{file_size_mb:.2f} MB",
|
||||||
|
'timestamp': timestamp
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
error_msg = result.stderr
|
||||||
|
print(f"Backup error: {error_msg}")
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'message': f'Backup failed: {error_msg}'
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Exception during backup: {e}")
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'message': f'Backup failed: {str(e)}'
|
||||||
|
}
|
||||||
|
|
||||||
|
def _save_backup_metadata(self, filename, file_size):
|
||||||
|
"""Save metadata about the backup"""
|
||||||
|
try:
|
||||||
|
metadata_file = os.path.join(self.backup_path, 'backups_metadata.json')
|
||||||
|
|
||||||
|
# Load existing metadata
|
||||||
|
metadata = []
|
||||||
|
if os.path.exists(metadata_file):
|
||||||
|
with open(metadata_file, 'r') as f:
|
||||||
|
metadata = json.load(f)
|
||||||
|
|
||||||
|
# Add new backup metadata
|
||||||
|
metadata.append({
|
||||||
|
'filename': filename,
|
||||||
|
'size': file_size,
|
||||||
|
'timestamp': datetime.now().isoformat(),
|
||||||
|
'database': self.config['database']
|
||||||
|
})
|
||||||
|
|
||||||
|
# Save updated metadata
|
||||||
|
with open(metadata_file, 'w') as f:
|
||||||
|
json.dump(metadata, f, indent=2)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error saving backup metadata: {e}")
|
||||||
|
|
||||||
|
def list_backups(self):
|
||||||
|
"""
|
||||||
|
List all available backups
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: List of backup information dictionaries
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
backups = []
|
||||||
|
|
||||||
|
# Get all .sql files in backup directory
|
||||||
|
if os.path.exists(self.backup_path):
|
||||||
|
for filename in os.listdir(self.backup_path):
|
||||||
|
if filename.endswith('.sql'):
|
||||||
|
file_path = os.path.join(self.backup_path, filename)
|
||||||
|
file_stat = os.stat(file_path)
|
||||||
|
|
||||||
|
backups.append({
|
||||||
|
'filename': filename,
|
||||||
|
'size': file_stat.st_size,
|
||||||
|
'size_mb': f"{file_stat.st_size / (1024 * 1024):.2f}",
|
||||||
|
'created': datetime.fromtimestamp(file_stat.st_ctime).strftime('%Y-%m-%d %H:%M:%S'),
|
||||||
|
'timestamp': file_stat.st_ctime
|
||||||
|
})
|
||||||
|
|
||||||
|
# Sort by timestamp (newest first)
|
||||||
|
backups.sort(key=lambda x: x['timestamp'], reverse=True)
|
||||||
|
|
||||||
|
return backups
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error listing backups: {e}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def delete_backup(self, filename):
|
||||||
|
"""
|
||||||
|
Delete a backup file
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename (str): Name of the backup file to delete
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Result with success status and message
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Security: ensure filename doesn't contain path traversal
|
||||||
|
if '..' in filename or '/' in filename:
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'message': 'Invalid filename'
|
||||||
|
}
|
||||||
|
|
||||||
|
file_path = os.path.join(self.backup_path, filename)
|
||||||
|
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
os.remove(file_path)
|
||||||
|
|
||||||
|
# Update metadata
|
||||||
|
self._remove_backup_metadata(filename)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'message': f'Backup {filename} deleted successfully'
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'message': 'Backup file not found'
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error deleting backup: {e}")
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'message': f'Failed to delete backup: {str(e)}'
|
||||||
|
}
|
||||||
|
|
||||||
|
def _remove_backup_metadata(self, filename):
|
||||||
|
"""Remove metadata entry for deleted backup"""
|
||||||
|
try:
|
||||||
|
metadata_file = os.path.join(self.backup_path, 'backups_metadata.json')
|
||||||
|
|
||||||
|
if os.path.exists(metadata_file):
|
||||||
|
with open(metadata_file, 'r') as f:
|
||||||
|
metadata = json.load(f)
|
||||||
|
|
||||||
|
# Filter out the deleted backup
|
||||||
|
metadata = [m for m in metadata if m['filename'] != filename]
|
||||||
|
|
||||||
|
with open(metadata_file, 'w') as f:
|
||||||
|
json.dump(metadata, f, indent=2)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error removing backup metadata: {e}")
|
||||||
|
|
||||||
|
def restore_backup(self, filename):
|
||||||
|
"""
|
||||||
|
Restore database from a backup file
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filename (str): Name of the backup file to restore
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Result with success status and message
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Security: ensure filename doesn't contain path traversal
|
||||||
|
if '..' in filename or '/' in filename:
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'message': 'Invalid filename'
|
||||||
|
}
|
||||||
|
|
||||||
|
file_path = os.path.join(self.backup_path, filename)
|
||||||
|
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'message': 'Backup file not found'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build mysql restore command
|
||||||
|
cmd = [
|
||||||
|
'mysql',
|
||||||
|
f"--host={self.config['host']}",
|
||||||
|
f"--port={self.config['port']}",
|
||||||
|
f"--user={self.config['user']}",
|
||||||
|
f"--password={self.config['password']}"
|
||||||
|
]
|
||||||
|
|
||||||
|
# Execute mysql restore
|
||||||
|
with open(file_path, 'r') as f:
|
||||||
|
result = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
stdin=f,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if result.returncode == 0:
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'message': f'Database restored successfully from {filename}'
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
error_msg = result.stderr
|
||||||
|
print(f"Restore error: {error_msg}")
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'message': f'Restore failed: {error_msg}'
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Exception during restore: {e}")
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'message': f'Restore failed: {str(e)}'
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_backup_schedule(self):
|
||||||
|
"""Get current backup schedule configuration"""
|
||||||
|
try:
|
||||||
|
schedule_file = os.path.join(self.backup_path, 'backup_schedule.json')
|
||||||
|
|
||||||
|
if os.path.exists(schedule_file):
|
||||||
|
with open(schedule_file, 'r') as f:
|
||||||
|
return json.load(f)
|
||||||
|
|
||||||
|
# Default schedule
|
||||||
|
return {
|
||||||
|
'enabled': False,
|
||||||
|
'time': '02:00', # 2 AM
|
||||||
|
'frequency': 'daily', # daily, weekly, monthly
|
||||||
|
'retention_days': 30 # Keep backups for 30 days
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading backup schedule: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def save_backup_schedule(self, schedule):
|
||||||
|
"""
|
||||||
|
Save backup schedule configuration
|
||||||
|
|
||||||
|
Args:
|
||||||
|
schedule (dict): Schedule configuration
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Result with success status and message
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
schedule_file = os.path.join(self.backup_path, 'backup_schedule.json')
|
||||||
|
|
||||||
|
with open(schedule_file, 'w') as f:
|
||||||
|
json.dump(schedule, f, indent=2)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'message': 'Backup schedule saved successfully'
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error saving backup schedule: {e}")
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'message': f'Failed to save schedule: {str(e)}'
|
||||||
|
}
|
||||||
|
|
||||||
|
def cleanup_old_backups(self, retention_days=30):
|
||||||
|
"""
|
||||||
|
Delete backups older than retention_days
|
||||||
|
|
||||||
|
Args:
|
||||||
|
retention_days (int): Number of days to keep backups
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Result with count of deleted backups
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
deleted_count = 0
|
||||||
|
cutoff_time = datetime.now() - timedelta(days=retention_days)
|
||||||
|
|
||||||
|
if os.path.exists(self.backup_path):
|
||||||
|
for filename in os.listdir(self.backup_path):
|
||||||
|
if filename.endswith('.sql'):
|
||||||
|
file_path = os.path.join(self.backup_path, filename)
|
||||||
|
file_time = datetime.fromtimestamp(os.path.getctime(file_path))
|
||||||
|
|
||||||
|
if file_time < cutoff_time:
|
||||||
|
os.remove(file_path)
|
||||||
|
self._remove_backup_metadata(filename)
|
||||||
|
deleted_count += 1
|
||||||
|
print(f"Deleted old backup: {filename}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
'success': True,
|
||||||
|
'deleted_count': deleted_count,
|
||||||
|
'message': f'Cleaned up {deleted_count} old backup(s)'
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error cleaning up old backups: {e}")
|
||||||
|
return {
|
||||||
|
'success': False,
|
||||||
|
'message': f'Cleanup failed: {str(e)}'
|
||||||
|
}
|
||||||
@@ -3,8 +3,6 @@ import os
|
|||||||
import mariadb
|
import mariadb
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from flask import Blueprint, render_template, redirect, url_for, request, flash, session, current_app, jsonify, send_from_directory
|
from flask import Blueprint, render_template, redirect, url_for, request, flash, session, current_app, jsonify, send_from_directory
|
||||||
from .models import User
|
|
||||||
from . import db
|
|
||||||
from reportlab.lib.pagesizes import letter
|
from reportlab.lib.pagesizes import letter
|
||||||
from reportlab.pdfgen import canvas
|
from reportlab.pdfgen import canvas
|
||||||
import csv
|
import csv
|
||||||
@@ -94,9 +92,9 @@ def login():
|
|||||||
except:
|
except:
|
||||||
user_modules = []
|
user_modules = []
|
||||||
|
|
||||||
# Superadmin and admin have access to all modules
|
# Superadmin has access to all modules
|
||||||
if user['role'] in ['superadmin', 'admin']:
|
if user['role'] == 'superadmin':
|
||||||
user_modules = ['quality', 'warehouse', 'labels']
|
user_modules = ['quality', 'warehouse', 'labels', 'daily_mirror']
|
||||||
|
|
||||||
session['modules'] = user_modules
|
session['modules'] = user_modules
|
||||||
print("Logged in as:", session.get('user'), session.get('role'), "modules:", user_modules)
|
print("Logged in as:", session.get('user'), session.get('role'), "modules:", user_modules)
|
||||||
@@ -3581,3 +3579,136 @@ def help(page='index'):
|
|||||||
# "printer_name": "default",
|
# "printer_name": "default",
|
||||||
# "copies": 1
|
# "copies": 1
|
||||||
# }
|
# }
|
||||||
|
|
||||||
|
# ========================================
|
||||||
|
# DATABASE BACKUP MANAGEMENT ROUTES
|
||||||
|
# ========================================
|
||||||
|
|
||||||
|
@bp.route('/api/backup/create', methods=['POST'])
|
||||||
|
@admin_plus
|
||||||
|
def api_backup_create():
|
||||||
|
"""Create a new database backup"""
|
||||||
|
try:
|
||||||
|
from app.database_backup import DatabaseBackupManager
|
||||||
|
|
||||||
|
backup_manager = DatabaseBackupManager()
|
||||||
|
result = backup_manager.create_backup()
|
||||||
|
|
||||||
|
return jsonify(result)
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': f'Backup failed: {str(e)}'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@bp.route('/api/backup/list', methods=['GET'])
|
||||||
|
@admin_plus
|
||||||
|
def api_backup_list():
|
||||||
|
"""List all available backups"""
|
||||||
|
try:
|
||||||
|
from app.database_backup import DatabaseBackupManager
|
||||||
|
|
||||||
|
backup_manager = DatabaseBackupManager()
|
||||||
|
backups = backup_manager.list_backups()
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'backups': backups
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': f'Failed to list backups: {str(e)}'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@bp.route('/api/backup/download/<filename>', methods=['GET'])
|
||||||
|
@admin_plus
|
||||||
|
def api_backup_download(filename):
|
||||||
|
"""Download a backup file"""
|
||||||
|
try:
|
||||||
|
from app.database_backup import DatabaseBackupManager
|
||||||
|
from flask import send_file
|
||||||
|
import os
|
||||||
|
|
||||||
|
backup_manager = DatabaseBackupManager()
|
||||||
|
backup_path = backup_manager.backup_path
|
||||||
|
file_path = os.path.join(backup_path, filename)
|
||||||
|
|
||||||
|
# Security: ensure filename doesn't contain path traversal
|
||||||
|
if '..' in filename or '/' in filename:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': 'Invalid filename'
|
||||||
|
}), 400
|
||||||
|
|
||||||
|
if os.path.exists(file_path):
|
||||||
|
return send_file(file_path, as_attachment=True, download_name=filename)
|
||||||
|
else:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': 'Backup file not found'
|
||||||
|
}), 404
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': f'Download failed: {str(e)}'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@bp.route('/api/backup/delete/<filename>', methods=['DELETE'])
|
||||||
|
@admin_plus
|
||||||
|
def api_backup_delete(filename):
|
||||||
|
"""Delete a backup file"""
|
||||||
|
try:
|
||||||
|
from app.database_backup import DatabaseBackupManager
|
||||||
|
|
||||||
|
backup_manager = DatabaseBackupManager()
|
||||||
|
result = backup_manager.delete_backup(filename)
|
||||||
|
|
||||||
|
return jsonify(result)
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': f'Delete failed: {str(e)}'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@bp.route('/api/backup/schedule', methods=['GET', 'POST'])
|
||||||
|
@admin_plus
|
||||||
|
def api_backup_schedule():
|
||||||
|
"""Get or save backup schedule configuration"""
|
||||||
|
try:
|
||||||
|
from app.database_backup import DatabaseBackupManager
|
||||||
|
|
||||||
|
backup_manager = DatabaseBackupManager()
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
schedule = request.json
|
||||||
|
result = backup_manager.save_backup_schedule(schedule)
|
||||||
|
return jsonify(result)
|
||||||
|
else:
|
||||||
|
schedule = backup_manager.get_backup_schedule()
|
||||||
|
return jsonify({
|
||||||
|
'success': True,
|
||||||
|
'schedule': schedule
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': f'Schedule operation failed: {str(e)}'
|
||||||
|
}), 500
|
||||||
|
|
||||||
|
@bp.route('/api/backup/restore/<filename>', methods=['POST'])
|
||||||
|
@superadmin_only
|
||||||
|
def api_backup_restore(filename):
|
||||||
|
"""Restore database from a backup file (superadmin only)"""
|
||||||
|
try:
|
||||||
|
from app.database_backup import DatabaseBackupManager
|
||||||
|
|
||||||
|
backup_manager = DatabaseBackupManager()
|
||||||
|
result = backup_manager.restore_backup(filename)
|
||||||
|
|
||||||
|
return jsonify(result)
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({
|
||||||
|
'success': False,
|
||||||
|
'message': f'Restore failed: {str(e)}'
|
||||||
|
}), 500
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
from flask import render_template, request, session, redirect, url_for, flash, current_app, jsonify
|
from flask import render_template, request, session, redirect, url_for, flash, current_app, jsonify
|
||||||
from .models import User
|
|
||||||
from . import db
|
|
||||||
from .permissions import APP_PERMISSIONS, ROLE_HIERARCHY, ACTIONS, get_all_permissions, get_default_permissions_for_role
|
from .permissions import APP_PERMISSIONS, ROLE_HIERARCHY, ACTIONS, get_all_permissions, get_default_permissions_for_role
|
||||||
import mariadb
|
import mariadb
|
||||||
import os
|
import os
|
||||||
|
|||||||
447
py_app/app/templates/daily_mirror.html
Normal file
447
py_app/app/templates/daily_mirror.html
Normal file
@@ -0,0 +1,447 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Daily Mirror - Quality Recticel{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<!-- Page Header -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h1 class="h3 mb-0">📈 Daily Mirror</h1>
|
||||||
|
<p class="text-muted">Generate comprehensive daily production reports</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="{{ url_for('daily_mirror.daily_mirror_history_route') }}" class="btn btn-outline-primary">
|
||||||
|
<i class="fas fa-history"></i> View History
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('main.dashboard') }}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Back to Dashboard
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Selection Card -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">
|
||||||
|
<i class="fas fa-calendar-alt"></i> Select Report Date
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label for="reportDate" class="form-label">Report Date:</label>
|
||||||
|
<input type="date" class="form-control" id="reportDate" value="{{ today }}">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 d-flex align-items-end">
|
||||||
|
<button type="button" class="btn btn-primary" onclick="generateDailyReport()">
|
||||||
|
<i class="fas fa-chart-line"></i> Generate Report
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4 d-flex align-items-end">
|
||||||
|
<button type="button" class="btn btn-success" onclick="setTodayDate()">
|
||||||
|
<i class="fas fa-calendar-day"></i> Today's Report
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading Indicator -->
|
||||||
|
<div id="loadingIndicator" class="row mb-4" style="display: none;">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 mb-0">Generating daily report...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Daily Report Results -->
|
||||||
|
<div id="reportResults" style="display: none;">
|
||||||
|
<!-- Key Metrics Overview -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">
|
||||||
|
<i class="fas fa-tachometer-alt"></i> Daily Production Overview
|
||||||
|
<span id="reportDateDisplay" class="badge bg-primary ms-2"></span>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="metric-card orders-quantity">
|
||||||
|
<div class="metric-icon">
|
||||||
|
<i class="fas fa-clipboard-list"></i>
|
||||||
|
</div>
|
||||||
|
<div class="metric-content">
|
||||||
|
<h3 id="ordersQuantity">-</h3>
|
||||||
|
<p>Orders Quantity</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="metric-card production-launched">
|
||||||
|
<div class="metric-icon">
|
||||||
|
<i class="fas fa-play-circle"></i>
|
||||||
|
</div>
|
||||||
|
<div class="metric-content">
|
||||||
|
<h3 id="productionLaunched">-</h3>
|
||||||
|
<p>Production Launched</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="metric-card production-finished">
|
||||||
|
<div class="metric-icon">
|
||||||
|
<i class="fas fa-check-circle"></i>
|
||||||
|
</div>
|
||||||
|
<div class="metric-content">
|
||||||
|
<h3 id="productionFinished">-</h3>
|
||||||
|
<p>Production Finished</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="metric-card orders-delivered">
|
||||||
|
<div class="metric-icon">
|
||||||
|
<i class="fas fa-truck"></i>
|
||||||
|
</div>
|
||||||
|
<div class="metric-content">
|
||||||
|
<h3 id="ordersDelivered">-</h3>
|
||||||
|
<p>Orders Delivered</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quality Control Metrics -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">
|
||||||
|
<i class="fas fa-search"></i> Quality Control Scans
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="quality-stats">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-4">
|
||||||
|
<div class="stat-item">
|
||||||
|
<h4 id="qualityTotalScans">-</h4>
|
||||||
|
<p>Total Scans</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-4">
|
||||||
|
<div class="stat-item approved">
|
||||||
|
<h4 id="qualityApprovedScans">-</h4>
|
||||||
|
<p>Approved</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-4">
|
||||||
|
<div class="stat-item rejected">
|
||||||
|
<h4 id="qualityRejectedScans">-</h4>
|
||||||
|
<p>Rejected</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<div class="progress">
|
||||||
|
<div id="qualityApprovalBar" class="progress-bar bg-success" role="progressbar" style="width: 0%"></div>
|
||||||
|
</div>
|
||||||
|
<p class="text-center mt-2 mb-0">
|
||||||
|
Approval Rate: <span id="qualityApprovalRate" class="fw-bold">0%</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">
|
||||||
|
<i class="fas fa-clipboard-check"></i> Finish Goods Quality
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="quality-stats">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-4">
|
||||||
|
<div class="stat-item">
|
||||||
|
<h4 id="fgQualityTotalScans">-</h4>
|
||||||
|
<p>Total Scans</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-4">
|
||||||
|
<div class="stat-item approved">
|
||||||
|
<h4 id="fgQualityApprovedScans">-</h4>
|
||||||
|
<p>Approved</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-4">
|
||||||
|
<div class="stat-item rejected">
|
||||||
|
<h4 id="fgQualityRejectedScans">-</h4>
|
||||||
|
<p>Rejected</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<div class="progress">
|
||||||
|
<div id="fgQualityApprovalBar" class="progress-bar bg-success" role="progressbar" style="width: 0%"></div>
|
||||||
|
</div>
|
||||||
|
<p class="text-center mt-2 mb-0">
|
||||||
|
Approval Rate: <span id="fgQualityApprovalRate" class="fw-bold">0%</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Export and Actions -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">
|
||||||
|
<i class="fas fa-download"></i> Export Options
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<button type="button" class="btn btn-outline-success" onclick="exportReportPDF()">
|
||||||
|
<i class="fas fa-file-pdf"></i> Export PDF
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-primary" onclick="exportReportExcel()">
|
||||||
|
<i class="fas fa-file-excel"></i> Export Excel
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-info" onclick="printReport()">
|
||||||
|
<i class="fas fa-print"></i> Print Report
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary" onclick="shareReport()">
|
||||||
|
<i class="fas fa-share"></i> Share Report
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error Message -->
|
||||||
|
<div id="errorMessage" class="row mb-4" style="display: none;">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="alert alert-danger" role="alert">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
<strong>Error:</strong> <span id="errorText"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.metric-card {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 1.5rem;
|
||||||
|
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
||||||
|
border-radius: 10px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card.orders-quantity {
|
||||||
|
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card.production-launched {
|
||||||
|
background: linear-gradient(135deg, #f3e5f5 0%, #ce93d8 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card.production-finished {
|
||||||
|
background: linear-gradient(135deg, #e8f5e8 0%, #a5d6a7 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card.orders-delivered {
|
||||||
|
background: linear-gradient(135deg, #fff3e0 0%, #ffcc02 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-icon {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-content h3 {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-content p {
|
||||||
|
margin: 0;
|
||||||
|
color: #6c757d;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quality-stats .stat-item {
|
||||||
|
text-align: center;
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quality-stats .stat-item h4 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quality-stats .stat-item.approved h4 {
|
||||||
|
color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quality-stats .stat-item.rejected h4 {
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.quality-stats .stat-item p {
|
||||||
|
margin: 0;
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.metric-card {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-icon {
|
||||||
|
margin-right: 0;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function setTodayDate() {
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
document.getElementById('reportDate').value = today;
|
||||||
|
generateDailyReport();
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateDailyReport() {
|
||||||
|
const reportDate = document.getElementById('reportDate').value;
|
||||||
|
|
||||||
|
if (!reportDate) {
|
||||||
|
showError('Please select a report date');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show loading indicator
|
||||||
|
document.getElementById('loadingIndicator').style.display = 'block';
|
||||||
|
document.getElementById('reportResults').style.display = 'none';
|
||||||
|
document.getElementById('errorMessage').style.display = 'none';
|
||||||
|
|
||||||
|
// Make API call to get daily data
|
||||||
|
fetch(`/daily_mirror/api/data?date=${reportDate}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.error) {
|
||||||
|
showError(data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update display with data
|
||||||
|
updateDailyReport(data);
|
||||||
|
|
||||||
|
// Hide loading and show results
|
||||||
|
document.getElementById('loadingIndicator').style.display = 'none';
|
||||||
|
document.getElementById('reportResults').style.display = 'block';
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error generating daily report:', error);
|
||||||
|
showError('Failed to generate daily report. Please try again.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDailyReport(data) {
|
||||||
|
// Update date display
|
||||||
|
document.getElementById('reportDateDisplay').textContent = data.date;
|
||||||
|
|
||||||
|
// Update key metrics
|
||||||
|
document.getElementById('ordersQuantity').textContent = data.orders_quantity.toLocaleString();
|
||||||
|
document.getElementById('productionLaunched').textContent = data.production_launched.toLocaleString();
|
||||||
|
document.getElementById('productionFinished').textContent = data.production_finished.toLocaleString();
|
||||||
|
document.getElementById('ordersDelivered').textContent = data.orders_delivered.toLocaleString();
|
||||||
|
|
||||||
|
// Update quality control data
|
||||||
|
document.getElementById('qualityTotalScans').textContent = data.quality_scans.total_scans.toLocaleString();
|
||||||
|
document.getElementById('qualityApprovedScans').textContent = data.quality_scans.approved_scans.toLocaleString();
|
||||||
|
document.getElementById('qualityRejectedScans').textContent = data.quality_scans.rejected_scans.toLocaleString();
|
||||||
|
document.getElementById('qualityApprovalRate').textContent = data.quality_scans.approval_rate + '%';
|
||||||
|
document.getElementById('qualityApprovalBar').style.width = data.quality_scans.approval_rate + '%';
|
||||||
|
|
||||||
|
// Update FG quality data
|
||||||
|
document.getElementById('fgQualityTotalScans').textContent = data.fg_quality_scans.total_scans.toLocaleString();
|
||||||
|
document.getElementById('fgQualityApprovedScans').textContent = data.fg_quality_scans.approved_scans.toLocaleString();
|
||||||
|
document.getElementById('fgQualityRejectedScans').textContent = data.fg_quality_scans.rejected_scans.toLocaleString();
|
||||||
|
document.getElementById('fgQualityApprovalRate').textContent = data.fg_quality_scans.approval_rate + '%';
|
||||||
|
document.getElementById('fgQualityApprovalBar').style.width = data.fg_quality_scans.approval_rate + '%';
|
||||||
|
}
|
||||||
|
|
||||||
|
function showError(message) {
|
||||||
|
document.getElementById('errorText').textContent = message;
|
||||||
|
document.getElementById('errorMessage').style.display = 'block';
|
||||||
|
document.getElementById('loadingIndicator').style.display = 'none';
|
||||||
|
document.getElementById('reportResults').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportReportPDF() {
|
||||||
|
alert('PDF export functionality will be implemented soon.');
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportReportExcel() {
|
||||||
|
alert('Excel export functionality will be implemented soon.');
|
||||||
|
}
|
||||||
|
|
||||||
|
function printReport() {
|
||||||
|
window.print();
|
||||||
|
}
|
||||||
|
|
||||||
|
function shareReport() {
|
||||||
|
alert('Share functionality will be implemented soon.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-generate today's report on page load
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
generateDailyReport();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
760
py_app/app/templates/daily_mirror_build_database.html
Normal file
760
py_app/app/templates/daily_mirror_build_database.html
Normal file
@@ -0,0 +1,760 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Build Database - Daily Mirror{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='daily_mirror_tune.css') }}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<!-- Page Header -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h1 class="h3 mb-0">🔨 Build Database</h1>
|
||||||
|
<p class="text-muted">Upload Excel files to populate Daily Mirror database tables</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-6 mb-4">
|
||||||
|
<!-- Card 1: Upload Excel File -->
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-header bg-primary text-white">
|
||||||
|
<h5 class="card-title mb-0">
|
||||||
|
<i class="fas fa-upload"></i> Upload Excel File
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="POST" enctype="multipart/form-data" id="uploadForm">
|
||||||
|
<!-- Table Selection -->
|
||||||
|
<div class="form-group mb-4">
|
||||||
|
<label for="target_table" class="form-label">
|
||||||
|
<strong>Select Target Table:</strong>
|
||||||
|
</label>
|
||||||
|
<select class="form-control" name="target_table" id="target_table" required>
|
||||||
|
<option value="">-- Choose a table --</option>
|
||||||
|
{% for table in available_tables %}
|
||||||
|
<option value="{{ table.name }}" data-description="{{ table.description }}">
|
||||||
|
{{ table.display }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<small id="tableDescription" class="form-text text-muted mt-2">
|
||||||
|
Select a table to see its description.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- File Upload -->
|
||||||
|
<div class="form-group mb-4">
|
||||||
|
<label for="excel_file" class="form-label">
|
||||||
|
<strong>Select Excel File:</strong>
|
||||||
|
</label>
|
||||||
|
<input type="file" class="form-control" name="excel_file" id="excel_file"
|
||||||
|
accept=".xlsx,.xls" required>
|
||||||
|
<small class="form-text text-muted">
|
||||||
|
Accepted formats: .xlsx, .xls (Maximum file size: 10MB)
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Upload Button -->
|
||||||
|
<div class="text-center">
|
||||||
|
<button type="button" class="btn btn-primary btn-lg" id="uploadBtn">
|
||||||
|
<i class="fas fa-cloud-upload-alt"></i> Upload and Process File
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-lg-6 mb-4">
|
||||||
|
<!-- Card 2: Excel File Format Instructions -->
|
||||||
|
<div class="card h-100">
|
||||||
|
<div class="card-header bg-info text-white">
|
||||||
|
<h5 class="card-title mb-0">
|
||||||
|
<i class="fas fa-info-circle"></i> Excel File Format Instructions
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="accordion" id="formatAccordion">
|
||||||
|
<!-- Production Data Format -->
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header">
|
||||||
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#productionCollapse" aria-expanded="false">
|
||||||
|
🏭 Production Data Format
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="productionCollapse" class="accordion-collapse collapse" data-bs-parent="#formatAccordion">
|
||||||
|
<div class="accordion-body">
|
||||||
|
<p><strong>Expected columns for Production Data:</strong></p>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<ul class="list-unstyled mb-0">
|
||||||
|
<li><code>Production Order ID</code> <span class="text-muted">Unique identifier</span></li>
|
||||||
|
<li><code>Customer Code</code> <span class="text-muted">Customer code</span></li>
|
||||||
|
<li><code>Customer Name</code> <span class="text-muted">Customer name</span></li>
|
||||||
|
<li><code>Article Code</code> <span class="text-muted">Article code</span></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<ul class="list-unstyled mb-0">
|
||||||
|
<li><code>Article Description</code> <span class="text-muted">Description</span></li>
|
||||||
|
<li><code>Quantity</code> <span class="text-muted">To produce</span></li>
|
||||||
|
<li><code>Production Date</code> <span class="text-muted">Date</span></li>
|
||||||
|
<li><code>Status</code> <span class="text-muted">Production status</span></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Orders Data Format -->
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header">
|
||||||
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#ordersCollapse" aria-expanded="false">
|
||||||
|
🛒 Orders Data Format
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="ordersCollapse" class="accordion-collapse collapse" data-bs-parent="#formatAccordion">
|
||||||
|
<div class="accordion-body">
|
||||||
|
<p><strong>Expected columns for Orders Data:</strong></p>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<ul class="list-unstyled mb-0">
|
||||||
|
<li><code>Order ID</code> <span class="text-muted">Unique identifier</span></li>
|
||||||
|
<li><code>Customer Code</code> <span class="text-muted">Customer code</span></li>
|
||||||
|
<li><code>Customer Name</code> <span class="text-muted">Customer name</span></li>
|
||||||
|
<li><code>Article Code</code> <span class="text-muted">Article code</span></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<ul class="list-unstyled mb-0">
|
||||||
|
<li><code>Article Description</code> <span class="text-muted">Description</span></li>
|
||||||
|
<li><code>Quantity Ordered</code> <span class="text-muted">Ordered</span></li>
|
||||||
|
<li><code>Order Date</code> <span class="text-muted">Date</span></li>
|
||||||
|
<li><code>Status</code> <span class="text-muted">Order status</span></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Delivery Data Format -->
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header">
|
||||||
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#deliveryCollapse" aria-expanded="false">
|
||||||
|
🚚 Delivery Data Format (Articole livrate)
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="deliveryCollapse" class="accordion-collapse collapse" data-bs-parent="#formatAccordion">
|
||||||
|
<div class="accordion-body">
|
||||||
|
<p><strong>Expected columns for Delivery Data:</strong></p>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<ul class="list-unstyled mb-0">
|
||||||
|
<li><code>Shipment ID</code> <span class="text-muted">Unique shipment identifier</span></li>
|
||||||
|
<li><code>Order ID</code> <span class="text-muted">Related order</span></li>
|
||||||
|
<li><code>Customer</code> <span class="text-muted">Customer info</span></li>
|
||||||
|
<li><code>Article</code> <span class="text-muted">Code/description</span></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<ul class="list-unstyled mb-0">
|
||||||
|
<li><code>Quantity Delivered</code> <span class="text-muted">Delivered quantity</span></li>
|
||||||
|
<li><code>Delivery Date</code> <span class="text-muted">Date</span></li>
|
||||||
|
<li><code>Status</code> <span class="text-muted">Delivery status</span></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Upload Result Modal (Better Solution) -->
|
||||||
|
<div class="modal fade" id="uploadResultModal" tabindex="-1" aria-labelledby="uploadResultModalLabel" aria-hidden="true" data-bs-backdrop="true" data-bs-keyboard="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header" id="modalHeader">
|
||||||
|
<h5 class="modal-title" id="uploadResultModalLabel">
|
||||||
|
<i class="fas fa-check-circle"></i> Upload Result
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" id="modalCloseBtn"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div id="uploadResultContent" class="text-center py-3">
|
||||||
|
<!-- Result content will be inserted here -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" id="modalOkBtn">OK</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Additional modal fixes specific to this page */
|
||||||
|
#uploadResultModal {
|
||||||
|
z-index: 10000 !important;
|
||||||
|
pointer-events: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#uploadResultModal .modal-dialog {
|
||||||
|
pointer-events: auto !important;
|
||||||
|
z-index: 10001 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#uploadResultModal .modal-content {
|
||||||
|
pointer-events: auto !important;
|
||||||
|
background-color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#uploadResultModal .modal-backdrop {
|
||||||
|
z-index: 9998 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#uploadResultModal .btn-close,
|
||||||
|
#uploadResultModal .modal-footer button {
|
||||||
|
pointer-events: auto !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure modal can be dismissed */
|
||||||
|
.modal-backdrop.show {
|
||||||
|
pointer-events: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode modal fixes */
|
||||||
|
body.dark-mode #uploadResultModal .modal-content {
|
||||||
|
background-color: #2d3748 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-button:not(.collapsed) {
|
||||||
|
background-color: #e7f3ff;
|
||||||
|
color: #0066cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
border-color: #007bff;
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 123, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Result stats styling */
|
||||||
|
.upload-stats {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-box {
|
||||||
|
text-align: center;
|
||||||
|
padding: 15px;
|
||||||
|
min-width: 100px;
|
||||||
|
margin: 5px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-box.success {
|
||||||
|
background-color: #d4edda;
|
||||||
|
border: 1px solid #c3e6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-box.warning {
|
||||||
|
background-color: #fff3cd;
|
||||||
|
border: 1px solid #ffeeba;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-box.error {
|
||||||
|
background-color: #f8d7da;
|
||||||
|
border: 1px solid #f5c6cb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: bold;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-label {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduce font size for Excel Format Instructions card rows */
|
||||||
|
.col-lg-6:nth-child(2) .card-body {
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make accordion button labels smaller */
|
||||||
|
.accordion-button {
|
||||||
|
font-size: 1rem;
|
||||||
|
padding-top: 0.4rem;
|
||||||
|
padding-bottom: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Override h2 size in accordion headers */
|
||||||
|
.accordion-header {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-header h2 {
|
||||||
|
font-size: 1rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal summary list styling */
|
||||||
|
#uploadResultContent ul {
|
||||||
|
list-style-type: none;
|
||||||
|
padding-left: 0;
|
||||||
|
margin: 10px auto;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#uploadResultContent ul li {
|
||||||
|
padding: 5px 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#uploadResultContent ul li::before {
|
||||||
|
content: '✓ ';
|
||||||
|
color: #28a745;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make "Expected columns" text smaller in accordion bodies */
|
||||||
|
.accordion-body p {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.accordion-body strong {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode styles */
|
||||||
|
body.dark-mode .card {
|
||||||
|
background-color: #2d3748;
|
||||||
|
border-color: #4a5568;
|
||||||
|
color: #e2e8f0;
|
||||||
|
box-shadow: 0 4px 6px rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .card-header {
|
||||||
|
background-color: #4a5568;
|
||||||
|
color: #e2e8f0;
|
||||||
|
border-bottom: 1px solid rgba(226, 232, 240, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .form-control {
|
||||||
|
background-color: #4a5568;
|
||||||
|
border-color: #6b7280;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .form-control:focus {
|
||||||
|
background-color: #4a5568;
|
||||||
|
border-color: #007bff;
|
||||||
|
color: #e2e8f0;
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .accordion-button {
|
||||||
|
background-color: #374151;
|
||||||
|
color: #e2e8f0;
|
||||||
|
border-color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .accordion-button:not(.collapsed) {
|
||||||
|
background-color: #1e3a8a;
|
||||||
|
color: #bfdbfe;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .accordion-body {
|
||||||
|
background-color: #374151;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode code {
|
||||||
|
background-color: #374151;
|
||||||
|
color: #e2e8f0;
|
||||||
|
border: 1px solid #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .modal-content {
|
||||||
|
background-color: #2d3748;
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .modal-header {
|
||||||
|
border-bottom-color: #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .modal-footer {
|
||||||
|
border-top-color: #4a5568;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .stat-box.success {
|
||||||
|
background-color: #1e4620;
|
||||||
|
border-color: #2d5a2e;
|
||||||
|
color: #a3d9a5;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .stat-box.warning {
|
||||||
|
background-color: #5a4a1e;
|
||||||
|
border-color: #6b5a2d;
|
||||||
|
color: #f4d88f;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .stat-box.error {
|
||||||
|
background-color: #5a1e1e;
|
||||||
|
border-color: #6b2d2d;
|
||||||
|
color: #f8a3a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .stat-label {
|
||||||
|
color: #a0aec0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .text-muted {
|
||||||
|
color: #a0aec0 !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Initialize theme on page load
|
||||||
|
const savedTheme = localStorage.getItem('theme');
|
||||||
|
const body = document.body;
|
||||||
|
|
||||||
|
if (savedTheme === 'dark') {
|
||||||
|
body.classList.add('dark-mode');
|
||||||
|
} else {
|
||||||
|
body.classList.remove('dark-mode');
|
||||||
|
}
|
||||||
|
|
||||||
|
const tableSelect = document.getElementById('target_table');
|
||||||
|
const tableDescription = document.getElementById('tableDescription');
|
||||||
|
const uploadBtn = document.getElementById('uploadBtn');
|
||||||
|
const uploadForm = document.getElementById('uploadForm');
|
||||||
|
const fileInput = document.getElementById('excel_file');
|
||||||
|
|
||||||
|
// Update table description when selection changes
|
||||||
|
tableSelect.addEventListener('change', function() {
|
||||||
|
const selectedOption = this.options[this.selectedIndex];
|
||||||
|
if (selectedOption.value) {
|
||||||
|
const description = selectedOption.getAttribute('data-description');
|
||||||
|
tableDescription.innerHTML = `<strong>Selected:</strong> ${description}`;
|
||||||
|
tableDescription.className = 'form-text text-info mt-2';
|
||||||
|
} else {
|
||||||
|
tableDescription.innerHTML = 'Select a table to see its description.';
|
||||||
|
tableDescription.className = 'form-text text-muted mt-2';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// File input change handler to show file info
|
||||||
|
fileInput.addEventListener('change', function(e) {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (file) {
|
||||||
|
const fileName = file.name;
|
||||||
|
const fileSize = (file.size / 1024 / 1024).toFixed(2);
|
||||||
|
console.log(`Selected file: ${fileName} (${fileSize} MB)`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Upload button click handler (AJAX submission)
|
||||||
|
uploadBtn.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Validate file selection
|
||||||
|
if (!fileInput.files.length) {
|
||||||
|
alert('Please select an Excel file to upload.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate table selection
|
||||||
|
if (!tableSelect.value) {
|
||||||
|
alert('Please select a target table.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check file size (10MB limit)
|
||||||
|
const file = fileInput.files[0];
|
||||||
|
if (file.size > 10 * 1024 * 1024) {
|
||||||
|
alert('File size must be less than 10MB.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare form data
|
||||||
|
const formData = new FormData(uploadForm);
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
uploadBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Processing...';
|
||||||
|
uploadBtn.disabled = true;
|
||||||
|
|
||||||
|
// Submit via AJAX
|
||||||
|
fetch('/daily_mirror/build_database', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData,
|
||||||
|
headers: {
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
return response.json().then(err => Promise.reject(err));
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(result => {
|
||||||
|
// Reset button
|
||||||
|
uploadBtn.innerHTML = '<i class="fas fa-cloud-upload-alt"></i> Upload and Process File';
|
||||||
|
uploadBtn.disabled = false;
|
||||||
|
|
||||||
|
// Show result in modal
|
||||||
|
showUploadResult(result);
|
||||||
|
|
||||||
|
// Reset form
|
||||||
|
uploadForm.reset();
|
||||||
|
tableDescription.innerHTML = 'Select a table to see its description.';
|
||||||
|
tableDescription.className = 'form-text text-muted mt-2';
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
// Reset button
|
||||||
|
uploadBtn.innerHTML = '<i class="fas fa-cloud-upload-alt"></i> Upload and Process File';
|
||||||
|
uploadBtn.disabled = false;
|
||||||
|
|
||||||
|
// Show error in modal
|
||||||
|
showUploadError(error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function showUploadResult(result) {
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('uploadResultModal'));
|
||||||
|
const modalHeader = document.getElementById('modalHeader');
|
||||||
|
const modalTitle = document.getElementById('uploadResultModalLabel');
|
||||||
|
const content = document.getElementById('uploadResultContent');
|
||||||
|
|
||||||
|
// Determine overall status
|
||||||
|
const hasErrors = result.error_count && result.error_count > 0;
|
||||||
|
const hasSuccess = result.created_rows > 0 || result.updated_rows > 0;
|
||||||
|
|
||||||
|
// Update modal header color
|
||||||
|
if (hasErrors && !hasSuccess) {
|
||||||
|
modalHeader.className = 'modal-header bg-danger text-white';
|
||||||
|
modalTitle.innerHTML = '<i class="fas fa-times-circle"></i> Upload Failed';
|
||||||
|
} else if (hasErrors && hasSuccess) {
|
||||||
|
modalHeader.className = 'modal-header bg-warning text-dark';
|
||||||
|
modalTitle.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Upload Completed with Warnings';
|
||||||
|
} else {
|
||||||
|
modalHeader.className = 'modal-header bg-success text-white';
|
||||||
|
modalTitle.innerHTML = '<i class="fas fa-check-circle"></i> Upload Successful';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build result content with stats
|
||||||
|
let html = '<div class="upload-stats">';
|
||||||
|
|
||||||
|
// Total rows processed from Excel
|
||||||
|
html += `
|
||||||
|
<div class="stat-box ${hasErrors ? 'warning' : 'success'}">
|
||||||
|
<span class="stat-value">${result.total_rows || 0}</span>
|
||||||
|
<span class="stat-label">Rows Processed</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Created rows (new in database)
|
||||||
|
if (result.created_rows > 0) {
|
||||||
|
html += `
|
||||||
|
<div class="stat-box success">
|
||||||
|
<span class="stat-value">${result.created_rows}</span>
|
||||||
|
<span class="stat-label">New Rows Created</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Updated rows (existing in database)
|
||||||
|
if (result.updated_rows > 0) {
|
||||||
|
html += `
|
||||||
|
<div class="stat-box success">
|
||||||
|
<span class="stat-value">${result.updated_rows}</span>
|
||||||
|
<span class="stat-label">Rows Updated</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Errors
|
||||||
|
if (hasErrors) {
|
||||||
|
html += `
|
||||||
|
<div class="stat-box error">
|
||||||
|
<span class="stat-value">${result.error_count}</span>
|
||||||
|
<span class="stat-label">Errors</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
html += '</div>';
|
||||||
|
|
||||||
|
// Add detailed summary message
|
||||||
|
const successCount = (result.created_rows || 0) + (result.updated_rows || 0);
|
||||||
|
if (successCount > 0) {
|
||||||
|
let msg = `<p class="mt-3 mb-0"><strong>Successfully processed ${result.total_rows} rows from Excel:</strong></p>`;
|
||||||
|
msg += '<ul class="text-start">';
|
||||||
|
if (result.created_rows > 0) {
|
||||||
|
msg += `<li>${result.created_rows} new ${result.created_rows === 1 ? 'record' : 'records'} created in database</li>`;
|
||||||
|
}
|
||||||
|
if (result.updated_rows > 0) {
|
||||||
|
msg += `<li>${result.updated_rows} existing ${result.updated_rows === 1 ? 'record' : 'records'} updated</li>`;
|
||||||
|
}
|
||||||
|
msg += '</ul>';
|
||||||
|
html += msg;
|
||||||
|
}
|
||||||
|
if (hasErrors) {
|
||||||
|
html += `<p class="text-danger mb-0"><strong>⚠️ ${result.error_count} ${result.error_count === 1 ? 'row' : 'rows'} could not be processed due to errors.</strong></p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add auto-close countdown for successful uploads without errors
|
||||||
|
if (!hasErrors && successCount > 0) {
|
||||||
|
html += `<p class="text-muted mt-2 mb-0" id="autoCloseCountdown"><small>This window will close automatically in <span id="countdown">3</span> seconds...</small></p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
content.innerHTML = html;
|
||||||
|
modal.show();
|
||||||
|
|
||||||
|
// Get modal element
|
||||||
|
const modalElement = document.getElementById('uploadResultModal');
|
||||||
|
|
||||||
|
// Add explicit close handlers
|
||||||
|
const okBtn = document.getElementById('modalOkBtn');
|
||||||
|
const closeBtn = document.getElementById('modalCloseBtn');
|
||||||
|
|
||||||
|
if (okBtn) {
|
||||||
|
okBtn.onclick = function() {
|
||||||
|
modal.hide();
|
||||||
|
// Also trigger Bootstrap's native close
|
||||||
|
modalElement.classList.remove('show');
|
||||||
|
document.querySelector('.modal-backdrop')?.remove();
|
||||||
|
document.body.classList.remove('modal-open');
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
document.body.style.paddingRight = '';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (closeBtn) {
|
||||||
|
closeBtn.onclick = function() {
|
||||||
|
modal.hide();
|
||||||
|
// Also trigger Bootstrap's native close
|
||||||
|
modalElement.classList.remove('show');
|
||||||
|
document.querySelector('.modal-backdrop')?.remove();
|
||||||
|
document.body.classList.remove('modal-open');
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
document.body.style.paddingRight = '';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-close after 3 seconds for successful uploads without errors
|
||||||
|
if (!hasErrors && successCount > 0) {
|
||||||
|
let countdown = 3;
|
||||||
|
const countdownInterval = setInterval(function() {
|
||||||
|
countdown--;
|
||||||
|
const countdownSpan = document.getElementById('countdown');
|
||||||
|
if (countdownSpan) {
|
||||||
|
countdownSpan.textContent = countdown;
|
||||||
|
}
|
||||||
|
if (countdown <= 0) {
|
||||||
|
clearInterval(countdownInterval);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
clearInterval(countdownInterval);
|
||||||
|
modal.hide();
|
||||||
|
modalElement.classList.remove('show');
|
||||||
|
document.querySelector('.modal-backdrop')?.remove();
|
||||||
|
document.body.classList.remove('modal-open');
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
document.body.style.paddingRight = '';
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showUploadError(error) {
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('uploadResultModal'));
|
||||||
|
const modalHeader = document.getElementById('modalHeader');
|
||||||
|
const modalTitle = document.getElementById('uploadResultModalLabel');
|
||||||
|
const content = document.getElementById('uploadResultContent');
|
||||||
|
|
||||||
|
// Update modal header
|
||||||
|
modalHeader.className = 'modal-header bg-danger text-white';
|
||||||
|
modalTitle.innerHTML = '<i class="fas fa-times-circle"></i> Upload Error';
|
||||||
|
|
||||||
|
// Show error message
|
||||||
|
const errorMsg = error.error || error.message || 'An unexpected error occurred during upload.';
|
||||||
|
content.innerHTML = `
|
||||||
|
<div class="alert alert-danger mb-0">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
<strong>Error:</strong> ${errorMsg}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
modal.show();
|
||||||
|
|
||||||
|
// Get modal element
|
||||||
|
const modalElement = document.getElementById('uploadResultModal');
|
||||||
|
|
||||||
|
// Add explicit close handlers
|
||||||
|
const okBtn = document.getElementById('modalOkBtn');
|
||||||
|
const closeBtn = document.getElementById('modalCloseBtn');
|
||||||
|
|
||||||
|
if (okBtn) {
|
||||||
|
okBtn.onclick = function() {
|
||||||
|
modal.hide();
|
||||||
|
// Also trigger Bootstrap's native close
|
||||||
|
modalElement.classList.remove('show');
|
||||||
|
document.querySelector('.modal-backdrop')?.remove();
|
||||||
|
document.body.classList.remove('modal-open');
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
document.body.style.paddingRight = '';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (closeBtn) {
|
||||||
|
closeBtn.onclick = function() {
|
||||||
|
modal.hide();
|
||||||
|
// Also trigger Bootstrap's native close
|
||||||
|
modalElement.classList.remove('show');
|
||||||
|
document.querySelector('.modal-backdrop')?.remove();
|
||||||
|
document.body.classList.remove('modal-open');
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
document.body.style.paddingRight = '';
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
449
py_app/app/templates/daily_mirror_history.html
Normal file
449
py_app/app/templates/daily_mirror_history.html
Normal file
@@ -0,0 +1,449 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Daily Mirror History - Quality Recticel{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<!-- Page Header -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h1 class="h3 mb-0">📋 Daily Mirror History</h1>
|
||||||
|
<p class="text-muted">Analyze historical daily production reports and trends</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<a href="{{ url_for('daily_mirror.daily_mirror_route') }}" class="btn btn-outline-success">
|
||||||
|
<i class="fas fa-chart-line"></i> Create New Report
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('main.dashboard') }}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left"></i> Back to Dashboard
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date Range Selection -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">
|
||||||
|
<i class="fas fa-calendar-week"></i> Select Date Range
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="startDate" class="form-label">Start Date:</label>
|
||||||
|
<input type="date" class="form-control" id="startDate" value="{{ start_date }}">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label for="endDate" class="form-label">End Date:</label>
|
||||||
|
<input type="date" class="form-control" id="endDate" value="{{ end_date }}">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 d-flex align-items-end">
|
||||||
|
<button type="button" class="btn btn-primary" onclick="loadHistoryData()">
|
||||||
|
<i class="fas fa-search"></i> Load History
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 d-flex align-items-end">
|
||||||
|
<div class="btn-group" role="group">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" onclick="setDateRange(7)">Last 7 days</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary" onclick="setDateRange(30)">Last 30 days</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading Indicator -->
|
||||||
|
<div id="loadingIndicator" class="row mb-4" style="display: none;">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<div class="spinner-border text-primary" role="status">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
<p class="mt-2 mb-0">Loading historical data...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Summary Statistics -->
|
||||||
|
<div id="summaryStats" style="display: none;">
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">
|
||||||
|
<i class="fas fa-chart-bar"></i> Period Summary
|
||||||
|
<span id="periodRange" class="badge bg-secondary ms-2"></span>
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="summary-metric">
|
||||||
|
<h4 id="totalOrdersQuantity">-</h4>
|
||||||
|
<p>Total Orders Quantity</p>
|
||||||
|
<small id="avgOrdersQuantity" class="text-muted">Avg: -</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="summary-metric">
|
||||||
|
<h4 id="totalProductionLaunched">-</h4>
|
||||||
|
<p>Total Production Launched</p>
|
||||||
|
<small id="avgProductionLaunched" class="text-muted">Avg: -</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="summary-metric">
|
||||||
|
<h4 id="totalProductionFinished">-</h4>
|
||||||
|
<p>Total Production Finished</p>
|
||||||
|
<small id="avgProductionFinished" class="text-muted">Avg: -</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="summary-metric">
|
||||||
|
<h4 id="totalOrdersDelivered">-</h4>
|
||||||
|
<p>Total Orders Delivered</p>
|
||||||
|
<small id="avgOrdersDelivered" class="text-muted">Avg: -</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Historical Data Table -->
|
||||||
|
<div id="historyTable" style="display: none;">
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="card-title mb-0">
|
||||||
|
<i class="fas fa-table"></i> Historical Daily Reports
|
||||||
|
</h5>
|
||||||
|
<div class="btn-group btn-group-sm" role="group">
|
||||||
|
<button type="button" class="btn btn-outline-primary" onclick="exportHistoryCSV()">
|
||||||
|
<i class="fas fa-file-csv"></i> CSV
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-success" onclick="exportHistoryExcel()">
|
||||||
|
<i class="fas fa-file-excel"></i> Excel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-hover" id="historyDataTable">
|
||||||
|
<thead class="table-dark">
|
||||||
|
<tr>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Orders Quantity</th>
|
||||||
|
<th>Production Launched</th>
|
||||||
|
<th>Production Finished</th>
|
||||||
|
<th>Orders Delivered</th>
|
||||||
|
<th>Quality Approval Rate</th>
|
||||||
|
<th>FG Quality Approval Rate</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="historyTableBody">
|
||||||
|
<!-- Data will be populated here -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
<nav aria-label="History pagination" id="historyPagination" style="display: none;">
|
||||||
|
<ul class="pagination pagination-sm justify-content-center">
|
||||||
|
<!-- Pagination will be populated here -->
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Chart Visualization -->
|
||||||
|
<div id="chartVisualization" style="display: none;">
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">
|
||||||
|
<i class="fas fa-chart-line"></i> Trend Analysis
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<canvas id="trendChart" height="100"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Error Message -->
|
||||||
|
<div id="errorMessage" class="row mb-4" style="display: none;">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="alert alert-danger" role="alert">
|
||||||
|
<i class="fas fa-exclamation-triangle"></i>
|
||||||
|
<strong>Error:</strong> <span id="errorText"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.summary-metric {
|
||||||
|
text-align: center;
|
||||||
|
padding: 1rem;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-metric h4 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summary-metric p {
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
color: #6c757d;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-rate {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-rate.high {
|
||||||
|
color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-rate.medium {
|
||||||
|
color: #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
.approval-rate.low {
|
||||||
|
color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.table-responsive {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let historyData = [];
|
||||||
|
let currentPage = 1;
|
||||||
|
const itemsPerPage = 20;
|
||||||
|
|
||||||
|
function setDateRange(days) {
|
||||||
|
const endDate = new Date();
|
||||||
|
const startDate = new Date();
|
||||||
|
startDate.setDate(endDate.getDate() - days);
|
||||||
|
|
||||||
|
document.getElementById('endDate').value = endDate.toISOString().split('T')[0];
|
||||||
|
document.getElementById('startDate').value = startDate.toISOString().split('T')[0];
|
||||||
|
|
||||||
|
loadHistoryData();
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadHistoryData() {
|
||||||
|
const startDate = document.getElementById('startDate').value;
|
||||||
|
const endDate = document.getElementById('endDate').value;
|
||||||
|
|
||||||
|
if (!startDate || !endDate) {
|
||||||
|
showError('Please select both start and end dates');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (new Date(startDate) > new Date(endDate)) {
|
||||||
|
showError('Start date cannot be after end date');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show loading indicator
|
||||||
|
document.getElementById('loadingIndicator').style.display = 'block';
|
||||||
|
document.getElementById('summaryStats').style.display = 'none';
|
||||||
|
document.getElementById('historyTable').style.display = 'none';
|
||||||
|
document.getElementById('chartVisualization').style.display = 'none';
|
||||||
|
document.getElementById('errorMessage').style.display = 'none';
|
||||||
|
|
||||||
|
// Make API call to get historical data
|
||||||
|
fetch(`/daily_mirror/api/history_data?start_date=${startDate}&end_date=${endDate}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.error) {
|
||||||
|
showError(data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
historyData = data.history;
|
||||||
|
|
||||||
|
// Update displays
|
||||||
|
updateSummaryStats(data);
|
||||||
|
updateHistoryTable();
|
||||||
|
updateTrendChart();
|
||||||
|
|
||||||
|
// Hide loading and show results
|
||||||
|
document.getElementById('loadingIndicator').style.display = 'none';
|
||||||
|
document.getElementById('summaryStats').style.display = 'block';
|
||||||
|
document.getElementById('historyTable').style.display = 'block';
|
||||||
|
document.getElementById('chartVisualization').style.display = 'block';
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading history data:', error);
|
||||||
|
showError('Failed to load historical data. Please try again.');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSummaryStats(data) {
|
||||||
|
const history = data.history;
|
||||||
|
|
||||||
|
if (history.length === 0) {
|
||||||
|
document.getElementById('periodRange').textContent = 'No Data';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('periodRange').textContent = `${data.start_date} to ${data.end_date}`;
|
||||||
|
|
||||||
|
// Calculate totals and averages
|
||||||
|
const totals = history.reduce((acc, day) => {
|
||||||
|
acc.ordersQuantity += day.orders_quantity;
|
||||||
|
acc.productionLaunched += day.production_launched;
|
||||||
|
acc.productionFinished += day.production_finished;
|
||||||
|
acc.ordersDelivered += day.orders_delivered;
|
||||||
|
return acc;
|
||||||
|
}, { ordersQuantity: 0, productionLaunched: 0, productionFinished: 0, ordersDelivered: 0 });
|
||||||
|
|
||||||
|
const avgDivisor = history.length;
|
||||||
|
|
||||||
|
document.getElementById('totalOrdersQuantity').textContent = totals.ordersQuantity.toLocaleString();
|
||||||
|
document.getElementById('avgOrdersQuantity').textContent = `Avg: ${Math.round(totals.ordersQuantity / avgDivisor).toLocaleString()}`;
|
||||||
|
|
||||||
|
document.getElementById('totalProductionLaunched').textContent = totals.productionLaunched.toLocaleString();
|
||||||
|
document.getElementById('avgProductionLaunched').textContent = `Avg: ${Math.round(totals.productionLaunched / avgDivisor).toLocaleString()}`;
|
||||||
|
|
||||||
|
document.getElementById('totalProductionFinished').textContent = totals.productionFinished.toLocaleString();
|
||||||
|
document.getElementById('avgProductionFinished').textContent = `Avg: ${Math.round(totals.productionFinished / avgDivisor).toLocaleString()}`;
|
||||||
|
|
||||||
|
document.getElementById('totalOrdersDelivered').textContent = totals.ordersDelivered.toLocaleString();
|
||||||
|
document.getElementById('avgOrdersDelivered').textContent = `Avg: ${Math.round(totals.ordersDelivered / avgDivisor).toLocaleString()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateHistoryTable() {
|
||||||
|
const tbody = document.getElementById('historyTableBody');
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
|
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||||
|
const endIndex = startIndex + itemsPerPage;
|
||||||
|
const pageData = historyData.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
pageData.forEach(day => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
|
||||||
|
const qualityRate = day.quality_scans.approval_rate;
|
||||||
|
const fgQualityRate = day.fg_quality_scans.approval_rate;
|
||||||
|
|
||||||
|
row.innerHTML = `
|
||||||
|
<td><strong>${day.date}</strong></td>
|
||||||
|
<td>${day.orders_quantity.toLocaleString()}</td>
|
||||||
|
<td>${day.production_launched.toLocaleString()}</td>
|
||||||
|
<td>${day.production_finished.toLocaleString()}</td>
|
||||||
|
<td>${day.orders_delivered.toLocaleString()}</td>
|
||||||
|
<td><span class="approval-rate ${getApprovalRateClass(qualityRate)}">${qualityRate}%</span></td>
|
||||||
|
<td><span class="approval-rate ${getApprovalRateClass(fgQualityRate)}">${fgQualityRate}%</span></td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-outline-primary" onclick="viewDayDetails('${day.date}')">
|
||||||
|
<i class="fas fa-eye"></i> View
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
|
||||||
|
tbody.appendChild(row);
|
||||||
|
});
|
||||||
|
|
||||||
|
updatePagination();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getApprovalRateClass(rate) {
|
||||||
|
if (rate >= 95) return 'high';
|
||||||
|
if (rate >= 85) return 'medium';
|
||||||
|
return 'low';
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePagination() {
|
||||||
|
const totalPages = Math.ceil(historyData.length / itemsPerPage);
|
||||||
|
|
||||||
|
if (totalPages <= 1) {
|
||||||
|
document.getElementById('historyPagination').style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('historyPagination').style.display = 'block';
|
||||||
|
// Pagination implementation can be added here
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTrendChart() {
|
||||||
|
// Chart implementation using Chart.js can be added here
|
||||||
|
// For now, we'll show a placeholder
|
||||||
|
const canvas = document.getElementById('trendChart');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
// Clear canvas
|
||||||
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
|
|
||||||
|
// Draw placeholder text
|
||||||
|
ctx.font = '16px Arial';
|
||||||
|
ctx.fillStyle = '#6c757d';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.fillText('Trend chart visualization will be implemented here', canvas.width / 2, canvas.height / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function viewDayDetails(date) {
|
||||||
|
// Navigate to daily mirror with specific date
|
||||||
|
window.open(`{{ url_for('daily_mirror.daily_mirror_route') }}?date=${date}`, '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportHistoryCSV() {
|
||||||
|
alert('CSV export functionality will be implemented soon.');
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportHistoryExcel() {
|
||||||
|
alert('Excel export functionality will be implemented soon.');
|
||||||
|
}
|
||||||
|
|
||||||
|
function showError(message) {
|
||||||
|
document.getElementById('errorText').textContent = message;
|
||||||
|
document.getElementById('errorMessage').style.display = 'block';
|
||||||
|
document.getElementById('loadingIndicator').style.display = 'none';
|
||||||
|
document.getElementById('summaryStats').style.display = 'none';
|
||||||
|
document.getElementById('historyTable').style.display = 'none';
|
||||||
|
document.getElementById('chartVisualization').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-load data on page load
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
loadHistoryData();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
262
py_app/app/templates/daily_mirror_main.html
Normal file
262
py_app/app/templates/daily_mirror_main.html
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Daily Mirror - Quality Recticel{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<!-- Page Header -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h1 class="h3 mb-0">📊 Daily Mirror</h1>
|
||||||
|
<p class="text-muted">Business Intelligence and Production Reporting</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<!-- Buttons removed; now present in top header -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Daily Mirror Cards -->
|
||||||
|
<div class="row">
|
||||||
|
<!-- Card 1: Build Database -->
|
||||||
|
<div class="col-lg-6 col-md-6 mb-4">
|
||||||
|
<div class="card h-100 daily-mirror-card">
|
||||||
|
<div class="card-body d-flex flex-column">
|
||||||
|
<div class="text-center mb-3">
|
||||||
|
<div class="feature-icon bg-primary">
|
||||||
|
<i class="fas fa-database"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h5 class="card-title text-center">Build Database</h5>
|
||||||
|
<p class="card-text flex-grow-1 text-center">
|
||||||
|
Upload Excel files to create and populate tables.
|
||||||
|
</p>
|
||||||
|
<div class="mt-auto">
|
||||||
|
<a href="{{ url_for('daily_mirror.daily_mirror_build_database') }}" class="btn btn-primary btn-block w-100">
|
||||||
|
<i class="fas fa-hammer"></i> Build Database
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Card 2: Tune Database -->
|
||||||
|
<div class="col-lg-6 col-md-6 mb-4">
|
||||||
|
<div class="card h-100 daily-mirror-card">
|
||||||
|
<div class="card-body d-flex flex-column">
|
||||||
|
<div class="text-center mb-3">
|
||||||
|
<div class="feature-icon bg-warning">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h5 class="card-title text-center">Tune Database</h5>
|
||||||
|
<p class="card-text flex-grow-1 text-center">
|
||||||
|
Edit and update records after import.
|
||||||
|
</p>
|
||||||
|
<div class="mt-auto">
|
||||||
|
<a href="{{ url_for('daily_mirror.tune_production_data') }}" class="btn btn-warning btn-block w-100 btn-sm mb-2">
|
||||||
|
<i class="fas fa-industry"></i> Production Orders
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('daily_mirror.tune_orders_data') }}" class="btn btn-warning btn-block w-100 btn-sm mb-2">
|
||||||
|
<i class="fas fa-shopping-cart"></i> Customer Orders
|
||||||
|
</a>
|
||||||
|
<a href="{{ url_for('daily_mirror.tune_delivery_data') }}" class="btn btn-warning btn-block w-100 btn-sm">
|
||||||
|
<i class="fas fa-truck"></i> Delivery Records
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Card 3: Daily Mirror -->
|
||||||
|
<div class="col-lg-6 col-md-6 mb-4">
|
||||||
|
<div class="card h-100 daily-mirror-card">
|
||||||
|
<div class="card-body d-flex flex-column">
|
||||||
|
<div class="text-center mb-3">
|
||||||
|
<div class="feature-icon bg-success">
|
||||||
|
<i class="fas fa-chart-line"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h5 class="card-title text-center">Daily Mirror</h5>
|
||||||
|
<p class="card-text flex-grow-1 text-center">
|
||||||
|
Generate daily production reports.
|
||||||
|
</p>
|
||||||
|
<div class="mt-auto">
|
||||||
|
<a href="{{ url_for('daily_mirror.daily_mirror_route') }}" class="btn btn-success btn-block w-100">
|
||||||
|
<i class="fas fa-plus-circle"></i> Create Daily Report
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Card 4: Daily Mirror History -->
|
||||||
|
<div class="col-lg-6 col-md-6 mb-4">
|
||||||
|
<div class="card h-100 daily-mirror-card">
|
||||||
|
<div class="card-body d-flex flex-column">
|
||||||
|
<div class="text-center mb-3">
|
||||||
|
<div class="feature-icon bg-info">
|
||||||
|
<i class="fas fa-history"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<h5 class="card-title text-center">Daily Mirror History</h5>
|
||||||
|
<p class="card-text flex-grow-1 text-center">
|
||||||
|
View historical production reports.
|
||||||
|
</p>
|
||||||
|
<div class="mt-auto">
|
||||||
|
<a href="{{ url_for('daily_mirror.daily_mirror_history_route') }}" class="btn btn-info btn-block w-100">
|
||||||
|
<i class="fas fa-chart-bar"></i> View History
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.daily-mirror-card {
|
||||||
|
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
background-color: var(--card-bg);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.daily-mirror-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
box-shadow: 0 4px 15px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-icon {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0 auto;
|
||||||
|
color: white;
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light mode styles */
|
||||||
|
body:not(.dark-mode) .daily-mirror-card {
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #333333;
|
||||||
|
border: 1px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark-mode) .card-text {
|
||||||
|
color: #666666;
|
||||||
|
}
|
||||||
|
|
||||||
|
body:not(.dark-mode) .text-muted {
|
||||||
|
color: #6c757d !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode styles */
|
||||||
|
body.dark-mode .daily-mirror-card {
|
||||||
|
background-color: #2d3748;
|
||||||
|
color: #e2e8f0;
|
||||||
|
border: 1px solid #4a5568;
|
||||||
|
box-shadow: 0 2px 4px rgba(255,255,255,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .daily-mirror-card:hover {
|
||||||
|
box-shadow: 0 4px 15px rgba(255,255,255,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .card-text {
|
||||||
|
color: #cbd5e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .text-muted {
|
||||||
|
color: #a0aec0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .h3 {
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .container-fluid {
|
||||||
|
color: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure buttons maintain their intended colors in both themes */
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, #007bff 0%, #0056b3 100%);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-warning {
|
||||||
|
background: linear-gradient(135deg, #ffc107 0%, #e0a800 100%);
|
||||||
|
border: none;
|
||||||
|
color: #212529;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background: linear-gradient(135deg, #28a745 0%, #1e7e34 100%);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-info {
|
||||||
|
background: linear-gradient(135deg, #17a2b8 0%, #138496 100%);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: linear-gradient(135deg, #6c757d 0%, #545b62 100%);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.feature-icon {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Initialize theme on page load
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Apply saved theme from localStorage
|
||||||
|
const savedTheme = localStorage.getItem('theme');
|
||||||
|
const body = document.body;
|
||||||
|
|
||||||
|
if (savedTheme === 'dark') {
|
||||||
|
body.classList.add('dark-mode');
|
||||||
|
} else {
|
||||||
|
body.classList.remove('dark-mode');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update theme toggle button text if it exists
|
||||||
|
const themeToggleButton = document.getElementById('theme-toggle');
|
||||||
|
if (themeToggleButton) {
|
||||||
|
if (body.classList.contains('dark-mode')) {
|
||||||
|
themeToggleButton.textContent = 'Change to Light Mode';
|
||||||
|
} else {
|
||||||
|
themeToggleButton.textContent = 'Change to Dark Mode';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function showComingSoon(feature) {
|
||||||
|
alert(`${feature} functionality will be available in a future update!\n\nThis feature is currently under development and will include advanced capabilities for enhanced Daily Mirror operations.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-refresh quick stats every 5 minutes
|
||||||
|
setInterval(function() {
|
||||||
|
// This could be implemented to refresh the quick stats
|
||||||
|
console.log('Auto-refresh daily stats (not implemented yet)');
|
||||||
|
}, 300000); // 5 minutes
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
549
py_app/app/templates/daily_mirror_tune_delivery.html
Normal file
549
py_app/app/templates/daily_mirror_tune_delivery.html
Normal file
@@ -0,0 +1,549 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Tune Delivery Data - Daily Mirror{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/daily_mirror_tune.css') }}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<!-- Page Header -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h1 class="h3 mb-0">🚚 Tune Delivery Data</h1>
|
||||||
|
<p class="text-muted">Edit and update delivery records information</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<!-- Buttons removed; now present in top header -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filters Section -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">
|
||||||
|
<i class="fas fa-filter"></i> Filters and Search
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<label for="searchInput" class="form-label">Search</label>
|
||||||
|
<input type="text" class="form-control" id="searchInput"
|
||||||
|
placeholder="Search by shipment, customer, or article...">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<label for="statusFilter" class="form-label">Delivery Status</label>
|
||||||
|
<select class="form-control" id="statusFilter">
|
||||||
|
<option value="">All Statuses</option>
|
||||||
|
<!-- Will be populated dynamically -->
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<label for="customerFilter" class="form-label">Customer</label>
|
||||||
|
<select class="form-control" id="customerFilter">
|
||||||
|
<option value="">All Customers</option>
|
||||||
|
<!-- Will be populated dynamically -->
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<label for="recordsPerPage" class="form-label">Records per page</label>
|
||||||
|
<select class="form-control" id="recordsPerPage">
|
||||||
|
<option value="25">25</option>
|
||||||
|
<option value="50" selected>50</option>
|
||||||
|
<option value="100">100</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<button class="btn btn-primary" onclick="loadDeliveryData()">
|
||||||
|
<i class="fas fa-search"></i> Apply Filters
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-secondary" onclick="clearFilters()">
|
||||||
|
<i class="fas fa-times"></i> Clear
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Data Table Section -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="card-title mb-0">
|
||||||
|
<i class="fas fa-table"></i> Delivery Records Data
|
||||||
|
</h5>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<span id="recordsInfo" class="text-muted me-3"></span>
|
||||||
|
{% if session.get('role') == 'superadmin' %}
|
||||||
|
<button class="btn btn-danger btn-sm me-2" onclick="clearAllDelivery()" id="clearAllBtn">
|
||||||
|
<i class="fas fa-trash-alt"></i> Clear All Delivery
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
<button class="btn btn-success btn-sm" onclick="saveAllChanges()">
|
||||||
|
<i class="fas fa-save"></i> Save All Changes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-hover" id="deliveryTable">
|
||||||
|
<thead class="table-dark">
|
||||||
|
<tr>
|
||||||
|
<th>Shipment ID</th>
|
||||||
|
<th>Customer</th>
|
||||||
|
<th>Order ID</th>
|
||||||
|
<th>Article Code</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Quantity</th>
|
||||||
|
<th>Shipment Date</th>
|
||||||
|
<th>Delivery Date</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Total Value</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="deliveryTableBody">
|
||||||
|
<!-- Data will be loaded here -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading indicator -->
|
||||||
|
<div id="loadingIndicator" class="text-center py-4" style="display: none;">
|
||||||
|
<i class="fas fa-spinner fa-spin fa-2x"></i>
|
||||||
|
<p class="mt-2">Loading data...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- No data message -->
|
||||||
|
<div id="noDataMessage" class="text-center py-4" style="display: none;">
|
||||||
|
<i class="fas fa-info-circle fa-2x text-muted"></i>
|
||||||
|
<p class="mt-2 text-muted">No delivery records found</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
<div class="card-footer">
|
||||||
|
<nav aria-label="Delivery data pagination">
|
||||||
|
<ul class="pagination pagination-sm justify-content-center mb-0" id="pagination">
|
||||||
|
<!-- Pagination will be generated here -->
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit Modal -->
|
||||||
|
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="editModalLabel">Edit Delivery Record</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="editForm">
|
||||||
|
<input type="hidden" id="editRecordId">
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="editShipmentId" class="form-label">Shipment ID</label>
|
||||||
|
<input type="text" class="form-control" id="editShipmentId" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="editOrderId" class="form-label">Order ID</label>
|
||||||
|
<input type="text" class="form-control" id="editOrderId">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="editCustomerCode" class="form-label">Customer Code</label>
|
||||||
|
<input type="text" class="form-control" id="editCustomerCode">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="editCustomerName" class="form-label">Customer Name</label>
|
||||||
|
<input type="text" class="form-control" id="editCustomerName">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="editArticleCode" class="form-label">Article Code</label>
|
||||||
|
<input type="text" class="form-control" id="editArticleCode">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="editQuantity" class="form-label">Quantity Delivered</label>
|
||||||
|
<input type="number" class="form-control" id="editQuantity">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editDescription" class="form-label">Article Description</label>
|
||||||
|
<textarea class="form-control" id="editDescription" rows="2"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="editShipmentDate" class="form-label">Shipment Date</label>
|
||||||
|
<input type="date" class="form-control" id="editShipmentDate">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="editDeliveryDate" class="form-label">Delivery Date</label>
|
||||||
|
<input type="date" class="form-control" id="editDeliveryDate">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="editDeliveryStatus" class="form-label">Delivery Status</label>
|
||||||
|
<select class="form-control" id="editDeliveryStatus">
|
||||||
|
<option value="Finalizat">Finalizat</option>
|
||||||
|
<option value="Proiect">Proiect</option>
|
||||||
|
<option value="SHIPPED">Shipped</option>
|
||||||
|
<option value="DELIVERED">Delivered</option>
|
||||||
|
<option value="RETURNED">Returned</option>
|
||||||
|
<option value="PARTIAL">Partial</option>
|
||||||
|
<option value="CANCELLED">Cancelled</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6 mb-3">
|
||||||
|
<label for="editTotalValue" class="form-label">Total Value (€)</label>
|
||||||
|
<input type="number" step="0.01" class="form-control" id="editTotalValue">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||||
|
<i class="fas fa-times"></i> Cancel
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="saveRecord()">
|
||||||
|
<i class="fas fa-save"></i> Save Changes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let currentPage = 1;
|
||||||
|
let currentPerPage = 50;
|
||||||
|
let currentSearch = '';
|
||||||
|
let currentStatusFilter = '';
|
||||||
|
let currentCustomerFilter = '';
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Initialize theme
|
||||||
|
const savedTheme = localStorage.getItem('theme');
|
||||||
|
if (savedTheme === 'dark') {
|
||||||
|
document.body.classList.add('dark-mode');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load initial data
|
||||||
|
loadDeliveryData();
|
||||||
|
|
||||||
|
// Setup search on enter key
|
||||||
|
document.getElementById('searchInput').addEventListener('keypress', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
loadDeliveryData();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function loadDeliveryData(page = 1) {
|
||||||
|
currentPage = page;
|
||||||
|
currentPerPage = document.getElementById('recordsPerPage').value;
|
||||||
|
currentSearch = document.getElementById('searchInput').value;
|
||||||
|
currentStatusFilter = document.getElementById('statusFilter').value;
|
||||||
|
currentCustomerFilter = document.getElementById('customerFilter').value;
|
||||||
|
|
||||||
|
// Show loading indicator
|
||||||
|
document.getElementById('loadingIndicator').style.display = 'block';
|
||||||
|
document.getElementById('deliveryTableBody').style.display = 'none';
|
||||||
|
document.getElementById('noDataMessage').style.display = 'none';
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
page: currentPage,
|
||||||
|
per_page: currentPerPage,
|
||||||
|
search: currentSearch,
|
||||||
|
status: currentStatusFilter,
|
||||||
|
customer: currentCustomerFilter
|
||||||
|
});
|
||||||
|
|
||||||
|
fetch(`/daily_mirror/api/tune/delivery_data?${params}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
document.getElementById('loadingIndicator').style.display = 'none';
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
if (data.data.length === 0) {
|
||||||
|
document.getElementById('noDataMessage').style.display = 'block';
|
||||||
|
} else {
|
||||||
|
displayDeliveryData(data.data);
|
||||||
|
updatePagination(data);
|
||||||
|
updateRecordsInfo(data);
|
||||||
|
|
||||||
|
// Populate filter dropdowns on first load
|
||||||
|
if (currentPage === 1) {
|
||||||
|
populateCustomerFilter(data.customers);
|
||||||
|
populateStatusFilter(data.statuses);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('Error loading data:', data.error);
|
||||||
|
alert('Error loading delivery data: ' + data.error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
document.getElementById('loadingIndicator').style.display = 'none';
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('Error loading delivery data: ' + error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayDeliveryData(data) {
|
||||||
|
const tbody = document.getElementById('deliveryTableBody');
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
tbody.style.display = 'table-row-group';
|
||||||
|
|
||||||
|
data.forEach(record => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
row.innerHTML = `
|
||||||
|
<td><strong>${record.shipment_id}</strong></td>
|
||||||
|
<td>
|
||||||
|
<small class="text-muted d-block">${record.customer_code}</small>
|
||||||
|
${record.customer_name}
|
||||||
|
</td>
|
||||||
|
<td>${record.order_id || '-'}</td>
|
||||||
|
<td><code>${record.article_code}</code></td>
|
||||||
|
<td><small>${record.article_description || '-'}</small></td>
|
||||||
|
<td><span class="badge bg-info">${record.quantity_delivered}</span></td>
|
||||||
|
<td>${record.shipment_date || '-'}</td>
|
||||||
|
<td>${record.delivery_date || '-'}</td>
|
||||||
|
<td><span class="badge bg-success">${record.delivery_status}</span></td>
|
||||||
|
<td><strong>€${parseFloat(record.total_value || 0).toFixed(2)}</strong></td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-primary" onclick="editRecord(${record.id})"
|
||||||
|
title="Edit Delivery">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
tbody.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateCustomerFilter(customers) {
|
||||||
|
const filter = document.getElementById('customerFilter');
|
||||||
|
const currentValue = filter.value;
|
||||||
|
filter.innerHTML = '<option value="">All Customers</option>';
|
||||||
|
|
||||||
|
customers.forEach(customer => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = customer.code;
|
||||||
|
option.textContent = `${customer.code} - ${customer.name}`;
|
||||||
|
filter.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
filter.value = currentValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateStatusFilter(statuses) {
|
||||||
|
const filter = document.getElementById('statusFilter');
|
||||||
|
const currentValue = filter.value;
|
||||||
|
filter.innerHTML = '<option value="">All Statuses</option>';
|
||||||
|
|
||||||
|
statuses.forEach(status => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = status;
|
||||||
|
option.textContent = status;
|
||||||
|
filter.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
filter.value = currentValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePagination(data) {
|
||||||
|
const pagination = document.getElementById('pagination');
|
||||||
|
pagination.innerHTML = '';
|
||||||
|
|
||||||
|
if (data.total_pages <= 1) return;
|
||||||
|
|
||||||
|
// Previous button
|
||||||
|
const prevLi = document.createElement('li');
|
||||||
|
prevLi.className = `page-item ${data.page === 1 ? 'disabled' : ''}`;
|
||||||
|
prevLi.innerHTML = `<a class="page-link" href="#" onclick="loadDeliveryData(${data.page - 1})">Previous</a>`;
|
||||||
|
pagination.appendChild(prevLi);
|
||||||
|
|
||||||
|
// Page numbers
|
||||||
|
const startPage = Math.max(1, data.page - 2);
|
||||||
|
const endPage = Math.min(data.total_pages, data.page + 2);
|
||||||
|
|
||||||
|
for (let i = startPage; i <= endPage; i++) {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.className = `page-item ${i === data.page ? 'active' : ''}`;
|
||||||
|
li.innerHTML = `<a class="page-link" href="#" onclick="loadDeliveryData(${i})">${i}</a>`;
|
||||||
|
pagination.appendChild(li);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next button
|
||||||
|
const nextLi = document.createElement('li');
|
||||||
|
nextLi.className = `page-item ${data.page === data.total_pages ? 'disabled' : ''}`;
|
||||||
|
nextLi.innerHTML = `<a class="page-link" href="#" onclick="loadDeliveryData(${data.page + 1})">Next</a>`;
|
||||||
|
pagination.appendChild(nextLi);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRecordsInfo(data) {
|
||||||
|
const start = (data.page - 1) * data.per_page + 1;
|
||||||
|
const end = Math.min(data.page * data.per_page, data.total_records);
|
||||||
|
document.getElementById('recordsInfo').textContent =
|
||||||
|
`Showing ${start}-${end} of ${data.total_records} deliveries`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFilters() {
|
||||||
|
document.getElementById('searchInput').value = '';
|
||||||
|
document.getElementById('statusFilter').value = '';
|
||||||
|
document.getElementById('customerFilter').value = '';
|
||||||
|
loadDeliveryData(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function editRecord(recordId) {
|
||||||
|
// Get data via API for editing
|
||||||
|
fetch(`/daily_mirror/api/tune/delivery_data?page=${currentPage}&per_page=${currentPerPage}&search=${currentSearch}&status=${currentStatusFilter}&customer=${currentCustomerFilter}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
const recordData = data.data.find(record => record.id === recordId);
|
||||||
|
if (recordData) {
|
||||||
|
populateEditModal(recordData);
|
||||||
|
const editModal = new bootstrap.Modal(document.getElementById('editModal'));
|
||||||
|
editModal.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('Error loading record data: ' + error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateEditModal(record) {
|
||||||
|
document.getElementById('editRecordId').value = record.id;
|
||||||
|
document.getElementById('editShipmentId').value = record.shipment_id;
|
||||||
|
document.getElementById('editOrderId').value = record.order_id || '';
|
||||||
|
document.getElementById('editCustomerCode').value = record.customer_code;
|
||||||
|
document.getElementById('editCustomerName').value = record.customer_name;
|
||||||
|
document.getElementById('editArticleCode').value = record.article_code;
|
||||||
|
document.getElementById('editDescription').value = record.article_description || '';
|
||||||
|
document.getElementById('editQuantity').value = record.quantity_delivered;
|
||||||
|
document.getElementById('editShipmentDate').value = record.shipment_date;
|
||||||
|
document.getElementById('editDeliveryDate').value = record.delivery_date;
|
||||||
|
document.getElementById('editDeliveryStatus').value = record.delivery_status;
|
||||||
|
document.getElementById('editTotalValue').value = record.total_value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveRecord() {
|
||||||
|
const recordId = document.getElementById('editRecordId').value;
|
||||||
|
const data = {
|
||||||
|
customer_code: document.getElementById('editCustomerCode').value,
|
||||||
|
customer_name: document.getElementById('editCustomerName').value,
|
||||||
|
order_id: document.getElementById('editOrderId').value,
|
||||||
|
article_code: document.getElementById('editArticleCode').value,
|
||||||
|
article_description: document.getElementById('editDescription').value,
|
||||||
|
quantity_delivered: parseInt(document.getElementById('editQuantity').value) || 0,
|
||||||
|
shipment_date: document.getElementById('editShipmentDate').value,
|
||||||
|
delivery_date: document.getElementById('editDeliveryDate').value,
|
||||||
|
delivery_status: document.getElementById('editDeliveryStatus').value,
|
||||||
|
total_value: parseFloat(document.getElementById('editTotalValue').value) || 0
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch(`/daily_mirror/api/tune/delivery_data/${recordId}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(result => {
|
||||||
|
if (result.success) {
|
||||||
|
// Close modal
|
||||||
|
const editModal = bootstrap.Modal.getInstance(document.getElementById('editModal'));
|
||||||
|
editModal.hide();
|
||||||
|
|
||||||
|
// Reload data
|
||||||
|
loadDeliveryData(currentPage);
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
alert('Delivery record updated successfully!');
|
||||||
|
} else {
|
||||||
|
alert('Error updating delivery record: ' + result.error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('Error updating delivery record: ' + error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveAllChanges() {
|
||||||
|
alert('Save All Changes functionality will be implemented for bulk operations.');
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAllDelivery() {
|
||||||
|
if (!confirm('⚠️ WARNING: This will permanently delete ALL delivery records from the database!\n\nAre you absolutely sure you want to continue?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second confirmation for safety
|
||||||
|
if (!confirm('This action CANNOT be undone! All delivery data will be lost.\n\nClick OK to proceed with deletion.')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearBtn = document.getElementById('clearAllBtn');
|
||||||
|
const originalText = clearBtn.innerHTML;
|
||||||
|
clearBtn.disabled = true;
|
||||||
|
clearBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Deleting...';
|
||||||
|
|
||||||
|
fetch('/daily_mirror/clear_delivery', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
alert('✅ ' + data.message);
|
||||||
|
// Reload the page to show empty table
|
||||||
|
loadDeliveryData(1);
|
||||||
|
} else {
|
||||||
|
alert('❌ Error: ' + (data.message || 'Failed to clear delivery records'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('❌ Error clearing delivery records: ' + error.message);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
clearBtn.disabled = false;
|
||||||
|
clearBtn.innerHTML = originalText;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
753
py_app/app/templates/daily_mirror_tune_orders.html
Normal file
753
py_app/app/templates/daily_mirror_tune_orders.html
Normal file
@@ -0,0 +1,753 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Tune Orders Data - Daily Mirror{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/daily_mirror_tune.css') }}">
|
||||||
|
<style>
|
||||||
|
/* Force modal width - using viewport width for maximum responsiveness */
|
||||||
|
.modal#editModal .modal-dialog {
|
||||||
|
max-width: 95vw !important;
|
||||||
|
width: 95vw !important;
|
||||||
|
margin: 1.75rem auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal#editModal .modal-dialog.modal-xl {
|
||||||
|
max-width: 95vw !important;
|
||||||
|
width: 95vw !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Override ALL Bootstrap media queries */
|
||||||
|
@media (min-width: 576px) {
|
||||||
|
.modal#editModal .modal-dialog {
|
||||||
|
max-width: 95vw !important;
|
||||||
|
width: 95vw !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.modal#editModal .modal-dialog {
|
||||||
|
max-width: 95vw !important;
|
||||||
|
width: 95vw !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.modal#editModal .modal-dialog,
|
||||||
|
.modal#editModal .modal-dialog.modal-xl {
|
||||||
|
max-width: 95vw !important;
|
||||||
|
width: 95vw !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure pointer events work */
|
||||||
|
.modal#editModal .modal-dialog {
|
||||||
|
pointer-events: auto !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<!-- Page Header -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h1 class="h3 mb-0">🛒 Tune Orders Data</h1>
|
||||||
|
<p class="text-muted">Edit and update customer orders information</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<!-- Buttons removed; now present in top header -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filters Section -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">
|
||||||
|
<i class="fas fa-filter"></i> Filters and Search
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<label for="searchInput" class="form-label">Search</label>
|
||||||
|
<input type="text" class="form-control" id="searchInput"
|
||||||
|
placeholder="Search by order, customer, or article...">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<label for="statusFilter" class="form-label">Order Status</label>
|
||||||
|
<select class="form-control" id="statusFilter">
|
||||||
|
<option value="">All Statuses</option>
|
||||||
|
<!-- Will be populated dynamically -->
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<label for="customerFilter" class="form-label">Customer</label>
|
||||||
|
<select class="form-control" id="customerFilter">
|
||||||
|
<option value="">All Customers</option>
|
||||||
|
<!-- Will be populated dynamically -->
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<label for="recordsPerPage" class="form-label">Records per page</label>
|
||||||
|
<select class="form-control" id="recordsPerPage">
|
||||||
|
<option value="25">25</option>
|
||||||
|
<option value="50" selected>50</option>
|
||||||
|
<option value="100">100</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<button class="btn btn-primary" onclick="loadOrdersData()">
|
||||||
|
<i class="fas fa-search"></i> Apply Filters
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-secondary" onclick="clearFilters()">
|
||||||
|
<i class="fas fa-times"></i> Clear
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Data Table Section -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="card-title mb-0">
|
||||||
|
<i class="fas fa-table"></i> Customer Orders Data
|
||||||
|
</h5>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<span id="recordsInfo" class="text-muted me-3"></span>
|
||||||
|
{% if session.get('role') == 'superadmin' %}
|
||||||
|
<button class="btn btn-danger btn-sm me-2" onclick="clearAllOrders()" id="clearAllBtn">
|
||||||
|
<i class="fas fa-trash-alt"></i> Clear All Orders
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
<button class="btn btn-success btn-sm" onclick="saveAllChanges()">
|
||||||
|
<i class="fas fa-save"></i> Save All Changes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-hover" id="ordersTable">
|
||||||
|
<thead class="table-dark">
|
||||||
|
<tr>
|
||||||
|
<th>Order Line</th>
|
||||||
|
<th>Customer</th>
|
||||||
|
<th>Client Order Line</th>
|
||||||
|
<th>Article Code</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Quantity</th>
|
||||||
|
<th>Delivery Date</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Priority</th>
|
||||||
|
<th>Product Group</th>
|
||||||
|
<th>Order Date</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="ordersTableBody">
|
||||||
|
<!-- Data will be loaded here -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading indicator -->
|
||||||
|
<div id="loadingIndicator" class="text-center py-4" style="display: none;">
|
||||||
|
<i class="fas fa-spinner fa-spin fa-2x"></i>
|
||||||
|
<p class="mt-2">Loading data...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- No data message -->
|
||||||
|
<div id="noDataMessage" class="text-center py-4" style="display: none;">
|
||||||
|
<i class="fas fa-info-circle fa-2x text-muted"></i>
|
||||||
|
<p class="mt-2 text-muted">No orders found</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
<div class="card-footer">
|
||||||
|
<nav aria-label="Orders data pagination">
|
||||||
|
<ul class="pagination pagination-sm justify-content-center mb-0" id="pagination">
|
||||||
|
<!-- Pagination will be generated here -->
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit Modal -->
|
||||||
|
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true" data-bs-backdrop="true" data-bs-keyboard="true">
|
||||||
|
<div class="modal-dialog modal-xl modal-dialog-scrollable" style="max-width: 95vw !important; width: 95vw !important;">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="editModalLabel">Edit Customer Order</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="editForm">
|
||||||
|
<input type="hidden" id="editRecordId">
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<!-- Column 1: Order Information -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editOrderLine" class="form-label">Order Line</label>
|
||||||
|
<input type="text" class="form-control" id="editOrderLine" readonly>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editOrderId" class="form-label">Order ID</label>
|
||||||
|
<input type="text" class="form-control" id="editOrderId">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editLineNumber" class="form-label">Line Number</label>
|
||||||
|
<input type="text" class="form-control" id="editLineNumber">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editClientOrderLine" class="form-label">Client Order Line</label>
|
||||||
|
<input type="text" class="form-control" id="editClientOrderLine" readonly>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editOrderDate" class="form-label">Order Date</label>
|
||||||
|
<input type="date" class="form-control" id="editOrderDate">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editDeliveryDate" class="form-label">Delivery Date</label>
|
||||||
|
<input type="date" class="form-control" id="editDeliveryDate">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editOrderStatus" class="form-label">Order Status</label>
|
||||||
|
<select class="form-control" id="editOrderStatus">
|
||||||
|
<option value="PENDING">Pending</option>
|
||||||
|
<option value="CONFIRMED">Confirmed</option>
|
||||||
|
<option value="Confirmat">Confirmat</option>
|
||||||
|
<option value="IN_PROGRESS">In Progress</option>
|
||||||
|
<option value="FINISHED">Finished</option>
|
||||||
|
<option value="DELIVERED">Delivered</option>
|
||||||
|
<option value="CANCELLED">Cancelled</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editPriority" class="form-label">Priority</label>
|
||||||
|
<select class="form-control" id="editPriority">
|
||||||
|
<option value="LOW">Low</option>
|
||||||
|
<option value="NORMAL">Normal</option>
|
||||||
|
<option value="HIGH">High</option>
|
||||||
|
<option value="URGENT">Urgent</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Column 2: Customer and Article Information -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editCustomerCode" class="form-label">Customer Code</label>
|
||||||
|
<input type="text" class="form-control" id="editCustomerCode">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editCustomerName" class="form-label">Customer Name</label>
|
||||||
|
<input type="text" class="form-control" id="editCustomerName">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editArticleCode" class="form-label">Article Code</label>
|
||||||
|
<input type="text" class="form-control" id="editArticleCode">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editDescription" class="form-label">Article Description</label>
|
||||||
|
<textarea class="form-control" id="editDescription" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editQuantity" class="form-label">Quantity Requested</label>
|
||||||
|
<input type="number" class="form-control" id="editQuantity">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editBalance" class="form-label">Balance</label>
|
||||||
|
<input type="number" step="0.01" class="form-control" id="editBalance">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editUnitOfMeasure" class="form-label">Unit of Measure</label>
|
||||||
|
<input type="text" class="form-control" id="editUnitOfMeasure">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editArticleStatus" class="form-label">Article Status</label>
|
||||||
|
<input type="text" class="form-control" id="editArticleStatus">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Column 3: Production Information -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editProductGroup" class="form-label">Product Group</label>
|
||||||
|
<input type="text" class="form-control" id="editProductGroup">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editModel" class="form-label">Model</label>
|
||||||
|
<input type="text" class="form-control" id="editModel">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editProductionOrder" class="form-label">Production Order</label>
|
||||||
|
<input type="text" class="form-control" id="editProductionOrder">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editProductionStatus" class="form-label">Production Status</label>
|
||||||
|
<input type="text" class="form-control" id="editProductionStatus">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editClosed" class="form-label">Closed</label>
|
||||||
|
<input type="text" class="form-control" id="editClosed">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer d-flex justify-content-between">
|
||||||
|
<button type="button" class="btn btn-danger" onclick="deleteRecord()" style="min-width: 150px;">
|
||||||
|
<i class="fas fa-trash"></i> Delete Record
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<button type="button" class="btn btn-secondary me-2" data-bs-dismiss="modal" style="min-width: 100px;">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="saveRecord()" style="min-width: 150px;">
|
||||||
|
<i class="fas fa-save"></i> Save Changes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let currentPage = 1;
|
||||||
|
let currentPerPage = 50;
|
||||||
|
let currentSearch = '';
|
||||||
|
let currentStatusFilter = '';
|
||||||
|
let currentCustomerFilter = '';
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Initialize theme
|
||||||
|
const savedTheme = localStorage.getItem('theme');
|
||||||
|
if (savedTheme === 'dark') {
|
||||||
|
document.body.classList.add('dark-mode');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load initial data
|
||||||
|
loadOrdersData();
|
||||||
|
|
||||||
|
// Setup search on enter key
|
||||||
|
document.getElementById('searchInput').addEventListener('keypress', function(e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
loadOrdersData();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function loadOrdersData(page = 1) {
|
||||||
|
currentPage = page;
|
||||||
|
currentPerPage = document.getElementById('recordsPerPage').value;
|
||||||
|
currentSearch = document.getElementById('searchInput').value;
|
||||||
|
currentStatusFilter = document.getElementById('statusFilter').value;
|
||||||
|
currentCustomerFilter = document.getElementById('customerFilter').value;
|
||||||
|
|
||||||
|
// Show loading indicator
|
||||||
|
document.getElementById('loadingIndicator').style.display = 'block';
|
||||||
|
document.getElementById('ordersTableBody').style.display = 'none';
|
||||||
|
document.getElementById('noDataMessage').style.display = 'none';
|
||||||
|
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
page: currentPage,
|
||||||
|
per_page: currentPerPage,
|
||||||
|
search: currentSearch,
|
||||||
|
status: currentStatusFilter,
|
||||||
|
customer: currentCustomerFilter
|
||||||
|
});
|
||||||
|
|
||||||
|
fetch(`/daily_mirror/api/tune/orders_data?${params}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
document.getElementById('loadingIndicator').style.display = 'none';
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
if (data.data.length === 0) {
|
||||||
|
document.getElementById('noDataMessage').style.display = 'block';
|
||||||
|
} else {
|
||||||
|
displayOrdersData(data.data);
|
||||||
|
updatePagination(data);
|
||||||
|
updateRecordsInfo(data);
|
||||||
|
|
||||||
|
// Populate filter dropdowns on first load
|
||||||
|
if (currentPage === 1) {
|
||||||
|
populateCustomerFilter(data.customers);
|
||||||
|
populateStatusFilter(data.statuses);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.error('Error loading data:', data.error);
|
||||||
|
alert('Error loading orders data: ' + data.error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
document.getElementById('loadingIndicator').style.display = 'none';
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('Error loading orders data: ' + error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayOrdersData(data) {
|
||||||
|
const tbody = document.getElementById('ordersTableBody');
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
tbody.style.display = 'table-row-group';
|
||||||
|
|
||||||
|
data.forEach(record => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
row.innerHTML = `
|
||||||
|
<td><strong>${record.order_line}</strong></td>
|
||||||
|
<td>
|
||||||
|
<small class="text-muted d-block">${record.customer_code}</small>
|
||||||
|
${record.customer_name}
|
||||||
|
</td>
|
||||||
|
<td>${record.client_order_line || '-'}</td>
|
||||||
|
<td><code>${record.article_code}</code></td>
|
||||||
|
<td><small>${record.article_description || '-'}</small></td>
|
||||||
|
<td><span class="badge bg-info">${record.quantity_requested}</span></td>
|
||||||
|
<td>${record.delivery_date || '-'}</td>
|
||||||
|
<td><span class="badge bg-primary">${record.order_status}</span></td>
|
||||||
|
<td><span class="badge bg-warning">${record.priority || 'NORMAL'}</span></td>
|
||||||
|
<td><small>${record.product_group || '-'}</small></td>
|
||||||
|
<td>${record.order_date || '-'}</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-sm btn-primary" onclick="editRecord(${record.id})"
|
||||||
|
title="Edit Order">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
tbody.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateCustomerFilter(customers) {
|
||||||
|
const filter = document.getElementById('customerFilter');
|
||||||
|
// Keep the "All Customers" option and add new ones
|
||||||
|
const currentValue = filter.value;
|
||||||
|
filter.innerHTML = '<option value="">All Customers</option>';
|
||||||
|
|
||||||
|
customers.forEach(customer => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = customer.code;
|
||||||
|
option.textContent = `${customer.code} - ${customer.name}`;
|
||||||
|
filter.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
filter.value = currentValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateStatusFilter(statuses) {
|
||||||
|
const filter = document.getElementById('statusFilter');
|
||||||
|
const currentValue = filter.value;
|
||||||
|
filter.innerHTML = '<option value="">All Statuses</option>';
|
||||||
|
|
||||||
|
statuses.forEach(status => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = status;
|
||||||
|
option.textContent = status;
|
||||||
|
filter.appendChild(option);
|
||||||
|
});
|
||||||
|
|
||||||
|
filter.value = currentValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updatePagination(data) {
|
||||||
|
const pagination = document.getElementById('pagination');
|
||||||
|
pagination.innerHTML = '';
|
||||||
|
|
||||||
|
if (data.total_pages <= 1) return;
|
||||||
|
|
||||||
|
// Previous button
|
||||||
|
const prevLi = document.createElement('li');
|
||||||
|
prevLi.className = `page-item ${data.page === 1 ? 'disabled' : ''}`;
|
||||||
|
prevLi.innerHTML = `<a class="page-link" href="#" onclick="loadOrdersData(${data.page - 1})">Previous</a>`;
|
||||||
|
pagination.appendChild(prevLi);
|
||||||
|
|
||||||
|
// Page numbers
|
||||||
|
const startPage = Math.max(1, data.page - 2);
|
||||||
|
const endPage = Math.min(data.total_pages, data.page + 2);
|
||||||
|
|
||||||
|
for (let i = startPage; i <= endPage; i++) {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.className = `page-item ${i === data.page ? 'active' : ''}`;
|
||||||
|
li.innerHTML = `<a class="page-link" href="#" onclick="loadOrdersData(${i})">${i}</a>`;
|
||||||
|
pagination.appendChild(li);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next button
|
||||||
|
const nextLi = document.createElement('li');
|
||||||
|
nextLi.className = `page-item ${data.page === data.total_pages ? 'disabled' : ''}`;
|
||||||
|
nextLi.innerHTML = `<a class="page-link" href="#" onclick="loadOrdersData(${data.page + 1})">Next</a>`;
|
||||||
|
pagination.appendChild(nextLi);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRecordsInfo(data) {
|
||||||
|
const start = (data.page - 1) * data.per_page + 1;
|
||||||
|
const end = Math.min(data.page * data.per_page, data.total_records);
|
||||||
|
document.getElementById('recordsInfo').textContent =
|
||||||
|
`Showing ${start}-${end} of ${data.total_records} orders`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFilters() {
|
||||||
|
document.getElementById('searchInput').value = '';
|
||||||
|
document.getElementById('statusFilter').value = '';
|
||||||
|
document.getElementById('customerFilter').value = '';
|
||||||
|
loadOrdersData(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function editRecord(recordId) {
|
||||||
|
// Find the record data from the current display
|
||||||
|
const rows = document.querySelectorAll('#ordersTableBody tr');
|
||||||
|
let recordData = null;
|
||||||
|
|
||||||
|
// Get data via API for editing
|
||||||
|
fetch(`/daily_mirror/api/tune/orders_data?page=${currentPage}&per_page=${currentPerPage}&search=${currentSearch}&status=${currentStatusFilter}&customer=${currentCustomerFilter}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
recordData = data.data.find(record => record.id === recordId);
|
||||||
|
if (recordData) {
|
||||||
|
populateEditModal(recordData);
|
||||||
|
|
||||||
|
// Get modal elements
|
||||||
|
const modalElement = document.getElementById('editModal');
|
||||||
|
const modalDialog = modalElement.querySelector('.modal-dialog');
|
||||||
|
|
||||||
|
// Remove any existing modal instances to prevent conflicts
|
||||||
|
const existingModal = bootstrap.Modal.getInstance(modalElement);
|
||||||
|
if (existingModal) {
|
||||||
|
existingModal.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
const modal = new bootstrap.Modal(modalElement, {
|
||||||
|
backdrop: true,
|
||||||
|
keyboard: true,
|
||||||
|
focus: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show the modal first
|
||||||
|
modal.show();
|
||||||
|
|
||||||
|
// Force the modal dialog width AFTER modal is shown - using 95% of viewport width
|
||||||
|
setTimeout(() => {
|
||||||
|
if (modalDialog) {
|
||||||
|
console.log('Applying width after modal shown');
|
||||||
|
modalDialog.style.setProperty('max-width', '95vw', 'important');
|
||||||
|
modalDialog.style.setProperty('width', '95vw', 'important');
|
||||||
|
modalDialog.style.setProperty('margin', '1.75rem auto', 'important');
|
||||||
|
|
||||||
|
// Also apply to modal content for good measure
|
||||||
|
const modalContent = modalDialog.querySelector('.modal-content');
|
||||||
|
if (modalContent) {
|
||||||
|
modalContent.style.setProperty('width', '100%', 'important');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Modal dialog computed width:', window.getComputedStyle(modalDialog).width);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('Error loading record data: ' + error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function populateEditModal(record) {
|
||||||
|
document.getElementById('editRecordId').value = record.id;
|
||||||
|
document.getElementById('editOrderLine').value = record.order_line;
|
||||||
|
document.getElementById('editOrderId').value = record.order_id;
|
||||||
|
document.getElementById('editLineNumber').value = record.line_number || '';
|
||||||
|
document.getElementById('editCustomerCode').value = record.customer_code;
|
||||||
|
document.getElementById('editCustomerName').value = record.customer_name;
|
||||||
|
document.getElementById('editClientOrderLine').value = record.client_order_line || '';
|
||||||
|
document.getElementById('editArticleCode').value = record.article_code;
|
||||||
|
document.getElementById('editDescription').value = record.article_description || '';
|
||||||
|
document.getElementById('editQuantity').value = record.quantity_requested;
|
||||||
|
document.getElementById('editBalance').value = record.balance || '';
|
||||||
|
document.getElementById('editUnitOfMeasure').value = record.unit_of_measure || '';
|
||||||
|
document.getElementById('editDeliveryDate').value = record.delivery_date;
|
||||||
|
document.getElementById('editOrderDate').value = record.order_date;
|
||||||
|
document.getElementById('editOrderStatus').value = record.order_status;
|
||||||
|
document.getElementById('editArticleStatus').value = record.article_status || '';
|
||||||
|
document.getElementById('editPriority').value = record.priority || 'NORMAL';
|
||||||
|
document.getElementById('editProductGroup').value = record.product_group || '';
|
||||||
|
document.getElementById('editProductionOrder').value = record.production_order || '';
|
||||||
|
document.getElementById('editProductionStatus').value = record.production_status || '';
|
||||||
|
document.getElementById('editModel').value = record.model || '';
|
||||||
|
document.getElementById('editClosed').value = record.closed || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveRecord() {
|
||||||
|
const recordId = document.getElementById('editRecordId').value;
|
||||||
|
const data = {
|
||||||
|
order_line: document.getElementById('editOrderLine').value,
|
||||||
|
order_id: document.getElementById('editOrderId').value,
|
||||||
|
line_number: document.getElementById('editLineNumber').value,
|
||||||
|
customer_code: document.getElementById('editCustomerCode').value,
|
||||||
|
customer_name: document.getElementById('editCustomerName').value,
|
||||||
|
client_order_line: document.getElementById('editClientOrderLine').value,
|
||||||
|
article_code: document.getElementById('editArticleCode').value,
|
||||||
|
article_description: document.getElementById('editDescription').value,
|
||||||
|
quantity_requested: parseInt(document.getElementById('editQuantity').value) || 0,
|
||||||
|
balance: parseFloat(document.getElementById('editBalance').value) || null,
|
||||||
|
unit_of_measure: document.getElementById('editUnitOfMeasure').value,
|
||||||
|
delivery_date: document.getElementById('editDeliveryDate').value,
|
||||||
|
order_date: document.getElementById('editOrderDate').value,
|
||||||
|
order_status: document.getElementById('editOrderStatus').value,
|
||||||
|
article_status: document.getElementById('editArticleStatus').value,
|
||||||
|
priority: document.getElementById('editPriority').value,
|
||||||
|
product_group: document.getElementById('editProductGroup').value,
|
||||||
|
production_order: document.getElementById('editProductionOrder').value,
|
||||||
|
production_status: document.getElementById('editProductionStatus').value,
|
||||||
|
model: document.getElementById('editModel').value,
|
||||||
|
closed: document.getElementById('editClosed').value
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch(`/daily_mirror/api/tune/orders_data/${recordId}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(result => {
|
||||||
|
if (result.success) {
|
||||||
|
// Close modal
|
||||||
|
const editModal = bootstrap.Modal.getInstance(document.getElementById('editModal'));
|
||||||
|
editModal.hide();
|
||||||
|
|
||||||
|
// Reload data
|
||||||
|
loadOrdersData(currentPage);
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
alert('Order updated successfully!');
|
||||||
|
} else {
|
||||||
|
alert('Error updating order: ' + result.error);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('Error updating order: ' + error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteRecord() {
|
||||||
|
const recordId = document.getElementById('editRecordId').value;
|
||||||
|
const orderLine = document.getElementById('editOrderLine').value;
|
||||||
|
|
||||||
|
if (!confirm(`⚠️ Are you sure you want to delete order line "${orderLine}"?\n\nThis action cannot be undone.`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(`/daily_mirror/api/tune/orders_data/${recordId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.error) {
|
||||||
|
throw new Error(data.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close modal and reload data
|
||||||
|
const modal = bootstrap.Modal.getInstance(document.getElementById('editModal'));
|
||||||
|
modal.hide();
|
||||||
|
|
||||||
|
alert('Record deleted successfully!');
|
||||||
|
loadOrdersData(currentPage);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error deleting record:', error);
|
||||||
|
alert('Error deleting record: ' + error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveAllChanges() {
|
||||||
|
alert('Save All Changes functionality will be implemented for bulk operations.');
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAllOrders() {
|
||||||
|
if (!confirm('⚠️ WARNING: This will permanently delete ALL customer orders from the database!\n\nAre you absolutely sure you want to continue?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second confirmation for safety
|
||||||
|
if (!confirm('This action CANNOT be undone! All customer order data will be lost.\n\nClick OK to proceed with deletion.')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearBtn = document.getElementById('clearAllBtn');
|
||||||
|
const originalText = clearBtn.innerHTML;
|
||||||
|
clearBtn.disabled = true;
|
||||||
|
clearBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Deleting...';
|
||||||
|
|
||||||
|
fetch('/daily_mirror/clear_orders', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
alert('✅ ' + data.message);
|
||||||
|
// Reload the page to show empty table
|
||||||
|
loadOrdersData(1);
|
||||||
|
} else {
|
||||||
|
alert('❌ Error: ' + (data.message || 'Failed to clear orders'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('❌ Error clearing orders: ' + error.message);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
clearBtn.disabled = false;
|
||||||
|
clearBtn.innerHTML = originalText;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
683
py_app/app/templates/daily_mirror_tune_production.html
Normal file
683
py_app/app/templates/daily_mirror_tune_production.html
Normal file
@@ -0,0 +1,683 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Tune Production Data - Daily Mirror{% endblock %}
|
||||||
|
|
||||||
|
{% block extra_css %}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/daily_mirror_tune.css') }}">
|
||||||
|
<style>
|
||||||
|
/* Force modal width - using viewport width for maximum responsiveness */
|
||||||
|
.modal#editModal .modal-dialog {
|
||||||
|
max-width: 95vw !important;
|
||||||
|
width: 95vw !important;
|
||||||
|
margin: 1.75rem auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal#editModal .modal-dialog.modal-xl {
|
||||||
|
max-width: 95vw !important;
|
||||||
|
width: 95vw !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Override ALL Bootstrap media queries */
|
||||||
|
@media (min-width: 576px) {
|
||||||
|
.modal#editModal .modal-dialog {
|
||||||
|
max-width: 95vw !important;
|
||||||
|
width: 95vw !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
.modal#editModal .modal-dialog {
|
||||||
|
max-width: 95vw !important;
|
||||||
|
width: 95vw !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1200px) {
|
||||||
|
.modal#editModal .modal-dialog,
|
||||||
|
.modal#editModal .modal-dialog.modal-xl {
|
||||||
|
max-width: 95vw !important;
|
||||||
|
width: 95vw !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure pointer events work */
|
||||||
|
.modal#editModal .modal-dialog {
|
||||||
|
pointer-events: auto !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-fluid">
|
||||||
|
<!-- Page Header -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<h1 class="h3 mb-0">🏭 Tune Production Data</h1>
|
||||||
|
<p class="text-muted">Edit and update production orders information</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<!-- Buttons removed; now present in top header -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filters Section -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title mb-0">
|
||||||
|
<i class="fas fa-filter"></i> Filters and Search
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<label for="searchInput" class="form-label">Search</label>
|
||||||
|
<input type="text" class="form-control" id="searchInput"
|
||||||
|
placeholder="Search by order, customer, or article...">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<label for="statusFilter" class="form-label">Production Status</label>
|
||||||
|
<select class="form-control" id="statusFilter">
|
||||||
|
<option value="">All Statuses</option>
|
||||||
|
<option value="PENDING">Pending</option>
|
||||||
|
<option value="IN_PROGRESS">In Progress</option>
|
||||||
|
<option value="FINISHED">Finished</option>
|
||||||
|
<option value="CANCELLED">Cancelled</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<label for="customerFilter" class="form-label">Customer</label>
|
||||||
|
<select class="form-control" id="customerFilter">
|
||||||
|
<option value="">All Customers</option>
|
||||||
|
<!-- Will be populated dynamically -->
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<label for="recordsPerPage" class="form-label">Records per page</label>
|
||||||
|
<select class="form-control" id="recordsPerPage">
|
||||||
|
<option value="25">25</option>
|
||||||
|
<option value="50" selected>50</option>
|
||||||
|
<option value="100">100</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<button class="btn btn-primary" onclick="loadProductionData()">
|
||||||
|
<i class="fas fa-search"></i> Apply Filters
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-secondary" onclick="clearFilters()">
|
||||||
|
<i class="fas fa-times"></i> Clear
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Data Table Section -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="card-title mb-0">
|
||||||
|
<i class="fas fa-table"></i> Production Orders Data
|
||||||
|
</h5>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<span id="recordsInfo" class="text-muted me-3"></span>
|
||||||
|
{% if session.get('role') == 'superadmin' %}
|
||||||
|
<button class="btn btn-danger btn-sm me-2" onclick="clearAllProductionOrders()" id="clearAllBtn">
|
||||||
|
<i class="fas fa-trash-alt"></i> Clear All Orders
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
<button class="btn btn-success btn-sm" onclick="saveAllChanges()">
|
||||||
|
<i class="fas fa-save"></i> Save All Changes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-hover" id="productionTable">
|
||||||
|
<thead class="table-dark">
|
||||||
|
<tr>
|
||||||
|
<th>Production Order</th>
|
||||||
|
<th>Opened for Order</th>
|
||||||
|
<th>Client Order-Line</th>
|
||||||
|
<th>Customer</th>
|
||||||
|
<th>Article Code</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Quantity</th>
|
||||||
|
<th>Delivery Date</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Machine</th>
|
||||||
|
<th>Planning Date</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="productionTableBody">
|
||||||
|
<!-- Data will be loaded here -->
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading indicator -->
|
||||||
|
<div id="loadingIndicator" class="text-center py-4" style="display: none;">
|
||||||
|
<i class="fas fa-spinner fa-spin fa-2x"></i>
|
||||||
|
<p class="mt-2">Loading data...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- No data message -->
|
||||||
|
<div id="noDataMessage" class="text-center py-4" style="display: none;">
|
||||||
|
<i class="fas fa-info-circle fa-2x text-muted"></i>
|
||||||
|
<p class="mt-2 text-muted">No production orders found</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pagination -->
|
||||||
|
<div class="card-footer">
|
||||||
|
<nav aria-label="Production data pagination">
|
||||||
|
<ul class="pagination pagination-sm justify-content-center mb-0" id="pagination">
|
||||||
|
<!-- Pagination will be generated here -->
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edit Modal -->
|
||||||
|
<div class="modal fade" id="editModal" tabindex="-1" aria-labelledby="editModalLabel" aria-hidden="true" data-bs-backdrop="true" data-bs-keyboard="true">
|
||||||
|
<div class="modal-dialog modal-xl modal-dialog-scrollable" style="max-width: 95vw !important; width: 95vw !important;">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="editModalLabel">Edit Production Order</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" id="modalCloseBtn"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="editForm">
|
||||||
|
<input type="hidden" id="editRecordId">
|
||||||
|
|
||||||
|
<!-- Production Order (Full Width) -->
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<label for="editProductionOrder" class="form-label fw-bold">Production Order</label>
|
||||||
|
<input type="text" class="form-control form-control-lg" id="editProductionOrder" readonly>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-3">
|
||||||
|
|
||||||
|
<!-- Three Column Layout -->
|
||||||
|
<div class="row">
|
||||||
|
<!-- Column 1 -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editOpenForOrderLine" class="form-label">Opened for Order-Line</label>
|
||||||
|
<input type="text" class="form-control" id="editOpenForOrderLine" readonly>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editCustomerCode" class="form-label">Customer Code</label>
|
||||||
|
<input type="text" class="form-control" id="editCustomerCode">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editArticleCode" class="form-label">Article Code</label>
|
||||||
|
<input type="text" class="form-control" id="editArticleCode" readonly>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editDeliveryDate" class="form-label">Delivery Date</label>
|
||||||
|
<input type="date" class="form-control" id="editDeliveryDate">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Column 2 -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editClientOrderLine" class="form-label">Client Order-Line</label>
|
||||||
|
<input type="text" class="form-control" id="editClientOrderLine" readonly>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editCustomerName" class="form-label">Customer Name</label>
|
||||||
|
<input type="text" class="form-control" id="editCustomerName">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editQuantity" class="form-label">Quantity</label>
|
||||||
|
<input type="number" class="form-control" id="editQuantity">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editStatus" class="form-label">Production Status</label>
|
||||||
|
<select class="form-control" id="editStatus">
|
||||||
|
<option value="PENDING">Pending</option>
|
||||||
|
<option value="IN_PROGRESS">In Progress</option>
|
||||||
|
<option value="FINISHED">Finished</option>
|
||||||
|
<option value="CANCELLED">Cancelled</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Column 3 -->
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editMachine" class="form-label">Machine Code</label>
|
||||||
|
<input type="text" class="form-control" id="editMachine">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="editDescription" class="form-label">Article Description</label>
|
||||||
|
<textarea class="form-control" id="editDescription" rows="6"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer d-flex justify-content-between">
|
||||||
|
<button type="button" class="btn btn-danger" onclick="deleteRecord()" style="min-width: 150px;">
|
||||||
|
<i class="fas fa-trash"></i> Delete Record
|
||||||
|
</button>
|
||||||
|
<div>
|
||||||
|
<button type="button" class="btn btn-secondary me-2" data-bs-dismiss="modal" style="min-width: 100px;">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-primary" onclick="saveRecord()" style="min-width: 150px;">
|
||||||
|
<i class="fas fa-save"></i> Save Changes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let currentPage = 1;
|
||||||
|
let currentData = [];
|
||||||
|
let hasChanges = false;
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Initialize theme
|
||||||
|
const savedTheme = localStorage.getItem('theme');
|
||||||
|
if (savedTheme === 'dark') {
|
||||||
|
document.body.classList.add('dark-mode');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load initial data
|
||||||
|
loadProductionData();
|
||||||
|
});
|
||||||
|
|
||||||
|
function loadProductionData(page = 1) {
|
||||||
|
currentPage = page;
|
||||||
|
|
||||||
|
// Show loading
|
||||||
|
document.getElementById('loadingIndicator').style.display = 'block';
|
||||||
|
document.getElementById('productionTableBody').innerHTML = '';
|
||||||
|
document.getElementById('noDataMessage').style.display = 'none';
|
||||||
|
|
||||||
|
// Get filter values
|
||||||
|
const search = document.getElementById('searchInput').value;
|
||||||
|
const status = document.getElementById('statusFilter').value;
|
||||||
|
const customer = document.getElementById('customerFilter').value;
|
||||||
|
const perPage = document.getElementById('recordsPerPage').value;
|
||||||
|
|
||||||
|
// Build query parameters
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
page: page,
|
||||||
|
per_page: perPage
|
||||||
|
});
|
||||||
|
|
||||||
|
if (search) params.append('search', search);
|
||||||
|
if (status) params.append('status', status);
|
||||||
|
if (customer) params.append('customer', customer);
|
||||||
|
|
||||||
|
// Fetch data
|
||||||
|
fetch(`/daily_mirror/api/tune/production_data?${params}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.error) {
|
||||||
|
throw new Error(data.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentData = data.records;
|
||||||
|
renderTable(data);
|
||||||
|
renderPagination(data);
|
||||||
|
updateRecordsInfo(data);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading production data:', error);
|
||||||
|
alert('Error loading data: ' + error.message);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
document.getElementById('loadingIndicator').style.display = 'none';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTable(data) {
|
||||||
|
const tbody = document.getElementById('productionTableBody');
|
||||||
|
|
||||||
|
if (data.records.length === 0) {
|
||||||
|
document.getElementById('noDataMessage').style.display = 'block';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody.innerHTML = data.records.map((record, index) => `
|
||||||
|
<tr id="row-${record.id}">
|
||||||
|
<td><strong>${record.production_order}</strong></td>
|
||||||
|
<td>${record.open_for_order_line || ''}</td>
|
||||||
|
<td>${record.client_order_line || ''}</td>
|
||||||
|
<td>${record.customer_code}<br><small class="text-muted">${record.customer_name || ''}</small></td>
|
||||||
|
<td>${record.article_code || ''}</td>
|
||||||
|
<td><small>${record.article_description || ''}</small></td>
|
||||||
|
<td>${record.quantity_requested || ''}</td>
|
||||||
|
<td>${record.delivery_date || ''}</td>
|
||||||
|
<td><span class="badge bg-${getStatusColor(record.production_status)}">${record.production_status || ''}</span></td>
|
||||||
|
<td>${record.machine_code || ''}</td>
|
||||||
|
<td>${record.data_planificare || ''}</td>
|
||||||
|
<td>
|
||||||
|
<button class="btn btn-primary btn-action btn-sm" onclick="editRecord(${record.id})" title="Edit">
|
||||||
|
<i class="fas fa-edit"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusColor(status) {
|
||||||
|
switch(status) {
|
||||||
|
case 'PENDING': return 'warning';
|
||||||
|
case 'IN_PROGRESS': return 'info';
|
||||||
|
case 'FINISHED': return 'success';
|
||||||
|
case 'CANCELLED': return 'danger';
|
||||||
|
default: return 'secondary';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPagination(data) {
|
||||||
|
const pagination = document.getElementById('pagination');
|
||||||
|
|
||||||
|
if (data.total_pages <= 1) {
|
||||||
|
pagination.innerHTML = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let paginationHTML = '';
|
||||||
|
|
||||||
|
// Previous button
|
||||||
|
if (data.page > 1) {
|
||||||
|
paginationHTML += `<li class="page-item"><a class="page-link" href="#" onclick="loadProductionData(${data.page - 1})">Previous</a></li>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Page numbers
|
||||||
|
for (let i = Math.max(1, data.page - 2); i <= Math.min(data.total_pages, data.page + 2); i++) {
|
||||||
|
const active = i === data.page ? 'active' : '';
|
||||||
|
paginationHTML += `<li class="page-item ${active}"><a class="page-link" href="#" onclick="loadProductionData(${i})">${i}</a></li>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next button
|
||||||
|
if (data.page < data.total_pages) {
|
||||||
|
paginationHTML += `<li class="page-item"><a class="page-link" href="#" onclick="loadProductionData(${data.page + 1})">Next</a></li>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
pagination.innerHTML = paginationHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRecordsInfo(data) {
|
||||||
|
const info = document.getElementById('recordsInfo');
|
||||||
|
const start = (data.page - 1) * data.per_page + 1;
|
||||||
|
const end = Math.min(data.page * data.per_page, data.total);
|
||||||
|
info.textContent = `Showing ${start}-${end} of ${data.total} records`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function editRecord(recordId) {
|
||||||
|
const record = currentData.find(r => r.id === recordId);
|
||||||
|
if (!record) return;
|
||||||
|
|
||||||
|
// Populate the edit form
|
||||||
|
document.getElementById('editRecordId').value = record.id;
|
||||||
|
document.getElementById('editProductionOrder').value = record.production_order;
|
||||||
|
document.getElementById('editOpenForOrderLine').value = record.open_for_order_line || '';
|
||||||
|
document.getElementById('editClientOrderLine').value = record.client_order_line || '';
|
||||||
|
document.getElementById('editCustomerCode').value = record.customer_code || '';
|
||||||
|
document.getElementById('editCustomerName').value = record.customer_name || '';
|
||||||
|
document.getElementById('editArticleCode').value = record.article_code || '';
|
||||||
|
document.getElementById('editDescription').value = record.article_description || '';
|
||||||
|
document.getElementById('editQuantity').value = record.quantity_requested || '';
|
||||||
|
document.getElementById('editDeliveryDate').value = record.delivery_date || '';
|
||||||
|
document.getElementById('editStatus').value = record.production_status || '';
|
||||||
|
document.getElementById('editMachine').value = record.machine_code || '';
|
||||||
|
|
||||||
|
// Explicitly enable all editable fields
|
||||||
|
const editableFields = ['editCustomerCode',
|
||||||
|
'editCustomerName', 'editDescription', 'editQuantity',
|
||||||
|
'editDeliveryDate', 'editStatus', 'editMachine'];
|
||||||
|
editableFields.forEach(fieldId => {
|
||||||
|
const field = document.getElementById(fieldId);
|
||||||
|
if (field) {
|
||||||
|
field.disabled = false;
|
||||||
|
field.removeAttribute('disabled');
|
||||||
|
field.removeAttribute('readonly');
|
||||||
|
field.style.backgroundColor = '#ffffff';
|
||||||
|
field.style.color = '#000000';
|
||||||
|
field.style.opacity = '1';
|
||||||
|
field.style.pointerEvents = 'auto';
|
||||||
|
field.style.cursor = 'text';
|
||||||
|
field.style.userSelect = 'text';
|
||||||
|
field.tabIndex = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show the modal with proper configuration
|
||||||
|
const modalElement = document.getElementById('editModal');
|
||||||
|
const modalDialog = modalElement.querySelector('.modal-dialog');
|
||||||
|
|
||||||
|
// Remove any existing modal instances to prevent conflicts
|
||||||
|
const existingModal = bootstrap.Modal.getInstance(modalElement);
|
||||||
|
if (existingModal) {
|
||||||
|
existingModal.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
const modal = new bootstrap.Modal(modalElement, {
|
||||||
|
backdrop: true,
|
||||||
|
keyboard: true,
|
||||||
|
focus: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show the modal first
|
||||||
|
modal.show();
|
||||||
|
|
||||||
|
// Force the modal dialog width AFTER modal is shown - using 95% of viewport width
|
||||||
|
setTimeout(() => {
|
||||||
|
if (modalDialog) {
|
||||||
|
console.log('Applying width after modal shown');
|
||||||
|
modalDialog.style.setProperty('max-width', '95vw', 'important');
|
||||||
|
modalDialog.style.setProperty('width', '95vw', 'important');
|
||||||
|
modalDialog.style.setProperty('margin', '1.75rem auto', 'important');
|
||||||
|
|
||||||
|
// Also apply to modal content for good measure
|
||||||
|
const modalContent = modalDialog.querySelector('.modal-content');
|
||||||
|
if (modalContent) {
|
||||||
|
modalContent.style.setProperty('width', '100%', 'important');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Modal dialog computed width:', window.getComputedStyle(modalDialog).width);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
|
||||||
|
// Ensure form inputs are focusable and interactive after modal is shown
|
||||||
|
modalElement.addEventListener('shown.bs.modal', function () {
|
||||||
|
// Re-enable all fields after modal animation completes
|
||||||
|
editableFields.forEach(fieldId => {
|
||||||
|
const field = document.getElementById(fieldId);
|
||||||
|
if (field) {
|
||||||
|
field.disabled = false;
|
||||||
|
field.removeAttribute('disabled');
|
||||||
|
field.style.pointerEvents = 'auto';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Focus on the first editable field
|
||||||
|
const firstField = document.getElementById('editCustomerCode');
|
||||||
|
if (firstField) {
|
||||||
|
setTimeout(() => {
|
||||||
|
firstField.focus();
|
||||||
|
firstField.select();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
}, { once: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveRecord() {
|
||||||
|
const recordId = document.getElementById('editRecordId').value;
|
||||||
|
|
||||||
|
const formData = {
|
||||||
|
open_for_order_line: document.getElementById('editOpenForOrderLine').value,
|
||||||
|
client_order_line: document.getElementById('editClientOrderLine').value,
|
||||||
|
customer_code: document.getElementById('editCustomerCode').value,
|
||||||
|
customer_name: document.getElementById('editCustomerName').value,
|
||||||
|
article_code: document.getElementById('editArticleCode').value,
|
||||||
|
article_description: document.getElementById('editDescription').value,
|
||||||
|
quantity_requested: parseInt(document.getElementById('editQuantity').value) || 0,
|
||||||
|
delivery_date: document.getElementById('editDeliveryDate').value,
|
||||||
|
production_status: document.getElementById('editStatus').value,
|
||||||
|
machine_code: document.getElementById('editMachine').value
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch(`/daily_mirror/api/tune/production_data/${recordId}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(formData)
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.error) {
|
||||||
|
throw new Error(data.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close modal and reload data
|
||||||
|
const modal = bootstrap.Modal.getInstance(document.getElementById('editModal'));
|
||||||
|
modal.hide();
|
||||||
|
|
||||||
|
alert('Record updated successfully!');
|
||||||
|
loadProductionData(currentPage);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error saving record:', error);
|
||||||
|
alert('Error saving record: ' + error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteRecord() {
|
||||||
|
const recordId = document.getElementById('editRecordId').value;
|
||||||
|
const productionOrder = document.getElementById('editProductionOrder').value;
|
||||||
|
|
||||||
|
if (!confirm(`⚠️ Are you sure you want to delete production order "${productionOrder}"?\n\nThis action cannot be undone.`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(`/daily_mirror/api/tune/production_data/${recordId}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.error) {
|
||||||
|
throw new Error(data.error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close modal and reload data
|
||||||
|
const modal = bootstrap.Modal.getInstance(document.getElementById('editModal'));
|
||||||
|
modal.hide();
|
||||||
|
|
||||||
|
alert('Record deleted successfully!');
|
||||||
|
loadProductionData(currentPage);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error deleting record:', error);
|
||||||
|
alert('Error deleting record: ' + error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFilters() {
|
||||||
|
document.getElementById('searchInput').value = '';
|
||||||
|
document.getElementById('statusFilter').value = '';
|
||||||
|
document.getElementById('customerFilter').value = '';
|
||||||
|
loadProductionData(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveAllChanges() {
|
||||||
|
alert('Bulk save functionality will be implemented in a future update!');
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAllProductionOrders() {
|
||||||
|
if (!confirm('⚠️ WARNING: This will permanently delete ALL production orders from the database!\n\nAre you absolutely sure you want to continue?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Second confirmation for safety
|
||||||
|
if (!confirm('This action CANNOT be undone! All production order data will be lost.\n\nClick OK to proceed with deletion.')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clearBtn = document.getElementById('clearAllBtn');
|
||||||
|
const originalText = clearBtn.innerHTML;
|
||||||
|
clearBtn.disabled = true;
|
||||||
|
clearBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Deleting...';
|
||||||
|
|
||||||
|
fetch('/daily_mirror/clear_production_orders', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
alert('✅ ' + data.message);
|
||||||
|
// Reload the page to show empty table
|
||||||
|
loadProductionData(1);
|
||||||
|
} else {
|
||||||
|
alert('❌ Error: ' + (data.message || 'Failed to clear production orders'));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
alert('❌ Error clearing production orders: ' + error.message);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
clearBtn.disabled = false;
|
||||||
|
clearBtn.innerHTML = originalText;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listeners for real-time filtering
|
||||||
|
document.getElementById('searchInput').addEventListener('input', function() {
|
||||||
|
clearTimeout(this.searchTimeout);
|
||||||
|
this.searchTimeout = setTimeout(() => {
|
||||||
|
loadProductionData(1);
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('statusFilter').addEventListener('change', function() {
|
||||||
|
loadProductionData(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('customerFilter').addEventListener('change', function() {
|
||||||
|
loadProductionData(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('recordsPerPage').addEventListener('change', function() {
|
||||||
|
loadProductionData(1);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
@@ -50,6 +50,72 @@
|
|||||||
Recommended: Use the simplified user management for easier administration
|
Recommended: Use the simplified user management for easier administration
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if session.role in ['superadmin', 'admin'] %}
|
||||||
|
<div class="card" style="margin-top: 32px;">
|
||||||
|
<h3>💾 Database Backup Management</h3>
|
||||||
|
<p><strong>Automated Backup System:</strong> Schedule and manage database backups</p>
|
||||||
|
|
||||||
|
<!-- Backup Controls -->
|
||||||
|
<div style="margin-top: 20px; padding: 15px; background: #f5f5f5; border-radius: 8px;">
|
||||||
|
<h4 style="margin-top: 0;">Quick Actions</h4>
|
||||||
|
<button id="backup-now-btn" class="btn" style="background-color: #4caf50; color: white; margin-right: 10px;">
|
||||||
|
⚡ Backup Now
|
||||||
|
</button>
|
||||||
|
<button id="refresh-backups-btn" class="btn" style="background-color: #2196f3; color: white;">
|
||||||
|
🔄 Refresh List
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Schedule Configuration -->
|
||||||
|
<div style="margin-top: 20px; padding: 15px; background: #f9f9f9; border-radius: 8px;">
|
||||||
|
<h4 style="margin-top: 0;">Backup Schedule</h4>
|
||||||
|
<form id="backup-schedule-form" style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px;">
|
||||||
|
<div>
|
||||||
|
<label for="schedule-enabled">
|
||||||
|
<input type="checkbox" id="schedule-enabled" name="enabled"> Enable Scheduled Backups
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="schedule-time">Backup Time:</label>
|
||||||
|
<input type="time" id="schedule-time" name="time" value="02:00">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="schedule-frequency">Frequency:</label>
|
||||||
|
<select id="schedule-frequency" name="frequency">
|
||||||
|
<option value="daily">Daily</option>
|
||||||
|
<option value="weekly">Weekly</option>
|
||||||
|
<option value="monthly">Monthly</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="retention-days">Keep backups for (days):</label>
|
||||||
|
<input type="number" id="retention-days" name="retention_days" value="30" min="1" max="365">
|
||||||
|
</div>
|
||||||
|
<div style="grid-column: span 2;">
|
||||||
|
<button type="submit" class="btn" style="background-color: #ff9800; color: white;">
|
||||||
|
💾 Save Schedule
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Backup List -->
|
||||||
|
<div style="margin-top: 20px;">
|
||||||
|
<h4>Available Backups</h4>
|
||||||
|
<div id="backup-list" style="max-height: 400px; overflow-y: auto;">
|
||||||
|
<p style="text-align: center; color: #999;">Loading backups...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Backup Path Info -->
|
||||||
|
<div style="margin-top: 15px; padding: 10px; background: #e3f2fd; border-left: 4px solid #2196f3; border-radius: 4px;">
|
||||||
|
<strong>ℹ️ Backup Location:</strong> <code id="backup-path-display">/srv/quality_app/backups</code>
|
||||||
|
<br>
|
||||||
|
<small>Configure backup path in docker-compose.yml (BACKUP_PATH environment variable)</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Popup for creating/editing a user -->
|
<!-- Popup for creating/editing a user -->
|
||||||
@@ -136,5 +202,162 @@ Array.from(document.getElementsByClassName('delete-user-btn')).forEach(function(
|
|||||||
document.getElementById('close-delete-popup-btn').onclick = function() {
|
document.getElementById('close-delete-popup-btn').onclick = function() {
|
||||||
document.getElementById('delete-user-popup').style.display = 'none';
|
document.getElementById('delete-user-popup').style.display = 'none';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ========================================
|
||||||
|
// Database Backup Management Functions
|
||||||
|
// ========================================
|
||||||
|
|
||||||
|
// Load backup schedule on page load
|
||||||
|
function loadBackupSchedule() {
|
||||||
|
fetch('/api/backup/schedule')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
document.getElementById('schedule-enabled').checked = data.schedule.enabled;
|
||||||
|
document.getElementById('schedule-time').value = data.schedule.time;
|
||||||
|
document.getElementById('schedule-frequency').value = data.schedule.frequency;
|
||||||
|
document.getElementById('retention-days').value = data.schedule.retention_days;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Error loading schedule:', error));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load backup list
|
||||||
|
function loadBackupList() {
|
||||||
|
const backupList = document.getElementById('backup-list');
|
||||||
|
if (!backupList) return;
|
||||||
|
|
||||||
|
backupList.innerHTML = '<p style="text-align: center; color: #999;">Loading backups...</p>';
|
||||||
|
|
||||||
|
fetch('/api/backup/list')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success && data.backups.length > 0) {
|
||||||
|
let html = '<table style="width: 100%; border-collapse: collapse;">';
|
||||||
|
html += '<thead><tr style="background: #f0f0f0;"><th style="padding: 10px; text-align: left;">Filename</th><th>Size</th><th>Created</th><th>Actions</th></tr></thead>';
|
||||||
|
html += '<tbody>';
|
||||||
|
|
||||||
|
data.backups.forEach(backup => {
|
||||||
|
html += `<tr style="border-bottom: 1px solid #ddd;">
|
||||||
|
<td style="padding: 10px;">${backup.filename}</td>
|
||||||
|
<td style="text-align: center;">${backup.size_mb} MB</td>
|
||||||
|
<td style="text-align: center;">${backup.created}</td>
|
||||||
|
<td style="text-align: center;">
|
||||||
|
<button onclick="downloadBackup('${backup.filename}')" class="btn" style="background: #2196f3; color: white; padding: 5px 10px; margin: 2px;">⬇️ Download</button>
|
||||||
|
<button onclick="deleteBackup('${backup.filename}')" class="btn" style="background: #f44336; color: white; padding: 5px 10px; margin: 2px;">🗑️ Delete</button>
|
||||||
|
</td>
|
||||||
|
</tr>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
html += '</tbody></table>';
|
||||||
|
backupList.innerHTML = html;
|
||||||
|
} else {
|
||||||
|
backupList.innerHTML = '<p style="text-align: center; color: #999;">No backups available</p>';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading backups:', error);
|
||||||
|
backupList.innerHTML = '<p style="text-align: center; color: #f44336;">Error loading backups</p>';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backup now button
|
||||||
|
document.getElementById('backup-now-btn')?.addEventListener('click', function() {
|
||||||
|
const btn = this;
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = '⏳ Creating backup...';
|
||||||
|
|
||||||
|
fetch('/api/backup/create', {
|
||||||
|
method: 'POST'
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
alert('✅ ' + data.message + '\nFile: ' + data.filename + '\nSize: ' + data.size);
|
||||||
|
loadBackupList();
|
||||||
|
} else {
|
||||||
|
alert('❌ ' + data.message);
|
||||||
|
}
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = '⚡ Backup Now';
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error creating backup:', error);
|
||||||
|
alert('❌ Failed to create backup');
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = '⚡ Backup Now';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Refresh backups button
|
||||||
|
document.getElementById('refresh-backups-btn')?.addEventListener('click', function() {
|
||||||
|
loadBackupList();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Save schedule form
|
||||||
|
document.getElementById('backup-schedule-form')?.addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const formData = {
|
||||||
|
enabled: document.getElementById('schedule-enabled').checked,
|
||||||
|
time: document.getElementById('schedule-time').value,
|
||||||
|
frequency: document.getElementById('schedule-frequency').value,
|
||||||
|
retention_days: parseInt(document.getElementById('retention-days').value)
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch('/api/backup/schedule', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(formData)
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
alert('✅ ' + data.message);
|
||||||
|
} else {
|
||||||
|
alert('❌ ' + data.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error saving schedule:', error);
|
||||||
|
alert('❌ Failed to save schedule');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Download backup function
|
||||||
|
function downloadBackup(filename) {
|
||||||
|
window.location.href = `/api/backup/download/${filename}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete backup function
|
||||||
|
function deleteBackup(filename) {
|
||||||
|
if (confirm(`Are you sure you want to delete backup: ${filename}?`)) {
|
||||||
|
fetch(`/api/backup/delete/${filename}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
alert('✅ ' + data.message);
|
||||||
|
loadBackupList();
|
||||||
|
} else {
|
||||||
|
alert('❌ ' + data.message);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error deleting backup:', error);
|
||||||
|
alert('❌ Failed to delete backup');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load backup data on page load
|
||||||
|
if (document.getElementById('backup-list')) {
|
||||||
|
loadBackupSchedule();
|
||||||
|
loadBackupList();
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -355,6 +355,100 @@
|
|||||||
background-color: #2a2a2a !important;
|
background-color: #2a2a2a !important;
|
||||||
color: #fff !important;
|
color: #fff !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Modal fixes for accessibility */
|
||||||
|
.modal {
|
||||||
|
z-index: 1050 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-backdrop {
|
||||||
|
z-index: 1040 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme-aware modal styling */
|
||||||
|
body.light-mode .modal-content {
|
||||||
|
background-color: #fff !important;
|
||||||
|
color: #000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .modal-content {
|
||||||
|
background-color: #2a2a2a !important;
|
||||||
|
color: #fff !important;
|
||||||
|
border: 1px solid #444 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.light-mode .modal-header {
|
||||||
|
background-color: #f8f9fa !important;
|
||||||
|
border-bottom: 1px solid #dee2e6 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .modal-header {
|
||||||
|
background-color: #1e1e1e !important;
|
||||||
|
border-bottom: 1px solid #444 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.light-mode .modal-title {
|
||||||
|
color: #000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .modal-title {
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.light-mode .modal-body {
|
||||||
|
background-color: #fff !important;
|
||||||
|
color: #000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .modal-body {
|
||||||
|
background-color: #2a2a2a !important;
|
||||||
|
color: #fff !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.light-mode .modal-footer {
|
||||||
|
background-color: #f8f9fa !important;
|
||||||
|
border-top: 1px solid #dee2e6 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .modal-footer {
|
||||||
|
background-color: #1e1e1e !important;
|
||||||
|
border-top: 1px solid #444 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal form elements */
|
||||||
|
body.light-mode .modal .form-control {
|
||||||
|
background-color: #fff !important;
|
||||||
|
color: #000 !important;
|
||||||
|
border-color: #ced4da !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .modal .form-control {
|
||||||
|
background-color: #1a1a1a !important;
|
||||||
|
color: #fff !important;
|
||||||
|
border-color: #444 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.light-mode .modal label {
|
||||||
|
color: #000 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .modal label {
|
||||||
|
color: #ccc !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure modal is clickable and not greyed out */
|
||||||
|
.modal.show {
|
||||||
|
display: block !important;
|
||||||
|
pointer-events: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-dialog {
|
||||||
|
pointer-events: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
pointer-events: auto !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@@ -408,6 +502,10 @@
|
|||||||
<input type="checkbox" id="module_labels" name="modules" value="labels">
|
<input type="checkbox" id="module_labels" name="modules" value="labels">
|
||||||
<label for="module_labels">Label Management</label>
|
<label for="module_labels">Label Management</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="module-checkbox">
|
||||||
|
<input type="checkbox" id="module_daily_mirror" name="modules" value="daily_mirror">
|
||||||
|
<label for="module_daily_mirror">Daily Mirror (BI & Reports)</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="accessLevelInfo" class="access-level-info" style="display: none;"></div>
|
<div id="accessLevelInfo" class="access-level-info" style="display: none;"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -454,6 +552,10 @@
|
|||||||
<input type="checkbox" id="quick_module_labels" name="quick_modules" value="labels">
|
<input type="checkbox" id="quick_module_labels" name="quick_modules" value="labels">
|
||||||
<label for="quick_module_labels">Label Management</label>
|
<label for="quick_module_labels">Label Management</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="module-checkbox">
|
||||||
|
<input type="checkbox" id="quick_module_daily_mirror" name="quick_modules" value="daily_mirror">
|
||||||
|
<label for="quick_module_daily_mirror">Daily Mirror (BI & Reports)</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -621,6 +723,10 @@
|
|||||||
<input type="checkbox" id="edit_module_labels" name="modules" value="labels">
|
<input type="checkbox" id="edit_module_labels" name="modules" value="labels">
|
||||||
<label for="edit_module_labels">Label Management</label>
|
<label for="edit_module_labels">Label Management</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="module-checkbox">
|
||||||
|
<input type="checkbox" id="edit_module_daily_mirror" name="modules" value="daily_mirror">
|
||||||
|
<label for="edit_module_daily_mirror">Daily Mirror (BI & Reports)</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="editAccessLevelInfo" class="access-level-info" style="display: none;"></div>
|
<div id="editAccessLevelInfo" class="access-level-info" style="display: none;"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -46,8 +46,10 @@ max_requests_jitter = int(os.getenv("GUNICORN_MAX_REQUESTS_JITTER", "100"))
|
|||||||
# LOGGING CONFIGURATION
|
# LOGGING CONFIGURATION
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Docker-friendly: logs to stdout/stderr by default, but allow file logging
|
# Docker-friendly: logs to stdout/stderr by default, but allow file logging
|
||||||
accesslog = os.getenv("GUNICORN_ACCESS_LOG", "/srv/quality_recticel/logs/access.log")
|
# Automatically detect the correct log path based on current directory
|
||||||
errorlog = os.getenv("GUNICORN_ERROR_LOG", "/srv/quality_recticel/logs/error.log")
|
default_log_dir = "/srv/quality_app/logs" if "/srv/quality_app" in os.getcwd() else "/srv/quality_recticel/logs"
|
||||||
|
accesslog = os.getenv("GUNICORN_ACCESS_LOG", f"{default_log_dir}/access.log")
|
||||||
|
errorlog = os.getenv("GUNICORN_ERROR_LOG", f"{default_log_dir}/error.log")
|
||||||
|
|
||||||
# For pure Docker logging (12-factor app), use:
|
# For pure Docker logging (12-factor app), use:
|
||||||
# accesslog = "-" # stdout
|
# accesslog = "-" # stdout
|
||||||
@@ -162,4 +164,4 @@ def worker_abort(worker):
|
|||||||
|
|
||||||
def child_exit(server, worker):
|
def child_exit(server, worker):
|
||||||
"""Called just after a worker has been exited, in the master process."""
|
"""Called just after a worker has been exited, in the master process."""
|
||||||
server.log.info("👋 Worker %s exited (exit code: %s)", worker.pid, worker.tmp.last_mtime)
|
server.log.info("👋 Worker %s exited", worker.pid)
|
||||||
@@ -3,3 +3,4 @@ port=3306
|
|||||||
database_name=trasabilitate
|
database_name=trasabilitate
|
||||||
username=trasabilitate
|
username=trasabilitate
|
||||||
password=Initial01!
|
password=Initial01!
|
||||||
|
backup_path=/srv/quality_app/backups
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ Flask
|
|||||||
Flask-SSLify
|
Flask-SSLify
|
||||||
Werkzeug
|
Werkzeug
|
||||||
gunicorn
|
gunicorn
|
||||||
flask-sqlalchemy
|
|
||||||
pyodbc
|
pyodbc
|
||||||
mariadb
|
mariadb
|
||||||
reportlab
|
reportlab
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
394337
|
399048
|
||||||
|
|||||||
Reference in New Issue
Block a user