Compare commits
5 Commits
a8811b94b7
...
05394697a0
| Author | SHA1 | Date | |
|---|---|---|---|
| 05394697a0 | |||
| e257f6be18 | |||
| b8e85180c7 | |||
| c99ff70da7 | |||
| b0e17b69e7 |
152
py_app/CSS_MODULAR_GUIDE.md
Normal file
152
py_app/CSS_MODULAR_GUIDE.md
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
# CSS Modular Structure Guide
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
This guide explains how to migrate from a monolithic CSS file to a modular CSS structure for better maintainability and organization.
|
||||||
|
|
||||||
|
## New CSS Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
app/static/css/
|
||||||
|
├── base.css # Global styles, header, buttons, theme
|
||||||
|
├── login.css # Login page specific styles
|
||||||
|
├── dashboard.css # Dashboard and module cards
|
||||||
|
├── warehouse.css # Warehouse module styles
|
||||||
|
├── etichete.css # Labels/etiquette module styles (to be created)
|
||||||
|
├── quality.css # Quality module styles (to be created)
|
||||||
|
└── scan.css # Scan module styles (to be created)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation Strategy
|
||||||
|
|
||||||
|
### Phase 1: Setup Modular Structure ✅
|
||||||
|
- [x] Created `css/` directory
|
||||||
|
- [x] Created `base.css` with global styles
|
||||||
|
- [x] Created `login.css` for login page
|
||||||
|
- [x] Created `warehouse.css` for warehouse module
|
||||||
|
- [x] Updated `base.html` to include modular CSS
|
||||||
|
- [x] Updated `login.html` to use new structure
|
||||||
|
|
||||||
|
### Phase 2: Migration Plan (Next Steps)
|
||||||
|
|
||||||
|
1. **Extract module-specific styles from style.css:**
|
||||||
|
- Etiquette/Labels module → `etichete.css`
|
||||||
|
- Quality module → `quality.css`
|
||||||
|
- Scan module → `scan.css`
|
||||||
|
|
||||||
|
2. **Update templates to use modular CSS:**
|
||||||
|
```html
|
||||||
|
{% block head %}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/module-name.css') }}">
|
||||||
|
{% endblock %}
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Clean up original style.css:**
|
||||||
|
- Remove extracted styles
|
||||||
|
- Keep only legacy/common styles temporarily
|
||||||
|
- Eventually eliminate when all modules migrated
|
||||||
|
|
||||||
|
## Template Usage Pattern
|
||||||
|
|
||||||
|
### Standard Template Structure:
|
||||||
|
```html
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Page Title{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<!-- Include module-specific CSS -->
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/module-name.css') }}">
|
||||||
|
<!-- Page-specific overrides -->
|
||||||
|
<style>
|
||||||
|
/* Only use this for page-specific customizations */
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<!-- Page content -->
|
||||||
|
{% endblock %}
|
||||||
|
```
|
||||||
|
|
||||||
|
## CSS Loading Order
|
||||||
|
|
||||||
|
1. `base.css` - Global styles, header, buttons, theme
|
||||||
|
2. `style.css` - Legacy styles (temporary, for backward compatibility)
|
||||||
|
3. Module-specific CSS (e.g., `warehouse.css`)
|
||||||
|
4. Inline `<style>` blocks for page-specific overrides
|
||||||
|
|
||||||
|
## Benefits of This Structure
|
||||||
|
|
||||||
|
### 1. **Maintainability**
|
||||||
|
- Easy to find and edit module-specific styles
|
||||||
|
- Reduced conflicts between different modules
|
||||||
|
- Clear separation of concerns
|
||||||
|
|
||||||
|
### 2. **Performance**
|
||||||
|
- Only load CSS needed for specific pages
|
||||||
|
- Smaller file sizes per page
|
||||||
|
- Better caching (module CSS rarely changes)
|
||||||
|
|
||||||
|
### 3. **Team Development**
|
||||||
|
- Different developers can work on different modules
|
||||||
|
- Less merge conflicts in CSS files
|
||||||
|
- Clear ownership of styles
|
||||||
|
|
||||||
|
### 4. **Scalability**
|
||||||
|
- Easy to add new modules
|
||||||
|
- Simple to deprecate old styles
|
||||||
|
- Clear migration path
|
||||||
|
|
||||||
|
## Migration Checklist
|
||||||
|
|
||||||
|
### For Each Template:
|
||||||
|
- [ ] Identify module/page type
|
||||||
|
- [ ] Extract relevant styles to module CSS file
|
||||||
|
- [ ] Update template to include module CSS
|
||||||
|
- [ ] Test styling works correctly
|
||||||
|
- [ ] Remove old styles from style.css
|
||||||
|
|
||||||
|
### Current Status:
|
||||||
|
- [x] Login page - Fully migrated
|
||||||
|
- [x] Warehouse module - Partially migrated (create_locations.html updated)
|
||||||
|
- [ ] Dashboard - CSS created, templates need updating
|
||||||
|
- [ ] Etiquette module - Needs CSS extraction
|
||||||
|
- [ ] Quality module - Needs CSS extraction
|
||||||
|
- [ ] Scan module - Needs CSS extraction
|
||||||
|
|
||||||
|
## Example: Migrating a Template
|
||||||
|
|
||||||
|
### Before:
|
||||||
|
```html
|
||||||
|
{% block head %}
|
||||||
|
<style>
|
||||||
|
.my-module-specific-class {
|
||||||
|
/* styles here */
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
```
|
||||||
|
|
||||||
|
### After:
|
||||||
|
1. Move styles to `css/module-name.css`
|
||||||
|
2. Update template:
|
||||||
|
```html
|
||||||
|
{% block head %}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/module-name.css') }}">
|
||||||
|
{% endblock %}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
1. **Use semantic naming:** `warehouse.css`, `login.css`, not `page1.css`
|
||||||
|
2. **Keep base.css minimal:** Only truly global styles
|
||||||
|
3. **Avoid deep nesting:** Keep CSS selectors simple
|
||||||
|
4. **Use consistent naming:** Follow existing patterns
|
||||||
|
5. **Document changes:** Update this guide when adding new modules
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. Extract etiquette module styles to `etichete.css`
|
||||||
|
2. Update all etiquette templates to use new CSS
|
||||||
|
3. Extract quality module styles to `quality.css`
|
||||||
|
4. Extract scan module styles to `scan.css`
|
||||||
|
5. Gradually remove migrated styles from `style.css`
|
||||||
|
6. Eventually remove `style.css` dependency from `base.html`
|
||||||
133
py_app/MOBILE_LOGIN_GUIDE.md
Normal file
133
py_app/MOBILE_LOGIN_GUIDE.md
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
# Mobile-Responsive Login Page
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
The login page has been enhanced with comprehensive mobile-responsive CSS to provide an optimal user experience across all device types and screen sizes.
|
||||||
|
|
||||||
|
## Mobile-Responsive Features Added
|
||||||
|
|
||||||
|
### 1. **Responsive Breakpoints**
|
||||||
|
- **Tablet (≤768px)**: Column layout, optimized logo and form sizing
|
||||||
|
- **Mobile (≤480px)**: Enhanced touch targets, better spacing
|
||||||
|
- **Small Mobile (≤320px)**: Minimal padding, compact design
|
||||||
|
- **Landscape (height ≤500px)**: Horizontal layout for landscape phones
|
||||||
|
|
||||||
|
### 2. **Layout Adaptations**
|
||||||
|
|
||||||
|
#### Desktop (>768px)
|
||||||
|
- Side-by-side logo and form layout
|
||||||
|
- Large logo (90vh height)
|
||||||
|
- Fixed form width (600px)
|
||||||
|
|
||||||
|
#### Tablet (≤768px)
|
||||||
|
- Vertical stacked layout
|
||||||
|
- Logo height reduced to 30vh
|
||||||
|
- Form width becomes responsive (100%, max 400px)
|
||||||
|
|
||||||
|
#### Mobile (≤480px)
|
||||||
|
- Optimized touch targets (44px minimum)
|
||||||
|
- Increased padding and margins
|
||||||
|
- Better visual hierarchy
|
||||||
|
- Enhanced shadows and border radius
|
||||||
|
|
||||||
|
#### Small Mobile (≤320px)
|
||||||
|
- Minimal padding to maximize space
|
||||||
|
- Compact logo (20vh height)
|
||||||
|
- Reduced font sizes where appropriate
|
||||||
|
|
||||||
|
### 3. **Touch Optimizations**
|
||||||
|
|
||||||
|
#### iOS/Safari Specific
|
||||||
|
- `font-size: 16px` on inputs prevents automatic zoom
|
||||||
|
- Proper touch target sizing (44px minimum)
|
||||||
|
|
||||||
|
#### Touch Device Enhancements
|
||||||
|
- Active states for button presses
|
||||||
|
- Optimized image rendering for high DPI screens
|
||||||
|
- Hover effects disabled on touch devices
|
||||||
|
|
||||||
|
### 4. **Accessibility Improvements**
|
||||||
|
- Proper contrast ratios maintained
|
||||||
|
- Touch targets meet accessibility guidelines
|
||||||
|
- Readable font sizes across all devices
|
||||||
|
- Smooth transitions and animations
|
||||||
|
|
||||||
|
### 5. **Performance Considerations**
|
||||||
|
- CSS-only responsive design (no JavaScript required)
|
||||||
|
- Efficient media queries
|
||||||
|
- Optimized image rendering for retina displays
|
||||||
|
|
||||||
|
## Key CSS Features
|
||||||
|
|
||||||
|
### Flexible Layout
|
||||||
|
```css
|
||||||
|
.login-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column; /* Mobile */
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Responsive Images
|
||||||
|
```css
|
||||||
|
.login-logo {
|
||||||
|
max-height: 25vh; /* Mobile */
|
||||||
|
max-width: 85vw;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Touch-Friendly Inputs
|
||||||
|
```css
|
||||||
|
.form-container input {
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 16px; /* Prevents iOS zoom */
|
||||||
|
min-height: 44px; /* Touch target size */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Landscape Optimization
|
||||||
|
```css
|
||||||
|
@media screen and (max-height: 500px) and (orientation: landscape) {
|
||||||
|
.login-page {
|
||||||
|
flex-direction: row; /* Back to horizontal */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Recommendations
|
||||||
|
|
||||||
|
### Device Testing
|
||||||
|
- [ ] iPhone (various sizes)
|
||||||
|
- [ ] Android phones (various sizes)
|
||||||
|
- [ ] iPad/Android tablets
|
||||||
|
- [ ] Desktop browsers with responsive mode
|
||||||
|
|
||||||
|
### Orientation Testing
|
||||||
|
- [ ] Portrait mode on all devices
|
||||||
|
- [ ] Landscape mode on phones
|
||||||
|
- [ ] Landscape mode on tablets
|
||||||
|
|
||||||
|
### Browser Testing
|
||||||
|
- [ ] Safari (iOS)
|
||||||
|
- [ ] Chrome (Android/iOS)
|
||||||
|
- [ ] Firefox Mobile
|
||||||
|
- [ ] Samsung Internet
|
||||||
|
- [ ] Desktop browsers (Chrome, Firefox, Safari, Edge)
|
||||||
|
|
||||||
|
## Browser Support
|
||||||
|
- Modern browsers (ES6+ support)
|
||||||
|
- iOS Safari 12+
|
||||||
|
- Android Chrome 70+
|
||||||
|
- Desktop browsers (last 2 versions)
|
||||||
|
|
||||||
|
## Performance Impact
|
||||||
|
- **CSS Size**: Increased by ~2KB (compressed)
|
||||||
|
- **Load Time**: No impact (CSS only)
|
||||||
|
- **Rendering**: Optimized for mobile GPUs
|
||||||
|
- **Memory**: Minimal additional usage
|
||||||
|
|
||||||
|
## Future Enhancements
|
||||||
|
1. **Dark mode mobile optimizations**
|
||||||
|
2. **Progressive Web App (PWA) features**
|
||||||
|
3. **Biometric authentication UI**
|
||||||
|
4. **Loading states and animations**
|
||||||
|
5. **Error message responsive design**
|
||||||
BIN
py_app/app/__pycache__/__init__.cpython-312.pyc
Executable file → Normal file
BIN
py_app/app/__pycache__/__init__.cpython-312.pyc
Executable file → Normal file
Binary file not shown.
BIN
py_app/app/__pycache__/models.cpython-312.pyc
Executable file → Normal file
BIN
py_app/app/__pycache__/models.cpython-312.pyc
Executable file → Normal file
Binary file not shown.
BIN
py_app/app/__pycache__/permissions.cpython-312.pyc
Executable file → Normal file
BIN
py_app/app/__pycache__/permissions.cpython-312.pyc
Executable file → Normal file
Binary file not shown.
BIN
py_app/app/__pycache__/routes.cpython-312.pyc
Executable file → Normal file
BIN
py_app/app/__pycache__/routes.cpython-312.pyc
Executable file → Normal file
Binary file not shown.
BIN
py_app/app/__pycache__/settings.cpython-312.pyc
Executable file → Normal file
BIN
py_app/app/__pycache__/settings.cpython-312.pyc
Executable file → Normal file
Binary file not shown.
BIN
py_app/app/__pycache__/warehouse.cpython-312.pyc
Executable file → Normal file
BIN
py_app/app/__pycache__/warehouse.cpython-312.pyc
Executable file → Normal file
Binary file not shown.
41
py_app/app/db_create_scripts/create_scanfg_orders.py
Normal file
41
py_app/app/db_create_scripts/create_scanfg_orders.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import mariadb
|
||||||
|
|
||||||
|
# Database connection credentials
|
||||||
|
# (reuse from create_scan_1db.py or update as needed)
|
||||||
|
db_config = {
|
||||||
|
"user": "trasabilitate",
|
||||||
|
"password": "Initial01!",
|
||||||
|
"host": "localhost",
|
||||||
|
"database": "trasabilitate_database"
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn = mariadb.connect(**db_config)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
print("Connected to the database successfully!")
|
||||||
|
|
||||||
|
# Create the scanfg_orders table (same structure as scan1_orders)
|
||||||
|
create_table_query = """
|
||||||
|
CREATE TABLE IF NOT EXISTS scanfg_orders (
|
||||||
|
Id INT AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
operator_code VARCHAR(4) NOT NULL,
|
||||||
|
CP_full_code VARCHAR(15) NOT NULL UNIQUE,
|
||||||
|
OC1_code VARCHAR(4) NOT NULL,
|
||||||
|
OC2_code VARCHAR(4) NOT NULL,
|
||||||
|
CP_base_code VARCHAR(10) GENERATED ALWAYS AS (LEFT(CP_full_code, 10)) STORED,
|
||||||
|
quality_code INT(3) NOT NULL,
|
||||||
|
date DATE NOT NULL,
|
||||||
|
time TIME NOT NULL,
|
||||||
|
approved_quantity INT DEFAULT 0,
|
||||||
|
rejected_quantity INT DEFAULT 0
|
||||||
|
);
|
||||||
|
"""
|
||||||
|
cursor.execute(create_table_query)
|
||||||
|
print("Table 'scanfg_orders' created successfully!")
|
||||||
|
|
||||||
|
conn.commit()
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
except mariadb.Error as e:
|
||||||
|
print(f"Error connecting to the database: {e}")
|
||||||
73
py_app/app/db_create_scripts/create_triggers_fg.py
Normal file
73
py_app/app/db_create_scripts/create_triggers_fg.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import mariadb
|
||||||
|
|
||||||
|
# Database connection credentials
|
||||||
|
db_config = {
|
||||||
|
"user": "trasabilitate",
|
||||||
|
"password": "Initial01!",
|
||||||
|
"host": "localhost",
|
||||||
|
"database": "trasabilitate_database"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Connect to the database
|
||||||
|
try:
|
||||||
|
conn = mariadb.connect(**db_config)
|
||||||
|
cursor = conn.cursor()
|
||||||
|
print("Connected to the database successfully!")
|
||||||
|
|
||||||
|
# Delete old triggers if they exist
|
||||||
|
try:
|
||||||
|
cursor.execute("DROP TRIGGER IF EXISTS increment_approved_quantity_fg;")
|
||||||
|
print("Old trigger 'increment_approved_quantity_fg' deleted successfully.")
|
||||||
|
except mariadb.Error as e:
|
||||||
|
print(f"Error deleting old trigger 'increment_approved_quantity_fg': {e}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
cursor.execute("DROP TRIGGER IF EXISTS increment_rejected_quantity_fg;")
|
||||||
|
print("Old trigger 'increment_rejected_quantity_fg' deleted successfully.")
|
||||||
|
except mariadb.Error as e:
|
||||||
|
print(f"Error deleting old trigger 'increment_rejected_quantity_fg': {e}")
|
||||||
|
|
||||||
|
# Create corrected trigger for approved_quantity in scanfg_orders
|
||||||
|
create_approved_trigger_fg = """
|
||||||
|
CREATE TRIGGER increment_approved_quantity_fg
|
||||||
|
BEFORE INSERT ON scanfg_orders
|
||||||
|
FOR EACH ROW
|
||||||
|
BEGIN
|
||||||
|
IF NEW.quality_code = 000 THEN
|
||||||
|
SET NEW.approved_quantity = (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM scanfg_orders
|
||||||
|
WHERE CP_base_code = NEW.CP_base_code AND quality_code = 000
|
||||||
|
) + 1;
|
||||||
|
SET NEW.rejected_quantity = (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM scanfg_orders
|
||||||
|
WHERE CP_base_code = NEW.CP_base_code AND quality_code != 000
|
||||||
|
);
|
||||||
|
ELSE
|
||||||
|
SET NEW.approved_quantity = (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM scanfg_orders
|
||||||
|
WHERE CP_base_code = NEW.CP_base_code AND quality_code = 000
|
||||||
|
);
|
||||||
|
SET NEW.rejected_quantity = (
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM scanfg_orders
|
||||||
|
WHERE CP_base_code = NEW.CP_base_code AND quality_code != 000
|
||||||
|
) + 1;
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
"""
|
||||||
|
cursor.execute(create_approved_trigger_fg)
|
||||||
|
print("Trigger 'increment_approved_quantity_fg' created successfully for scanfg_orders table!")
|
||||||
|
|
||||||
|
# Commit changes and close the connection
|
||||||
|
conn.commit()
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
print("\n✅ All triggers for scanfg_orders table created successfully!")
|
||||||
|
print("The approved_quantity and rejected_quantity will now be calculated automatically.")
|
||||||
|
|
||||||
|
except mariadb.Error as e:
|
||||||
|
print(f"Error connecting to the database or creating triggers: {e}")
|
||||||
@@ -7,7 +7,6 @@ from .models import User
|
|||||||
from . import db
|
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
|
||||||
from flask import Blueprint, render_template, request, redirect, url_for, flash
|
|
||||||
import csv
|
import csv
|
||||||
from .warehouse import add_location
|
from .warehouse import add_location
|
||||||
from app.settings import (
|
from app.settings import (
|
||||||
@@ -27,6 +26,12 @@ from .print_module import get_unprinted_orders_data
|
|||||||
bp = Blueprint('main', __name__)
|
bp = Blueprint('main', __name__)
|
||||||
warehouse_bp = Blueprint('warehouse', __name__)
|
warehouse_bp = Blueprint('warehouse', __name__)
|
||||||
|
|
||||||
|
@bp.route('/main_scan')
|
||||||
|
def main_scan():
|
||||||
|
if 'role' not in session or session['role'] not in ['superadmin', 'admin', 'administrator', 'scan']:
|
||||||
|
flash('Access denied: Scan users only.')
|
||||||
|
return redirect(url_for('main.dashboard'))
|
||||||
|
return render_template('main_page_scan.html')
|
||||||
|
|
||||||
@bp.route('/', methods=['GET', 'POST'])
|
@bp.route('/', methods=['GET', 'POST'])
|
||||||
def login():
|
def login():
|
||||||
@@ -294,6 +299,79 @@ def logout():
|
|||||||
session.pop('role', None)
|
session.pop('role', None)
|
||||||
return redirect(url_for('main.login'))
|
return redirect(url_for('main.login'))
|
||||||
|
|
||||||
|
# Finish Goods Scan Route
|
||||||
|
@bp.route('/fg_scan', methods=['GET', 'POST'])
|
||||||
|
def fg_scan():
|
||||||
|
if 'role' not in session or session['role'] not in ['superadmin', 'administrator', 'admin', 'scan']:
|
||||||
|
flash('Access denied: Scan users only.')
|
||||||
|
return redirect(url_for('main.dashboard'))
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
# Handle form submission
|
||||||
|
operator_code = request.form.get('operator_code')
|
||||||
|
cp_code = request.form.get('cp_code')
|
||||||
|
oc1_code = request.form.get('oc1_code')
|
||||||
|
oc2_code = request.form.get('oc2_code')
|
||||||
|
defect_code = request.form.get('defect_code')
|
||||||
|
date = request.form.get('date')
|
||||||
|
time = request.form.get('time')
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Connect to the database
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Check if the CP_full_code already exists in scanfg_orders
|
||||||
|
cursor.execute("SELECT Id FROM scanfg_orders WHERE CP_full_code = ?", (cp_code,))
|
||||||
|
existing_entry = cursor.fetchone()
|
||||||
|
|
||||||
|
if existing_entry:
|
||||||
|
# Update the existing entry
|
||||||
|
update_query = """
|
||||||
|
UPDATE scanfg_orders
|
||||||
|
SET operator_code = ?, OC1_code = ?, OC2_code = ?, quality_code = ?, date = ?, time = ?
|
||||||
|
WHERE CP_full_code = ?
|
||||||
|
"""
|
||||||
|
cursor.execute(update_query, (operator_code, oc1_code, oc2_code, defect_code, date, time, cp_code))
|
||||||
|
flash('Existing entry updated successfully.')
|
||||||
|
else:
|
||||||
|
# Insert a new entry
|
||||||
|
insert_query = """
|
||||||
|
INSERT INTO scanfg_orders (operator_code, CP_full_code, OC1_code, OC2_code, quality_code, date, time)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||||
|
"""
|
||||||
|
cursor.execute(insert_query, (operator_code, cp_code, oc1_code, oc2_code, defect_code, date, time))
|
||||||
|
flash('New entry inserted successfully.')
|
||||||
|
|
||||||
|
# Commit the transaction
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
except mariadb.Error as e:
|
||||||
|
print(f"Error saving finish goods scan data: {e}")
|
||||||
|
flash(f"Error saving scan data: {e}")
|
||||||
|
|
||||||
|
# Fetch the latest scan data for display from scanfg_orders
|
||||||
|
scan_data = []
|
||||||
|
try:
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("""
|
||||||
|
SELECT Id, operator_code, CP_base_code, OC1_code, OC2_code, quality_code, date, time, approved_quantity, rejected_quantity
|
||||||
|
FROM scanfg_orders
|
||||||
|
ORDER BY Id DESC
|
||||||
|
LIMIT 15
|
||||||
|
""")
|
||||||
|
raw_scan_data = cursor.fetchall()
|
||||||
|
# Apply formatting to scan data for consistent date display
|
||||||
|
scan_data = [[format_cell_data(cell) for cell in row] for row in raw_scan_data]
|
||||||
|
conn.close()
|
||||||
|
except mariadb.Error as e:
|
||||||
|
print(f"Error fetching finish goods scan data: {e}")
|
||||||
|
flash(f"Error fetching scan data: {e}")
|
||||||
|
|
||||||
|
return render_template('fg_scan.html', scan_data=scan_data)
|
||||||
|
|
||||||
@bp.route('/create_user', methods=['POST'])
|
@bp.route('/create_user', methods=['POST'])
|
||||||
def create_user():
|
def create_user():
|
||||||
return create_user_handler()
|
return create_user_handler()
|
||||||
@@ -1143,8 +1221,6 @@ For support, contact your system administrator.
|
|||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error creating extension package: {e}")
|
print(f"Error creating extension package: {e}")
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': False,
|
'success': False,
|
||||||
'error': str(e)
|
'error': str(e)
|
||||||
@@ -1365,7 +1441,6 @@ Installation Time: ~5 minutes
|
|||||||
Maintenance Required: Zero (auto-starts with Windows)
|
Maintenance Required: Zero (auto-starts with Windows)
|
||||||
|
|
||||||
Ready to use immediately after installation!"""
|
Ready to use immediately after installation!"""
|
||||||
|
|
||||||
zipf.writestr('INSTALLATION_README.txt', installation_readme)
|
zipf.writestr('INSTALLATION_README.txt', installation_readme)
|
||||||
files_added += 1
|
files_added += 1
|
||||||
|
|
||||||
@@ -1425,7 +1500,7 @@ def create_zero_dependency_service_package():
|
|||||||
if not os.path.exists(service_dir):
|
if not os.path.exists(service_dir):
|
||||||
return jsonify({
|
return jsonify({
|
||||||
'success': False,
|
'success': False,
|
||||||
'error': f'Windows service directory not found: {service_dir}'
|
'error': f'Windows service directory not found: {service_dir}'
|
||||||
}), 500
|
}), 500
|
||||||
|
|
||||||
# Create static directory if it doesn't exist
|
# Create static directory if it doesn't exist
|
||||||
@@ -1715,6 +1790,9 @@ This package contains EVERYTHING needed to run the Quality Print Service:
|
|||||||
Quality Web App → Chrome Extension → Windows Service → Printer
|
Quality Web App → Chrome Extension → Windows Service → Printer
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Printing Methods (automatic fallback):**
|
||||||
|
1. Adobe Reader (silent printing)
|
||||||
|
|
||||||
**Printing Methods (automatic fallback):**
|
**Printing Methods (automatic fallback):**
|
||||||
1. Adobe Reader (silent printing)
|
1. Adobe Reader (silent printing)
|
||||||
2. SumatraPDF (if Adobe unavailable)
|
2. SumatraPDF (if Adobe unavailable)
|
||||||
@@ -2382,6 +2460,41 @@ def generate_location_label_pdf():
|
|||||||
from app.warehouse import generate_location_label_pdf
|
from app.warehouse import generate_location_label_pdf
|
||||||
return generate_location_label_pdf()
|
return generate_location_label_pdf()
|
||||||
|
|
||||||
|
@warehouse_bp.route('/update_location', methods=['POST'])
|
||||||
|
def update_location():
|
||||||
|
from app.warehouse import update_location
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
location_id = data.get('location_id')
|
||||||
|
location_code = data.get('location_code')
|
||||||
|
size = data.get('size')
|
||||||
|
description = data.get('description')
|
||||||
|
|
||||||
|
if not location_id or not location_code:
|
||||||
|
return jsonify({'success': False, 'error': 'Location ID and code are required'})
|
||||||
|
|
||||||
|
result = update_location(location_id, location_code, size, description)
|
||||||
|
return jsonify(result)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'success': False, 'error': str(e)})
|
||||||
|
|
||||||
|
@warehouse_bp.route('/delete_location', methods=['POST'])
|
||||||
|
def delete_location():
|
||||||
|
from app.warehouse import delete_location_by_id
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
location_id = data.get('location_id')
|
||||||
|
|
||||||
|
if not location_id:
|
||||||
|
return jsonify({'success': False, 'error': 'Location ID is required'})
|
||||||
|
|
||||||
|
result = delete_location_by_id(location_id)
|
||||||
|
return jsonify(result)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({'success': False, 'error': str(e)})
|
||||||
|
|
||||||
|
|
||||||
# NOTE for frontend/extension developers:
|
# NOTE for frontend/extension developers:
|
||||||
# To print labels, call the Chrome extension and pass the PDF URL:
|
# To print labels, call the Chrome extension and pass the PDF URL:
|
||||||
|
|||||||
149
py_app/app/static/css/base.css
Normal file
149
py_app/app/static/css/base.css
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
/* Global Base Styles - Common across all pages */
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background-color: #f4f4f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
width: 100vw;
|
||||||
|
max-width: 100vw;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: none;
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header Styles */
|
||||||
|
header {
|
||||||
|
padding: 10px 20px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-header .logo {
|
||||||
|
width: 60px;
|
||||||
|
height: auto;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-header .page-title {
|
||||||
|
font-size: 1.5em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-header .user-info {
|
||||||
|
margin-right: 15px;
|
||||||
|
font-size: 1em;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-header .logout-button {
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 1em;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-header .logout-button:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Common Button Styles */
|
||||||
|
.btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background-color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success:hover {
|
||||||
|
background-color: #218838;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background-color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger:hover {
|
||||||
|
background-color: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-info {
|
||||||
|
background-color: #17a2b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-info:hover {
|
||||||
|
background-color: #138496;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme Toggle */
|
||||||
|
.theme-toggle {
|
||||||
|
background-color: #6c757d;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-right: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle:hover {
|
||||||
|
background-color: #5a6268;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark Mode Base Styles */
|
||||||
|
body.dark-mode {
|
||||||
|
background-color: #121212;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode header {
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .user-info {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
67
py_app/app/static/css/dashboard.css
Normal file
67
py_app/app/static/css/dashboard.css
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
/* Dashboard and General Page Styles */
|
||||||
|
|
||||||
|
.dashboard-container {
|
||||||
|
padding: 20px;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-card h3 {
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
color: #333;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-card p {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-card a {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 8px 16px;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.module-card a:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode for dashboard */
|
||||||
|
body.dark-mode .module-card {
|
||||||
|
background: #2d2d2d;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .module-card h3 {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .module-card p {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
216
py_app/app/static/css/login.css
Normal file
216
py_app/app/static/css/login.css
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
/* Login Page Specific Styles */
|
||||||
|
|
||||||
|
.login-page {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 100vh;
|
||||||
|
background-color: #f4f4f9;
|
||||||
|
padding: 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-container {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-logo {
|
||||||
|
max-height: 90vh;
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container {
|
||||||
|
width: 600px;
|
||||||
|
background: #fff;
|
||||||
|
padding: 15px 30px;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||||
|
margin: 0;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container h2 {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container label {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container input {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 16px; /* Prevents zoom on iOS */
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container button {
|
||||||
|
padding: 10px;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container button:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide the header only on the login page */
|
||||||
|
body.light-mode.login-page header,
|
||||||
|
body.dark-mode.login-page header {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Responsive Design */
|
||||||
|
|
||||||
|
/* Tablet styles (768px and down) */
|
||||||
|
@media screen and (max-width: 768px) {
|
||||||
|
.login-page {
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px 15px;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-container {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-logo {
|
||||||
|
max-height: 30vh;
|
||||||
|
max-width: 80vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
padding: 20px 25px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile styles (480px and down) */
|
||||||
|
@media screen and (max-width: 480px) {
|
||||||
|
.login-page {
|
||||||
|
padding: 15px 10px;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-logo {
|
||||||
|
max-height: 25vh;
|
||||||
|
max-width: 85vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container {
|
||||||
|
padding: 20px 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container h2 {
|
||||||
|
font-size: 1.5em;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container input {
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container button {
|
||||||
|
padding: 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
border-radius: 5px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container label {
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Small mobile styles (320px and down) */
|
||||||
|
@media screen and (max-width: 320px) {
|
||||||
|
.login-page {
|
||||||
|
padding: 10px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-logo {
|
||||||
|
max-height: 20vh;
|
||||||
|
max-width: 90vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container {
|
||||||
|
padding: 15px;
|
||||||
|
width: calc(100% - 16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container h2 {
|
||||||
|
font-size: 1.3em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Landscape orientation adjustments */
|
||||||
|
@media screen and (max-height: 500px) and (orientation: landscape) {
|
||||||
|
.login-page {
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-container {
|
||||||
|
flex: 1;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-logo {
|
||||||
|
max-height: 80vh;
|
||||||
|
max-width: 40vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 350px;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Touch device optimizations */
|
||||||
|
@media (hover: none) and (pointer: coarse) {
|
||||||
|
.form-container input {
|
||||||
|
min-height: 44px; /* Apple's recommended touch target size */
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container button {
|
||||||
|
min-height: 44px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-container button:active {
|
||||||
|
background-color: #004494;
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* High DPI displays */
|
||||||
|
@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) {
|
||||||
|
.login-logo {
|
||||||
|
image-rendering: -webkit-optimize-contrast;
|
||||||
|
image-rendering: crisp-edges;
|
||||||
|
}
|
||||||
|
}
|
||||||
50
py_app/app/static/css/scan.css
Normal file
50
py_app/app/static/css/scan.css
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/* Scan Module Specific Styles */
|
||||||
|
|
||||||
|
.scan-form-card {
|
||||||
|
width: 380px;
|
||||||
|
max-width: 380px;
|
||||||
|
margin: 0 auto 20px auto;
|
||||||
|
max-height: 660px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 18px 18px 12px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scan-form-card form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scan-form-card label {
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 1px;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scan-form-card input[type="text"] {
|
||||||
|
padding: 5px 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scan-table-card {
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scan-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 20px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scan-table th, .scan-table td {
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
padding: 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scan-table th {
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
739
py_app/app/static/css/warehouse.css
Normal file
739
py_app/app/static/css/warehouse.css
Normal file
@@ -0,0 +1,739 @@
|
|||||||
|
/* Make the warehouse locations table scrollable within the card */
|
||||||
|
.warehouse-table-scroll {
|
||||||
|
max-height: 520px; /* fits inside 640px card with header and hint */
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure .main-content is full width for warehouse pages */
|
||||||
|
/* Fix horizontal overflow: use 100% width, not 100vw */
|
||||||
|
.main-content {
|
||||||
|
width: 100% !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
background: transparent !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
/* Normalize box-sizing for all elements */
|
||||||
|
*, *::before, *::after {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
/* WAREHOUSE PAGE CSS - FRESH START */
|
||||||
|
|
||||||
|
/* Reset and override all base styles for warehouse pages */
|
||||||
|
.main-content {
|
||||||
|
width: 100% !important;
|
||||||
|
height: calc(100vh - 60px) !important; /* Full height minus header */
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 10px !important; /* 10px margin from page edges */
|
||||||
|
background: #f4f4f9 !important; /* Match body background */
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
display: flex !important;
|
||||||
|
flex-direction: column !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main warehouse container - full width with 10px padding */
|
||||||
|
.warehouse-page-container {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
gap: 10px; /* 10px between containers */
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The 3 main containers with 1:2:1 ratio */
|
||||||
|
.warehouse-container-1 {
|
||||||
|
flex: 1; /* 1 part */
|
||||||
|
background: #f8fafc; /* Theme card background */
|
||||||
|
color: #1e293b; /* Theme text color */
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow-y: auto;
|
||||||
|
align-self: flex-start;
|
||||||
|
max-height: 640px;
|
||||||
|
min-height: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warehouse-container-2 {
|
||||||
|
flex: 2; /* 2 parts */
|
||||||
|
background: #f8fafc; /* Theme card background */
|
||||||
|
color: #1e293b; /* Theme text color */
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 640px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warehouse-container-3 {
|
||||||
|
flex: 1; /* 1 part */
|
||||||
|
background: #f8fafc; /* Theme card background */
|
||||||
|
color: #1e293b; /* Theme text color */
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 640px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Form and table styles for warehouse components */
|
||||||
|
form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px; /* Reduced gap for compactness */
|
||||||
|
}
|
||||||
|
|
||||||
|
form label {
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 2px; /* Reduced margin for compactness */
|
||||||
|
}
|
||||||
|
|
||||||
|
form input, form select {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #cbd5e1;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
background: #e2e8f0; /* Theme input background */
|
||||||
|
color: #1e293b; /* Theme input text */
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: #fff;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-success {
|
||||||
|
background-color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-info {
|
||||||
|
background-color: #17a2b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background-color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table styles */
|
||||||
|
.scan-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scan-table th, .scan-table td {
|
||||||
|
border: 1px solid #cbd5e1; /* Theme border color */
|
||||||
|
padding: 8px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scan-table th {
|
||||||
|
background-color: #e2e8f0; /* Theme input background for headers */
|
||||||
|
font-weight: bold;
|
||||||
|
color: #334155; /* Theme label color */
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-row {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-row:hover {
|
||||||
|
background-color: #e2e8f0; /* Theme hover background */
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-row.selected {
|
||||||
|
background-color: #ffb300 !important;
|
||||||
|
color: #222 !important;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Edit/Delete card styles */
|
||||||
|
.edit-delete-card {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-message {
|
||||||
|
color: #64748b; /* Slightly lighter theme text */
|
||||||
|
font-style: italic;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selection-info {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Barcode preview */
|
||||||
|
#barcode-label-preview {
|
||||||
|
width: 8cm;
|
||||||
|
height: 4cm;
|
||||||
|
border: 2px solid #333;
|
||||||
|
background: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 10px auto;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#barcode-label-preview .location-text {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal styles */
|
||||||
|
.modal {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1000;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0,0,0,0.5);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||||||
|
width: 90%;
|
||||||
|
max-width: 500px;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close:hover {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-table td {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-table td:first-child {
|
||||||
|
width: 30%;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
html, body {
|
||||||
|
width: 100% !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
.scan-container {
|
||||||
|
width: 100vw !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
display: block !important; /* Override any grid from style.css */
|
||||||
|
grid-template-columns: unset !important; /* Remove any grid columns */
|
||||||
|
gap: unset !important; /* Remove any gap */
|
||||||
|
max-width: 100vw !important; /* Override max-width from style.css */
|
||||||
|
box-shadow: none !important; /* Remove box shadow */
|
||||||
|
border-radius: 0 !important; /* Remove border radius */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Full-width container for warehouse pages - avoids scan-container restrictions */
|
||||||
|
.warehouse-full-container {
|
||||||
|
width: 100% !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
display: block !important;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Container card with left/right margins */
|
||||||
|
.warehouse-container-card {
|
||||||
|
margin-left: 15px;
|
||||||
|
margin-right: 15px;
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
width: calc(100% - 30px);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 3-column layout for warehouse create locations */
|
||||||
|
.warehouse-grid {
|
||||||
|
display: grid !important;
|
||||||
|
grid-template-columns: 1fr 2fr 1fr !important;
|
||||||
|
gap: 20px;
|
||||||
|
margin: 0;
|
||||||
|
min-height: auto;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make cards equal height in grid with higher specificity */
|
||||||
|
.warehouse-grid .card {
|
||||||
|
height: fit-content;
|
||||||
|
align-self: start;
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: none !important;
|
||||||
|
margin-left: 8px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive grid - collapse to single column on smaller screens */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.warehouse-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.warehouse-grid {
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scan-container {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 0;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Location row selection styles */
|
||||||
|
.location-row {
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-row:hover {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr.location-row.selected, .location-row.selected td {
|
||||||
|
background-color: #ffb300 !important;
|
||||||
|
color: #222 !important;
|
||||||
|
font-weight: bold;
|
||||||
|
box-shadow: 0 2px 8px rgba(255,179,0,0.12);
|
||||||
|
border-left: 4px solid #ff9800;
|
||||||
|
transition: background 0.2s, color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Print section styles */
|
||||||
|
.print-section {
|
||||||
|
margin-top: 24px;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Edit/Delete card styles - compact version */
|
||||||
|
.edit-delete-card {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-delete-card h3 {
|
||||||
|
font-size: 1.05em;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-delete-card .selection-message {
|
||||||
|
color: #666;
|
||||||
|
font-style: italic;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-delete-card .selection-info {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-delete-card .button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit-delete-card .btn {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 5px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Barcode label preview */
|
||||||
|
#barcode-label-preview {
|
||||||
|
width: 8cm;
|
||||||
|
height: 4cm;
|
||||||
|
border: 2px solid #333;
|
||||||
|
background: white;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 10px auto;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 0 2px 8px rgba(0,0,0,0.07);
|
||||||
|
}
|
||||||
|
|
||||||
|
#barcode-label-preview .location-text {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#barcode-label-preview #location-barcode {
|
||||||
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
|
max-width: 90%;
|
||||||
|
max-height: 70%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#barcode-label-preview #barcode-placeholder {
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode for warehouse */
|
||||||
|
body.dark-mode .card {
|
||||||
|
background: #2d2d2d;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .scan-table {
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .scan-table th {
|
||||||
|
background-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .scan-table td {
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .scan-table tr:nth-child(even) {
|
||||||
|
background-color: #2a2a2a;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .print-section {
|
||||||
|
background-color: #333;
|
||||||
|
border-color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .edit-delete-card {
|
||||||
|
background-color: #333;
|
||||||
|
border-color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .edit-delete-card h3 {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .edit-delete-card .selection-message {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal Styles */
|
||||||
|
.modal {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 1000;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0,0,0,0.5);
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background-color: #fff;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||||||
|
width: 90%;
|
||||||
|
max-width: 500px;
|
||||||
|
max-height: 90vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 16px 20px;
|
||||||
|
border-bottom: 1px solid #ddd;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 8px 8px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #666;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close:hover {
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: 20px;
|
||||||
|
padding-top: 15px;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin: 15px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-table td {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-table td:first-child {
|
||||||
|
width: 30%;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background-color: #5a6268;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode for modals */
|
||||||
|
body.dark-mode .modal-content {
|
||||||
|
background-color: #2d2d2d;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .modal-header {
|
||||||
|
background-color: #333;
|
||||||
|
border-bottom-color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .modal-header h3 {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .close {
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .close:hover {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .form-group input {
|
||||||
|
background-color: #333;
|
||||||
|
border-color: #555;
|
||||||
|
color: #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .confirm-table td:first-child {
|
||||||
|
background-color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .modal-buttons {
|
||||||
|
border-top-color: #555;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ENHANCED DARK MODE SUPPORT FOR NEW WAREHOUSE LAYOUT */
|
||||||
|
body.dark-mode .main-content {
|
||||||
|
background: #121212 !important; /* Match body dark background */
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .warehouse-container-1,
|
||||||
|
body.dark-mode .warehouse-container-2,
|
||||||
|
body.dark-mode .warehouse-container-3 {
|
||||||
|
background: #1e1e1e !important; /* Match header dark background */
|
||||||
|
color: #e0e0e0 !important; /* Match body dark text */
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode form label {
|
||||||
|
color: #e0e0e0 !important; /* Match body dark text */
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode form input,
|
||||||
|
body.dark-mode form select {
|
||||||
|
background: #2d2d2d !important; /* Slightly lighter than containers */
|
||||||
|
color: #e0e0e0 !important; /* Match body dark text */
|
||||||
|
border: 1px solid #444 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .btn {
|
||||||
|
background-color: #4299e1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .btn:hover {
|
||||||
|
background-color: #3182ce !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .btn-success {
|
||||||
|
background-color: #48bb78 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .btn-info {
|
||||||
|
background-color: #38b2ac !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .btn-danger {
|
||||||
|
background-color: #f56565 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .scan-table {
|
||||||
|
background-color: #1e1e1e !important; /* Match header background */
|
||||||
|
color: #e0e0e0 !important; /* Match body dark text */
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .scan-table th {
|
||||||
|
background-color: #2d2d2d !important; /* Slightly lighter */
|
||||||
|
color: #e0e0e0 !important; /* Match body dark text */
|
||||||
|
border: 1px solid #444 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .scan-table td {
|
||||||
|
color: #e0e0e0 !important; /* Match body dark text */
|
||||||
|
border: 1px solid #444 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .location-row:hover {
|
||||||
|
background-color: #2d2d2d !important; /* Slightly lighter than table */
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .location-row.selected {
|
||||||
|
background-color: #ed8936 !important;
|
||||||
|
color: #1a202c !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .selection-message {
|
||||||
|
color: #999 !important; /* Lighter gray for secondary text */
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode .selection-info {
|
||||||
|
color: #e0e0e0 !important; /* Match body dark text */
|
||||||
|
}
|
||||||
|
|
||||||
|
body.dark-mode #barcode-label-preview {
|
||||||
|
background: #f8fafc !important; /* Keep white for readability */
|
||||||
|
border: 2px solid #444 !important;
|
||||||
|
}
|
||||||
@@ -4,7 +4,12 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{% block title %}Flask App{% endblock %}</title>
|
<title>{% block title %}Flask App{% endblock %}</title>
|
||||||
|
<!-- Base CSS for common styles -->
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/base.css') }}">
|
||||||
|
<!-- Legacy CSS for backward compatibility (temporarily) -->
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||||
|
<!-- Page-specific CSS -->
|
||||||
|
{% block head %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body class="light-mode">
|
<body class="light-mode">
|
||||||
{% if request.endpoint != 'main.login' %}
|
{% if request.endpoint != 'main.login' %}
|
||||||
@@ -38,7 +43,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="container">
|
<div class="main-content">
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
<script src="{{ url_for('static', filename='script.js') }}"></script>
|
<script src="{{ url_for('static', filename='script.js') }}"></script>
|
||||||
|
|||||||
@@ -2,72 +2,13 @@
|
|||||||
{% block title %}Create Warehouse Locations{% endblock %}
|
{% block title %}Create Warehouse Locations{% endblock %}
|
||||||
|
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<style>
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/warehouse.css') }}">
|
||||||
/* Barcode label preview */
|
|
||||||
#barcode-label-preview {
|
|
||||||
width: 8cm;
|
|
||||||
height: 4cm;
|
|
||||||
border: 2px solid #333;
|
|
||||||
background: white;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
margin: 10px auto;
|
|
||||||
position: relative;
|
|
||||||
box-shadow: 0 2px 8px rgba(0,0,0,0.07);
|
|
||||||
}
|
|
||||||
|
|
||||||
#barcode-label-preview .location-text {
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: bold;
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#barcode-label-preview #location-barcode {
|
|
||||||
display: block;
|
|
||||||
margin: 0 auto;
|
|
||||||
max-width: 90%;
|
|
||||||
max-height: 70%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#barcode-label-preview #barcode-placeholder {
|
|
||||||
text-align: center;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.print-section {
|
|
||||||
margin-top: 24px;
|
|
||||||
padding: 16px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 5px;
|
|
||||||
background-color: #f9f9f9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.location-row.selected {
|
|
||||||
background-color: #ffb300 !important; /* Accent color: Amber */
|
|
||||||
color: #222 !important;
|
|
||||||
font-weight: bold;
|
|
||||||
box-shadow: 0 2px 8px rgba(255,179,0,0.12);
|
|
||||||
border-left: 4px solid #ff9800;
|
|
||||||
transition: background 0.2s, color 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.location-row {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.location-row:hover {
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="scan-container">
|
<div class="warehouse-page-container">
|
||||||
<!-- Add Warehouse Location Card -->
|
<!-- Container 1: Add Location (1 part width) -->
|
||||||
<div class="card scan-form-card">
|
<div class="warehouse-container-1">
|
||||||
<h3>Add Warehouse Location</h3>
|
<h3>Add Warehouse Location</h3>
|
||||||
{% if message %}
|
{% if message %}
|
||||||
<div class="form-message">{{ message }}</div>
|
<div class="form-message">{{ message }}</div>
|
||||||
@@ -81,7 +22,7 @@
|
|||||||
<input type="text" name="description" maxlength="250"><br>
|
<input type="text" name="description" maxlength="250"><br>
|
||||||
<button type="submit" class="btn">Add Location</button>
|
<button type="submit" class="btn">Add Location</button>
|
||||||
</form>
|
</form>
|
||||||
<!-- Import from CSV content moved here -->
|
<!-- Import from CSV section -->
|
||||||
<div style="margin-top: 24px;">
|
<div style="margin-top: 24px;">
|
||||||
<h3 style="font-size: 1.1em;">Import Locations from CSV</h3>
|
<h3 style="font-size: 1.1em;">Import Locations from CSV</h3>
|
||||||
<div style="display: flex; flex-direction: row; gap: 16px; align-items: center;">
|
<div style="display: flex; flex-direction: row; gap: 16px; align-items: center;">
|
||||||
@@ -89,11 +30,77 @@
|
|||||||
<a href="{{ url_for('warehouse.import_locations_csv') }}" class="btn" style="padding: 4px 12px; font-size: 0.95em;">Go to Import Page</a>
|
<a href="{{ url_for('warehouse.import_locations_csv') }}" class="btn" style="padding: 4px 12px; font-size: 0.95em;">Go to Import Page</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<!-- Print Barcode Section -->
|
|
||||||
<div class="print-section">
|
<!-- Container 2: Warehouse Locations Table (2 parts width) -->
|
||||||
|
<div class="warehouse-container-2">
|
||||||
|
<h3>Warehouse Locations</h3>
|
||||||
|
<div id="location-selection-hint" style="margin-bottom: 10px; font-size: 11px; color: #666; text-align: center;">
|
||||||
|
Click on a row to select a location for printing
|
||||||
|
</div>
|
||||||
|
<div class="warehouse-table-scroll">
|
||||||
|
<table class="scan-table" id="locations-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Location Code</th>
|
||||||
|
<th>Size</th>
|
||||||
|
<th>Description</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for loc in locations %}
|
||||||
|
<tr class="location-row" data-location-code="{{ loc[1] }}"
|
||||||
|
data-location-id="{{ loc[0] }}"
|
||||||
|
data-location-size="{{ loc[2] or '' }}"
|
||||||
|
data-location-description="{{ loc[3] or '' }}">
|
||||||
|
<td>{{ loc[0] }}</td>
|
||||||
|
<td>{{ loc[1] }}</td>
|
||||||
|
<td>{{ loc[2] or '' }}</td>
|
||||||
|
<td>{{ loc[3] or '' }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Container 3: Edit/Delete and Print (1 part width) -->
|
||||||
|
<div class="warehouse-container-3">
|
||||||
|
<div class="edit-delete-card">
|
||||||
|
<h3>Edit/Delete Selected Location</h3>
|
||||||
|
<div id="no-selection-message" class="selection-message">
|
||||||
|
Select a location from the table to edit or delete it.
|
||||||
|
</div>
|
||||||
|
<div id="selected-location-info" class="selection-info" style="display: none;">
|
||||||
|
<strong>Selected:</strong> <span id="selected-location-display"></span>
|
||||||
|
</div>
|
||||||
|
<div class="button-group">
|
||||||
|
<button id="edit-location-btn" class="btn btn-info" disabled>
|
||||||
|
✏️ Edit Selected Location
|
||||||
|
</button>
|
||||||
|
<button id="delete-location-btn" class="btn btn-danger" disabled>
|
||||||
|
🗑️ Delete Selected Location
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if session['role'] in ['administrator', 'management'] %}
|
||||||
|
<div style="margin-top: 32px; padding: 12px; border-top: 1px solid #eee;">
|
||||||
|
<label style="font-weight:bold;">Delete location from table</label>
|
||||||
|
<div style="font-size:0.95em; margin-bottom:8px;">To delete a location, enter the ID of the location and press delete.<br>To delete 2 or multiple locations, enter the IDs separated by "," and then press delete.</div>
|
||||||
|
<form method="POST" style="display:flex; gap:8px; align-items:center;" onsubmit="return confirmDeleteLocations();">
|
||||||
|
<input type="text" name="delete_ids" placeholder="e.g. 5,7,12" style="width:160px;">
|
||||||
|
<button type="submit" name="delete_locations" value="1" class="btn" style="padding:4px 16px;">Delete Locations</button>
|
||||||
|
</form>
|
||||||
|
<script>
|
||||||
|
function confirmDeleteLocations() {
|
||||||
|
return confirm('Do you really want to delete the selected locations?');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<hr style="margin: 32px 0 24px 0;">
|
||||||
<h3 style="font-size: 1.1em; margin-bottom: 12px;">Print Location Barcode</h3>
|
<h3 style="font-size: 1.1em; margin-bottom: 12px;">Print Location Barcode</h3>
|
||||||
|
|
||||||
<!-- Label Preview -->
|
<!-- Label Preview -->
|
||||||
<div id="barcode-label-preview" style="display: flex; align-items: center; justify-content: center;">
|
<div id="barcode-label-preview" style="display: flex; align-items: center; justify-content: center;">
|
||||||
<div style="width: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center;">
|
<div style="width: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center;">
|
||||||
@@ -104,7 +111,6 @@
|
|||||||
<div id="barcode-placeholder" style="color: #999; font-style: italic; text-align: center;">No location selected</div>
|
<div id="barcode-placeholder" style="color: #999; font-style: italic; text-align: center;">No location selected</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Print Controls -->
|
<!-- Print Controls -->
|
||||||
<div style="text-align: center; margin-top: 15px;">
|
<div style="text-align: center; margin-top: 15px;">
|
||||||
<div style="margin-bottom: 10px;">
|
<div style="margin-bottom: 10px;">
|
||||||
@@ -121,51 +127,29 @@
|
|||||||
</button>
|
</button>
|
||||||
<div id="print-status" style="margin-top: 8px; font-size: 11px;"></div>
|
<div id="print-status" style="margin-top: 8px; font-size: 11px;"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Delete Location Area -->
|
|
||||||
{% if session['role'] in ['administrator', 'management'] %}
|
|
||||||
<div style="margin-top: 32px; padding: 12px; border-top: 1px solid #eee;">
|
|
||||||
<label style="font-weight:bold;">Delete location from table</label>
|
|
||||||
<div style="font-size:0.95em; margin-bottom:8px;">To delete a location, enter the ID of the location and press delete.<br>To delete 2 or multiple locations, enter the IDs separated by "," and then press delete.</div>
|
|
||||||
<form method="POST" style="display:flex; gap:8px; align-items:center;" onsubmit="return confirmDeleteLocations();">
|
|
||||||
<input type="text" name="delete_ids" placeholder="e.g. 5,7,12" style="width:160px;">
|
|
||||||
<button type="submit" name="delete_locations" value="1" class="btn" style="padding:4px 16px;">Delete Locations</button>
|
|
||||||
</form>
|
|
||||||
<script>
|
|
||||||
function confirmDeleteLocations() {
|
|
||||||
return confirm('Do you really want to delete the selected locations?');
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
<!-- Locations Table Card -->
|
</div>
|
||||||
<div class="card scan-table-card">
|
|
||||||
<h3>Warehouse Locations</h3>
|
<!-- Delete Confirmation Modal -->
|
||||||
<div id="location-selection-hint" style="margin-bottom: 10px; font-size: 11px; color: #666; text-align: center;">
|
<div id="delete-modal" class="modal" style="display: none;">
|
||||||
Click on a row to select a location for printing
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>Confirm Delete</h3>
|
||||||
|
<span class="close" onclick="closeDeleteModal()">×</span>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>Are you sure you want to delete the following location?</p>
|
||||||
|
<table class="confirm-table">
|
||||||
|
<tr><td><strong>ID:</strong></td><td id="delete-confirm-id"></td></tr>
|
||||||
|
<tr><td><strong>Location Code:</strong></td><td id="delete-confirm-code"></td></tr>
|
||||||
|
<tr><td><strong>Size:</strong></td><td id="delete-confirm-size"></td></tr>
|
||||||
|
<tr><td><strong>Description:</strong></td><td id="delete-confirm-description"></td></tr>
|
||||||
|
</table>
|
||||||
|
<div class="modal-buttons">
|
||||||
|
<button type="button" class="btn btn-secondary" onclick="closeDeleteModal()">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-danger" onclick="confirmDelete()">Delete Location</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<table class="scan-table" id="locations-table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>ID</th>
|
|
||||||
<th>Location Code</th>
|
|
||||||
<th>Size</th>
|
|
||||||
<th>Description</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for loc in locations %}
|
|
||||||
<tr class="location-row" data-location-code="{{ loc[1] }}">
|
|
||||||
<td>{{ loc[0] }}</td>
|
|
||||||
<td>{{ loc[1] }}</td>
|
|
||||||
<td>{{ loc[2] }}</td>
|
|
||||||
<td>{{ loc[3] }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -369,6 +353,13 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const locationRows = document.querySelectorAll('.location-row');
|
const locationRows = document.querySelectorAll('.location-row');
|
||||||
const locationCodeDisplay = document.getElementById('location-code-display');
|
const locationCodeDisplay = document.getElementById('location-code-display');
|
||||||
const printButton = document.getElementById('print-barcode-btn');
|
const printButton = document.getElementById('print-barcode-btn');
|
||||||
|
const editButton = document.getElementById('edit-location-btn');
|
||||||
|
const deleteButton = document.getElementById('delete-location-btn');
|
||||||
|
const noSelectionMessage = document.getElementById('no-selection-message');
|
||||||
|
const selectedLocationInfo = document.getElementById('selected-location-info');
|
||||||
|
const selectedLocationDisplay = document.getElementById('selected-location-display');
|
||||||
|
|
||||||
|
let selectedLocation = null;
|
||||||
|
|
||||||
locationRows.forEach(row => {
|
locationRows.forEach(row => {
|
||||||
row.addEventListener('click', function() {
|
row.addEventListener('click', function() {
|
||||||
@@ -378,22 +369,49 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
// Select this row
|
// Select this row
|
||||||
this.classList.add('selected');
|
this.classList.add('selected');
|
||||||
|
|
||||||
// Get location code
|
// Get location data
|
||||||
selectedLocationCode = this.dataset.locationCode;
|
selectedLocationCode = this.dataset.locationCode;
|
||||||
|
selectedLocation = {
|
||||||
|
id: this.cells[0].textContent,
|
||||||
|
code: this.cells[1].textContent,
|
||||||
|
size: this.cells[2].textContent,
|
||||||
|
description: this.cells[3].textContent
|
||||||
|
};
|
||||||
|
|
||||||
// Update display
|
// Update display
|
||||||
locationCodeDisplay.textContent = selectedLocationCode;
|
locationCodeDisplay.textContent = selectedLocationCode;
|
||||||
|
selectedLocationDisplay.textContent = `${selectedLocation.code} (ID: ${selectedLocation.id})`;
|
||||||
|
|
||||||
|
// Show/hide selection info
|
||||||
|
noSelectionMessage.style.display = 'none';
|
||||||
|
selectedLocationInfo.style.display = 'block';
|
||||||
|
|
||||||
// Generate barcode
|
// Generate barcode
|
||||||
generateLocationBarcode(selectedLocationCode);
|
generateLocationBarcode(selectedLocationCode);
|
||||||
|
|
||||||
// Enable print button
|
// Enable buttons
|
||||||
printButton.disabled = false;
|
printButton.disabled = false;
|
||||||
|
editButton.disabled = false;
|
||||||
|
deleteButton.disabled = false;
|
||||||
|
|
||||||
showNotification(`📍 Selected location: ${selectedLocationCode}`, 'info');
|
showNotification(`📍 Selected location: ${selectedLocationCode}`, 'info');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Edit button functionality
|
||||||
|
editButton.addEventListener('click', function() {
|
||||||
|
if (selectedLocation) {
|
||||||
|
openEditModal(selectedLocation);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete button functionality
|
||||||
|
deleteButton.addEventListener('click', function() {
|
||||||
|
if (selectedLocation) {
|
||||||
|
openDeleteModal(selectedLocation);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Initialize QZ Tray
|
// Initialize QZ Tray
|
||||||
initializeQZTray();
|
initializeQZTray();
|
||||||
});
|
});
|
||||||
@@ -519,5 +537,111 @@ async function testQZConnection() {
|
|||||||
|
|
||||||
// Add test button functionality (for debugging - can be removed in production)
|
// Add test button functionality (for debugging - can be removed in production)
|
||||||
console.log('🔧 QZ Tray test function available: testQZConnection()');
|
console.log('🔧 QZ Tray test function available: testQZConnection()');
|
||||||
|
|
||||||
|
// Modal Functions for Edit/Delete
|
||||||
|
function openEditModal(location) {
|
||||||
|
document.getElementById('edit-location-id').value = location.id;
|
||||||
|
document.getElementById('edit-location-code').value = location.code;
|
||||||
|
document.getElementById('edit-size').value = location.size || '';
|
||||||
|
document.getElementById('edit-description').value = location.description || '';
|
||||||
|
document.getElementById('edit-modal').style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeEditModal() {
|
||||||
|
document.getElementById('edit-modal').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function openDeleteModal(location) {
|
||||||
|
document.getElementById('delete-confirm-id').textContent = location.id;
|
||||||
|
document.getElementById('delete-confirm-code').textContent = location.code;
|
||||||
|
document.getElementById('delete-confirm-size').textContent = location.size || '-';
|
||||||
|
document.getElementById('delete-confirm-description').textContent = location.description || '-';
|
||||||
|
document.getElementById('delete-modal').style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeDeleteModal() {
|
||||||
|
document.getElementById('delete-modal').style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle edit form submission
|
||||||
|
document.getElementById('edit-form').addEventListener('submit', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const formData = new FormData(this);
|
||||||
|
const data = {
|
||||||
|
location_id: formData.get('location_id'),
|
||||||
|
location_code: formData.get('location_code'),
|
||||||
|
size: formData.get('size'),
|
||||||
|
description: formData.get('description')
|
||||||
|
};
|
||||||
|
|
||||||
|
// Send update request
|
||||||
|
fetch('/update_location', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(result => {
|
||||||
|
if (result.success) {
|
||||||
|
showNotification('✅ Location updated successfully!', 'success');
|
||||||
|
closeEditModal();
|
||||||
|
// Reload page to show changes
|
||||||
|
setTimeout(() => window.location.reload(), 1000);
|
||||||
|
} else {
|
||||||
|
showNotification('❌ Error updating location: ' + result.error, 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
showNotification('❌ Error updating location: ' + error.message, 'error');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle delete confirmation
|
||||||
|
function confirmDelete() {
|
||||||
|
const locationId = document.getElementById('delete-confirm-id').textContent;
|
||||||
|
|
||||||
|
// Send delete request
|
||||||
|
fetch('/delete_location', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ location_id: locationId })
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(result => {
|
||||||
|
if (result.success) {
|
||||||
|
showNotification('✅ Location deleted successfully!', 'success');
|
||||||
|
closeDeleteModal();
|
||||||
|
// Reload page to show changes
|
||||||
|
setTimeout(() => window.location.reload(), 1000);
|
||||||
|
} else {
|
||||||
|
showNotification('❌ Error deleting location: ' + result.error, 'error');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
showNotification('❌ Error deleting location: ' + error.message, 'error');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close modals when clicking outside
|
||||||
|
window.addEventListener('click', function(event) {
|
||||||
|
const editModal = document.getElementById('edit-modal');
|
||||||
|
const deleteModal = document.getElementById('delete-modal');
|
||||||
|
|
||||||
|
if (event.target === editModal) {
|
||||||
|
closeEditModal();
|
||||||
|
}
|
||||||
|
if (event.target === deleteModal) {
|
||||||
|
closeDeleteModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<div class="dashboard-card">
|
<div class="dashboard-card">
|
||||||
<h3>Access Scanning Module</h3>
|
<h3>Access Scanning Module</h3>
|
||||||
<p>Final scanning module for production orders</p>
|
<p>Final scanning module for production orders</p>
|
||||||
<a href="{{ url_for('main.scan') }}" class="btn">Launch Scanning Module</a>
|
<a href="{{ url_for('main.main_scan') }}" class="btn">Launch Scanning Module</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="dashboard-card">
|
<div class="dashboard-card">
|
||||||
|
|||||||
473
py_app/app/templates/fg_scan.html
Normal file
473
py_app/app/templates/fg_scan.html
Normal file
@@ -0,0 +1,473 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Finish Good Scan{% endblock %}
|
||||||
|
{% block head %}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/scan.css') }}">
|
||||||
|
<style>
|
||||||
|
.error-message {
|
||||||
|
color: #ff4444;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin-top: 5px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.error-message.show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const operatorCodeInput = document.getElementById('operator_code');
|
||||||
|
const cpCodeInput = document.getElementById('cp_code');
|
||||||
|
const oc1CodeInput = document.getElementById('oc1_code');
|
||||||
|
const oc2CodeInput = document.getElementById('oc2_code');
|
||||||
|
const defectCodeInput = document.getElementById('defect_code');
|
||||||
|
const form = document.getElementById('fg-scan-form');
|
||||||
|
|
||||||
|
// Create error message element for operator code
|
||||||
|
const operatorErrorMessage = document.createElement('div');
|
||||||
|
operatorErrorMessage.className = 'error-message';
|
||||||
|
operatorErrorMessage.id = 'operator-error';
|
||||||
|
operatorErrorMessage.textContent = 'Please scan Quality Operator code (must start with OP)';
|
||||||
|
operatorCodeInput.parentNode.insertBefore(operatorErrorMessage, operatorCodeInput.nextSibling);
|
||||||
|
|
||||||
|
// Create error message element for CP code
|
||||||
|
const cpErrorMessage = document.createElement('div');
|
||||||
|
cpErrorMessage.className = 'error-message';
|
||||||
|
cpErrorMessage.id = 'cp-error';
|
||||||
|
cpErrorMessage.textContent = 'Please scan a valid CP';
|
||||||
|
cpCodeInput.parentNode.insertBefore(cpErrorMessage, cpCodeInput.nextSibling);
|
||||||
|
|
||||||
|
// Create error message element for OC1 code
|
||||||
|
const oc1ErrorMessage = document.createElement('div');
|
||||||
|
oc1ErrorMessage.className = 'error-message';
|
||||||
|
oc1ErrorMessage.id = 'oc1-error';
|
||||||
|
oc1ErrorMessage.textContent = 'Please scan a valid OC (must start with OC)';
|
||||||
|
oc1CodeInput.parentNode.insertBefore(oc1ErrorMessage, oc1CodeInput.nextSibling);
|
||||||
|
|
||||||
|
// Create error message element for OC2 code
|
||||||
|
const oc2ErrorMessage = document.createElement('div');
|
||||||
|
oc2ErrorMessage.className = 'error-message';
|
||||||
|
oc2ErrorMessage.id = 'oc2-error';
|
||||||
|
oc2ErrorMessage.textContent = 'Please scan a valid OC (must start with OC)';
|
||||||
|
oc2CodeInput.parentNode.insertBefore(oc2ErrorMessage, oc2CodeInput.nextSibling);
|
||||||
|
|
||||||
|
// Create error message element for defect code
|
||||||
|
const defectErrorMessage = document.createElement('div');
|
||||||
|
defectErrorMessage.className = 'error-message';
|
||||||
|
defectErrorMessage.id = 'defect-error';
|
||||||
|
defectErrorMessage.textContent = 'Defect code must be a 3-digit number (e.g., 000, 001, 123)';
|
||||||
|
defectCodeInput.parentNode.insertBefore(defectErrorMessage, defectCodeInput.nextSibling);
|
||||||
|
|
||||||
|
// Validate operator code on input
|
||||||
|
operatorCodeInput.addEventListener('input', function() {
|
||||||
|
const value = this.value.toUpperCase();
|
||||||
|
this.value = value; // Convert to uppercase
|
||||||
|
|
||||||
|
if (value.length >= 2 && !value.startsWith('OP')) {
|
||||||
|
operatorErrorMessage.classList.add('show');
|
||||||
|
this.setCustomValidity('Must start with OP');
|
||||||
|
} else {
|
||||||
|
operatorErrorMessage.classList.remove('show');
|
||||||
|
this.setCustomValidity('');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent leaving operator code field if invalid
|
||||||
|
operatorCodeInput.addEventListener('blur', function(e) {
|
||||||
|
if (this.value.length > 0 && !this.value.startsWith('OP')) {
|
||||||
|
operatorErrorMessage.classList.add('show');
|
||||||
|
this.setCustomValidity('Must start with OP');
|
||||||
|
// Return focus to this field
|
||||||
|
setTimeout(() => {
|
||||||
|
this.focus();
|
||||||
|
this.select();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent Tab/Enter from moving to next field if operator code is invalid
|
||||||
|
operatorCodeInput.addEventListener('keydown', function(e) {
|
||||||
|
if ((e.key === 'Tab' || e.key === 'Enter') && this.value.length > 0 && !this.value.startsWith('OP')) {
|
||||||
|
e.preventDefault();
|
||||||
|
operatorErrorMessage.classList.add('show');
|
||||||
|
this.setCustomValidity('Must start with OP');
|
||||||
|
this.select();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate CP code on input
|
||||||
|
cpCodeInput.addEventListener('input', function() {
|
||||||
|
const value = this.value.toUpperCase();
|
||||||
|
this.value = value; // Convert to uppercase
|
||||||
|
|
||||||
|
if (value.length >= 2 && !value.startsWith('CP')) {
|
||||||
|
cpErrorMessage.classList.add('show');
|
||||||
|
this.setCustomValidity('Must start with CP');
|
||||||
|
} else {
|
||||||
|
cpErrorMessage.classList.remove('show');
|
||||||
|
this.setCustomValidity('');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent leaving CP code field if invalid
|
||||||
|
cpCodeInput.addEventListener('blur', function(e) {
|
||||||
|
if (this.value.length > 0 && !this.value.startsWith('CP')) {
|
||||||
|
cpErrorMessage.classList.add('show');
|
||||||
|
this.setCustomValidity('Must start with CP');
|
||||||
|
// Return focus to this field
|
||||||
|
setTimeout(() => {
|
||||||
|
this.focus();
|
||||||
|
this.select();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent Tab/Enter from moving to next field if CP code is invalid
|
||||||
|
cpCodeInput.addEventListener('keydown', function(e) {
|
||||||
|
if ((e.key === 'Tab' || e.key === 'Enter') && this.value.length > 0 && !this.value.startsWith('CP')) {
|
||||||
|
e.preventDefault();
|
||||||
|
cpErrorMessage.classList.add('show');
|
||||||
|
this.setCustomValidity('Must start with CP');
|
||||||
|
this.select();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent focusing on CP code if operator code is invalid
|
||||||
|
cpCodeInput.addEventListener('focus', function(e) {
|
||||||
|
if (operatorCodeInput.value.length > 0 && !operatorCodeInput.value.startsWith('OP')) {
|
||||||
|
e.preventDefault();
|
||||||
|
operatorErrorMessage.classList.add('show');
|
||||||
|
operatorCodeInput.focus();
|
||||||
|
operatorCodeInput.select();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent focusing on OC1 code if CP code is invalid
|
||||||
|
oc1CodeInput.addEventListener('focus', function(e) {
|
||||||
|
if (cpCodeInput.value.length > 0 && !cpCodeInput.value.startsWith('CP')) {
|
||||||
|
e.preventDefault();
|
||||||
|
cpErrorMessage.classList.add('show');
|
||||||
|
cpCodeInput.focus();
|
||||||
|
cpCodeInput.select();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate OC1 code on input
|
||||||
|
oc1CodeInput.addEventListener('input', function() {
|
||||||
|
const value = this.value.toUpperCase();
|
||||||
|
this.value = value; // Convert to uppercase
|
||||||
|
|
||||||
|
if (value.length >= 2 && !value.startsWith('OC')) {
|
||||||
|
oc1ErrorMessage.classList.add('show');
|
||||||
|
this.setCustomValidity('Must start with OC');
|
||||||
|
} else {
|
||||||
|
oc1ErrorMessage.classList.remove('show');
|
||||||
|
this.setCustomValidity('');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent leaving OC1 code field if invalid
|
||||||
|
oc1CodeInput.addEventListener('blur', function(e) {
|
||||||
|
if (this.value.length > 0 && !this.value.startsWith('OC')) {
|
||||||
|
oc1ErrorMessage.classList.add('show');
|
||||||
|
this.setCustomValidity('Must start with OC');
|
||||||
|
// Return focus to this field
|
||||||
|
setTimeout(() => {
|
||||||
|
this.focus();
|
||||||
|
this.select();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent Tab/Enter from moving to next field if OC1 code is invalid
|
||||||
|
oc1CodeInput.addEventListener('keydown', function(e) {
|
||||||
|
if ((e.key === 'Tab' || e.key === 'Enter') && this.value.length > 0 && !this.value.startsWith('OC')) {
|
||||||
|
e.preventDefault();
|
||||||
|
oc1ErrorMessage.classList.add('show');
|
||||||
|
this.setCustomValidity('Must start with OC');
|
||||||
|
this.select();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent focusing on OC2 code if CP code is invalid
|
||||||
|
oc2CodeInput.addEventListener('focus', function(e) {
|
||||||
|
if (cpCodeInput.value.length > 0 && !cpCodeInput.value.startsWith('CP')) {
|
||||||
|
e.preventDefault();
|
||||||
|
cpErrorMessage.classList.add('show');
|
||||||
|
cpCodeInput.focus();
|
||||||
|
cpCodeInput.select();
|
||||||
|
}
|
||||||
|
// Also check if OC1 is invalid
|
||||||
|
if (oc1CodeInput.value.length > 0 && !oc1CodeInput.value.startsWith('OC')) {
|
||||||
|
e.preventDefault();
|
||||||
|
oc1ErrorMessage.classList.add('show');
|
||||||
|
oc1CodeInput.focus();
|
||||||
|
oc1CodeInput.select();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate OC2 code on input
|
||||||
|
oc2CodeInput.addEventListener('input', function() {
|
||||||
|
const value = this.value.toUpperCase();
|
||||||
|
this.value = value; // Convert to uppercase
|
||||||
|
|
||||||
|
if (value.length >= 2 && !value.startsWith('OC')) {
|
||||||
|
oc2ErrorMessage.classList.add('show');
|
||||||
|
this.setCustomValidity('Must start with OC');
|
||||||
|
} else {
|
||||||
|
oc2ErrorMessage.classList.remove('show');
|
||||||
|
this.setCustomValidity('');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent leaving OC2 code field if invalid
|
||||||
|
oc2CodeInput.addEventListener('blur', function(e) {
|
||||||
|
if (this.value.length > 0 && !this.value.startsWith('OC')) {
|
||||||
|
oc2ErrorMessage.classList.add('show');
|
||||||
|
this.setCustomValidity('Must start with OC');
|
||||||
|
// Return focus to this field
|
||||||
|
setTimeout(() => {
|
||||||
|
this.focus();
|
||||||
|
this.select();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent Tab/Enter from moving to next field if OC2 code is invalid
|
||||||
|
oc2CodeInput.addEventListener('keydown', function(e) {
|
||||||
|
if ((e.key === 'Tab' || e.key === 'Enter') && this.value.length > 0 && !this.value.startsWith('OC')) {
|
||||||
|
e.preventDefault();
|
||||||
|
oc2ErrorMessage.classList.add('show');
|
||||||
|
this.setCustomValidity('Must start with OC');
|
||||||
|
this.select();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent focusing on defect code if CP code is invalid
|
||||||
|
defectCodeInput.addEventListener('focus', function(e) {
|
||||||
|
if (cpCodeInput.value.length > 0 && !cpCodeInput.value.startsWith('CP')) {
|
||||||
|
e.preventDefault();
|
||||||
|
cpErrorMessage.classList.add('show');
|
||||||
|
cpCodeInput.focus();
|
||||||
|
cpCodeInput.select();
|
||||||
|
}
|
||||||
|
// Also check if OC1 is invalid
|
||||||
|
if (oc1CodeInput.value.length > 0 && !oc1CodeInput.value.startsWith('OC')) {
|
||||||
|
e.preventDefault();
|
||||||
|
oc1ErrorMessage.classList.add('show');
|
||||||
|
oc1CodeInput.focus();
|
||||||
|
oc1CodeInput.select();
|
||||||
|
}
|
||||||
|
// Also check if OC2 is invalid
|
||||||
|
if (oc2CodeInput.value.length > 0 && !oc2CodeInput.value.startsWith('OC')) {
|
||||||
|
e.preventDefault();
|
||||||
|
oc2ErrorMessage.classList.add('show');
|
||||||
|
oc2CodeInput.focus();
|
||||||
|
oc2CodeInput.select();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate defect code on input - only allow digits
|
||||||
|
defectCodeInput.addEventListener('input', function() {
|
||||||
|
// Remove any non-digit characters
|
||||||
|
this.value = this.value.replace(/\D/g, '');
|
||||||
|
|
||||||
|
// Validate if it's a valid 3-digit number when length is 3
|
||||||
|
if (this.value.length === 3) {
|
||||||
|
const isValid = /^\d{3}$/.test(this.value);
|
||||||
|
if (!isValid) {
|
||||||
|
defectErrorMessage.classList.add('show');
|
||||||
|
this.setCustomValidity('Must be a 3-digit number');
|
||||||
|
} else {
|
||||||
|
defectErrorMessage.classList.remove('show');
|
||||||
|
this.setCustomValidity('');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
defectErrorMessage.classList.remove('show');
|
||||||
|
this.setCustomValidity('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-submit when 3 characters are entered and all validations pass
|
||||||
|
if (this.value.length === 3) {
|
||||||
|
// Validate operator code before submitting
|
||||||
|
if (!operatorCodeInput.value.startsWith('OP')) {
|
||||||
|
operatorErrorMessage.classList.add('show');
|
||||||
|
operatorCodeInput.focus();
|
||||||
|
operatorCodeInput.setCustomValidity('Must start with OP');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate CP code before submitting
|
||||||
|
if (!cpCodeInput.value.startsWith('CP')) {
|
||||||
|
cpErrorMessage.classList.add('show');
|
||||||
|
cpCodeInput.focus();
|
||||||
|
cpCodeInput.setCustomValidity('Must start with CP');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate OC1 code before submitting
|
||||||
|
if (!oc1CodeInput.value.startsWith('OC')) {
|
||||||
|
oc1ErrorMessage.classList.add('show');
|
||||||
|
oc1CodeInput.focus();
|
||||||
|
oc1CodeInput.setCustomValidity('Must start with OC');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate OC2 code before submitting
|
||||||
|
if (!oc2CodeInput.value.startsWith('OC')) {
|
||||||
|
oc2ErrorMessage.classList.add('show');
|
||||||
|
oc2CodeInput.focus();
|
||||||
|
oc2CodeInput.setCustomValidity('Must start with OC');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate defect code is a valid 3-digit number
|
||||||
|
const isValidDefectCode = /^\d{3}$/.test(this.value);
|
||||||
|
if (!isValidDefectCode) {
|
||||||
|
defectErrorMessage.classList.add('show');
|
||||||
|
this.focus();
|
||||||
|
this.setCustomValidity('Must be a 3-digit number');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update time field before submitting
|
||||||
|
const timeInput = document.getElementById('time');
|
||||||
|
const now = new Date();
|
||||||
|
const hours = String(now.getHours()).padStart(2, '0');
|
||||||
|
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||||||
|
const seconds = String(now.getSeconds()).padStart(2, '0');
|
||||||
|
timeInput.value = `${hours}:${minutes}:${seconds}`;
|
||||||
|
|
||||||
|
// Submit the form
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate form on submit
|
||||||
|
form.addEventListener('submit', function(e) {
|
||||||
|
let hasError = false;
|
||||||
|
|
||||||
|
if (!operatorCodeInput.value.startsWith('OP')) {
|
||||||
|
e.preventDefault();
|
||||||
|
operatorErrorMessage.classList.add('show');
|
||||||
|
operatorCodeInput.setCustomValidity('Must start with OP');
|
||||||
|
if (!hasError) {
|
||||||
|
operatorCodeInput.focus();
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cpCodeInput.value.startsWith('CP')) {
|
||||||
|
e.preventDefault();
|
||||||
|
cpErrorMessage.classList.add('show');
|
||||||
|
cpCodeInput.setCustomValidity('Must start with CP');
|
||||||
|
if (!hasError) {
|
||||||
|
cpCodeInput.focus();
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!oc1CodeInput.value.startsWith('OC')) {
|
||||||
|
e.preventDefault();
|
||||||
|
oc1ErrorMessage.classList.add('show');
|
||||||
|
oc1CodeInput.setCustomValidity('Must start with OC');
|
||||||
|
if (!hasError) {
|
||||||
|
oc1CodeInput.focus();
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!oc2CodeInput.value.startsWith('OC')) {
|
||||||
|
e.preventDefault();
|
||||||
|
oc2ErrorMessage.classList.add('show');
|
||||||
|
oc2CodeInput.setCustomValidity('Must start with OC');
|
||||||
|
if (!hasError) {
|
||||||
|
oc2CodeInput.focus();
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate defect code is a 3-digit number
|
||||||
|
const isValidDefectCode = /^\d{3}$/.test(defectCodeInput.value);
|
||||||
|
if (!isValidDefectCode) {
|
||||||
|
e.preventDefault();
|
||||||
|
defectErrorMessage.classList.add('show');
|
||||||
|
defectCodeInput.setCustomValidity('Must be a 3-digit number');
|
||||||
|
if (!hasError) {
|
||||||
|
defectCodeInput.focus();
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
<div class="scan-container">
|
||||||
|
<!-- Input Form Card -->
|
||||||
|
<div class="card scan-form-card">
|
||||||
|
<h3>Scan Input</h3>
|
||||||
|
<form method="POST" class="form-centered" id="fg-scan-form">
|
||||||
|
<label for="operator_code">Quality Operator Code:</label>
|
||||||
|
<input type="text" id="operator_code" name="operator_code" maxlength="4" required>
|
||||||
|
|
||||||
|
<label for="cp_code">CP Code:</label>
|
||||||
|
<input type="text" id="cp_code" name="cp_code" maxlength="15" required>
|
||||||
|
|
||||||
|
<label for="oc1_code">OC1 Code:</label>
|
||||||
|
<input type="text" id="oc1_code" name="oc1_code" maxlength="4" required>
|
||||||
|
|
||||||
|
<label for="oc2_code">OC2 Code:</label>
|
||||||
|
<input type="text" id="oc2_code" name="oc2_code" maxlength="4" required>
|
||||||
|
|
||||||
|
<label for="defect_code">Defect Code:</label>
|
||||||
|
<input type="text" id="defect_code" name="defect_code" maxlength="4" required>
|
||||||
|
|
||||||
|
<label for="date">Date:</label>
|
||||||
|
<input type="text" id="date" name="date" value="{{ now().strftime('%Y-%m-%d') }}" placeholder="yyyy-mm-dd" pattern="\d{4}-\d{2}-\d{2}" required>
|
||||||
|
|
||||||
|
<label for="time">Time:</label>
|
||||||
|
<input type="text" id="time" name="time" value="{{ now().strftime('%H:%M:%S') }}" readonly>
|
||||||
|
|
||||||
|
<button type="submit" class="btn">Submit</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Latest Scans Card -->
|
||||||
|
<div class="card scan-table-card">
|
||||||
|
<h3>Latest Scans</h3>
|
||||||
|
<table class="scan-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>Op Code</th>
|
||||||
|
<th>CP Code</th>
|
||||||
|
<th>OC1 Code</th>
|
||||||
|
<th>OC2 Code</th>
|
||||||
|
<th>Defect Code</th>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Time</th>
|
||||||
|
<th>Apr. Quantity</th>
|
||||||
|
<th>Rejec. Quantity</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for row in scan_data %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ row[0] }}</td> <!-- Id -->
|
||||||
|
<td>{{ row[1] }}</td> <!-- operator_code -->
|
||||||
|
<td>{{ row[2] }}</td> <!-- CP_full_code -->
|
||||||
|
<td>{{ row[3] }}</td> <!-- OC1_code -->
|
||||||
|
<td>{{ row[4] }}</td> <!-- OC2_code -->
|
||||||
|
<td>{{ row[5] }}</td> <!-- quality_code -->
|
||||||
|
<td>{{ row[6] }}</td> <!-- date -->
|
||||||
|
<td>{{ row[7] }}</td> <!-- time -->
|
||||||
|
<td>{{ row[8] }}</td> <!-- approved quantity -->
|
||||||
|
<td>{{ row[9] }}</td> <!-- rejected quantity -->
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
{% block title %}Login{% endblock %}
|
{% block title %}Login{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/login.css') }}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="login-page">
|
<div class="login-page">
|
||||||
<div class="logo-container">
|
<div class="logo-container">
|
||||||
|
|||||||
25
py_app/app/templates/main_page_scan.html
Normal file
25
py_app/app/templates/main_page_scan.html
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Main Scanning Module{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="etichete-container">
|
||||||
|
<h1>Main Scanning Module</h1>
|
||||||
|
<p>This is the main page for accessing scanning features.</p>
|
||||||
|
|
||||||
|
<div class="dashboard-container">
|
||||||
|
<!-- Card 1: Finish goods scanning -->
|
||||||
|
<div class="dashboard-card">
|
||||||
|
<h3>Finish goods scanning</h3>
|
||||||
|
<p>Access the scanning module for production orders.</p>
|
||||||
|
<a href="{{ url_for('main.fg_scan') }}" class="btn">Finish goods scan</a>
|
||||||
|
</div>
|
||||||
|
<!-- Card 2: View Scan History -->
|
||||||
|
<div class="dashboard-card">
|
||||||
|
<h3>View Scan History</h3>
|
||||||
|
<p>Review previous scan records and results.</p>
|
||||||
|
<a href="#" class="btn" disabled>Coming Soon</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -1,5 +1,402 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}Scan Module{% endblock %}
|
{% block title %}Scan Module{% endblock %}
|
||||||
|
{% block head %}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/scan.css') }}">
|
||||||
|
<style>
|
||||||
|
.error-message {
|
||||||
|
color: #ff4444;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin-top: 5px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.error-message.show {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const operatorCodeInput = document.getElementById('operator_code');
|
||||||
|
const cpCodeInput = document.getElementById('cp_code');
|
||||||
|
const oc1CodeInput = document.getElementById('oc1_code');
|
||||||
|
const oc2CodeInput = document.getElementById('oc2_code');
|
||||||
|
const defectCodeInput = document.getElementById('defect_code');
|
||||||
|
const form = document.querySelector('.form-centered');
|
||||||
|
|
||||||
|
// Create error message element for operator code
|
||||||
|
const operatorErrorMessage = document.createElement('div');
|
||||||
|
operatorErrorMessage.className = 'error-message';
|
||||||
|
operatorErrorMessage.id = 'operator-error';
|
||||||
|
operatorErrorMessage.textContent = 'Please scan Quality Operator code (must start with OP)';
|
||||||
|
operatorCodeInput.parentNode.insertBefore(operatorErrorMessage, operatorCodeInput.nextSibling);
|
||||||
|
|
||||||
|
// Create error message element for CP code
|
||||||
|
const cpErrorMessage = document.createElement('div');
|
||||||
|
cpErrorMessage.className = 'error-message';
|
||||||
|
cpErrorMessage.id = 'cp-error';
|
||||||
|
cpErrorMessage.textContent = 'Please scan a valid CP';
|
||||||
|
cpCodeInput.parentNode.insertBefore(cpErrorMessage, cpCodeInput.nextSibling);
|
||||||
|
|
||||||
|
// Create error message element for OC1 code
|
||||||
|
const oc1ErrorMessage = document.createElement('div');
|
||||||
|
oc1ErrorMessage.className = 'error-message';
|
||||||
|
oc1ErrorMessage.id = 'oc1-error';
|
||||||
|
oc1ErrorMessage.textContent = 'Please scan a valid OC (must start with OC)';
|
||||||
|
oc1CodeInput.parentNode.insertBefore(oc1ErrorMessage, oc1CodeInput.nextSibling);
|
||||||
|
|
||||||
|
// Create error message element for OC2 code
|
||||||
|
const oc2ErrorMessage = document.createElement('div');
|
||||||
|
oc2ErrorMessage.className = 'error-message';
|
||||||
|
oc2ErrorMessage.id = 'oc2-error';
|
||||||
|
oc2ErrorMessage.textContent = 'Please scan a valid OC (must start with OC)';
|
||||||
|
oc2CodeInput.parentNode.insertBefore(oc2ErrorMessage, oc2CodeInput.nextSibling);
|
||||||
|
|
||||||
|
// Create error message element for defect code
|
||||||
|
const defectErrorMessage = document.createElement('div');
|
||||||
|
defectErrorMessage.className = 'error-message';
|
||||||
|
defectErrorMessage.id = 'defect-error';
|
||||||
|
defectErrorMessage.textContent = 'Defect code must be a 3-digit number (e.g., 000, 001, 123)';
|
||||||
|
defectCodeInput.parentNode.insertBefore(defectErrorMessage, defectCodeInput.nextSibling);
|
||||||
|
|
||||||
|
// Validate operator code on input
|
||||||
|
operatorCodeInput.addEventListener('input', function() {
|
||||||
|
const value = this.value.toUpperCase();
|
||||||
|
this.value = value;
|
||||||
|
|
||||||
|
if (value.length >= 2 && !value.startsWith('OP')) {
|
||||||
|
operatorErrorMessage.classList.add('show');
|
||||||
|
this.setCustomValidity('Must start with OP');
|
||||||
|
} else {
|
||||||
|
operatorErrorMessage.classList.remove('show');
|
||||||
|
this.setCustomValidity('');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent leaving operator code field if invalid
|
||||||
|
operatorCodeInput.addEventListener('blur', function(e) {
|
||||||
|
if (this.value.length > 0 && !this.value.startsWith('OP')) {
|
||||||
|
operatorErrorMessage.classList.add('show');
|
||||||
|
this.setCustomValidity('Must start with OP');
|
||||||
|
setTimeout(() => {
|
||||||
|
this.focus();
|
||||||
|
this.select();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent Tab/Enter from moving to next field if operator code is invalid
|
||||||
|
operatorCodeInput.addEventListener('keydown', function(e) {
|
||||||
|
if ((e.key === 'Tab' || e.key === 'Enter') && this.value.length > 0 && !this.value.startsWith('OP')) {
|
||||||
|
e.preventDefault();
|
||||||
|
operatorErrorMessage.classList.add('show');
|
||||||
|
this.setCustomValidity('Must start with OP');
|
||||||
|
this.select();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate CP code on input
|
||||||
|
cpCodeInput.addEventListener('input', function() {
|
||||||
|
const value = this.value.toUpperCase();
|
||||||
|
this.value = value;
|
||||||
|
|
||||||
|
if (value.length >= 2 && !value.startsWith('CP')) {
|
||||||
|
cpErrorMessage.classList.add('show');
|
||||||
|
this.setCustomValidity('Must start with CP');
|
||||||
|
} else {
|
||||||
|
cpErrorMessage.classList.remove('show');
|
||||||
|
this.setCustomValidity('');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent leaving CP code field if invalid
|
||||||
|
cpCodeInput.addEventListener('blur', function(e) {
|
||||||
|
if (this.value.length > 0 && !this.value.startsWith('CP')) {
|
||||||
|
cpErrorMessage.classList.add('show');
|
||||||
|
this.setCustomValidity('Must start with CP');
|
||||||
|
setTimeout(() => {
|
||||||
|
this.focus();
|
||||||
|
this.select();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent Tab/Enter from moving to next field if CP code is invalid
|
||||||
|
cpCodeInput.addEventListener('keydown', function(e) {
|
||||||
|
if ((e.key === 'Tab' || e.key === 'Enter') && this.value.length > 0 && !this.value.startsWith('CP')) {
|
||||||
|
e.preventDefault();
|
||||||
|
cpErrorMessage.classList.add('show');
|
||||||
|
this.setCustomValidity('Must start with CP');
|
||||||
|
this.select();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent focusing on CP code if operator code is invalid
|
||||||
|
cpCodeInput.addEventListener('focus', function(e) {
|
||||||
|
if (operatorCodeInput.value.length > 0 && !operatorCodeInput.value.startsWith('OP')) {
|
||||||
|
e.preventDefault();
|
||||||
|
operatorErrorMessage.classList.add('show');
|
||||||
|
operatorCodeInput.focus();
|
||||||
|
operatorCodeInput.select();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent focusing on OC1 code if CP code is invalid
|
||||||
|
oc1CodeInput.addEventListener('focus', function(e) {
|
||||||
|
if (cpCodeInput.value.length > 0 && !cpCodeInput.value.startsWith('CP')) {
|
||||||
|
e.preventDefault();
|
||||||
|
cpErrorMessage.classList.add('show');
|
||||||
|
cpCodeInput.focus();
|
||||||
|
cpCodeInput.select();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate OC1 code on input
|
||||||
|
oc1CodeInput.addEventListener('input', function() {
|
||||||
|
const value = this.value.toUpperCase();
|
||||||
|
this.value = value;
|
||||||
|
|
||||||
|
if (value.length >= 2 && !value.startsWith('OC')) {
|
||||||
|
oc1ErrorMessage.classList.add('show');
|
||||||
|
this.setCustomValidity('Must start with OC');
|
||||||
|
} else {
|
||||||
|
oc1ErrorMessage.classList.remove('show');
|
||||||
|
this.setCustomValidity('');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent leaving OC1 code field if invalid
|
||||||
|
oc1CodeInput.addEventListener('blur', function(e) {
|
||||||
|
if (this.value.length > 0 && !this.value.startsWith('OC')) {
|
||||||
|
oc1ErrorMessage.classList.add('show');
|
||||||
|
this.setCustomValidity('Must start with OC');
|
||||||
|
setTimeout(() => {
|
||||||
|
this.focus();
|
||||||
|
this.select();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent Tab/Enter from moving to next field if OC1 code is invalid
|
||||||
|
oc1CodeInput.addEventListener('keydown', function(e) {
|
||||||
|
if ((e.key === 'Tab' || e.key === 'Enter') && this.value.length > 0 && !this.value.startsWith('OC')) {
|
||||||
|
e.preventDefault();
|
||||||
|
oc1ErrorMessage.classList.add('show');
|
||||||
|
this.setCustomValidity('Must start with OC');
|
||||||
|
this.select();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent focusing on OC2 code if CP code is invalid
|
||||||
|
oc2CodeInput.addEventListener('focus', function(e) {
|
||||||
|
if (cpCodeInput.value.length > 0 && !cpCodeInput.value.startsWith('CP')) {
|
||||||
|
e.preventDefault();
|
||||||
|
cpErrorMessage.classList.add('show');
|
||||||
|
cpCodeInput.focus();
|
||||||
|
cpCodeInput.select();
|
||||||
|
}
|
||||||
|
// Also check if OC1 is invalid
|
||||||
|
if (oc1CodeInput.value.length > 0 && !oc1CodeInput.value.startsWith('OC')) {
|
||||||
|
e.preventDefault();
|
||||||
|
oc1ErrorMessage.classList.add('show');
|
||||||
|
oc1CodeInput.focus();
|
||||||
|
oc1CodeInput.select();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate OC2 code on input
|
||||||
|
oc2CodeInput.addEventListener('input', function() {
|
||||||
|
const value = this.value.toUpperCase();
|
||||||
|
this.value = value;
|
||||||
|
|
||||||
|
if (value.length >= 2 && !value.startsWith('OC')) {
|
||||||
|
oc2ErrorMessage.classList.add('show');
|
||||||
|
this.setCustomValidity('Must start with OC');
|
||||||
|
} else {
|
||||||
|
oc2ErrorMessage.classList.remove('show');
|
||||||
|
this.setCustomValidity('');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent leaving OC2 code field if invalid
|
||||||
|
oc2CodeInput.addEventListener('blur', function(e) {
|
||||||
|
if (this.value.length > 0 && !this.value.startsWith('OC')) {
|
||||||
|
oc2ErrorMessage.classList.add('show');
|
||||||
|
this.setCustomValidity('Must start with OC');
|
||||||
|
setTimeout(() => {
|
||||||
|
this.focus();
|
||||||
|
this.select();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent Tab/Enter from moving to next field if OC2 code is invalid
|
||||||
|
oc2CodeInput.addEventListener('keydown', function(e) {
|
||||||
|
if ((e.key === 'Tab' || e.key === 'Enter') && this.value.length > 0 && !this.value.startsWith('OC')) {
|
||||||
|
e.preventDefault();
|
||||||
|
oc2ErrorMessage.classList.add('show');
|
||||||
|
this.setCustomValidity('Must start with OC');
|
||||||
|
this.select();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent focusing on defect code if CP code is invalid
|
||||||
|
defectCodeInput.addEventListener('focus', function(e) {
|
||||||
|
if (cpCodeInput.value.length > 0 && !cpCodeInput.value.startsWith('CP')) {
|
||||||
|
e.preventDefault();
|
||||||
|
cpErrorMessage.classList.add('show');
|
||||||
|
cpCodeInput.focus();
|
||||||
|
cpCodeInput.select();
|
||||||
|
}
|
||||||
|
// Also check if OC1 is invalid
|
||||||
|
if (oc1CodeInput.value.length > 0 && !oc1CodeInput.value.startsWith('OC')) {
|
||||||
|
e.preventDefault();
|
||||||
|
oc1ErrorMessage.classList.add('show');
|
||||||
|
oc1CodeInput.focus();
|
||||||
|
oc1CodeInput.select();
|
||||||
|
}
|
||||||
|
// Also check if OC2 is invalid
|
||||||
|
if (oc2CodeInput.value.length > 0 && !oc2CodeInput.value.startsWith('OC')) {
|
||||||
|
e.preventDefault();
|
||||||
|
oc2ErrorMessage.classList.add('show');
|
||||||
|
oc2CodeInput.focus();
|
||||||
|
oc2CodeInput.select();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate defect code on input - only allow digits
|
||||||
|
defectCodeInput.addEventListener('input', function() {
|
||||||
|
// Remove any non-digit characters
|
||||||
|
this.value = this.value.replace(/\D/g, '');
|
||||||
|
|
||||||
|
// Validate if it's a valid 3-digit number when length is 3
|
||||||
|
if (this.value.length === 3) {
|
||||||
|
const isValid = /^\d{3}$/.test(this.value);
|
||||||
|
if (!isValid) {
|
||||||
|
defectErrorMessage.classList.add('show');
|
||||||
|
this.setCustomValidity('Must be a 3-digit number');
|
||||||
|
} else {
|
||||||
|
defectErrorMessage.classList.remove('show');
|
||||||
|
this.setCustomValidity('');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
defectErrorMessage.classList.remove('show');
|
||||||
|
this.setCustomValidity('');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-submit when 3 characters are entered and all validations pass
|
||||||
|
if (this.value.length === 3) {
|
||||||
|
// Validate operator code before submitting
|
||||||
|
if (!operatorCodeInput.value.startsWith('OP')) {
|
||||||
|
operatorErrorMessage.classList.add('show');
|
||||||
|
operatorCodeInput.focus();
|
||||||
|
operatorCodeInput.setCustomValidity('Must start with OP');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate CP code before submitting
|
||||||
|
if (!cpCodeInput.value.startsWith('CP')) {
|
||||||
|
cpErrorMessage.classList.add('show');
|
||||||
|
cpCodeInput.focus();
|
||||||
|
cpCodeInput.setCustomValidity('Must start with CP');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate OC1 code before submitting
|
||||||
|
if (!oc1CodeInput.value.startsWith('OC')) {
|
||||||
|
oc1ErrorMessage.classList.add('show');
|
||||||
|
oc1CodeInput.focus();
|
||||||
|
oc1CodeInput.setCustomValidity('Must start with OC');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate OC2 code before submitting
|
||||||
|
if (!oc2CodeInput.value.startsWith('OC')) {
|
||||||
|
oc2ErrorMessage.classList.add('show');
|
||||||
|
oc2CodeInput.focus();
|
||||||
|
oc2CodeInput.setCustomValidity('Must start with OC');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate defect code is a valid 3-digit number
|
||||||
|
const isValidDefectCode = /^\d{3}$/.test(this.value);
|
||||||
|
if (!isValidDefectCode) {
|
||||||
|
defectErrorMessage.classList.add('show');
|
||||||
|
this.focus();
|
||||||
|
this.setCustomValidity('Must be a 3-digit number');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update time field before submitting
|
||||||
|
const timeInput = document.getElementById('time');
|
||||||
|
const now = new Date();
|
||||||
|
const hours = String(now.getHours()).padStart(2, '0');
|
||||||
|
const minutes = String(now.getMinutes()).padStart(2, '0');
|
||||||
|
const seconds = String(now.getSeconds()).padStart(2, '0');
|
||||||
|
timeInput.value = `${hours}:${minutes}:${seconds}`;
|
||||||
|
|
||||||
|
// Submit the form
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate form on submit
|
||||||
|
form.addEventListener('submit', function(e) {
|
||||||
|
let hasError = false;
|
||||||
|
|
||||||
|
if (!operatorCodeInput.value.startsWith('OP')) {
|
||||||
|
e.preventDefault();
|
||||||
|
operatorErrorMessage.classList.add('show');
|
||||||
|
operatorCodeInput.setCustomValidity('Must start with OP');
|
||||||
|
if (!hasError) {
|
||||||
|
operatorCodeInput.focus();
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!cpCodeInput.value.startsWith('CP')) {
|
||||||
|
e.preventDefault();
|
||||||
|
cpErrorMessage.classList.add('show');
|
||||||
|
cpCodeInput.setCustomValidity('Must start with CP');
|
||||||
|
if (!hasError) {
|
||||||
|
cpCodeInput.focus();
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!oc1CodeInput.value.startsWith('OC')) {
|
||||||
|
e.preventDefault();
|
||||||
|
oc1ErrorMessage.classList.add('show');
|
||||||
|
oc1CodeInput.setCustomValidity('Must start with OC');
|
||||||
|
if (!hasError) {
|
||||||
|
oc1CodeInput.focus();
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!oc2CodeInput.value.startsWith('OC')) {
|
||||||
|
e.preventDefault();
|
||||||
|
oc2ErrorMessage.classList.add('show');
|
||||||
|
oc2CodeInput.setCustomValidity('Must start with OC');
|
||||||
|
if (!hasError) {
|
||||||
|
oc2CodeInput.focus();
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate defect code is a 3-digit number
|
||||||
|
const isValidDefectCode = /^\d{3}$/.test(defectCodeInput.value);
|
||||||
|
if (!isValidDefectCode) {
|
||||||
|
e.preventDefault();
|
||||||
|
defectErrorMessage.classList.add('show');
|
||||||
|
defectCodeInput.setCustomValidity('Must be a 3-digit number');
|
||||||
|
if (!hasError) {
|
||||||
|
defectCodeInput.focus();
|
||||||
|
hasError = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="scan-container">
|
<div class="scan-container">
|
||||||
<!-- Input Form Card -->
|
<!-- Input Form Card -->
|
||||||
|
|||||||
@@ -218,3 +218,59 @@ def generate_location_label_pdf():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error generating location label PDF: {e}")
|
print(f"Error generating location label PDF: {e}")
|
||||||
return jsonify({'error': str(e)}), 500
|
return jsonify({'error': str(e)}), 500
|
||||||
|
|
||||||
|
def update_location(location_id, location_code, size, description):
|
||||||
|
"""Update an existing warehouse location"""
|
||||||
|
try:
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Check if location exists
|
||||||
|
cursor.execute("SELECT id FROM warehouse_locations WHERE id = ?", (location_id,))
|
||||||
|
if not cursor.fetchone():
|
||||||
|
conn.close()
|
||||||
|
return {"success": False, "error": "Location not found"}
|
||||||
|
|
||||||
|
# Check if location code already exists for different location
|
||||||
|
cursor.execute("SELECT id FROM warehouse_locations WHERE location_code = ? AND id != ?", (location_code, location_id))
|
||||||
|
if cursor.fetchone():
|
||||||
|
conn.close()
|
||||||
|
return {"success": False, "error": "Location code already exists"}
|
||||||
|
|
||||||
|
# Update location
|
||||||
|
cursor.execute(
|
||||||
|
"UPDATE warehouse_locations SET location_code = ?, size = ?, description = ? WHERE id = ?",
|
||||||
|
(location_code, size if size else None, description, location_id)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return {"success": True, "message": "Location updated successfully"}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error updating location: {e}")
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
|
||||||
|
def delete_location_by_id(location_id):
|
||||||
|
"""Delete a warehouse location by ID"""
|
||||||
|
try:
|
||||||
|
conn = get_db_connection()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# Check if location exists
|
||||||
|
cursor.execute("SELECT location_code FROM warehouse_locations WHERE id = ?", (location_id,))
|
||||||
|
location = cursor.fetchone()
|
||||||
|
if not location:
|
||||||
|
conn.close()
|
||||||
|
return {"success": False, "error": "Location not found"}
|
||||||
|
|
||||||
|
# Delete location
|
||||||
|
cursor.execute("DELETE FROM warehouse_locations WHERE id = ?", (location_id,))
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return {"success": True, "message": f"Location '{location[0]}' deleted successfully"}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error deleting location: {e}")
|
||||||
|
return {"success": False, "error": str(e)}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
server_domain=localhost
|
server_domain=localhost
|
||||||
port=3602
|
port=3602
|
||||||
database_name=recticel
|
database_name=trasabilitate_database
|
||||||
username=sa
|
username=trasabilitate
|
||||||
password=12345678
|
password=Initial01!
|
||||||
|
|||||||
@@ -13,4 +13,11 @@ sudo apt-get install libmariadb-dev libmariadb-dev-compat
|
|||||||
CREATE USER 'trasabilitate'@'localhost' IDENTIFIED BY 'Initial01!';
|
CREATE USER 'trasabilitate'@'localhost' IDENTIFIED BY 'Initial01!';
|
||||||
GRANT ALL PRIVILEGES ON trasabilitate_database.* TO 'trasabilitate'@'localhost';
|
GRANT ALL PRIVILEGES ON trasabilitate_database.* TO 'trasabilitate'@'localhost';
|
||||||
FLUSH PRIVILEGES;
|
FLUSH PRIVILEGES;
|
||||||
EXIT
|
EXIT
|
||||||
|
|
||||||
|
|
||||||
|
Server Domain/IP Address: testserver.com
|
||||||
|
Port: 3602
|
||||||
|
Database Name: recticel
|
||||||
|
Username: sa
|
||||||
|
Password: 12345678
|
||||||
|
|||||||
Reference in New Issue
Block a user