import { useState } from 'react'; import { View, Text, TouchableOpacity, LayoutAnimation, Platform, UIManager } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { useTranslation } from 'react-i18next'; import { useColors } from '../../lib/theme'; import type { BackendCooldownEntry } from '../../hooks/useProfileData'; if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) { UIManager.setLayoutAnimationEnabledExperimental(true); } const DE_STOP_WORDS = new Set([ 'der', 'die', 'das', 'den', 'dem', 'des', 'ein', 'eine', 'einer', 'einem', 'einen', 'eines', 'und', 'oder', 'aber', 'auch', 'noch', 'nur', 'nicht', 'kein', 'keine', 'keinen', 'ich', 'du', 'er', 'sie', 'es', 'wir', 'ihr', 'sie', 'mich', 'dich', 'sich', 'mir', 'dir', 'uns', 'euch', 'ihm', 'ihr', 'ihnen', 'mein', 'dein', 'sein', 'ihr', 'unser', 'euer', 'meinen', 'deinen', 'seinen', 'ihren', 'unseren', 'meiner', 'deiner', 'seiner', 'ihrer', 'unserer', 'meinem', 'deinem', 'seinem', 'ihrem', 'unserem', 'ist', 'sind', 'war', 'waren', 'bin', 'bist', 'hat', 'haben', 'hatte', 'hatten', 'wird', 'werden', 'wurde', 'wurden', 'kann', 'muss', 'soll', 'will', 'mag', 'bei', 'von', 'mit', 'aus', 'auf', 'in', 'an', 'im', 'am', 'zu', 'zum', 'zur', 'als', 'wie', 'wenn', 'weil', 'dass', 'ob', 'damit', 'wann', 'wo', 'was', 'wer', 'so', 'da', 'hier', 'dort', 'nun', 'mal', 'gerade', 'dann', 'nach', 'vor', 'seit', 'sehr', 'mehr', 'viel', 'wieder', 'immer', 'schon', 'halt', 'einfach', 'eigentlich', 'heute', 'jetzt', 'wieder', 'etwa', ]); const EN_STOP_WORDS = new Set([ 'the', 'a', 'an', 'and', 'or', 'but', 'not', 'no', 'nor', 'i', 'me', 'my', 'myself', 'we', 'our', 'you', 'your', 'he', 'she', 'it', 'they', 'him', 'her', 'his', 'its', 'them', 'their', 'this', 'that', 'these', 'those', 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'have', 'has', 'had', 'do', 'does', 'did', 'will', 'would', 'could', 'should', 'may', 'might', 'can', 'of', 'in', 'to', 'for', 'on', 'at', 'by', 'with', 'from', 'as', 'into', 'about', 'so', 'just', 'also', 'then', 'now', 'here', 'there', 'up', 'out', 'very', 'really', 'already', 'again', 'too', 'today', 'after', 'before', ]); const WEEKDAY_KEYS = [ 'profile.cooldown.patterns.weekday_mon', 'profile.cooldown.patterns.weekday_tue', 'profile.cooldown.patterns.weekday_wed', 'profile.cooldown.patterns.weekday_thu', 'profile.cooldown.patterns.weekday_fri', 'profile.cooldown.patterns.weekday_sat', 'profile.cooldown.patterns.weekday_sun', ] as const; const MAX_BAR_HEIGHT = 48; const MIN_BAR_HEIGHT = 2; type TopWord = { word: string; count: number }; function buildHourBuckets(entries: BackendCooldownEntry[]): number[] { const buckets = Array(24).fill(0); for (const e of entries) { const h = new Date(e.startedAt).getHours(); buckets[h]++; } return buckets; } function buildWeekdayBuckets(entries: BackendCooldownEntry[]): number[] { const buckets = Array(7).fill(0); for (const e of entries) { const jsDay = new Date(e.startedAt).getDay(); const moFirst = jsDay === 0 ? 6 : jsDay - 1; buckets[moFirst]++; } return buckets; } function extractTopWords(entries: BackendCooldownEntry[], lang: string): TopWord[] { const stopWords = lang.startsWith('de') ? DE_STOP_WORDS : EN_STOP_WORDS; const freq: Record = {}; for (const e of entries) { if (!e.reason) continue; const words = e.reason .toLowerCase() .replace(/[^a-zA-ZäöüÄÖÜß\s]/g, ' ') .split(/\s+/); for (const w of words) { if (w.length < 3) continue; if (stopWords.has(w)) continue; freq[w] = (freq[w] ?? 0) + 1; } } return Object.entries(freq) .sort((a, b) => b[1] - a[1]) .slice(0, 5) .map(([word, count]) => ({ word, count })); } function cancelRate(entries: BackendCooldownEntry[]): number { if (entries.length === 0) return 0; const cancelled = entries.filter((e) => e.status === 'cancelled').length; return Math.round((cancelled / entries.length) * 100); } type BarChartProps = { buckets: number[]; labels: string[]; highlightIndices?: number[]; colors: ReturnType; }; function BarChart({ buckets, labels, highlightIndices, colors }: BarChartProps) { const maxCount = Math.max(...buckets, 1); return ( {buckets.map((count, i) => { const isEmpty = count === 0; const barHeight = isEmpty ? MIN_BAR_HEIGHT : Math.max(MIN_BAR_HEIGHT, (count / maxCount) * MAX_BAR_HEIGHT); const isHighlight = highlightIndices?.includes(i); return ( {labels[i] !== '' ? ( {labels[i]} ) : ( )} ); })} ); } type SectionHeadingProps = { label: string; colors: ReturnType }; function SectionHeading({ label, colors }: SectionHeadingProps) { return ( {label.toUpperCase()} ); } type Props = { rawCooldowns: BackendCooldownEntry[] | null; }; export function CooldownPatternAnalysis({ rawCooldowns }: Props) { const { t, i18n } = useTranslation(); const colors = useColors(); const lang = i18n.language ?? 'de'; const [expanded, setExpanded] = useState(false); function toggle() { LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); setExpanded((v) => !v); } const entries = rawCooldowns ?? []; const hasData = entries.length > 0; const hourBuckets = hasData ? buildHourBuckets(entries) : Array(24).fill(0); const weekdayBuckets = hasData ? buildWeekdayBuckets(entries) : Array(7).fill(0); const topWords = hasData ? extractTopWords(entries, lang) : []; const pct = cancelRate(entries); const hourLabels: string[] = Array(24) .fill('') .map((_, h) => { if (h === 6) return t('profile.cooldown.patterns.hour_morning'); if (h === 12) return t('profile.cooldown.patterns.hour_afternoon'); if (h === 18) return t('profile.cooldown.patterns.hour_evening'); if (h === 0) return t('profile.cooldown.patterns.hour_night'); return ''; }); const weekdayLabels: string[] = WEEKDAY_KEYS.map((k) => t(k)); const peakHour = hasData ? hourBuckets.indexOf(Math.max(...hourBuckets)) : -1; const peakDay = hasData ? weekdayBuckets.indexOf(Math.max(...weekdayBuckets)) : -1; return ( {t('profile.cooldown.patterns.toggle_label')} {expanded ? ( {!hasData ? ( {t('profile.cooldown.patterns.not_enough')} ) : ( <> = 0 ? [peakHour] : []} colors={colors} /> = 0 ? [peakDay] : []} colors={colors} /> {topWords.length >= 3 ? ( {topWords.map(({ word, count }) => ( {word} ({count}) ))} ) : ( {t('profile.cooldown.patterns.not_enough')} )} {t('profile.cooldown.patterns.cancel_rate', { pct })} )} ) : null} ); }