""" Messenger API - Contact Routes. CRUD, CSV import/export for contacts. """ import csv import uuid from io import StringIO from datetime import datetime from typing import List, Optional from fastapi import APIRouter, HTTPException, UploadFile, File, Query from fastapi.responses import StreamingResponse from messenger_models import ( Contact, ContactCreate, ContactUpdate, CSVImportResult, ) from messenger_helpers import get_contacts, save_contacts router = APIRouter(tags=["Messenger"]) # ========================================== # CONTACTS ENDPOINTS # ========================================== @router.get("/contacts", response_model=List[Contact]) async def list_contacts( role: Optional[str] = Query(None, description="Filter by role"), class_name: Optional[str] = Query(None, description="Filter by class"), search: Optional[str] = Query(None, description="Search in name/email") ): """Listet alle Kontakte auf.""" contacts = get_contacts() # Filter anwenden if role: contacts = [c for c in contacts if c.get("role") == role] if class_name: contacts = [c for c in contacts if c.get("class_name") == class_name] if search: search_lower = search.lower() contacts = [c for c in contacts if search_lower in c.get("name", "").lower() or search_lower in (c.get("email") or "").lower() or search_lower in (c.get("student_name") or "").lower()] return contacts @router.post("/contacts", response_model=Contact) async def create_contact(contact: ContactCreate): """Erstellt einen neuen Kontakt.""" contacts = get_contacts() # Pruefen ob Email bereits existiert if contact.email: existing = [c for c in contacts if c.get("email") == contact.email] if existing: raise HTTPException(status_code=400, detail="Kontakt mit dieser Email existiert bereits") now = datetime.utcnow().isoformat() new_contact = { "id": str(uuid.uuid4()), "created_at": now, "updated_at": now, "online": False, "last_seen": None, **contact.dict() } contacts.append(new_contact) save_contacts(contacts) return new_contact @router.get("/contacts/{contact_id}", response_model=Contact) async def get_contact(contact_id: str): """Ruft einen einzelnen Kontakt ab.""" contacts = get_contacts() contact = next((c for c in contacts if c["id"] == contact_id), None) if not contact: raise HTTPException(status_code=404, detail="Kontakt nicht gefunden") return contact @router.put("/contacts/{contact_id}", response_model=Contact) async def update_contact(contact_id: str, update: ContactUpdate): """Aktualisiert einen Kontakt.""" contacts = get_contacts() contact_idx = next((i for i, c in enumerate(contacts) if c["id"] == contact_id), None) if contact_idx is None: raise HTTPException(status_code=404, detail="Kontakt nicht gefunden") update_data = update.dict(exclude_unset=True) contacts[contact_idx].update(update_data) contacts[contact_idx]["updated_at"] = datetime.utcnow().isoformat() save_contacts(contacts) return contacts[contact_idx] @router.delete("/contacts/{contact_id}") async def delete_contact(contact_id: str): """Loescht einen Kontakt.""" contacts = get_contacts() contacts = [c for c in contacts if c["id"] != contact_id] save_contacts(contacts) return {"status": "deleted", "id": contact_id} @router.post("/contacts/import", response_model=CSVImportResult) async def import_contacts_csv(file: UploadFile = File(...)): """ Importiert Kontakte aus einer CSV-Datei. Erwartete Spalten: - name (required) - email - phone - role (parent/teacher/staff/student) - student_name - class_name - notes - tags (komma-separiert) """ if not file.filename.endswith('.csv'): raise HTTPException(status_code=400, detail="Nur CSV-Dateien werden unterstuetzt") content = await file.read() try: text = content.decode('utf-8') except UnicodeDecodeError: text = content.decode('latin-1') contacts = get_contacts() existing_emails = {c.get("email") for c in contacts if c.get("email")} imported = [] skipped = 0 errors = [] reader = csv.DictReader(StringIO(text), delimiter=';') # Deutsche CSV meist mit Semikolon if not reader.fieldnames or 'name' not in [f.lower() for f in reader.fieldnames]: # Versuche mit Komma reader = csv.DictReader(StringIO(text), delimiter=',') for row_num, row in enumerate(reader, start=2): try: # Normalisiere Spaltennamen row = {k.lower().strip(): v.strip() if v else "" for k, v in row.items()} name = row.get('name') or row.get('kontakt') or row.get('elternname') if not name: errors.append(f"Zeile {row_num}: Name fehlt") skipped += 1 continue email = row.get('email') or row.get('e-mail') or row.get('mail') if email and email in existing_emails: errors.append(f"Zeile {row_num}: Email {email} existiert bereits") skipped += 1 continue now = datetime.utcnow().isoformat() tags_str = row.get('tags') or row.get('kategorien') or "" tags = [t.strip() for t in tags_str.split(',') if t.strip()] # Matrix-ID und preferred_channel auslesen matrix_id = row.get('matrix_id') or row.get('matrix') or None preferred_channel = row.get('preferred_channel') or row.get('kanal') or "email" if preferred_channel not in ["email", "matrix", "pwa"]: preferred_channel = "email" new_contact = { "id": str(uuid.uuid4()), "name": name, "email": email if email else None, "phone": row.get('phone') or row.get('telefon') or row.get('tel'), "role": row.get('role') or row.get('rolle') or "parent", "student_name": row.get('student_name') or row.get('schueler') or row.get('kind'), "class_name": row.get('class_name') or row.get('klasse'), "notes": row.get('notes') or row.get('notizen') or row.get('bemerkungen'), "tags": tags, "matrix_id": matrix_id if matrix_id else None, "preferred_channel": preferred_channel, "created_at": now, "updated_at": now, "online": False, "last_seen": None } contacts.append(new_contact) imported.append(new_contact) if email: existing_emails.add(email) except Exception as e: errors.append(f"Zeile {row_num}: {str(e)}") skipped += 1 save_contacts(contacts) return CSVImportResult( imported=len(imported), skipped=skipped, errors=errors[:20], # Maximal 20 Fehler zurueckgeben contacts=imported ) @router.get("/contacts/export/csv") async def export_contacts_csv(): """Exportiert alle Kontakte als CSV.""" contacts = get_contacts() output = StringIO() fieldnames = ['name', 'email', 'phone', 'role', 'student_name', 'class_name', 'notes', 'tags', 'matrix_id', 'preferred_channel'] writer = csv.DictWriter(output, fieldnames=fieldnames, delimiter=';') writer.writeheader() for contact in contacts: writer.writerow({ 'name': contact.get('name', ''), 'email': contact.get('email', ''), 'phone': contact.get('phone', ''), 'role': contact.get('role', ''), 'student_name': contact.get('student_name', ''), 'class_name': contact.get('class_name', ''), 'notes': contact.get('notes', ''), 'tags': ','.join(contact.get('tags', [])), 'matrix_id': contact.get('matrix_id', ''), 'preferred_channel': contact.get('preferred_channel', 'email') }) output.seek(0) return StreamingResponse( iter([output.getvalue()]), media_type="text/csv", headers={"Content-Disposition": "attachment; filename=kontakte.csv"} )