Files
rfcp/frontend/src/components/panels/BatchEdit.tsx

419 lines
14 KiB
TypeScript

import { useState } from 'react';
import { useSitesStore } from '@/store/sites.ts';
import { useToastStore } from '@/components/ui/Toast.tsx';
import Button from '@/components/ui/Button.tsx';
interface BatchEditProps {
onBatchApplied?: (affectedIds: string[]) => void;
}
const QUICK_FREQS = [800, 1800, 1900, 2100, 2600];
export default function BatchEdit({ onBatchApplied }: BatchEditProps) {
const selectedSiteIds = useSitesStore((s) => s.selectedSiteIds);
const batchUpdateHeight = useSitesStore((s) => s.batchUpdateHeight);
const batchSetHeight = useSitesStore((s) => s.batchSetHeight);
const batchAdjustAzimuth = useSitesStore((s) => s.batchAdjustAzimuth);
const batchSetAzimuth = useSitesStore((s) => s.batchSetAzimuth);
const batchAdjustPower = useSitesStore((s) => s.batchAdjustPower);
const batchSetPower = useSitesStore((s) => s.batchSetPower);
const batchAdjustTilt = useSitesStore((s) => s.batchAdjustTilt);
const batchSetTilt = useSitesStore((s) => s.batchSetTilt);
const batchSetFrequency = useSitesStore((s) => s.batchSetFrequency);
const clearSelection = useSitesStore((s) => s.clearSelection);
const addToast = useToastStore((s) => s.addToast);
const [customHeight, setCustomHeight] = useState('');
const [customAzimuth, setCustomAzimuth] = useState('');
const [customPower, setCustomPower] = useState('');
const [customTilt, setCustomTilt] = useState('');
const [customFrequency, setCustomFrequency] = useState('');
if (selectedSiteIds.length === 0) return null;
const notifyBatch = (ids: string[]) => {
onBatchApplied?.(ids);
};
// ── Height ──
const handleAdjustHeight = async (delta: number) => {
const ids = [...selectedSiteIds];
await batchUpdateHeight(delta);
notifyBatch(ids);
addToast(
`Updated ${ids.length} site(s) height by ${delta > 0 ? '+' : ''}${delta}m`,
'success'
);
};
const handleSetHeight = async () => {
const height = parseInt(customHeight, 10);
if (isNaN(height) || height < 1 || height > 100) {
addToast('Height must be between 1-100m', 'error');
return;
}
const ids = [...selectedSiteIds];
await batchSetHeight(height);
notifyBatch(ids);
addToast(`Set ${ids.length} site(s) to ${height}m`, 'success');
setCustomHeight('');
};
// ── Azimuth ──
const handleAdjustAzimuth = async (delta: number) => {
const ids = [...selectedSiteIds];
await batchAdjustAzimuth(delta);
notifyBatch(ids);
addToast(
`Rotated ${ids.length} site(s) by ${delta > 0 ? '+' : ''}${delta}\u00B0`,
'success'
);
};
const handleSetAzimuth = async () => {
const az = parseInt(customAzimuth, 10);
if (isNaN(az) || az < 0 || az > 359) {
addToast('Azimuth must be between 0-359\u00B0', 'error');
return;
}
const ids = [...selectedSiteIds];
await batchSetAzimuth(az);
notifyBatch(ids);
addToast(`Set ${ids.length} site(s) azimuth to ${az}\u00B0`, 'success');
setCustomAzimuth('');
};
// ── Power ──
const handleAdjustPower = async (delta: number) => {
const ids = [...selectedSiteIds];
await batchAdjustPower(delta);
notifyBatch(ids);
addToast(
`Adjusted ${ids.length} site(s) power by ${delta > 0 ? '+' : ''}${delta} dB`,
'success'
);
};
const handleSetPower = async () => {
const power = parseInt(customPower, 10);
if (isNaN(power) || power < 10 || power > 50) {
addToast('Power must be between 10-50 dBm', 'error');
return;
}
const ids = [...selectedSiteIds];
await batchSetPower(power);
notifyBatch(ids);
addToast(`Set ${ids.length} site(s) power to ${power} dBm`, 'success');
setCustomPower('');
};
// ── Tilt ──
const handleAdjustTilt = async (delta: number) => {
const ids = [...selectedSiteIds];
await batchAdjustTilt(delta);
notifyBatch(ids);
addToast(
`Adjusted ${ids.length} site(s) tilt by ${delta > 0 ? '+' : ''}${delta}\u00B0`,
'success'
);
};
const handleSetTilt = async () => {
const tilt = parseInt(customTilt, 10);
if (isNaN(tilt) || tilt < -90 || tilt > 90) {
addToast('Tilt must be between -90\u00B0 and +90\u00B0', 'error');
return;
}
const ids = [...selectedSiteIds];
await batchSetTilt(tilt);
notifyBatch(ids);
addToast(`Set ${ids.length} site(s) tilt to ${tilt}\u00B0`, 'success');
setCustomTilt('');
};
// ── Frequency ──
const handleSetFrequencyQuick = async (freq: number) => {
const ids = [...selectedSiteIds];
await batchSetFrequency(freq);
notifyBatch(ids);
addToast(`Set ${ids.length} site(s) to ${freq} MHz`, 'success');
};
const handleSetFrequencyCustom = async () => {
const freq = parseInt(customFrequency, 10);
if (isNaN(freq) || freq < 100 || freq > 6000) {
addToast('Frequency must be between 100-6000 MHz', 'error');
return;
}
const ids = [...selectedSiteIds];
await batchSetFrequency(freq);
notifyBatch(ids);
addToast(`Set ${ids.length} site(s) to ${freq} MHz`, 'success');
setCustomFrequency('');
};
const inputClass =
'flex-1 px-3 py-1.5 border border-gray-300 dark:border-dark-border dark:bg-dark-bg dark:text-dark-text rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500';
return (
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-3 space-y-3">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-blue-900 dark:text-blue-200">
Batch Edit ({selectedSiteIds.length} selected)
</h3>
<Button onClick={clearSelection} size="sm" variant="ghost">
Clear
</Button>
</div>
{/* ── Height ── */}
<div>
<label className="text-xs font-medium text-gray-700 dark:text-gray-300 mb-1.5 block">
Adjust Height:
</label>
<div className="flex gap-1.5 flex-wrap">
<Button size="sm" variant="secondary" onClick={() => handleAdjustHeight(10)}>
+10m
</Button>
<Button size="sm" variant="secondary" onClick={() => handleAdjustHeight(5)}>
+5m
</Button>
<Button size="sm" variant="secondary" onClick={() => handleAdjustHeight(-5)}>
-5m
</Button>
<Button size="sm" variant="secondary" onClick={() => handleAdjustHeight(-10)}>
-10m
</Button>
</div>
</div>
<div>
<label className="text-xs font-medium text-gray-700 dark:text-gray-300 mb-1.5 block">
Set Height:
</label>
<div className="flex gap-2">
<input
type="number"
min={1}
max={100}
value={customHeight}
onChange={(e) => setCustomHeight(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSetHeight()}
placeholder="meters"
className={inputClass}
/>
<Button size="sm" onClick={handleSetHeight} disabled={!customHeight}>
Apply
</Button>
</div>
</div>
<div className="border-t border-blue-200 dark:border-blue-700" />
{/* ── Azimuth ── */}
<div>
<label className="text-xs font-medium text-gray-700 dark:text-gray-300 mb-1.5 block">
Adjust Azimuth:
</label>
<div className="flex gap-1 flex-wrap">
<Button size="sm" variant="secondary" onClick={() => handleAdjustAzimuth(-90)}>
-90{'\u00B0'}
</Button>
<Button size="sm" variant="secondary" onClick={() => handleAdjustAzimuth(-45)}>
-45{'\u00B0'}
</Button>
<Button size="sm" variant="secondary" onClick={() => handleAdjustAzimuth(-10)}>
-10{'\u00B0'}
</Button>
<Button size="sm" variant="secondary" onClick={() => handleAdjustAzimuth(10)}>
+10{'\u00B0'}
</Button>
<Button size="sm" variant="secondary" onClick={() => handleAdjustAzimuth(45)}>
+45{'\u00B0'}
</Button>
<Button size="sm" variant="secondary" onClick={() => handleAdjustAzimuth(90)}>
+90{'\u00B0'}
</Button>
</div>
</div>
<div>
<label className="text-xs font-medium text-gray-700 dark:text-gray-300 mb-1.5 block">
Set Azimuth:
</label>
<div className="flex gap-2">
<input
type="number"
min={0}
max={359}
value={customAzimuth}
onChange={(e) => setCustomAzimuth(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSetAzimuth()}
placeholder="0-359\u00B0"
className={inputClass}
/>
<Button
size="sm"
variant="secondary"
onClick={() => {
const ids = [...selectedSiteIds];
batchSetAzimuth(0).then(() => {
notifyBatch(ids);
addToast(`Set ${ids.length} site(s) to North (0\u00B0)`, 'success');
});
}}
>
N 0{'\u00B0'}
</Button>
<Button size="sm" onClick={handleSetAzimuth} disabled={!customAzimuth}>
Apply
</Button>
</div>
</div>
<div className="border-t border-blue-200 dark:border-blue-700" />
{/* ── Power ── */}
<div>
<label className="text-xs font-medium text-gray-700 dark:text-gray-300 mb-1.5 block">
Adjust Power:
</label>
<div className="flex gap-1 flex-wrap">
<Button size="sm" variant="secondary" onClick={() => handleAdjustPower(6)}>
+6dB
</Button>
<Button size="sm" variant="secondary" onClick={() => handleAdjustPower(3)}>
+3dB
</Button>
<Button size="sm" variant="secondary" onClick={() => handleAdjustPower(1)}>
+1dB
</Button>
<Button size="sm" variant="secondary" onClick={() => handleAdjustPower(-1)}>
-1dB
</Button>
<Button size="sm" variant="secondary" onClick={() => handleAdjustPower(-3)}>
-3dB
</Button>
<Button size="sm" variant="secondary" onClick={() => handleAdjustPower(-6)}>
-6dB
</Button>
</div>
</div>
<div>
<label className="text-xs font-medium text-gray-700 dark:text-gray-300 mb-1.5 block">
Set Power:
</label>
<div className="flex gap-2">
<input
type="number"
min={10}
max={50}
value={customPower}
onChange={(e) => setCustomPower(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSetPower()}
placeholder="dBm"
className={inputClass}
/>
<Button size="sm" onClick={handleSetPower} disabled={!customPower}>
Apply
</Button>
</div>
</div>
<div className="border-t border-blue-200 dark:border-blue-700" />
{/* ── Tilt ── */}
<div>
<label className="text-xs font-medium text-gray-700 dark:text-gray-300 mb-1.5 block">
Adjust Tilt:
</label>
<div className="flex gap-1 flex-wrap">
<Button size="sm" variant="secondary" onClick={() => handleAdjustTilt(10)}>
+10{'\u00B0'}
</Button>
<Button size="sm" variant="secondary" onClick={() => handleAdjustTilt(5)}>
+5{'\u00B0'}
</Button>
<Button size="sm" variant="secondary" onClick={() => handleAdjustTilt(2)}>
+2{'\u00B0'}
</Button>
<Button size="sm" variant="secondary" onClick={() => handleAdjustTilt(-2)}>
-2{'\u00B0'}
</Button>
<Button size="sm" variant="secondary" onClick={() => handleAdjustTilt(-5)}>
-5{'\u00B0'}
</Button>
<Button size="sm" variant="secondary" onClick={() => handleAdjustTilt(-10)}>
-10{'\u00B0'}
</Button>
</div>
</div>
<div>
<label className="text-xs font-medium text-gray-700 dark:text-gray-300 mb-1.5 block">
Set Tilt:
</label>
<div className="flex gap-2">
<input
type="number"
min={-90}
max={90}
value={customTilt}
onChange={(e) => setCustomTilt(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSetTilt()}
placeholder="degrees"
className={inputClass}
/>
<Button size="sm" onClick={handleSetTilt} disabled={!customTilt}>
Apply
</Button>
</div>
</div>
<div className="border-t border-blue-200 dark:border-blue-700" />
{/* ── Frequency ── */}
<div>
<label className="text-xs font-medium text-gray-700 dark:text-gray-300 mb-1.5 block">
Set Frequency:
</label>
<div className="flex gap-1.5 flex-wrap">
{QUICK_FREQS.map((freq) => (
<Button
key={freq}
size="sm"
variant="secondary"
onClick={() => handleSetFrequencyQuick(freq)}
>
{freq}
</Button>
))}
<span className="self-center text-xs text-gray-400 dark:text-dark-muted">MHz</span>
</div>
</div>
<div>
<label className="text-xs font-medium text-gray-700 dark:text-gray-300 mb-1.5 block">
Custom Frequency:
</label>
<div className="flex gap-2">
<input
type="number"
min={100}
max={6000}
value={customFrequency}
onChange={(e) => setCustomFrequency(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSetFrequencyCustom()}
placeholder="MHz"
className={inputClass}
/>
<Button size="sm" onClick={handleSetFrequencyCustom} disabled={!customFrequency}>
Apply
</Button>
</div>
</div>
</div>
);
}