"""Global search API endpoint.""" from flask import Blueprint, request from flask_jwt_extended import jwt_required from shopdb.extensions import db from shopdb.core.models import ( Application, KnowledgeBase, Asset, AssetType ) from shopdb.utils.responses import success_response search_bp = Blueprint('search', __name__) @search_bp.route('', methods=['GET']) @jwt_required(optional=True) def global_search(): """ Global search across multiple entity types. Returns combined results from: - Machines (equipment and PCs) - Applications - Knowledge Base articles - Printers (if available) Results are sorted by relevance score. """ query = request.args.get('q', '').strip() if not query or len(query) < 2: return success_response({ 'results': [], 'query': query, 'message': 'Search query must be at least 2 characters' }) if len(query) > 200: return success_response({ 'results': [], 'query': query[:200], 'message': 'Search query too long' }) results = [] search_term = f'%{query}%' # NOTE: Legacy Machine search is disabled - all data is now in the Asset table # The Asset search below handles equipment, computers, network devices, and printers # with proper plugin-specific IDs for correct routing # Search Applications try: apps = Application.query.filter( Application.isactive == True, db.or_( Application.appname.ilike(search_term), Application.appdescription.ilike(search_term) ) ).limit(10).all() for app in apps: relevance = 20 if query.lower() == app.appname.lower(): relevance = 100 elif query.lower() in app.appname.lower(): relevance = 50 results.append({ 'type': 'application', 'id': app.appid, 'title': app.appname, 'subtitle': app.appdescription[:100] if app.appdescription else None, 'url': f"/applications/{app.appid}", 'relevance': relevance }) except Exception as e: import logging logging.error(f"Application search failed: {e}") # Search Knowledge Base try: kb_articles = KnowledgeBase.query.filter( KnowledgeBase.isactive == True, db.or_( KnowledgeBase.shortdescription.ilike(search_term), KnowledgeBase.keywords.ilike(search_term) ) ).limit(20).all() for kb in kb_articles: # Weight by clicks and keyword match relevance = 10 + (kb.clicks or 0) * 0.1 if kb.keywords and query.lower() in kb.keywords.lower(): relevance += 15 results.append({ 'type': 'knowledgebase', 'id': kb.linkid, 'title': kb.shortdescription, 'subtitle': kb.application.appname if kb.application else None, 'url': f"/knowledgebase/{kb.linkid}", 'linkurl': kb.linkurl, 'relevance': relevance }) except Exception as e: import logging logging.error(f"KnowledgeBase search failed: {e}") # NOTE: Legacy Printer search removed - printers are now in the unified Asset table # The Asset search below handles printers with correct plugin-specific IDs # Search Employees (separate database) try: import pymysql emp_conn = pymysql.connect( host='localhost', user='root', password='rootpassword', database='wjf_employees', cursorclass=pymysql.cursors.DictCursor ) with emp_conn.cursor() as cur: cur.execute(''' SELECT SSO, First_Name, Last_Name, Team, Role FROM employees WHERE First_Name LIKE %s OR Last_Name LIKE %s OR CAST(SSO AS CHAR) LIKE %s ORDER BY Last_Name, First_Name LIMIT 10 ''', (search_term, search_term, search_term)) employees = cur.fetchall() emp_conn.close() for emp in employees: full_name = f"{emp['First_Name'].strip()} {emp['Last_Name'].strip()}" sso_str = str(emp['SSO']) # Calculate relevance relevance = 20 if query == sso_str: relevance = 100 elif query.lower() == full_name.lower(): relevance = 95 elif query.lower() in full_name.lower(): relevance = 60 results.append({ 'type': 'employee', 'id': emp['SSO'], 'title': full_name, 'subtitle': emp.get('Team') or emp.get('Role') or f"SSO: {sso_str}", 'url': f"/employees/{emp['SSO']}", 'relevance': relevance }) except Exception as e: import logging logging.error(f"Employee search failed: {e}") # Search unified Assets table try: assets = Asset.query.join(AssetType).filter( Asset.isactive == True, db.or_( Asset.assetnumber.ilike(search_term), Asset.name.ilike(search_term), Asset.serialnumber.ilike(search_term), Asset.notes.ilike(search_term) ) ).limit(10).all() for asset in assets: # Calculate relevance relevance = 15 if asset.assetnumber and query.lower() == asset.assetnumber.lower(): relevance = 100 elif asset.name and query.lower() == asset.name.lower(): relevance = 90 elif asset.serialnumber and query.lower() == asset.serialnumber.lower(): relevance = 85 elif asset.name and query.lower() in asset.name.lower(): relevance = 50 # Determine URL and type based on asset type asset_type_name = asset.assettype.assettype if asset.assettype else 'asset' # Get the plugin-specific ID for proper routing plugin_id = asset.assetid # fallback if asset_type_name == 'equipment' and hasattr(asset, 'equipment') and asset.equipment: plugin_id = asset.equipment.equipmentid elif asset_type_name == 'computer' and hasattr(asset, 'computer') and asset.computer: plugin_id = asset.computer.computerid elif asset_type_name == 'network_device' and hasattr(asset, 'network_device') and asset.network_device: plugin_id = asset.network_device.networkdeviceid elif asset_type_name == 'printer' and hasattr(asset, 'printer') and asset.printer: plugin_id = asset.printer.printerid url_map = { 'equipment': f"/machines/{plugin_id}", 'computer': f"/pcs/{plugin_id}", 'network_device': f"/network/{plugin_id}", 'printer': f"/printers/{plugin_id}", } url = url_map.get(asset_type_name, f"/assets/{asset.assetid}") display_name = asset.display_name subtitle = None if asset.name and asset.assetnumber != asset.name: subtitle = asset.assetnumber # Get location name location_name = asset.location.locationname if asset.location else None results.append({ 'type': asset_type_name, 'id': plugin_id, 'title': display_name, 'subtitle': subtitle, 'location': location_name, 'url': url, 'relevance': relevance }) except Exception as e: import logging logging.error(f"Asset search failed: {e}") # Sort by relevance (highest first) results.sort(key=lambda x: x['relevance'], reverse=True) # Remove duplicates (prefer higher relevance) seen_ids = {} unique_results = [] for r in results: key = (r['type'], r['id']) if key not in seen_ids: seen_ids[key] = True unique_results.append(r) # Limit total results unique_results = unique_results[:30] return success_response({ 'results': unique_results, 'query': query, 'total': len(unique_results) })