Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 48 additions & 10 deletions src/components/channels/ChannelEditModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { RXGroup } from '../../models/RXGroup';
import type { EncryptionKey } from '../../models/EncryptionKey';
import type { QuickContact } from '../../models/QuickContact';
import { CTCSS_FREQUENCIES, DCS_CODES, formatCTCSSFrequency, formatDCSCode } from '../../utils/ctcssConstants';
import { isNoTxFrequency, isRxInNoTxBand } from '../../services/validation/frequencyValidator';

// Frequency input component that only updates parent on blur
interface FrequencyInputProps {
Expand Down Expand Up @@ -140,8 +141,8 @@ export const ChannelEditModal: React.FC<ChannelEditModalProps> = ({
)}
</div>

<div className="grid grid-cols-2 gap-3">
<div>
<div className="flex gap-2 items-end">
<div className="flex-1 min-w-0">
<label className="block text-xs font-medium text-cool-gray mb-1">
Receive Frequency (MHz)
</label>
Expand All @@ -152,16 +153,49 @@ export const ChannelEditModal: React.FC<ChannelEditModalProps> = ({
/>
<p className="text-xs text-cool-gray mt-0.5">Frequency the radio receives on</p>
</div>
<div>
<div className="flex flex-col gap-0.5 pb-5">
<button
type="button"
onClick={() => {
if (!(isRxInNoTxBand(editedChannel.rxFrequency) && isNoTxFrequency(editedChannel.txFrequency))) {
handleChange('txFrequency', editedChannel.rxFrequency);
}
}}
disabled={isRxInNoTxBand(editedChannel.rxFrequency) && isNoTxFrequency(editedChannel.txFrequency)}
className="p-1.5 rounded border border-neon-cyan border-opacity-30 text-neon-cyan hover:bg-neon-cyan hover:bg-opacity-10 hover:border-neon-cyan focus:outline-none focus:border-neon-cyan disabled:opacity-40 disabled:text-cool-gray disabled:border-opacity-20 disabled:cursor-not-allowed disabled:hover:bg-transparent"
title={isRxInNoTxBand(editedChannel.rxFrequency) && isNoTxFrequency(editedChannel.txFrequency) ? 'Receive-only (no TX)' : 'Copy RX to TX'}
aria-label="Copy RX to TX"
>
<span className="text-sm font-bold">→</span>
</button>
</div>
<div className="flex-1 min-w-0">
<label className="block text-xs font-medium text-cool-gray mb-1">
Transmit Frequency (MHz)
</label>
<FrequencyInput
value={editedChannel.txFrequency}
onChange={(val) => handleChange('txFrequency', val)}
className="w-full bg-transparent border border-neon-cyan border-opacity-30 rounded px-2 py-1 text-sm text-white focus:outline-none focus:border-neon-cyan focus:shadow-glow-cyan"
/>
<p className="text-xs text-cool-gray mt-0.5">Frequency the radio transmits on</p>
{isRxInNoTxBand(editedChannel.rxFrequency) && isNoTxFrequency(editedChannel.txFrequency) ? (
<>
<input
type="text"
readOnly
disabled
value=""
title="Receive-only (no TX)"
aria-label="No transmit"
className="w-full bg-deep-gray border border-neon-cyan border-opacity-20 rounded px-2 py-1 text-sm text-cool-gray opacity-60 cursor-not-allowed"
/>
<p className="text-xs text-cool-gray mt-0.5">Receive-only (87–136 MHz); TX disabled</p>
</>
) : (
<>
<FrequencyInput
value={editedChannel.txFrequency}
onChange={(val) => handleChange('txFrequency', val)}
className="w-full bg-transparent border border-neon-cyan border-opacity-30 rounded px-2 py-1 text-sm text-white focus:outline-none focus:border-neon-cyan focus:shadow-glow-cyan"
/>
<p className="text-xs text-cool-gray mt-0.5">Frequency the radio transmits on</p>
</>
)}
</div>
</div>

Expand Down Expand Up @@ -557,7 +591,11 @@ export const ChannelEditModal: React.FC<ChannelEditModalProps> = ({
<input
type="checkbox"
checked={editedChannel.forbidTx}
onChange={(e) => handleChange('forbidTx', e.target.checked)}
onChange={(e) => {
const next = e.target.checked;
if (!next && isRxInNoTxBand(editedChannel.rxFrequency) && isNoTxFrequency(editedChannel.txFrequency)) return;
handleChange('forbidTx', next);
}}
className="w-4 h-4 accent-neon-cyan flex-shrink-0"
/>
<div>
Expand Down
102 changes: 69 additions & 33 deletions src/components/channels/ChannelsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { ConfirmModal } from '../ui/ConfirmModal';
import { Card } from '../ui/Card';
import { EmptyState } from '../ui/EmptyState';
import { CTCSS_FREQUENCIES, DCS_CODES, formatCTCSSFrequency, formatDCSCode } from '../../utils/ctcssConstants';
import { isNoTxFrequency, isRxInNoTxBand } from '../../services/validation/frequencyValidator';

// Frequency input component that only updates parent on blur (prevents cursor jumping)
interface FrequencyInputProps {
Expand Down Expand Up @@ -267,40 +268,41 @@ export const ChannelsTable: React.FC<ChannelsTableProps> = ({
title="Clear selection"
/>
</th>
<th className="px-2 py-2 text-left text-neon-cyan font-bold sticky left-[28px] bg-dark-charcoal z-30 min-w-[40px]">#</th>
<th className="px-2 py-2 text-left text-neon-cyan font-bold sticky left-[68px] bg-dark-charcoal z-30 min-w-[120px]">Name</th>
<th className="px-2 py-2 text-left text-neon-cyan font-bold min-w-[110px]">RX Freq</th>
<th className="px-2 py-2 text-left text-neon-cyan font-bold min-w-[110px]">TX Freq</th>
<th className="px-2 py-2 text-center text-neon-cyan font-bold min-w-[50px]">Mode</th>
<th className="px-2 py-2 text-center text-neon-cyan font-bold min-w-[40px]">PWR</th>
<th className="px-2 py-2 text-center text-neon-cyan font-bold min-w-[40px]">BW</th>
<th className="px-2 py-2 text-left text-neon-cyan font-bold sticky left-[28px] bg-dark-charcoal z-30 min-w-[40px]" title="Channel number">#</th>
<th className="px-2 py-2 text-left text-neon-cyan font-bold sticky left-[68px] bg-dark-charcoal z-30 min-w-[120px]" title="Channel name">Name</th>
<th className="px-2 py-2 text-left text-neon-cyan font-bold min-w-[110px]" title="Receive frequency (MHz)">RX Freq</th>
<th className="px-2 py-2 text-center text-neon-cyan font-bold w-0 min-w-0" title="Copy RX to TX"><span className="sr-only">Copy</span></th>
<th className="px-2 py-2 text-left text-neon-cyan font-bold min-w-[110px]" title="Transmit frequency (MHz)">TX Freq</th>
<th className="px-2 py-2 text-center text-neon-cyan font-bold min-w-[50px]" title="Channel mode (Analog/Digital)">Mode</th>
<th className="px-2 py-2 text-center text-neon-cyan font-bold min-w-[40px]" title="Power level">PWR</th>
<th className="px-2 py-2 text-center text-neon-cyan font-bold min-w-[40px]" title="Bandwidth (12.5 kHz / 25 kHz)">BW</th>
{/* Common fields - work for both analog and digital */}
<th className="px-2 py-2 text-center text-neon-cyan font-bold min-w-[35px]">Forbid TX</th>
<th className="px-2 py-2 text-left text-neon-cyan font-bold min-w-[75px]">RX Tone</th>
<th className="px-2 py-2 text-left text-neon-cyan font-bold min-w-[75px]">TX Tone</th>
<th className="px-2 py-2 text-center text-neon-cyan font-bold min-w-[35px]" title="Forbid transmit">Forbid TX</th>
<th className="px-2 py-2 text-left text-neon-cyan font-bold min-w-[75px]" title="Receive tone (CTCSS/DCS)">RX Tone</th>
<th className="px-2 py-2 text-left text-neon-cyan font-bold min-w-[75px]" title="Transmit tone (CTCSS/DCS)">TX Tone</th>
<th className="px-2 py-2 text-center text-neon-cyan font-bold min-w-[30px]" title="Lone Worker">LW</th>
<th className="px-2 py-2 text-left text-neon-cyan font-bold min-w-[50px]">Scan List</th>
<th className="px-2 py-2 text-center text-neon-cyan font-bold min-w-[35px]">FTA</th>
<th className="px-2 py-2 text-center text-neon-cyan font-bold min-w-[35px]">Emerg</th>
<th className="px-2 py-2 text-center text-neon-cyan font-bold min-w-[35px]">Emerg Ack</th>
<th className="px-2 py-2 text-left text-neon-cyan font-bold min-w-[70px]">Emerg ID</th>
<th className="px-2 py-2 text-center text-neon-cyan font-bold min-w-[35px]">APRS RX</th>
<th className="px-2 py-2 text-center text-neon-cyan font-bold min-w-[35px]">APRS TX</th>
<th className="px-2 py-2 text-center text-neon-cyan font-bold min-w-[35px]">VOX</th>
<th className="px-2 py-2 text-left text-neon-cyan font-bold min-w-[50px]" title="Scan list assignment">Scan List</th>
<th className="px-2 py-2 text-center text-neon-cyan font-bold min-w-[35px]" title="Free to Air">FTA</th>
<th className="px-2 py-2 text-center text-neon-cyan font-bold min-w-[35px]" title="Emergency">Emerg</th>
<th className="px-2 py-2 text-center text-neon-cyan font-bold min-w-[35px]" title="Emergency acknowledge">Emerg Ack</th>
<th className="px-2 py-2 text-left text-neon-cyan font-bold min-w-[70px]" title="Emergency ID">Emerg ID</th>
<th className="px-2 py-2 text-center text-neon-cyan font-bold min-w-[35px]" title="APRS receive">APRS RX</th>
<th className="px-2 py-2 text-center text-neon-cyan font-bold min-w-[35px]" title="APRS transmit">APRS TX</th>
<th className="px-2 py-2 text-center text-neon-cyan font-bold min-w-[35px]" title="Voice operated transmit">VOX</th>
<th className="px-2 py-2 text-center text-neon-cyan font-bold min-w-[30px]" title="Scramble">SCR</th>
<th className="px-2 py-2 text-center text-neon-cyan font-bold min-w-[30px]" title="Compander">CMP</th>
<th className="px-2 py-2 text-center text-neon-cyan font-bold min-w-[30px]" title="Talkback">TB</th>
<th className="px-2 py-2 text-center text-neon-cyan font-bold min-w-[30px]" title="Compander Dup">CMP DUP</th>
<th className="px-2 py-2 text-left text-neon-cyan font-bold min-w-[60px]">SQL</th>
<th className="px-2 py-2 text-center text-neon-cyan font-bold min-w-[35px]">PTT ID Display</th>
<th className="px-2 py-2 text-left text-neon-cyan font-bold min-w-[60px]">PTT ID</th>
<th className="px-2 py-2 text-center text-neon-cyan font-bold min-w-[35px]">VOX Related</th>
<th className="px-2 py-2 text-left text-neon-cyan font-bold min-w-[100px]">RX Squelch Mode</th>
<th className="px-2 py-2 text-left text-neon-cyan font-bold min-w-[70px]">Step Freq</th>
<th className="px-2 py-2 text-left text-neon-cyan font-bold min-w-[65px]">Sig Type</th>
<th className="px-2 py-2 text-left text-neon-cyan font-bold min-w-[65px]">PTT ID Type</th>
<th className="px-2 py-2 text-left text-neon-cyan font-bold min-w-[60px]" title="Squelch">SQL</th>
<th className="px-2 py-2 text-center text-neon-cyan font-bold min-w-[35px]" title="PTT ID display">PTT ID Display</th>
<th className="px-2 py-2 text-left text-neon-cyan font-bold min-w-[60px]" title="PTT ID">PTT ID</th>
<th className="px-2 py-2 text-center text-neon-cyan font-bold min-w-[35px]" title="VOX related">VOX Related</th>
<th className="px-2 py-2 text-left text-neon-cyan font-bold min-w-[100px]" title="Receive squelch mode">RX Squelch Mode</th>
<th className="px-2 py-2 text-left text-neon-cyan font-bold min-w-[70px]" title="Step frequency">Step Freq</th>
<th className="px-2 py-2 text-left text-neon-cyan font-bold min-w-[65px]" title="Signal type">Sig Type</th>
<th className="px-2 py-2 text-left text-neon-cyan font-bold min-w-[65px]" title="PTT ID type">PTT ID Type</th>
{/* Digital-only fields */}
<th className="px-2 py-2 text-left text-neon-cyan font-bold min-w-[60px]">Color Code</th>
<th className="px-2 py-2 text-left text-neon-cyan font-bold min-w-[60px]" title="DMR color code">Color Code</th>
<th className="px-2 py-2 text-left text-neon-cyan font-bold min-w-[80px]" title="RX Group List">RX Group</th>
<th className="px-2 py-2 text-left text-neon-cyan font-bold min-w-[60px]" title="Slot Operation">Slot</th>
<th className="px-2 py-2 text-center text-neon-cyan font-bold min-w-[35px]" title="Encryption">Enc</th>
Expand Down Expand Up @@ -362,12 +364,42 @@ export const ChannelsTable: React.FC<ChannelsTableProps> = ({
className="bg-transparent border border-neon-cyan border-opacity-30 rounded px-2 py-1 text-white focus:outline-none focus:border-neon-cyan focus:shadow-glow-cyan w-full text-xs"
/>
</td>
<td className="px-1 py-2 align-middle">
<button
type="button"
onClick={(e) => {
e.stopPropagation();
if (!(isRxInNoTxBand(channel.rxFrequency) && isNoTxFrequency(channel.txFrequency))) {
handleCellChange(channel.number, 'txFrequency', channel.rxFrequency);
}
}}
disabled={isRxInNoTxBand(channel.rxFrequency) && isNoTxFrequency(channel.txFrequency)}
className="p-1 rounded border border-neon-cyan border-opacity-30 text-xs font-bold disabled:opacity-40 disabled:text-cool-gray disabled:border-opacity-20 disabled:cursor-not-allowed text-neon-cyan hover:bg-neon-cyan hover:bg-opacity-10 disabled:hover:bg-transparent"
title={isRxInNoTxBand(channel.rxFrequency) && isNoTxFrequency(channel.txFrequency) ? 'Receive-only (no TX)' : 'Copy RX to TX'}
aria-label="Copy RX to TX"
>
</button>
</td>
<td className="px-2 py-2">
<FrequencyInput
value={channel.txFrequency}
onChange={(val) => handleCellChange(channel.number, 'txFrequency', val)}
className="bg-transparent border border-neon-cyan border-opacity-30 rounded px-2 py-1 text-white focus:outline-none focus:border-neon-cyan focus:shadow-glow-cyan w-full text-xs"
/>
{isRxInNoTxBand(channel.rxFrequency) && isNoTxFrequency(channel.txFrequency) ? (
<input
type="text"
readOnly
disabled
value=""
placeholder=""
title="Receive-only (no TX)"
aria-label="No transmit"
className="w-full text-xs rounded px-2 py-1 bg-deep-gray border border-neon-cyan border-opacity-20 text-cool-gray opacity-60 cursor-not-allowed"
/>
) : (
<FrequencyInput
value={channel.txFrequency}
onChange={(val) => handleCellChange(channel.number, 'txFrequency', val)}
className="bg-transparent border border-neon-cyan border-opacity-30 rounded px-2 py-1 text-white focus:outline-none focus:border-neon-cyan focus:shadow-glow-cyan w-full text-xs"
/>
)}
</td>
<td className="px-2 py-2 text-center">
<button
Expand Down Expand Up @@ -424,7 +456,11 @@ export const ChannelsTable: React.FC<ChannelsTableProps> = ({
<input
type="checkbox"
checked={channel.forbidTx}
onChange={(e) => handleCellChange(channel.number, 'forbidTx', e.target.checked)}
onChange={(e) => {
const next = e.target.checked;
if (!next && isRxInNoTxBand(channel.rxFrequency) && isNoTxFrequency(channel.txFrequency)) return;
handleCellChange(channel.number, 'forbidTx', next);
}}
className="checkbox-theme"
/>
</td>
Expand Down
Loading