import { View, Text } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { useTranslation } from 'react-i18next'; import { useColors } from '../../lib/theme'; import { HalfDonut } from '../common/HalfDonut'; import { CooldownPatternAnalysis } from './CooldownPatternAnalysis'; import type { BackendCooldownEntry } from '../../hooks/useProfileData'; import type { ProtectionCoverageData } from '../../hooks/useProfileData'; export type CooldownEntry = { id: string; startedAt: string; rawStartedAt: string; durationLabel: string; status: 'active' | 'resolved' | 'cancelled'; reason: string | null; }; type Props = { coverage: ProtectionCoverageData | null; cooldowns: CooldownEntry[]; rawCooldowns: BackendCooldownEntry[] | null; }; const WEEKS = 8; const MAX_BAR_HEIGHT = 28; const MIN_BAR_HEIGHT = 2; const DONUT_WIDTH = 180; const PROTECTED_COLOR = '#22c55e'; const UNPROTECTED_COLOR = '#e5e5e5'; function getMondayOfWeek(date: Date): Date { const d = new Date(date); const day = d.getDay(); const diff = (day === 0 ? -6 : 1 - day); d.setDate(d.getDate() + diff); d.setHours(0, 0, 0, 0); return d; } function buildWeekBuckets(cooldowns: CooldownEntry[]): number[] { const now = new Date(); const currentWeekMonday = getMondayOfWeek(now); const buckets: number[] = Array(WEEKS).fill(0); for (const c of cooldowns) { if (!c.rawStartedAt) continue; const started = new Date(c.rawStartedAt); const weekMonday = getMondayOfWeek(started); const diffMs = currentWeekMonday.getTime() - weekMonday.getTime(); const diffWeeks = Math.round(diffMs / (7 * 24 * 60 * 60 * 1000)); if (diffWeeks >= 0 && diffWeeks < WEEKS) { const bucketIndex = WEEKS - 1 - diffWeeks; buckets[bucketIndex]++; } } return buckets; } function formatLastDate(cooldowns: CooldownEntry[], language: string): string { if (cooldowns.length === 0) return ''; const sorted = [...cooldowns].sort( (a, b) => new Date(b.rawStartedAt).getTime() - new Date(a.rawStartedAt).getTime(), ); const latest = new Date(sorted[0].rawStartedAt); if (language === 'de') { const day = String(latest.getDate()).padStart(2, '0'); const month = String(latest.getMonth() + 1).padStart(2, '0'); return `${day}.${month}.`; } return latest.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }); } function formatAvg(totalCount: number, language: string): string { if (totalCount === 0) return '0'; const avg = WEEKS / totalCount; if (language === 'de') { return avg.toFixed(1).replace('.', ','); } return avg.toFixed(1); } export function StreakSection({ coverage, cooldowns, rawCooldowns }: Props) { const colors = useColors(); const { t, i18n } = useTranslation(); const lang = i18n.language ?? 'de'; const buckets = buildWeekBuckets(cooldowns); const maxCount = Math.max(...buckets, 1); const totalInWindow = buckets.reduce((s, v) => s + v, 0); const cooldownsInWindow = totalInWindow; const lastDate = cooldowns.length > 0 ? formatLastDate(cooldowns, lang) : null; const avgStr = formatAvg(cooldownsInWindow, lang); const countLabel = cooldownsInWindow === 0 ? t('profile.cooldown.none') : cooldownsInWindow === 1 ? t('profile.cooldown.count_one', { weeks: WEEKS }) : t('profile.cooldown.count_other', { n: cooldownsInWindow, weeks: WEEKS }); const avgLabel = cooldownsInWindow > 0 && lastDate ? t('profile.cooldown.avg_last', { avg: avgStr, date: lastDate }) : null; const hasData = coverage !== null && coverage.firstProtectionAt !== null; const donutSegments = hasData ? [ { value: Math.max(coverage!.protectedDays, 1), color: PROTECTED_COLOR }, { value: Math.max(coverage!.unprotectedDays, 0), color: UNPROTECTED_COLOR }, ] : [{ value: 1, color: UNPROTECTED_COLOR }]; const current = coverage?.currentStreakDays ?? 0; const record = coverage?.longestStreakDays ?? 0; const progressRatio = record > 0 ? Math.min(current / record, 1) : 1; const isNewRecord = current >= record && record > 0; const isFirstPhase = record === 0; let streakLabel: string; if (isFirstPhase) { streakLabel = t('profile.streak_first_phase', { days: current }); } else if (isNewRecord) { streakLabel = t('profile.streak_new_record', { days: current }); } else { streakLabel = t('profile.streak_to_record', { days: record - current }); } return ( {t('profile.streak_section_label')} {hasData ? ( <> {coverage!.protectedDays} {t('profile.streak_days_protected')} {coverage!.unprotectedDays} {t('profile.coverage_unprotected_label')} {t('profile.streak_phase_label')} {streakLabel} ) : ( {t('profile.coverage_no_data')} {t('profile.coverage_no_data_hint')} )} {t('profile.cooldown.heading')} {t('profile.cooldown.window_label', { weeks: WEEKS })} {buckets.map((count, i) => { const isEmpty = count === 0; const barHeight = isEmpty ? MIN_BAR_HEIGHT : Math.max( MIN_BAR_HEIGHT, Math.min(count, 5) / Math.min(maxCount, 5) * MAX_BAR_HEIGHT, ); return ( ); })} {buckets.map((_, i) => ( {t('profile.cooldown.week_label', { n: i + 1 })} ))} {countLabel} {avgLabel ? ( {avgLabel} ) : null} ); }