import { useEffect, useMemo, useRef, useState } from 'react'; import { View, Text, Pressable, TextInput, Modal, LayoutAnimation, Platform, UIManager, ScrollView, } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { colors } from '../../lib/theme'; import type { Plan } from '../../hooks/useUserPlan'; if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) { UIManager.setLayoutAnimationEnabledExperimental(true); } export type Demographics = { birthYear: number | null; gender: string | null; maritalStatus: string | null; profession: string | null; bundesland: string | null; city: string | null; }; type Props = { demographics: Demographics; plan: Plan; defaultExpanded?: boolean; onChange?: (next: Demographics) => void; onRevokeConsent?: () => void; }; // Select-Optionen — Display-Label DE, value für DB-Persistenz const GENDER_OPTIONS: Array<{ label: string; value: string }> = [ { label: 'männlich', value: 'male' }, { label: 'weiblich', value: 'female' }, { label: 'divers', value: 'diverse' }, { label: 'keine Angabe', value: 'none' }, ]; const MARITAL_OPTIONS: Array<{ label: string; value: string }> = [ { label: 'ledig', value: 'single' }, { label: 'Partnerschaft', value: 'partnership' }, { label: 'verheiratet', value: 'married' }, { label: 'geschieden', value: 'divorced' }, { label: 'verwitwet', value: 'widowed' }, { label: 'keine Angabe', value: 'none' }, ]; // ISO-3166-2:DE — value=ISO, label=DE-Display const BUNDESLAND_OPTIONS: Array<{ label: string; value: string }> = [ { label: 'Baden-Württemberg', value: 'BW' }, { label: 'Bayern', value: 'BY' }, { label: 'Berlin', value: 'BE' }, { label: 'Brandenburg', value: 'BB' }, { label: 'Bremen', value: 'HB' }, { label: 'Hamburg', value: 'HH' }, { label: 'Hessen', value: 'HE' }, { label: 'Mecklenburg-Vorpommern', value: 'MV' }, { label: 'Niedersachsen', value: 'NI' }, { label: 'Nordrhein-Westfalen', value: 'NW' }, { label: 'Rheinland-Pfalz', value: 'RP' }, { label: 'Saarland', value: 'SL' }, { label: 'Sachsen', value: 'SN' }, { label: 'Sachsen-Anhalt', value: 'ST' }, { label: 'Schleswig-Holstein', value: 'SH' }, { label: 'Thüringen', value: 'TH' }, ]; const FIELD_WHY: Record = { birthYear: 'Lyra spricht dich altersgerecht an, DiGA-Berichte erkennen Risiko nach Altersgruppe.', gender: 'Glücksspiel-Muster unterscheiden sich; Lyra coacht gendersensibel.', profession: 'Schichtarbeit, Banking-Stress, Selbstständigkeit haben verschiedene Trigger — Lyra kennt deinen Kontext.', maritalStatus: 'Trennung/Beziehungs-Konflikte sind klassische Trigger — Lyra erkennt sie früher in dir.', bundesland: 'Lokale Beratungsstellen + anonyme DiGA-Studien.', city: 'Lokale Beratungsstellen + anonyme DiGA-Studien.', }; function lookupLabel(options: Array<{ label: string; value: string }>, v: string | null) { if (!v) return null; return options.find((o) => o.value === v)?.label ?? v; } function isComplete(d: Demographics) { return ( d.birthYear !== null && !!d.gender && !!d.maritalStatus && !!d.profession && !!d.bundesland && !!d.city ); } // TODO Phase C: PATCH /api/profile/me/demographics — debounced auto-save (~500ms idle). // Bis Endpoint live: lokaler State + onChange-Callback Richtung Parent. function mockPersist(_next: Demographics) { // no-op placeholder — Parent ruft echten Endpoint } export function DemographicsAccordion({ demographics, plan, defaultExpanded = false, onChange, onRevokeConsent, }: Props) { const [expanded, setExpanded] = useState(defaultExpanded); const [local, setLocal] = useState(demographics); // Select-Sheet-State const [pickerField, setPickerField] = useState(null); // Debounce-Save Ref const saveTimer = useRef | null>(null); useEffect(() => { setLocal(demographics); }, [demographics]); function toggle() { LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); setExpanded((v) => !v); } function persist(next: Demographics) { setLocal(next); if (saveTimer.current) clearTimeout(saveTimer.current); saveTimer.current = setTimeout(() => { mockPersist(next); onChange?.(next); }, 500); } function flushSave(next: Demographics) { if (saveTimer.current) clearTimeout(saveTimer.current); mockPersist(next); onChange?.(next); setLocal(next); } const completed = isComplete(local); const showProTrialBanner = plan === 'free' && completed; return ( {/* Privacy-Header */} ({ opacity: pressed ? 0.7 : 1, backgroundColor: '#ffffff', borderWidth: 1, borderColor: '#e5e5e5', borderRadius: 14, padding: 16, })} > ANONYMER BEITRAG ZUR FORSCHUNG Optional. Niemals mit Name oder Email verknüpft. Jederzeit löschbar. {expanded ? ( {/* Pro-Trial-Reward-Banner — nur free + (idealerweise) nicht-vollständig. Wir zeigen ihn aber auch im "completed"-State als sanfte Bestätigung, tatsächliche Trial-Vergabe ist Backend-Sache (Phase C). */} {plan === 'free' ? ( {showProTrialBanner ? 'Du bekommst 1 Woche Pro geschenkt' : 'Vervollständige dein Profil — 1 Woche Pro geschenkt'} Mit deinen anonymen Daten machen wir rebreak zur ersten DiGA-zertifizierten Spielsucht-App. Als Dankeschön: 1 Woche Pro. ) : null} {/* Birth Year — Number-Input */} { const cleaned = raw.replace(/[^0-9]/g, '').slice(0, 4); if (cleaned === '') { persist({ ...local, birthYear: null }); return; } const n = parseInt(cleaned, 10); // Erlaube tippen — Validierung beim Blur persist({ ...local, birthYear: Number.isNaN(n) ? null : n }); }} onBlur={() => { const n = local.birthYear; if (n !== null && (n < 1920 || n > 2010)) { // ungültig — auf null zurücksetzen flushSave({ ...local, birthYear: null }); } }} keyboardType="number-pad" maxLength={4} placeholder="z.B. 1989" placeholderTextColor={colors.textMuted} style={inputStyle} /> {/* Gender — Select */} setPickerField('gender')} /> {/* Profession — TextInput */} persist({ ...local, profession: t })} onBlur={() => { const trimmed = (local.profession ?? '').trim(); flushSave({ ...local, profession: trimmed === '' ? null : trimmed }); }} maxLength={80} placeholder="z.B. Pflege, IT, Schichtarbeit" placeholderTextColor={colors.textMuted} style={inputStyle} /> {/* Marital — Select */} setPickerField('maritalStatus')} /> {/* Bundesland — Select */} setPickerField('bundesland')} /> {/* City — TextInput */} persist({ ...local, city: t })} onBlur={() => { const trimmed = (local.city ?? '').trim(); flushSave({ ...local, city: trimmed === '' ? null : trimmed }); }} maxLength={60} placeholder="z.B. München" placeholderTextColor={colors.textMuted} style={inputStyle} /> {/* Revoke Consent */} ({ opacity: pressed ? 0.7 : 1, marginTop: 4, paddingHorizontal: 14, paddingVertical: 12, borderTopWidth: 1, borderTopColor: 'rgba(0,0,0,0.06)', })} > Einwilligung widerrufen ) : null} setPickerField(null)} onSelect={(v) => { flushSave({ ...local, gender: v }); setPickerField(null); }} /> setPickerField(null)} onSelect={(v) => { flushSave({ ...local, maritalStatus: v }); setPickerField(null); }} /> setPickerField(null)} onSelect={(v) => { flushSave({ ...local, bundesland: v }); setPickerField(null); }} /> ); } const inputStyle = { fontSize: 14, color: colors.text, fontFamily: 'Nunito_600SemiBold', paddingVertical: 8, paddingHorizontal: 10, backgroundColor: '#fafafa', borderRadius: 8, borderWidth: 1, borderColor: '#ececec', minWidth: 140, textAlign: 'right' as const, }; function FieldRow({ label, why, isLast, children, }: { label: string; why: string; isLast?: boolean; children: React.ReactNode; }) { return ( {label} {children} {why} ); } function SelectButton({ value, onPress }: { value: string | null; onPress: () => void }) { return ( ({ opacity: pressed ? 0.6 : 1, flexDirection: 'row', alignItems: 'center', gap: 6, paddingVertical: 8, paddingHorizontal: 10, backgroundColor: '#fafafa', borderRadius: 8, borderWidth: 1, borderColor: '#ececec', minWidth: 140, justifyContent: 'flex-end', })} > {value ?? 'auswählen'} ); } function SelectSheet({ visible, title, options, selectedValue, onClose, onSelect, }: { visible: boolean; title: string; options: Array<{ label: string; value: string }>; selectedValue: string | null; onClose: () => void; onSelect: (v: string) => void; }) { const sortedOptions = useMemo(() => options, [options]); return ( { /* swallow */ }} style={{ backgroundColor: '#ffffff', borderTopLeftRadius: 18, borderTopRightRadius: 18, paddingHorizontal: 8, paddingTop: 12, paddingBottom: 24, maxHeight: '70%', }} > {title} {sortedOptions.map((opt) => { const isSelected = opt.value === selectedValue; return ( onSelect(opt.value)} style={({ pressed }) => ({ opacity: pressed ? 0.6 : 1, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 14, paddingVertical: 14, borderRadius: 10, backgroundColor: isSelected ? '#f5f8ff' : 'transparent', })} > {opt.label} {isSelected ? ( ) : null} ); })} ); }