/** * Ergebnis-Ansicht: Ampel, einfache Erklärung, Übersetzung, Fachbegriffe, * Frist/Termin mit Kalender-Export, Checkliste und Antwort-Einstieg. */ import React, { useState } from 'react'; import { Alert, Modal, Pressable, ScrollView, StyleSheet, Text, View, } from 'react-native'; import * as Speech from 'expo-speech'; import { NativeStackScreenProps } from '@react-navigation/native-stack'; import { RootStackParamList, SPRACHEN, Sprache } from '../types'; import { useAppStore } from '../store/useAppStore'; import { uebersetzeAnalyse } from '../services/uebersetzung'; import { ClaudeFehler } from '../services/claudeClient'; import { terminZumKalender } from '../services/kalender'; import { formatiereDatum } from '../services/erinnerungen'; import { berechneAmpel } from '../utils/ampel'; import { Ampel } from '../components/Ampel'; import { GrossButton } from '../components/GrossButton'; import { Ikone } from '../components/Ikone'; import { farben, schrift, abstand, TOUCH_TARGET } from '../theme'; type Props = NativeStackScreenProps; const DEUTSCH: Sprache = { code: 'de', name: 'Deutsch', eigenname: 'Deutsch' }; export function AnalyseScreen({ navigation, route }: Props) { const brief = useAppStore((s) => s.briefe.find((b) => b.id === route.params.briefId)); const setzeUebersetzung = useAppStore((s) => s.setzeUebersetzung); const removeBrief = useAppStore((s) => s.removeBrief); const [sprache, setSprache] = useState(DEUTSCH); const [sprachwahlOffen, setSprachwahlOffen] = useState(false); const [uebersetzt, setUebersetzt] = useState(false); const [liest, setLiest] = useState(false); const [erledigt, setErledigt] = useState>({}); if (!brief) { // Brief wurde gelöscht — zurück zum Start return ( Brief nicht gefunden. ); } const { analyse } = brief; const ampel = berechneAmpel(analyse); // Anzeige-Inhalte: deutsch oder aus dem Übersetzungs-Cache const cache = sprache.code !== 'de' ? brief.uebersetzungen[sprache.code] : undefined; const kernaussage = cache?.kernaussage ?? analyse.kernaussage; const erklaerung = cache?.erklaerung_einfach ?? analyse.erklaerung_einfach; const checkliste = cache?.checkliste ?? analyse.checkliste; const fachbegriffe = cache?.fachbegriffe ?? analyse.fachbegriffe; const sprscheWaehlen = async (neu: Sprache) => { setSprachwahlOffen(false); setSprache(neu); if (neu.code === 'de' || brief.uebersetzungen[neu.code]) return; // Noch nicht im Cache -> einmalig übersetzen setUebersetzt(true); try { const u = await uebersetzeAnalyse(analyse, neu); await setzeUebersetzung(brief.id, neu.code, u); } catch (e) { setSprache(DEUTSCH); Alert.alert( 'Übersetzung fehlgeschlagen', e instanceof ClaudeFehler ? e.message : 'Bitte versuchen Sie es erneut.' ); } finally { setUebersetzt(false); } }; /** Vorlese-Funktion für Menschen mit Leseschwäche. */ const vorlesen = () => { if (liest) { Speech.stop(); setLiest(false); return; } setLiest(true); Speech.speak(`${kernaussage}. ${erklaerung}`, { language: sprache.code, onDone: () => setLiest(false), onStopped: () => setLiest(false), onError: () => setLiest(false), }); }; const kalenderExport = async () => { if (!analyse.termin) return; const ok = await terminZumKalender(analyse.termin, `Behörden-Termin: ${analyse.brieftyp}`); Alert.alert( ok ? 'Termin gespeichert' : 'Nicht möglich', ok ? 'Der Termin wurde in Ihren Kalender eingetragen (mit Erinnerung 1 Tag vorher).' : 'Bitte erlauben Sie den Kalender-Zugriff in den Geräte-Einstellungen.' ); }; const loeschen = () => { Alert.alert('Brief löschen?', 'Der Brief wird dauerhaft vom Gerät gelöscht.', [ { text: 'Abbrechen', style: 'cancel' }, { text: 'Löschen', style: 'destructive', onPress: async () => { Speech.stop(); await removeBrief(brief.id); navigation.popToTop(); }, }, ]); }; return ( {/* Dringlichkeits-Ampel ganz oben */} {/* Sprach-Auswahl */} setSprachwahlOffen(true)} accessibilityRole="button" accessibilityLabel={`Sprache ändern. Aktuell: ${sprache.name}`} > {sprache.eigenname} {uebersetzt ? ' (übersetzt…)' : ''} {/* Kernaussage: Was will das Amt von mir? */} Was will das Amt von mir? {kernaussage} {/* Frist / Termin */} {(analyse.frist || analyse.termin) && ( {analyse.frist && ( Frist: {formatiereDatum(analyse.frist.datum)} {'\n'} {analyse.frist.aktion} )} {analyse.termin && ( <> Termin: {formatiereDatum(analyse.termin.datum)} {analyse.termin.uhrzeit ? ` um ${analyse.termin.uhrzeit} Uhr` : ''} {analyse.termin.ort ? `\nOrt: ${analyse.termin.ort}` : ''} )} )} {/* Ausführliche Erklärung */} Erklärung in einfacher Sprache {erklaerung} {/* Checkliste */} {checkliste.length > 0 && ( Das müssen Sie tun {checkliste.map((punkt, i) => ( setErledigt((e) => ({ ...e, [i]: !e[i] }))} accessibilityRole="checkbox" accessibilityState={{ checked: !!erledigt[i] }} > {punkt} ))} )} {/* Fachbegriffe */} {fachbegriffe.length > 0 && ( Schwere Wörter erklärt {fachbegriffe.map((f, i) => ( {f.begriff} {f.erklaerung} ))} )} {/* Antwort erstellen */} {analyse.antwort_noetig && ( navigation.navigate('Antwort', { briefId: brief.id })} /> )} {/* Sprach-Auswahl-Dialog */} setSprachwahlOffen(false)}> Sprache wählen {[DEUTSCH, ...SPRACHEN].map((s) => ( sprscheWaehlen(s)} accessibilityRole="button" > {s.eigenname} {s.code !== 'de' ? `(${s.name})` : ''} {brief.uebersetzungen[s.code] ? ' ✓' : ''} ))} ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: farben.hintergrund }, zentriert: { flex: 1, justifyContent: 'center', alignItems: 'center' }, karte: { backgroundColor: farben.flaeche, borderRadius: 12, padding: abstand.m, marginTop: abstand.m, gap: abstand.s, }, // DIE Antwort der App — dezent im Marken-Tint hervorgehoben karteWichtig: { backgroundColor: farben.hervorhebung, borderRadius: 12, borderWidth: 1, borderColor: farben.hervorhebungRand, padding: abstand.m, marginTop: abstand.m, }, abschnittTitel: { fontSize: schrift.gross, fontWeight: '700', color: farben.primaer, marginBottom: abstand.xs, }, kernaussage: { fontSize: schrift.gross, color: farben.text, lineHeight: 30, fontWeight: '500' }, text: { fontSize: schrift.basis, color: farben.text, lineHeight: 27 }, fett: { fontWeight: '700' }, fristText: { fontSize: schrift.basis, color: farben.text, lineHeight: 28 }, checkZeile: { flexDirection: 'row', alignItems: 'flex-start', gap: abstand.s, minHeight: 44, paddingVertical: 4, }, durchgestrichen: { textDecorationLine: 'line-through', color: farben.textSekundaer }, begriffBlock: { marginBottom: abstand.s }, begriff: { fontSize: schrift.basis, fontWeight: '700', color: farben.text }, sprachButton: { marginTop: abstand.m, minHeight: TOUCH_TARGET, borderRadius: 12, backgroundColor: farben.flaeche, justifyContent: 'center', paddingHorizontal: abstand.m, }, sprachZeileInhalt: { flexDirection: 'row', alignItems: 'center', gap: abstand.s }, sprachButtonText: { flex: 1, fontSize: schrift.basis, color: farben.text, fontWeight: '600' }, modalHintergrund: { flex: 1, backgroundColor: 'rgba(0,0,0,0.5)', justifyContent: 'center', padding: abstand.l, }, modalKarte: { backgroundColor: farben.flaeche, borderRadius: 14, padding: abstand.m, maxHeight: '75%', }, sprachZeile: { minHeight: TOUCH_TARGET, justifyContent: 'center' }, sprachZeileText: { fontSize: schrift.gross, color: farben.text }, });