import { useCallback, useEffect, useRef, useState } from 'react'; import { apiFetch } from '../lib/api'; export type BlockedByDayEntry = { date: string; count: number; }; export type BlockedByConnectionEntry = { connectionId: string; title: string | null; email: string; providerLabel: string; count: number; }; type MailStatsState = { blockedByDay: BlockedByDayEntry[]; blockedByConnection: BlockedByConnectionEntry[]; loading: boolean; }; type ConnectionAgeGranularity = 'too-new' | 'day' | 'week' | 'month'; function resolveGranularity(createdAt: string | null | undefined): ConnectionAgeGranularity { if (!createdAt) return 'day'; const ageMs = Date.now() - new Date(createdAt).getTime(); const ageH = ageMs / 3_600_000; if (ageH < 24) return 'too-new'; const ageD = ageH / 24; if (ageD <= 14) return 'day'; if (ageD <= 90) return 'week'; return 'month'; } type ConnectionStatsState = { data: BlockedByDayEntry[]; granularity: ConnectionAgeGranularity; loading: boolean; }; export function useMailConnectionStats( connectionId: string, createdAt: string | null | undefined, enabled: boolean, ) { const fetchedRef = useRef(false); const [state, setState] = useState({ data: [], granularity: resolveGranularity(createdAt), loading: false, }); const granularity = resolveGranularity(createdAt); const fetch = useCallback(async () => { if (!enabled || !connectionId) return; setState((s) => ({ ...s, loading: true })); try { const ageDays = createdAt ? Math.max(1, Math.ceil((Date.now() - new Date(createdAt).getTime()) / 86_400_000)) : 30; const days = Math.min(30, ageDays); const raw = await apiFetch( `/api/mail/stats/blocked-by-day?days=${days}&connectionId=${connectionId}`, ); const nonEmpty = raw.filter((e) => e.count > 0); let data: BlockedByDayEntry[]; if (granularity === 'week') { data = aggregateToWeeks(raw); } else if (granularity === 'month') { data = aggregateToMonths(raw); } else if (nonEmpty.length > 0) { // IMMER auf die echte Hit-Range zoomen — von erstem nonEmpty bis // letztem nonEmpty. Gaps dazwischen bleiben sichtbar (für realistische // Verteilung), aber 29 leere Slots am Anfang + 1 Bar am Ende wird // auf z.B. 1-3 Bars geschrumpft. Trim eliminiert das visuelle Rauschen. const firstDate = nonEmpty[0].date; const lastDate = nonEmpty[nonEmpty.length - 1].date; data = raw.filter((e) => e.date >= firstDate && e.date <= lastDate); } else { data = raw; } setState({ data, granularity, loading: false }); } catch { setState((s) => ({ ...s, loading: false })); } }, [enabled, connectionId, granularity, createdAt]); useEffect(() => { if (!enabled) return; if (fetchedRef.current) return; fetchedRef.current = true; fetch(); }, [enabled, fetch]); return { ...state, refresh: fetch }; } function aggregateToWeeks(entries: BlockedByDayEntry[]): BlockedByDayEntry[] { const buckets: Map = new Map(); for (const e of entries) { const d = new Date(e.date + 'T00:00:00'); const day = d.getDay(); const diff = d.getDate() - day + (day === 0 ? -6 : 1); const monday = new Date(d); monday.setDate(diff); const key = monday.toISOString().slice(0, 10); buckets.set(key, (buckets.get(key) ?? 0) + e.count); } return [...buckets.entries()] .sort(([a], [b]) => a.localeCompare(b)) .map(([date, count]) => ({ date, count })); } function aggregateToMonths(entries: BlockedByDayEntry[]): BlockedByDayEntry[] { const buckets: Map = new Map(); for (const e of entries) { const key = e.date.slice(0, 7); buckets.set(key, (buckets.get(key) ?? 0) + e.count); } return [...buckets.entries()] .sort(([a], [b]) => a.localeCompare(b)) .map(([date, count]) => ({ date, count })); } export function useMailStats(enabled: boolean) { const [state, setState] = useState({ blockedByDay: [], blockedByConnection: [], loading: false, }); const fetch = useCallback(async () => { if (!enabled) return; setState((s) => ({ ...s, loading: true })); try { const [byDay, byConn] = await Promise.all([ apiFetch('/api/mail/stats/blocked-by-day?days=30'), apiFetch('/api/mail/stats/blocked-by-connection'), ]); setState({ blockedByDay: byDay, blockedByConnection: byConn, loading: false }); } catch { setState((s) => ({ ...s, loading: false })); } }, [enabled]); useEffect(() => { fetch(); }, [fetch]); return { ...state, refresh: fetch }; }