bmc_hub/app/contacts/backend/router.py

309 lines
11 KiB
Python
Raw Normal View History

"""
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))