);
}
export function AlertsModal({ onClose, bands, modes, countries }: {
onClose: () => void; bands: string[]; modes: string[]; countries: string[];
}) {
const { t } = useI18n();
const [rules, setRules] = useState([]);
const [draft, setDraft] = useState(null);
// Raw text of the callsigns box, kept separately from draft.calls. The list is
// normalised (trim + drop empties) on the way into the rule, so binding the
// textarea straight to calls.join('\n') round-trips every keystroke through
// that normalisation — a trailing newline or space is stripped before React
// re-renders, and Enter/Space appear to do nothing. Editing the raw text and
// deriving the list from it keeps typing intact.
const [callsText, setCallsText] = useState('');
const [tab, setTab] = useState('def'); // active editor tab (reset to Definition on new/select)
const [emailTo, setEmailTo] = useState('');
const [err, setErr] = useState('');
const refresh = useCallback(async () => {
try { setRules(((await ListAlertRules()) ?? []) as Rule[]); }
catch (e: any) { setErr(String(e?.message ?? e)); }
}, []);
useEffect(() => { refresh(); GetAlertEmailTo().then((v) => setEmailTo(v || '')).catch(() => {}); }, [refresh]);
// loadDraft opens a rule in the editor — always through here, so the raw
// callsigns text is re-seeded from whatever rule is now being edited.
const loadDraft = (r: Rule | null) => {
setDraft(r);
setCallsText((r?.calls ?? []).join('\n'));
};
const patch = (p: Partial) => setDraft((d) => (d ? alerts.Rule.createFrom({ ...d, ...p }) : d));
const toggleIn = (key: keyof Rule, v: string) => setDraft((d) => {
if (!d) return d;
const cur = ((d as any)[key] as string[]) ?? [];
const has = cur.some((x) => x.toLowerCase() === v.toLowerCase());
const next = has ? cur.filter((x) => x.toLowerCase() !== v.toLowerCase()) : [...cur, v];
return alerts.Rule.createFrom({ ...d, [key]: next });
});
// Saving used to be silent: the dialog stays open and the rule looks exactly
// as it did a second earlier, so there was nothing to tell an operator whether
// the click had registered. A line that says so, and clears itself.
const [savedMsg, setSavedMsg] = useState('');
const savedTimer = useRef(0);
function flashSaved(text: string) {
setSavedMsg(text);
window.clearTimeout(savedTimer.current);
savedTimer.current = window.setTimeout(() => setSavedMsg(''), 3000);
}
useEffect(() => () => window.clearTimeout(savedTimer.current), []);
async function save() {
if (!draft) return;
if (!draft.name.trim()) { setErr(t('altm.giveName')); return; }
try {
const saved = await SaveAlertRule(draft);
await refresh();
loadDraft(saved as Rule);
setErr('');
flashSaved(t('altm.saved', { name: draft.name.trim() }));
} catch (e: any) { setErr(String(e?.message ?? e)); }
}
async function del() {
if (!draft) return;
if (!draft.id) { loadDraft(null); return; }
if (!window.confirm(t('altm.deleteConfirm', { name: draft.name }))) return;
try {
const name = draft.name;
await DeleteAlertRule(draft.id);
loadDraft(null);
await refresh();
flashSaved(t('altm.deleted', { name }));
}
catch (e: any) { setErr(String(e?.message ?? e)); }
}
return (
);
}