From fa3a7923e2bfd8ed89aaffdee658a2a9083b344c Mon Sep 17 00:00:00 2001 From: Nguyen Ngo Date: Tue, 12 Aug 2025 16:31:28 -0400 Subject: [PATCH] Add project management pages --- app.py | 788 ++++++++++++++---------------- migrate_project.py | 347 +++++++++++++ static/css/projects.css | 494 +++++++++++++++++++ templates/base_authenticated.html | 5 + templates/create_project.html | 159 ++++++ templates/create_qr_code.html | 152 +++--- templates/edit_project.html | 198 ++++++++ templates/edit_qr_code.html | 24 + templates/projects.html | 188 +++++++ 9 files changed, 1886 insertions(+), 469 deletions(-) create mode 100644 migrate_project.py create mode 100644 static/css/projects.css create mode 100644 templates/create_project.html create mode 100644 templates/edit_project.html create mode 100644 templates/projects.html diff --git a/app.py b/app.py index 6fba63d..ec7938e 100644 --- a/app.py +++ b/app.py @@ -97,13 +97,15 @@ class QRCode(db.Model): created_date = db.Column(db.DateTime, default=datetime.utcnow) active_status = db.Column(db.Boolean, default=True) qr_url = db.Column(db.String(255), unique=True, nullable=True) - - # NEW: Address Coordinates Fields + # Address Coordinates Fields address_latitude = db.Column(db.Float, nullable=True) address_longitude = db.Column(db.Float, nullable=True) coordinate_accuracy = db.Column(db.String(50), nullable=True, default='geocoded') coordinates_updated_date = db.Column(db.DateTime, nullable=True) - + project_id = db.Column(db.Integer, db.ForeignKey('projects.id'), nullable=True) + # The project_id field: + project_id = db.Column(db.Integer, db.ForeignKey('projects.id'), nullable=True) + @property def has_coordinates(self): """Check if this QR code has address coordinates""" @@ -123,6 +125,36 @@ class QRCode(db.Model): self.coordinate_accuracy = accuracy self.coordinates_updated_date = datetime.utcnow() +class Project(db.Model): + """ + Project model to organize QR codes by projects + """ + __tablename__ = 'projects' + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(100), nullable=False) + description = db.Column(db.Text, nullable=True) + created_by = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=True) + created_date = db.Column(db.DateTime, default=datetime.utcnow) + active_status = db.Column(db.Boolean, default=True) + + # Relationships + qr_codes = db.relationship('QRCode', backref='project', lazy='dynamic') + creator = db.relationship('User', backref='created_projects') + + def __repr__(self): + return f'' + + @property + def qr_count(self): + """Get count of QR codes in this project""" + return self.qr_codes.filter_by(active_status=True).count() + + @property + def total_qr_count(self): + """Get total count of QR codes (including inactive) in this project""" + return self.qr_codes.count() + # Attendance Data Model class AttendanceData(db.Model): """Enhanced attendance tracking model with location support""" @@ -1186,6 +1218,70 @@ def register(): return render_template('register.html') +@app.route('/login', methods=['GET', 'POST']) +def login(): + """Enhanced user authentication with comprehensive logging""" + if request.method == 'POST': + username = request.form.get('username', '').strip() + password = request.form.get('password', '') + + if not username or not password: + flash('Please enter both username and password.', 'error') + return render_template('login.html') + + try: + # Find user (case-insensitive username) + user = User.query.filter( + User.username.ilike(username), + User.active_status == True + ).first() + + if user and user.check_password(password): + # Successful login + session['user_id'] = user.id + session['username'] = user.username + session['role'] = user.role + session['full_name'] = user.full_name + session['login_time'] = datetime.now().isoformat() + + # Update last login date + user.last_login_date = datetime.utcnow() + db.session.commit() + + # Log successful login + logger_handler.log_user_login( + user_id=user.id, + username=user.username, + success=True + ) + + flash(f'Welcome back, {user.full_name}!', 'success') + print(f"User {user.username} logged in successfully") + + # Redirect to intended page or dashboard + next_page = request.args.get('next') + return redirect(next_page) if next_page else redirect(url_for('dashboard')) + + else: + # Invalid credentials - log failed attempt + user_id = user.id if user else None + logger_handler.log_user_login( + user_id=user_id, + username=username, + success=False, + failure_reason="Invalid credentials" + ) + + flash('Invalid username or password.', 'error') + print(f"Failed login attempt for username: {username}") + + except Exception as e: + logger_handler.log_database_error('user_login', e) + print(f"Login error: {e}") + flash('Login error. Please try again.', 'error') + + return render_template('login.html') + @app.route('/logout') def logout(): """User logout endpoint with session duration logging""" @@ -1865,272 +1961,6 @@ def permanently_delete_user(user_id): flash('Error deleting user. Please try again.', 'error') return redirect(url_for('users')) -# BULK USER OPERATIONS -@app.route('/users/bulk/deactivate', methods=['POST']) -@admin_required -def bulk_deactivate_users(): - """Bulk deactivate multiple users (Admin only)""" - try: - user_ids = request.json.get('user_ids', []) - current_user_id = session['user_id'] - current_user = User.query.get(current_user_id) - - if not user_ids: - return jsonify({'error': 'No users selected'}), 400 - - # Filter out current user and validate - valid_user_ids = [] - admin_count = User.query.filter_by(role='admin', active_status=True).count() - admins_to_deactivate = 0 - - for user_id in user_ids: - if user_id == current_user_id: - continue # Skip current user - - user = User.query.get(user_id) - if user and user.active_status: - if user.role == 'admin': - admins_to_deactivate += 1 - valid_user_ids.append(user_id) - - # Check if we're trying to deactivate all admins - if admin_count - admins_to_deactivate < 1: - return jsonify({'error': 'Cannot deactivate all admin users'}), 400 - - # Deactivate users - deactivated_count = 0 - for user_id in valid_user_ids: - user = User.query.get(user_id) - if user: - user.active_status = False - deactivated_count += 1 - - db.session.commit() - - return jsonify({ - 'success': True, - 'message': f'Successfully deactivated {deactivated_count} users', - 'deactivated_count': deactivated_count - }) - - except Exception as e: - db.session.rollback() - print(f"Error in bulk deactivate: {e}") - return jsonify({'error': 'Failed to deactivate users'}), 500 - -@app.route('/users/bulk/activate', methods=['POST']) -@admin_required -def bulk_activate_users(): - """Bulk activate multiple users (Admin only)""" - try: - user_ids = request.json.get('user_ids', []) - - if not user_ids: - return jsonify({'error': 'No users selected'}), 400 - - # Activate users - activated_count = 0 - for user_id in user_ids: - user = User.query.get(user_id) - if user and not user.active_status: - user.active_status = True - activated_count += 1 - - db.session.commit() - - return jsonify({ - 'success': True, - 'message': f'Successfully activated {activated_count} users', - 'activated_count': activated_count - }) - - except Exception as e: - db.session.rollback() - print(f"Error in bulk activate: {e}") - return jsonify({'error': 'Failed to activate users'}), 500 - -@app.route('/users/bulk-permanently-delete', methods=['POST']) -@admin_required -def bulk_permanently_delete_users(): - """Bulk permanently delete users but preserve associated QR codes (Admin only)""" - try: - current_user_id = session['user_id'] - current_user = User.query.get(current_user_id) - - # Get user IDs from request - user_ids = request.json.get('user_ids', []) - - if not user_ids: - return jsonify({'error': 'No user IDs provided'}), 400 - - # Convert string IDs to integers for safety - try: - user_ids = [int(uid) for uid in user_ids] - except (ValueError, TypeError): - return jsonify({'error': 'Invalid user IDs provided'}), 400 - - # Security validations - deleted_users = [] - preserved_qr_count = 0 # Changed from deleted_qr_count to preserved_qr_count - errors = [] - - for user_id in user_ids: - try: - # Skip current user - if user_id == current_user_id: - errors.append(f"Cannot delete your own account") - continue - - user_to_delete = User.query.get(user_id) - if not user_to_delete: - errors.append(f"User with ID {user_id} not found") - continue - - # Only allow deletion of inactive users for safety - if user_to_delete.active_status: - errors.append(f"User '{user_to_delete.full_name}' must be deactivated before permanent deletion") - continue - - # If deleting an admin, ensure at least one admin remains - if user_to_delete.role == 'admin': - active_admin_count = User.query.filter_by(role='admin', active_status=True).count() - if active_admin_count <= 1: - errors.append(f"Cannot delete the last admin user '{user_to_delete.full_name}'") - continue - - # Count QR codes before deletion for reporting - user_qr_count = user_to_delete.created_qr_codes.count() - preserved_qr_count += user_qr_count - - # MODIFIED: Preserve QR codes by setting created_by to NULL instead of deleting them - orphaned_qr_codes = QRCode.query.filter_by(created_by=user_id).all() - for qr_code in orphaned_qr_codes: - qr_code.created_by = None - - # Update any users that were created by this user (set created_by to None) - created_users = User.query.filter_by(created_by=user_id).all() - for created_user in created_users: - created_user.created_by = None - - # Delete the user - deleted_users.append({ - 'name': user_to_delete.full_name, - 'username': user_to_delete.username, - 'qr_count': user_qr_count - }) - - db.session.delete(user_to_delete) - - except Exception as e: - print(f"Error processing user {user_id}: {e}") - errors.append(f"Error processing user ID {user_id}") - continue - - # Commit all changes if we have deletions - if deleted_users: - db.session.commit() - - # Log the bulk deletion - deleted_names = [user['name'] for user in deleted_users] - print(f"Admin {current_user.username} permanently deleted {len(deleted_users)} users: {', '.join(deleted_names)}, preserved {preserved_qr_count} QR codes") - - # Prepare response message - if deleted_users and not errors: - message = f'Successfully deleted {len(deleted_users)} users and preserved {preserved_qr_count} associated QR codes' - elif deleted_users and errors: - message = f'Deleted {len(deleted_users)} users and preserved {preserved_qr_count} QR codes. {len(errors)} operations failed' - elif not deleted_users and errors: - return jsonify({ - 'success': False, - 'error': 'No users could be deleted', - 'details': errors - }), 400 - else: - return jsonify({ - 'success': False, - 'error': 'No valid users to delete' - }), 400 - - return jsonify({ - 'success': True, - 'message': message, - 'deleted_count': len(deleted_users), - 'preserved_qr_count': preserved_qr_count, # Changed from deleted_qr_count - 'errors': errors if errors else None - }) - - except Exception as e: - db.session.rollback() - print(f"Error in bulk permanently delete users: {e}") - return jsonify({ - 'success': False, - 'error': 'Failed to delete users. Please try again.' - }), 500 - -@app.route('/login', methods=['GET', 'POST']) -def login(): - """Enhanced user authentication with comprehensive logging""" - if request.method == 'POST': - username = request.form.get('username', '').strip() - password = request.form.get('password', '') - - if not username or not password: - flash('Please enter both username and password.', 'error') - return render_template('login.html') - - try: - # Find user (case-insensitive username) - user = User.query.filter( - User.username.ilike(username), - User.active_status == True - ).first() - - if user and user.check_password(password): - # Successful login - session['user_id'] = user.id - session['username'] = user.username - session['role'] = user.role - session['full_name'] = user.full_name - session['login_time'] = datetime.now().isoformat() - - # Update last login date - user.last_login_date = datetime.utcnow() - db.session.commit() - - # Log successful login - logger_handler.log_user_login( - user_id=user.id, - username=user.username, - success=True - ) - - flash(f'Welcome back, {user.full_name}!', 'success') - print(f"User {user.username} logged in successfully") - - # Redirect to intended page or dashboard - next_page = request.args.get('next') - return redirect(next_page) if next_page else redirect(url_for('dashboard')) - - else: - # Invalid credentials - log failed attempt - user_id = user.id if user else None - logger_handler.log_user_login( - user_id=user_id, - username=username, - success=False, - failure_reason="Invalid credentials" - ) - - flash('Invalid username or password.', 'error') - print(f"Failed login attempt for username: {username}") - - except Exception as e: - logger_handler.log_database_error('user_login', e) - print(f"Login error: {e}") - flash('Login error. Please try again.', 'error') - - return render_template('login.html') - # Admin logging routes @app.route('/admin/logs') @admin_required @@ -2239,63 +2069,220 @@ def api_cleanup_logs(): 'success': False, 'error': 'Failed to cleanup old logs' }), 500 + +# PROJECT MANAGEMENT ROUTES +@app.route('/projects') +@admin_required +def projects(): + """Display all projects""" + try: + projects = Project.query.order_by(Project.created_date.desc()).all() + return render_template('projects.html', projects=projects) + except Exception as e: + logger_handler.log_database_error('projects_list', e) + flash('Error loading projects list.', 'error') + return redirect(url_for('dashboard')) + +@app.route('/projects/create', methods=['GET', 'POST']) +@admin_required +@log_database_operations('project_creation') +def create_project(): + """Create new project""" + if request.method == 'POST': + try: + name = request.form['name'] + description = request.form.get('description', '') + + # Check if project name already exists + if Project.query.filter_by(name=name).first(): + flash('Project name already exists.', 'error') + return render_template('create_project.html') + + # Create new project + new_project = Project( + name=name, + description=description, + created_by=session['user_id'] + ) + + db.session.add(new_project) + db.session.commit() + + # Log project creation + logger_handler.logger.info(f"User {session['username']} created new project: {name}") + + flash(f'Project "{name}" created successfully.', 'success') + return redirect(url_for('projects')) + + except Exception as e: + db.session.rollback() + logger_handler.log_database_error('project_creation', e) + flash('Project creation failed. Please try again.', 'error') + return render_template('create_project.html') + +@app.route('/projects//edit', methods=['GET', 'POST']) +@admin_required +@log_database_operations('project_edit') +def edit_project(project_id): + """Edit existing project""" + try: + project = Project.query.get_or_404(project_id) + + if request.method == 'POST': + old_name = project.name + old_description = project.description + + project.name = request.form['name'] + project.description = request.form.get('description', '') + + db.session.commit() + + # Log project update + changes = {} + if old_name != project.name: + changes['name'] = {'old': old_name, 'new': project.name} + if old_description != project.description: + changes['description'] = {'old': old_description, 'new': project.description} + + if changes: + logger_handler.logger.info(f"User {session['username']} updated project {project_id}: {json.dumps(changes)}") + + flash(f'Project "{project.name}" updated successfully.', 'success') + return redirect(url_for('projects')) + + return render_template('edit_project.html', project=project) + + except Exception as e: + db.session.rollback() + logger_handler.log_database_error('project_edit', e) + flash('Project update failed. Please try again.', 'error') + return redirect(url_for('projects')) + +@app.route('/projects//toggle', methods=['POST']) +@admin_required +@log_database_operations('project_toggle') +def toggle_project(project_id): + """Toggle project active status""" + try: + project = Project.query.get_or_404(project_id) + old_status = project.active_status + project.active_status = not project.active_status + + db.session.commit() + + # Log status change + status = "activated" if project.active_status else "deactivated" + logger_handler.logger.info(f"User {session['username']} {status} project: {project.name}") + + flash(f'Project "{project.name}" {status} successfully.', 'success') + + except Exception as e: + db.session.rollback() + logger_handler.log_database_error('project_toggle', e) + flash('Failed to update project status.', 'error') + + return redirect(url_for('projects')) + +# API ENDPOINTS FOR DROPDOWN FUNCTIONALITY +@app.route('/api/projects/active') +@login_required +def api_active_projects(): + """Get active projects for dropdown""" + try: + projects = Project.query.filter_by(active_status=True).order_by(Project.name.asc()).all() + + projects_data = [ + { + 'id': project.id, + 'name': project.name, + 'description': project.description, + 'qr_count': project.qr_count + } + for project in projects + ] + + return jsonify({ + 'success': True, + 'projects': projects_data + }) + + except Exception as e: + logger_handler.log_database_error('api_active_projects', e) + return jsonify({ + 'success': False, + 'error': 'Failed to fetch projects' + }), 500 + # QR code management routes @app.route('/qr-codes/create', methods=['GET', 'POST']) @login_required @log_database_operations('qr_code_creation') def create_qr_code(): - """Enhanced create QR code with comprehensive logging""" + """Enhanced create QR code with readable URL format and coordinates saving""" if request.method == 'POST': try: name = request.form['name'] location = request.form['location'] location_address = request.form['location_address'] - location_event = request.form['location_event'] + location_event = request.form.get('location_event', '') + project_id = request.form.get('project_id') - # Get coordinates from hidden form fields (set by JavaScript) - address_latitude = request.form.get('address_latitude') - address_longitude = request.form.get('address_longitude') + # Extract coordinates data from form + latitude = request.form.get('latitude') + longitude = request.form.get('longitude') coordinate_accuracy = request.form.get('coordinate_accuracy', 'geocoded') - # Create QR code record first (without QR image and URL) + # Convert coordinates to float if they exist + address_latitude = None + address_longitude = None + has_coordinates = False + + if latitude and longitude: + try: + address_latitude = float(latitude) + address_longitude = float(longitude) + has_coordinates = True + print(f"✓ Coordinates received: {address_latitude}, {address_longitude}") + except (ValueError, TypeError) as e: + print(f"⚠️ Invalid coordinates format: {e}") + address_latitude = None + address_longitude = None + has_coordinates = False + + # Validate project_id if provided + project = None + if project_id: + project_id = int(project_id) + project = Project.query.get(project_id) + if not project or not project.active_status: + flash('Selected project is not valid or inactive.', 'error') + return render_template('create_qr_code.html') + + # Create new QR code record first (without URL and image) new_qr_code = QRCode( name=name, location=location, location_address=location_address, location_event=location_event, - qr_code_image='', # Temporary empty value - qr_url='', # Temporary empty value - created_by=session['user_id'] + qr_code_image="", # Will be updated after URL generation + qr_url="", # Will be updated after ID is assigned + created_by=session['user_id'], + project_id=project_id, + address_latitude=address_latitude, + address_longitude=address_longitude, + coordinate_accuracy=coordinate_accuracy if has_coordinates else None, + coordinates_updated_date=datetime.utcnow() if has_coordinates else None ) - # Add coordinates if available - has_coordinates = False - if address_latitude and address_longitude: - try: - lat = float(address_latitude) - lng = float(address_longitude) - new_qr_code.address_latitude = lat - new_qr_code.address_longitude = lng - new_qr_code.coordinate_accuracy = coordinate_accuracy - new_qr_code.coordinates_updated_date = datetime.utcnow() - has_coordinates = True - print(f"✅ Added coordinates to QR code: {lat:.10f}, {lng:.10f}") - except (ValueError, TypeError) as e: - logger_handler.log_flask_error( - error_type="invalid_coordinates", - error_message=f"Invalid coordinates provided: {e}" - ) - print(f"⚠️ Invalid coordinates provided: {e}") - # Add to session and flush to get the ID db.session.add(new_qr_code) db.session.flush() # This assigns the ID without committing - # Now we can use the ID to generate the URL + # Now generate the readable URL using the ID qr_url = generate_qr_url(name, new_qr_code.id) - # Generate QR code data with the destination URL + # Generate QR code data with the destination URL qr_data = f"{request.url_root}qr/{qr_url}" qr_image = generate_qr_code(qr_data) @@ -2306,160 +2293,135 @@ def create_qr_code(): # Now commit all changes db.session.commit() - # Log QR code creation - qr_data_for_log = { + # Enhanced logging with project and coordinates information + log_data = { + 'qr_code_id': new_qr_code.id, + 'name': name, 'location': location, - 'location_address': location_address, - 'location_event': location_event, - 'has_coordinates': has_coordinates + 'qr_url': qr_url, # Log the readable URL + 'project_id': project_id, + 'project_name': project.name if project else None, + 'has_coordinates': has_coordinates, + 'latitude': address_latitude, + 'longitude': address_longitude, + 'coordinate_accuracy': coordinate_accuracy } + logger_handler.logger.info(f"User {session['username']} created QR code: {json.dumps(log_data)}") - if has_coordinates: - qr_data_for_log.update({ - 'latitude': new_qr_code.address_latitude, - 'longitude': new_qr_code.address_longitude, - 'coordinate_accuracy': coordinate_accuracy - }) + # Success message with coordinates info + project_info = f" in project '{project.name}'" if project else "" + coord_info = f" with coordinates ({new_qr_code.coordinates_display})" if has_coordinates else "" - logger_handler.log_qr_code_created( - qr_code_id=new_qr_code.id, - qr_code_name=name, - created_by_user_id=session['user_id'], - qr_data=qr_data_for_log - ) - - coord_msg = "" - if new_qr_code.has_coordinates: - coord_msg = f" with coordinates ({new_qr_code.coordinates_display})" - - flash(f'QR code created successfully{coord_msg}!', 'success') + flash(f'QR Code "{name}" created successfully{project_info}{coord_info}! URL: {qr_url}', 'success') return redirect(url_for('dashboard')) except Exception as e: db.session.rollback() logger_handler.log_database_error('qr_code_creation', e) - flash('Failed to create QR code. Please try again.', 'error') + flash('QR Code creation failed. Please try again.', 'error') + print(f"❌ QR Code creation error: {e}") - return render_template('create_qr_code.html') + # Get active projects for dropdown + projects = Project.query.filter_by(active_status=True).order_by(Project.name.asc()).all() + return render_template('create_qr_code.html', projects=projects) @app.route('/qr-codes//edit', methods=['GET', 'POST']) @login_required -@log_database_operations('qr_code_update') +@log_database_operations('qr_code_edit') def edit_qr_code(qr_id): - """Enhanced edit QR code with change tracking and logging - PRESERVING EXACT ROUTE""" + """Enhanced edit QR code with project association and URL regeneration""" try: qr_code = QRCode.query.get_or_404(qr_id) - # Check permissions (admin can edit any, users can edit their own) - if not has_admin_privileges(session.get('role')) and qr_code.created_by != session['user_id']: - flash('You can only edit QR codes you created.', 'error') - return redirect(url_for('dashboard')) - if request.method == 'POST': # Track changes for logging - changes = {} - - # Store original values for comparison - original_values = { + old_data = { 'name': qr_code.name, 'location': qr_code.location, 'location_address': qr_code.location_address, - 'location_event': qr_code.location_event + 'location_event': qr_code.location_event, + 'project_id': qr_code.project_id, + 'qr_url': qr_code.qr_url } # Update QR code fields new_name = request.form['name'] - new_address = request.form['location_address'] - qr_code.name = new_name qr_code.location = request.form['location'] - qr_code.location_address = new_address - qr_code.location_event = request.form['location_event'] + qr_code.location_address = request.form['location_address'] + qr_code.location_event = request.form.get('location_event', '') - # Track field changes - for field, old_value in original_values.items(): - new_value = getattr(qr_code, field) - if old_value != new_value: - changes[field] = {'old': old_value, 'new': new_value} - - # Handle address coordinates - address_latitude = request.form.get('address_latitude') - address_longitude = request.form.get('address_longitude') + # Handle coordinates update + latitude = request.form.get('latitude') + longitude = request.form.get('longitude') coordinate_accuracy = request.form.get('coordinate_accuracy', 'geocoded') - # Update coordinates if provided - if address_latitude and address_longitude: + if latitude and longitude: try: - lat = float(address_latitude) - lng = float(address_longitude) - - # Check if coordinates changed - if (qr_code.address_latitude != lat or - qr_code.address_longitude != lng or - qr_code.coordinate_accuracy != coordinate_accuracy): - - old_coords = qr_code.coordinates_display - qr_code.address_latitude = lat - qr_code.address_longitude = lng - qr_code.coordinate_accuracy = coordinate_accuracy - qr_code.coordinates_updated_date = datetime.utcnow() - changes['coordinates'] = { - 'old': old_coords, - 'new': qr_code.coordinates_display - } - print(f"✅ Updated coordinates for QR code: {lat:.10f}, {lng:.10f}") - except (ValueError, TypeError) as e: - logger_handler.log_flask_error( - error_type="invalid_coordinates_update", - error_message=f"Invalid coordinates during update: {e}" - ) - print(f"⚠️ Invalid coordinates provided during edit: {e}") - - # Check if name changed and handle URL regeneration - original_name = original_values['name'] - if original_name != new_name: - # Name changed, regenerate URL - new_qr_url = generate_qr_url(new_name, qr_code.id) - qr_code.qr_url = new_qr_url - - # Update QR code data with new URL - qr_data = f"{request.url_root}qr/{new_qr_url}" - else: - # Name didn't change, use existing URL (if it exists) - if qr_code.qr_url: - qr_data = f"{request.url_root}qr/{qr_code.qr_url}" + qr_code.address_latitude = float(latitude) + qr_code.address_longitude = float(longitude) + qr_code.coordinate_accuracy = coordinate_accuracy + qr_code.coordinates_updated_date = datetime.utcnow() + except (ValueError, TypeError): + pass # Keep existing coordinates if invalid + + # Handle project association + new_project_id = request.form.get('project_id') + if new_project_id: + new_project_id = int(new_project_id) + project = Project.query.get(new_project_id) + if project and project.active_status: + qr_code.project_id = new_project_id else: - # Fallback: generate URL if it doesn't exist (for legacy QR codes) - new_qr_url = generate_qr_url(new_name, qr_code.id) - qr_code.qr_url = new_qr_url - qr_data = f"{request.url_root}qr/{new_qr_url}" - - # Regenerate QR code with updated data (destination URL) - qr_code.qr_code_image = generate_qr_code(qr_data) - + flash('Selected project is not valid or inactive.', 'error') + return render_template('edit_qr_code.html', qr_code=qr_code, projects=Project.query.filter_by(active_status=True).all()) + else: + qr_code.project_id = None + + # Regenerate URL if name changed + name_changed = old_data['name'] != new_name + if name_changed: + new_qr_url = generate_qr_url(new_name, qr_code.id) + + # Regenerate QR code image with new URL + qr_data = f"{request.url_root}qr/{new_qr_url}" + new_qr_image = generate_qr_code(qr_data) + + qr_code.qr_url = new_qr_url + qr_code.qr_code_image = new_qr_image + db.session.commit() - # Log QR code update if there were changes - if changes: - logger_handler.log_qr_code_updated( - qr_code_id=qr_code.id, - qr_code_name=qr_code.name, - updated_by_user_id=session['user_id'], - changes=changes - ) + # Log changes + new_data = { + 'name': qr_code.name, + 'location': qr_code.location, + 'location_address': qr_code.location_address, + 'location_event': qr_code.location_event, + 'project_id': qr_code.project_id, + 'qr_url': qr_code.qr_url + } - coord_msg = "" - if qr_code.has_coordinates: - coord_msg = f" Coordinates: ({qr_code.coordinates_display})" - - flash(f'QR code updated successfully!{coord_msg}', 'success') + changes = {} + for key in old_data: + if old_data[key] != new_data[key]: + changes[key] = {'old': old_data[key], 'new': new_data[key]} + + if changes: + logger_handler.logger.info(f"User {session['username']} updated QR code {qr_id}: {json.dumps(changes)}") + + url_message = f" URL updated to: {qr_code.qr_url}" if name_changed else "" + flash(f'QR Code "{qr_code.name}" updated successfully!{url_message}', 'success') return redirect(url_for('dashboard')) - return render_template('edit_qr_code.html', qr_code=qr_code) + # Get active projects for dropdown + projects = Project.query.filter_by(active_status=True).order_by(Project.name.asc()).all() + return render_template('edit_qr_code.html', qr_code=qr_code, projects=projects) except Exception as e: - logger_handler.log_database_error('qr_code_update', e) - flash('Error updating QR code. Please try again.', 'error') + db.session.rollback() + logger_handler.log_database_error('qr_code_edit', e) + flash('QR Code update failed. Please try again.', 'error') return redirect(url_for('dashboard')) @app.route('/qr-codes//delete', methods=['GET', 'POST']) diff --git a/migrate_project.py b/migrate_project.py new file mode 100644 index 0000000..2494c0d --- /dev/null +++ b/migrate_project.py @@ -0,0 +1,347 @@ +#!/usr/bin/env python3 +""" +Database Migration Script for Project Model +========================================== + +This script safely migrates your existing database to add the Project model +and associate QR codes with projects. + +The script will: +1. Backup your current database +2. Create the new projects table +3. Add project_id column to qr_codes table +4. Create some sample projects (optional) +5. Provide rollback instructions + +Usage: + python migrate_projects.py + +Requirements: + - Your existing Flask app with database models + - Database write permissions +""" + +import os +import sys +import shutil +from datetime import datetime +from sqlalchemy import create_engine, text, inspect +from sqlalchemy.orm import sessionmaker + +# Add your app to the Python path +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +try: + from app import app, db, User, QRCode, Project + from dotenv import load_dotenv +except ImportError as e: + print(f"Error importing app modules: {e}") + print("Make sure this script is in the same directory as your app.py file") + sys.exit(1) + +# Load environment variables +load_dotenv() + +# Configuration +BACKUP_DIR = "database_backups" +MIGRATION_VERSION = "v1.1_add_project_model" + +def create_backup_directory(): + """Create backup directory if it doesn't exist""" + if not os.path.exists(BACKUP_DIR): + os.makedirs(BACKUP_DIR) + print(f"✓ Created backup directory: {BACKUP_DIR}") + +def backup_database(): + """Create a backup of the current database""" + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + backup_filename = f"backup_{MIGRATION_VERSION}_{timestamp}.db" + backup_path = os.path.join(BACKUP_DIR, backup_filename) + + # Get database path from config + db_url = app.config['SQLALCHEMY_DATABASE_URI'] + + if db_url.startswith('sqlite:///'): + # SQLite database + db_path = db_url.replace('sqlite:///', '') + + if os.path.exists(db_path): + shutil.copy2(db_path, backup_path) + print(f"✓ Database backed up to: {backup_path}") + return backup_path + else: + print(f"⚠ Database file not found: {db_path}") + return None + else: + print("⚠ Non-SQLite databases require manual backup") + print("Please ensure you have a recent backup before proceeding") + return None + +def validate_current_database(): + """Validate the current database structure and data""" + print("\n🔍 Validating current database...") + + try: + with app.app_context(): + # Check if required tables exist + inspector = inspect(db.engine) + tables = inspector.get_table_names() + + required_tables = ['users', 'qr_codes'] + for table in required_tables: + if table not in tables: + print(f"❌ Required table '{table}' not found") + return False + print(f"✓ Table '{table}' exists") + + # Check current data + users = User.query.all() + qr_codes = QRCode.query.all() + print(f"✓ Found {len(users)} users in database") + print(f"✓ Found {len(qr_codes)} QR codes in database") + + # Check if projects table already exists + if 'projects' in tables: + print("⚠ Projects table already exists - migration may have been run before") + projects = Project.query.all() + print(f"✓ Found {len(projects)} existing projects") + + print("✓ Database validation passed") + return True + + except Exception as e: + print(f"❌ Database validation failed: {e}") + return False + +def perform_migration(): + """Perform the actual migration""" + print("\n🚀 Starting migration...") + + try: + with app.app_context(): + # Create all tables (this will create the projects table if it doesn't exist) + db.create_all() + print("✓ Database tables created/updated") + + # Check if project_id column exists in qr_codes table + inspector = inspect(db.engine) + qr_columns = inspector.get_columns('qr_codes') + qr_column_names = [col['name'] for col in qr_columns] + + if 'project_id' not in qr_column_names: + # Add project_id column to qr_codes table + print("➕ Adding project_id column to qr_codes table...") + db.session.execute(text('ALTER TABLE qr_codes ADD COLUMN project_id INTEGER')) + db.session.commit() + print("✓ project_id column added to qr_codes table") + else: + print("✓ project_id column already exists in qr_codes table") + + print("✓ Migration completed successfully!") + return True + + except Exception as e: + print(f"❌ Migration failed: {e}") + import traceback + traceback.print_exc() + return False + +def create_sample_projects(): + """Create some sample projects (optional)""" + print("\n📁 Creating sample projects...") + + try: + with app.app_context(): + # Get the first admin user to assign as creator + admin_user = User.query.filter_by(role='admin').first() + creator_id = admin_user.id if admin_user else None + + # Check if any projects exist + existing_projects = Project.query.count() + if existing_projects > 0: + print(f"✓ Found {existing_projects} existing projects - skipping sample creation") + return True + + # Sample projects + sample_projects = [ + { + 'name': 'Office Locations', + 'description': 'QR codes for various office locations and facilities' + }, + { + 'name': 'Events', + 'description': 'QR codes for company events and meetings' + }, + { + 'name': 'Training Materials', + 'description': 'QR codes for training sessions and educational content' + } + ] + + created_count = 0 + for project_data in sample_projects: + # Check if project with this name already exists + existing = Project.query.filter_by(name=project_data['name']).first() + if not existing: + project = Project( + name=project_data['name'], + description=project_data['description'], + created_by=creator_id + ) + db.session.add(project) + created_count += 1 + + if created_count > 0: + db.session.commit() + print(f"✓ Created {created_count} sample projects") + else: + print("✓ Sample projects already exist") + + return True + + except Exception as e: + print(f"❌ Failed to create sample projects: {e}") + return False + +def test_new_functionality(): + """Test the new project functionality""" + print("\n🧪 Testing new project functionality...") + + try: + with app.app_context(): + # Test Project model + projects = Project.query.all() + print(f"✓ Can query projects: {len(projects)} found") + + # Test QRCode project relationship + qr_codes = QRCode.query.all() + for qr in qr_codes[:3]: # Test first 3 QR codes + project = qr.project # This should not raise an error + print(f"✓ QR code '{qr.name}' project: {project.name if project else 'None'}") + + # Test Project.qr_codes relationship + if projects: + first_project = projects[0] + qr_count = first_project.qr_count + print(f"✓ Project '{first_project.name}' has {qr_count} QR codes") + + print("✓ New functionality tests passed") + return True + + except Exception as e: + print(f"❌ Functionality tests failed: {e}") + import traceback + traceback.print_exc() + return False + +def display_summary(): + """Display migration summary""" + print("\n📊 MIGRATION SUMMARY") + print("=" * 50) + + try: + with app.app_context(): + users = User.query.count() + projects = Project.query.count() + qr_codes = QRCode.query.count() + + print(f"👥 Users: {users}") + print(f"📁 Projects: {projects}") + print(f"🔗 QR Codes: {qr_codes}") + + # Show project distribution + if projects > 0: + print(f"\n📁 Project Details:") + for project in Project.query.all(): + print(f" • {project.name}: {project.qr_count} QR codes") + + # Show unassigned QR codes + unassigned = QRCode.query.filter_by(project_id=None).count() + if unassigned > 0: + print(f"\n⚠️ {unassigned} QR codes are not assigned to any project") + + except Exception as e: + print(f"Error generating summary: {e}") + +def rollback_instructions(): + """Show rollback instructions""" + print("\n🔄 ROLLBACK INSTRUCTIONS") + print("=" * 50) + print("If you need to rollback this migration:") + print("1. Stop your application") + print("2. Restore the database backup:") + print(" - For SQLite: Replace your database file with the backup") + print(" - For other databases: Restore from your backup") + print("3. Remove the project_id column from qr_codes table:") + print(" ALTER TABLE qr_codes DROP COLUMN project_id;") + print("4. Drop the projects table:") + print(" DROP TABLE projects;") + print("5. Update your app.py to remove Project model and related code") + print("6. Restart your application") + +def main(): + """Main migration process""" + print("🗃️ PROJECT MODEL MIGRATION") + print("=" * 50) + print("This will add project functionality to your QR code system.") + print("Projects allow you to organize QR codes into logical groups.") + print("\nWhat this migration does:") + print("• Creates a new 'projects' table") + print("• Adds 'project_id' column to 'qr_codes' table") + print("• Creates sample projects (optional)") + print("• Updates relationships between models") + + # Confirm migration + response = input("\nProceed with migration? (y/N): ").strip().lower() + if response not in ['y', 'yes']: + print("Migration cancelled.") + sys.exit(0) + + # Step 1: Create backup directory + create_backup_directory() + + # Step 2: Backup database + backup_path = backup_database() + if not backup_path: + response = input("No backup created. Continue anyway? (y/N): ").strip().lower() + if response not in ['y', 'yes']: + print("Migration cancelled for safety.") + sys.exit(0) + + # Step 3: Validate current database + if not validate_current_database(): + print("❌ Database validation failed. Migration cancelled.") + sys.exit(1) + + # Step 4: Perform migration + if not perform_migration(): + print("❌ Migration failed. Please check the errors above.") + sys.exit(1) + + # Step 5: Create sample projects + create_sample = input("\nCreate sample projects? (Y/n): ").strip().lower() + if create_sample not in ['n', 'no']: + create_sample_projects() + + # Step 6: Test new functionality + if not test_new_functionality(): + print("❌ Functionality testing failed. Migration may be incomplete.") + sys.exit(1) + + # Step 7: Display summary + display_summary() + + # Step 8: Show rollback instructions + show_rollback = input("\nWould you like to see rollback instructions? (y/N): ").strip().lower() + if show_rollback in ['y', 'yes']: + rollback_instructions() + + print("\n🚀 Migration completed successfully!") + print("You can now:") + print("• Create and manage projects") + print("• Associate QR codes with projects") + print("• Use the project dropdown in QR code forms") + print("• View project statistics and organization") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/static/css/projects.css b/static/css/projects.css new file mode 100644 index 0000000..c504193 --- /dev/null +++ b/static/css/projects.css @@ -0,0 +1,494 @@ +/** + * Projects Page Dedicated CSS + * static/css/projects.css + * + * This file contains all necessary styles for the projects management page + * ensuring it works independently with proper layout and responsive design. + */ + +/* Projects Page Container */ +.projects-page { + max-width: 1600px; + margin: 0 auto; + padding: 2rem; + min-height: 100vh; + background: #f8fafc; +} + +/* Page Header */ +.projects-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 2rem; + padding: 1.5rem; + background: #ffffff; + border-radius: 0.75rem; + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); + position: relative; + overflow: hidden; +} + +.projects-header::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, #2563eb, #1d4ed8); +} + +.header-content h1 { + font-size: 1.875rem; + font-weight: 700; + color: #0f172a; + margin-bottom: 0.5rem; + display: flex; + align-items: center; + gap: 0.75rem; +} + +.header-content h1 i { + color: #2563eb; +} + +.header-content p { + color: #64748b; + font-size: 1.125rem; + margin: 0; +} + +.header-actions { + display: flex; + gap: 0.75rem; +} + +/* Project Statistics Grid */ +.project-stats { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 1.5rem; + margin-bottom: 2rem; +} + +.stat-card { + background: #ffffff; + padding: 1.5rem; + border-radius: 0.75rem; + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); + display: flex; + align-items: center; + gap: 1rem; + transition: all 0.2s ease-in-out; + border: 1px solid #e2e8f0; + position: relative; + overflow: hidden; +} + +.stat-card::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: linear-gradient(90deg, #2563eb, #1d4ed8); +} + +.stat-card:hover { + transform: translateY(-2px); + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); +} + +.stat-card.active::before { + background: linear-gradient(90deg, #10b981, #047857); +} + +.stat-icon { + width: 50px; + height: 50px; + border-radius: 0.5rem; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.25rem; + color: #ffffff; + background: linear-gradient(135deg, #2563eb, #1d4ed8); +} + +.stat-icon.active { + background: linear-gradient(135deg, #10b981, #047857); +} + +.stat-info h3 { + font-size: 1.75rem; + font-weight: 700; + color: #0f172a; + margin-bottom: 0.25rem; +} + +.stat-info p { + color: #64748b; + font-size: 0.875rem; + margin: 0; +} + +/* Content Section */ +.content-section { + background: #ffffff; + border-radius: 0.75rem; + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); + overflow: hidden; +} + +.section-header { + padding: 1.5rem; + border-bottom: 1px solid #e2e8f0; + background: #f8fafc; +} + +.section-header h2 { + font-size: 1.5rem; + font-weight: 600; + color: #0f172a; + margin: 0; + display: flex; + align-items: center; + gap: 0.5rem; +} + +/* Projects Grid */ +.projects-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); + gap: 1.5rem; + padding: 1.5rem; +} + +.project-card { + background: #ffffff; + border: 1px solid #e2e8f0; + border-radius: 0.75rem; + padding: 1.5rem; + transition: all 0.2s ease-in-out; + position: relative; + overflow: hidden; +} + +.project-card::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: linear-gradient(90deg, #2563eb, #1d4ed8); +} + +.project-card:hover { + transform: translateY(-2px); + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); + border-color: #2563eb; +} + +.project-card.inactive { + opacity: 0.7; + background: #f8fafc; +} + +.project-card.inactive::before { + background: linear-gradient(90deg, #6b7280, #4b5563); +} + +.project-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 1rem; +} + +.project-info h3 { + font-size: 1.25rem; + font-weight: 600; + color: #0f172a; + margin-bottom: 0.5rem; + line-height: 1.4; +} + +.project-description { + color: #64748b; + font-size: 0.875rem; + line-height: 1.5; + margin: 0; +} + +.project-status { + flex-shrink: 0; +} + +/* Status Badges */ +.status-badge { + padding: 0.25rem 0.75rem; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +.status-badge.active { + background: rgba(16, 185, 129, 0.1); + color: #047857; + border: 1px solid rgba(16, 185, 129, 0.2); +} + +.status-badge.inactive { + background: rgba(107, 114, 128, 0.1); + color: #4b5563; + border: 1px solid rgba(107, 114, 128, 0.2); +} + +/* Project Stats */ +.project-stats-section { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin-bottom: 1.5rem; + padding: 1rem; + background: #f8fafc; + border-radius: 0.5rem; + border: 1px solid #e2e8f0; +} + +.stat-item { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 0.875rem; + color: #64748b; +} + +.stat-item i { + width: 16px; + color: #2563eb; + text-align: center; +} + +.stat-item span { + font-weight: 500; + color: #374151; +} + +/* Project Actions */ +.project-actions { + display: flex; + gap: 0.5rem; + justify-content: flex-end; + flex-wrap: wrap; +} + +.project-actions .btn { + font-size: 0.875rem; + padding: 0.5rem 1rem; + border-radius: 0.375rem; + text-decoration: none; + border: none; + cursor: pointer; + transition: all 0.2s ease-in-out; + display: inline-flex; + align-items: center; + gap: 0.375rem; +} + +.project-actions .btn-secondary { + background: #f1f5f9; + color: #475569; + border: 1px solid #cbd5e1; +} + +.project-actions .btn-secondary:hover { + background: #e2e8f0; + color: #334155; +} + +.project-actions .btn-warning { + background: #fbbf24; + color: #92400e; + border: 1px solid #f59e0b; +} + +.project-actions .btn-warning:hover { + background: #f59e0b; + color: #78350f; +} + +.project-actions .btn-success { + background: #10b981; + color: #ffffff; + border: 1px solid #059669; +} + +.project-actions .btn-success:hover { + background: #059669; +} + +/* Empty State */ +.empty-state { + text-align: center; + padding: 3rem 1.5rem; + color: #64748b; +} + +.empty-icon { + width: 80px; + height: 80px; + margin: 0 auto 1.5rem; + background: #f1f5f9; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 2rem; + color: #94a3b8; +} + +.empty-state h3 { + font-size: 1.5rem; + font-weight: 600; + color: #374151; + margin-bottom: 0.75rem; +} + +.empty-state p { + font-size: 1rem; + margin-bottom: 1.5rem; + max-width: 400px; + margin-left: auto; + margin-right: auto; +} + +.empty-state .btn { + background: #2563eb; + color: #ffffff; + padding: 0.75rem 1.5rem; + border-radius: 0.5rem; + text-decoration: none; + font-weight: 500; + display: inline-flex; + align-items: center; + gap: 0.5rem; + transition: all 0.2s ease-in-out; + border: none; +} + +.empty-state .btn:hover { + background: #1d4ed8; + transform: translateY(-1px); +} + +/* Responsive Design */ +@media (max-width: 1024px) { + .projects-grid { + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1rem; + } + + .project-stats { + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + } +} + +@media (max-width: 768px) { + .projects-page { + padding: 1rem; + } + + .projects-header { + flex-direction: column; + align-items: stretch; + gap: 1rem; + text-align: center; + } + + .header-actions { + justify-content: center; + } + + .projects-grid { + grid-template-columns: 1fr; + padding: 1rem; + } + + .project-stats { + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 1rem; + } + + .project-header { + flex-direction: column; + align-items: stretch; + gap: 0.75rem; + } + + .project-status { + align-self: flex-start; + } + + .project-actions { + justify-content: stretch; + } + + .project-actions .btn { + flex: 1; + justify-content: center; + } + + .stat-card { + padding: 1rem; + } + + .stat-icon { + width: 40px; + height: 40px; + font-size: 1rem; + } + + .stat-info h3 { + font-size: 1.5rem; + } +} + +@media (max-width: 480px) { + .projects-page { + padding: 0.75rem; + } + + .projects-header { + padding: 1rem; + } + + .header-content h1 { + font-size: 1.5rem; + } + + .header-content p { + font-size: 1rem; + } + + .project-stats { + grid-template-columns: 1fr; + } + + .project-card { + padding: 1rem; + } + + .project-stats-section { + padding: 0.75rem; + } + + .stat-item { + font-size: 0.8125rem; + } +} \ No newline at end of file diff --git a/templates/base_authenticated.html b/templates/base_authenticated.html index 39a219e..c508f5a 100644 --- a/templates/base_authenticated.html +++ b/templates/base_authenticated.html @@ -55,6 +55,11 @@ {% if session.role == 'admin' %} + + + Projects + Users diff --git a/templates/create_project.html b/templates/create_project.html new file mode 100644 index 0000000..ed47565 --- /dev/null +++ b/templates/create_project.html @@ -0,0 +1,159 @@ +{% extends "base.html" %} +{% block title %}Create Project - QR Code Management{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +
+ + + + +
+
+

Project Information

+
+ +
+
+
+ +
+ + + 0/100 +
+ + +
+ + + 0/500 + + + Provide a brief description of what this project is for + +
+ + +
+ + + Cancel + + +
+
+
+
+
+
+{% endblock %} + +{% block extra_scripts %} + +{% endblock %} \ No newline at end of file diff --git a/templates/create_qr_code.html b/templates/create_qr_code.html index c07422a..a974964 100644 --- a/templates/create_qr_code.html +++ b/templates/create_qr_code.html @@ -431,6 +431,25 @@ 0/100 + +
+ + + + + Organize your QR code by assigning it to a project + +
@@ -541,17 +560,9 @@ - - - + + +
@@ -581,6 +592,7 @@ initializeForm(); initializeCharacterCounters(); initializeCoordinatesFeature(); + initializeProjectDropdown(); }); function initializeForm() { @@ -730,12 +742,31 @@ } function updateHiddenFields() { - document.getElementById("address_latitude").value = - coordinatesData.latitude || ""; - document.getElementById("address_longitude").value = - coordinatesData.longitude || ""; - document.getElementById("coordinate_accuracy").value = - coordinatesData.accuracy || ""; + console.log("📝 Updating hidden coordinate fields"); + + // Update hidden form fields with coordinate data + const latitudeField = document.getElementById("latitude"); + const longitudeField = document.getElementById("longitude"); + const accuracyField = document.getElementById("coordinate_accuracy"); + + if (coordinatesData.latitude && coordinatesData.longitude) { + latitudeField.value = coordinatesData.latitude; + longitudeField.value = coordinatesData.longitude; + accuracyField.value = coordinatesData.accuracy || 'geocoded'; + + console.log("✓ Hidden fields updated:", { + latitude: latitudeField.value, + longitude: longitudeField.value, + accuracy: accuracyField.value + }); + } else { + // Clear fields if no coordinates + latitudeField.value = ''; + longitudeField.value = ''; + accuracyField.value = ''; + + console.log("✓ Hidden fields cleared"); + } } function clearCoordinates() { @@ -745,11 +776,18 @@ accuracy: null, }; - updateCoordinatesDisplay(); + // Clear display + document.getElementById("latitudeValue").textContent = "--"; + document.getElementById("longitudeValue").textContent = "--"; + document.getElementById("accuracyValue").textContent = "--"; + + // Clear hidden fields updateHiddenFields(); - // Clear status - document.getElementById("coordinateStatus").innerHTML = ""; + // Hide coordinates section + document.getElementById("coordinatesSection").style.display = "none"; + + console.log("🧹 Coordinates cleared"); } function showStatus(type, message) { @@ -776,47 +814,32 @@ } function validateForm() { - const requiredFields = [ - { id: "name", name: "QR Code Name" }, - { id: "location", name: "Location Name" }, - { id: "location_address", name: "Complete Address" }, - { id: "location_event", name: "Event or Purpose" }, - ]; + const name = document.getElementById("name").value.trim(); + const location = document.getElementById("location").value.trim(); + const address = document.getElementById("location_address").value.trim(); - let isValid = true; + if (!name || !location || !address) { + alert("Please fill in all required fields"); + return false; + } - requiredFields.forEach((field) => { - const input = document.getElementById(field.id); - if (!input.value.trim()) { - showFieldError(input, `${field.name} is required`); - isValid = false; - } else { - clearFieldError(input); + // DEBUG: Log coordinates being submitted + const latitude = document.getElementById("latitude").value; + const longitude = document.getElementById("longitude").value; + const accuracy = document.getElementById("coordinate_accuracy").value; + + console.log("🚀 Form submission data:", { + name: name, + location: location, + address: address, + coordinates: { + latitude: latitude, + longitude: longitude, + accuracy: accuracy } }); - // Additional validation - const name = document.getElementById("name").value.trim(); - if (name && name.length < 3) { - showFieldError( - document.getElementById("name"), - "QR Code name must be at least 3 characters" - ); - isValid = false; - } - - const address = document - .getElementById("location_address") - .value.trim(); - if (address && address.length < 10) { - showFieldError( - document.getElementById("location_address"), - "Please provide a complete address" - ); - isValid = false; - } - - return isValid; + return true; } function showFieldError(input, message) { @@ -842,6 +865,23 @@ errorElement.remove(); } } + + // Project dropdown functionality + function initializeProjectDropdown() { + const projectSelect = document.getElementById('project_id'); + + if (projectSelect) { + // Load projects dynamically (optional enhancement) + // For now, projects are loaded server-side + projectSelect.addEventListener('change', function() { + const selectedOption = this.options[this.selectedIndex]; + + if (selectedOption.value) { + console.log('Selected project:', selectedOption.text); + } + }); + } + } diff --git a/templates/edit_project.html b/templates/edit_project.html new file mode 100644 index 0000000..ef431f6 --- /dev/null +++ b/templates/edit_project.html @@ -0,0 +1,198 @@ +{% extends "base.html" %} +{% block title %}Edit Project - QR Code Management{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +
+ +
+
+

Edit Project

+

Update project information and settings

+
+ +
+ + +
+
+

Current Project Information

+
+ +
+
+
+ Project Name: + {{ project.name }} +
+ +
+ Description: + {{ project.description if project.description else 'No description provided' }} +
+ +
+ QR Codes: + {{ project.qr_count }} active QR codes +
+ +
+ Created: + {{ project.created_date.strftime('%B %d, %Y at %I:%M %p') }} +
+ +
+ Status: + + {% if project.active_status %}Active{% else %}Inactive{% endif %} + +
+
+
+
+ + +
+
+

Update Project Information

+
+ +
+
+
+ +
+ + + {{ project.name|length }}/100 +
+ + +
+ + + {{ (project.description|length) if project.description else 0 }}/500 + + + Provide a brief description of what this project is for + +
+ + +
+ + + Cancel + + +
+
+
+
+
+
+{% endblock %} + +{% block extra_scripts %} + +{% endblock %} \ No newline at end of file diff --git a/templates/edit_qr_code.html b/templates/edit_qr_code.html index 0c635f4..5d64530 100644 --- a/templates/edit_qr_code.html +++ b/templates/edit_qr_code.html @@ -535,6 +535,30 @@ >
+ +
+ + + + + {% if qr_code.project %} + Currently assigned to: {{ qr_code.project.name }} + {% else %} + This QR code is not assigned to any project + {% endif %} + +
diff --git a/templates/projects.html b/templates/projects.html new file mode 100644 index 0000000..62bbf29 --- /dev/null +++ b/templates/projects.html @@ -0,0 +1,188 @@ +{% extends "base.html" %} +{% block title %}Projects - QR Code Management{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} +
+ +
+
+

Projects

+

Organize and manage your QR code projects

+
+ +
+ + +
+
+
+ +
+
+

{{ projects|length }}

+

Total Projects

+
+
+ +
+
+ +
+
+

{{ projects|selectattr('active_status', 'equalto', True)|list|length }}

+

Active Projects

+
+
+ +
+
+ +
+
+

{{ projects|sum(attribute='total_qr_count') }}

+

Total QR Codes

+
+
+
+ + +
+
+

All Projects

+
+ + {% if projects %} +
+ {% for project in projects %} +
+
+
+

{{ project.name }}

+

+ {% if project.description %} + {{ project.description[:100] }}{% if project.description|length > 100 %}...{% endif %} + {% else %} + No description provided + {% endif %} +

+
+
+ {% if project.active_status %} + Active + {% else %} + Inactive + {% endif %} +
+
+ +
+
+ + {{ project.qr_count }} QR Codes +
+
+ + {{ project.created_date.strftime('%b %d, %Y') }} +
+
+ + {{ project.creator.full_name if project.creator else 'Unknown' }} +
+
+ +
+ + + Edit + + +
+ +
+
+
+ {% endfor %} +
+ {% else %} +
+
+ +
+

No Projects Yet

+

Create your first project to organize your QR codes

+ + + Create First Project + +
+ {% endif %} +
+
+{% endblock %} + +{% block extra_scripts %} + +{% endblock %} \ No newline at end of file