/** * Brief-Detail ("Ihr Brief erklärt") — Layout nach dem Claude-Design-Entwurf: * große Frist-Karte mit Countdown oben, dann "Das ist passiert", * "Das müssen Sie tun", schwere Wörter und der Einstieg zur Antwort. * Alle Funktionen bleiben: Übersetzung, Vorlesen, Kalender, Löschen. */ import React, { useLayoutEffect, 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 { berechneAmpel, naechsteFrist, formatiereLangDatum } from '../utils/ampel'; import { GrossButton } from '../components/GrossButton'; import { Ikone } from '../components/Ikone'; import { farben, schrift, abstand, TOUCH_TARGET, schriftart, ecke, kartenSchatten } from '../theme'; type Props = NativeStackScreenProps; const DEUTSCH: Sprache = { code: 'de', name: 'Deutsch', eigenname: 'Deutsch' }; const STUFEN_LABEL: Record = { rot: 'Dringend', gelb: 'Handlung nötig', gruen: 'Zur Kenntnis', }; 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>({}); // Header-Titel = Brieftyp (wie im Entwurf), sobald der Brief geladen ist useLayoutEffect(() => { if (brief) navigation.setOptions({ title: brief.analyse.brieftyp }); }, [navigation, brief]); if (!brief) { return ( Brief nicht gefunden. ); } const { analyse } = brief; const ampel = berechneAmpel(analyse); const frist = naechsteFrist(analyse); const stufenLabel = STUFEN_LABEL[ampel.stufe]; // 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; // Arabisch, Farsi, Urdu, Paschtu laufen von rechts nach links. Ohne das // steht der übersetzte Text linksbündig und liest sich falsch. const rtl = !!sprache.rtl; const rtlText = rtl ? ({ writingDirection: 'rtl', textAlign: 'right' } as const) : null; const sprscheWaehlen = async (neu: Sprache) => { setSprachwahlOffen(false); setSprache(neu); if (neu.code === 'de' || brief.uebersetzungen[neu.code]) return; 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); } }; 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 ( {/* ── Frist-Karte mit Countdown ── */} {frist ? ( {frist.label} · {stufenLabel} {frist.tage < 0 ? '!' : frist.tage} {frist.tage < 0 ? 'überfällig' : 'Tage'} {frist.tage < 0 ? '' : 'verbleiben'} {frist.label === 'Termin' ? 'Termin am' : 'Fällig am'} {formatiereLangDatum(frist.datumIso)} {analyse.frist?.aktion && ( Zu tun {analyse.frist.aktion} )} {analyse.termin && ( <> {(analyse.termin.uhrzeit || analyse.termin.ort) && ( Wo & wann {analyse.termin.uhrzeit ? `${analyse.termin.uhrzeit} Uhr` : ''} {analyse.termin.uhrzeit && analyse.termin.ort ? ' · ' : ''} {analyse.termin.ort ?? ''} )} )} ) : ( Keine Frist Dieser Brief ist nur zur Kenntnis. Sie müssen nichts tun. )} {/* ── Steuerung: Sprache + Vorlesen ── */} setSprachwahlOffen(true)} accessibilityRole="button" accessibilityLabel={`Sprache ändern. Aktuell: ${sprache.name}`} > {uebersetzt ? 'übersetzt…' : sprache.eigenname} {liest ? 'Stopp' : 'Vorlesen'} {/* ── Das ist passiert ── */} Das ist passiert {kernaussage} {erklaerung} {/* ── Das müssen Sie tun ── */} {checkliste.length > 0 && ( <> Das müssen Sie tun {checkliste.map((punkt, i) => { const fertig = !!erledigt[i]; return ( setErledigt((e) => ({ ...e, [i]: !e[i] }))} accessibilityRole="checkbox" accessibilityState={{ checked: fertig }} > {fertig ? ( ) : ( {i + 1} )} {punkt} ); })} )} {/* ── Schwere Wörter ── */} {fachbegriffe.length > 0 && ( <> Schwere Wörter erklärt {fachbegriffe.map((f, i) => ( {f.begriff} {f.erklaerung} ))} )} {/* ── Antwort ── */} {analyse.antwort_noetig && ( navigation.navigate('Antwort', { briefId: brief.id })} /> )} {/* ── In der Beratung: Brief auf dem Bildschirm zeigen ── */} navigation.navigate('Verbinden', { modus: 'senden', briefId: brief.id })} /> {/* ── Datenschutz-Fuß + Löschen ── */} Auf Ihrem Gerät ausgewertet · verschlüsselt gespeichert {/* 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' }, // Frist-Karte fristKarte: { borderRadius: ecke, borderWidth: 1, padding: abstand.l }, fristKopf: { flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 10 }, fristKicker: { fontFamily: schriftart.mono, fontSize: 12, letterSpacing: 0.6, textTransform: 'uppercase', fontWeight: '700' }, grosseZahl: { fontFamily: schriftart.serif, fontSize: 64, lineHeight: 64, fontWeight: '800', fontVariant: ['tabular-nums'] }, zahlEinheit: { fontFamily: schriftart.serif, fontSize: 22, fontWeight: '700', lineHeight: 24 }, zahlUnter: { fontFamily: schriftart.serif, fontSize: 13, color: farben.textSekundaer }, trenner: { height: 1, backgroundColor: farben.rand, marginVertical: 14 }, datenZeile: { flexDirection: 'row', justifyContent: 'space-between', gap: 12, marginTop: 5 }, datenLabel: { fontFamily: schriftart.serif, fontSize: schrift.klein, color: farben.textSekundaer }, datenWert: { fontFamily: schriftart.serif, fontSize: schrift.klein, fontWeight: '700', color: farben.text }, keineFrist: { flexDirection: 'row', gap: abstand.s, alignItems: 'center', backgroundColor: farben.ampelGruenHintergrund, borderRadius: ecke, borderLeftWidth: 4, borderLeftColor: farben.ampelGruen, padding: abstand.m, }, keineFristTitel: { fontFamily: schriftart.serif, fontSize: schrift.basis, fontWeight: '700', color: farben.text }, keineFristText: { fontFamily: schriftart.serif, fontSize: schrift.klein, color: farben.textSekundaer, marginTop: 2 }, // Werkzeuge werkzeugZeile: { flexDirection: 'row', gap: abstand.s, marginTop: abstand.m }, werkzeug: { ...kartenSchatten, flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 7, backgroundColor: farben.flaeche, borderRadius: ecke, minHeight: 48, paddingHorizontal: abstand.s, }, werkzeugText: { fontFamily: schriftart.serif, fontSize: schrift.klein, fontWeight: '600', color: farben.primaerFuellung }, kicker: { fontFamily: schriftart.mono, fontSize: 12, letterSpacing: 1.1, textTransform: 'uppercase', color: farben.textSekundaer, fontWeight: '700', marginTop: abstand.l, marginBottom: abstand.xs, }, kernaussage: { fontFamily: schriftart.serif, fontSize: schrift.gross, color: farben.text, lineHeight: 29, fontWeight: '600' }, erklaerung: { fontFamily: schriftart.serif, fontSize: schrift.basis, color: farben.textSekundaer, lineHeight: 27, marginTop: abstand.s }, dünnerTrenner: { height: 1, backgroundColor: farben.rand, marginTop: abstand.l }, // To-do todoZeile: { flexDirection: 'row', gap: abstand.s, alignItems: 'flex-start', minHeight: 40 }, todoKreis: { width: 28, height: 28, borderRadius: ecke, borderWidth: 1.5, borderColor: farben.primaer, alignItems: 'center', justifyContent: 'center', marginTop: 1, }, todoKreisFertig: { backgroundColor: farben.ampelGruen, borderColor: farben.ampelGruen }, todoNummer: { fontFamily: schriftart.serif, fontSize: schrift.klein, fontWeight: '700', color: farben.primaerFuellung, fontVariant: ['tabular-nums'] }, todoText: { fontFamily: schriftart.serif, flex: 1, fontSize: schrift.basis, color: farben.text, lineHeight: 25, paddingTop: 3 }, durchgestrichen: { textDecorationLine: 'line-through', color: farben.textSekundaer }, // Fachbegriffe begriffKarte: { ...kartenSchatten, backgroundColor: farben.flaeche, borderRadius: ecke, padding: abstand.m }, begriff: { fontFamily: schriftart.serif, fontSize: schrift.basis, fontWeight: '700', color: farben.text, marginBottom: 3 }, begriffText: { fontFamily: schriftart.serif, fontSize: schrift.klein, color: farben.textSekundaer, lineHeight: 23 }, schutzZeile: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 6, marginTop: abstand.l }, schutzText: { fontFamily: schriftart.serif, fontSize: 11.5, color: farben.textTertiaer }, text: { fontFamily: schriftart.serif, fontSize: schrift.basis, color: farben.text, lineHeight: 27 }, // Modal modalHintergrund: { flex: 1, backgroundColor: 'rgba(0,0,0,0.45)', justifyContent: 'center', padding: abstand.l }, modalKarte: { ...kartenSchatten, backgroundColor: farben.flaeche, borderRadius: ecke, padding: abstand.m, maxHeight: '75%' }, modalTitel: { fontFamily: schriftart.serif, fontSize: schrift.gross, fontWeight: '700', color: farben.text, marginBottom: abstand.xs }, sprachZeile: { minHeight: TOUCH_TARGET, justifyContent: 'center' }, sprachZeileText: { fontFamily: schriftart.serif, fontSize: schrift.gross, color: farben.text }, });