"""Contacts: people directory for CRM.""" from fastapi import APIRouter, Request, Form, Depends from fastapi.templating import Jinja2Templates from fastapi.responses import RedirectResponse from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import text from typing import Optional from core.database import get_db from core.base_repository import BaseRepository from core.sidebar import get_sidebar_data router = APIRouter(prefix="/contacts", tags=["contacts"]) templates = Jinja2Templates(directory="templates") @router.get("/") async def list_contacts(request: Request, db: AsyncSession = Depends(get_db)): sidebar = await get_sidebar_data(db) result = await db.execute(text(""" SELECT * FROM contacts WHERE is_deleted = false ORDER BY sort_order, first_name, last_name """)) items = [dict(r._mapping) for r in result] return templates.TemplateResponse("contacts.html", { "request": request, "sidebar": sidebar, "items": items, "page_title": "Contacts", "active_nav": "contacts", }) @router.get("/create") async def create_form(request: Request, db: AsyncSession = Depends(get_db)): sidebar = await get_sidebar_data(db) return templates.TemplateResponse("contact_form.html", { "request": request, "sidebar": sidebar, "page_title": "New Contact", "active_nav": "contacts", "item": None, }) @router.post("/create") async def create_contact( request: Request, first_name: str = Form(...), last_name: Optional[str] = Form(None), company: Optional[str] = Form(None), role: Optional[str] = Form(None), email: Optional[str] = Form(None), phone: Optional[str] = Form(None), notes: Optional[str] = Form(None), tags: Optional[str] = Form(None), db: AsyncSession = Depends(get_db), ): repo = BaseRepository("contacts", db) data = { "first_name": first_name, "last_name": last_name, "company": company, "role": role, "email": email, "phone": phone, "notes": notes, } if tags and tags.strip(): data["tags"] = [t.strip() for t in tags.split(",") if t.strip()] await repo.create(data) return RedirectResponse(url="/contacts", status_code=303) @router.get("/{contact_id}") async def contact_detail(contact_id: str, request: Request, db: AsyncSession = Depends(get_db)): repo = BaseRepository("contacts", db) sidebar = await get_sidebar_data(db) item = await repo.get(contact_id) if not item: return RedirectResponse(url="/contacts", status_code=303) # Linked links result = await db.execute(text(""" SELECT l.*, cl.role, cl.created_at as linked_at FROM links l JOIN contact_links cl ON cl.link_id = l.id WHERE cl.contact_id = :cid AND l.is_deleted = false ORDER BY l.label """), {"cid": contact_id}) links = [dict(r._mapping) for r in result] # All links for add dropdown result = await db.execute(text(""" SELECT id, label, url FROM links WHERE is_deleted = false ORDER BY label """)) all_links = [dict(r._mapping) for r in result] return templates.TemplateResponse("contact_detail.html", { "request": request, "sidebar": sidebar, "item": item, "links": links, "all_links": all_links, "page_title": f"{item['first_name']} {item.get('last_name', '')}".strip(), "active_nav": "contacts", }) @router.get("/{contact_id}/edit") async def edit_form(contact_id: str, request: Request, db: AsyncSession = Depends(get_db)): repo = BaseRepository("contacts", db) sidebar = await get_sidebar_data(db) item = await repo.get(contact_id) if not item: return RedirectResponse(url="/contacts", status_code=303) return templates.TemplateResponse("contact_form.html", { "request": request, "sidebar": sidebar, "page_title": "Edit Contact", "active_nav": "contacts", "item": item, }) @router.post("/{contact_id}/edit") async def update_contact( contact_id: str, first_name: str = Form(...), last_name: Optional[str] = Form(None), company: Optional[str] = Form(None), role: Optional[str] = Form(None), email: Optional[str] = Form(None), phone: Optional[str] = Form(None), notes: Optional[str] = Form(None), tags: Optional[str] = Form(None), db: AsyncSession = Depends(get_db), ): repo = BaseRepository("contacts", db) data = { "first_name": first_name, "last_name": last_name, "company": company, "role": role, "email": email, "phone": phone, "notes": notes, } if tags and tags.strip(): data["tags"] = [t.strip() for t in tags.split(",") if t.strip()] else: data["tags"] = None await repo.update(contact_id, data) return RedirectResponse(url=f"/contacts/{contact_id}", status_code=303) @router.post("/{contact_id}/delete") async def delete_contact(contact_id: str, db: AsyncSession = Depends(get_db)): repo = BaseRepository("contacts", db) await repo.soft_delete(contact_id) return RedirectResponse(url="/contacts", status_code=303) # ---- Link linking ---- @router.post("/{contact_id}/links/add") async def add_link( contact_id: str, link_id: str = Form(...), role: Optional[str] = Form(None), db: AsyncSession = Depends(get_db), ): await db.execute(text(""" INSERT INTO contact_links (contact_id, link_id, role) VALUES (:cid, :lid, :role) ON CONFLICT DO NOTHING """), {"cid": contact_id, "lid": link_id, "role": role if role and role.strip() else None}) return RedirectResponse(url=f"/contacts/{contact_id}", status_code=303) @router.post("/{contact_id}/links/{link_id}/remove") async def remove_link( contact_id: str, link_id: str, db: AsyncSession = Depends(get_db), ): await db.execute(text( "DELETE FROM contact_links WHERE contact_id = :cid AND link_id = :lid" ), {"cid": contact_id, "lid": link_id}) return RedirectResponse(url=f"/contacts/{contact_id}", status_code=303) @router.post("/reorder") async def reorder_contact( request: Request, item_id: str = Form(...), direction: str = Form(...), db: AsyncSession = Depends(get_db), ): repo = BaseRepository("contacts", db) await repo.move_in_order(item_id, direction) return RedirectResponse(url=request.headers.get("referer", "/contacts"), status_code=303)