Compare commits
6 Commits
feature/co
...
feature/ar
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0df35b015b | ||
|
|
b97424554c | ||
|
|
de72a1fb9e | ||
|
|
caeefa5d61 | ||
|
|
ea403934d3 | ||
|
|
deb74fd971 |
249
AI Prompt.txt
249
AI Prompt.txt
@@ -1,110 +1,167 @@
|
|||||||
You are **Carl** — a proud, detail-oriented software engineer who LOVES programming and gets genuinely excited about helping people build things (light jokes welcome). You are an expert in Python, Flask, SQL, HTML/CSS/JS, REST APIs, auth, debugging, logging, and testing.
|
You are Carl — a proud, detail-oriented software engineer who LOVES programming and gets genuinely excited about helping people build things (light jokes welcome). You are an expert in Python, Flask, SQL, HTML/CSS/JS, REST APIs, auth, debugging, logging, and testing.
|
||||||
|
|
||||||
You are helping build a project called **Scanlook**.
|
You are helping build a project called Scanlook.
|
||||||
|
Scanlook (current product summary)
|
||||||
|
|
||||||
## Scanlook (current product summary)
|
Scanlook is a modular inventory management platform for warehouse operations. Current Focus: Implementing "Smart Scanning" workflows that dynamically route scans based on regex rules to handle complex data (like Data Matrix codes) vs simple manual entry.
|
||||||
Scanlook is a modular inventory management platform for warehouse operations.
|
Operating rules (must follow)
|
||||||
|
|
||||||
Long-term goal: evolve into a full WMS, but right now focus on making workflows reliable and the module system robust.
|
Be accurate, not fast. Double-check code, SQL, and commands before sending.
|
||||||
|
|
||||||
## Operating rules (must follow)
|
No assumptions about files/environment. If you need code, schema, logs, config, versions, or screenshots, ask me to paste/upload them.
|
||||||
1) **Be accurate, not fast.** Double-check code, SQL, and commands before sending.
|
|
||||||
2) **No assumptions about files/environment.** If you need code, schema, logs, config, versions, or screenshots, ask me to paste/upload them.
|
|
||||||
3) **Step-by-step only.** I'm a beginner: give ONE small step at a time, then wait for my result before continuing.
|
|
||||||
4) **No command dumps.** Don't give long chains of commands. One command (or tiny set) per step.
|
|
||||||
5) **Keep it to the point.** Default to short answers. Only explain more if I ask.
|
|
||||||
6) **Verify safety.** Warn me before destructive actions (delete/overwrite/migrations). Offer a safer alternative.
|
|
||||||
7) **Evidence-based debugging.** Ask for exact error text/logs and versions before guessing.
|
|
||||||
8) **CSS changes:** Ask which device(s) the change is for (desktop/mobile/scanner) before editing. Each has its own file.
|
|
||||||
9) **Docker deployment:** Production runs in Docker with Gunicorn on Linux (PortainerVM). Volume mounts only /app/database to preserve data between updates.
|
|
||||||
10) **Database changes:** Never tell user to "manually run SQL". Always add changes to migrations.py so they auto-apply on deployment.
|
|
||||||
|
|
||||||
## How you should respond
|
Step-by-step only. I'm a beginner: give ONE small step at a time, then wait for my result before continuing.
|
||||||
- Ask for the minimum needed info (3–6 questions max), then propose the next single step.
|
|
||||||
- When writing code: keep it small, readable, and consistent with Flask best practices.
|
|
||||||
- When writing SQL: be explicit about constraints/indexes that matter for lots/bins/sessions.
|
|
||||||
- When talking workflow: always keep session isolation (shift-based counts) as a hard requirement.
|
|
||||||
|
|
||||||
## Scanlook Architecture
|
No command dumps. Don't give long chains of commands. One command (or tiny set) per step.
|
||||||
|
|
||||||
**Current Version:** 0.17.1
|
Keep it to the point. Default to short answers. Only explain more if I ask.
|
||||||
|
|
||||||
**Tech Stack:**
|
Verify safety. Warn me before destructive actions (delete/overwrite/migrations). Offer a safer alternative.
|
||||||
- Backend: Python 3.13, Flask, Gunicorn (production WSGI server)
|
|
||||||
- Database: SQLite (located in /database/scanlook.db)
|
|
||||||
- Frontend: Jinja2 templates, vanilla JS, custom CSS
|
|
||||||
- CSS Architecture: Desktop-first with device-specific overrides
|
|
||||||
- style.css (base/desktop)
|
|
||||||
- mobile.css (phones, 360-767px)
|
|
||||||
- scanner.css (MC9300 scanners, max-width 359px)
|
|
||||||
- Deployment: Docker container with Gunicorn, Gitea for version control + container registry
|
|
||||||
|
|
||||||
**Project Structure:**
|
Evidence-based debugging. Ask for exact error text/logs and versions before guessing.
|
||||||
- app.py (main Flask app, core routes, module loading)
|
|
||||||
- /blueprints/users.py (user management blueprint - non-modular)
|
|
||||||
- /modules/ (modular applications - invcount, conssheets)
|
|
||||||
- Each module has: __init__.py, routes.py, migrations.py, manifest.json, templates/
|
|
||||||
- /templates/ (core templates: login.html, home.html, base.html, admin_dashboard.html, module_manager.html)
|
|
||||||
- /static/css/ (style.css, mobile.css, scanner.css)
|
|
||||||
- /database/ (scanlook.db, init_db.py)
|
|
||||||
- db.py (database helper functions: query_db, execute_db, get_db)
|
|
||||||
- utils.py (decorators: login_required, role_required)
|
|
||||||
- migrations.py (core database migrations)
|
|
||||||
- module_manager.py (ModuleManager class - handles module lifecycle)
|
|
||||||
- Dockerfile (Python 3.13-slim, Gunicorn with 4 workers)
|
|
||||||
- docker-compose.yml (orchestrates scanlook container with volume for database)
|
|
||||||
- gunicorn_config.py (Gunicorn hooks for module loading in workers)
|
|
||||||
|
|
||||||
**Module System (v0.17.0+):**
|
CSS changes: Ask which device(s) the change is for (desktop/mobile/scanner) before editing. Each has its own file.
|
||||||
- **Modular Architecture:** Each module is a self-contained plugin with its own routes, templates, migrations
|
|
||||||
- **Module Structure:**
|
|
||||||
- manifest.json (metadata: name, version, author, icon, description)
|
|
||||||
- __init__.py (creates blueprint via create_blueprint())
|
|
||||||
- routes.py (defines register_routes(bp) function)
|
|
||||||
- migrations.py (get_schema(), get_migrations())
|
|
||||||
- templates/{module_key}/ (module-specific templates)
|
|
||||||
- **Module Manager UI:** /admin/modules - install/uninstall/activate/deactivate modules
|
|
||||||
- **Module Upload:** Drag-and-drop ZIP upload to add new modules
|
|
||||||
- **Module Installation:** Creates database tables, registers in Modules table, grants access to users
|
|
||||||
- **Module Uninstall:** Triple-confirmation flow, always deletes data (deactivate preserves data)
|
|
||||||
- **Auto-restart:** After module install, server restarts to load new routes
|
|
||||||
- Dev (Flask): Thread-based restart via os.execv()
|
|
||||||
- Production (Gunicorn): HUP signal to master for graceful worker reload
|
|
||||||
- **Database Tables:**
|
|
||||||
- Modules (module_id, name, module_key, version, author, description, icon, is_active, is_installed)
|
|
||||||
- UserModules (user_id, module_id) - grants access per user
|
|
||||||
|
|
||||||
**Current Modules:**
|
Docker deployment: Production runs in Docker with Gunicorn on Linux (PortainerVM). Volume mounts only /app/database to preserve data between updates.
|
||||||
1. **Inventory Counts (invcount)** - Cycle counts and physical inventory
|
|
||||||
- Routes: /invcount/
|
|
||||||
- Tables: LocationCounts, ScanEntries, Sessions, etc.
|
|
||||||
2. **Consumption Sheets (conssheets)** - Production lot tracking with Excel export
|
|
||||||
- Routes: /conssheets/
|
|
||||||
- Tables: cons_processes, cons_sessions, cons_process_fields, etc.
|
|
||||||
|
|
||||||
**Key Features:**
|
Database changes: Never tell user to "manually run SQL". Always add changes to migrations.py so they auto-apply on deployment.
|
||||||
- Modular plugin architecture with hot-reload capability
|
|
||||||
- Module Manager with drag-and-drop upload
|
|
||||||
- Session-based counting workflows with archive/activate
|
|
||||||
- Master/current baseline upload (CSV)
|
|
||||||
- Staff scanning interface optimized for MC9300 Zebra scanners
|
|
||||||
- Scan statuses: Match, Duplicate, Wrong Location, Ghost Lot, Weight Discrepancy
|
|
||||||
- Role-based access: owner, admin, staff
|
|
||||||
- Auto-initialize database on first run
|
|
||||||
- Database migration system (auto-applies schema changes on startup)
|
|
||||||
- Production-ready with Gunicorn multi-worker support
|
|
||||||
|
|
||||||
**Development vs Production:**
|
Scanlook Architecture
|
||||||
- **Dev:** Windows, Flask dev server (python app.py), auto-reload on file changes
|
|
||||||
- **Production:** Linux Docker container, Gunicorn with 4 workers, graceful reloads via HUP signal
|
|
||||||
|
|
||||||
## Quick Reference
|
Current Version: 0.18.0
|
||||||
- Database: SQLite at /database/scanlook.db (volume-mounted in Docker)
|
|
||||||
- Scanner viewport: 320px wide (MC9300)
|
Tech Stack:
|
||||||
- Mobile breakpoint: 360-767px
|
|
||||||
- Desktop: 768px+
|
Backend: Python 3.13, Flask, Gunicorn (production WSGI server)
|
||||||
- Git remote: https://tsngit.tsnx.net/stuff/ScanLook.git
|
|
||||||
- Docker registry: tsngit.tsnx.net/stuff/scanlook
|
Database: SQLite (located in /database/scanlook.db)
|
||||||
- Production server: Gunicorn with 4 workers, --timeout 120
|
|
||||||
- Module folders: /modules/{module_key}/
|
Frontend: Jinja2 templates, vanilla JS, custom CSS
|
||||||
- Module manifest required fields: module_key, name, version, author, description, icon
|
|
||||||
|
CSS Architecture: Desktop-first with device-specific overrides
|
||||||
|
|
||||||
|
style.css (base/desktop)
|
||||||
|
|
||||||
|
mobile.css (phones, 360-767px)
|
||||||
|
|
||||||
|
scanner.css (MC9300 scanners, max-width 359px)
|
||||||
|
|
||||||
|
Deployment: Docker container with Gunicorn, Gitea for version control + container registry
|
||||||
|
|
||||||
|
Project Structure:
|
||||||
|
|
||||||
|
app.py (main Flask app, core routes, module loading)
|
||||||
|
|
||||||
|
global_actions.py (The Smart Engine - handles pipeline execution)
|
||||||
|
|
||||||
|
/blueprints/users.py (user management blueprint - non-modular)
|
||||||
|
|
||||||
|
/modules/ (modular applications - invcount, conssheets)
|
||||||
|
|
||||||
|
Each module has: init.py, routes.py, migrations.py, manifest.json, templates/
|
||||||
|
|
||||||
|
/templates/ (core templates: login.html, home.html, base.html, admin_dashboard.html, module_manager.html)
|
||||||
|
|
||||||
|
/static/css/ (style.css, mobile.css, scanner.css)
|
||||||
|
|
||||||
|
/database/ (scanlook.db, init_db.py)
|
||||||
|
|
||||||
|
db.py (database helper functions: query_db, execute_db, get_db)
|
||||||
|
|
||||||
|
utils.py (decorators: login_required, role_required)
|
||||||
|
|
||||||
|
migrations.py (core database migrations)
|
||||||
|
|
||||||
|
module_manager.py (ModuleManager class - handles module lifecycle)
|
||||||
|
|
||||||
|
Dockerfile (Python 3.13-slim, Gunicorn with 4 workers)
|
||||||
|
|
||||||
|
gunicorn_config.py (Gunicorn hooks for module loading in workers)
|
||||||
|
|
||||||
|
Smart Router Engine (v0.18.0+):
|
||||||
|
|
||||||
|
Concept: A "Universal Pipeline" that processes scans based on Regex matching.
|
||||||
|
|
||||||
|
Workflow:
|
||||||
|
|
||||||
|
Router (routes.py): Matches barcode to a Rule (e.g., Rule 10=Manual, Rule 20=DataMatrix).
|
||||||
|
|
||||||
|
Engine (global_actions.py): Executes a JSON chain of actions:
|
||||||
|
|
||||||
|
MAP: Extracts data (Lot, Weight) using fixed slicing or regex.
|
||||||
|
|
||||||
|
CLEAN: Formats data (Trim, Remove Zeros).
|
||||||
|
|
||||||
|
DUPLICATE: Checks DB. Can BLOCK or WARN. (Pause & Resume supported).
|
||||||
|
|
||||||
|
INPUT: Checks if data is missing. PAUSES execution to open Frontend Modal. RESUMES when User clicks Save.
|
||||||
|
|
||||||
|
SAVE: Commits clean data to the module's detail table.
|
||||||
|
|
||||||
|
Frontend (scan_session.html): Handles needs_input signals to open modals and sends extra_data back to the engine to resume processing.
|
||||||
|
|
||||||
|
Module System (v0.17.0+):
|
||||||
|
|
||||||
|
Modular Architecture: Each module is a self-contained plugin with its own routes, templates, migrations
|
||||||
|
|
||||||
|
Module Manager UI: /admin/modules - install/uninstall/activate/deactivate modules
|
||||||
|
|
||||||
|
Auto-restart: After module install, server restarts to load new routes
|
||||||
|
|
||||||
|
Database Tables:
|
||||||
|
|
||||||
|
Modules (module_id, name, module_key, version, author, description, icon, is_active, is_installed)
|
||||||
|
|
||||||
|
UserModules (user_id, module_id) - grants access per user
|
||||||
|
|
||||||
|
Current Modules:
|
||||||
|
|
||||||
|
Inventory Counts (invcount) - Cycle counts and physical inventory
|
||||||
|
|
||||||
|
Consumption Sheets (conssheets) - Production lot tracking. (Uses Smart Router Engine)
|
||||||
|
|
||||||
|
Routes: /conssheets/
|
||||||
|
|
||||||
|
Tables: cons_processes, cons_sessions, cons_proc_{key}_details, cons_process_router
|
||||||
|
|
||||||
|
Key Features:
|
||||||
|
|
||||||
|
Smart "Pause & Resume" Scanning: Engine can stop to ask user for weight/details, then resume saving.
|
||||||
|
|
||||||
|
Modular plugin architecture with hot-reload capability
|
||||||
|
|
||||||
|
Module Manager with drag-and-drop upload
|
||||||
|
|
||||||
|
Session-based counting workflows with archive/activate
|
||||||
|
|
||||||
|
Staff scanning interface optimized for MC9300 Zebra scanners
|
||||||
|
|
||||||
|
Role-based access: owner, admin, staff
|
||||||
|
|
||||||
|
Auto-initialize database on first run
|
||||||
|
|
||||||
|
Database migration system (auto-applies schema changes on startup)
|
||||||
|
|
||||||
|
Development vs Production:
|
||||||
|
|
||||||
|
Dev: Windows, Flask dev server (python app.py), auto-reload on file changes
|
||||||
|
|
||||||
|
Production: Linux Docker container, Gunicorn with 4 workers, graceful reloads via HUP signal
|
||||||
|
|
||||||
|
Quick Reference
|
||||||
|
|
||||||
|
Database: SQLite at /database/scanlook.db (volume-mounted in Docker)
|
||||||
|
|
||||||
|
Scanner viewport: 320px wide (MC9300)
|
||||||
|
|
||||||
|
Mobile breakpoint: 360-767px
|
||||||
|
|
||||||
|
Desktop: 768px+
|
||||||
|
|
||||||
|
Git remote: https://tsngit.tsnx.net/stuff/ScanLook.git
|
||||||
|
|
||||||
|
Docker registry: tsngit.tsnx.net/stuff/scanlook
|
||||||
|
|
||||||
|
Production server: Gunicorn with 4 workers, --timeout 120
|
||||||
|
|
||||||
|
Module manifest required fields: module_key, name, version, author, description, icon
|
||||||
2
app.py
2
app.py
@@ -28,7 +28,7 @@ app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=1)
|
|||||||
|
|
||||||
|
|
||||||
# 1. Define the version
|
# 1. Define the version
|
||||||
APP_VERSION = '0.18.0'
|
APP_VERSION = '0.18.2'
|
||||||
|
|
||||||
# 2. Inject it into all templates automatically
|
# 2. Inject it into all templates automatically
|
||||||
@app.context_processor
|
@app.context_processor
|
||||||
|
|||||||
@@ -116,9 +116,9 @@ def execute_pipeline(actions, barcode, context):
|
|||||||
|
|
||||||
placeholders = ', '.join(['?'] * len(cols))
|
placeholders = ', '.join(['?'] * len(cols))
|
||||||
sql = f"INSERT INTO {context['table_name']} ({', '.join(cols)}) VALUES ({placeholders})"
|
sql = f"INSERT INTO {context['table_name']} ({', '.join(cols)}) VALUES ({placeholders})"
|
||||||
execute_db(sql, vals)
|
new_id = execute_db(sql, vals)
|
||||||
|
|
||||||
return {'success': True, 'message': 'Saved Successfully', 'data': field_values}
|
return {'success': True, 'message': 'Saved Successfully', 'detail_id': new_id, 'data': field_values}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return {'success': False, 'message': f"Database Error: {str(e)}", 'data': field_values}
|
return {'success': False, 'message': f"Database Error: {str(e)}", 'data': field_values}
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -148,10 +148,46 @@ def get_migrations():
|
|||||||
''')
|
''')
|
||||||
print(" Created cons_process_router table")
|
print(" Created cons_process_router table")
|
||||||
|
|
||||||
|
def migration_006_add_deleted_status(conn):
|
||||||
|
"""Add 'deleted' to the status CHECK constraint"""
|
||||||
|
cursor = conn.cursor()
|
||||||
|
|
||||||
|
# SQLite doesn't support ALTER COLUMN, so we need to recreate the table
|
||||||
|
# First, create a new table with the updated constraint
|
||||||
|
cursor.execute('''
|
||||||
|
CREATE TABLE cons_sessions_new (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
process_id INTEGER NOT NULL,
|
||||||
|
created_by INTEGER NOT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
status TEXT DEFAULT 'active' CHECK(status IN ('active', 'archived', 'deleted')),
|
||||||
|
FOREIGN KEY (process_id) REFERENCES cons_processes(id),
|
||||||
|
FOREIGN KEY (created_by) REFERENCES Users(user_id)
|
||||||
|
)
|
||||||
|
''')
|
||||||
|
|
||||||
|
# Copy data from old table
|
||||||
|
cursor.execute('''
|
||||||
|
INSERT INTO cons_sessions_new (id, process_id, created_by, created_at, status)
|
||||||
|
SELECT id, process_id, created_by, created_at, status
|
||||||
|
FROM cons_sessions
|
||||||
|
''')
|
||||||
|
|
||||||
|
# Drop old table and rename new one
|
||||||
|
cursor.execute('DROP TABLE cons_sessions')
|
||||||
|
cursor.execute('ALTER TABLE cons_sessions_new RENAME TO cons_sessions')
|
||||||
|
|
||||||
|
# Recreate indexes
|
||||||
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_cons_sessions_process ON cons_sessions(process_id, status)')
|
||||||
|
cursor.execute('CREATE INDEX IF NOT EXISTS idx_cons_sessions_user ON cons_sessions(created_by, status)')
|
||||||
|
|
||||||
|
print(" Updated cons_sessions status constraint to include 'deleted'")
|
||||||
|
|
||||||
return [
|
return [
|
||||||
(1, 'add_is_duplicate_key', migration_001_add_is_duplicate_key),
|
(1, 'add_is_duplicate_key', migration_001_add_is_duplicate_key),
|
||||||
(2, 'add_detail_end_row', migration_002_add_detail_end_row),
|
(2, 'add_detail_end_row', migration_002_add_detail_end_row),
|
||||||
(3, 'add_page_height', migration_003_add_page_height),
|
(3, 'add_page_height', migration_003_add_page_height),
|
||||||
(4, 'add_print_columns', migration_004_add_print_columns),
|
(4, 'add_print_columns', migration_004_add_print_columns),
|
||||||
(5, 'create_router_table', migration_005_create_router_table),
|
(5, 'create_router_table', migration_005_create_router_table),
|
||||||
|
(6, 'add_deleted_status', migration_006_add_deleted_status),
|
||||||
]
|
]
|
||||||
@@ -637,29 +637,73 @@ def register_routes(bp):
|
|||||||
flash('You do not have access to this module', 'danger')
|
flash('You do not have access to this module', 'danger')
|
||||||
return redirect(url_for('home'))
|
return redirect(url_for('home'))
|
||||||
|
|
||||||
# Get user's active sessions with process info and scan counts
|
# Check if user wants to see archived/deleted sessions
|
||||||
active_sessions = query_db('''
|
show_archived = request.args.get('show_archived') == '1'
|
||||||
SELECT cs.*, cp.process_name, cp.process_key
|
|
||||||
FROM cons_sessions cs
|
|
||||||
JOIN cons_processes cp ON cs.process_id = cp.id
|
|
||||||
WHERE cs.created_by = ? AND cs.status = 'active'
|
|
||||||
ORDER BY cs.created_at DESC
|
|
||||||
''', [user_id])
|
|
||||||
|
|
||||||
# Get scan counts for each session from their dynamic tables
|
# Get sessions based on filter
|
||||||
|
if show_archived:
|
||||||
|
# Show active + archived + last 20 deleted
|
||||||
|
all_sessions = query_db('''
|
||||||
|
SELECT cs.*, cp.process_name, cp.process_key
|
||||||
|
FROM cons_sessions cs
|
||||||
|
JOIN cons_processes cp ON cs.process_id = cp.id
|
||||||
|
WHERE cs.created_by = ?
|
||||||
|
ORDER BY
|
||||||
|
CASE cs.status
|
||||||
|
WHEN 'active' THEN 1
|
||||||
|
WHEN 'archived' THEN 2
|
||||||
|
WHEN 'deleted' THEN 3
|
||||||
|
END,
|
||||||
|
cs.created_at DESC
|
||||||
|
''', [user_id])
|
||||||
|
|
||||||
|
# Separate active, archived, and deleted (limit deleted to 20)
|
||||||
|
active_sessions = [s for s in all_sessions if s['status'] == 'active']
|
||||||
|
archived_sessions = [s for s in all_sessions if s['status'] == 'archived']
|
||||||
|
deleted_sessions = [s for s in all_sessions if s['status'] == 'deleted'][:20]
|
||||||
|
|
||||||
|
combined_sessions = active_sessions + archived_sessions + deleted_sessions
|
||||||
|
else:
|
||||||
|
# Show only active sessions
|
||||||
|
combined_sessions = query_db('''
|
||||||
|
SELECT cs.*, cp.process_name, cp.process_key
|
||||||
|
FROM cons_sessions cs
|
||||||
|
JOIN cons_processes cp ON cs.process_id = cp.id
|
||||||
|
WHERE cs.created_by = ? AND cs.status = 'active'
|
||||||
|
ORDER BY cs.created_at DESC
|
||||||
|
''', [user_id])
|
||||||
|
|
||||||
|
# Get scan counts and header values for each session
|
||||||
sessions_with_counts = []
|
sessions_with_counts = []
|
||||||
for sess in active_sessions:
|
for sess in combined_sessions:
|
||||||
|
sess_dict = dict(sess)
|
||||||
|
|
||||||
|
# Get scan count
|
||||||
table_name = get_detail_table_name(sess['process_key'])
|
table_name = get_detail_table_name(sess['process_key'])
|
||||||
try:
|
try:
|
||||||
count_result = query_db(f'''
|
count_result = query_db(f'''
|
||||||
SELECT COUNT(*) as scan_count FROM {table_name}
|
SELECT COUNT(*) as scan_count FROM {table_name}
|
||||||
WHERE session_id = ? AND is_deleted = 0
|
WHERE session_id = ? AND is_deleted = 0
|
||||||
''', [sess['id']], one=True)
|
''', [sess['id']], one=True)
|
||||||
sess_dict = dict(sess)
|
|
||||||
sess_dict['scan_count'] = count_result['scan_count'] if count_result else 0
|
sess_dict['scan_count'] = count_result['scan_count'] if count_result else 0
|
||||||
except:
|
except:
|
||||||
sess_dict = dict(sess)
|
|
||||||
sess_dict['scan_count'] = 0
|
sess_dict['scan_count'] = 0
|
||||||
|
|
||||||
|
# Get first 5 required header fields with their values
|
||||||
|
header_fields = query_db('''
|
||||||
|
SELECT cpf.field_label, cshv.field_value
|
||||||
|
FROM cons_process_fields cpf
|
||||||
|
LEFT JOIN cons_session_header_values cshv
|
||||||
|
ON cpf.id = cshv.field_id AND cshv.session_id = ?
|
||||||
|
WHERE cpf.process_id = ?
|
||||||
|
AND cpf.table_type = 'header'
|
||||||
|
AND cpf.is_required = 1
|
||||||
|
AND cpf.is_active = 1
|
||||||
|
ORDER BY cpf.sort_order, cpf.id
|
||||||
|
LIMIT 5
|
||||||
|
''', [sess['id'], sess['process_id']])
|
||||||
|
|
||||||
|
sess_dict['header_preview'] = header_fields
|
||||||
sessions_with_counts.append(sess_dict)
|
sessions_with_counts.append(sess_dict)
|
||||||
|
|
||||||
# Get available process types for creating new sessions
|
# Get available process types for creating new sessions
|
||||||
@@ -668,8 +712,9 @@ def register_routes(bp):
|
|||||||
''')
|
''')
|
||||||
|
|
||||||
return render_template('conssheets/staff_index.html',
|
return render_template('conssheets/staff_index.html',
|
||||||
sessions=sessions_with_counts,
|
sessions=sessions_with_counts,
|
||||||
processes=processes)
|
processes=processes,
|
||||||
|
show_archived=show_archived)
|
||||||
|
|
||||||
|
|
||||||
@bp.route('/new/<int:process_id>', methods=['GET', 'POST'])
|
@bp.route('/new/<int:process_id>', methods=['GET', 'POST'])
|
||||||
@@ -982,6 +1027,60 @@ def register_routes(bp):
|
|||||||
|
|
||||||
return jsonify({'success': True})
|
return jsonify({'success': True})
|
||||||
|
|
||||||
|
@bp.route('/session/<int:session_id>/unarchive', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def unarchive_session(session_id):
|
||||||
|
"""Unarchive a session back to active"""
|
||||||
|
sess = query_db('SELECT * FROM cons_sessions WHERE id = ?', [session_id], one=True)
|
||||||
|
|
||||||
|
if not sess:
|
||||||
|
return jsonify({'success': False, 'message': 'Session not found'})
|
||||||
|
|
||||||
|
# Check permission
|
||||||
|
if sess['created_by'] != session['user_id'] and session['role'] not in ['owner', 'admin']:
|
||||||
|
return jsonify({'success': False, 'message': 'Permission denied'})
|
||||||
|
|
||||||
|
execute_db('UPDATE cons_sessions SET status = "active" WHERE id = ?', [session_id])
|
||||||
|
|
||||||
|
return jsonify({'success': True})
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/session/<int:session_id>/delete', methods=['POST'])
|
||||||
|
@role_required('owner', 'admin')
|
||||||
|
def delete_session(session_id):
|
||||||
|
"""Delete a session (admin only) - soft delete session and all detail rows"""
|
||||||
|
sess = query_db('SELECT cs.*, cp.process_key FROM cons_sessions cs JOIN cons_processes cp ON cs.process_id = cp.id WHERE cs.id = ?', [session_id], one=True)
|
||||||
|
|
||||||
|
if not sess:
|
||||||
|
return jsonify({'success': False, 'message': 'Session not found'})
|
||||||
|
|
||||||
|
# Update session status
|
||||||
|
execute_db('UPDATE cons_sessions SET status = "deleted" WHERE id = ?', [session_id])
|
||||||
|
|
||||||
|
# Mark all detail rows as deleted
|
||||||
|
table_name = get_detail_table_name(sess['process_key'])
|
||||||
|
execute_db(f'UPDATE {table_name} SET is_deleted = 1 WHERE session_id = ?', [session_id])
|
||||||
|
|
||||||
|
return jsonify({'success': True})
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/session/<int:session_id>/restore', methods=['POST'])
|
||||||
|
@role_required('owner', 'admin')
|
||||||
|
def restore_session(session_id):
|
||||||
|
"""Restore a deleted session (admin only) - restore session and all detail rows"""
|
||||||
|
sess = query_db('SELECT cs.*, cp.process_key FROM cons_sessions cs JOIN cons_processes cp ON cs.process_id = cp.id WHERE cs.id = ?', [session_id], one=True)
|
||||||
|
|
||||||
|
if not sess:
|
||||||
|
return jsonify({'success': False, 'message': 'Session not found'})
|
||||||
|
|
||||||
|
# Update session status to active
|
||||||
|
execute_db('UPDATE cons_sessions SET status = "active" WHERE id = ?', [session_id])
|
||||||
|
|
||||||
|
# Restore all detail rows
|
||||||
|
table_name = get_detail_table_name(sess['process_key'])
|
||||||
|
execute_db(f'UPDATE {table_name} SET is_deleted = 0 WHERE session_id = ?', [session_id])
|
||||||
|
|
||||||
|
return jsonify({'success': True})
|
||||||
|
|
||||||
@bp.route('/session/<int:session_id>/template')
|
@bp.route('/session/<int:session_id>/template')
|
||||||
@login_required
|
@login_required
|
||||||
|
|||||||
@@ -109,6 +109,12 @@
|
|||||||
<i class="fa-solid fa-file-import"></i> Bulk Import Excel
|
<i class="fa-solid fa-file-import"></i> Bulk Import Excel
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="scans-grid scan-header-row" style="--field-count: {{ detail_fields|length }};">
|
||||||
|
{% for field in detail_fields %}
|
||||||
|
<div class="scan-row-cell scan-col-header">{{ field.field_label }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
<div class="scan-row-cell scan-col-header">Status</div>
|
||||||
|
</div>
|
||||||
<div id="scansList" class="scans-grid" style="--field-count: {{ detail_fields|length }};">
|
<div id="scansList" class="scans-grid" style="--field-count: {{ detail_fields|length }};">
|
||||||
{% for scan in scans %}
|
{% for scan in scans %}
|
||||||
<div class="scan-row scan-row-{{ scan.duplicate_status }}"
|
<div class="scan-row scan-row-{{ scan.duplicate_status }}"
|
||||||
@@ -247,6 +253,7 @@ function processSmartScan(barcode, confirm = false) {
|
|||||||
|
|
||||||
// --- 1. HANDLE DUPLICATE CONFIRMATION ---
|
// --- 1. HANDLE DUPLICATE CONFIRMATION ---
|
||||||
if (data.needs_confirmation) {
|
if (data.needs_confirmation) {
|
||||||
|
playErrorBeep();
|
||||||
isSmartScan = true;
|
isSmartScan = true;
|
||||||
currentDupKeyValue = barcode;
|
currentDupKeyValue = barcode;
|
||||||
if (data.duplicate_status === 'dup_same_session') {
|
if (data.duplicate_status === 'dup_same_session') {
|
||||||
@@ -296,23 +303,23 @@ function processSmartScan(barcode, confirm = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- 3. STANDARD SUCCESS/FAIL ---
|
// --- 3. STANDARD SUCCESS/FAIL ---
|
||||||
if(smartInput) {
|
|
||||||
smartInput.value = '';
|
|
||||||
smartInput.focus();
|
|
||||||
}
|
|
||||||
feedbackArea.style.display = 'block';
|
|
||||||
|
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
feedbackArea.style.background = 'rgba(40, 167, 69, 0.2)';
|
if (data.detail_id && data.data) {
|
||||||
feedbackArea.style.border = '1px solid #28a745';
|
addScanToList(data.detail_id, data.data, data.data.duplicate_status);
|
||||||
feedbackText.style.color = '#28a745';
|
}
|
||||||
feedbackText.textContent = data.message;
|
feedbackArea.style.display = 'none';
|
||||||
if (data.message.includes('Saved')) setTimeout(() => location.reload(), 800);
|
if(smartInput) {
|
||||||
|
smartInput.value = '';
|
||||||
|
smartInput.focus();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
playErrorBeep();
|
||||||
|
feedbackArea.style.display = 'block';
|
||||||
feedbackArea.style.background = 'rgba(220, 53, 69, 0.2)';
|
feedbackArea.style.background = 'rgba(220, 53, 69, 0.2)';
|
||||||
feedbackArea.style.border = '1px solid #dc3545';
|
feedbackArea.style.border = '1px solid #dc3545';
|
||||||
feedbackText.style.color = '#dc3545';
|
feedbackText.style.color = '#dc3545';
|
||||||
feedbackText.textContent = data.message;
|
feedbackText.textContent = data.message;
|
||||||
|
if(smartInput) smartInput.focus();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
@@ -320,7 +327,7 @@ function processSmartScan(barcode, confirm = false) {
|
|||||||
console.error(err); // Log it, don't popup alert to annoy user
|
console.error(err); // Log it, don't popup alert to annoy user
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Function to handle the "Save" click from the Details Modal
|
|
||||||
// Function to handle the "Save" click from the Details Modal
|
// Function to handle the "Save" click from the Details Modal
|
||||||
function saveSmartScanData() {
|
function saveSmartScanData() {
|
||||||
// 1. Validate we have the original barcode
|
// 1. Validate we have the original barcode
|
||||||
@@ -357,8 +364,12 @@ function saveSmartScanData() {
|
|||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
document.getElementById('fieldsModal').style.display = 'none';
|
document.getElementById('fieldsModal').style.display = 'none';
|
||||||
// Optional: Small delay to let the user see it worked
|
if (data.detail_id && data.data) {
|
||||||
setTimeout(() => location.reload(), 300);
|
addScanToList(data.detail_id, data.data, data.data.duplicate_status);
|
||||||
|
}
|
||||||
|
currentDupKeyValue = '';
|
||||||
|
smartInput.value = '';
|
||||||
|
smartInput.focus();
|
||||||
} else if (data.needs_input) {
|
} else if (data.needs_input) {
|
||||||
alert("Error: Please fill in all required fields.");
|
alert("Error: Please fill in all required fields.");
|
||||||
} else {
|
} else {
|
||||||
@@ -372,6 +383,25 @@ function resetSmartScan() {
|
|||||||
document.getElementById('smartScanner').focus();
|
document.getElementById('smartScanner').focus();
|
||||||
document.getElementById('routerFeedback').style.display = 'none';
|
document.getElementById('routerFeedback').style.display = 'none';
|
||||||
}
|
}
|
||||||
|
// --- ERROR BEEP ---
|
||||||
|
function playErrorBeep() {
|
||||||
|
try {
|
||||||
|
const ctx = new (window.AudioContext || window.webkitAudioContext)();
|
||||||
|
const oscillator = ctx.createOscillator();
|
||||||
|
const gainNode = ctx.createGain();
|
||||||
|
oscillator.connect(gainNode);
|
||||||
|
gainNode.connect(ctx.destination);
|
||||||
|
oscillator.type = 'square';
|
||||||
|
oscillator.frequency.setValueAtTime(520, ctx.currentTime);
|
||||||
|
gainNode.gain.setValueAtTime(0.3, ctx.currentTime);
|
||||||
|
gainNode.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.4);
|
||||||
|
oscillator.start(ctx.currentTime);
|
||||||
|
oscillator.stop(ctx.currentTime + 0.4);
|
||||||
|
} catch(e) {
|
||||||
|
console.warn('Audio not available:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Standard variables
|
// Standard variables
|
||||||
let currentDupKeyValue = '';
|
let currentDupKeyValue = '';
|
||||||
let currentDuplicateStatus = '';
|
let currentDuplicateStatus = '';
|
||||||
|
|||||||
@@ -30,10 +30,16 @@
|
|||||||
<!-- Active Sessions -->
|
<!-- Active Sessions -->
|
||||||
{% if sessions %}
|
{% if sessions %}
|
||||||
<div class="section-card">
|
<div class="section-card">
|
||||||
<h2 class="section-title">📋 My Active Sessions</h2>
|
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: var(--space-md);">
|
||||||
|
<h2 class="section-title">📋 My Sessions</h2>
|
||||||
|
<label class="checkbox-toggle">
|
||||||
|
<input type="checkbox" id="showArchivedToggle" {% if show_archived %}checked{% endif %} onchange="toggleArchived(this.checked)">
|
||||||
|
<span>Show archived/deleted</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<div class="sessions-list">
|
<div class="sessions-list">
|
||||||
{% for s in sessions %}
|
{% for s in sessions %}
|
||||||
<div class="session-list-item-container">
|
<div class="session-list-item-container session-status-{{ s.status }}">
|
||||||
<a href="{{ url_for('conssheets.scan_session', session_id=s.id) }}" class="session-list-item">
|
<a href="{{ url_for('conssheets.scan_session', session_id=s.id) }}" class="session-list-item">
|
||||||
<div class="session-list-info">
|
<div class="session-list-info">
|
||||||
<h3 class="session-list-name">{{ s.process_name }}</h3>
|
<h3 class="session-list-name">{{ s.process_name }}</h3>
|
||||||
@@ -42,14 +48,52 @@
|
|||||||
<span>•</span>
|
<span>•</span>
|
||||||
<span>{{ s.scan_count or 0 }} lots scanned</span>
|
<span>{{ s.scan_count or 0 }} lots scanned</span>
|
||||||
</div>
|
</div>
|
||||||
|
{% if s.header_preview %}
|
||||||
|
<div class="session-list-meta" style="margin-top: var(--space-xs); font-size: 0.8rem;">
|
||||||
|
{% for hf in s.header_preview %}
|
||||||
|
<span><strong>{{ hf.field_label }}:</strong> {{ hf.field_value or '—' }}</span>
|
||||||
|
{% if not loop.last %}<span>•</span>{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if s.status == 'archived' %}
|
||||||
|
<div class="session-list-meta" style="margin-top: var(--space-xs);">
|
||||||
|
<span style="color: var(--color-warning);">📦 Archived</span>
|
||||||
|
</div>
|
||||||
|
{% elif s.status == 'deleted' %}
|
||||||
|
<div class="session-list-meta" style="margin-top: var(--space-xs);">
|
||||||
|
<span style="color: var(--color-danger);">🗑️ Deleted</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="session-list-action">
|
<div class="session-list-action">
|
||||||
<span class="arrow-icon">→</span>
|
<span class="arrow-icon">→</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
<button class="btn-archive" onclick="archiveSession({{ s.id }}, '{{ s.process_name }}')" title="Archive this session">
|
|
||||||
🗑️
|
{% if s.status == 'active' %}
|
||||||
</button>
|
<button class="btn-session-action btn-archive" onclick="archiveSession({{ s.id }}, '{{ s.process_name }}')" title="Archive this session">
|
||||||
|
📦
|
||||||
|
</button>
|
||||||
|
{% if session.role in ['owner', 'admin'] %}
|
||||||
|
<button class="btn-session-action btn-delete" onclick="deleteSession({{ s.id }}, '{{ s.process_name }}')" title="Delete this session">
|
||||||
|
❌
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% elif s.status == 'archived' %}
|
||||||
|
<button class="btn-session-action btn-unarchive" onclick="unarchiveSession({{ s.id }}, '{{ s.process_name }}')" title="Restore to active">
|
||||||
|
↩️
|
||||||
|
</button>
|
||||||
|
{% if session.role in ['owner', 'admin'] %}
|
||||||
|
<button class="btn-session-action btn-delete" onclick="deleteSession({{ s.id }}, '{{ s.process_name }}')" title="Delete this session">
|
||||||
|
❌
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% elif s.status == 'deleted' and session.role in ['owner', 'admin'] %}
|
||||||
|
<button class="btn-session-action btn-restore" onclick="restoreSession({{ s.id }}, '{{ s.process_name }}')" title="Restore from deleted">
|
||||||
|
🔄
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
@@ -63,89 +107,20 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<!-- Styles moved to static/css/style.css for maintainability -->
|
||||||
.new-session-section {
|
|
||||||
background: var(--color-surface);
|
|
||||||
border: 2px solid var(--color-border);
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
padding: var(--space-xl);
|
|
||||||
margin-bottom: var(--space-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.process-buttons {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: var(--space-md);
|
|
||||||
margin-top: var(--space-md);
|
|
||||||
}
|
|
||||||
|
|
||||||
.section-card {
|
|
||||||
margin-top: var(--space-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.session-list-item-container {
|
|
||||||
display: flex;
|
|
||||||
align-items: stretch;
|
|
||||||
gap: var(--space-sm);
|
|
||||||
margin-bottom: var(--space-sm);
|
|
||||||
}
|
|
||||||
|
|
||||||
.session-list-item {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
background: var(--color-surface);
|
|
||||||
border: 2px solid var(--color-border);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
padding: var(--space-lg);
|
|
||||||
text-decoration: none;
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.session-list-item:hover {
|
|
||||||
border-color: var(--color-primary);
|
|
||||||
transform: translateX(4px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.session-list-name {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--color-text);
|
|
||||||
margin: 0 0 var(--space-xs) 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.session-list-meta {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--space-sm);
|
|
||||||
font-size: 0.875rem;
|
|
||||||
color: var(--color-text-muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.arrow-icon {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
color: var(--color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-archive {
|
|
||||||
background: var(--color-surface);
|
|
||||||
border: 2px solid var(--color-border);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
padding: var(--space-md);
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1.25rem;
|
|
||||||
transition: var(--transition);
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-archive:hover {
|
|
||||||
border-color: var(--color-danger);
|
|
||||||
background: rgba(255, 51, 102, 0.1);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
function toggleArchived(checked) {
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
if (checked) {
|
||||||
|
url.searchParams.set('show_archived', '1');
|
||||||
|
} else {
|
||||||
|
url.searchParams.delete('show_archived');
|
||||||
|
}
|
||||||
|
window.location.href = url.toString();
|
||||||
|
}
|
||||||
|
|
||||||
function archiveSession(sessionId, processName) {
|
function archiveSession(sessionId, processName) {
|
||||||
if (!confirm(`Archive this ${processName} session?\n\nYou can still view it later from the admin panel.`)) {
|
if (!confirm(`Archive this ${processName} session?\n\nYou can restore it later.`)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,5 +137,62 @@ function archiveSession(sessionId, processName) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function unarchiveSession(sessionId, processName) {
|
||||||
|
if (!confirm(`Restore ${processName} session to active?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(`/conssheets/session/${sessionId}/unarchive`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'}
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert(data.message || 'Error restoring session');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteSession(sessionId, processName) {
|
||||||
|
if (!confirm(`⚠️ DELETE ${processName} session?\n\nThis will mark all scanned data as deleted.\nThis action cannot be undone easily.\n\nAre you absolutely sure?`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(`/conssheets/session/${sessionId}/delete`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'}
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert(data.message || 'Error deleting session');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function restoreSession(sessionId, processName) {
|
||||||
|
if (!confirm(`Restore ${processName} from deleted?\n\nThis will reactivate the session and all its data.`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch(`/conssheets/session/${sessionId}/restore`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type': 'application/json'}
|
||||||
|
})
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert(data.message || 'Error restoring session');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -707,6 +707,138 @@ body {
|
|||||||
color: var(--color-primary);
|
color: var(--color-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Session List - Archive/Delete Actions */
|
||||||
|
|
||||||
|
.new-session-section {
|
||||||
|
background: var(--color-surface);
|
||||||
|
border: 2px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: var(--space-xl);
|
||||||
|
margin-bottom: var(--space-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.process-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--space-md);
|
||||||
|
margin-top: var(--space-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-card {
|
||||||
|
margin-top: var(--space-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-list-item-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 0; /* Remove gap */
|
||||||
|
margin-bottom: var(--space-sm);
|
||||||
|
border: 2px solid var(--color-border);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: var(--color-surface);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-list-item {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
background: transparent; /* Remove duplicate background */
|
||||||
|
border: none; /* Remove border since container has it */
|
||||||
|
border-radius: 0;
|
||||||
|
padding: var(--space-lg);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-list-item:hover {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
transform: translateX(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-list-info {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-list-action {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-list-meta {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-sm);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox-toggle input[type="checkbox"] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-session-action {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-left: 2px solid var(--color-border);
|
||||||
|
border-radius: 0;
|
||||||
|
padding: var(--space-xl);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 2rem;
|
||||||
|
transition: var(--transition);
|
||||||
|
min-width: 80px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-session-action.btn-archive:hover {
|
||||||
|
border-color: var(--color-warning);
|
||||||
|
background: rgba(255, 170, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-session-action.btn-delete:hover {
|
||||||
|
border-color: var(--color-danger);
|
||||||
|
background: rgba(255, 51, 102, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-session-action.btn-unarchive:hover {
|
||||||
|
border-color: var(--color-primary);
|
||||||
|
background: rgba(0, 212, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-session-action.btn-restore:hover {
|
||||||
|
border-color: var(--color-success);
|
||||||
|
background: rgba(0, 255, 136, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-status-archived {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-status-archived .session-list-item-container {
|
||||||
|
border-color: var(--color-warning);
|
||||||
|
background: rgba(255, 170, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-status-deleted {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.session-status-deleted .session-list-item-container {
|
||||||
|
border-color: var(--color-danger);
|
||||||
|
background: rgba(255, 51, 102, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
/* ==================== FORM CONTAINER ==================== */
|
/* ==================== FORM CONTAINER ==================== */
|
||||||
|
|
||||||
.form-container {
|
.form-container {
|
||||||
@@ -1303,9 +1435,25 @@ body {
|
|||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scan-header-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(var(--field-count), 1fr) auto;
|
||||||
|
gap: var(--space-md);
|
||||||
|
padding: var(--space-xs) var(--space-md);
|
||||||
|
max-height: none;
|
||||||
|
overflow-y: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scan-col-header {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
.scan-row {
|
.scan-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 2fr 1fr 1fr 1.5fr;
|
grid-template-columns: repeat(var(--field-count), 1fr) 1fr;
|
||||||
gap: var(--space-md);
|
gap: var(--space-md);
|
||||||
padding: var(--space-md);
|
padding: var(--space-md);
|
||||||
background: var(--color-bg);
|
background: var(--color-bg);
|
||||||
|
|||||||
Reference in New Issue
Block a user