from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, or_ from datetime import datetime, timezone from typing import Optional, List from app.database import get_db from app.models.person import Person from app.schemas.person import PersonCreate, PersonUpdate, PersonResponse from app.routers.auth import get_current_user from app.models.user import User router = APIRouter() def _compute_display_name( first_name: Optional[str], last_name: Optional[str], nickname: Optional[str], name: Optional[str], ) -> str: """Denormalise a display name. Nickname wins; else 'First Last'; else legacy name; else empty.""" if nickname: return nickname full = ((first_name or '') + ' ' + (last_name or '')).strip() if full: return full # Don't fall back to stale `name` if all fields were explicitly cleared return (name or '').strip() if name else '' @router.get("/", response_model=List[PersonResponse]) async def get_people( search: Optional[str] = Query(None), category: Optional[str] = Query(None), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """Get all people with optional search and category filter.""" query = select(Person) if search: term = f"%{search}%" query = query.where( or_( Person.name.ilike(term), Person.first_name.ilike(term), Person.last_name.ilike(term), Person.nickname.ilike(term), Person.email.ilike(term), Person.company.ilike(term), ) ) if category: query = query.where(Person.category == category) query = query.order_by(Person.name.asc()) result = await db.execute(query) people = result.scalars().all() return people @router.post("/", response_model=PersonResponse, status_code=201) async def create_person( person: PersonCreate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """Create a new person with denormalised display name.""" data = person.model_dump() # Auto-split legacy name into first/last if only name is provided if data.get('name') and not data.get('first_name') and not data.get('last_name') and not data.get('nickname'): parts = data['name'].split(' ', 1) data['first_name'] = parts[0] data['last_name'] = parts[1] if len(parts) > 1 else None new_person = Person(**data) new_person.name = _compute_display_name( new_person.first_name, new_person.last_name, new_person.nickname, new_person.name, ) db.add(new_person) await db.commit() await db.refresh(new_person) return new_person @router.get("/{person_id}", response_model=PersonResponse) async def get_person( person_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """Get a specific person by ID.""" result = await db.execute(select(Person).where(Person.id == person_id)) person = result.scalar_one_or_none() if not person: raise HTTPException(status_code=404, detail="Person not found") return person @router.put("/{person_id}", response_model=PersonResponse) async def update_person( person_id: int, person_update: PersonUpdate, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """Update a person and refresh the denormalised display name.""" result = await db.execute(select(Person).where(Person.id == person_id)) person = result.scalar_one_or_none() if not person: raise HTTPException(status_code=404, detail="Person not found") update_data = person_update.model_dump(exclude_unset=True) for key, value in update_data.items(): setattr(person, key, value) # Recompute display name after applying updates person.name = _compute_display_name( person.first_name, person.last_name, person.nickname, person.name, ) # Guarantee timestamp refresh regardless of DB driver behaviour person.updated_at = datetime.now(timezone.utc).replace(tzinfo=None) await db.commit() await db.refresh(person) return person @router.delete("/{person_id}", status_code=204) async def delete_person( person_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user) ): """Delete a person.""" result = await db.execute(select(Person).where(Person.id == person_id)) person = result.scalar_one_or_none() if not person: raise HTTPException(status_code=404, detail="Person not found") await db.delete(person) await db.commit() return None