diff --git a/app.py b/app.py index 2fbba0e..b78a078 100644 --- a/app.py +++ b/app.py @@ -162,43 +162,8 @@ def install_module(module_key): result = module_manager.install_module(module_key) - # Hot-reload: Register the blueprint immediately if installation succeeded if result['success']: - try: - from pathlib import Path - import importlib.util - import sys - - module = module_manager.get_module_by_key(module_key) - if module: - init_path = Path(module['path']) / '__init__.py' - - # Import the module - spec = importlib.util.spec_from_file_location( - f"modules.{module_key}", - init_path - ) - module_package = importlib.util.module_from_spec(spec) - sys.modules[spec.name] = module_package - spec.loader.exec_module(module_package) - - # Create and register blueprint - if hasattr(module_package, 'create_blueprint'): - blueprint = module_package.create_blueprint() - app.register_blueprint(blueprint) - print(f"šŸ”„ Hot-loaded: {module['name']} at {module.get('routes_prefix')}") - result['message'] += ' (Module loaded - no restart needed!)' - else: - print(f"āš ļø Module {module_key} missing create_blueprint()") - result['message'] += ' (Restart required - missing create_blueprint)' - else: - print(f"āš ļø Could not find module {module_key} after installation") - result['message'] += ' (Restart required - module not found)' - except Exception as e: - print(f"āŒ Hot-reload failed for {module_key}: {e}") - import traceback - traceback.print_exc() - result['message'] += f' (Restart required - hot-reload failed)' + result['restart_required'] = True return jsonify(result) @@ -209,7 +174,11 @@ def uninstall_module(module_key): if session.get('role') not in ['owner', 'admin']: return jsonify({'success': False, 'message': 'Access denied'}), 403 - result = module_manager.uninstall_module(module_key, drop_tables=True) + # Check if user wants to keep data + keep_data = request.args.get('keep_data') == 'true' + drop_tables = not keep_data + + result = module_manager.uninstall_module(module_key, drop_tables=drop_tables) return jsonify(result) @@ -234,7 +203,39 @@ def deactivate_module(module_key): result = module_manager.deactivate_module(module_key) return jsonify(result) - +@app.route('/admin/restart', methods=['POST']) +@login_required +def restart_server(): + """Restart the Flask server""" + if session.get('role') not in ['owner', 'admin']: + return jsonify({'success': False, 'message': 'Access denied'}), 403 + + import os + import sys + + try: + print("\nšŸ”„ Server restart requested by admin...") + + # Return response first + response = jsonify({'success': True, 'message': 'Server restarting...'}) + + # Schedule restart after response is sent + def restart(): + import time + time.sleep(0.5) # Give time for response to send + + if os.name == 'nt': # Windows + os.execv(sys.executable, ['python'] + sys.argv) + else: # Linux/Mac + os.execv(sys.executable, [sys.executable] + sys.argv) + + from threading import Thread + Thread(target=restart).start() + + return response + + except Exception as e: + return jsonify({'success': False, 'message': f'Restart failed: {str(e)}'}) # ==================== PWA SUPPORT ROUTES ==================== @app.route('/manifest.json') diff --git a/modules/conssheets/manifest.json b/modules/conssheets/manifest.json index 3588b4c..c9224d8 100644 --- a/modules/conssheets/manifest.json +++ b/modules/conssheets/manifest.json @@ -4,6 +4,7 @@ "version": "1.0.0", "author": "STUFF", "description": "Production lot tracking and consumption reporting with Excel export", + "icon": "fa-clipboard-list", "requires_roles": ["owner", "admin", "staff"], "routes_prefix": "/conssheets", "has_migrations": true, diff --git a/modules/conssheets/routes.py b/modules/conssheets/routes.py index f9db78b..b7baf04 100644 --- a/modules/conssheets/routes.py +++ b/modules/conssheets/routes.py @@ -1,12 +1,14 @@ """ Consumption Sheets Module - Routes -Converted from cons_sheets.py +Converted from conssheets.py """ from flask import render_template, request, redirect, url_for, flash, jsonify, session, send_file from db import query_db, execute_db from utils import login_required, role_required from datetime import datetime +import sqlite3 import io +import os def register_routes(bp): @@ -16,7 +18,7 @@ def register_routes(bp): # CONSUMPTION SHEETS ROUTES # ========================================================================= - @bp.route('/admin/consumption-sheets') + @bp.route('/admin') @role_required('owner', 'admin') def admin_processes(): """List all consumption sheet process types (Active or Archived)""" @@ -33,7 +35,7 @@ def register_routes(bp): ORDER BY cp.process_name ASC ''', [is_active_val]) - return render_template('cons_sheets/admin_processes.html', + return render_template('conssheets/admin_processes.html', processes=processes, showing_archived=show_archived) @@ -47,7 +49,7 @@ def register_routes(bp): if not process_name: flash('Process name is required', 'danger') - return redirect(url_for('cons_sheets.create_process')) + return redirect(url_for('conssheets.create_process')) # Generate process_key from name (lowercase, underscores) process_key = process_name.lower().replace(' ', '_').replace('-', '_') @@ -58,7 +60,7 @@ def register_routes(bp): existing = query_db('SELECT id FROM cons_processes WHERE process_key = ?', [process_key], one=True) if existing: flash(f'A process with key "{process_key}" already exists', 'danger') - return redirect(url_for('cons_sheets.create_process')) + return redirect(url_for('conssheets.create_process')) process_id = execute_db(''' INSERT INTO cons_processes (process_key, process_name, created_by) @@ -69,15 +71,15 @@ def register_routes(bp): create_process_detail_table(process_key) flash(f'Process "{process_name}" created successfully!', 'success') - return redirect(url_for('cons_sheets.process_detail', process_id=process_id)) + return redirect(url_for('conssheets.process_detail', process_id=process_id)) - return render_template('cons_sheets/create_process.html') + return render_template('conssheets/create_process.html') def get_db_path(): """Get the database path""" - db_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'database', 'scanlook.db') + db_path = 'database/scanlook.db' print(f"DEBUG: Database path is: {db_path}") print(f"DEBUG: Path exists: {os.path.exists(db_path)}") return db_path @@ -167,7 +169,7 @@ def register_routes(bp): if not process: flash('Process not found', 'danger') - return redirect(url_for('cons_sheets.admin_processes')) + return redirect(url_for('conssheets.admin_processes')) # Soft delete: Set is_active = 0 # The existing admin_processes route already filters for is_active=1, @@ -175,7 +177,7 @@ def register_routes(bp): execute_db('UPDATE cons_processes SET is_active = 0 WHERE id = ?', [process_id]) flash(f'Process "{process["process_name"]}" has been deleted.', 'success') - return redirect(url_for('cons_sheets.admin_processes')) + return redirect(url_for('conssheets.admin_processes')) @bp.route('/admin/consumption-sheets//restore', methods=['POST']) @@ -184,7 +186,7 @@ def register_routes(bp): """Restore a soft-deleted process type""" execute_db('UPDATE cons_processes SET is_active = 1 WHERE id = ?', [process_id]) flash('Process has been restored.', 'success') - return redirect(url_for('cons_sheets.admin_processes', archived=1)) + return redirect(url_for('conssheets.admin_processes', archived=1)) @@ -196,7 +198,7 @@ def register_routes(bp): if not process: flash('Process not found', 'danger') - return redirect(url_for('cons_sheets.admin_processes')) + return redirect(url_for('conssheets.admin_processes')) # Get header fields header_fields = query_db(''' @@ -212,7 +214,7 @@ def register_routes(bp): ORDER BY sort_order, id ''', [process_id]) - return render_template('cons_sheets/process_detail.html', + return render_template('conssheets/process_detail.html', process=process, header_fields=header_fields, detail_fields=detail_fields) @@ -226,7 +228,7 @@ def register_routes(bp): if not process: flash('Process not found', 'danger') - return redirect(url_for('cons_sheets.admin_processes')) + return redirect(url_for('conssheets.admin_processes')) # Get header fields header_fields = query_db(''' @@ -242,7 +244,7 @@ def register_routes(bp): ORDER BY sort_order, id ''', [process_id]) - return render_template('cons_sheets/process_fields.html', + return render_template('conssheets/process_fields.html', process=process, header_fields=header_fields, detail_fields=detail_fields) @@ -256,7 +258,7 @@ def register_routes(bp): if not process: flash('Process not found', 'danger') - return redirect(url_for('cons_sheets.admin_processes')) + return redirect(url_for('conssheets.admin_processes')) # Get all active fields for mapping display header_fields = query_db(''' @@ -271,7 +273,7 @@ def register_routes(bp): ORDER BY sort_order, id ''', [process_id]) - return render_template('cons_sheets/process_template.html', + return render_template('conssheets/process_template.html', process=process, header_fields=header_fields, detail_fields=detail_fields) @@ -285,21 +287,21 @@ def register_routes(bp): if not process: flash('Process not found', 'danger') - return redirect(url_for('cons_sheets.admin_processes')) + return redirect(url_for('conssheets.admin_processes')) if 'template_file' not in request.files: flash('No file selected', 'danger') - return redirect(url_for('cons_sheets.process_template', process_id=process_id)) + return redirect(url_for('conssheets.process_template', process_id=process_id)) file = request.files['template_file'] if file.filename == '': flash('No file selected', 'danger') - return redirect(url_for('cons_sheets.process_template', process_id=process_id)) + return redirect(url_for('conssheets.process_template', process_id=process_id)) if not file.filename.endswith('.xlsx'): flash('Only .xlsx files are allowed', 'danger') - return redirect(url_for('cons_sheets.process_template', process_id=process_id)) + return redirect(url_for('conssheets.process_template', process_id=process_id)) # Read file as binary template_data = file.read() @@ -313,7 +315,7 @@ def register_routes(bp): ''', [template_data, filename, process_id]) flash(f'Template "{filename}" uploaded successfully!', 'success') - return redirect(url_for('cons_sheets.process_template', process_id=process_id)) + return redirect(url_for('conssheets.process_template', process_id=process_id)) @bp.route('/admin/consumption-sheets//template/settings', methods=['POST']) @@ -324,7 +326,7 @@ def register_routes(bp): if not process: flash('Process not found', 'danger') - return redirect(url_for('cons_sheets.admin_processes')) + return redirect(url_for('conssheets.admin_processes')) rows_per_page = request.form.get('rows_per_page', 30) detail_start_row = request.form.get('detail_start_row', 10) @@ -340,11 +342,11 @@ def register_routes(bp): if not page_height: flash('Page Height is required for the new strategy', 'danger') - return redirect(url_for('cons_sheets.process_template', process_id=process_id)) + return redirect(url_for('conssheets.process_template', process_id=process_id)) except ValueError: flash('Invalid number values', 'danger') - return redirect(url_for('cons_sheets.process_template', process_id=process_id)) + return redirect(url_for('conssheets.process_template', process_id=process_id)) # Update query - We ignore detail_end_row (leave it as is or null) execute_db(''' @@ -355,7 +357,7 @@ def register_routes(bp): ''', [rows_per_page, detail_start_row, page_height, print_start_col, print_end_col, process_id]) flash('Settings updated successfully!', 'success') - return redirect(url_for('cons_sheets.process_template', process_id=process_id)) + return redirect(url_for('conssheets.process_template', process_id=process_id)) @bp.route('/admin/consumption-sheets//template/download') @role_required('owner', 'admin') @@ -367,7 +369,7 @@ def register_routes(bp): if not process or not process['template_file']: flash('No template found', 'danger') - return redirect(url_for('cons_sheets.process_template', process_id=process_id)) + return redirect(url_for('conssheets.process_template', process_id=process_id)) return Response( process['template_file'], @@ -382,13 +384,13 @@ def register_routes(bp): """Add a new field to a process""" if table_type not in ['header', 'detail']: flash('Invalid table type', 'danger') - return redirect(url_for('cons_sheets.process_fields', process_id=process_id)) + return redirect(url_for('conssheets.process_fields', process_id=process_id)) process = query_db('SELECT * FROM cons_processes WHERE id = ?', [process_id], one=True) if not process: flash('Process not found', 'danger') - return redirect(url_for('cons_sheets.admin_processes')) + return redirect(url_for('conssheets.admin_processes')) if request.method == 'POST': field_label = request.form.get('field_label', '').strip() @@ -399,7 +401,7 @@ def register_routes(bp): if not field_label: flash('Field label is required', 'danger') - return redirect(url_for('cons_sheets.add_field', process_id=process_id, table_type=table_type)) + return redirect(url_for('conssheets.add_field', process_id=process_id, table_type=table_type)) # Generate field_name from label (lowercase, underscores) field_name = field_label.lower().replace(' ', '_').replace('-', '_') @@ -413,7 +415,7 @@ def register_routes(bp): if existing: flash(f'A field with name "{field_name}" already exists', 'danger') - return redirect(url_for('cons_sheets.add_field', process_id=process_id, table_type=table_type)) + return redirect(url_for('conssheets.add_field', process_id=process_id, table_type=table_type)) # Get next sort_order max_sort = query_db(''' @@ -438,9 +440,9 @@ def register_routes(bp): add_column_to_detail_table(process['process_key'], field_name, field_type) flash(f'Field "{field_label}" added successfully!', 'success') - return redirect(url_for('cons_sheets.process_fields', process_id=process_id)) + return redirect(url_for('conssheets.process_fields', process_id=process_id)) - return render_template('cons_sheets/add_field.html', + return render_template('conssheets/add_field.html', process=process, table_type=table_type) @@ -454,7 +456,7 @@ def register_routes(bp): if not process or not field: flash('Process or field not found', 'danger') - return redirect(url_for('cons_sheets.admin_processes')) + return redirect(url_for('conssheets.admin_processes')) if request.method == 'POST': field_label = request.form.get('field_label', '').strip() @@ -466,7 +468,7 @@ def register_routes(bp): if not field_label: flash('Field label is required', 'danger') - return redirect(url_for('cons_sheets.edit_field', process_id=process_id, field_id=field_id)) + return redirect(url_for('conssheets.edit_field', process_id=process_id, field_id=field_id)) execute_db(''' UPDATE cons_process_fields @@ -475,9 +477,9 @@ def register_routes(bp): ''', [field_label, field_type, int(max_length) if max_length else None, is_required, is_duplicate_key, excel_cell or None, field_id]) flash(f'Field "{field_label}" updated successfully!', 'success') - return redirect(url_for('cons_sheets.process_fields', process_id=process_id)) + return redirect(url_for('conssheets.process_fields', process_id=process_id)) - return render_template('cons_sheets/edit_field.html', + return render_template('conssheets/edit_field.html', process=process, field=field) @@ -520,7 +522,7 @@ def register_routes(bp): ''', [process_id], one=True) - @bp.route('/cons-sheets') + @bp.route('/') @login_required def index(): """Consumption Sheets module landing - show user's sessions""" @@ -530,7 +532,7 @@ def register_routes(bp): has_access = query_db(''' SELECT 1 FROM UserModules um JOIN Modules m ON um.module_id = m.module_id - WHERE um.user_id = ? AND m.module_key = 'cons_sheets' AND m.is_active = 1 + WHERE um.user_id = ? AND m.module_key = 'conssheets' AND m.is_active = 1 ''', [user_id], one=True) if not has_access: @@ -567,12 +569,12 @@ def register_routes(bp): SELECT * FROM cons_processes WHERE is_active = 1 ORDER BY process_name ''') - return render_template('cons_sheets/staff_index.html', + return render_template('conssheets/staff_index.html', sessions=sessions_with_counts, processes=processes) - @bp.route('/cons-sheets/new/', methods=['GET', 'POST']) + @bp.route('/new/', methods=['GET', 'POST']) @login_required def new_session(process_id): """Create a new scanning session - enter header info""" @@ -580,7 +582,7 @@ def register_routes(bp): if not process: flash('Process not found', 'danger') - return redirect(url_for('cons_sheets.index')) + return redirect(url_for('conssheets.index')) # Get header fields for this process header_fields = query_db(''' @@ -600,7 +602,7 @@ def register_routes(bp): if missing_required: flash(f'Required fields missing: {", ".join(missing_required)}', 'danger') - return render_template('cons_sheets/new_session.html', + return render_template('conssheets/new_session.html', process=process, header_fields=header_fields, form_data=request.form) @@ -621,15 +623,15 @@ def register_routes(bp): ''', [session_id, field['id'], value]) flash('Session created! Start scanning lots.', 'success') - return redirect(url_for('cons_sheets.scan_session', session_id=session_id)) + return redirect(url_for('conssheets.scan_session', session_id=session_id)) - return render_template('cons_sheets/new_session.html', + return render_template('conssheets/new_session.html', process=process, header_fields=header_fields, form_data={}) - @bp.route('/cons-sheets/session/') + @bp.route('/session/') @login_required def scan_session(session_id): """Main scanning interface for a session""" @@ -643,11 +645,11 @@ def register_routes(bp): if not sess: flash('Session not found', 'danger') - return redirect(url_for('cons_sheets.index')) + return redirect(url_for('conssheets.index')) if sess['status'] == 'archived': flash('This session has been archived', 'warning') - return redirect(url_for('cons_sheets.index')) + return redirect(url_for('conssheets.index')) # Get header values for display header_values = query_db(''' @@ -680,7 +682,7 @@ def register_routes(bp): dup_key_field_row = get_duplicate_key_field(sess['process_id']) dup_key_field = dict(dup_key_field_row) if dup_key_field_row else None - return render_template('cons_sheets/scan_session.html', + return render_template('conssheets/scan_session.html', session=sess, header_values=header_values, scans=scans, @@ -688,7 +690,7 @@ def register_routes(bp): dup_key_field=dup_key_field) - @bp.route('/cons-sheets/session//scan', methods=['POST']) + @bp.route('/session//scan', methods=['POST']) @login_required def scan_lot(session_id): """Process a scan with duplicate detection using dynamic tables""" @@ -823,7 +825,7 @@ def register_routes(bp): }) - @bp.route('/cons-sheets/session//detail/') + @bp.route('/session//detail/') @login_required def get_detail(session_id, detail_id): """Get detail info for editing""" @@ -852,7 +854,7 @@ def register_routes(bp): return jsonify({'success': True, 'detail': dict(detail)}) - @bp.route('/cons-sheets/session//detail//update', methods=['POST']) + @bp.route('/session//detail//update', methods=['POST']) @login_required def update_detail(session_id, detail_id): """Update a scanned detail""" @@ -909,7 +911,7 @@ def register_routes(bp): return jsonify({'success': True}) - @bp.route('/cons-sheets/session//detail//delete', methods=['POST']) + @bp.route('/session//detail//delete', methods=['POST']) @login_required def delete_detail(session_id, detail_id): """Soft-delete a scanned detail""" @@ -939,7 +941,7 @@ def register_routes(bp): return jsonify({'success': True}) - @bp.route('/cons-sheets/session//archive', methods=['POST']) + @bp.route('/session//archive', methods=['POST']) @login_required def archive_session(session_id): """Archive (soft-delete) a session""" @@ -957,7 +959,7 @@ def register_routes(bp): return jsonify({'success': True}) - @bp.route('/cons-sheets/session//template') + @bp.route('/session//template') @login_required def download_import_template(session_id): """Generate a blank Excel template for bulk import""" @@ -967,7 +969,7 @@ def register_routes(bp): # Get Process ID sess = query_db('SELECT process_id FROM cons_sessions WHERE id = ?', [session_id], one=True) - if not sess: return redirect(url_for('cons_sheets.index')) + if not sess: return redirect(url_for('conssheets.index')) # Get Detail Fields fields = query_db(''' @@ -996,7 +998,7 @@ def register_routes(bp): headers={'Content-Disposition': 'attachment; filename=import_template.xlsx'} ) - @bp.route('/cons-sheets/session//import', methods=['POST']) + @bp.route('/session//import', methods=['POST']) @login_required def import_session_data(session_id): """Bulk import detail rows from Excel""" @@ -1015,17 +1017,17 @@ def register_routes(bp): if not sess: flash('Session not found', 'danger') - return redirect(url_for('cons_sheets.index')) + return redirect(url_for('conssheets.index')) # 2. Check File if 'file' not in request.files: flash('No file uploaded', 'danger') - return redirect(url_for('cons_sheets.scan_session', session_id=session_id)) + return redirect(url_for('conssheets.scan_session', session_id=session_id)) file = request.files['file'] if file.filename == '': flash('No file selected', 'danger') - return redirect(url_for('cons_sheets.scan_session', session_id=session_id)) + return redirect(url_for('conssheets.scan_session', session_id=session_id)) try: # 3. Read Excel @@ -1051,7 +1053,7 @@ def register_routes(bp): if not col_mapping: flash('Error: No matching columns found in Excel. Please use the template.', 'danger') - return redirect(url_for('cons_sheets.scan_session', session_id=session_id)) + return redirect(url_for('conssheets.scan_session', session_id=session_id)) # 4. Process Rows table_name = f"cons_proc_{sess['process_key']}_details" @@ -1095,9 +1097,9 @@ def register_routes(bp): flash(f'Import Error: {str(e)}', 'danger') print(f"DEBUG IMPORT ERROR: {str(e)}") # Print to console for good measure - return redirect(url_for('cons_sheets.scan_session', session_id=session_id)) + return redirect(url_for('conssheets.scan_session', session_id=session_id)) - @bp.route('/cons-sheets/session//export') + @bp.route('/session//export') @login_required def export_session(session_id): """Export session: Hide Rows Strategy + Manual Column Widths""" @@ -1123,7 +1125,7 @@ def register_routes(bp): if not sess or not sess['template_file']: flash('Session or Template not found', 'danger') - return redirect(url_for('cons_sheets.index')) + return redirect(url_for('conssheets.index')) # Validation page_height = sess['page_height'] @@ -1132,7 +1134,7 @@ def register_routes(bp): if not page_height: flash('Configuration Error: Page Height is not set.', 'danger') - return redirect(url_for('cons_sheets.scan_session', session_id=session_id)) + return redirect(url_for('conssheets.scan_session', session_id=session_id)) # Get Data header_fields = query_db(''' diff --git a/modules/conssheets/templates/conssheets/add_field.html b/modules/conssheets/templates/conssheets/add_field.html index 809a57e..3e4452a 100644 --- a/modules/conssheets/templates/conssheets/add_field.html +++ b/modules/conssheets/templates/conssheets/add_field.html @@ -5,7 +5,7 @@ {% block content %}
@@ -72,7 +72,7 @@
- Cancel + Cancel
diff --git a/modules/conssheets/templates/conssheets/admin_processes.html b/modules/conssheets/templates/conssheets/admin_processes.html index 6ca23cc..b152893 100644 --- a/modules/conssheets/templates/conssheets/admin_processes.html +++ b/modules/conssheets/templates/conssheets/admin_processes.html @@ -18,18 +18,18 @@

Manage process types and templates

{% if showing_archived %} - + Return to Active List {% else %} - + View Archived Processes {% endif %} - + + New Process @@ -48,7 +48,7 @@ {% if showing_archived %}
diff --git a/modules/conssheets/templates/conssheets/edit_field.html b/modules/conssheets/templates/conssheets/edit_field.html index 5760c3f..6e23528 100644 --- a/modules/conssheets/templates/conssheets/edit_field.html +++ b/modules/conssheets/templates/conssheets/edit_field.html @@ -5,7 +5,7 @@ {% block content %}
@@ -71,7 +71,7 @@
- Cancel + Cancel
diff --git a/modules/conssheets/templates/conssheets/new_session.html b/modules/conssheets/templates/conssheets/new_session.html index a6968d7..fffc743 100644 --- a/modules/conssheets/templates/conssheets/new_session.html +++ b/modules/conssheets/templates/conssheets/new_session.html @@ -5,7 +5,7 @@ {% block content %}
@@ -76,7 +76,7 @@ {% endif %}
- Cancel + Cancel diff --git a/modules/conssheets/templates/conssheets/process_detail.html b/modules/conssheets/templates/conssheets/process_detail.html index 81d09c5..2d61f4b 100644 --- a/modules/conssheets/templates/conssheets/process_detail.html +++ b/modules/conssheets/templates/conssheets/process_detail.html @@ -5,7 +5,7 @@ {% block content %}
@@ -38,7 +38,7 @@
- + Configure Fields
@@ -67,7 +67,7 @@ - + Configure Template diff --git a/modules/conssheets/templates/conssheets/process_fields.html b/modules/conssheets/templates/conssheets/process_fields.html index 3b7b4cb..fd03917 100644 --- a/modules/conssheets/templates/conssheets/process_fields.html +++ b/modules/conssheets/templates/conssheets/process_fields.html @@ -5,7 +5,7 @@ {% block content %}
@@ -21,7 +21,7 @@

Header Fields

- + + Add Field
@@ -48,7 +48,7 @@ {{ 'āœ“' if field.is_required else '—' }} {{ field.excel_cell or '—' }} - Edit + Edit
@@ -157,12 +157,12 @@

Step 1: Get the correct format

- + Download Template
- +
@@ -226,7 +226,7 @@ document.getElementById('lotScanForm').addEventListener('submit', function(e) { function checkDuplicate() { const fieldValues = {}; fieldValues[dupKeyFieldName] = currentDupKeyValue; - fetch(`/cons-sheets/session/${sessionId}/scan`, { + fetch(`/conssheets/session/${sessionId}/scan`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ field_values: fieldValues, check_only: true }) @@ -300,7 +300,7 @@ function submitScan() { if (input) fieldValues[field.field_name] = input.value; } }); - fetch(`/cons-sheets/session/${sessionId}/scan`, { + fetch(`/conssheets/session/${sessionId}/scan`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ field_values: fieldValues, confirm_duplicate: isDuplicateConfirmed }) @@ -359,7 +359,7 @@ function addScanToList(detailId, fieldValues, duplicateStatus) { } function openScanDetail(detailId) { - fetch(`/cons-sheets/session/${sessionId}/detail/${detailId}`) + fetch(`/conssheets/session/${sessionId}/detail/${detailId}`) .then(r => r.json()) .then(data => { if (data.success) displayScanDetail(data.detail); @@ -413,7 +413,7 @@ function saveDetail(detailId) { if (input) fieldValues[field.field_name] = input.value; }); const comment = document.getElementById('editComment').value; - fetch(`/cons-sheets/session/${sessionId}/detail/${detailId}/update`, { + fetch(`/conssheets/session/${sessionId}/detail/${detailId}/update`, { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ field_values: fieldValues, comment: comment }) @@ -427,7 +427,7 @@ function saveDetail(detailId) { function deleteDetail(detailId) { if (!confirm('Delete this scan?')) return; - fetch(`/cons-sheets/session/${sessionId}/detail/${detailId}/delete`, { + fetch(`/conssheets/session/${sessionId}/detail/${detailId}/delete`, { method: 'POST', headers: {'Content-Type': 'application/json'} }) @@ -439,7 +439,7 @@ function deleteDetail(detailId) { } function exportToExcel() { - window.location.href = `/cons-sheets/session/${sessionId}/export?format=xlsx`; + window.location.href = `/conssheets/session/${sessionId}/export?format=xlsx`; } document.addEventListener('keydown', function(e) { diff --git a/modules/conssheets/templates/conssheets/staff_index.html b/modules/conssheets/templates/conssheets/staff_index.html index e3a241f..b608760 100644 --- a/modules/conssheets/templates/conssheets/staff_index.html +++ b/modules/conssheets/templates/conssheets/staff_index.html @@ -17,7 +17,7 @@

Start New Session

{% for p in processes %} - + + {{ p.process_name }} {% endfor %} @@ -34,7 +34,7 @@
{% for s in sessions %}
- +

{{ s.process_name }}

@@ -48,7 +48,7 @@ {% if showing_archived %}
@@ -100,7 +100,7 @@
šŸ“

No Processes Defined

Create a process type to get started (e.g., "AD WIP")

- + Create First Process
diff --git a/templates/home.html b/templates/home.html index d3499aa..92ea408 100644 --- a/templates/home.html +++ b/templates/home.html @@ -5,19 +5,12 @@ {% block content %}
- - {% if session.role in ['owner', 'admin'] %} - - {% endif %} +

Welcome, {{ session.full_name }}

-

Select a module to get started

+

Select a module to get started

{% if modules %}
diff --git a/templates/module_manager.html b/templates/module_manager.html index fc10c7d..380d8ef 100644 --- a/templates/module_manager.html +++ b/templates/module_manager.html @@ -58,14 +58,14 @@ - {% else %} - {% endif %} @@ -87,7 +87,7 @@ function installModule(moduleKey) { if (!confirm(`Install module "${moduleKey}"?\n\nThis will create database tables and activate the module.`)) { return; } - + fetch(`/admin/modules/${moduleKey}/install`, { method: 'POST', headers: { @@ -97,8 +97,14 @@ function installModule(moduleKey) { .then(response => response.json()) .then(data => { if (data.success) { - alert(`āœ… ${data.message}\n\nPlease reload the page.`); - location.reload(); + if (data.restart_required) { + // Auto-restart server + alert(`āœ… ${data.message}\n\nServer will restart automatically...`); + restartServerSilent(); // Restart without confirmation + } else { + alert(`āœ… ${data.message}`); + location.reload(); + } } else { alert(`āŒ ${data.message}`); } @@ -108,11 +114,180 @@ function installModule(moduleKey) { }); } -function uninstallModule(moduleKey) { - if (!confirm(`āš ļø UNINSTALL module "${moduleKey}"?\n\nThis will DELETE all module data and cannot be undone!`)) { +function restartServerSilent() { + // Auto-restart without confirmation (used after module install) + fetch('/admin/restart', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + // Show loading message + document.body.innerHTML = ` +
+
šŸ”„
+

Server Restarting...

+

Module installed successfully. Please wait...

+
+ `; + + // Wait 3 seconds then reload + setTimeout(() => { + location.reload(); + }, 3000); + } + }) + .catch(error => { + alert(`āŒ Restart failed: ${error}\n\nPlease restart manually.`); + }); +} + +/** + * 3-Stage Uninstall Confirmation - ALWAYS DELETES DATA + * If users want to keep data, they should use "Deactivate" instead + */ + +function uninstallModule(moduleKey, moduleName) { + // STAGE 1: Initial warning + const stage1Modal = ` +
+
+

āš ļø Uninstall Module?

+

Module: ${moduleName}

+

This will:

+
    +
  • Deactivate the module
  • +
  • Remove it from the system
  • +
  • Users will lose access
  • +
  • DELETE ALL DATA PERMANENTLY
  • +
+

+ šŸ’” Want to keep the data?
+ Use "Deactivate" instead of "Uninstall" +

+
+ + +
+
+
+ `; + + document.body.insertAdjacentHTML('beforeend', stage1Modal); +} + +function proceedToStage2(moduleKey, moduleName) { + // Remove stage 1 + document.getElementById('uninstall-modal-stage1').remove(); + + // STAGE 2: Data deletion warning + const stage2Modal = ` +
+
+

🚨 Data Will Be Deleted

+

Module: ${moduleName}

+
+

āš ļø This will permanently delete:

+
    +
  • All sessions
  • +
  • All scans and entries
  • +
  • All locations
  • +
  • All historical data
  • +
  • All database tables
  • +
+

THIS CANNOT BE UNDONE!

+
+

+ Are you absolutely sure you want to proceed? +

+
+ + +
+
+
+ `; + + document.body.insertAdjacentHTML('beforeend', stage2Modal); +} + +function proceedToStage3(moduleKey, moduleName) { + // Remove stage 2 + document.getElementById('uninstall-modal-stage2').remove(); + + // STAGE 3: Type "DELETE" confirmation + const stage3Modal = ` +
+
+

🚨 FINAL WARNING 🚨

+

Module: ${moduleName}

+
+

+ ALL DATA WILL BE
PERMANENTLY DELETED +

+
+

+ This is your LAST CHANCE to cancel. +

+

Type DELETE to confirm:

+ + +
+ + +
+
+
+ `; + + document.body.insertAdjacentHTML('beforeend', stage3Modal); + + // Focus the input + setTimeout(() => { + document.getElementById('delete-confirmation-text').focus(); + }, 100); +} + +function cancelUninstall() { + // Remove any open modals + const modal1 = document.getElementById('uninstall-modal-stage1'); + const modal2 = document.getElementById('uninstall-modal-stage2'); + const modal3 = document.getElementById('uninstall-modal-stage3'); + if (modal1) modal1.remove(); + if (modal2) modal2.remove(); + if (modal3) modal3.remove(); +} + +function finalUninstall(moduleKey, moduleName) { + const confirmText = document.getElementById('delete-confirmation-text').value; + + if (confirmText !== 'DELETE') { + document.getElementById('delete-text-error').style.display = 'block'; + document.getElementById('delete-confirmation-text').style.borderColor = '#ff0000'; + document.getElementById('delete-confirmation-text').style.boxShadow = '0 0 10px rgba(255, 0, 0, 0.5)'; + document.getElementById('delete-confirmation-text').focus(); return; } - + + // Remove modal + document.getElementById('uninstall-modal-stage3').remove(); + + // Actually uninstall - ALWAYS delete tables fetch(`/admin/modules/${moduleKey}/uninstall`, { method: 'POST', headers: { @@ -122,7 +297,7 @@ function uninstallModule(moduleKey) { .then(response => response.json()) .then(data => { if (data.success) { - alert(`āœ… ${data.message}\n\nPlease reload the page.`); + alert(`āœ… ${data.message}\n\nAll data has been permanently deleted.\n\nPlease reload the page.`); location.reload(); } else { alert(`āŒ ${data.message}`);