""" Global Search API Provides unified search across all app content and features Security: All searches filtered by user_id to prevent data leakage """ from flask import Blueprint, request, jsonify from flask_login import login_required, current_user from app.models import Expense, Document, Category, RecurringExpense, Tag from sqlalchemy import or_, func from datetime import datetime bp = Blueprint('search', __name__, url_prefix='/api/search') # App features/pages for navigation APP_FEATURES = [ { 'id': 'dashboard', 'name': 'Dashboard', 'name_ro': 'Tablou de bord', 'description': 'View your financial overview', 'description_ro': 'Vezi prezentarea generală financiară', 'icon': 'dashboard', 'url': '/dashboard', 'keywords': ['dashboard', 'tablou', 'bord', 'overview', 'home', 'start'] }, { 'id': 'transactions', 'name': 'Transactions', 'name_ro': 'Tranzacții', 'description': 'Manage your expenses and transactions', 'description_ro': 'Gestionează cheltuielile și tranzacțiile', 'icon': 'receipt_long', 'url': '/transactions', 'keywords': ['transactions', 'tranzactii', 'expenses', 'cheltuieli', 'spending'] }, { 'id': 'recurring', 'name': 'Recurring Expenses', 'name_ro': 'Cheltuieli recurente', 'description': 'Manage subscriptions and recurring bills', 'description_ro': 'Gestionează abonamente și facturi recurente', 'icon': 'repeat', 'url': '/recurring', 'keywords': ['recurring', 'recurente', 'subscriptions', 'abonamente', 'bills', 'facturi', 'monthly'] }, { 'id': 'reports', 'name': 'Reports', 'name_ro': 'Rapoarte', 'description': 'View detailed financial reports', 'description_ro': 'Vezi rapoarte financiare detaliate', 'icon': 'analytics', 'url': '/reports', 'keywords': ['reports', 'rapoarte', 'analytics', 'analize', 'statistics', 'statistici'] }, { 'id': 'documents', 'name': 'Documents', 'name_ro': 'Documente', 'description': 'Upload and manage your documents', 'description_ro': 'Încarcă și gestionează documentele', 'icon': 'description', 'url': '/documents', 'keywords': ['documents', 'documente', 'files', 'fisiere', 'upload', 'receipts', 'chitante'] }, { 'id': 'settings', 'name': 'Settings', 'name_ro': 'Setări', 'description': 'Configure your account settings', 'description_ro': 'Configurează setările contului', 'icon': 'settings', 'url': '/settings', 'keywords': ['settings', 'setari', 'preferences', 'preferinte', 'account', 'cont', 'profile', 'profil'] } ] # Admin-only features ADMIN_FEATURES = [ { 'id': 'admin', 'name': 'Admin Panel', 'name_ro': 'Panou Admin', 'description': 'Manage users and system settings', 'description_ro': 'Gestionează utilizatori și setări sistem', 'icon': 'admin_panel_settings', 'url': '/admin', 'keywords': ['admin', 'administration', 'users', 'utilizatori', 'system', 'sistem'] } ] @bp.route('/', methods=['GET']) @login_required def global_search(): """ Global search across all content and app features Security: All data searches filtered by current_user.id Query params: - q: Search query string - limit: Max results per category (default 5) Returns: - features: Matching app features/pages - expenses: Matching expenses (by description or OCR text) - documents: Matching documents (by filename or OCR text) - categories: Matching categories - recurring: Matching recurring expenses """ query = request.args.get('q', '').strip() limit = request.args.get('limit', 5, type=int) if not query or len(query) < 2: return jsonify({ 'success': False, 'message': 'Query must be at least 2 characters' }), 400 results = { 'features': [], 'expenses': [], 'documents': [], 'categories': [], 'recurring': [], 'tags': [] } # Search app features query_lower = query.lower() for feature in APP_FEATURES: # Check if query matches any keyword if any(query_lower in keyword.lower() for keyword in feature['keywords']): results['features'].append({ 'id': feature['id'], 'type': 'feature', 'name': feature['name'], 'name_ro': feature['name_ro'], 'description': feature['description'], 'description_ro': feature['description_ro'], 'icon': feature['icon'], 'url': feature['url'] }) # Add admin features if user is admin if current_user.is_admin: for feature in ADMIN_FEATURES: if any(query_lower in keyword.lower() for keyword in feature['keywords']): results['features'].append({ 'id': feature['id'], 'type': 'feature', 'name': feature['name'], 'name_ro': feature['name_ro'], 'description': feature['description'], 'description_ro': feature['description_ro'], 'icon': feature['icon'], 'url': feature['url'] }) # Search expenses - Security: filter by user_id expense_query = Expense.query.filter_by(user_id=current_user.id) expense_query = expense_query.filter( or_( Expense.description.ilike(f'%{query}%'), Expense.receipt_ocr_text.ilike(f'%{query}%') ) ) expenses = expense_query.order_by(Expense.date.desc()).limit(limit).all() for expense in expenses: # Check if match is from OCR text ocr_match = expense.receipt_ocr_text and query_lower in expense.receipt_ocr_text.lower() results['expenses'].append({ 'id': expense.id, 'type': 'expense', 'description': expense.description, 'amount': expense.amount, 'currency': expense.currency, 'category_name': expense.category.name if expense.category else None, 'category_color': expense.category.color if expense.category else None, 'date': expense.date.isoformat(), 'has_receipt': bool(expense.receipt_path), 'ocr_match': ocr_match, 'url': '/transactions' }) # Search documents - Security: filter by user_id doc_query = Document.query.filter_by(user_id=current_user.id) doc_query = doc_query.filter( or_( Document.original_filename.ilike(f'%{query}%'), Document.ocr_text.ilike(f'%{query}%') ) ) documents = doc_query.order_by(Document.created_at.desc()).limit(limit).all() for doc in documents: # Check if match is from OCR text ocr_match = doc.ocr_text and query_lower in doc.ocr_text.lower() results['documents'].append({ 'id': doc.id, 'type': 'document', 'filename': doc.original_filename, 'file_type': doc.file_type, 'file_size': doc.file_size, 'category': doc.document_category, 'created_at': doc.created_at.isoformat(), 'ocr_match': ocr_match, 'url': '/documents' }) # Search categories - Security: filter by user_id categories = Category.query.filter_by(user_id=current_user.id).filter( Category.name.ilike(f'%{query}%') ).order_by(Category.display_order).limit(limit).all() for category in categories: results['categories'].append({ 'id': category.id, 'type': 'category', 'name': category.name, 'color': category.color, 'icon': category.icon, 'url': '/transactions' }) # Search recurring expenses - Security: filter by user_id recurring = RecurringExpense.query.filter_by(user_id=current_user.id).filter( or_( RecurringExpense.name.ilike(f'%{query}%'), RecurringExpense.notes.ilike(f'%{query}%') ) ).order_by(RecurringExpense.next_due_date).limit(limit).all() for rec in recurring: results['recurring'].append({ 'id': rec.id, 'type': 'recurring', 'name': rec.name, 'amount': rec.amount, 'currency': rec.currency, 'frequency': rec.frequency, 'category_name': rec.category.name if rec.category else None, 'category_color': rec.category.color if rec.category else None, 'next_due_date': rec.next_due_date.isoformat(), 'is_active': rec.is_active, 'url': '/recurring' }) # Search tags # Security: Filtered by user_id tags = Tag.query.filter( Tag.user_id == current_user.id, Tag.name.ilike(f'%{query}%') ).limit(limit).all() for tag in tags: results['tags'].append({ 'id': tag.id, 'type': 'tag', 'name': tag.name, 'color': tag.color, 'icon': tag.icon, 'use_count': tag.use_count, 'is_auto': tag.is_auto }) # Calculate total results total_results = sum([ len(results['features']), len(results['expenses']), len(results['documents']), len(results['categories']), len(results['recurring']), len(results['tags']) ]) return jsonify({ 'success': True, 'query': query, 'total_results': total_results, 'results': results })