import { useCallback, useEffect, useState } from 'react'; import { Antenna as AntennaIcon, Radio, Plus, Trash2, Star, ChevronRight, ChevronDown, Edit2, } from 'lucide-react'; import { ListOperatingTree, SaveOperatingStation, DeleteOperatingStation, SaveOperatingAntenna, DeleteOperatingAntenna, } from '../../wailsjs/go/main/App'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Checkbox } from '@/components/ui/checkbox'; import { cn } from '@/lib/utils'; import { useI18n } from '@/lib/i18n'; type Band = { band: string; is_default: boolean }; type Antenna = { id: number; station_id: number; name: string; sort_order: number; bands: Band[]; }; type Station = { id: number; profile_id: number; name: string; tx_pwr?: number; sort_order: number; antennas?: Antenna[]; }; type Props = { /** ADIF bands available to toggle, in display order (from ListsSettings). */ bands: string[]; /** External error sink — parent shows it next to the Save button. */ onError: (msg: string) => void; }; export function OperatingPanel({ bands, onError }: Props) { const { t } = useI18n(); const [tree, setTree] = useState([]); const [loading, setLoading] = useState(true); // expanded keeps which stations show their antennas; everything open by // default so the user sees the full setup at a glance. const [expanded, setExpanded] = useState>(new Set()); // editingId tracks the row currently in edit mode. Use a string namespace // to keep station ids and antenna ids in the same Set. const [editing, setEditing] = useState(null); const reload = useCallback(async () => { try { const t = await ListOperatingTree(); const list = (t ?? []) as Station[]; setTree(list); setExpanded((prev) => { if (prev.size > 0) return prev; return new Set(list.map((s) => s.id)); }); } catch (e: any) { onError(String(e?.message ?? e)); } finally { setLoading(false); } }, [onError]); useEffect(() => { void reload(); }, [reload]); function toggleExpanded(id: number) { setExpanded((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); } async function addStation() { try { const created = await SaveOperatingStation({ id: 0, profile_id: 0, name: 'New rig', sort_order: tree.length, } as any); const c = created as Station; setTree((prev) => [...prev, { ...c, antennas: [] }]); setExpanded((prev) => new Set(prev).add(c.id)); setEditing(`station:${c.id}`); } catch (e: any) { onError(String(e?.message ?? e)); } } async function updateStation(s: Station) { try { const saved = await SaveOperatingStation(s as any) as Station; setTree((prev) => prev.map((x) => x.id === s.id ? { ...x, ...saved } : x)); } catch (e: any) { onError(String(e?.message ?? e)); } } async function removeStation(id: number) { if (!confirm('Delete this rig and all its antennas?')) return; try { await DeleteOperatingStation(id); setTree((prev) => prev.filter((s) => s.id !== id)); } catch (e: any) { onError(String(e?.message ?? e)); } } async function addAntenna(stationId: number) { try { const created = await SaveOperatingAntenna({ id: 0, station_id: stationId, name: 'New antenna', sort_order: 0, bands: [], } as any) as Antenna; setTree((prev) => prev.map((s) => s.id === stationId ? { ...s, antennas: [...(s.antennas ?? []), created] } : s )); setEditing(`antenna:${created.id}`); } catch (e: any) { onError(String(e?.message ?? e)); } } async function updateAntenna(a: Antenna) { try { const saved = await SaveOperatingAntenna(a as any) as Antenna; setTree((prev) => prev.map((s) => s.id === a.station_id ? { ...s, antennas: (s.antennas ?? []).map((x) => x.id === a.id ? saved : x), } : { // The save may have cleared is_default on antennas of OTHER // stations (one default per band per profile). Refresh those // by reloading the tree wholesale. ...s, } )); // Reload to pick up cross-station default flips. void reload(); } catch (e: any) { onError(String(e?.message ?? e)); } } async function removeAntenna(stationId: number, antId: number) { if (!confirm(t('op.deleteAntConfirm'))) return; try { await DeleteOperatingAntenna(antId); setTree((prev) => prev.map((s) => s.id === stationId ? { ...s, antennas: (s.antennas ?? []).filter((a) => a.id !== antId) } : s )); } catch (e: any) { onError(String(e?.message ?? e)); } } if (loading) { return
Loading…
; } return (
{t('op.intro1')}{t('op.intro2')}
{tree.length === 0 ? (
{t('op.noRig')}
) : (
{tree.map((station) => ( toggleExpanded(station.id)} onUpdate={updateStation} onDelete={() => removeStation(station.id)} onAddAntenna={() => addAntenna(station.id)} onUpdateAntenna={updateAntenna} onDeleteAntenna={(antId) => removeAntenna(station.id, antId)} /> ))}
)}
); } // ── Station row ──────────────────────────────────────────────────────── type StationRowProps = { station: Station; bands: string[]; expanded: boolean; editing: string | null; setEditing: (id: string | null) => void; onToggleExpanded: () => void; onUpdate: (s: Station) => void; onDelete: () => void; onAddAntenna: () => void; onUpdateAntenna: (a: Antenna) => void; onDeleteAntenna: (antId: number) => void; }; function StationRow({ station, bands, expanded, editing, setEditing, onToggleExpanded, onUpdate, onDelete, onAddAntenna, onUpdateAntenna, onDeleteAntenna, }: StationRowProps) { const { t } = useI18n(); const editKey = `station:${station.id}`; const isEditing = editing === editKey; const [draft, setDraft] = useState({ name: station.name, tx_pwr: station.tx_pwr != null ? String(station.tx_pwr) : '', }); useEffect(() => { if (!isEditing) setDraft({ name: station.name, tx_pwr: station.tx_pwr != null ? String(station.tx_pwr) : '', }); }, [isEditing, station.name, station.tx_pwr]); function commit() { const pwrNum = draft.tx_pwr.trim() === '' ? undefined : parseFloat(draft.tx_pwr); onUpdate({ ...station, name: draft.name.trim() || station.name, tx_pwr: Number.isFinite(pwrNum as number) ? (pwrNum as number) : undefined, }); setEditing(null); } return (
{isEditing ? (
setDraft((d) => ({ ...d, name: e.target.value }))} onKeyDown={(e) => { if (e.key === 'Enter') commit(); if (e.key === 'Escape') setEditing(null); }} /> setDraft((d) => ({ ...d, tx_pwr: e.target.value }))} onKeyDown={(e) => { if (e.key === 'Enter') commit(); if (e.key === 'Escape') setEditing(null); }} />
) : ( <> {station.name} {station.tx_pwr != null && ( {station.tx_pwr} W )}
)}
{expanded && (
{(station.antennas ?? []).length === 0 ? (
{t('op.noAntenna')}
) : ( (station.antennas ?? []).map((a) => ( onDeleteAntenna(a.id)} /> )) )}
)}
); } // ── Antenna row ──────────────────────────────────────────────────────── type AntennaRowProps = { antenna: Antenna; bands: string[]; editing: string | null; setEditing: (id: string | null) => void; onUpdate: (a: Antenna) => void; onDelete: () => void; }; function AntennaRow({ antenna, bands, editing, setEditing, onUpdate, onDelete }: AntennaRowProps) { const { t } = useI18n(); const editKey = `antenna:${antenna.id}`; const isEditing = editing === editKey; const [draft, setDraft] = useState({ name: antenna.name }); useEffect(() => { if (!isEditing) setDraft({ name: antenna.name }); }, [isEditing, antenna.name]); const enabledBands = new Map( (antenna.bands ?? []).map((b) => [b.band, b]) ); function commitNames() { onUpdate({ ...antenna, name: draft.name.trim() || antenna.name, bands: antenna.bands ?? [], }); setEditing(null); } function toggleBand(band: string, on: boolean) { let next = [...(antenna.bands ?? [])]; if (on) { if (!next.find((b) => b.band === band)) { next.push({ band, is_default: false }); } } else { next = next.filter((b) => b.band !== band); } onUpdate({ ...antenna, bands: next }); } function setDefault(band: string, isDefault: boolean) { const next = (antenna.bands ?? []).map((b) => b.band === band ? { ...b, is_default: isDefault } : b ); onUpdate({ ...antenna, bands: next }); } return (
{isEditing ? (
setDraft((d) => ({ ...d, name: e.target.value }))} onKeyDown={(e) => { if (e.key === 'Enter') commitNames(); if (e.key === 'Escape') setEditing(null); }} />
) : ( <> {antenna.name}
)}
{bands.length === 0 ? ( No band configured in Settings → Bands. ) : bands.map((band) => { const entry = enabledBands.get(band); const enabled = !!entry; const isDefault = !!entry?.is_default; return (
toggleBand(band, !!c)} className="size-3" /> {band} {enabled && ( )}
); })}
); }