/** * Antwort-Assistent — 3-Schritte-Assistent nach dem Claude-Design-Entwurf: * 1. Antwort-Art wählen 2. Angaben ergänzen 3. Entwurf prüfen & exportieren * Der KI-Entwurf (generiereAntwort) bleibt das Herzstück; nur die Führung ist * jetzt klar in Schritte gegliedert, mit Fortschrittsbalken. */ import React, { useState } from 'react'; import { Alert, KeyboardAvoidingView, Platform, Pressable, ScrollView, Share, StyleSheet, Text, TextInput, View, } from 'react-native'; import * as Print from 'expo-print'; import * as Sharing from 'expo-sharing'; import { NativeStackScreenProps } from '@react-navigation/native-stack'; import { AntwortTyp, ANTWORT_TYP_LABEL, RootStackParamList } from '../types'; import { useAppStore } from '../store/useAppStore'; import { generiereAntwort } from '../services/antwort'; import { ClaudeFehler } from '../services/claudeClient'; import { GrossButton } from '../components/GrossButton'; import { Ikone, IkonenName } from '../components/Ikone'; import { farben, schrift, abstand, schriftart, ecke, kartenSchatten } from '../theme'; type Props = NativeStackScreenProps; const ALLE_TYPEN: AntwortTyp[] = [ 'terminbestaetigung', 'terminverschiebung', 'widerspruch', 'unterlagen_nachreichen', 'rueckfrage', ]; const TYP_INFO: Record = { terminbestaetigung: { ikone: 'checkAn', beschreibung: 'Sie bestätigen, dass Sie zum Termin kommen.' }, terminverschiebung: { ikone: 'kalender', beschreibung: 'Sie bitten um einen neuen Termin.' }, widerspruch: { ikone: 'warnung', beschreibung: 'Sie sind nicht einverstanden und legen Einspruch ein.' }, unterlagen_nachreichen: { ikone: 'dokument', beschreibung: 'Sie reichen die geforderten Unterlagen ein.' }, rueckfrage: { ikone: 'info', beschreibung: 'Sie haben eine Frage zum Brief.' }, }; export function AntwortScreen({ route }: Props) { const brief = useAppStore((s) => s.briefe.find((b) => b.id === route.params.briefId)); const setzeAntwortEntwurf = useAppStore((s) => s.setzeAntwortEntwurf); const [schritt, setSchritt] = useState<1 | 2 | 3>(brief?.antwortEntwurf?.text ? 3 : 1); const [typ, setTyp] = useState(brief?.antwortEntwurf?.typ ?? null); const [hinweise, setHinweise] = useState(''); const [betreff, setBetreff] = useState(brief?.antwortEntwurf?.betreff ?? ''); const [text, setText] = useState(brief?.antwortEntwurf?.text ?? ''); const [laedt, setLaedt] = useState(false); if (!brief) { return ( Brief nicht gefunden. ); } const optionen = [ ...brief.analyse.antwort_optionen, ...ALLE_TYPEN.filter((t) => !brief.analyse.antwort_optionen.includes(t)), ]; const typWaehlen = (t: AntwortTyp) => { setTyp(t); setSchritt(2); }; const erzeugen = async () => { if (!typ) return; setLaedt(true); try { const entwurf = await generiereAntwort(brief.analyse, typ, hinweise); setBetreff(entwurf.betreff); setText(entwurf.text); await setzeAntwortEntwurf(brief.id, { typ, ...entwurf }); setSchritt(3); } catch (e) { Alert.alert( 'Fehler', e instanceof ClaudeFehler ? e.message : 'Der Entwurf konnte nicht erstellt werden.' ); } finally { setLaedt(false); } }; const sichern = async () => { if (typ && text) await setzeAntwortEntwurf(brief.id, { typ, betreff, text }); }; const alsPdf = async () => { await sichern(); try { const html = `
${escapeHtml(betreff)}
${escapeHtml(text)}
`; const { uri } = await Print.printToFileAsync({ html }); if (await Sharing.isAvailableAsync()) { await Sharing.shareAsync(uri, { mimeType: 'application/pdf', dialogTitle: 'Antwort als PDF' }); } } catch { Alert.alert('Fehler', 'Das PDF konnte nicht erstellt werden.'); } }; const alsText = async () => { await sichern(); await Share.share({ message: `${betreff}\n\n${text}` }); }; return ( {/* Fortschritt */} Schritt {schritt} von 3 {[1, 2, 3].map((n) => ( ))} {/* ── Schritt 1: Art wählen ── */} {schritt === 1 && ( Welche Antwort brauchen Sie? Für {brief.analyse.brieftyp} von {brief.analyse.absender}. {optionen.map((t) => { const empfohlen = brief.analyse.antwort_optionen.includes(t); return ( typWaehlen(t)} accessibilityRole="button" style={({ pressed }) => [styles.typKarte, pressed && { opacity: 0.7 }]} > {ANTWORT_TYP_LABEL[t]} {empfohlen && ( Empfohlen )} {TYP_INFO[t].beschreibung} ); })} )} {/* ── Schritt 2: Angaben ── */} {schritt === 2 && typ && ( {ANTWORT_TYP_LABEL[typ]} Angaben ergänzen Zusätzliche Angaben (optional) Aktenzeichen, Behörde und die Angaben aus dem Brief übernimmt die App automatisch — Sie müssen nichts abtippen. setSchritt(1)} style={styles.zurueck} accessibilityRole="button"> Andere Antwort wählen )} {/* ── Schritt 3: Entwurf ── */} {schritt === 3 && ( Ihr Schreiben ist fertig Prüfen Sie den Entwurf und ersetzen Sie die Platzhalter in [eckigen Klammern] durch Ihre Daten. Betreff Schreiben setSchritt(2)} style={styles.zurueck} accessibilityRole="button"> Angaben ändern & neu erstellen )} ); } function escapeHtml(s: string): string { return s.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: farben.hintergrund }, zentriert: { flex: 1, justifyContent: 'center', alignItems: 'center' }, text: { fontFamily: schriftart.serif, fontSize: schrift.basis, color: farben.text }, schrittLabel: { fontFamily: schriftart.serif, fontSize: schrift.klein, color: farben.textSekundaer, fontWeight: '600', marginBottom: abstand.xs }, fortschritt: { flexDirection: 'row', gap: 6, marginBottom: abstand.l }, balken: { flex: 1, height: 5, borderRadius: 3 }, titel: { fontFamily: schriftart.serif, fontSize: schrift.titel, fontWeight: '800', color: farben.text, marginBottom: 4 }, unterTitel: { fontFamily: schriftart.serif, fontSize: schrift.klein, color: farben.textSekundaer, lineHeight: 22, marginBottom: abstand.s }, // Schritt 1 — Karten typKarte: { ...kartenSchatten, flexDirection: 'row', alignItems: 'center', gap: abstand.s, backgroundColor: farben.flaeche, borderRadius: ecke, padding: abstand.m, }, typIkone: { width: 44, height: 44, borderRadius: ecke, borderWidth: 1, borderColor: farben.primaer, alignItems: 'center', justifyContent: 'center', flexShrink: 0, }, typTitel: { fontFamily: schriftart.serif, fontSize: schrift.basis, fontWeight: '700', color: farben.text }, typBeschreibung: { fontFamily: schriftart.serif, fontSize: schrift.klein, color: farben.textSekundaer, marginTop: 2, lineHeight: 20 }, empfohlenTag: { backgroundColor: farben.hervorhebung, borderRadius: 6, paddingHorizontal: 8, paddingVertical: 2 }, empfohlenText: { fontFamily: schriftart.serif, fontSize: 11, fontWeight: '700', color: farben.primaerFuellung }, // Schritt 2/3 wahlPill: { flexDirection: 'row', alignSelf: 'flex-start', alignItems: 'center', gap: 7, borderWidth: 1, borderColor: farben.primaer, borderRadius: 9999, paddingHorizontal: 12, paddingVertical: 6, marginBottom: abstand.m, }, wahlPillText: { fontFamily: schriftart.serif, fontSize: schrift.klein, color: farben.primaerFuellung, fontWeight: '600' }, feldLabel: { fontFamily: schriftart.serif, fontSize: schrift.klein, color: farben.textSekundaer, fontWeight: '600', marginTop: abstand.s, marginBottom: 6 }, eingabe: { fontFamily: schriftart.serif, borderWidth: 1, borderColor: farben.rand, borderRadius: ecke, padding: abstand.s, fontSize: schrift.basis, color: farben.text, backgroundColor: farben.flaeche, minHeight: 52, }, mehrzeilig: { minHeight: 110 }, briefText: { minHeight: 300, lineHeight: 26 }, infoBox: { flexDirection: 'row', gap: abstand.s, alignItems: 'flex-start', backgroundColor: farben.hervorhebung, borderRadius: ecke, padding: abstand.m, marginTop: abstand.m, }, infoBoxText: { fontFamily: schriftart.serif, flex: 1, fontSize: schrift.klein, color: farben.text, lineHeight: 21 }, zurueck: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 6, marginTop: abstand.m, paddingVertical: abstand.s }, zurueckText: { fontFamily: schriftart.serif, fontSize: schrift.klein, color: farben.primaerFuellung, fontWeight: '600' }, });