from flask import Flask, render_template, request, redirect, url_for, flash, jsonify from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.utils import secure_filename import sqlite3 import json import os from datetime import datetime import pandas as pd import unicodedata from flask import send_file app = Flask(__name__) app.secret_key = os.urandom(24) app.config['UPLOAD_FOLDER'] = 'uploads' app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB max file size # Create uploads folder if it doesn't exist os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) # Flask-Login setup login_manager = LoginManager() login_manager.init_app(app) login_manager.login_view = 'login' # Database connection def get_db(): db = sqlite3.connect('checkin.db') db.row_factory = sqlite3.Row db.execute('PRAGMA foreign_keys = ON') return db # Database migration function def migrate_database(): """Add new columns if they don't exist""" db = get_db() cursor = db.cursor() # Check existing columns existing_columns = cursor.execute("PRAGMA table_info(events)").fetchall() column_names = [col[1] for col in existing_columns] # Add is_passed column if it doesn't exist if 'is_passed' not in column_names: try: cursor.execute('ALTER TABLE events ADD COLUMN is_passed BOOLEAN DEFAULT 0') print("Added is_passed column to events table") except sqlite3.OperationalError as e: print(f"Error adding is_passed column: {e}") # Check if event_columns table exists tables = cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='event_columns'").fetchall() if not tables: # Create event_columns table cursor.execute(''' CREATE TABLE IF NOT EXISTS event_columns ( id INTEGER PRIMARY KEY AUTOINCREMENT, event_id INTEGER NOT NULL, column_name TEXT NOT NULL, column_order INTEGER NOT NULL, FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE ) ''') print("Created event_columns table") # Check if additional_data column exists in guests table existing_guest_columns = cursor.execute("PRAGMA table_info(guests)").fetchall() guest_column_names = [col[1] for col in existing_guest_columns] if 'additional_data' not in guest_column_names: try: cursor.execute('ALTER TABLE guests ADD COLUMN additional_data TEXT') print("Added additional_data column to guests table") except sqlite3.OperationalError as e: print(f"Error adding additional_data column: {e}") # Migrate old data if needed if 'additional_data' in guest_column_names and all(col in guest_column_names for col in ['data1', 'data2', 'data3', 'data4', 'data5']): # Check if there's old data to migrate old_guests = cursor.execute( 'SELECT id, data1, data2, data3, data4, data5 FROM guests WHERE additional_data IS NULL' ).fetchall() if old_guests: for guest in old_guests: additional_data = {} if guest['data1']: additional_data['data1'] = guest['data1'] if guest['data2']: additional_data['data2'] = guest['data2'] if guest['data3']: additional_data['data3'] = guest['data3'] if guest['data4']: additional_data['data4'] = guest['data4'] if guest['data5']: additional_data['data5'] = guest['data5'] if additional_data: cursor.execute( 'UPDATE guests SET additional_data = ? WHERE id = ?', (json.dumps(additional_data), guest['id']) ) print(f"Migrated {len(old_guests)} old guest records") db.commit() db.close() print("Database migration completed") # Initialize database def init_db(): with app.app_context(): db = get_db() # Create tables if they don't exist with app.open_resource('schema.sql', mode='r') as f: db.cursor().executescript(f.read()) # Create default users if they don't exist admin = db.execute('SELECT * FROM users WHERE username = ?', ('admin',)).fetchone() if not admin: db.execute( 'INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)', ('admin', generate_password_hash('admin123'), 'admin') ) manager = db.execute('SELECT * FROM users WHERE username = ?', ('manager',)).fetchone() if not manager: db.execute( 'INSERT INTO users (username, password_hash, role) VALUES (?, ?, ?)', ('manager', generate_password_hash('manager123'), 'manager') ) db.commit() db.close() # Run migration to add new columns migrate_database() # User class for Flask-Login class User(UserMixin): def __init__(self, id, username, role): self.id = id self.username = username self.role = role @login_manager.user_loader def load_user(user_id): db = get_db() user = db.execute('SELECT * FROM users WHERE id = ?', (user_id,)).fetchone() db.close() if user: return User(user['id'], user['username'], user['role']) return None # Helper function to process Excel file def process_excel_file(file_path): try: # Read Excel file df = pd.read_excel(file_path) # Print available columns for debugging print(f"Excel columns found: {list(df.columns)}") # Create a mapping of possible column names to standard names column_mapping = {} # Define all possible name mappings name_variants = ['имя', 'name', 'first_name', 'first name', 'firstname', 'имя гостя'] surname_variants = ['фамилия', 'surname', 'last_name', 'last name', 'lastname', 'фамилия гостя'] second_name_variants = ['отчество', 'second_name', 'second name', 'middlename', 'middle_name', 'middle name', 'patronymic'] for col in df.columns: col_original = str(col) col_lower = col_original.lower().strip() # Check if this is a name column if col_lower in name_variants: column_mapping[col] = 'name' print(f"Mapped '{col}' -> 'name'") # Check if this is a surname column elif col_lower in surname_variants: column_mapping[col] = 'surname' print(f"Mapped '{col}' -> 'surname'") # Check if this is a second name column elif col_lower in second_name_variants: column_mapping[col] = 'second_name' print(f"Mapped '{col}' -> 'second_name'") else: # Keep original column name for additional data # Don't lowercase to preserve original formatting column_mapping[col] = col_original # Rename columns based on mapping df = df.rename(columns=column_mapping) # Now check for required columns if 'name' not in df.columns and 'surname' not in df.columns: raise ValueError( f"Excel file must contain 'Фамилия' and 'Имя' columns.\n" f"Found columns: {', '.join(df.columns)}\n" f"Please ensure your Excel file has columns named 'Фамилия' and 'Имя'" ) elif 'name' not in df.columns: raise ValueError( f"Excel file must contain an 'Имя' column.\n" f"Found columns: {', '.join(df.columns)}\n" f"Please add a column named 'Имя'" ) elif 'surname' not in df.columns: raise ValueError( f"Excel file must contain a 'Фамилия' column.\n" f"Found columns: {', '.join(df.columns)}\n" f"Please add a column named 'Фамилия'" ) # Separate required columns from additional columns required_cols = ['name', 'surname'] if 'second_name' in df.columns: required_cols.append('second_name') # Get additional columns (keep original names from Excel) additional_cols = [col for col in df.columns if col not in required_cols] return df, required_cols, additional_cols except Exception as e: raise Exception(f"Error processing Excel file: {str(e)}") # Routes @app.route('/') def index(): if current_user.is_authenticated: if current_user.role == 'admin': return redirect(url_for('admin_dashboard')) else: return redirect(url_for('manager_dashboard')) return redirect(url_for('login')) @app.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': username = request.form['username'] password = request.form['password'] db = get_db() user = db.execute('SELECT * FROM users WHERE username = ?', (username,)).fetchone() db.close() if user and check_password_hash(user['password_hash'], password): user_obj = User(user['id'], user['username'], user['role']) login_user(user_obj) flash('Успешно вошли!', 'success') if user['role'] == 'admin': return redirect(url_for('admin_dashboard')) else: return redirect(url_for('manager_dashboard')) else: flash('Неправильные логин или пароль', 'error') return render_template('login.html') @app.route('/logout') @login_required def logout(): logout_user() return redirect(url_for('login')) @app.route('/manager/event//export') @login_required def export_event_stats(event_id): if current_user.role not in ['manager', 'admin']: flash('Access denied', 'error') return redirect(url_for('index')) db = get_db() # Get event details event = db.execute('SELECT * FROM events WHERE id = ?', (event_id,)).fetchone() if not event: flash('Event not found', 'error') return redirect(url_for('manager_dashboard')) # Get column definitions try: columns = db.execute( 'SELECT column_name FROM event_columns WHERE event_id = ? ORDER BY column_order', (event_id,) ).fetchall() column_list = [c['column_name'] for c in columns] except: column_list = ['name', 'surname'] # Get all guests for this event guests = db.execute( '''SELECT id, name, surname, second_name, additional_data, checked_in, checked_in_at FROM guests WHERE event_id = ? ORDER BY surname, name''', (event_id,) ).fetchall() # Get statistics total_guests = len(guests) checked_in_count = sum(1 for g in guests if g['checked_in']) not_checked_in_count = total_guests - checked_in_count check_in_percentage = round((checked_in_count / total_guests * 100), 2) if total_guests > 0 else 0 db.close() # Create Excel file in memory from io import BytesIO from openpyxl import Workbook from openpyxl.styles import Font, PatternFill, Alignment, Border, Side from openpyxl.utils import get_column_letter wb = Workbook() # ===== Sheet 1: Summary Statistics ===== ws_summary = wb.active ws_summary.title = "Check-in Summary" # Title ws_summary.merge_cells('A1:D1') ws_summary['A1'] = f"Event Check-in Statistics: {event['name']}" ws_summary['A1'].font = Font(size=16, bold=True, color='2c3e50') ws_summary['A1'].alignment = Alignment(horizontal='center') # Event info ws_summary['A3'] = 'Event Name:' ws_summary['B3'] = event['name'] ws_summary['A3'].font = Font(bold=True) ws_summary['A4'] = 'Created Date:' ws_summary['B4'] = event['created_at'] ws_summary['A4'].font = Font(bold=True) ws_summary['A5'] = 'Export Date:' ws_summary['B5'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S') ws_summary['A5'].font = Font(bold=True) # Statistics header ws_summary['A7'] = 'Statistics' ws_summary['A7'].font = Font(size=14, bold=True, color='2c3e50') # Statistics data stats = [ ('Total Guests', total_guests), ('Checked In', checked_in_count), ('Not Checked In', not_checked_in_count), ('Check-in Percentage', f'{check_in_percentage}%') ] header_fill = PatternFill(start_color='3498db', end_color='3498db', fill_type='solid') header_font = Font(bold=True, color='ffffff') ws_summary['A9'] = 'Metric' ws_summary['B9'] = 'Value' ws_summary['A9'].fill = header_fill ws_summary['B9'].fill = header_fill ws_summary['A9'].font = header_font ws_summary['B9'].font = header_font for idx, (metric, value) in enumerate(stats, start=10): ws_summary[f'A{idx}'] = metric ws_summary[f'B{idx}'] = value if idx % 2 == 0: ws_summary[f'A{idx}'].fill = PatternFill(start_color='ecf0f1', end_color='ecf0f1', fill_type='solid') ws_summary[f'B{idx}'].fill = PatternFill(start_color='ecf0f1', end_color='ecf0f1', fill_type='solid') # Adjust column widths for summary ws_summary.column_dimensions['A'].width = 25 ws_summary.column_dimensions['B'].width = 30 ws_summary.column_dimensions['C'].width = 20 ws_summary.column_dimensions['D'].width = 20 # ===== Sheet 2: Detailed Guest List ===== ws_guests = wb.create_sheet("Guest List") # Headers headers = ['#', 'Surname', 'Name'] if 'second_name' in column_list: headers.append('Second Name') # Add additional columns additional_cols = [col for col in column_list if col not in ['name', 'surname', 'second_name']] for col in additional_cols: headers.append(col.replace('_', ' ').title()) headers.extend(['Check-in Status', 'Check-in Time']) # Style for headers for col_idx, header in enumerate(headers, 1): cell = ws_guests.cell(row=1, column=col_idx, value=header) cell.fill = PatternFill(start_color='2c3e50', end_color='2c3e50', fill_type='solid') cell.font = Font(bold=True, color='ffffff', size=11) cell.alignment = Alignment(horizontal='center', vertical='center') # Border style thin_border = Border( left=Side(style='thin'), right=Side(style='thin'), top=Side(style='thin'), bottom=Side(style='thin') ) # Add data rows checked_in_fill = PatternFill(start_color='d4edda', end_color='d4edda', fill_type='solid') not_checked_fill = PatternFill(start_color='f8d7da', end_color='f8d7da', fill_type='solid') for row_idx, guest in enumerate(guests, 2): # Row number ws_guests.cell(row=row_idx, column=1, value=row_idx - 1) # Basic info ws_guests.cell(row=row_idx, column=2, value=guest['surname']) ws_guests.cell(row=row_idx, column=3, value=guest['name']) col_offset = 4 if 'second_name' in column_list: ws_guests.cell(row=row_idx, column=col_offset, value=guest['second_name'] or '') col_offset += 1 # Additional data additional_data = {} if guest['additional_data']: try: additional_data = json.loads(guest['additional_data']) except: pass for col in additional_cols: ws_guests.cell(row=row_idx, column=col_offset, value=additional_data.get(col, '')) col_offset += 1 # Check-in status status = 'Checked In' if guest['checked_in'] else 'Not Checked In' ws_guests.cell(row=row_idx, column=col_offset, value=status) # Check-in time checkin_time = guest['checked_in_at'] if guest['checked_in_at'] else '-' ws_guests.cell(row=row_idx, column=col_offset + 1, value=checkin_time) # Apply row styling based on check-in status for col_idx in range(1, len(headers) + 1): cell = ws_guests.cell(row=row_idx, column=col_idx) cell.border = thin_border cell.alignment = Alignment(vertical='center') if guest['checked_in']: cell.fill = checked_in_fill else: cell.fill = not_checked_fill # Auto-adjust column widths for guest list for col_idx in range(1, len(headers) + 1): column_letter = get_column_letter(col_idx) max_length = 0 for row in ws_guests.iter_rows(min_col=col_idx, max_col=col_idx, values_only=True): for cell_value in row: if cell_value: max_length = max(max_length, len(str(cell_value))) adjusted_width = min(max_length + 2, 50) ws_guests.column_dimensions[column_letter].width = max(adjusted_width, 12) # Freeze header row ws_guests.freeze_panes = 'A2' # Add autofilter ws_guests.auto_filter.ref = f"A1:{get_column_letter(len(headers))}{len(guests) + 1}" # ===== Sheet 3: Checked-in Guests Only ===== ws_checked = wb.create_sheet("Checked-in Guests") # Copy headers for col_idx, header in enumerate(headers, 1): cell = ws_checked.cell(row=1, column=col_idx, value=header) cell.fill = PatternFill(start_color='27ae60', end_color='27ae60', fill_type='solid') cell.font = Font(bold=True, color='ffffff', size=11) cell.alignment = Alignment(horizontal='center', vertical='center') # Add only checked-in guests checked_guests = [g for g in guests if g['checked_in']] for row_idx, guest in enumerate(checked_guests, 2): ws_checked.cell(row=row_idx, column=1, value=row_idx - 1) ws_checked.cell(row=row_idx, column=2, value=guest['surname']) ws_checked.cell(row=row_idx, column=3, value=guest['name']) col_offset = 4 if 'second_name' in column_list: ws_checked.cell(row=row_idx, column=col_offset, value=guest['second_name'] or '') col_offset += 1 additional_data = {} if guest['additional_data']: try: additional_data = json.loads(guest['additional_data']) except: pass for col in additional_cols: ws_checked.cell(row=row_idx, column=col_offset, value=additional_data.get(col, '')) col_offset += 1 ws_checked.cell(row=row_idx, column=col_offset, value='Checked In') ws_checked.cell(row=row_idx, column=col_offset + 1, value=guest['checked_in_at'] if guest['checked_in_at'] else '') for col_idx in range(1, len(headers) + 1): cell = ws_checked.cell(row=row_idx, column=col_idx) cell.border = thin_border cell.alignment = Alignment(vertical='center') cell.fill = PatternFill(start_color='d4edda', end_color='d4edda', fill_type='solid') # Auto-adjust columns for checked-in sheet for col_idx in range(1, len(headers) + 1): column_letter = get_column_letter(col_idx) max_length = 0 for row in ws_checked.iter_rows(min_col=col_idx, max_col=col_idx, values_only=True): for cell_value in row: if cell_value: max_length = max(max_length, len(str(cell_value))) adjusted_width = min(max_length + 2, 50) ws_checked.column_dimensions[column_letter].width = max(adjusted_width, 12) ws_checked.freeze_panes = 'A2' ws_checked.auto_filter.ref = f"A1:{get_column_letter(len(headers))}{len(checked_guests) + 1}" # Save to BytesIO output = BytesIO() wb.save(output) output.seek(0) # Generate filename filename = f"checkin_stats_{event['name'].replace(' ', '_')}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx" return send_file( output, mimetype='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', as_attachment=True, download_name=filename ) # Admin routes @app.route('/admin/dashboard') @login_required def admin_dashboard(): if current_user.role != 'admin': flash('Access denied', 'error') return redirect(url_for('index')) db = get_db() # Check if is_passed column exists columns = db.execute("PRAGMA table_info(events)").fetchall() column_names = [col[1] for col in columns] if 'is_passed' not in column_names: events = db.execute( 'SELECT *, 0 as is_passed FROM events ORDER BY created_at DESC' ).fetchall() else: events = db.execute( 'SELECT * FROM events ORDER BY created_at DESC' ).fetchall() # Get guest counts for each event event_data = [] for event in events: guest_count = db.execute( 'SELECT COUNT(*) as count FROM guests WHERE event_id = ?', (event['id'],) ).fetchone()['count'] checked_in_count = db.execute( 'SELECT COUNT(*) as count FROM guests WHERE event_id = ? AND checked_in = 1', (event['id'],) ).fetchone()['count'] # Safely get is_passed is_passed = event['is_passed'] if 'is_passed' in event.keys() else 0 event_data.append({ 'id': event['id'], 'name': event['name'], 'created_at': event['created_at'], 'is_active': event['is_active'], 'is_passed': is_passed, 'guest_count': guest_count, 'checked_in_count': checked_in_count }) db.close() return render_template('admin_dashboard.html', events=event_data) @app.route('/admin/create_event', methods=['GET', 'POST']) @login_required def create_event(): if current_user.role != 'admin': flash('Access denied', 'error') return redirect(url_for('index')) if request.method == 'POST': event_name = request.form['event_name'] excel_file = request.files.get('guest_list') if not event_name: flash('Event name is required', 'error') return render_template('create_event.html') if not excel_file or not excel_file.filename: flash('Excel file is required', 'error') return render_template('create_event.html') if not excel_file.filename.endswith(('.xlsx', '.xls')): flash('Only Excel files (.xlsx, .xls) are allowed', 'error') return render_template('create_event.html') db = get_db() try: # Save uploaded file filename = secure_filename(f"{datetime.now().strftime('%Y%m%d_%H%M%S')}_{excel_file.filename}") file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) excel_file.save(file_path) # Process Excel file df, required_cols, additional_cols = process_excel_file(file_path) cursor = db.cursor() # Create event cursor.execute( 'INSERT INTO events (name, created_by) VALUES (?, ?)', (event_name, current_user.id) ) event_id = cursor.lastrowid # Save column definitions all_columns = required_cols + additional_cols for idx, col in enumerate(all_columns): cursor.execute( 'INSERT INTO event_columns (event_id, column_name, column_order) VALUES (?, ?, ?)', (event_id, col, idx) ) # Import guests for _, row in df.iterrows(): # Prepare required data name = str(row.get('name', '')).strip() surname = str(row.get('surname', '')).strip() second_name = str(row.get('second_name', '')).strip() if 'second_name' in df.columns else '' # Prepare additional data as JSON additional_data = {} for col in additional_cols: value = row[col] if pd.notna(value): additional_data[col] = str(value).strip() else: additional_data[col] = '' cursor.execute( '''INSERT INTO guests (event_id, name, surname, second_name, additional_data) VALUES (?, ?, ?, ?, ?)''', (event_id, name, surname, second_name, json.dumps(additional_data)) ) db.commit() # Clean up uploaded file if os.path.exists(file_path): os.remove(file_path) flash(f'Event created successfully with {len(df)} guests imported!', 'success') return redirect(url_for('admin_dashboard')) except Exception as e: db.rollback() flash(f'Error: {str(e)}', 'error') return render_template('create_event.html') finally: db.close() return render_template('create_event.html') @app.route('/admin/edit_event/', methods=['GET', 'POST']) @login_required def edit_event(event_id): if current_user.role != 'admin': flash('Access denied', 'error') return redirect(url_for('index')) db = get_db() event = db.execute('SELECT * FROM events WHERE id = ?', (event_id,)).fetchone() if not event: flash('Event not found', 'error') return redirect(url_for('admin_dashboard')) if request.method == 'POST': event_name = request.form['event_name'] excel_file = request.files.get('guest_list') if not event_name: flash('Event name is required', 'error') return render_template('edit_event.html', event=event) try: # Update event name db.execute('UPDATE events SET name = ? WHERE id = ?', (event_name, event_id)) # If new Excel file is uploaded, reset guest data if excel_file and excel_file.filename: if not excel_file.filename.endswith(('.xlsx', '.xls')): flash('Only Excel files (.xlsx, .xls) are allowed', 'error') return render_template('edit_event.html', event=event) # Save uploaded file filename = secure_filename(f"{datetime.now().strftime('%Y%m%d_%H%M%S')}_{excel_file.filename}") file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) excel_file.save(file_path) # Process Excel file df, required_cols, additional_cols = process_excel_file(file_path) # Delete old guests and columns db.execute('DELETE FROM guests WHERE event_id = ?', (event_id,)) db.execute('DELETE FROM event_columns WHERE event_id = ?', (event_id,)) # Save new column definitions all_columns = required_cols + additional_cols for idx, col in enumerate(all_columns): db.execute( 'INSERT INTO event_columns (event_id, column_name, column_order) VALUES (?, ?, ?)', (event_id, col, idx) ) # Import new guests for _, row in df.iterrows(): name = str(row.get('name', '')).strip() surname = str(row.get('surname', '')).strip() second_name = str(row.get('second_name', '')).strip() if 'second_name' in df.columns else '' additional_data = {} for col in additional_cols: value = row[col] if pd.notna(value): additional_data[col] = str(value).strip() else: additional_data[col] = '' db.execute( '''INSERT INTO guests (event_id, name, surname, second_name, additional_data) VALUES (?, ?, ?, ?, ?)''', (event_id, name, surname, second_name, json.dumps(additional_data)) ) # Clean up uploaded file if os.path.exists(file_path): os.remove(file_path) flash(f'Event updated and {len(df)} guests imported!', 'success') else: flash('Event name updated successfully!', 'success') db.commit() return redirect(url_for('admin_dashboard')) except Exception as e: db.rollback() flash(f'Error: {str(e)}', 'error') return render_template('edit_event.html', event=event) finally: db.close() # Get current columns for display try: columns = db.execute( 'SELECT column_name FROM event_columns WHERE event_id = ? ORDER BY column_order', (event_id,) ).fetchall() column_list = [c['column_name'] for c in columns] except: column_list = [] db.close() return render_template('edit_event.html', event=event, columns=column_list) @app.route('/admin/toggle_event_passed/', methods=['POST']) @login_required def toggle_event_passed(event_id): if current_user.role != 'admin': return jsonify({'success': False, 'message': 'Access denied'}), 403 db = get_db() event = db.execute('SELECT * FROM events WHERE id = ?', (event_id,)).fetchone() if event: # Safely get is_passed value try: current_status = event['is_passed'] except (KeyError, IndexError): current_status = 0 new_status = 0 if current_status else 1 try: db.execute('UPDATE events SET is_passed = ? WHERE id = ?', (new_status, event_id)) except sqlite3.OperationalError: # If column doesn't exist, add it first db.execute('ALTER TABLE events ADD COLUMN is_passed BOOLEAN DEFAULT 0') db.execute('UPDATE events SET is_passed = ? WHERE id = ?', (new_status, event_id)) db.commit() status_text = 'marked as passed' if new_status else 'marked as active' flash(f'Event {status_text} successfully!', 'success') else: flash('Event not found', 'error') db.close() return redirect(url_for('admin_dashboard')) # Manager routes @app.route('/manager/dashboard') @login_required def manager_dashboard(): if current_user.role not in ['manager', 'admin']: flash('Access denied', 'error') return redirect(url_for('index')) db = get_db() # Check if is_passed column exists columns = db.execute("PRAGMA table_info(events)").fetchall() column_names = [col[1] for col in columns] if 'is_passed' not in column_names: events = db.execute( 'SELECT *, 0 as is_passed FROM events WHERE is_active = 1 ORDER BY created_at DESC' ).fetchall() else: events = db.execute( 'SELECT * FROM events WHERE is_active = 1 AND is_passed = 0 ORDER BY created_at DESC' ).fetchall() db.close() return render_template('event_select.html', events=events) @app.route('/manager/event/') @login_required def guest_search(event_id): if current_user.role not in ['manager', 'admin']: flash('Access denied', 'error') return redirect(url_for('index')) db = get_db() event = db.execute('SELECT * FROM events WHERE id = ?', (event_id,)).fetchone() if not event: flash('Event not found', 'error') return redirect(url_for('manager_dashboard')) # Get column definitions try: columns = db.execute( 'SELECT column_name FROM event_columns WHERE event_id = ? ORDER BY column_order', (event_id,) ).fetchall() column_list = [c['column_name'] for c in columns] except: # If event_columns table doesn't exist or no columns defined column_list = ['name', 'surname'] # Get all unique first letters of surnames for this event letters = db.execute( '''SELECT DISTINCT UPPER(SUBSTR(surname, 1, 1)) as letter FROM guests WHERE event_id = ? AND surname != '' ORDER BY letter''', (event_id,) ).fetchall() db.close() return render_template( 'guest_search.html', event=event, letters=[l['letter'] for l in letters], columns=column_list ) @app.route('/api/search_guests/') @login_required def search_guests(event_id): search_term = request.args.get('term', '') # Debug logging app.logger.info(f"Search term received: '{search_term}'") app.logger.info(f"Search term bytes: {search_term.encode('utf-8')}") db = get_db() if search_term and search_term != 'ALL': # Use LIKE directly without UPPER() to avoid Unicode issues # This works because SQLite LIKE is case-insensitive for ASCII # For Unicode, we'll do a case-insensitive comparison in a different way # Method 1: Direct LIKE comparison (works for both Latin and Cyrillic in most cases) search_pattern = f'{search_term}%' app.logger.info(f"Search pattern: '{search_pattern}'") # Try the search guests = db.execute( '''SELECT id, name, surname, second_name, additional_data, checked_in, checked_in_at FROM guests WHERE event_id = ? AND surname LIKE ? ORDER BY surname, name''', (event_id, search_pattern) ).fetchall() app.logger.info(f"Found {len(guests)} guests with direct LIKE") # If no results and search term might be mixed case, try case-insensitive approach if len(guests) == 0: # Get all guests and filter manually (less efficient but works for Unicode) all_guests = db.execute( '''SELECT id, name, surname, second_name, additional_data, checked_in, checked_in_at FROM guests WHERE event_id = ? ORDER BY surname, name''', (event_id,) ).fetchall() # Manual filtering search_upper = search_term.upper() guests = [g for g in all_guests if g['surname'].upper().startswith(search_upper)] app.logger.info(f"Found {len(guests)} guests with manual filtering") else: # Show all guests guests = db.execute( '''SELECT id, name, surname, second_name, additional_data, checked_in, checked_in_at FROM guests WHERE event_id = ? ORDER BY surname, name''', (event_id,) ).fetchall() # Get column definitions try: columns = db.execute( 'SELECT column_name FROM event_columns WHERE event_id = ? ORDER BY column_order', (event_id,) ).fetchall() column_list = [c['column_name'] for c in columns] except: column_list = ['name', 'surname'] db.close() # Process guests data guest_list = [] for g in guests: additional_data = {} if g['additional_data']: try: additional_data = json.loads(g['additional_data']) except: additional_data = {} guest_list.append({ 'id': g['id'], 'name': g['name'], 'surname': g['surname'], 'second_name': g['second_name'] or '', 'additional_data': additional_data, 'checked_in': g['checked_in'], 'checked_in_at': g['checked_in_at'] if g['checked_in_at'] else None }) return jsonify({ 'guests': guest_list, 'columns': column_list }) @app.route('/api/checkin/', methods=['POST']) @login_required def checkin_guest(guest_id): if current_user.role not in ['manager', 'admin']: return jsonify({'success': False, 'message': 'Access denied'}), 403 data = request.get_json() check_in = data.get('check_in', True) db = get_db() try: if check_in: db.execute( 'UPDATE guests SET checked_in = 1, checked_in_at = ? WHERE id = ? AND checked_in = 0', (datetime.now(), guest_id) ) else: db.execute( 'UPDATE guests SET checked_in = 0, checked_in_at = NULL WHERE id = ? AND checked_in = 1', (guest_id,) ) db.commit() if db.total_changes > 0: action = 'checked in' if check_in else 'unchecked' return jsonify({ 'success': True, 'message': f'Guest {action} successfully', 'checked_in': check_in, 'checked_in_at': datetime.now().isoformat() if check_in else None }) else: status = 'already checked in' if check_in else 'not checked in' return jsonify({'success': False, 'message': f'Guest {status}'}) except Exception as e: return jsonify({'success': False, 'message': str(e)}), 500 finally: db.close() @app.route('/api/update_guest/', methods=['POST']) @login_required def update_guest(guest_id): if current_user.role not in ['manager', 'admin']: return jsonify({'success': False, 'message': 'Access denied'}), 403 data = request.get_json() db = get_db() try: # Get current guest data guest = db.execute('SELECT * FROM guests WHERE id = ?', (guest_id,)).fetchone() if not guest: return jsonify({'success': False, 'message': 'Guest not found'}), 404 # Update basic fields name = data.get('name', guest['name']) surname = data.get('surname', guest['surname']) second_name = data.get('second_name', guest['second_name']) # Update additional data additional_data = {} if guest['additional_data']: try: additional_data = json.loads(guest['additional_data']) except: pass # Update additional fields from the request if 'additional_data' in data: for key, value in data['additional_data'].items(): additional_data[key] = value # Update the database db.execute( '''UPDATE guests SET name = ?, surname = ?, second_name = ?, additional_data = ? WHERE id = ?''', (name, surname, second_name, json.dumps(additional_data), guest_id) ) db.commit() # Fetch updated guest updated_guest = db.execute( '''SELECT id, name, surname, second_name, additional_data, checked_in, checked_in_at FROM guests WHERE id = ?''', (guest_id,) ).fetchone() # Process additional data for response updated_additional_data = {} if updated_guest['additional_data']: try: updated_additional_data = json.loads(updated_guest['additional_data']) except: pass return jsonify({ 'success': True, 'message': 'Guest updated successfully', 'guest': { 'id': updated_guest['id'], 'name': updated_guest['name'], 'surname': updated_guest['surname'], 'second_name': updated_guest['second_name'] or '', 'additional_data': updated_additional_data, 'checked_in': updated_guest['checked_in'], 'checked_in_at': updated_guest['checked_in_at'] if updated_guest['checked_in_at'] else None } }) except Exception as e: db.rollback() return jsonify({'success': False, 'message': str(e)}), 500 finally: db.close() if __name__ == '__main__': init_db() app.run(debug=True, host='0.0.0.0', port=5000)