@mytec: 2nd iteration implemented for tests

This commit is contained in:
2026-01-30 08:39:49 +02:00
parent e59eb59525
commit d7f1204e35
6 changed files with 342 additions and 50 deletions

View File

@@ -0,0 +1,96 @@
import { useState } from 'react';
import { useSitesStore } from '@/store/sites.ts';
import { useToastStore } from '@/components/ui/Toast.tsx';
import Button from '@/components/ui/Button.tsx';
export default function BatchEdit() {
const selectedSiteIds = useSitesStore((s) => s.selectedSiteIds);
const batchUpdateHeight = useSitesStore((s) => s.batchUpdateHeight);
const batchSetHeight = useSitesStore((s) => s.batchSetHeight);
const clearSelection = useSitesStore((s) => s.clearSelection);
const addToast = useToastStore((s) => s.addToast);
const [customHeight, setCustomHeight] = useState('');
if (selectedSiteIds.length === 0) return null;
const handleAdjustHeight = async (delta: number) => {
await batchUpdateHeight(delta);
addToast(
`Adjusted ${selectedSiteIds.length} site(s) 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;
}
await batchSetHeight(height);
addToast(`Set ${selectedSiteIds.length} site(s) to ${height}m`, 'success');
setCustomHeight('');
};
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>
{/* Quick height adjustments */}
<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>
{/* Set exact height */}
<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="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"
/>
<Button
size="sm"
onClick={handleSetHeight}
disabled={!customHeight}
>
Apply
</Button>
</div>
</div>
</div>
);
}

View File

@@ -2,6 +2,7 @@ import type { Site } from '@/types/index.ts';
import { useSitesStore } from '@/store/sites.ts';
import { useToastStore } from '@/components/ui/Toast.tsx';
import Button from '@/components/ui/Button.tsx';
import BatchEdit from './BatchEdit.tsx';
interface SiteListProps {
onEditSite: (site: Site) => void;
@@ -15,6 +16,10 @@ export default function SiteList({ onEditSite, onAddSite }: SiteListProps) {
const selectedSiteId = useSitesStore((s) => s.selectedSiteId);
const isPlacingMode = useSitesStore((s) => s.isPlacingMode);
const togglePlacingMode = useSitesStore((s) => s.togglePlacingMode);
const selectedSiteIds = useSitesStore((s) => s.selectedSiteIds);
const toggleSiteSelection = useSitesStore((s) => s.toggleSiteSelection);
const selectAllSites = useSitesStore((s) => s.selectAllSites);
const clearSelection = useSitesStore((s) => s.clearSelection);
const addToast = useToastStore((s) => s.addToast);
const handleDelete = async (id: string, name: string) => {
@@ -22,8 +27,11 @@ export default function SiteList({ onEditSite, onAddSite }: SiteListProps) {
addToast(`"${name}" deleted`, 'info');
};
const allSelected = sites.length > 0 && selectedSiteIds.length === sites.length;
return (
<div className="bg-white dark:bg-dark-surface border border-gray-200 dark:border-dark-border rounded-lg shadow-sm">
{/* Header */}
<div className="px-4 py-3 border-b border-gray-200 dark:border-dark-border flex items-center justify-between">
<h2 className="text-sm font-semibold text-gray-800 dark:text-dark-text">
Sites ({sites.length})
@@ -42,63 +50,105 @@ export default function SiteList({ onEditSite, onAddSite }: SiteListProps) {
</div>
</div>
{/* Select All row (only when sites exist) */}
{sites.length > 0 && (
<div className="px-4 py-1.5 border-b border-gray-100 dark:border-dark-border flex items-center justify-between">
<label className="flex items-center gap-2 cursor-pointer text-xs text-gray-500 dark:text-dark-muted">
<input
type="checkbox"
checked={allSelected}
onChange={() => (allSelected ? clearSelection() : selectAllSites())}
className="w-3.5 h-3.5 rounded border-gray-300 dark:border-dark-border accent-blue-600"
/>
{allSelected ? 'Deselect All' : 'Select All'}
</label>
{selectedSiteIds.length > 0 && (
<span className="text-xs text-blue-600 dark:text-blue-400">
{selectedSiteIds.length} selected
</span>
)}
</div>
)}
{/* Batch edit panel (appears when sites are selected) */}
{selectedSiteIds.length > 0 && (
<div className="px-3 pt-3">
<BatchEdit />
</div>
)}
{/* Sites list */}
{sites.length === 0 ? (
<div className="p-4 text-center text-sm text-gray-400 dark:text-dark-muted">
No sites yet. Click on the map or use "+ Manual" to add one.
</div>
) : (
<div className="divide-y divide-gray-100 dark:divide-dark-border max-h-60 overflow-y-auto">
{sites.map((site) => (
<div
key={site.id}
className={`px-4 py-2.5 flex items-center gap-3 cursor-pointer
hover:bg-gray-50 dark:hover:bg-dark-border/50 transition-colors
${selectedSiteId === site.id ? 'bg-blue-50 dark:bg-blue-900/20' : ''}`}
onClick={() => selectSite(site.id)}
>
{/* Color indicator */}
{sites.map((site) => {
const isBatchSelected = selectedSiteIds.includes(site.id);
return (
<div
className="w-4 h-4 rounded-full flex-shrink-0 border-2 border-white dark:border-dark-border shadow"
style={{ backgroundColor: site.color }}
/>
key={site.id}
className={`px-4 py-2.5 flex items-center gap-2.5 cursor-pointer
hover:bg-gray-50 dark:hover:bg-dark-border/50 transition-colors
${selectedSiteId === site.id ? 'bg-blue-50 dark:bg-blue-900/20' : ''}
${isBatchSelected && selectedSiteId !== site.id ? 'bg-blue-50/50 dark:bg-blue-900/10' : ''}`}
onClick={() => selectSite(site.id)}
>
{/* Batch checkbox */}
<input
type="checkbox"
checked={isBatchSelected}
onChange={() => toggleSiteSelection(site.id)}
onClick={(e) => e.stopPropagation()}
className="w-3.5 h-3.5 rounded border-gray-300 dark:border-dark-border accent-blue-600 flex-shrink-0"
/>
{/* Info */}
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-gray-800 dark:text-dark-text truncate">
{site.name}
{/* Color indicator */}
<div
className="w-4 h-4 rounded-full flex-shrink-0 border-2 border-white dark:border-dark-border shadow"
style={{ backgroundColor: site.color }}
/>
{/* Info */}
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-gray-800 dark:text-dark-text truncate">
{site.name}
</div>
<div className="text-xs text-gray-500 dark:text-dark-muted">
{site.frequency} MHz · {site.power} dBm ·{' '}
{site.antennaType === 'omni'
? 'Omni'
: `Sector ${site.azimuth ?? 0}°`}
{' '}· {site.height}m
</div>
</div>
<div className="text-xs text-gray-500 dark:text-dark-muted">
{site.frequency} MHz · {site.power} dBm ·{' '}
{site.antennaType === 'omni'
? 'Omni'
: `Sector ${site.azimuth ?? 0}°`}
{' '}· {site.height}m
{/* Actions */}
<div className="flex gap-1 flex-shrink-0">
<button
onClick={(e) => {
e.stopPropagation();
onEditSite(site);
}}
className="px-2 py-1 text-xs text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded min-w-[44px] min-h-[32px] flex items-center justify-center"
>
Edit
</button>
<button
onClick={(e) => {
e.stopPropagation();
handleDelete(site.id, site.name);
}}
className="px-2 py-1 text-xs text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 rounded min-w-[32px] min-h-[32px] flex items-center justify-center"
>
×
</button>
</div>
</div>
{/* Actions */}
<div className="flex gap-1 flex-shrink-0">
<button
onClick={(e) => {
e.stopPropagation();
onEditSite(site);
}}
className="px-2 py-1 text-xs text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20 rounded min-w-[44px] min-h-[32px] flex items-center justify-center"
>
Edit
</button>
<button
onClick={(e) => {
e.stopPropagation();
handleDelete(site.id, site.name);
}}
className="px-2 py-1 text-xs text-red-600 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 rounded min-w-[32px] min-h-[32px] flex items-center justify-center"
>
×
</button>
</div>
</div>
))}
);
})}
</div>
)}
</div>