157 lines
6.4 KiB
TypeScript
157 lines
6.4 KiB
TypeScript
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;
|
||
onAddSite: () => void;
|
||
}
|
||
|
||
export default function SiteList({ onEditSite, onAddSite }: SiteListProps) {
|
||
const sites = useSitesStore((s) => s.sites);
|
||
const deleteSite = useSitesStore((s) => s.deleteSite);
|
||
const selectSite = useSitesStore((s) => s.selectSite);
|
||
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) => {
|
||
await deleteSite(id);
|
||
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})
|
||
</h2>
|
||
<div className="flex gap-2">
|
||
<Button
|
||
size="sm"
|
||
variant={isPlacingMode ? 'danger' : 'primary'}
|
||
onClick={togglePlacingMode}
|
||
>
|
||
{isPlacingMode ? 'Cancel' : '+ Place on Map'}
|
||
</Button>
|
||
<Button size="sm" variant="secondary" onClick={onAddSite}>
|
||
+ Manual
|
||
</Button>
|
||
</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) => {
|
||
const isBatchSelected = selectedSiteIds.includes(site.id);
|
||
|
||
return (
|
||
<div
|
||
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"
|
||
/>
|
||
|
||
{/* 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>
|
||
|
||
{/* 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>
|
||
);
|
||
}
|