17 KiB
17 KiB
RFCP Iteration 10.6 — Site Modal Dialogs + Batch Power/Tilt
Date: 2025-01-30
Status: Ready for Implementation
Priority: Medium-High
Estimated Effort: 60-90 minutes
Overview
Two UX improvements:
- Part A: Modal dialogs for Create/Edit site (instead of sidebar form)
- Part B: Additional batch operations (Power, Tilt)
Part A: Site Configuration Modal
Problem
Current flow is confusing:
- User clicks "Place on Map" → no visual feedback
- User clicks on map → form appears in sidebar (easy to miss)
- User doesn't understand they're creating a site
Same issue with Edit — form in sidebar is not obvious.
Solution
Center-screen modal dialog for:
- Creating new site (after clicking on map)
- Editing existing site
Modal Design
┌──────────────────────────────────────────────────────┐
│ ✕ │
│ 📍 New Site Configuration │
│ │
├──────────────────────────────────────────────────────┤
│ │
│ Site Name │
│ ┌────────────────────────────────────────────────┐ │
│ │ Station-1 │ │
│ └────────────────────────────────────────────────┘ │
│ │
│ Coordinates │
│ ┌───────────────────┐ ┌───────────────────┐ │
│ │ 48.52660975 │ │ 35.68222045 │ │
│ └───────────────────┘ └───────────────────┘ │
│ Latitude Longitude │
│ │
│ ─────────────── RF Parameters ─────────────── │
│ │
│ Transmit Power 43 dBm │
│ ┌────────────────────────────────────────────────┐ │
│ │ ●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━○ │ │
│ └────────────────────────────────────────────────┘ │
│ 10 dBm 50 dBm │
│ LimeSDR 20, BBU 43, RRU 46 │
│ │
│ Antenna Gain 8 dBi │
│ ┌────────────────────────────────────────────────┐ │
│ │ ○━━━━━━━●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ │ │
│ └────────────────────────────────────────────────┘ │
│ 0 dBi 25 dBi │
│ Omni 2-8, Sector 15-18, Parabolic 20-25 │
│ │
│ Operating Frequency │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │ 800 │ │1800 │ │1900 │ │2100 │ │2600 │ │
│ └─────┘ └─────┘ └─────┘ └─────┘ └─────┘ │
│ ▲ selected │
│ │
│ Custom MHz ┌──────────────┐ [Set] │
│ │ │ │
│ └──────────────┘ │
│ │
│ Current: 1800 MHz │
│ Band 3 (1710-1880 MHz) │
│ λ = 16.7 cm · medium range · good penetration │
│ │
├──────────────────────────────────────────────────────┤
│ │
│ [Cancel] [Create Site] │
│ │
└──────────────────────────────────────────────────────┘
Modal Behavior
Opening:
- "Place on Map" → click on map → Modal opens with coordinates pre-filled
- "Edit" button on site → Modal opens with all data pre-filled
- "+ Manual" button → Modal opens with empty/default values
Closing:
- Click ✕ → close without saving
- Click "Cancel" → close without saving
- Press Escape → close without saving
- Click outside modal (backdrop) → close without saving
- Click "Create Site" / "Save Changes" → validate, save, close
Validation:
- Site name: required, non-empty
- Coordinates: valid lat (-90 to 90), lon (-180 to 180)
- Power: 10-50 dBm
- Gain: 0-25 dBi
- Frequency: 100-6000 MHz
Component Structure
src/components/
├── modals/
│ ├── SiteConfigModal.tsx # Main modal component
│ ├── ModalBackdrop.tsx # Reusable backdrop
│ └── index.ts # Exports
Implementation
SiteConfigModal.tsx:
interface SiteConfigModalProps {
isOpen: boolean;
onClose: () => void;
onSave: (site: SiteData) => void;
initialData?: Partial<SiteData>; // For edit mode
mode: 'create' | 'edit';
}
function SiteConfigModal({ isOpen, onClose, onSave, initialData, mode }: SiteConfigModalProps) {
const [formData, setFormData] = useState<SiteFormData>({
name: initialData?.name || 'Station-1',
lat: initialData?.lat || 0,
lon: initialData?.lon || 0,
power: initialData?.power || 43,
gain: initialData?.gain || 8,
frequency: initialData?.frequency || 2100,
});
// ... form handling
if (!isOpen) return null;
return (
<ModalBackdrop onClose={onClose}>
<div className="modal-content">
<header>
<h2>{mode === 'create' ? '📍 New Site Configuration' : '✏️ Edit Site'}</h2>
<button onClick={onClose}>✕</button>
</header>
{/* Form fields */}
<footer>
<button onClick={onClose}>Cancel</button>
<button onClick={handleSave}>
{mode === 'create' ? 'Create Site' : 'Save Changes'}
</button>
</footer>
</div>
</ModalBackdrop>
);
}
ModalBackdrop.tsx:
interface ModalBackdropProps {
children: React.ReactNode;
onClose: () => void;
}
function ModalBackdrop({ children, onClose }: ModalBackdropProps) {
const handleBackdropClick = (e: React.MouseEvent) => {
if (e.target === e.currentTarget) {
onClose();
}
};
useEffect(() => {
const handleEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
document.addEventListener('keydown', handleEscape);
return () => document.removeEventListener('keydown', handleEscape);
}, [onClose]);
return (
<div
className="modal-backdrop"
onClick={handleBackdropClick}
>
{children}
</div>
);
}
CSS Styling
.modal-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
backdrop-filter: blur(4px);
}
.modal-content {
background: #1e293b; /* slate-800 */
border-radius: 12px;
width: 90%;
max-width: 480px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.modal-content header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.modal-content header h2 {
font-size: 1.25rem;
font-weight: 600;
color: white;
}
.modal-content footer {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 16px 20px;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
Integration with App
In App.tsx or SiteList.tsx:
const [modalState, setModalState] = useState<{
isOpen: boolean;
mode: 'create' | 'edit';
initialData?: Partial<SiteData>;
}>({ isOpen: false, mode: 'create' });
// When user clicks on map after "Place on Map"
const handleMapClick = (lat: number, lon: number) => {
if (isPlacingMode) {
setModalState({
isOpen: true,
mode: 'create',
initialData: { lat, lon },
});
setIsPlacingMode(false);
}
};
// When user clicks "Edit" on a site
const handleEditSite = (site: Site) => {
setModalState({
isOpen: true,
mode: 'edit',
initialData: site,
});
};
// Render modal
<SiteConfigModal
isOpen={modalState.isOpen}
mode={modalState.mode}
initialData={modalState.initialData}
onClose={() => setModalState({ ...modalState, isOpen: false })}
onSave={handleSaveSite}
/>
Part B: Batch Power/Tilt Operations
Current Batch Operations
Already implemented:
- ✅ Adjust Height: +10m, +5m, -5m, -10m
- ✅ Set Height: exact value
- ✅ Adjust Azimuth: -90°, -45°, -10°, +10°, +45°, +90°
- ✅ Set Azimuth: exact value
New Batch Operations to Add
1. Power Adjustment:
Adjust Power:
[+6dB] [+3dB] [+1dB] [-1dB] [-3dB] [-6dB]
Set Power:
┌─────────────────┐ [Apply]
│ dBm │
└─────────────────┘
2. Tilt Adjustment (for directional antennas):
Adjust Tilt:
[+10°] [+5°] [+2°] [-2°] [-5°] [-10°]
Set Tilt:
┌─────────────────┐ [Apply]
│ degrees │
└─────────────────┘
3. Frequency (Set only, no adjust):
Set Frequency:
[800] [1800] [1900] [2100] [2600] ← quick buttons
Custom: ┌─────────────────┐ [Apply]
│ MHz │
└─────────────────┘
Implementation Location
In the existing batch edit panel (visible when multiple sectors selected).
File to modify:
src/components/panels/SiteList.tsxor wherever BatchEditPanel is
Batch Operation Handler
const handleBatchAdjustPower = (delta: number) => {
selectedSectors.forEach(sector => {
const newPower = Math.max(10, Math.min(50, sector.power + delta));
updateSector(sector.id, { power: newPower });
});
};
const handleBatchSetPower = (value: number) => {
const clamped = Math.max(10, Math.min(50, value));
selectedSectors.forEach(sector => {
updateSector(sector.id, { power: clamped });
});
};
const handleBatchAdjustTilt = (delta: number) => {
selectedSectors.forEach(sector => {
const newTilt = Math.max(-90, Math.min(90, sector.tilt + delta));
updateSector(sector.id, { tilt: newTilt });
});
};
const handleBatchSetFrequency = (frequency: number) => {
selectedSectors.forEach(sector => {
updateSector(sector.id, { frequency });
});
};
UI Layout for Batch Panel
┌─────────────────────────────────────────┐
│ Batch Edit (3 selected) Clear │
├─────────────────────────────────────────┤
│ │
│ Adjust Height: │
│ [+10m] [+5m] [-5m] [-10m] │
│ │
│ Set Height: │
│ [________] meters [Apply] │
│ │
│ ─────────────────────────────────────── │
│ │
│ Adjust Azimuth: │
│ [-90°] [-45°] [-10°] [+10°] [+45°] [+90°]│
│ │
│ Set Azimuth: │
│ [________] 0-359° [N 0°] [Apply] │
│ │
│ ─────────────────────────────────────── │
│ │
│ Adjust Power: [NEW] │
│ [+6dB] [+3dB] [+1dB] [-1dB] [-3dB] [-6dB]│
│ │
│ Set Power: │
│ [________] dBm [Apply] │
│ │
│ ─────────────────────────────────────── │
│ │
│ Adjust Tilt: [NEW] │
│ [+10°] [+5°] [+2°] [-2°] [-5°] [-10°] │
│ │
│ Set Tilt: │
│ [________] degrees [Apply] │
│ │
│ ─────────────────────────────────────── │
│ │
│ Set Frequency: [NEW] │
│ [800] [1800] [1900] [2100] [2600] MHz │
│ │
│ Custom: [________] MHz [Apply] │
│ │
└─────────────────────────────────────────┘
Testing Checklist
Part A: Site Modal
- "Place on Map" → click map → modal opens
- Modal shows correct coordinates from click
- Can edit all fields in modal
- "Create Site" saves and closes modal
- Site appears on map after creation
- "Cancel" closes without saving
- Clicking backdrop closes without saving
- Escape key closes without saving
- "Edit" on existing site opens modal with data
- "Save Changes" updates site correctly
- Validation prevents invalid data
- Modal scrolls if content too tall
- Modal looks good on mobile
Part B: Batch Operations
- Select multiple sectors shows batch panel
- Adjust Power buttons work (+6, +3, +1, -1, -3, -6)
- Set Power applies exact value to all selected
- Adjust Tilt buttons work
- Set Tilt applies exact value
- Frequency quick buttons work (800, 1800, etc.)
- Custom frequency input works
- Values stay within valid ranges (clamped)
- Coverage updates after batch changes
Files to Modify/Create
New Files:
src/components/modals/SiteConfigModal.tsxsrc/components/modals/ModalBackdrop.tsxsrc/components/modals/index.ts
Modify:
src/App.tsx— integrate modal statesrc/components/panels/SiteList.tsx— trigger modal instead of sidebar formsrc/components/panels/BatchEditPanel.tsx(or equivalent) — add Power/Tilt/Frequency
Notes
- Modal should use same form components as current sidebar (reuse)
- Consider extracting form fields to shared component
- Batch operations should trigger coverage recalculation
- Test with both single-sector and multi-sector sites
Reference
- Previous iteration: RFCP-Iteration10.5-Input-Validation-Hierarchy-UI.md
- Current SiteForm: check existing implementation for field structure