""" Contact API Router Handles contact CRUD operations with multi-company support """ from fastapi import APIRouter, HTTPException, Query from typing import Optional, List from app.core.database import execute_query, execute_insert, execute_update from app.models.schemas import Contact, ContactCreate, ContactUpdate, ContactCompanyLink, CompanyInfo import logging logger = logging.getLogger(__name__) router = APIRouter() @router.get("/contacts", response_model=dict) async def get_contacts( search: Optional[str] = None, customer_id: Optional[int] = None, is_active: Optional[bool] = None, limit: int = Query(default=20, le=100), offset: int = Query(default=0, ge=0) ): """ Get all contacts with optional filtering, search, and pagination """ try: # Build WHERE clauses where_clauses = [] params = [] if search: where_clauses.append("(c.first_name ILIKE %s OR c.last_name ILIKE %s OR c.email ILIKE %s)") params.extend([f"%{search}%", f"%{search}%", f"%{search}%"]) if is_active is not None: where_clauses.append("c.is_active = %s") params.append(is_active) if customer_id is not None: where_clauses.append("EXISTS (SELECT 1 FROM contact_companies cc WHERE cc.contact_id = c.id AND cc.customer_id = %s)") params.append(customer_id) where_sql = "WHERE " + " AND ".join(where_clauses) if where_clauses else "" # Count total count_query = f""" SELECT COUNT(DISTINCT c.id) FROM contacts c {where_sql} """ count_result = execute_query(count_query, tuple(params), fetchone=True) total = count_result['count'] if count_result else 0 # Get contacts with company count query = f""" SELECT c.id, c.first_name, c.last_name, c.email, c.phone, c.mobile, c.title, c.department, c.is_active, c.vtiger_id, c.created_at, c.updated_at, COUNT(DISTINCT cc.customer_id) as company_count, ARRAY_AGG(DISTINCT cu.name ORDER BY cu.name) FILTER (WHERE cu.name IS NOT NULL) as company_names FROM contacts c LEFT JOIN contact_companies cc ON c.id = cc.contact_id LEFT JOIN customers cu ON cc.customer_id = cu.id {where_sql} GROUP BY c.id, c.first_name, c.last_name, c.email, c.phone, c.mobile, c.title, c.department, c.is_active, c.vtiger_id, c.created_at, c.updated_at ORDER BY c.last_name, c.first_name LIMIT %s OFFSET %s """ params.extend([limit, offset]) contacts = execute_query(query, tuple(params)) # Default is fetchall return { "contacts": contacts or [], "total": total, "limit": limit, "offset": offset } except Exception as e: logger.error(f"Failed to get contacts: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.get("/contacts/{contact_id}", response_model=dict) async def get_contact(contact_id: int): """ Get a single contact with all linked companies """ try: # Get contact contact_query = """ SELECT id, first_name, last_name, email, phone, mobile, title, department, is_active, vtiger_id, created_at, updated_at FROM contacts WHERE id = %s """ contact = execute_query(contact_query, (contact_id,), fetchone=True) if not contact: raise HTTPException(status_code=404, detail="Contact not found") # Get linked companies companies_query = """ SELECT cu.id, cu.name, cc.is_primary, cc.role, cc.notes FROM contact_companies cc JOIN customers cu ON cc.customer_id = cu.id WHERE cc.contact_id = %s ORDER BY cc.is_primary DESC, cu.name """ companies = execute_query(companies_query, (contact_id,)) # Default is fetchall contact['companies'] = companies or [] return contact except HTTPException: raise except Exception as e: logger.error(f"Failed to get contact {contact_id}: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.post("/contacts", response_model=dict) async def create_contact(contact: ContactCreate): """ Create a new contact and link to companies """ try: # Insert contact insert_query = """ INSERT INTO contacts (first_name, last_name, email, phone, mobile, title, department, is_active) VALUES (%s, %s, %s, %s, %s, %s, %s, %s) """ contact_id = execute_insert( insert_query, (contact.first_name, contact.last_name, contact.email, contact.phone, contact.mobile, contact.title, contact.department, contact.is_active) ) # Link to companies if contact.company_ids: for idx, customer_id in enumerate(contact.company_ids): is_primary = idx == 0 and contact.is_primary link_query = """ INSERT INTO contact_companies (contact_id, customer_id, is_primary, role, notes) VALUES (%s, %s, %s, %s, %s) """ execute_insert( link_query, (contact_id, customer_id, is_primary, contact.role, contact.notes) ) # Return created contact return await get_contact(contact_id) except Exception as e: logger.error(f"Failed to create contact: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.put("/contacts/{contact_id}", response_model=dict) async def update_contact(contact_id: int, contact: ContactUpdate): """ Update a contact """ try: # Check if contact exists existing = execute_query("SELECT id FROM contacts WHERE id = %s", (contact_id,), fetchone=True) if not existing: raise HTTPException(status_code=404, detail="Contact not found") # Build update query update_fields = [] params = [] if contact.first_name is not None: update_fields.append("first_name = %s") params.append(contact.first_name) if contact.last_name is not None: update_fields.append("last_name = %s") params.append(contact.last_name) if contact.email is not None: update_fields.append("email = %s") params.append(contact.email) if contact.phone is not None: update_fields.append("phone = %s") params.append(contact.phone) if contact.mobile is not None: update_fields.append("mobile = %s") params.append(contact.mobile) if contact.title is not None: update_fields.append("title = %s") params.append(contact.title) if contact.department is not None: update_fields.append("department = %s") params.append(contact.department) if contact.is_active is not None: update_fields.append("is_active = %s") params.append(contact.is_active) if not update_fields: return await get_contact(contact_id) update_query = f""" UPDATE contacts SET {', '.join(update_fields)}, updated_at = CURRENT_TIMESTAMP WHERE id = %s """ params.append(contact_id) execute_update(update_query, tuple(params)) return await get_contact(contact_id) except HTTPException: raise except Exception as e: logger.error(f"Failed to update contact {contact_id}: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.delete("/contacts/{contact_id}") async def delete_contact(contact_id: int): """ Delete a contact (cascade deletes company links) """ try: result = execute_update("DELETE FROM contacts WHERE id = %s", (contact_id,)) if result == 0: raise HTTPException(status_code=404, detail="Contact not found") return {"message": "Contact deleted successfully"} except HTTPException: raise except Exception as e: logger.error(f"Failed to delete contact {contact_id}: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.post("/contacts/{contact_id}/companies") async def link_contact_to_company(contact_id: int, link: ContactCompanyLink): """ Link a contact to a company """ try: # Check if contact exists contact = execute_query("SELECT id FROM contacts WHERE id = %s", (contact_id,), fetchone=True) if not contact: raise HTTPException(status_code=404, detail="Contact not found") # Check if company exists customer = execute_query("SELECT id FROM customers WHERE id = %s", (link.customer_id,), fetchone=True) if not customer: raise HTTPException(status_code=404, detail="Customer not found") # Insert link (ON CONFLICT updates) query = """ INSERT INTO contact_companies (contact_id, customer_id, is_primary, role, notes) VALUES (%s, %s, %s, %s, %s) ON CONFLICT (contact_id, customer_id) DO UPDATE SET is_primary = EXCLUDED.is_primary, role = EXCLUDED.role, notes = EXCLUDED.notes """ execute_insert(query, (contact_id, link.customer_id, link.is_primary, link.role, link.notes)) return {"message": "Contact linked to company successfully"} except HTTPException: raise except Exception as e: logger.error(f"Failed to link contact to company: {e}") raise HTTPException(status_code=500, detail=str(e)) @router.delete("/contacts/{contact_id}/companies/{customer_id}") async def unlink_contact_from_company(contact_id: int, customer_id: int): """ Unlink a contact from a company """ try: result = execute_update( "DELETE FROM contact_companies WHERE contact_id = %s AND customer_id = %s", (contact_id, customer_id) ) if result == 0: raise HTTPException(status_code=404, detail="Link not found") return {"message": "Contact unlinked from company successfully"} except HTTPException: raise except Exception as e: logger.error(f"Failed to unlink contact from company: {e}") raise HTTPException(status_code=500, detail=str(e))