# Contract-Paket fürs iOS-Frontend

Quelle der Wahrheit für Feldnamen/IDs sind die Zod-Schemas in `packages/schemas`. JSON-Keys sind
**Deutsch (CAS-nah)**. IDs kommen aus den Daten – das Frontend erzeugt/verändert **nie** IDs.

## Enums

```
Rolle               = innendienst | aussendienst | vertriebsleitung | support
Delta               = neu | offen | erledigt_seit_letztem_besuch | aktualisiert_seit_letztem_besuch
BearbeiterAufloesung= eindeutig | mehrdeutig | nicht_gefunden | rolle
ReportStatus        = entwurf | freigegeben
NaechsterSchrittTyp = aufgabe | strategisch
Aktion              = status_update | update | anlegen
TranskriptRolle     = agent | aussendienst | kunde
GGUID               = String, Präfix "0x" (nie erfinden)
TempId              = String, Präfix "tmp-"
```

## Stammdaten / Auth

```jsonc
// benutzer.json — userID == gguid (stabiler Join-Key, bleibt bei Supabase konstant)
{
  "benutzer": [
    { "gguid": "0x…", "kuerzel": "…", "anzeigename": "…", "vorname": "…", "nachname": "…",
      "email": "…", "abteilung": "…", "rolle": Rolle, "aktiv": true, "aliase": ["…"] }
  ],
  "abteilungs_defaults": { "innendienst": { "standard_bearbeiter_gguid": "0x…"|null, "hinweis": "…" } }
}

// visits.json — Besuchsindex (Liste + Karte). owner_gguid == benutzer.gguid == userID
[ { "visitId": "…", "owner_gguid": "0x…", "kunde": "…", "datum": "YYYY-MM-DD",
    "geo": { "breitengrad": 53.0793, "laengengrad": 8.8017 }, "prep_ref": "…" } ]
```
Auth-PoC: Dev-Login ohne Bundle-Passwort; `userID = gguid`. Owner-Filter im PoC lokal, später
Supabase-RLS auf `owner_gguid`.

## preparation.json (Screen B-Briefing) — Auszug

```jsonc
{
  "typ": "vorbereitung",
  "besuch": { "besuch_id": "0x…", "geplant_am": "…", "typ": "Besuch",
              "aussendienst": { "gguid": "0x…", "name": "…" } },
  "kunde": { "gguid": "0x…", "firma": "…", "branche": "…",
             "anschrift": { "strasse": "…", "plz": "…", "ort": "…", "land": "DE",
                            "geo": { "breitengrad": 0, "laengengrad": 0 } },
             "kundenbetreuer": "…",
             "ansprechpartner": [ { "gguid": "0x…", "name": "…", "funktion": "…", "telefon": "…", "email": "…" } ] },
  "aufgaben": [ { "gguid": "0x…", "betreff": "…", "status": "…", "prioritaet": "…",
                  "faellig_am": "…"|null, "bearbeiter": "…", "delta": Delta } ],
  "verkaufschancen": [ { "gguid": "0x…", "bezeichnung": "…", "phase": "…",
                         "abschlusswahrscheinlichkeit": 60, "erwartetes_volumen": 85000|null,
                         "waehrung": "EUR", "status": "…", "delta": Delta } ],
  "kundenanfragen": [ { "gguid": "0x…", "betreff": "…", "kanal": "…", "status": "…", "delta": Delta } ],
  "offen_aus_letztem_besuch": [ { "ref_gguid": "0x…", "objekt": "…", "hinweis": "…" } ]
}
```

## transkript.json

```jsonc
{
  "typ": "transkript", "besuch_id": "0x…", "kunde_gguid": "0x…", "basiert_auf": "preparation.json",
  "session": { "modus": "dokumentation", "start": "…", "ende": "…", "sprache": "de",
               "voice_provider": "xai-grok-voice", "voice_modell": "grok-voice-fast-1.0" },
  // voice_modell spiegelt das tatsaechlich genutzte Modell (Default grok-voice-fast-1.0, per Env umschaltbar).
  "turns": [ { "index": 0, "rolle": TranskriptRolle, "zeit": "ISO", "text": "…" } ],
  "verfeinerungen": []
}
```

## report.json (Screen C/D — der Prüf-Screen)

```jsonc
{
  "typ": "besuchsbericht", "besuch_id": "0x…", "kunde_gguid": "0x…",
  "basiert_auf": { "vorbereitung": "preparation.json", "transkript": "transkript.json" },
  "status": ReportStatus, "version": 1, "erstellt_am": "ISO",
  "freigabe": { "durch": String|null, "am": String|null },
  "zusammenfassung": "…",

  "aktualisierte_objekte": {
    "aufgaben":        [ { "gguid": "0x…", "aktion": Aktion, "neuer_status"?: "…", "notiz"?: "…", "faellig_am"?: "YYYY-MM-DD" } ],
    "verkaufschancen": [ { "gguid": "0x…", "aktion": "update", "abschlusswahrscheinlichkeit"?: 80, "phase"?: "…", "notiz"?: "…" } ],
    "kundenanfragen":  [ { "gguid": "0x…", "aktion": "status_update", "notiz"?: "…" } ]
  },
  "neue_objekte": {
    "aufgaben": [ { "temp_id": "tmp-…", "betreff": "…", "verantwortlich": "…",
                    "bearbeiter_typ": Rolle, "bearbeiter_gguid": "0x…"|null,
                    "bearbeiter_name"?: "…", "bearbeiter_aufloesung": BearbeiterAufloesung,
                    "genannt_als"?: "…", "faellig_am"?: "YYYY-MM-DD",
                    "abgeleitet_aus_gespraech": Bool, "quelle_turn"?: 7, "notiz"?: "…" } ],
    "verkaufschancen": [ { "temp_id": "tmp-…", "bezeichnung": "…", "phase": "…",
                           "abschlusswahrscheinlichkeit": 15, "erwartetes_volumen": null,
                           "waehrung": "EUR", "geplanter_abschluss": null,
                           "verknuepft_mit_kunde": "0x…", "notiz"?: "…" } ],
    "kundenanfragen": [ { "temp_id": "tmp-…", "betreff": "…", "abgeleitet_aus_gespraech": Bool } ]
  },
  "naechste_schritte": [
    { "id": "ns-1", "beschreibung": "…", "typ": NaechsterSchrittTyp, "verantwortlich": "…",
      "bearbeiter_typ": Rolle, "bearbeiter_gguid"?: "0x…", "bearbeiter_name"?: "…",
      "faellig_am": "YYYY-MM-DD"|null, "abgeleitet_aus_gespraech": Bool,
      "verknuepft_mit": { "gguid": "0x…" } | { "temp_id": "tmp-…" } | null }
  ],
  "offen_geblieben": [
    { "ref_gguid": "0x…", "objekt": "…", "betreff": "…", "grund": "…", "mitnehmen_zum_naechsten_besuch": Bool }
  ],
  "doku_feedback": [
    { "kategorie": "dokumentation"|"vertrieb", "beobachtung": "…", "tipp": "…", "beispiel"?: "…" }
  ]
}
```

**`doku_feedback`** = Coaching NUR fuer den Aussendienst (kein CRM-Feld, kein Write-back): 0–4 konkrete
Tipps in zwei Kategorien — `dokumentation` (ergebnisorientierter dokumentieren) und `vertrieb`
(Verkaufschance voranbringen: fehlt Budget/Entscheider/Angebot/Termin?). **Leeres Array = alles gut.**
Eigene UI-Karte auf Screen C/D, per `kategorie` gruppier-/labelbar, klar getrennt von den CRM-Objekten.
`beispiel` ist optional (Umformulierung bzw. konkrete Handlungsformulierung).

**Review-Signale (Screen C/D-UI):** `abgeleitet_aus_gespraech: true` = „vom Agent interpretiert"
(AD bestätigt). `bearbeiter_aufloesung != eindeutig` = AD muss Person/Rolle klären → **Freigabe-Gate**.
`temp_id` vs `gguid` = neu vs. bestehend.

## HTTP-Endpunkte

```
GET  /visits                          → Besuchsliste (Screen A: Liste + Karte via geo)
  200:   { "data": [ { visitId, owner_gguid, kunde, datum, geo?, prep_ref,
                        bericht_status: "keiner"|"entwurf"|"freigegeben" } ] }
  → bericht_status pro Eintrag fuer Anzeige/Sortierung (freigegebene nach unten); spart der
    App N Einzel-Report-Calls. "keiner" = Doku noch nicht gelaufen.

GET  /visits/:visitId/preparation     → Briefing-Daten (Screen B)
  200:   { "data": <preparation.json> }
  404:   { "error": { code, message } }   (unbekannter Besuch)

GET  /visits/:visitId/report          → Besuchsbericht (Screen C/D)
  200 (vorhanden):     { "data": { "vorhanden": true,  "report": <report.json> } }
  200 (noch keiner):   { "data": { "vorhanden": false, "report": null, "hinweis": "…" } }
  → KEIN 404: wenn die Doku noch nicht lief, kommt der Platzhalter (vorhanden:false).

GET  /visits/:visitId/transcript      → Gespraech als Chat (Screen C)
  200 (vorhanden):     { "data": { "vorhanden": true,  "transkript": <transkript.json> } }
  200 (noch keiner):   { "data": { "vorhanden": false, "transkript": null, "hinweis": "…" } }
  → KEIN 404: wie bei /report Platzhalter, solange die Doku noch nicht lief.
  → Bei mehreren Verfeinerungs-Runden das KOMBINIERTE Transkript ALLER Runden (turns durchgehend
    neu indexiert 0..N; session.start = erste Runde, session.ende = letzte Runde). Ganzer Verlauf.

POST /token
  Body:  { "visitId": "…", "mode": "vorbereitung"|"dokumentation", "identity": "…", "name"?: "…" }
  200:   { "data": { "token": "<JWT>", "url": "wss://…", "roomName": "<visitId>__<mode>" } }

POST /visits/:visitId/freigabe
  Body:  { "durch": "…", "am"?: "ISO" }
  200:   { "data": <report.json mit status:"freigegeben" + freigabe> }
  404:   kein Report · 400: Validierungsfehler
Response-Format: { data } bzw. { error: { code, message } }.
```

> Report-Fluss der App: nach der Doku `GET …/report` pollen bis `vorhanden:true` (die Extraktion
> laeuft ein paar Sekunden nach dem Auflegen), dann Report pruefen und via `POST …/freigabe` freigeben.
> Fuer die Chat-Ansicht auf Screen C parallel `GET …/transcript` laden (gleicher Platzhalter-Fluss).

## Iterative Verfeinerung per Sprache (mehrere Doku-Runden)

Der AD kann eine fertige Doku beliebig oft per Sprache verfeinern. **Kein neuer Modus** – die App
startet die Verfeinerung wieder mit `POST /token` `mode:"dokumentation"` (frischer Raum
`<visitId>__dokumentation`). Das Backend leitet die Runde aus der Zahl bereits gespeicherter
Transkripte ab (App schickt keine Rundennummer):
- Jede Runde speichert ihr Transkript separat (Runde 1, 2, 3 …) – nichts wird ueberschrieben.
- Runde N re-extrahiert ueber ALLE Runden kombiniert → verfeinerter Bericht. Bestehende CRM-Objekte
  behalten ihre `gguid`; neue `temp_id` koennen sich pro Runde neu vergeben (Vollersatz, echte IDs
  kommen spaeter aus dem CRM).
- **`report.version` zaehlt hoch** (v1 → v2 → …, = Rundennummer). `GET /report` liefert immer die
  hoechste/aktuelle Fassung.
- **App-Polling:** nach dem Auflegen `GET …/report` pollen und die neue Fassung ERST akzeptieren,
  wenn `version` GROESSER ist als vor der Runde. (Leere Runde ohne Gespraech → keine neue Version.)

## Voice-Anbindung (LiveKit)

1. App holt Token via `POST /token` → verbindet sich mit `url` + `token` (LiveKit Swift SDK).
2. **Agent-Status** (`idle/listening/thinking/speaking`): über LiveKits **Standard-Agent-State**
   (Participant-Attribut `lk.agent.state`) – kein Custom-Protokoll nötig, der Swift SDK liefert es.
3. **Transkript/Live-Text**: über LiveKits **native Transcription-Streams** – ebenfalls ohne
   Custom-Data-Channel.
4. `activeTopic` wird im Backend-PoC **nicht** emittiert (im Handoff als optional/subtil markiert) –
   Frontend kann es weglassen; bei Bedarf ergänzen wir später eine Data-Message.
5. Übergang B→C: Session beenden → der Worker schreibt `transkript.json` und fährt die Extraktion;
   die App lädt danach den Report zur Prüfung, Freigabe via `POST /visits/:id/freigabe`.

> Voraussetzung Betrieb: LiveKit-Cloud-Creds + xAI-Guthaben (Voice läuft über xAI Grok).
