import { useEffect, useState } from 'react'; import { Copy, Loader2, Trash2, X } from 'lucide-react'; import { FindDuplicates, DeleteQSOs } from '../../wailsjs/go/main/App'; import { useI18n } from '@/lib/i18n'; import { cn } from '@/lib/utils'; type QSO = { id: number; callsign: string; qso_date: string; band?: string; mode?: string; freq_hz?: number; rst_sent?: string; rst_rcvd?: string; name?: string; }; type Group = { key: string; qsos: QSO[] }; const fmtDate = (iso: string) => { const d = new Date(iso); if (isNaN(d.getTime())) return iso; const p = (n: number) => String(n).padStart(2, '0'); return `${d.getUTCFullYear()}-${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())} ${p(d.getUTCHours())}:${p(d.getUTCMinutes())}`; }; const fmtFreq = (hz?: number) => (hz && hz > 0 ? (hz / 1e6).toFixed(3) : '—'); // DuplicatesModal — scans the log for duplicate QSOs (same call+band+mode on the // same day; optionally same minute), groups them, and lets the operator pick // which copies to delete. The oldest in each group is left unchecked by default. export function DuplicatesModal({ onClose, onDeleted }: { onClose: () => void; onDeleted: () => void }) { const { t } = useI18n(); const [windowMin, setWindowMin] = useState(5); // duplicates land a few minutes apart const [loading, setLoading] = useState(true); const [groups, setGroups] = useState([]); const [sel, setSel] = useState>(new Set()); const [busy, setBusy] = useState(false); const load = (w: number) => { setLoading(true); FindDuplicates(w) .then((g) => { const gs = (g ?? []) as Group[]; setGroups(gs); // Default: select every QSO except the first (oldest) of each group. const next = new Set(); gs.forEach((grp) => grp.qsos.slice(1).forEach((q) => next.add(q.id))); setSel(next); }) .catch(() => { setGroups([]); setSel(new Set()); }) .finally(() => setLoading(false)); }; useEffect(() => { load(windowMin); /* eslint-disable-next-line */ }, [windowMin]); const toggle = (id: number) => setSel((s) => { const n = new Set(s); n.has(id) ? n.delete(id) : n.add(id); return n; }); const selectExtras = () => { const n = new Set(); groups.forEach((g) => g.qsos.slice(1).forEach((q) => n.add(q.id))); setSel(n); }; const deselectAll = () => setSel(new Set()); const totalQsos = groups.reduce((a, g) => a + g.qsos.length, 0); const doDelete = async () => { if (sel.size === 0) return; if (!confirm(t('dup.deleteConfirm', { n: sel.size }))) return; setBusy(true); try { await DeleteQSOs(Array.from(sel) as any); onDeleted(); load(windowMin); // rescan so the list reflects what's left } catch { /* ignore */ } finally { setBusy(false); } }; return (
e.stopPropagation()}> {/* Header */}
{t('dup.title')}
{/* Controls */}

{t('dup.hint')}

{t('dup.summary', { groups: groups.length, qsos: totalQsos, sel: sel.size })}
{/* Body */}
{loading ? (
{t('dup.scanning')}
) : groups.length === 0 ? (
{t('dup.none')}
) : ( groups.map((g) => (
{g.qsos[0].callsign} {g.qsos[0].band} · {g.qsos[0].mode} · {g.qsos.length}×
{g.qsos.map((q, i) => { const checked = sel.has(q.id); return ( ); })}
{t('dup.colDate')} {t('dup.colFreq')} {t('dup.colRst')} {t('dup.colName')}
toggle(q.id)} /> {fmtDate(q.qso_date)} {i === 0 && {t('dup.keep')}} {fmtFreq(q.freq_hz)} {q.rst_sent || '—'}/{q.rst_rcvd || '—'} {q.name || ''}
)) )}
{/* Footer */}
); }