import { useEffect, useMemo, useState } from 'react'; import { Trash2 } from 'lucide-react'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'; import { Badge } from '@/components/ui/badge'; import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem, } from '@/components/ui/select'; import { cn } from '@/lib/utils'; import type { QSOForm } from '@/types'; type QSO = QSOForm; const BANDS = ['160m','80m','60m','40m','30m','20m','17m','15m','12m','10m','6m','4m','2m','70cm','23cm']; const MODES = ['SSB','CW','FT8','FT4','RTTY','PSK31','AM','FM','DIGITALVOICE','MFSK','OLIVIA','JS8','JT65','JT9']; const QSL_STATUSES = [ { value: '_', label: '—' }, { value: 'Y', label: 'Yes' }, { value: 'N', label: 'No' }, { value: 'R', label: 'Requested' }, { value: 'I', label: 'Ignore' }, ]; const PROP_MODES = ['_','AS','AUE','AUR','BS','ECH','EME','ES','F2','F2M','FAI','GWAVE','INTERNET','ION','IRL','LOS','MS','RPT','RS','SAT','TEP','TR']; interface Props { qso: QSO; onSave: (q: QSO) => void; onDelete: (id: number) => void; onClose: () => void; } function toLocalISO(d: any): string { if (!d) return ''; const date = new Date(d); if (isNaN(date.getTime())) return ''; const p = (n: number) => String(n).padStart(2, '0'); return `${date.getUTCFullYear()}-${p(date.getUTCMonth()+1)}-${p(date.getUTCDate())}T${p(date.getUTCHours())}:${p(date.getUTCMinutes())}`; } function parseLocalISO(s: string): string | null { if (!s) return null; const m = s.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})/); if (!m) return null; return `${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:00.000Z`; } function stringifyExtras(e?: Record): string { if (!e) return ''; return Object.entries(e).map(([k, v]) => `${k} = ${v}`).join('\n'); } function parseExtras(t: string): Record | undefined { const out: Record = {}; for (const raw of t.split('\n')) { const line = raw.trim(); if (!line) continue; const idx = line.indexOf('='); if (idx < 0) continue; const k = line.slice(0, idx).trim().toUpperCase(); const v = line.slice(idx + 1).trim(); if (k && v) out[k] = v; } return Object.keys(out).length ? out : undefined; } function numOrUndef(v: any): number | undefined { if (v === '' || v === null || v === undefined) return undefined; const n = typeof v === 'number' ? v : parseFloat(String(v)); return isNaN(n) ? undefined : n; } function intOrUndef(v: any): number | undefined { const n = numOrUndef(v); return n === undefined ? undefined : Math.trunc(n); } function F({ label, span = 1, children }: { label: string; span?: 1 | 2 | 3 | 6; children: React.ReactNode }) { return (
{children}
); } function QslSelect({ value, onChange }: { value?: string; onChange: (v: string) => void }) { return ( ); } export function QSOEditModal({ qso, onSave, onDelete, onClose }: Props) { const [draft, setDraft] = useState(() => JSON.parse(JSON.stringify(qso))); const [freqMhz, setFreqMhz] = useState(draft.freq_hz ? String(draft.freq_hz / 1_000_000) : ''); const [freqRxMhz, setFreqRxMhz] = useState(draft.freq_rx_hz ? String(draft.freq_rx_hz / 1_000_000) : ''); const [dateOn, setDateOn] = useState(toLocalISO(draft.qso_date)); const [dateOff, setDateOff] = useState(toLocalISO(draft.qso_date_off)); const [extrasText, setExtrasText] = useState(stringifyExtras(draft.extras)); const [localErr, setLocalErr] = useState(''); const [saving, setSaving] = useState(false); function set(key: K, value: QSO[K]) { setDraft((d) => ({ ...d, [key]: value })); } function save() { if (!draft.callsign?.trim()) { setLocalErr('Callsign required'); return; } setSaving(true); setLocalErr(''); const out: any = { ...draft, callsign: draft.callsign.trim().toUpperCase(), grid: (draft.grid ?? '').trim().toUpperCase(), gridsquare_ext: (draft.gridsquare_ext ?? '').trim().toUpperCase(), station_callsign: (draft.station_callsign ?? '').trim().toUpperCase(), operator: (draft.operator ?? '').trim().toUpperCase(), my_grid: (draft.my_grid ?? '').trim().toUpperCase(), my_gridsquare_ext: (draft.my_gridsquare_ext ?? '').trim().toUpperCase(), iota: (draft.iota ?? '').trim().toUpperCase(), sota_ref: (draft.sota_ref ?? '').trim().toUpperCase(), pota_ref: (draft.pota_ref ?? '').trim().toUpperCase(), my_iota: (draft.my_iota ?? '').trim().toUpperCase(), my_sota_ref: (draft.my_sota_ref ?? '').trim().toUpperCase(), my_pota_ref: (draft.my_pota_ref ?? '').trim().toUpperCase(), qso_date: parseLocalISO(dateOn) ?? new Date().toISOString(), qso_date_off: parseLocalISO(dateOff) ?? undefined, freq_hz: freqMhz.trim() ? Math.round(parseFloat(freqMhz) * 1_000_000) : undefined, freq_rx_hz: freqRxMhz.trim() ? Math.round(parseFloat(freqRxMhz) * 1_000_000) : undefined, dxcc: intOrUndef(draft.dxcc), cqz: intOrUndef(draft.cqz), ituz: intOrUndef(draft.ituz), age: intOrUndef(draft.age), srx: intOrUndef(draft.srx), stx: intOrUndef(draft.stx), my_dxcc: intOrUndef(draft.my_dxcc), my_cq_zone: intOrUndef(draft.my_cq_zone), my_itu_zone: intOrUndef(draft.my_itu_zone), lat: numOrUndef(draft.lat), lon: numOrUndef(draft.lon), my_lat: numOrUndef(draft.my_lat), my_lon: numOrUndef(draft.my_lon), ant_az: numOrUndef(draft.ant_az), ant_el: numOrUndef(draft.ant_el), tx_pwr: numOrUndef(draft.tx_pwr), extras: parseExtras(extrasText), }; onSave(out); } useEffect(() => { function onKey(e: KeyboardEvent) { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) save(); } window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); // eslint-disable-next-line react-hooks/exhaustive-deps }); const extrasCount = useMemo( () => (draft.extras ? Object.keys(draft.extras).length : 0), [draft.extras], ); return ( { if (!o) onClose(); }}> Edit QSO #{draft.id} — {draft.callsign} Edit fields for QSO #{draft.id} Basic Contacted QSL Contest Sat / Prop My station Notes Extras {extrasCount > 0 && ( {extrasCount} )} {localErr && (
{localErr}
)}
set('callsign', e.target.value)} /> setDateOn(e.target.value)} /> setDateOff(e.target.value)} /> set('submode', e.target.value)} /> setFreqMhz(e.target.value)} /> setFreqRxMhz(e.target.value)} /> set('rst_sent', e.target.value)} /> set('rst_rcvd', e.target.value)} /> set('tx_pwr', numOrUndef(e.target.value) as any)} />
set('name', e.target.value)} /> set('qth', e.target.value)} /> set('address', e.target.value)} /> (set as any)('email', e.target.value)} /> set('web', e.target.value)} /> set('country', e.target.value)} /> set('dxcc', intOrUndef(e.target.value) as any)} /> set('cont', e.target.value)} /> set('cqz', intOrUndef(e.target.value) as any)} /> set('ituz', intOrUndef(e.target.value) as any)} /> set('state', e.target.value)} /> set('cnty', e.target.value)} /> set('grid', e.target.value)} /> set('gridsquare_ext', e.target.value)} /> set('vucc_grids', e.target.value)} /> set('iota', e.target.value)} /> set('sota_ref', e.target.value)} /> set('pota_ref', e.target.value)} /> set('age', intOrUndef(e.target.value) as any)} /> set('lat', numOrUndef(e.target.value) as any)} /> set('lon', numOrUndef(e.target.value) as any)} /> set('rig', e.target.value)} /> set('ant', e.target.value)} />
set('qsl_sent', v)} /> set('qsl_rcvd', v)} /> set('qsl_sent_date', e.target.value)} /> set('qsl_rcvd_date', e.target.value)} /> set('qsl_via', e.target.value)} /> set('qsl_msg', e.target.value)} /> set('qslmsg_rcvd', e.target.value)} /> set('lotw_sent', v)} /> set('lotw_rcvd', v)} /> set('lotw_sent_date', e.target.value)} /> set('lotw_rcvd_date', e.target.value)} /> set('eqsl_sent', v)} /> set('eqsl_rcvd', v)} /> set('eqsl_sent_date', e.target.value)} /> set('eqsl_rcvd_date', e.target.value)} /> set('clublog_qso_upload_status', e.target.value)} /> set('clublog_qso_upload_date', e.target.value)} /> set('hrdlog_qso_upload_status', e.target.value)} /> set('hrdlog_qso_upload_date', e.target.value)} /> set('qrzcom_qso_upload_status', e.target.value)} /> set('qrzcom_qso_upload_date', e.target.value)} />
set('contest_id', e.target.value)} /> set('srx', intOrUndef(e.target.value) as any)} /> set('stx', intOrUndef(e.target.value) as any)} /> set('srx_string', e.target.value)} /> set('stx_string', e.target.value)} /> set('check', e.target.value)} /> set('precedence', e.target.value)} /> set('arrl_sect', e.target.value)} />
set('sat_name', e.target.value)} /> set('sat_mode', e.target.value)} /> set('ant_az', numOrUndef(e.target.value) as any)} /> set('ant_el', numOrUndef(e.target.value) as any)} /> set('ant_path', e.target.value)} />

These override the active station profile for this QSO only.

set('station_callsign', e.target.value)} /> set('operator', e.target.value)} /> set('my_grid', e.target.value)} /> set('my_gridsquare_ext', e.target.value)} /> set('my_country', e.target.value)} /> set('my_state', e.target.value)} /> set('my_cnty', e.target.value)} /> set('my_dxcc', intOrUndef(e.target.value) as any)} /> set('my_cq_zone', intOrUndef(e.target.value) as any)} /> set('my_itu_zone', intOrUndef(e.target.value) as any)} /> set('my_iota', e.target.value)} /> set('my_sota_ref', e.target.value)} /> set('my_pota_ref', e.target.value)} /> set('my_lat', numOrUndef(e.target.value) as any)} /> set('my_lon', numOrUndef(e.target.value) as any)} /> set('my_street', e.target.value)} /> set('my_city', e.target.value)} /> set('my_postal_code', e.target.value)} /> set('my_rig', e.target.value)} /> set('my_antenna', e.target.value)} />
set('comment', e.target.value)} />