# RFCP Iteration 10.5 β€” Input Validation + Site/Sector Hierarchy UI **Date:** 2025-01-30 **Status:** Ready for Implementation **Priority:** Medium **Estimated Effort:** 45-60 minutes --- ## Overview This iteration addresses two UX issues: 1. **Bug:** Number input fields don't accept manually typed values 2. **Enhancement:** Site/Sector list needs clear visual hierarchy --- ## Part A: Input Validation Fix ### Problem When user clears an input field and tries to type a new value (e.g., "15" or "120"), the input doesn't accept it. The field either: - Resets to minimum value immediately - Doesn't allow intermediate states (typing "1" before "15") **Affected Fields:** - Radius (1-100 km) - Resolution (50-500 m) - Min Signal (-140 to -50 dBm) - Heatmap Opacity (30-100%) - All numeric inputs in site/sector forms ### Root Cause The `onChange` handler immediately validates and clamps values to min/max range. When user types "1" (intending to type "15"), it gets clamped or rejected before they can finish typing. ### Solution **Validate on blur, not on change:** ```typescript // BEFORE (problematic): const handleChange = (e: React.ChangeEvent) => { const value = Math.max(min, Math.min(max, Number(e.target.value))); setValue(value); }; // AFTER (correct): const [localValue, setLocalValue] = useState(String(value)); const handleChange = (e: React.ChangeEvent) => { // Allow any input while typing setLocalValue(e.target.value); }; const handleBlur = () => { // Validate and clamp only when focus leaves const parsed = Number(localValue); if (isNaN(parsed)) { setLocalValue(String(value)); // Reset to previous valid value } else { const clamped = Math.max(min, Math.min(max, parsed)); setLocalValue(String(clamped)); setValue(clamped); } }; // Sync local state when prop changes useEffect(() => { setLocalValue(String(value)); }, [value]); ``` ### Files to Check/Modify Look for numeric input components in: - `src/components/panels/SettingsPanel.tsx` (or similar) - `src/components/ui/NumberInput.tsx` (if exists) - `src/components/ui/Slider.tsx` (if has input field) - `src/components/panels/SiteForm.tsx` ### Implementation Pattern If there's a reusable `NumberInput` component, fix it once. Otherwise, apply the pattern to each input. **Key Points:** 1. Use local string state for the input value 2. Allow free typing (no validation on change) 3. Validate and clamp on blur 4. Also validate on Enter key press 5. Sync local state when external value changes ```typescript interface NumberInputProps { value: number; onChange: (value: number) => void; min: number; max: number; step?: number; label?: string; } function NumberInput({ value, onChange, min, max, step = 1, label }: NumberInputProps) { const [localValue, setLocalValue] = useState(String(value)); // Sync when external value changes useEffect(() => { setLocalValue(String(value)); }, [value]); const handleChange = (e: React.ChangeEvent) => { setLocalValue(e.target.value); }; const commitValue = () => { const parsed = parseFloat(localValue); if (isNaN(parsed)) { setLocalValue(String(value)); return; } const clamped = Math.max(min, Math.min(max, parsed)); setLocalValue(String(clamped)); if (clamped !== value) { onChange(clamped); } }; const handleBlur = () => commitValue(); const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { commitValue(); (e.target as HTMLInputElement).blur(); } if (e.key === 'Escape') { setLocalValue(String(value)); (e.target as HTMLInputElement).blur(); } }; return ( ); } ``` --- ## Part B: Site/Sector Hierarchy UI ### Problem Current UI shows flat list with confusing structure: ``` Station-1 2 sectors Station-1-Beta Edit + Sector Γ— Station-1-Beta Edit + Sector Γ— ``` Issues: - Sector names duplicate site name ("Station-1-Beta") - No clear visual hierarchy - "+ Sector" button on each sector (should be on site only) - Can't easily distinguish site from sector ### Desired UI ``` πŸ“ Station-1 [Edit Site] [+ Sector] [Γ—] β”œβ”€ Ξ± Alpha 140Β° Β· 2100MHz Β· 43dBm [Edit] [Γ—] └─ Ξ² Beta 260Β° Β· 2100MHz Β· 43dBm [Edit] [Γ—] πŸ“ Station-2 [Edit Site] [+ Sector] [Γ—] └─ Ξ± Alpha 90Β° Β· 1800MHz Β· 40dBm [Edit] [Γ—] ``` ### Design Specifications **Site Row:** - Icon: πŸ“ or radio tower icon - Name: Bold, larger font - Sector count badge: "2 sectors" (subtle) - Actions: [Edit Site] [+ Sector] [Delete Site] - Background: Slightly different shade - Click to expand/collapse sectors **Sector Row:** - Indented (padding-left: 24px or similar) - Greek letter prefix: Ξ±, Ξ², Ξ³, Ξ΄, Ξ΅, ΞΆ (visual identifier) - Sector name: "Alpha", "Beta" (without site prefix) - Key params inline: Azimuth Β· Frequency Β· Power - Actions: [Edit] [Delete] - Tree line connector: β”œβ”€ and └─ (optional, CSS pseudo-elements) **Greek Letters Mapping:** ```typescript const GREEK_LETTERS = ['Ξ±', 'Ξ²', 'Ξ³', 'Ξ΄', 'Ξ΅', 'ΞΆ', 'Ξ·', 'ΞΈ']; // Alpha=Ξ±, Beta=Ξ², Gamma=Ξ³, Delta=Ξ΄, Epsilon=Ξ΅, Zeta=ΞΆ, Eta=Ξ·, Theta=ΞΈ ``` ### Component Structure ``` SiteList β”œβ”€β”€ SiteItem (for each site) β”‚ β”œβ”€β”€ SiteHeader (name, actions, expand/collapse) β”‚ └── SectorList (when expanded) β”‚ └── SectorItem (for each sector) β”‚ β”œβ”€β”€ SectorInfo (greek letter, name, params) β”‚ └── SectorActions (edit, delete) ``` ### File to Modify `/opt/rfcp/frontend/src/components/panels/SiteList.tsx` Or if there are sub-components: - `SiteItem.tsx` - `SectorItem.tsx` ### Implementation Notes **1. Data Structure (existing):** ```typescript interface Site { id: string; name: string; lat: number; lon: number; sectors: Sector[]; } interface Sector { id: string; name: string; // "Alpha", "Beta", etc. azimuth: number; frequency: number; power: number; // ... other props } ``` **2. Greek Letter Helper:** ```typescript const SECTOR_GREEK: Record = { 'Alpha': 'Ξ±', 'Beta': 'Ξ²', 'Gamma': 'Ξ³', 'Delta': 'Ξ΄', 'Epsilon': 'Ξ΅', 'Zeta': 'ΞΆ', 'Eta': 'Ξ·', 'Theta': 'ΞΈ', }; const getGreekLetter = (sectorName: string): string => { return SECTOR_GREEK[sectorName] || 'β€’'; }; ``` **3. CSS for Hierarchy:** ```css .site-item { border-left: 3px solid transparent; } .site-item.selected { border-left-color: #7c3aed; background: rgba(124, 58, 237, 0.1); } .site-header { display: flex; align-items: center; padding: 8px 12px; font-weight: 600; } .sector-list { padding-left: 16px; border-left: 1px solid rgba(255, 255, 255, 0.1); margin-left: 12px; } .sector-item { display: flex; align-items: center; padding: 6px 12px; font-size: 0.9em; } .sector-greek { width: 20px; color: #7c3aed; font-weight: 600; } .sector-params { color: rgba(255, 255, 255, 0.6); font-size: 0.85em; margin-left: 8px; } ``` **4. Expand/Collapse (optional):** ```typescript const [expanded, setExpanded] = useState>(new Set()); const toggleExpand = (siteId: string) => { setExpanded(prev => { const next = new Set(prev); if (next.has(siteId)) { next.delete(siteId); } else { next.add(siteId); } return next; }); }; ``` --- ## Testing Checklist ### Part A: Input Validation - [ ] Can type "15" in Radius field (not rejected after "1") - [ ] Can type "120" in Resolution field - [ ] Value validates on blur (focus out) - [ ] Value validates on Enter key - [ ] Escape key reverts to previous value - [ ] Invalid input (letters) reverts to previous value - [ ] Values outside range are clamped on blur - [ ] Slider still works with arrow buttons - [ ] Mobile: numeric keyboard appears ### Part B: Hierarchy UI - [ ] Sites show with icon and bold name - [ ] Sectors indented under their site - [ ] Greek letters (Ξ±, Ξ², Ξ³) show for sectors - [ ] Sector names without site prefix ("Alpha" not "Station-1-Alpha") - [ ] Key params visible inline (azimuth, frequency, power) - [ ] "+ Sector" button only on site row - [ ] Edit/Delete buttons work correctly - [ ] Selected site/sector highlighted - [ ] Multiple sites display correctly - [ ] Empty state (no sites) handled --- ## Visual Reference **Before:** ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Station-1 2 sectors β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ Station-1-Beta Edit +Sector Γ— β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ β”‚ β”‚ Station-1-Beta Edit +Sector Γ— β”‚ β”‚ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` **After:** ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ πŸ“ Station-1 [Edit] [+] [Γ—] β”‚ β”‚ β”‚ β”‚ β”‚ β”œβ”€ Ξ± Alpha 140Β° Β· 2100MHz Β· 43dBm [✎][Γ—] β”‚ β”‚ └─ Ξ² Beta 260Β° Β· 2100MHz Β· 43dBm [✎][Γ—] β”‚ β”‚ β”‚ β”‚ πŸ“ Station-2 [Edit] [+] [Γ—] β”‚ β”‚ β”‚ β”‚ β”‚ └─ Ξ± Alpha 90Β° Β· 1800MHz Β· 40dBm [✎][Γ—] β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` --- ## Reference - Previous iteration: RFCP-Iteration10.4-Fix-Stadia-Maps-401.md - Site/Sector data model: `/opt/rfcp/frontend/src/types/` - Current SiteList: `/opt/rfcp/frontend/src/components/panels/SiteList.tsx`