diff --git a/src/screens/AnalyseScreen.tsx b/src/screens/AnalyseScreen.tsx index 13ae141..ebe97df 100644 --- a/src/screens/AnalyseScreen.tsx +++ b/src/screens/AnalyseScreen.tsx @@ -1,8 +1,10 @@ /** - * Ergebnis-Ansicht: Ampel, einfache Erklärung, Übersetzung, Fachbegriffe, - * Frist/Termin mit Kalender-Export, Checkliste und Antwort-Einstieg. + * 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, { useState } from 'react'; +import React, { useLayoutEffect, useState } from 'react'; import { Alert, Modal, @@ -19,9 +21,7 @@ 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 { berechneAmpel, naechsteFrist, formatiereLangDatum } from '../utils/ampel'; import { GrossButton } from '../components/GrossButton'; import { Ikone } from '../components/Ikone'; import { farben, schrift, abstand, TOUCH_TARGET } from '../theme'; @@ -30,6 +30,12 @@ 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); @@ -41,8 +47,12 @@ export function AnalyseScreen({ navigation, route }: Props) { 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) { - // Brief wurde gelöscht — zurück zum Start return ( Brief nicht gefunden. @@ -52,6 +62,8 @@ export function AnalyseScreen({ navigation, route }: Props) { 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; @@ -64,7 +76,6 @@ export function AnalyseScreen({ navigation, route }: Props) { 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); @@ -80,7 +91,6 @@ export function AnalyseScreen({ navigation, route }: Props) { } }; - /** Vorlese-Funktion für Menschen mit Leseschwäche. */ const vorlesen = () => { if (liest) { Speech.stop(); @@ -123,135 +133,169 @@ export function AnalyseScreen({ navigation, route }: Props) { }; 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} + + {/* ── 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 && ( <> - - Termin: {formatiereDatum(analyse.termin.datum)} - {analyse.termin.uhrzeit ? ` um ${analyse.termin.uhrzeit} Uhr` : ''} - {analyse.termin.ort ? `\nOrt: ${analyse.termin.ort}` : ''} - - + {(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. + + )} - {/* Ausführliche Erklärung */} - - Erklärung in einfacher Sprache - {erklaerung} + {/* ── Steuerung: Sprache + Vorlesen ── */} + + setSprachwahlOffen(true)} + accessibilityRole="button" + accessibilityLabel={`Sprache ändern. Aktuell: ${sprache.name}`} + > + + + {uebersetzt ? 'übersetzt…' : sprache.eigenname} + + + + + {liest ? 'Stopp' : 'Vorlesen'} + - {/* Checkliste */} + {/* ── 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) => ( - setErledigt((e) => ({ ...e, [i]: !e[i] }))} - accessibilityRole="checkbox" - accessibilityState={{ checked: !!erledigt[i] }} - > - - - {punkt} - - - ))} - + <> + + 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} + + ); + })} + + )} - {/* Fachbegriffe */} + {/* ── Schwere Wörter ── */} {fachbegriffe.length > 0 && ( - - Schwere Wörter erklärt - {fachbegriffe.map((f, i) => ( - - {f.begriff} - {f.erklaerung} - - ))} + <> + + Schwere Wörter erklärt + + {fachbegriffe.map((f, i) => ( + + {f.begriff} + {f.erklaerung} + + ))} + + + )} + + {/* ── Antwort ── */} + {analyse.antwort_noetig && ( + + navigation.navigate('Antwort', { briefId: brief.id })} + /> )} - {/* Antwort erstellen */} - {analyse.antwort_noetig && ( - navigation.navigate('Antwort', { briefId: brief.id })} - /> - )} - - - - + {/* ── Datenschutz-Fuß + Löschen ── */} + + + Auf Ihrem Gerät ausgewertet · verschlüsselt gespeichert + + + + {/* Sprach-Auswahl-Dialog */} setSprachwahlOffen(false)}> - Sprache wählen + Sprache wählen {[DEUTSCH, ...SPRACHEN].map((s) => ( {s.eigenname} {s.code !== 'de' ? `(${s.name})` : ''} @@ -282,64 +326,68 @@ export function AnalyseScreen({ navigation, route }: Props) { 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, + + // Frist-Karte + fristKarte: { borderRadius: 16, borderWidth: 1, padding: abstand.l }, + fristKopf: { flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: 10 }, + fristKicker: { fontSize: 12, letterSpacing: 0.6, textTransform: 'uppercase', fontWeight: '700' }, + grosseZahl: { fontSize: 64, lineHeight: 64, fontWeight: '800', fontVariant: ['tabular-nums'] }, + zahlEinheit: { fontSize: 22, fontWeight: '700', lineHeight: 24 }, + zahlUnter: { fontSize: 13, color: farben.textSekundaer }, + trenner: { height: 1, backgroundColor: farben.rand, marginVertical: 14 }, + datenZeile: { flexDirection: 'row', justifyContent: 'space-between', gap: 12, marginTop: 5 }, + datenLabel: { fontSize: schrift.klein, color: farben.textSekundaer }, + datenWert: { fontSize: schrift.klein, fontWeight: '700', color: farben.text }, + + keineFrist: { + flexDirection: 'row', gap: abstand.s, alignItems: 'center', + backgroundColor: farben.ampelGruenHintergrund, borderRadius: 12, + borderLeftWidth: 4, borderLeftColor: farben.ampelGruen, padding: abstand.m, }, - // 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, + keineFristTitel: { fontSize: schrift.basis, fontWeight: '700', color: farben.text }, + keineFristText: { fontSize: schrift.klein, color: farben.textSekundaer, marginTop: 2 }, + + // Werkzeuge + werkzeugZeile: { flexDirection: 'row', gap: abstand.s, marginTop: abstand.m }, + werkzeug: { + flex: 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 7, + backgroundColor: farben.flaeche, borderRadius: 12, minHeight: 48, paddingHorizontal: abstand.s, }, - abschnittTitel: { - fontSize: schrift.gross, - fontWeight: '700', - color: farben.primaer, - marginBottom: abstand.xs, + werkzeugText: { fontSize: schrift.klein, fontWeight: '600', color: farben.primaerFuellung }, + + kicker: { + fontSize: 12, letterSpacing: 1.1, textTransform: 'uppercase', + color: farben.primaer, fontWeight: '700', marginTop: abstand.l, 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, + kernaussage: { fontSize: schrift.gross, color: farben.text, lineHeight: 29, fontWeight: '600' }, + erklaerung: { 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: 14, borderWidth: 1.5, borderColor: farben.primaer, + alignItems: 'center', justifyContent: 'center', marginTop: 1, }, + todoKreisFertig: { backgroundColor: farben.ampelGruen, borderColor: farben.ampelGruen }, + todoNummer: { fontSize: schrift.klein, fontWeight: '700', color: farben.primaerFuellung, fontVariant: ['tabular-nums'] }, + todoText: { flex: 1, fontSize: schrift.basis, color: farben.text, lineHeight: 25, paddingTop: 3 }, 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%', - }, + + // Fachbegriffe + begriffKarte: { backgroundColor: farben.flaeche, borderRadius: 12, padding: abstand.m }, + begriff: { fontSize: schrift.basis, fontWeight: '700', color: farben.text, marginBottom: 3 }, + begriffText: { fontSize: schrift.klein, color: farben.textSekundaer, lineHeight: 23 }, + + schutzZeile: { flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 6, marginTop: abstand.l }, + schutzText: { fontSize: 11.5, color: farben.textTertiaer }, + + text: { 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: { backgroundColor: farben.flaeche, borderRadius: 14, padding: abstand.m, maxHeight: '75%' }, + modalTitel: { fontSize: schrift.gross, fontWeight: '700', color: farben.text, marginBottom: abstand.xs }, sprachZeile: { minHeight: TOUCH_TARGET, justifyContent: 'center' }, sprachZeileText: { fontSize: schrift.gross, color: farben.text }, });