jagd-apps/nachsuche/frontend/src/components/public/PublicUserList.js

296 lines
9.3 KiB
JavaScript
Raw Blame History

import React, { useState, useMemo, useEffect, useCallback } from 'react';
import { useConfigContext } from '../../contexts/ConfigContext';
import MapView from '../map/MapView';
import FilterPanel from '../users/FilterPanel';
import { calculateDistance } from '../../utils/helpers';
import { getGeocodeByPostalCode } from '../../services/users';
import './PublicUserList.css';
const PublicUserList = ({ users, loading, onRefetch }) => {
const { userTypeLabels } = useConfigContext();
// Load saved location from localStorage on mount
const savedLocation = localStorage.getItem('userLocation');
const initialCoords = savedLocation ? JSON.parse(savedLocation) : null;
const [showMap, setShowMap] = useState(true);
const [showFilters, setShowFilters] = useState(false);
const [locationStatus, setLocationStatus] = useState(initialCoords ? 'granted' : 'idle');
const [locationError, setLocationError] = useState('');
const [coords, setCoords] = useState(initialCoords);
const [radiusKm, setRadiusKm] = useState(100);
const [postalCode, setPostalCode] = useState('');
const [postalSearching, setPostalSearching] = useState(false);
const [filters, setFilters] = useState({
type: null,
sortBy: 'name'
});
// Save location to localStorage whenever it changes
useEffect(() => {
if (coords) {
localStorage.setItem('userLocation', JSON.stringify(coords));
}
}, [coords]);
const handleFilterChange = useCallback((key, value) => {
setFilters(prev => ({ ...prev, [key]: value }));
}, []);
// Re-fetch from backend when type filter changes (server-side filtering)
useEffect(() => {
if (onRefetch) {
onRefetch(filters.type ? { type: filters.type } : {});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [filters.type]);
const requestLocation = () => {
if (!navigator.geolocation) {
return;
}
setLocationStatus('requesting');
setLocationError('');
navigator.geolocation.getCurrentPosition(
(position) => {
setCoords({
lat: position.coords.latitude,
lng: position.coords.longitude
});
setLocationStatus('granted');
},
() => {
setLocationStatus('idle');
},
{
enableHighAccuracy: true,
timeout: 15000,
maximumAge: 0
}
);
};
const searchByPostalCode = async () => {
if (!postalCode || postalCode.trim().length < 4) {
setLocationError('Bitte geben Sie eine gültige Postleitzahl ein');
return;
}
setPostalSearching(true);
setLocationError('');
const result = await getGeocodeByPostalCode(postalCode.trim());
if (result.success) {
setCoords({ lat: result.data.lat, lng: result.data.lng });
setLocationStatus('granted');
setLocationError('');
} else {
setLocationError(result.message || 'Fehler bei der PLZ-Suche');
}
setPostalSearching(false);
};
const { list: filteredAndSortedUsers, missingGpsCount, allUsersWithGPS } = useMemo(() => {
let filtered = [...users];
const missingGps = filtered.filter(user => !(user.gps && user.gps.lat && user.gps.lng)).length;
// Alle Benutzer mit GPS für die Karte (vor Radius-Filterung)
const allWithGPS = filtered.filter(user => user.gps && user.gps.lat && user.gps.lng);
if (coords) {
const withDistance = filtered
.filter(user => user.gps && user.gps.lat && user.gps.lng)
.map(user => ({
...user,
distanceKm: calculateDistance(
coords.lat,
coords.lng,
user.gps.lat,
user.gps.lng
)
}))
.filter(user => user.distanceKm <= radiusKm)
.sort((a, b) => a.distanceKm - b.distanceKm);
return { list: withDistance, missingGpsCount: missingGps, allUsersWithGPS: allWithGPS };
}
// Sort by name/type when no location
filtered.sort((a, b) => {
switch (filters.sortBy) {
case 'nameDesc':
return b.name.localeCompare(a.name);
case 'type':
return a.type.localeCompare(b.type);
case 'name':
default:
return a.name.localeCompare(b.name);
}
});
return { list: filtered, missingGpsCount: missingGps, allUsersWithGPS: allWithGPS };
}, [users, filters, coords, radiusKm]);
if (loading) {
return <div className="loading">Lädt verfügbare Führer...</div>;
}
if (!users || users.length === 0) {
return (
<div className="no-users">
<p>Derzeit sind keine Nachsuchenführer verfügbar.</p>
</div>
);
}
return (
<div className="public-user-list">
<div className="list-header">
<h2>Verfügbare Nachsuchenführer</h2>
<button
className="toggle-map-button"
onClick={() => setShowMap(!showMap)}
>
{showMap ? 'Karte ausblenden' : 'Karte anzeigen'}
</button>
</div>
<div className="location-panel">
<div className="location-controls">
<button
className="location-button"
onClick={requestLocation}
disabled={locationStatus === 'requesting'}
>
{locationStatus === 'requesting' ? 'Wird geladen...' : coords ? 'Standort aktualisieren' : 'Standort freigeben'}
</button>
<div className="postal-search">
<input
type="text"
placeholder="oder PLZ eingeben"
value={postalCode}
onChange={(e) => setPostalCode(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && searchByPostalCode()}
disabled={postalSearching}
/>
<button
onClick={searchByPostalCode}
disabled={postalSearching || !postalCode}
>
{postalSearching ? 'Suche...' : 'Suchen'}
</button>
</div>
{coords && (
<span className="location-status success">
Standort aktiv
</span>
)}
{locationError && (
<span className="location-status error">
{locationError}
</span>
)}
</div>
<div className="radius-filter">
<label htmlFor="radiusKm">Radius (km)</label>
<div className="radius-inputs">
<input
type="range"
id="radiusKm"
min="1"
max="200"
value={radiusKm}
onChange={(e) => setRadiusKm(parseInt(e.target.value, 10))}
disabled={!coords}
/>
<input
type="number"
min="1"
max="200"
value={radiusKm}
onChange={(e) => setRadiusKm(parseInt(e.target.value || '1', 10))}
disabled={!coords}
/>
</div>
<p className="radius-hint">
Standort wird nur im Browser genutzt und nicht gespeichert.
</p>
</div>
{coords && missingGpsCount > 0 && (
<div className="gps-hint">
{missingGpsCount} verfügbare Führer ohne GPS-Daten werden nicht nach Entfernung angezeigt.
</div>
)}
</div>
<div className="filter-toggle">
<button
className="filter-toggle-button"
onClick={() => setShowFilters(!showFilters)}
>
{showFilters ? 'Filter ausblenden' : 'Filter anzeigen'}
</button>
</div>
{showFilters && (
<FilterPanel
filters={filters}
onFilterChange={handleFilterChange}
showAvailableFilter={false}
/>
)}
{showMap && (
<MapView
users={allUsersWithGPS}
center={[52.5, 9.5]}
zoom={8}
userLocation={coords}
radiusKm={radiusKm}
/>
)}
{filteredAndSortedUsers.length === 0 ? (
<div className="no-users">
<p>Keine Nachsuchenführer im gewählten Radius gefunden.</p>
</div>
) : (
<div className="user-grid">
{filteredAndSortedUsers.map(user => (
<div key={user._id} className="user-card">
{user.photo && (
<div className="user-card-photo">
<img src={user.photo} alt={user.name} className="user-card-photo-img" />
</div>
)}
<h3 className="user-name">{user.name}</h3>
<p className="user-role">Nachsuchenführer</p>
<div className="user-info">
<p className="user-address">📍 {user.address}</p>
<a href={`tel:${user.phone}`} className="user-phone">
<EFBFBD> {user.phone}
</a>
{user.landline && (
<a href={`tel:${user.landline}`} className="user-phone">
📞 {user.landline}
</a>
)}
<p className="user-type">
<span className="type-badge" title={userTypeLabels[user.type]}>{user.type}</span>
</p>
</div>
{user.distanceKm !== undefined && (
<div className="user-distance">
Entfernung: {user.distanceKm.toFixed(1)} km
</div>
)}
</div>
))}
</div>
)}
</div>
);
};
export default PublicUserList;