487 lines
17 KiB
Markdown
487 lines
17 KiB
Markdown
# 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:
|
|
|
|
1. **Part A:** Modal dialogs for Create/Edit site (instead of sidebar form)
|
|
2. **Part B:** Additional batch operations (Power, Tilt)
|
|
|
|
---
|
|
|
|
## Part A: Site Configuration Modal
|
|
|
|
### Problem
|
|
|
|
Current flow is confusing:
|
|
1. User clicks "Place on Map" → no visual feedback
|
|
2. User clicks on map → form appears in sidebar (easy to miss)
|
|
3. 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:**
|
|
|
|
```typescript
|
|
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:**
|
|
|
|
```typescript
|
|
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
|
|
|
|
```css
|
|
.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:**
|
|
|
|
```typescript
|
|
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.tsx` or wherever BatchEditPanel is
|
|
|
|
### Batch Operation Handler
|
|
|
|
```typescript
|
|
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.tsx`
|
|
- `src/components/modals/ModalBackdrop.tsx`
|
|
- `src/components/modals/index.ts`
|
|
|
|
**Modify:**
|
|
- `src/App.tsx` — integrate modal state
|
|
- `src/components/panels/SiteList.tsx` — trigger modal instead of sidebar form
|
|
- `src/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
|