import logging from typing import List, Optional from fastapi import APIRouter, HTTPException, Query, UploadFile, File from app.core.database import execute_query from datetime import datetime, date import os import uuid logger = logging.getLogger(__name__) router = APIRouter() # ============================================================================ # CRUD Endpoints for Hardware Assets # ============================================================================ @router.get("/hardware", response_model=List[dict]) async def list_hardware( customer_id: Optional[int] = None, status: Optional[str] = None, asset_type: Optional[str] = None, q: Optional[str] = None ): """List all hardware with optional filters.""" query = "SELECT * FROM hardware_assets WHERE deleted_at IS NULL" params = [] if customer_id: query += " AND current_owner_customer_id = %s" params.append(customer_id) if status: query += " AND status = %s" params.append(status) if asset_type: query += " AND asset_type = %s" params.append(asset_type) if q: query += " AND (serial_number ILIKE %s OR model ILIKE %s OR brand ILIKE %s)" search_param = f"%{q}%" params.extend([search_param, search_param, search_param]) query += " ORDER BY created_at DESC" result = execute_query(query, tuple(params)) logger.info(f"✅ Listed {len(result) if result else 0} hardware assets") return result or [] @router.post("/hardware", response_model=dict) async def create_hardware(data: dict): """Create a new hardware asset.""" try: query = """ INSERT INTO hardware_assets ( asset_type, brand, model, serial_number, customer_asset_id, internal_asset_id, notes, current_owner_type, current_owner_customer_id, status, status_reason, warranty_until, end_of_life ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING * """ params = ( data.get("asset_type"), data.get("brand"), data.get("model"), data.get("serial_number"), data.get("customer_asset_id"), data.get("internal_asset_id"), data.get("notes"), data.get("current_owner_type", "bmc"), data.get("current_owner_customer_id"), data.get("status", "active"), data.get("status_reason"), data.get("warranty_until"), data.get("end_of_life"), ) result = execute_query(query, params) if not result: raise HTTPException(status_code=500, detail="Failed to create hardware") hardware = result[0] logger.info(f"✅ Created hardware asset: {hardware['id']} - {hardware['brand']} {hardware['model']}") # Create initial ownership record if owner specified if data.get("current_owner_type"): ownership_query = """ INSERT INTO hardware_ownership_history ( hardware_id, owner_type, owner_customer_id, start_date, notes ) VALUES (%s, %s, %s, %s, %s) """ ownership_params = ( hardware['id'], data.get("current_owner_type"), data.get("current_owner_customer_id"), date.today(), "Initial ownership record" ) execute_query(ownership_query, ownership_params) return hardware except Exception as e: logger.error(f"❌ Failed to create hardware: {str(e)}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/hardware/{hardware_id}", response_model=dict) async def get_hardware(hardware_id: int): """Get hardware details by ID.""" query = "SELECT * FROM hardware_assets WHERE id = %s AND deleted_at IS NULL" result = execute_query(query, (hardware_id,)) if not result: raise HTTPException(status_code=404, detail="Hardware not found") logger.info(f"✅ Retrieved hardware: {hardware_id}") return result[0] @router.patch("/hardware/{hardware_id}", response_model=dict) async def update_hardware(hardware_id: int, data: dict): """Update hardware asset.""" try: # Build dynamic update query update_fields = [] params = [] allowed_fields = [ "asset_type", "brand", "model", "serial_number", "customer_asset_id", "internal_asset_id", "notes", "current_owner_type", "current_owner_customer_id", "status", "status_reason", "warranty_until", "end_of_life", "follow_up_date", "follow_up_owner_user_id" ] for field in allowed_fields: if field in data: update_fields.append(f"{field} = %s") params.append(data[field]) if not update_fields: raise HTTPException(status_code=400, detail="No valid fields to update") update_fields.append("updated_at = NOW()") params.append(hardware_id) query = f""" UPDATE hardware_assets SET {', '.join(update_fields)} WHERE id = %s AND deleted_at IS NULL RETURNING * """ result = execute_query(query, tuple(params)) if not result: raise HTTPException(status_code=404, detail="Hardware not found") logger.info(f"✅ Updated hardware: {hardware_id}") return result[0] except HTTPException: raise except Exception as e: logger.error(f"❌ Failed to update hardware {hardware_id}: {str(e)}") raise HTTPException(status_code=500, detail=str(e)) @router.delete("/hardware/{hardware_id}") async def delete_hardware(hardware_id: int): """Soft-delete hardware asset.""" query = """ UPDATE hardware_assets SET deleted_at = NOW() WHERE id = %s AND deleted_at IS NULL RETURNING id """ result = execute_query(query, (hardware_id,)) if not result: raise HTTPException(status_code=404, detail="Hardware not found") logger.info(f"✅ Deleted hardware: {hardware_id}") return {"message": "Hardware deleted successfully"} # ============================================================================ # Ownership History Endpoints # ============================================================================ @router.get("/hardware/{hardware_id}/ownership", response_model=List[dict]) async def get_ownership_history(hardware_id: int): """Get ownership history for hardware.""" query = """ SELECT * FROM hardware_ownership_history WHERE hardware_id = %s AND deleted_at IS NULL ORDER BY start_date DESC """ result = execute_query(query, (hardware_id,)) logger.info(f"✅ Retrieved ownership history for hardware: {hardware_id}") return result or [] @router.post("/hardware/{hardware_id}/ownership", response_model=dict) async def add_ownership_record(hardware_id: int, data: dict): """Add ownership record (auto-closes previous active ownership).""" try: # Close any active ownership records close_query = """ UPDATE hardware_ownership_history SET end_date = %s WHERE hardware_id = %s AND end_date IS NULL AND deleted_at IS NULL """ execute_query(close_query, (date.today(), hardware_id)) # Create new ownership record insert_query = """ INSERT INTO hardware_ownership_history ( hardware_id, owner_type, owner_customer_id, start_date, notes ) VALUES (%s, %s, %s, %s, %s) RETURNING * """ params = ( hardware_id, data.get("owner_type"), data.get("owner_customer_id"), data.get("start_date", date.today()), data.get("notes") ) result = execute_query(insert_query, params) # Update current owner in hardware_assets update_query = """ UPDATE hardware_assets SET current_owner_type = %s, current_owner_customer_id = %s, updated_at = NOW() WHERE id = %s """ execute_query(update_query, (data.get("owner_type"), data.get("owner_customer_id"), hardware_id)) logger.info(f"✅ Added ownership record for hardware: {hardware_id}") return result[0] except Exception as e: logger.error(f"❌ Failed to add ownership record: {str(e)}") raise HTTPException(status_code=500, detail=str(e)) # ============================================================================ # Location History Endpoints # ============================================================================ @router.get("/hardware/{hardware_id}/locations", response_model=List[dict]) async def get_location_history(hardware_id: int): """Get location history for hardware.""" query = """ SELECT * FROM hardware_location_history WHERE hardware_id = %s AND deleted_at IS NULL ORDER BY start_date DESC """ result = execute_query(query, (hardware_id,)) logger.info(f"✅ Retrieved location history for hardware: {hardware_id}") return result or [] @router.post("/hardware/{hardware_id}/locations", response_model=dict) async def add_location_record(hardware_id: int, data: dict): """Add location record (auto-closes previous active location).""" try: # Close any active location records close_query = """ UPDATE hardware_location_history SET end_date = %s WHERE hardware_id = %s AND end_date IS NULL AND deleted_at IS NULL """ execute_query(close_query, (date.today(), hardware_id)) # Create new location record insert_query = """ INSERT INTO hardware_location_history ( hardware_id, location_id, location_name, start_date, notes ) VALUES (%s, %s, %s, %s, %s) RETURNING * """ params = ( hardware_id, data.get("location_id"), data.get("location_name"), data.get("start_date", date.today()), data.get("notes") ) result = execute_query(insert_query, params) # Update current location in hardware_assets update_query = """ UPDATE hardware_assets SET current_location_id = %s, updated_at = NOW() WHERE id = %s """ execute_query(update_query, (data.get("location_id"), hardware_id)) logger.info(f"✅ Added location record for hardware: {hardware_id}") return result[0] except Exception as e: logger.error(f"❌ Failed to add location record: {str(e)}") raise HTTPException(status_code=500, detail=str(e)) # ============================================================================ # Attachment Endpoints # ============================================================================ @router.get("/hardware/{hardware_id}/attachments", response_model=List[dict]) async def get_attachments(hardware_id: int): """Get all attachments for hardware.""" query = """ SELECT * FROM hardware_attachments WHERE hardware_id = %s AND deleted_at IS NULL ORDER BY uploaded_at DESC """ result = execute_query(query, (hardware_id,)) logger.info(f"✅ Retrieved {len(result) if result else 0} attachments for hardware: {hardware_id}") return result or [] @router.post("/hardware/{hardware_id}/attachments", response_model=dict) async def upload_attachment(hardware_id: int, data: dict): """Upload attachment for hardware.""" try: # Generate storage reference (in production, this would upload to cloud storage) storage_ref = f"hardware/{hardware_id}/{uuid.uuid4()}_{data.get('file_name')}" query = """ INSERT INTO hardware_attachments ( hardware_id, file_type, file_name, storage_ref, file_size_bytes, mime_type, description, uploaded_by_user_id ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s) RETURNING * """ params = ( hardware_id, data.get("file_type", "other"), data.get("file_name"), storage_ref, data.get("file_size_bytes"), data.get("mime_type"), data.get("description"), data.get("uploaded_by_user_id") ) result = execute_query(query, params) logger.info(f"✅ Uploaded attachment for hardware: {hardware_id}") return result[0] except Exception as e: logger.error(f"❌ Failed to upload attachment: {str(e)}") raise HTTPException(status_code=500, detail=str(e)) @router.delete("/hardware/{hardware_id}/attachments/{attachment_id}") async def delete_attachment(hardware_id: int, attachment_id: int): """Soft-delete attachment.""" query = """ UPDATE hardware_attachments SET deleted_at = NOW() WHERE id = %s AND hardware_id = %s AND deleted_at IS NULL RETURNING id """ result = execute_query(query, (attachment_id, hardware_id)) if not result: raise HTTPException(status_code=404, detail="Attachment not found") logger.info(f"✅ Deleted attachment {attachment_id} for hardware: {hardware_id}") return {"message": "Attachment deleted successfully"} # ============================================================================ # Case Relations Endpoints # ============================================================================ @router.get("/hardware/{hardware_id}/cases", response_model=List[dict]) async def get_related_cases(hardware_id: int): """Get all cases related to this hardware.""" query = """ SELECT hcr.*, s.titel, s.status, s.customer_id FROM hardware_case_relations hcr LEFT JOIN sag_sager s ON hcr.case_id = s.id WHERE hcr.hardware_id = %s AND hcr.deleted_at IS NULL AND s.deleted_at IS NULL ORDER BY hcr.created_at DESC """ result = execute_query(query, (hardware_id,)) logger.info(f"✅ Retrieved {len(result) if result else 0} related cases for hardware: {hardware_id}") return result or [] @router.post("/hardware/{hardware_id}/cases", response_model=dict) async def link_case(hardware_id: int, data: dict): """Link hardware to a case.""" try: query = """ INSERT INTO hardware_case_relations ( hardware_id, case_id, relation_type ) VALUES (%s, %s, %s) RETURNING * """ params = ( hardware_id, data.get("case_id"), data.get("relation_type", "related") ) result = execute_query(query, params) logger.info(f"✅ Linked hardware {hardware_id} to case {data.get('case_id')}") return result[0] except Exception as e: logger.error(f"❌ Failed to link case: {str(e)}") raise HTTPException(status_code=500, detail=str(e)) @router.delete("/hardware/{hardware_id}/cases/{case_id}") async def unlink_case(hardware_id: int, case_id: int): """Unlink hardware from a case.""" query = """ UPDATE hardware_case_relations SET deleted_at = NOW() WHERE hardware_id = %s AND case_id = %s AND deleted_at IS NULL RETURNING id """ result = execute_query(query, (hardware_id, case_id)) if not result: raise HTTPException(status_code=404, detail="Case relation not found") logger.info(f"✅ Unlinked hardware {hardware_id} from case {case_id}") return {"message": "Case unlinked successfully"} # ============================================================================ # Tag Endpoints # ============================================================================ @router.get("/hardware/{hardware_id}/tags", response_model=List[dict]) async def get_tags(hardware_id: int): """Get all tags for hardware.""" query = """ SELECT * FROM hardware_tags WHERE hardware_id = %s AND deleted_at IS NULL ORDER BY created_at DESC """ result = execute_query(query, (hardware_id,)) logger.info(f"✅ Retrieved {len(result) if result else 0} tags for hardware: {hardware_id}") return result or [] @router.post("/hardware/{hardware_id}/tags", response_model=dict) async def add_tag(hardware_id: int, data: dict): """Add tag to hardware.""" try: query = """ INSERT INTO hardware_tags ( hardware_id, tag_name, tag_type ) VALUES (%s, %s, %s) RETURNING * """ params = ( hardware_id, data.get("tag_name"), data.get("tag_type", "manual") ) result = execute_query(query, params) logger.info(f"✅ Added tag '{data.get('tag_name')}' to hardware: {hardware_id}") return result[0] except Exception as e: logger.error(f"❌ Failed to add tag: {str(e)}") raise HTTPException(status_code=500, detail=str(e)) @router.delete("/hardware/{hardware_id}/tags/{tag_id}") async def delete_tag(hardware_id: int, tag_id: int): """Delete tag from hardware.""" query = """ UPDATE hardware_tags SET deleted_at = NOW() WHERE id = %s AND hardware_id = %s AND deleted_at IS NULL RETURNING id """ result = execute_query(query, (tag_id, hardware_id)) if not result: raise HTTPException(status_code=404, detail="Tag not found") logger.info(f"✅ Deleted tag {tag_id} from hardware: {hardware_id}") return {"message": "Tag deleted successfully"} # ============================================================================ # Search Endpoint # ============================================================================ @router.get("/search/hardware", response_model=List[dict]) async def search_hardware(q: str = Query(..., min_length=1)): """Search hardware by serial number, model, or brand.""" query = """ SELECT * FROM hardware_assets WHERE deleted_at IS NULL AND ( serial_number ILIKE %s OR model ILIKE %s OR brand ILIKE %s OR customer_asset_id ILIKE %s OR internal_asset_id ILIKE %s ) ORDER BY created_at DESC LIMIT 50 """ search_param = f"%{q}%" result = execute_query(query, (search_param, search_param, search_param, search_param, search_param)) logger.info(f"✅ Search for '{q}' returned {len(result) if result else 0} results") return result or []