@mytec: iter10.6 ready for testing
This commit is contained in:
@@ -7,24 +7,39 @@ 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);
|
||||
onBatchApplied?.(ids);
|
||||
notifyBatch(ids);
|
||||
addToast(
|
||||
`Updated ${ids.length} site(s) height by ${delta > 0 ? '+' : ''}${delta}m`,
|
||||
'success'
|
||||
@@ -39,17 +54,18 @@ export default function BatchEdit({ onBatchApplied }: BatchEditProps) {
|
||||
}
|
||||
const ids = [...selectedSiteIds];
|
||||
await batchSetHeight(height);
|
||||
onBatchApplied?.(ids);
|
||||
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);
|
||||
onBatchApplied?.(ids);
|
||||
notifyBatch(ids);
|
||||
addToast(
|
||||
`Rotated ${ids.length} site(s) by ${delta > 0 ? '+' : ''}${delta}°`,
|
||||
`Rotated ${ids.length} site(s) by ${delta > 0 ? '+' : ''}${delta}\u00B0`,
|
||||
'success'
|
||||
);
|
||||
};
|
||||
@@ -57,16 +73,88 @@ export default function BatchEdit({ onBatchApplied }: BatchEditProps) {
|
||||
const handleSetAzimuth = async () => {
|
||||
const az = parseInt(customAzimuth, 10);
|
||||
if (isNaN(az) || az < 0 || az > 359) {
|
||||
addToast('Azimuth must be between 0-359°', 'error');
|
||||
addToast('Azimuth must be between 0-359\u00B0', 'error');
|
||||
return;
|
||||
}
|
||||
const ids = [...selectedSiteIds];
|
||||
await batchSetAzimuth(az);
|
||||
onBatchApplied?.(ids);
|
||||
addToast(`Set ${ids.length} site(s) azimuth to ${az}°`, 'success');
|
||||
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">
|
||||
@@ -78,7 +166,7 @@ export default function BatchEdit({ onBatchApplied }: BatchEditProps) {
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Quick height adjustments */}
|
||||
{/* ── Height ── */}
|
||||
<div>
|
||||
<label className="text-xs font-medium text-gray-700 dark:text-gray-300 mb-1.5 block">
|
||||
Adjust Height:
|
||||
@@ -99,7 +187,6 @@ export default function BatchEdit({ onBatchApplied }: BatchEditProps) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Set exact height */}
|
||||
<div>
|
||||
<label className="text-xs font-medium text-gray-700 dark:text-gray-300 mb-1.5 block">
|
||||
Set Height:
|
||||
@@ -113,47 +200,43 @@ export default function BatchEdit({ onBatchApplied }: BatchEditProps) {
|
||||
onChange={(e) => setCustomHeight(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSetHeight()}
|
||||
placeholder="meters"
|
||||
className="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"
|
||||
className={inputClass}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleSetHeight}
|
||||
disabled={!customHeight}
|
||||
>
|
||||
<Button size="sm" onClick={handleSetHeight} disabled={!customHeight}>
|
||||
Apply
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Adjust azimuth */}
|
||||
<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°
|
||||
-90{'\u00B0'}
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" onClick={() => handleAdjustAzimuth(-45)}>
|
||||
-45°
|
||||
-45{'\u00B0'}
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" onClick={() => handleAdjustAzimuth(-10)}>
|
||||
-10°
|
||||
-10{'\u00B0'}
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" onClick={() => handleAdjustAzimuth(10)}>
|
||||
+10°
|
||||
+10{'\u00B0'}
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" onClick={() => handleAdjustAzimuth(45)}>
|
||||
+45°
|
||||
+45{'\u00B0'}
|
||||
</Button>
|
||||
<Button size="sm" variant="secondary" onClick={() => handleAdjustAzimuth(90)}>
|
||||
+90°
|
||||
+90{'\u00B0'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Set exact azimuth */}
|
||||
<div>
|
||||
<label className="text-xs font-medium text-gray-700 dark:text-gray-300 mb-1.5 block">
|
||||
Set Azimuth:
|
||||
@@ -166,9 +249,8 @@ export default function BatchEdit({ onBatchApplied }: BatchEditProps) {
|
||||
value={customAzimuth}
|
||||
onChange={(e) => setCustomAzimuth(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSetAzimuth()}
|
||||
placeholder="0-359°"
|
||||
className="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"
|
||||
placeholder="0-359\u00B0"
|
||||
className={inputClass}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
@@ -176,18 +258,157 @@ export default function BatchEdit({ onBatchApplied }: BatchEditProps) {
|
||||
onClick={() => {
|
||||
const ids = [...selectedSiteIds];
|
||||
batchSetAzimuth(0).then(() => {
|
||||
onBatchApplied?.(ids);
|
||||
addToast(`Set ${ids.length} site(s) to North (0°)`, 'success');
|
||||
notifyBatch(ids);
|
||||
addToast(`Set ${ids.length} site(s) to North (0\u00B0)`, 'success');
|
||||
});
|
||||
}}
|
||||
>
|
||||
N 0°
|
||||
N 0{'\u00B0'}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleSetAzimuth}
|
||||
disabled={!customAzimuth}
|
||||
>
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user